Skip to content

Commit

Permalink
DOCR-1201: Add new RegistriesService to support methods for multiple-…
Browse files Browse the repository at this point in the history
…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 <[email protected]>
Co-authored-by: Andrew Starr-Bochicchio <[email protected]>
  • Loading branch information
3 people authored Nov 1, 2024
1 parent 2654a9d commit 29a46b0
Show file tree
Hide file tree
Showing 3 changed files with 303 additions and 0 deletions.
2 changes: 2 additions & 0 deletions godo.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ type Client struct {
Projects ProjectsService
Regions RegionsService
Registry RegistryService
Registries RegistriesService
ReservedIPs ReservedIPsService
ReservedIPActions ReservedIPActionsService
Sizes SizesService
Expand Down Expand Up @@ -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}
Expand Down
120 changes: 120 additions & 0 deletions registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
181 changes: 181 additions & 0 deletions registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}

0 comments on commit 29a46b0

Please sign in to comment.