From 29a46b0d1c45b1f612beaea8600ddb8af0fff1b8 Mon Sep 17 00:00:00 2001 From: Ravish Ahmad Date: Fri, 1 Nov 2024 19:46:48 +0530 Subject: [PATCH] DOCR-1201: Add new RegistriesService to support methods for multiple-registry open beta (#730) * docr: Update interface to add methods for multi-registry open beta * docr: Add TotalStorageUsageBytes field to registriesRoot struct * docr: Create new Registries service for multiple-registry endpoints * docr: Fix indentation --------- Co-authored-by: Anna Lushnikova Co-authored-by: Andrew Starr-Bochicchio --- godo.go | 2 + registry.go | 120 +++++++++++++++++++++++++++++++ registry_test.go | 181 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 303 insertions(+) diff --git a/godo.go b/godo.go index edf0f6d4..efa1bd61 100644 --- a/godo.go +++ b/godo.go @@ -81,6 +81,7 @@ type Client struct { Projects ProjectsService Regions RegionsService Registry RegistryService + Registries RegistriesService ReservedIPs ReservedIPsService ReservedIPActions ReservedIPActionsService Sizes SizesService @@ -292,6 +293,7 @@ func NewClient(httpClient *http.Client) *Client { c.Projects = &ProjectsServiceOp{client: c} c.Regions = &RegionsServiceOp{client: c} c.Registry = &RegistryServiceOp{client: c} + c.Registries = &RegistriesServiceOp{client: c} c.ReservedIPs = &ReservedIPsServiceOp{client: c} c.ReservedIPActions = &ReservedIPActionsServiceOp{client: c} c.Sizes = &SizesServiceOp{client: c} diff --git a/registry.go b/registry.go index b0c24328..e6482268 100644 --- a/registry.go +++ b/registry.go @@ -14,6 +14,9 @@ const ( registryPath = "/v2/registry" // RegistryServer is the hostname of the DigitalOcean registry service RegistryServer = "registry.digitalocean.com" + + // Multi-registry Open Beta API constants + registriesPath = "/v2/registries" ) // RegistryService is an interface for interfacing with the Registry endpoints @@ -240,6 +243,19 @@ type RegistryValidateNameRequest struct { Name string `json:"name"` } +// Multi-registry Open Beta API structs + +type registriesRoot struct { + Registries []*Registry `json:"registries,omitempty"` + TotalStorageUsageBytes uint64 `json:"total_storage_usage_bytes,omitempty"` +} + +// RegistriesCreateRequest represents a request to create a secondary registry. +type RegistriesCreateRequest struct { + Name string `json:"name,omitempty"` + Region string `json:"region,omitempty"` +} + // Get retrieves the details of a Registry. func (svc *RegistryServiceOp) Get(ctx context.Context) (*Registry, *Response, error) { req, err := svc.client.NewRequest(ctx, http.MethodGet, registryPath, nil) @@ -610,3 +626,107 @@ func (svc *RegistryServiceOp) ValidateName(ctx context.Context, request *Registr } return resp, nil } + +// RegistriesService is an interface for interfacing with the new multiple-registry beta endpoints +// of the DigitalOcean API. +// +// We are creating a separate Service in alignment with the new /v2/registries endpoints. +type RegistriesService interface { + Get(context.Context, string) (*Registry, *Response, error) + List(context.Context) ([]*Registry, *Response, error) + Create(context.Context, *RegistriesCreateRequest) (*Registry, *Response, error) + Delete(context.Context, string) (*Response, error) + DockerCredentials(context.Context, string, *RegistryDockerCredentialsRequest) (*DockerCredentials, *Response, error) +} + +var _ RegistriesService = &RegistriesServiceOp{} + +// RegistriesServiceOp handles communication with the multiple-registry beta methods. +type RegistriesServiceOp struct { + client *Client +} + +// Get returns the details of a named Registry. +func (svc *RegistriesServiceOp) Get(ctx context.Context, registry string) (*Registry, *Response, error) { + path := fmt.Sprintf("%s/%s", registriesPath, registry) + req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(registryRoot) + resp, err := svc.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root.Registry, resp, nil +} + +// List returns a list of the named Registries. +func (svc *RegistriesServiceOp) List(ctx context.Context) ([]*Registry, *Response, error) { + req, err := svc.client.NewRequest(ctx, http.MethodGet, registriesPath, nil) + if err != nil { + return nil, nil, err + } + root := new(registriesRoot) + resp, err := svc.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root.Registries, resp, nil +} + +// Create creates a named Registry. +func (svc *RegistriesServiceOp) Create(ctx context.Context, create *RegistriesCreateRequest) (*Registry, *Response, error) { + req, err := svc.client.NewRequest(ctx, http.MethodPost, registriesPath, create) + if err != nil { + return nil, nil, err + } + root := new(registryRoot) + resp, err := svc.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root.Registry, resp, nil +} + +// Delete deletes a named Registry. There is no way to recover a Registry once it has +// been destroyed. +func (svc *RegistriesServiceOp) Delete(ctx context.Context, registry string) (*Response, error) { + path := fmt.Sprintf("%s/%s", registriesPath, registry) + req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + resp, err := svc.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + return resp, nil +} + +// DockerCredentials retrieves a Docker config file containing named Registry's credentials. +func (svc *RegistriesServiceOp) DockerCredentials(ctx context.Context, registry string, request *RegistryDockerCredentialsRequest) (*DockerCredentials, *Response, error) { + path := fmt.Sprintf("%s/%s/%s", registriesPath, registry, "docker-credentials") + req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + q := req.URL.Query() + q.Add("read_write", strconv.FormatBool(request.ReadWrite)) + if request.ExpirySeconds != nil { + q.Add("expiry_seconds", strconv.Itoa(*request.ExpirySeconds)) + } + req.URL.RawQuery = q.Encode() + + var buf bytes.Buffer + resp, err := svc.client.Do(ctx, req, &buf) + if err != nil { + return nil, resp, err + } + + dc := &DockerCredentials{ + DockerConfigJSON: buf.Bytes(), + } + return dc, resp, nil +} diff --git a/registry_test.go b/registry_test.go index 7902a6c3..f7e0c93a 100644 --- a/registry_test.go +++ b/registry_test.go @@ -965,3 +965,184 @@ func TestRegistry_ValidateName(t *testing.T) { _, err := client.Registry.ValidateName(ctx, validateNameRequest) require.NoError(t, err) } + +// Tests for Registries service methods +func TestRegistries_Get(t *testing.T) { + setup() + defer teardown() + + want := &Registry{ + Name: testRegistry, + StorageUsageBytes: 0, + StorageUsageBytesUpdatedAt: testTime, + CreatedAt: testTime, + Region: testRegion, + } + + // We return `read_only` and `type` (only for multi-regsitry) -- check if we need to do this or not -- older tests don't add `read_only` to the response + getResponseJSON := ` +{ + "registry": { + "name": "` + testRegistry + `", + "storage_usage_bytes": 0, + "storage_usage_bytes_updated_at": "` + testTimeString + `", + "created_at": "` + testTimeString + `", + "region": "` + testRegion + `" + } +}` + + mux.HandleFunc(fmt.Sprintf("/v2/registries/%s", testRegistry), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint(w, getResponseJSON) + }) + got, _, err := client.Registries.Get(ctx, testRegistry) + require.NoError(t, err) + require.Equal(t, want, got) +} + +func TestRegistries_List(t *testing.T) { + setup() + defer teardown() + + wantRegistries := []*Registry{ + { + Name: testRegistry, + StorageUsageBytes: 0, + StorageUsageBytesUpdatedAt: testTime, + CreatedAt: testTime, + Region: testRegion, + }, + } + getResponseJSON := ` +{ + "registries": [ + { + "name": "` + testRegistry + `", + "storage_usage_bytes": 0, + "storage_usage_bytes_updated_at": "` + testTimeString + `", + "created_at": "` + testTimeString + `", + "region": "` + testRegion + `" + } + ] +}` + + mux.HandleFunc("/v2/registries", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Printf("Returning: %v", getResponseJSON) + fmt.Fprint(w, getResponseJSON) + }) + got, _, err := client.Registries.List(ctx) + require.NoError(t, err) + fmt.Printf("Expected: %+v\n", wantRegistries) + fmt.Printf("Got: %+v\n", got) + require.Equal(t, wantRegistries, got) +} + +func TestRegistries_Create(t *testing.T) { + setup() + defer teardown() + + want := &Registry{ + Name: testRegistry, + StorageUsageBytes: 0, + StorageUsageBytesUpdatedAt: testTime, + CreatedAt: testTime, + Region: testRegion, + } + + createRequest := &RegistriesCreateRequest{ + Name: want.Name, + Region: testRegion, + } + + createResponseJSON := ` +{ + "registry": { + "name": "` + testRegistry + `", + "storage_usage_bytes": 0, + "storage_usage_bytes_updated_at": "` + testTimeString + `", + "created_at": "` + testTimeString + `", + "region": "` + testRegion + `" + } +}` + + mux.HandleFunc("/v2/registries", func(w http.ResponseWriter, r *http.Request) { + v := new(RegistriesCreateRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatal(err) + } + + testMethod(t, r, http.MethodPost) + require.Equal(t, v, createRequest) + fmt.Fprint(w, createResponseJSON) + }) + + got, _, err := client.Registries.Create(ctx, createRequest) + require.NoError(t, err) + require.Equal(t, want, got) +} + +func TestRegistries_Delete(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc(fmt.Sprintf("/v2/registries/%s", testRegistry), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodDelete) + }) + + _, err := client.Registries.Delete(ctx, testRegistry) + require.NoError(t, err) +} + +func TestRegistries_DockerCredentials(t *testing.T) { + returnedConfig := "this could be a docker config" + tests := []struct { + name string + params *RegistryDockerCredentialsRequest + expectedReadWrite string + expectedExpirySeconds string + }{ + { + name: "read-only (default)", + params: &RegistryDockerCredentialsRequest{}, + expectedReadWrite: "false", + }, + { + name: "read/write", + params: &RegistryDockerCredentialsRequest{ReadWrite: true}, + expectedReadWrite: "true", + }, + { + name: "read-only + custom expiry", + params: &RegistryDockerCredentialsRequest{ExpirySeconds: PtrTo(60 * 60)}, + expectedReadWrite: "false", + expectedExpirySeconds: "3600", + }, + { + name: "read/write + custom expiry", + params: &RegistryDockerCredentialsRequest{ReadWrite: true, ExpirySeconds: PtrTo(60 * 60)}, + expectedReadWrite: "true", + expectedExpirySeconds: "3600", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc(fmt.Sprintf("/v2/registries/%s/docker-credentials", testRegistry), func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, test.expectedReadWrite, r.URL.Query().Get("read_write")) + require.Equal(t, test.expectedExpirySeconds, r.URL.Query().Get("expiry_seconds")) + testMethod(t, r, http.MethodGet) + fmt.Fprint(w, returnedConfig) + }) + + got, _, err := client.Registries.DockerCredentials(ctx, testRegistry, test.params) + fmt.Println(returnedConfig) + require.NoError(t, err) + require.Equal(t, []byte(returnedConfig), got.DockerConfigJSON) + }) + } +}