From 17531c1fed7ba2591cda3580d4ce85c697a1b7c4 Mon Sep 17 00:00:00 2001 From: Ravish Ahmad Date: Tue, 24 Sep 2024 16:08:48 +0530 Subject: [PATCH 1/4] docr: Update interface to add methods for multi-registry open beta --- registry.go | 109 ++++++++++++++++++++++++++++ registry_test.go | 181 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 290 insertions(+) diff --git a/registry.go b/registry.go index b0c24328..7f02585a 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 @@ -38,6 +41,13 @@ type RegistryService interface { GetSubscription(context.Context) (*RegistrySubscription, *Response, error) UpdateSubscription(context.Context, *RegistrySubscriptionUpdateRequest) (*RegistrySubscription, *Response, error) ValidateName(context.Context, *RegistryValidateNameRequest) (*Response, error) + + // Multi-registry Open Beta API methods + GetBeta(context.Context, string) (*Registry, *Response, error) + ListBeta(context.Context) ([]*Registry, *Response, error) + CreateBeta(context.Context, *RegistriesCreateRequest) (*Registry, *Response, error) + DeleteBeta(context.Context, string) (*Response, error) + DockerCredentialsBeta(context.Context, string, *RegistryDockerCredentialsRequest) (*DockerCredentials, *Response, error) } var _ RegistryService = &RegistryServiceOp{} @@ -240,6 +250,18 @@ type RegistryValidateNameRequest struct { Name string `json:"name"` } +// Multi-registry Open Beta API structs + +type registriesRoot struct { + Registries []*Registry `json:"registries,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 +632,90 @@ func (svc *RegistryServiceOp) ValidateName(ctx context.Context, request *Registr } return resp, nil } + +// Multi-registry Open Beta API endpoints + +// GetBeta returns the details of a named Registry. +func (svc *RegistryServiceOp) GetBeta(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 +} + +// ListBeta returns a list of the named Registries. +func (svc *RegistryServiceOp) ListBeta(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 +} + +// CreateBeta creates a named Registry. +func (svc *RegistryServiceOp) CreateBeta(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 *RegistryServiceOp) DeleteBeta(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 *RegistryServiceOp) DockerCredentialsBeta(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..859d89d1 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) } + +func TestRegistries_GetBeta(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.Registry.GetBeta(ctx, testRegistry) + require.NoError(t, err) + require.Equal(t, want, got) +} + +// TODO: Fix following test +func TestRegistries_ListBeta(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.Registry.ListBeta(ctx) + require.NoError(t, err) + fmt.Printf("Expected: %+v\n", wantRegistries) + fmt.Printf("Got: %+v\n", got) + require.Equal(t, wantRegistries, got) +} + +func TestRegistries_CreateBeta(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.Registry.CreateBeta(ctx, createRequest) + require.NoError(t, err) + require.Equal(t, want, got) +} + +func TestRegistries_DeleteBeta(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.Registry.DeleteBeta(ctx, testRegistry) + require.NoError(t, err) +} + +func TestRegistries_DockerCredentialsBeta(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.Registry.DockerCredentialsBeta(ctx, testRegistry, test.params) + fmt.Println(returnedConfig) + require.NoError(t, err) + require.Equal(t, []byte(returnedConfig), got.DockerConfigJSON) + }) + } +} From 70d4a883f3e66c86ba13edd8a7919ff6aaec12df Mon Sep 17 00:00:00 2001 From: Ravish Ahmad Date: Mon, 14 Oct 2024 18:20:10 +0530 Subject: [PATCH 2/4] docr: Add TotalStorageUsageBytes field to registriesRoot struct --- registry.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/registry.go b/registry.go index 7f02585a..70d16e1b 100644 --- a/registry.go +++ b/registry.go @@ -253,7 +253,8 @@ type RegistryValidateNameRequest struct { // Multi-registry Open Beta API structs type registriesRoot struct { - Registries []*Registry `json:"registries,omitempty"` + Registries []*Registry `json:"registries,omitempty"` + TotalStorageUsageBytes uint64 `json:"total_storage_usage_bytes,omitempty"` } // RegistriesCreateRequest represents a request to create a secondary registry. From 8cae449a5ba8b5f1c02f4d2dbd48b26f6ca887fb Mon Sep 17 00:00:00 2001 From: Ravish Ahmad Date: Wed, 23 Oct 2024 14:52:05 +0530 Subject: [PATCH 3/4] docr: Create new Registries service for multiple-registry endpoints --- godo.go | 2 ++ registry.go | 42 ++++++++++++++++++++++++++---------------- registry_test.go | 22 +++++++++++----------- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/godo.go b/godo.go index 3702ac1f..74e19ccf 100644 --- a/godo.go +++ b/godo.go @@ -80,6 +80,7 @@ type Client struct { Projects ProjectsService Regions RegionsService Registry RegistryService + Registries RegistriesService ReservedIPs ReservedIPsService ReservedIPActions ReservedIPActionsService Sizes SizesService @@ -290,6 +291,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 70d16e1b..e6482268 100644 --- a/registry.go +++ b/registry.go @@ -41,13 +41,6 @@ type RegistryService interface { GetSubscription(context.Context) (*RegistrySubscription, *Response, error) UpdateSubscription(context.Context, *RegistrySubscriptionUpdateRequest) (*RegistrySubscription, *Response, error) ValidateName(context.Context, *RegistryValidateNameRequest) (*Response, error) - - // Multi-registry Open Beta API methods - GetBeta(context.Context, string) (*Registry, *Response, error) - ListBeta(context.Context) ([]*Registry, *Response, error) - CreateBeta(context.Context, *RegistriesCreateRequest) (*Registry, *Response, error) - DeleteBeta(context.Context, string) (*Response, error) - DockerCredentialsBeta(context.Context, string, *RegistryDockerCredentialsRequest) (*DockerCredentials, *Response, error) } var _ RegistryService = &RegistryServiceOp{} @@ -634,10 +627,27 @@ func (svc *RegistryServiceOp) ValidateName(ctx context.Context, request *Registr return resp, nil } -// Multi-registry Open Beta API endpoints +// 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 +} -// GetBeta returns the details of a named Registry. -func (svc *RegistryServiceOp) GetBeta(ctx context.Context, registry string) (*Registry, *Response, error) { +// 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 { @@ -651,8 +661,8 @@ func (svc *RegistryServiceOp) GetBeta(ctx context.Context, registry string) (*Re return root.Registry, resp, nil } -// ListBeta returns a list of the named Registries. -func (svc *RegistryServiceOp) ListBeta(ctx context.Context) ([]*Registry, *Response, error) { +// 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 @@ -665,8 +675,8 @@ func (svc *RegistryServiceOp) ListBeta(ctx context.Context) ([]*Registry, *Respo return root.Registries, resp, nil } -// CreateBeta creates a named Registry. -func (svc *RegistryServiceOp) CreateBeta(ctx context.Context, create *RegistriesCreateRequest) (*Registry, *Response, error) { +// 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 @@ -681,7 +691,7 @@ func (svc *RegistryServiceOp) CreateBeta(ctx context.Context, create *Registries // Delete deletes a named Registry. There is no way to recover a Registry once it has // been destroyed. -func (svc *RegistryServiceOp) DeleteBeta(ctx context.Context, registry string) (*Response, error) { +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 { @@ -695,7 +705,7 @@ func (svc *RegistryServiceOp) DeleteBeta(ctx context.Context, registry string) ( } // DockerCredentials retrieves a Docker config file containing named Registry's credentials. -func (svc *RegistryServiceOp) DockerCredentialsBeta(ctx context.Context, registry string, request *RegistryDockerCredentialsRequest) (*DockerCredentials, *Response, error) { +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 { diff --git a/registry_test.go b/registry_test.go index 859d89d1..edf5e696 100644 --- a/registry_test.go +++ b/registry_test.go @@ -966,7 +966,8 @@ func TestRegistry_ValidateName(t *testing.T) { require.NoError(t, err) } -func TestRegistries_GetBeta(t *testing.T) { +// Tests for Registries service methods +func TestRegistries_Get(t *testing.T) { setup() defer teardown() @@ -994,13 +995,12 @@ func TestRegistries_GetBeta(t *testing.T) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, getResponseJSON) }) - got, _, err := client.Registry.GetBeta(ctx, testRegistry) + got, _, err := client.Registries.Get(ctx, testRegistry) require.NoError(t, err) require.Equal(t, want, got) } -// TODO: Fix following test -func TestRegistries_ListBeta(t *testing.T) { +func TestRegistries_List(t *testing.T) { setup() defer teardown() @@ -1031,14 +1031,14 @@ func TestRegistries_ListBeta(t *testing.T) { fmt.Printf("Returning: %v", getResponseJSON) fmt.Fprint(w, getResponseJSON) }) - got, _, err := client.Registry.ListBeta(ctx) + 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_CreateBeta(t *testing.T) { +func TestRegistries_Create(t *testing.T) { setup() defer teardown() @@ -1078,12 +1078,12 @@ func TestRegistries_CreateBeta(t *testing.T) { fmt.Fprint(w, createResponseJSON) }) - got, _, err := client.Registry.CreateBeta(ctx, createRequest) + got, _, err := client.Registries.Create(ctx, createRequest) require.NoError(t, err) require.Equal(t, want, got) } -func TestRegistries_DeleteBeta(t *testing.T) { +func TestRegistries_Delete(t *testing.T) { setup() defer teardown() @@ -1091,11 +1091,11 @@ func TestRegistries_DeleteBeta(t *testing.T) { testMethod(t, r, http.MethodDelete) }) - _, err := client.Registry.DeleteBeta(ctx, testRegistry) + _, err := client.Registries.Delete(ctx, testRegistry) require.NoError(t, err) } -func TestRegistries_DockerCredentialsBeta(t *testing.T) { +func TestRegistries_DockerCredentials(t *testing.T) { returnedConfig := "this could be a docker config" tests := []struct { name string @@ -1139,7 +1139,7 @@ func TestRegistries_DockerCredentialsBeta(t *testing.T) { fmt.Fprint(w, returnedConfig) }) - got, _, err := client.Registry.DockerCredentialsBeta(ctx, testRegistry, test.params) + got, _, err := client.Registries.DockerCredentials(ctx, testRegistry, test.params) fmt.Println(returnedConfig) require.NoError(t, err) require.Equal(t, []byte(returnedConfig), got.DockerConfigJSON) From 41e4e5ea956afd7e5440e0c4e8ec15d51569d380 Mon Sep 17 00:00:00 2001 From: Ravish Ahmad Date: Wed, 23 Oct 2024 14:55:31 +0530 Subject: [PATCH 4/4] docr: Fix indentation --- registry_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/registry_test.go b/registry_test.go index edf5e696..f7e0c93a 100644 --- a/registry_test.go +++ b/registry_test.go @@ -1060,8 +1060,8 @@ func TestRegistries_Create(t *testing.T) { "registry": { "name": "` + testRegistry + `", "storage_usage_bytes": 0, - "storage_usage_bytes_updated_at": "` + testTimeString + `", - "created_at": "` + testTimeString + `", + "storage_usage_bytes_updated_at": "` + testTimeString + `", + "created_at": "` + testTimeString + `", "region": "` + testRegion + `" } }`