package godo import ( "bytes" "context" "fmt" "net/http" "net/url" "strconv" "time" ) const ( registryPath = "/v2/registry" // RegistryServer is the hostname of the DigitalOcean registry service RegistryServer = "registry.digitalocean.com" ) // RegistryService is an interface for interfacing with the Registry endpoints // of the DigitalOcean API. // See: https://developers.digitalocean.com/documentation/v2#registry type RegistryService interface { Create(context.Context, *RegistryCreateRequest) (*Registry, *Response, error) 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) StartGarbageCollection(context.Context, string, ...*StartGarbageCollectionRequest) (*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) GetOptions(context.Context) (*RegistryOptions, *Response, error) GetSubscription(context.Context) (*RegistrySubscription, *Response, error) UpdateSubscription(context.Context, *RegistrySubscriptionUpdateRequest) (*RegistrySubscription, *Response, error) } var _ RegistryService = &RegistryServiceOp{} // RegistryServiceOp handles communication with Registry methods of the DigitalOcean API. type RegistryServiceOp struct { client *Client } // RegistryCreateRequest represents a request to create a registry. type RegistryCreateRequest struct { Name string `json:"name,omitempty"` SubscriptionTierSlug string `json:"subscription_tier_slug,omitempty"` } // RegistryDockerCredentialsRequest represents a request to retrieve docker // credentials for a registry. type RegistryDockerCredentialsRequest struct { ReadWrite bool `json:"read_write"` ExpirySeconds *int `json:"expiry_seconds,omitempty"` } // Registry represents a registry. type Registry struct { Name string `json:"name,omitempty"` 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"` } // GarbageCollection represents a garbage collection. type GarbageCollection struct { UUID string `json:"uuid"` RegistryName string `json:"registry_name"` Status string `json:"status"` Type GarbageCollectionType `json:"type"` 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"` } type GarbageCollectionType string const ( // GCTypeUntaggedManifestsOnly indicates that a garbage collection should // only delete untagged manifests. GCTypeUntaggedManifestsOnly = GarbageCollectionType("untagged manifests only") // GCTypeUnreferencedBlobsOnly indicates that a garbage collection should // only delete unreferenced blobs. GCTypeUnreferencedBlobsOnly = GarbageCollectionType("unreferenced blobs only") // GCTypeUntaggedManifestsAndUnreferencedBlobs indicates that a garbage // collection should delete both untagged manifests and unreferenced blobs. GCTypeUntaggedManifestsAndUnreferencedBlobs = GarbageCollectionType("untagged manifests and unreferenced blobs") ) // StartGarbageCollectionRequest represents options to a garbage collection // start request. type StartGarbageCollectionRequest struct { Type GarbageCollectionType `json:"type"` } // UpdateGarbageCollectionRequest represents a request to update a garbage // collection. type UpdateGarbageCollectionRequest struct { Cancel bool `json:"cancel"` } // RegistryOptions are options for users when creating or updating a registry. type RegistryOptions struct { SubscriptionTiers []*RegistrySubscriptionTier `json:"subscription_tiers,omitempty"` } type registryOptionsRoot struct { Options *RegistryOptions `json:"options"` } // RegistrySubscriptionTier is a subscription tier for container registry. type RegistrySubscriptionTier struct { Name string `json:"name"` Slug string `json:"slug"` IncludedRepositories uint64 `json:"included_repositories"` IncludedStorageBytes uint64 `json:"included_storage_bytes"` AllowStorageOverage bool `json:"allow_storage_overage"` IncludedBandwidthBytes uint64 `json:"included_bandwidth_bytes"` MonthlyPriceInCents uint64 `json:"monthly_price_in_cents"` Eligible bool `json:"eligible,omitempty"` // EligibilityReaons is included when Eligible is false, and indicates the // reasons why this tier is not availble to the user. EligibilityReasons []string `json:"eligibility_reasons,omitempty"` } // RegistrySubscription is a user's subscription. type RegistrySubscription struct { Tier *RegistrySubscriptionTier `json:"tier"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type registrySubscriptionRoot struct { Subscription *RegistrySubscription `json:"subscription"` } // RegistrySubscriptionUpdateRequest represents a request to update the // subscription plan for a registry. type RegistrySubscriptionUpdateRequest struct { TierSlug string `json:"tier_slug"` } // 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) 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 } // Create creates a registry. func (svc *RegistryServiceOp) Create(ctx context.Context, create *RegistryCreateRequest) (*Registry, *Response, error) { req, err := svc.client.NewRequest(ctx, http.MethodPost, registryPath, 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 registry. There is no way to recover a registry once it has // been destroyed. func (svc *RegistryServiceOp) Delete(ctx context.Context) (*Response, error) { req, err := svc.client.NewRequest(ctx, http.MethodDelete, registryPath, 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 is the content of a Docker config file // that is used by the docker CLI // See: https://docs.docker.com/engine/reference/commandline/cli/#configjson-properties type DockerCredentials struct { DockerConfigJSON []byte } // 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", 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 { return nil, resp, err } dc := &DockerCredentials{ DockerConfigJSON: buf.Bytes(), } 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 } // StartGarbageCollection requests a garbage collection for the specified // registry. func (svc *RegistryServiceOp) StartGarbageCollection(ctx context.Context, registry string, request ...*StartGarbageCollectionRequest) (*GarbageCollection, *Response, error) { path := fmt.Sprintf("%s/%s/garbage-collection", registryPath, registry) var requestParams interface{} if len(request) < 1 { // default to only garbage collecting unreferenced blobs for backwards // compatibility requestParams = &StartGarbageCollectionRequest{ Type: GCTypeUnreferencedBlobsOnly, } } else { requestParams = request[0] } req, err := svc.client.NewRequest(ctx, http.MethodPost, path, requestParams) 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 } // ListGarbageCollections 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 } // GetOptions returns options the user can use when creating or updating a // registry. func (svc *RegistryServiceOp) GetOptions(ctx context.Context) (*RegistryOptions, *Response, error) { path := fmt.Sprintf("%s/options", registryPath) req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(registryOptionsRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Options, resp, nil } // GetSubscription retrieves the user's subscription. func (svc *RegistryServiceOp) GetSubscription(ctx context.Context) (*RegistrySubscription, *Response, error) { path := fmt.Sprintf("%s/subscription", registryPath) req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(registrySubscriptionRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Subscription, resp, nil } // UpdateSubscription updates the user's registry subscription. func (svc *RegistryServiceOp) UpdateSubscription(ctx context.Context, request *RegistrySubscriptionUpdateRequest) (*RegistrySubscription, *Response, error) { path := fmt.Sprintf("%s/subscription", registryPath) req, err := svc.client.NewRequest(ctx, http.MethodPost, path, request) if err != nil { return nil, nil, err } root := new(registrySubscriptionRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Subscription, resp, nil }