diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c557202 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + go-pipeline: + name: test + runs-on: ubuntu-latest + + steps: + - name: checkout + uses: actions/checkout@v2 + + - name: go1.11 test + uses: digitalocean/golang-pipeline/go1.11/test@master + + - name: go1.12 test + uses: digitalocean/golang-pipeline/go1.12/test@master + + - name: go1.13 test + uses: digitalocean/golang-pipeline/go1.13/test@master + + - name: go1.14 test + uses: digitalocean/golang-pipeline/go1.14/test@master diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2099787..0000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: go - -go: - - 1.7.x - - 1.8.x - - 1.9.x - - 1.10.x - - 1.11.x - - 1.12.x - - 1.13.x - - tip - -matrix: - allow_failures: - - go: tip diff --git a/1-click.go b/1-click.go new file mode 100644 index 0000000..fab04fe --- /dev/null +++ b/1-click.go @@ -0,0 +1,52 @@ +package godo + +import ( + "context" + "fmt" + "net/http" +) + +const oneClickBasePath = "v2/1-clicks" + +// OneClickService is an interface for interacting with 1-clicks with the +// DigitalOcean API. +// See: https://developers.digitalocean.com/documentation/v2/#1-click-applications +type OneClickService interface { + List(context.Context, string) ([]*OneClick, *Response, error) +} + +var _ OneClickService = &OneClickServiceOp{} + +// OneClickServiceOp interfaces with 1-click endpoints in the DigitalOcean API. +type OneClickServiceOp struct { + client *Client +} + +// OneClick is the structure of a 1-click +type OneClick struct { + Slug string `json:"slug"` + Type string `json:"type"` +} + +// OneClicksRoot is the root of the json payload that contains a list of 1-clicks +type OneClicksRoot struct { + List []*OneClick `json:"1_clicks"` +} + +// List returns a list of the available 1-click applications. +func (ocs *OneClickServiceOp) List(ctx context.Context, oneClickType string) ([]*OneClick, *Response, error) { + path := fmt.Sprintf(`%s?type=%s`, oneClickBasePath, oneClickType) + + req, err := ocs.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(OneClicksRoot) + resp, err := ocs.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.List, resp, nil +} diff --git a/1-click_test.go b/1-click_test.go new file mode 100644 index 0000000..7b11f40 --- /dev/null +++ b/1-click_test.go @@ -0,0 +1,49 @@ +package godo + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var testOneClick = &OneClick{ + Slug: "test-slug", + Type: "droplet", +} + +var testOneClickJSON = ` + { + "slug":"test-slug", + "type":"droplet" + } +` + +func TestOneClick_List(t *testing.T) { + setup() + defer teardown() + + svc := client.OneClick + path := "/v2/1-clicks" + want := []*OneClick{ + testOneClick, + } + + jsonBlob := ` +{ + "1_clicks": [ +` + testOneClickJSON + ` + ] +} +` + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint(w, jsonBlob) + }) + + got, _, err := svc.List(ctx, "") + require.NoError(t, err) + assert.Equal(t, want, got) +} diff --git a/CHANGELOG.md b/CHANGELOG.md index abc8d7e..c826205 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,27 @@ # Change Log -## unreleased +## [v1.37.0] - 2020-06-01 + +- #336 registry: URL encode repository names when building URLs. @adamwg +- #335 Add 1-click service and request. @scottcrawford03 + +## [v1.36.0] - 2020-05-12 + +- #331 Expose expiry_seconds for Registry.DockerCredentials. @andrewsomething + +## [v1.35.1] - 2020-04-21 + +- #328 Update vulnerable x/crypto dependency - @bentranter + +## [v1.35.0] - 2020-04-20 + +- #326 Add TagCount field to registry/Repository - @nicktate +- #325 Add DOCR EA routes - @nicktate +- #324 Upgrade godo to Go 1.14 - @bentranter + +## [v1.34.0] - 2020-03-30 + +- #320 Add VPC v3 attributes - @viola ## [v1.33.1] - 2020-03-23 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 67cd6bd..33f0313 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,25 +1,17 @@ # Contributing -If you submit a pull request, please keep the following guidelines in mind: +We love contributions! You are welcome to open a pull request, but it's a good idea to +open an issue and discuss your idea with us first. + +Once you are ready to open a PR, please keep the following guidelines in mind: 1. Code should be `go fmt` compliant. -2. Types, structs and funcs should be documented. -3. Tests pass. +1. Types, structs and funcs should be documented. +1. Tests pass. ## Getting set up -Assuming your `$GOPATH` is set up according to your desires, run: - -```sh -go get github.com/digitalocean/godo -go get -u github.com/stretchr/testify/assert -``` - -If outside `$GOPATH`, just clone the repository: - -```sh -git clone https://github.com/digitalocean/godo -``` +`godo` uses go modules. Just fork this repo, clone your fork and off you go! ## Running tests @@ -31,13 +23,21 @@ go test -mod=vendor . ## Versioning -Godo follows [semver](https://www.semver.org) versioning semantics. New functionality should be accompanied by increment to the minor version number. The current strategy is to release often. Any code which is complete, tested, reviewed, and merged to master is worthy of release. +Godo follows [semver](https://www.semver.org) versioning semantics. +New functionality should be accompanied by increment to the minor +version number. Any code merged to master is subject to release. ## Releasing -Releasing a new version of godo is currently a manual process. +Releasing a new version of godo is currently a manual process. -1. Update the `CHANGELOG.md` with your changes. If a version header for the next (unreleased) version does not exist, create one. Include one bullet point for each piece of new functionality in the release, including the pull request ID, description, and author(s). +Submit a separate pull request for the version change from the pull +request with your changes. + +1. Update the `CHANGELOG.md` with your changes. If a version header + for the next (unreleased) version does not exist, create one. + Include one bullet point for each piece of new functionality in the + release, including the pull request ID, description, and author(s). ``` ## [v1.8.0] - 2019-03-13 diff --git a/go.mod b/go.mod index c7e16a1..36753b1 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,16 @@ module github.com/digitalocean/godo -go 1.13 +go 1.14 require ( + github.com/golang/protobuf v1.3.5 // indirect github.com/google/go-querystring v1.0.0 github.com/stretchr/testify v1.4.0 - golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a + golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d + google.golang.org/appengine v1.6.5 // indirect ) replace github.com/stretchr/objx => github.com/stretchr/objx v0.2.0 + +replace golang.org/x/crypto => golang.org/x/crypto v0.0.0-20200420201142-3c4aac89819a diff --git a/go.sum b/go.sum index 617c0b3..ccd0f08 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -13,16 +16,26 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +golang.org/x/crypto v0.0.0-20200420201142-3c4aac89819a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA= -golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/godo.go b/godo.go index bd20cbe..c6dde8e 100644 --- a/godo.go +++ b/godo.go @@ -18,7 +18,7 @@ import ( ) const ( - libraryVersion = "1.33.1" + libraryVersion = "1.37.0" defaultBaseURL = "https://api.digitalocean.com/" userAgent = "godo/" + libraryVersion mediaType = "application/json" @@ -72,6 +72,7 @@ type Client struct { Registry RegistryService Databases DatabasesService VPCs VPCsService + OneClick OneClickService // Optional function called after every successful request made to the DO APIs onRequestCompleted RequestCompletionCallback @@ -211,6 +212,7 @@ func NewClient(httpClient *http.Client) *Client { c.Registry = &RegistryServiceOp{client: c} c.Databases = &DatabasesServiceOp{client: c} c.VPCs = &VPCsServiceOp{client: c} + c.OneClick = &OneClickServiceOp{client: c} return c } diff --git a/registry.go b/registry.go index d20d0c3..1b5c40b 100644 --- a/registry.go +++ b/registry.go @@ -5,6 +5,8 @@ import ( "context" "fmt" "net/http" + "net/url" + "strconv" "time" ) @@ -22,6 +24,10 @@ type RegistryService interface { Get(context.Context) (*Registry, *Response, error) Delete(context.Context) (*Response, error) DockerCredentials(context.Context, *RegistryDockerCredentialsRequest) (*DockerCredentials, *Response, error) + ListRepositories(context.Context, string, *ListOptions) ([]*Repository, *Response, error) + 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) } var _ RegistryService = &RegistryServiceOp{} @@ -39,7 +45,8 @@ type RegistryCreateRequest struct { // RegistryDockerCredentialsRequest represents a request to retrieve docker // credentials for a registry. type RegistryDockerCredentialsRequest struct { - ReadWrite bool `json:"read_write"` + ReadWrite bool `json:"read_write"` + ExpirySeconds *int `json:"expiry_seconds,omitempty"` } // Registry represents a registry. @@ -48,10 +55,41 @@ type Registry struct { CreatedAt time.Time `json:"created_at,omitempty"` } +// Repository represents a repository +type Repository struct { + RegistryName string `json:"registry_name,omitempty"` + Name string `json:"name,omitempty"` + LatestTag *RepositoryTag `json:"latest_tag,omitempty"` + TagCount uint64 `json:"tag_count,omitempty"` +} + +// RepositoryTag represents a repository tag +type RepositoryTag struct { + RegistryName string `json:"registry_name,omitempty"` + Repository string `json:"repository,omitempty"` + Tag string `json:"tag,omitempty"` + ManifestDigest string `json:"manifest_digest,omitempty"` + CompressedSizeBytes uint64 `json:"compressed_size_bytes,omitempty"` + SizeBytes uint64 `json:"size_bytes,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` +} + type registryRoot struct { Registry *Registry `json:"registry,omitempty"` } +type repositoriesRoot struct { + Repositories []*Repository `json:"repositories,omitempty"` + Links *Links `json:"links,omitempty"` + Meta *Meta `json:"meta"` +} + +type repositoryTagsRoot struct { + Tags []*RepositoryTag `json:"tags,omitempty"` + Links *Links `json:"links,omitempty"` + Meta *Meta `json:"meta"` +} + // 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) @@ -103,13 +141,19 @@ type DockerCredentials struct { // DockerCredentials retrieves a Docker config file containing the registry's credentials. func (svc *RegistryServiceOp) DockerCredentials(ctx context.Context, request *RegistryDockerCredentialsRequest) (*DockerCredentials, *Response, error) { - path := fmt.Sprintf("%s/%s?read_write=%t", registryPath, "docker-credentials", request.ReadWrite) - + path := fmt.Sprintf("%s/%s", registryPath, "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 { @@ -121,3 +165,89 @@ func (svc *RegistryServiceOp) DockerCredentials(ctx context.Context, request *Re } return dc, resp, nil } + +// ListRepositories returns a list of the Repositories visible with the registry's credentials. +func (svc *RegistryServiceOp) ListRepositories(ctx context.Context, registry string, opts *ListOptions) ([]*Repository, *Response, error) { + path := fmt.Sprintf("%s/%s/repositories", 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(repositoriesRoot) + + resp, err := svc.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + if l := root.Links; l != nil { + resp.Links = l + } + if m := root.Meta; m != nil { + resp.Meta = m + } + + return root.Repositories, resp, nil +} + +// ListRepositoryTags returns a list of the RepositoryTags available within the given repository. +func (svc *RegistryServiceOp) ListRepositoryTags(ctx context.Context, registry, repository string, opts *ListOptions) ([]*RepositoryTag, *Response, error) { + path := fmt.Sprintf("%s/%s/repositories/%s/tags", registryPath, registry, url.PathEscape(repository)) + 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(repositoryTagsRoot) + + resp, err := svc.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + if l := root.Links; l != nil { + resp.Links = l + } + if m := root.Meta; m != nil { + resp.Meta = m + } + + return root.Tags, resp, nil +} + +// DeleteTag deletes a tag within a given repository. +func (svc *RegistryServiceOp) DeleteTag(ctx context.Context, registry, repository, tag string) (*Response, error) { + path := fmt.Sprintf("%s/%s/repositories/%s/tags/%s", registryPath, registry, url.PathEscape(repository), tag) + 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 +} + +// DeleteManifest deletes a manifest by its digest within a given repository. +func (svc *RegistryServiceOp) DeleteManifest(ctx context.Context, registry, repository, digest string) (*Response, error) { + path := fmt.Sprintf("%s/%s/repositories/%s/digests/%s", registryPath, registry, url.PathEscape(repository), digest) + 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 +} diff --git a/registry_test.go b/registry_test.go index adaab74..898d614 100644 --- a/registry_test.go +++ b/registry_test.go @@ -7,18 +7,32 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +const ( + testRegistry = "test-registry" + testRepository = "test/repository" + testEncodedRepository = "test%2Frepository" + testTag = "test-tag" + testDigest = "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + testCompressedSize = 2789669 + testSize = 5843968 +) + +var ( + testTime = time.Date(2020, 4, 1, 0, 0, 0, 0, time.UTC) + testTimeString = testTime.Format(time.RFC3339) +) + func TestRegistry_Create(t *testing.T) { setup() defer teardown() - createdAt, err := time.Parse(time.RFC3339, "2020-01-24T20:24:31Z") - require.NoError(t, err) want := &Registry{ - Name: "foo", - CreatedAt: createdAt, + Name: testRegistry, + CreatedAt: testTime, } createRequest := &RegistryCreateRequest{ @@ -28,8 +42,8 @@ func TestRegistry_Create(t *testing.T) { createResponseJSON := ` { "registry": { - "name": "foo", - "created_at": "2020-01-24T20:24:31Z" + "name": "` + testRegistry + `", + "created_at": "` + testTimeString + `" } }` @@ -55,13 +69,13 @@ func TestRegistry_Get(t *testing.T) { defer teardown() want := &Registry{ - Name: "foo", + Name: testRegistry, } getResponseJSON := ` { "registry": { - "name": "foo" + "name": "` + testRegistry + `" } }` @@ -89,9 +103,10 @@ func TestRegistry_Delete(t *testing.T) { func TestRegistry_DockerCredentials(t *testing.T) { returnedConfig := "this could be a docker config" tests := []struct { - name string - params *RegistryDockerCredentialsRequest - expectedReadWrite string + name string + params *RegistryDockerCredentialsRequest + expectedReadWrite string + expectedExpirySeconds string }{ { name: "read-only (default)", @@ -103,6 +118,18 @@ func TestRegistry_DockerCredentials(t *testing.T) { params: &RegistryDockerCredentialsRequest{ReadWrite: true}, expectedReadWrite: "true", }, + { + name: "read-only + custom expiry", + params: &RegistryDockerCredentialsRequest{ExpirySeconds: intPtr(60 * 60)}, + expectedReadWrite: "false", + expectedExpirySeconds: "3600", + }, + { + name: "read/write + custom expiry", + params: &RegistryDockerCredentialsRequest{ReadWrite: true, ExpirySeconds: intPtr(60 * 60)}, + expectedReadWrite: "true", + expectedExpirySeconds: "3600", + }, } for _, test := range tests { @@ -112,6 +139,7 @@ func TestRegistry_DockerCredentials(t *testing.T) { mux.HandleFunc("/v2/registry/docker-credentials", 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) }) @@ -122,3 +150,163 @@ func TestRegistry_DockerCredentials(t *testing.T) { }) } } + +func TestRepository_List(t *testing.T) { + setup() + defer teardown() + + wantRepositories := []*Repository{ + { + RegistryName: testRegistry, + Name: testRepository, + TagCount: 1, + LatestTag: &RepositoryTag{ + RegistryName: testRegistry, + Repository: testRepository, + Tag: testTag, + ManifestDigest: testDigest, + CompressedSizeBytes: testCompressedSize, + SizeBytes: testSize, + UpdatedAt: testTime, + }, + }, + } + getResponseJSON := `{ + "repositories": [ + { + "registry_name": "` + testRegistry + `", + "name": "` + testRepository + `", + "tag_count": 1, + "latest_tag": { + "registry_name": "` + testRegistry + `", + "repository": "` + testRepository + `", + "tag": "` + testTag + `", + "manifest_digest": "` + testDigest + `", + "compressed_size_bytes": ` + fmt.Sprintf("%d", testCompressedSize) + `, + "size_bytes": ` + fmt.Sprintf("%d", testSize) + `, + "updated_at": "` + testTimeString + `" + } + } + ], + "links": { + "pages": { + "next": "https://api.digitalocean.com/v2/registry/` + testRegistry + `/repositories?page=2", + "last": "https://api.digitalocean.com/v2/registry/` + testRegistry + `/repositories?page=2" + } + }, + "meta": { + "total": 2 + } +}` + + mux.HandleFunc(fmt.Sprintf("/v2/registry/%s/repositories", testRegistry), 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, getResponseJSON) + }) + got, response, err := client.Registry.ListRepositories(ctx, testRegistry, &ListOptions{Page: 1, PerPage: 1}) + require.NoError(t, err) + require.Equal(t, wantRepositories, got) + + gotRespLinks := response.Links + wantRespLinks := &Links{ + Pages: &Pages{ + Next: fmt.Sprintf("https://api.digitalocean.com/v2/registry/%s/repositories?page=2", testRegistry), + Last: fmt.Sprintf("https://api.digitalocean.com/v2/registry/%s/repositories?page=2", testRegistry), + }, + } + assert.Equal(t, wantRespLinks, gotRespLinks) + + gotRespMeta := response.Meta + wantRespMeta := &Meta{ + Total: 2, + } + assert.Equal(t, wantRespMeta, gotRespMeta) +} + +func TestRepository_ListTags(t *testing.T) { + setup() + defer teardown() + + wantTags := []*RepositoryTag{ + { + RegistryName: testRegistry, + Repository: testRepository, + Tag: testTag, + ManifestDigest: testDigest, + CompressedSizeBytes: testCompressedSize, + SizeBytes: testSize, + UpdatedAt: testTime, + }, + } + getResponseJSON := `{ + "tags": [ + { + "registry_name": "` + testRegistry + `", + "repository": "` + testRepository + `", + "tag": "` + testTag + `", + "manifest_digest": "` + testDigest + `", + "compressed_size_bytes": ` + fmt.Sprintf("%d", testCompressedSize) + `, + "size_bytes": ` + fmt.Sprintf("%d", testSize) + `, + "updated_at": "` + testTimeString + `" + } + ], + "links": { + "pages": { + "next": "https://api.digitalocean.com/v2/registry/` + testRegistry + `/repositories/` + testEncodedRepository + `/tags?page=2", + "last": "https://api.digitalocean.com/v2/registry/` + testRegistry + `/repositories/` + testEncodedRepository + `/tags?page=2" + } + }, + "meta": { + "total": 2 + } +}` + + mux.HandleFunc(fmt.Sprintf("/v2/registry/%s/repositories/%s/tags", testRegistry, testRepository), 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, getResponseJSON) + }) + got, response, err := client.Registry.ListRepositoryTags(ctx, testRegistry, testRepository, &ListOptions{Page: 1, PerPage: 1}) + require.NoError(t, err) + require.Equal(t, wantTags, got) + + gotRespLinks := response.Links + wantRespLinks := &Links{ + Pages: &Pages{ + Next: fmt.Sprintf("https://api.digitalocean.com/v2/registry/%s/repositories/%s/tags?page=2", testRegistry, testEncodedRepository), + Last: fmt.Sprintf("https://api.digitalocean.com/v2/registry/%s/repositories/%s/tags?page=2", testRegistry, testEncodedRepository), + }, + } + assert.Equal(t, wantRespLinks, gotRespLinks) + + gotRespMeta := response.Meta + wantRespMeta := &Meta{ + Total: 2, + } + assert.Equal(t, wantRespMeta, gotRespMeta) +} + +func TestRegistry_DeleteTag(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc(fmt.Sprintf("/v2/registry/%s/repositories/%s/tags/%s", testRegistry, testRepository, testTag), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodDelete) + }) + + _, err := client.Registry.DeleteTag(ctx, testRegistry, testRepository, testTag) + require.NoError(t, err) +} + +func TestRegistry_DeleteManifest(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc(fmt.Sprintf("/v2/registry/%s/repositories/%s/digests/%s", testRegistry, testRepository, testDigest), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodDelete) + }) + + _, err := client.Registry.DeleteManifest(ctx, testRegistry, testRepository, testDigest) + require.NoError(t, err) +}