diff --git a/registry.go b/registry.go index b0c2432..7f02585 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 7902a6c..859d89d 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) + }) + } +}