Skip to content

Commit

Permalink
registry: add garbage collection support (digitalocean#400)
Browse files Browse the repository at this point in the history
* registry: add garbage collection support

* rename RequestGarbageCollection -> StartGarbageCollection

Co-authored-by: Andrew Starr-Bochicchio <andrewsomething@users.noreply.github.com>
  • Loading branch information
waynr and andrewsomething authored Oct 27, 2020
1 parent 45e8230 commit da274fd
Show file tree
Hide file tree
Showing 2 changed files with 306 additions and 2 deletions.
120 changes: 120 additions & 0 deletions registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ type RegistryService interface {
ListRepositoryTags(context.Context, string, string, *ListOptions) ([]*RepositoryTag, *Response, error)
DeleteTag(context.Context, string, string, string) (*Response, error)
DeleteManifest(context.Context, string, string, string) (*Response, error)
StartGarbageCollection(context.Context, string) (*GarbageCollection, *Response, error)
GetGarbageCollection(context.Context, string) (*GarbageCollection, *Response, error)
ListGarbageCollections(context.Context, string, *ListOptions) ([]*GarbageCollection, *Response, error)
UpdateGarbageCollection(context.Context, string, string, *UpdateGarbageCollectionRequest) (*GarbageCollection, *Response, error)
}

var _ RegistryService = &RegistryServiceOp{}
Expand Down Expand Up @@ -90,6 +94,33 @@ type repositoryTagsRoot struct {
Meta *Meta `json:"meta"`
}

// GarbageCollection represents a garbage collection.
type GarbageCollection struct {
UUID string `json:"uuid"`
RegistryName string `json:"registry_name"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
BlobsDeleted uint64 `json:"blobs_deleted"`
FreedBytes uint64 `json:"freed_bytes"`
}

type garbageCollectionRoot struct {
GarbageCollection *GarbageCollection `json:"garbage_collection,omitempty"`
}

type garbageCollectionsRoot struct {
GarbageCollections []*GarbageCollection `json:"garbage_collections,omitempty"`
Links *Links `json:"links,omitempty"`
Meta *Meta `json:"meta"`
}

// UpdateGarbageCollectionRequest represents a request to update a garbage
// collection.
type UpdateGarbageCollectionRequest struct {
Cancel bool `json:"cancel"`
}

// 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 @@ -251,3 +282,92 @@ func (svc *RegistryServiceOp) DeleteManifest(ctx context.Context, registry, repo

return resp, nil
}

// StartGarbageCollection requests a garbage collection for the specified
// registry.
func (svc *RegistryServiceOp) StartGarbageCollection(ctx context.Context, registry string) (*GarbageCollection, *Response, error) {
path := fmt.Sprintf("%s/%s/garbage-collection", registryPath, registry)
req, err := svc.client.NewRequest(ctx, http.MethodPost, path, nil)
if err != nil {
return nil, nil, err
}

root := new(garbageCollectionRoot)
resp, err := svc.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}

return root.GarbageCollection, resp, err
}

// GetGarbageCollection retrieves the currently-active garbage collection for
// the specified registry; if there are no active garbage collections, then
// return a 404/NotFound error. There can only be one active garbage
// collection on a registry.
func (svc *RegistryServiceOp) GetGarbageCollection(ctx context.Context, registry string) (*GarbageCollection, *Response, error) {
path := fmt.Sprintf("%s/%s/garbage-collection", registryPath, registry)
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}

root := new(garbageCollectionRoot)
resp, err := svc.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}

return root.GarbageCollection, resp, nil
}

// ListGarbageCollection retrieves all garbage collections (active and
// inactive) for the specified registry.
func (svc *RegistryServiceOp) ListGarbageCollections(ctx context.Context, registry string, opts *ListOptions) ([]*GarbageCollection, *Response, error) {
path := fmt.Sprintf("%s/%s/garbage-collections", registryPath, registry)
path, err := addOptions(path, opts)
if err != nil {
return nil, nil, err
}
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}

root := new(garbageCollectionsRoot)
resp, err := svc.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}

if root.Links != nil {
resp.Links = root.Links
}
if root.Meta != nil {
resp.Meta = root.Meta
}

return root.GarbageCollections, resp, nil
}

// UpdateGarbageCollection updates the specified garbage collection for the
// specified registry. While only the currently-active garbage collection can
// be updated we still require the exact garbage collection to be specified to
// avoid race conditions that might may arise from issuing an update to the
// implicit "currently-active" garbage collection. Returns the updated garbage
// collection.
func (svc *RegistryServiceOp) UpdateGarbageCollection(ctx context.Context, registry, gcUUID string, request *UpdateGarbageCollectionRequest) (*GarbageCollection, *Response, error) {
path := fmt.Sprintf("%s/%s/garbage-collection/%s", registryPath, registry, gcUUID)
req, err := svc.client.NewRequest(ctx, http.MethodPut, path, request)
if err != nil {
return nil, nil, err
}

root := new(garbageCollectionRoot)
resp, err := svc.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}

return root.GarbageCollection, resp, nil
}
188 changes: 186 additions & 2 deletions registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package godo
import (
"encoding/json"
"fmt"
"html/template"
"net/http"
"strings"
"testing"
"time"

Expand All @@ -19,11 +21,24 @@ const (
testDigest = "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
testCompressedSize = 2789669
testSize = 5843968
testGCBlobsDeleted = 42
testGCFreedBytes = 666
testGCStatus = "requested"
testGCUUID = "mew-mew-id"
)

var (
testTime = time.Date(2020, 4, 1, 0, 0, 0, 0, time.UTC)
testTimeString = testTime.Format(time.RFC3339)
testTime = time.Date(2020, 4, 1, 0, 0, 0, 0, time.UTC)
testTimeString = testTime.Format(time.RFC3339)
testGarbageCollection = &GarbageCollection{
UUID: testGCUUID,
RegistryName: testRegistry,
Status: testGCStatus,
CreatedAt: testTime,
UpdatedAt: testTime,
BlobsDeleted: testGCBlobsDeleted,
FreedBytes: testGCFreedBytes,
}
)

func TestRegistry_Create(t *testing.T) {
Expand Down Expand Up @@ -310,3 +325,172 @@ func TestRegistry_DeleteManifest(t *testing.T) {
_, err := client.Registry.DeleteManifest(ctx, testRegistry, testRepository, testDigest)
require.NoError(t, err)
}

func reifyTemplateStr(t *testing.T, tmplStr string, v interface{}) string {
tmpl, err := template.New("meow").Parse(tmplStr)
require.NoError(t, err)

s := &strings.Builder{}
err = tmpl.Execute(s, v)
require.NoError(t, err)

return s.String()
}

func TestGarbageCollection_Start(t *testing.T) {
setup()
defer teardown()

want := testGarbageCollection
requestResponseJSONTmpl := `
{
"garbage_collection": {
"uuid": "{{.UUID}}",
"registry_name": "{{.RegistryName}}",
"status": "{{.Status}}",
"created_at": "{{.CreatedAt.Format "2006-01-02T15:04:05Z07:00"}}",
"updated_at": "{{.UpdatedAt.Format "2006-01-02T15:04:05Z07:00"}}",
"blobs_deleted": {{.BlobsDeleted}},
"freed_bytes": {{.FreedBytes}}
}
}`
requestResponseJSON := reifyTemplateStr(t, requestResponseJSONTmpl, want)

mux.HandleFunc("/v2/registry/"+testRegistry+"/garbage-collection",
func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodPost)
fmt.Fprint(w, requestResponseJSON)
})

got, _, err := client.Registry.StartGarbageCollection(ctx, testRegistry)
require.NoError(t, err)
require.Equal(t, want, got)
}

func TestGarbageCollection_Get(t *testing.T) {
setup()
defer teardown()

want := testGarbageCollection
requestResponseJSONTmpl := `
{
"garbage_collection": {
"uuid": "{{.UUID}}",
"registry_name": "{{.RegistryName}}",
"status": "{{.Status}}",
"created_at": "{{.CreatedAt.Format "2006-01-02T15:04:05Z07:00"}}",
"updated_at": "{{.UpdatedAt.Format "2006-01-02T15:04:05Z07:00"}}",
"blobs_deleted": {{.BlobsDeleted}},
"freed_bytes": {{.FreedBytes}}
}
}`
requestResponseJSON := reifyTemplateStr(t, requestResponseJSONTmpl, want)

mux.HandleFunc("/v2/registry/"+testRegistry+"/garbage-collection",
func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
fmt.Fprint(w, requestResponseJSON)
})

got, _, err := client.Registry.GetGarbageCollection(ctx, testRegistry)
require.NoError(t, err)
require.Equal(t, want, got)
}

func TestGarbageCollection_List(t *testing.T) {
setup()
defer teardown()

want := []*GarbageCollection{testGarbageCollection}
requestResponseJSONTmpl := `
{
"garbage_collections": [
{
"uuid": "{{.UUID}}",
"registry_name": "{{.RegistryName}}",
"status": "{{.Status}}",
"created_at": "{{.CreatedAt.Format "2006-01-02T15:04:05Z07:00"}}",
"updated_at": "{{.UpdatedAt.Format "2006-01-02T15:04:05Z07:00"}}",
"blobs_deleted": {{.BlobsDeleted}},
"freed_bytes": {{.FreedBytes}}
}
],
"links": {
"pages": {
"next": "https://api.digitalocean.com/v2/registry/` + testRegistry + `/garbage-collections?page=2",
"last": "https://api.digitalocean.com/v2/registry/` + testRegistry + `/garbage-collections?page=2"
}
},
"meta": {
"total": 2
}
}`
requestResponseJSON := reifyTemplateStr(t, requestResponseJSONTmpl, testGarbageCollection)

mux.HandleFunc("/v2/registry/"+testRegistry+"/garbage-collections",
func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
testFormValues(t, r, map[string]string{"page": "1", "per_page": "1"})
fmt.Fprint(w, requestResponseJSON)
})

got, resp, err := client.Registry.ListGarbageCollections(ctx, testRegistry, &ListOptions{Page: 1, PerPage: 1})
require.NoError(t, err)
require.Equal(t, want, got)

gotRespLinks := resp.Links
wantRespLinks := &Links{
Pages: &Pages{
Next: fmt.Sprintf("https://api.digitalocean.com/v2/registry/%s/garbage-collections?page=2", testRegistry),
Last: fmt.Sprintf("https://api.digitalocean.com/v2/registry/%s/garbage-collections?page=2", testRegistry),
},
}
assert.Equal(t, wantRespLinks, gotRespLinks)

gotRespMeta := resp.Meta
wantRespMeta := &Meta{
Total: 2,
}
assert.Equal(t, wantRespMeta, gotRespMeta)
}

func TestGarbageCollection_Update(t *testing.T) {
setup()
defer teardown()

updateRequest := &UpdateGarbageCollectionRequest{
Cancel: true,
}

want := testGarbageCollection
requestResponseJSONTmpl := `
{
"garbage_collection": {
"uuid": "{{.UUID}}",
"registry_name": "{{.RegistryName}}",
"status": "{{.Status}}",
"created_at": "{{.CreatedAt.Format "2006-01-02T15:04:05Z07:00"}}",
"updated_at": "{{.UpdatedAt.Format "2006-01-02T15:04:05Z07:00"}}",
"blobs_deleted": {{.BlobsDeleted}},
"freed_bytes": {{.FreedBytes}}
}
}`
requestResponseJSON := reifyTemplateStr(t, requestResponseJSONTmpl, want)

mux.HandleFunc("/v2/registry/"+testRegistry+"/garbage-collection/"+testGCUUID,
func(w http.ResponseWriter, r *http.Request) {
v := new(UpdateGarbageCollectionRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatal(err)
}

testMethod(t, r, http.MethodPut)
require.Equal(t, v, updateRequest)
fmt.Fprint(w, requestResponseJSON)
})

got, _, err := client.Registry.UpdateGarbageCollection(ctx, testRegistry, testGCUUID, updateRequest)
require.NoError(t, err)
require.Equal(t, want, got)
}

0 comments on commit da274fd

Please sign in to comment.