/*
Copyright 2018 The Doctl Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package commands

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"strings"

	"github.com/digitalocean/doctl"
	"github.com/digitalocean/doctl/commands/displayers"
	"github.com/digitalocean/doctl/do"
	"github.com/digitalocean/godo"
	dockerconf "github.com/docker/cli/cli/config"
	configtypes "github.com/docker/cli/cli/config/types"
	"github.com/spf13/cobra"
	k8sapiv1 "k8s.io/api/core/v1"
	k8smetav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	k8sjson "k8s.io/apimachinery/pkg/runtime/serializer/json"
)

type dockerConfig struct {
	Auths map[string]struct {
		Auth string `json:"auth,omitempty"`
	} `json:"auths"`
}

const (
	// DOSecretOperatorAnnotation is the annotation key so that dosecret operator can do it's magic
	// and help users pull private images automatically in their DOKS clusters
	DOSecretOperatorAnnotation = "digitalocean.com/dosecret-identifier"

	oauthTokenRevokeEndpoint = "https://cloud.digitalocean.com/v1/oauth/revoke"

	// defaultRegistryAPITokenExpirySeconds is the default number of seconds before a registry API
	// token expires. 2592000 is 30 days in seconds.
	defaultRegistryAPITokenExpirySeconds = 2592000
)

var errExpiryTimeAndNeverExpire = errors.New("the generated registry API token cannot have both the expiry time and never-expire set")

// Registry creates the registry command
func Registry() *Command {
	cmd := &Command{
		Command: &cobra.Command{
			Use:     "registry",
			Aliases: []string{"reg", "r"},
			Short:   "Display commands for working with container registries",
			Long:    "The subcommands of `doctl registry` create, manage, and allow access to your private container registry.",
			GroupID: manageResourcesGroup,
		},
	}

	createRegDesc := "Creates a new private container registry with the provided name."
	cmdRunRegistryCreate := CmdBuilder(cmd, RunRegistryCreate, "create <registry-name>",
		"Create a private container registry", createRegDesc, Writer)
	AddStringFlag(cmdRunRegistryCreate, doctl.ArgSubscriptionTier, "", "basic",
		"Subscription tier for the new registry. For a list of possible values, use the `doctl registry options subscription-tiers` command.", requiredOpt())
	AddStringFlag(cmdRunRegistryCreate, doctl.ArgRegionSlug, "", "",
		"A `slug` indicating which datacenter region the registry reside in. For a list of supported region slugs, use the `doctl registry options available-regions` command")
	cmdRunRegistryCreate.Example = `The following example creates a registry named ` + "`" + `example-registry` + "`" + ` in the NYC3 region: doctl registry create example-registry --region=nyc3`

	getRegDesc := "Retrieves details about a private container registry, including its name and the endpoint used to access it."
	cmdRegistryGet := CmdBuilder(cmd, RunRegistryGet, "get", "Retrieve details about a container registry",
		getRegDesc, Writer, aliasOpt("g"), displayerType(&displayers.Registry{}))
	cmdRegistryGet.Example = `The following example retrieves details about a registry named ` + "`" + `example-registry` + "`" + `: doctl registry get example-registry`

	deleteRegDesc := "Permanently deletes a private container registry and all of its contents. This is irreversible."
	cmdRunRegistryDelete := CmdBuilder(cmd, RunRegistryDelete, "delete",
		"Delete a container registry", deleteRegDesc, Writer, aliasOpt("d", "del", "rm"))
	AddBoolFlag(cmdRunRegistryDelete, doctl.ArgForce, doctl.ArgShortForce, false, "Force deletes the registry without confirmation prompt")
	cmdRunRegistryDelete.Example = `The following example deletes a registry named ` + "`" + `example-registry` + "`" + `: doctl registry delete example-registry`

	loginRegDesc := "Logs Docker into Container Registry making pull and push commands to your private container registry authenticated."
	cmdRegistryLogin := CmdBuilder(cmd, RunRegistryLogin, "login", "Log in Docker to a container registry",
		loginRegDesc, Writer)
	AddIntFlag(cmdRegistryLogin, doctl.ArgRegistryExpirySeconds, "", 0,
		"The length of time the registry credentials are valid for, in seconds. By default, the credentials expire after 30 days.")
	AddBoolFlag(cmdRegistryLogin, doctl.ArgRegistryReadOnly, "", false,
		"Sets the DigitalOcean API token generated by the login command to read-only, causing any push operations to fail. By default, the API token is read-write.")
	AddBoolFlag(cmdRegistryLogin, doctl.ArgRegistryNeverExpire, "", false,
		"Sets the DigitalOcean API token generated by the login command to never expire. By default, this is set to false.")
	cmdRegistryLogin.Example = `The following example logs Docker into a registry and provides Docker with read-only credentials: doctl registry login --read-only=true`

	logoutRegDesc := "This command logs Docker out of the private container registry, revoking access to it."
	cmdRunRegistryLogout := CmdBuilder(cmd, RunRegistryLogout, "logout", "Log out Docker from a container registry",
		logoutRegDesc, Writer)
	AddStringFlag(cmdRunRegistryLogout, doctl.ArgRegistryAuthorizationServerEndpoint, "", oauthTokenRevokeEndpoint, "The endpoint of the OAuth authorization server used to revoke credentials on logout.")

	kubeManifestDesc := `Outputs a YAML-formatted Kubernetes secret manifest that can be used to grant a Kubernetes cluster pull access to your private container registry.

By default, the secret manifest is applied to all the namespaces for the Kubernetes cluster using the DOSecret operator. The DOSecret operator is available on clusters running version 1.15.12-do.2 or greater. For older clusters, or to restrict the secret to a specific namespace, use the ` + "`" + `--namespace` + "`" + ` flag.

You can redirect the command's output to a file to save the manifest for later use or pipe it directly to ` + "`" + `kubectl` + "`" + ` to create the secret in your cluster:

    doctl registry kubernetes-manifest | kubectl apply -f -
`
	cmdRunKubernetesManifest := CmdBuilder(cmd, RunKubernetesManifest, "kubernetes-manifest",
		"Generate a Kubernetes secret manifest for a registry.",
		kubeManifestDesc, Writer, aliasOpt("k8s"))
	AddStringFlag(cmdRunKubernetesManifest, doctl.ArgObjectName, "", "",
		"The secret's name. Defaults to the registry name prefixed with \"registry-\"")
	AddStringFlag(cmdRunKubernetesManifest, doctl.ArgObjectNamespace, "",
		"kube-system", "The Kubernetes namespace to hold the secret")
	cmdRunKubernetesManifest.Example = `The following example generates a secret manifest for a registry named ` + "`" + `example-registry` + "`" + ` and applies it to the ` + "`" + `kube-system` + "`" + ` namespace: doctl registry kubernetes-manifest example-registry --namespace=kube-system`

	dockerConfigDesc := `Outputs a JSON-formatted Docker configuration that you can use to configure a Docker client to authenticate with your private container registry. This configuration is useful for configuring third-party tools that need access to your registry. For configuring your local Docker client use ` + "`" + `doctl registry login` + "`" + ` instead, as it preserves the configuration of any other registries you have authenticated to.

By default this command generates read-only credentials. Use the ` + "`" + `--read-write` + "`" + ` flag to generate credentials that can push. The configuration produced by this command contains a DigitalOcean API token that can be used to access your account and should be treated as sensitive information.`

	cmdRunDockerConfig := CmdBuilder(cmd, RunDockerConfig, "docker-config",
		"Generate a Docker auth configuration for a registry",
		dockerConfigDesc, Writer, aliasOpt("config"))
	AddBoolFlag(cmdRunDockerConfig, doctl.ArgReadWrite, "", false,
		"Generates credentials that can push to your registry")
	AddIntFlag(cmdRunDockerConfig, doctl.ArgRegistryExpirySeconds, "", 0,
		"The length of time the registry credentials are valid for, in seconds. By default, the credentials do not expire.")
	cmdRunDockerConfig.Example = `The following example generates a Docker configuration for a registry named ` + "`" + `example-registry` + "`" + ` and uses the ` + "`" + `--expiry-seconds` + "`" + ` to set the credentials to expire after one day: doctl registry docker-config example-registry --expiry-seconds=86400`

	cmd.AddCommand(Repository())
	cmd.AddCommand(GarbageCollection())
	cmd.AddCommand(RegistryOptions())

	return cmd
}

// Repository creates the repository sub-command
func Repository() *Command {
	cmd := &Command{
		Command: &cobra.Command{
			Use:     "repository",
			Aliases: []string{"repo", "r"},
			Short:   "Display commands for working with repositories in a container registry",
			Long:    "The subcommands of `doctl registry repository` allow you to manage various properties of your repository.",
		},
	}

	overrideNS := "registry.repository"

	listRepositoriesDesc := `Retrieves information about repositories in a registry, including:
  - The repository name
  - The latest tag for the repository
  - The compressed size for the latest tag
  - The manifest digest for the latest tag
  - The last updated timestamp
`
	cmdListRepositories := CmdBuilder(
		cmd,
		RunListRepositories, "list",
		"List repositories for a container registry", listRepositoriesDesc,
		Writer, aliasOpt("ls"), displayerType(&displayers.Repository{}),
		hiddenCmd(),
	)
	cmdListRepositories.overrideNS = overrideNS
	addRegistryFlag(cmdListRepositories)
	cmdListRepositories.Example = `The following example lists repositories in a registry named ` + "`" + `example-registry` + "`" + ` and uses the ` + "`" + `--format` + "`" + ` flag to return only the name and update time of each repository: doctl registry repository list --format Name,UpdatedAt`

	listRepositoriesV2Desc := `Retrieves information about repositories in a registry, including:
  - The repository name
  - The latest manifest of the repository
  - The latest manifest's latest tag, if any
  - The number of tags in the repository
  - The number of manifests in the repository
`
	cmdListRepositoriesV2 := CmdBuilder(
		cmd,
		RunListRepositoriesV2, "list-v2",
		"List repositories for a container registry", listRepositoriesV2Desc,
		Writer, aliasOpt("ls2"), displayerType(&displayers.Repository{}),
	)
	cmdListRepositoriesV2.overrideNS = overrideNS
	addRegistryFlag(cmdListRepositoriesV2)
	cmdListRepositoriesV2.Example = `The following example lists repositories in a registry named ` + "`" + `example-registry` + "`" + ` and uses the ` + "`" + `--format` + "`" + ` flag to return only the name and update time of each repository: doctl registry repository list-v2 --format Name,UpdatedAt`

	listRepositoryTagsDesc := `Retrieves information about tags in a repository, including:
  - The tag name
  - The compressed size
  - The manifest digest
  - The last updated timestamp
`
	cmdListRepositoryTags := CmdBuilder(
		cmd,
		RunListRepositoryTags, "list-tags <repository>",
		"List tags for a repository in a container registry", listRepositoryTagsDesc,
		Writer, aliasOpt("lt"), displayerType(&displayers.RepositoryTag{}),
	)
	cmdListRepositoryTags.overrideNS = overrideNS
	addRegistryFlag(cmdListRepositoryTags)
	cmdListRepositoryTags.Example = `The following example lists tags in a repository named ` + "`" + `example-repository` + "`" + ` in a registry named ` + "`" + `example-registry` + "`" + `. The command also uses the ` + "`" + `--format` + "`" + ` flag to return only the tag name and manifest digest for each tag: doctl registry repository list-tags example-repository --format Tag,ManifestDigest`

	deleteTagDesc := "Permanently deletes one or more repository tags."
	cmdRunRepositoryDeleteTag := CmdBuilder(
		cmd,
		RunRepositoryDeleteTag,
		"delete-tag <repository> <tag>...",
		"Delete one or more container repository tags",
		deleteTagDesc,
		Writer,
		aliasOpt("dt"),
	)
	cmdRunRepositoryDeleteTag.overrideNS = overrideNS
	addRegistryFlag(cmdRunRepositoryDeleteTag)
	AddBoolFlag(cmdRunRepositoryDeleteTag, doctl.ArgForce, doctl.ArgShortForce, false, "Delete tag without confirmation prompt")
	cmdRunRepositoryDeleteTag.Example = `The following example deletes a tag named ` + "`" + `web` + "`" + ` from a repository named ` + "`" + `example-repository` + "`" + ` in a registry named ` + "`" + `example-registry` + "`" + `: doctl registry repository delete-tag example-repository web`

	listRepositoryManifests := `Retrieves information about manifests in a repository, including:
  - The manifest digest
  - The compressed size
  - The uncompressed size
  - The last updated timestamp
  - The manifest tags
  - The manifest blobs (available in detailed output only)
`
	cmdListRepositoryManifests := CmdBuilder(
		cmd,
		RunListRepositoryManifests, "list-manifests <repository>",
		"List manifests for a repository in a container registry", listRepositoryManifests,
		Writer, aliasOpt("lm"), displayerType(&displayers.RepositoryManifest{}),
	)
	cmdListRepositoryManifests.overrideNS = overrideNS
	addRegistryFlag(cmdListRepositoryManifests)
	cmdListRepositoryManifests.Example = `The following example lists manifests in a repository named ` + "`" + `example-repository` + "`" + `. The command also uses the ` + "`" + `--format` + "`" + ` flag to return only the digest and update time for each manifest: doctl registry repository list-manifests example-repository --format Digest,UpdatedAt`

	deleteManifestDesc := "Permanently deletes one or more repository manifests by digest."
	cmdRunRepositoryDeleteManifest := CmdBuilder(
		cmd,
		RunRepositoryDeleteManifest,
		"delete-manifest <repository> <manifest-digest>...",
		"Delete one or more container repository manifests by digest",
		deleteManifestDesc,
		Writer,
		aliasOpt("dm"),
	)
	cmdRunRepositoryDeleteManifest.overrideNS = overrideNS
	addRegistryFlag(cmdRunRepositoryDeleteManifest)
	AddBoolFlag(cmdRunRepositoryDeleteManifest, doctl.ArgForce, doctl.ArgShortForce, false, "Deletes manifest without confirmation prompt")
	cmdRunRepositoryDeleteManifest.Example = `The following example deletes a manifest with digest ` + "`" + `sha256:1234567890abcdef` + "`" + ` from a repository named ` + "`" + `example-repository` + "`" + ` in a registry named ` + "`" + `example-registry` + "`" + `: doctl registry repository delete-manifest example-repository sha256:a67c20e45178d90cbe686575719bd81f279b06842dc77521690e292c1eea685`

	return cmd
}

func addRegistryFlag(cmd *Command) {
	AddStringFlag(cmd, doctl.ArgRegistry, "", "",
		"Name of the registry that the repository belongs to. For a list of possible values, use the 'doctl registries list' command. This is optional and only needed if you have multiple registries.")
}

// GarbageCollection creates the garbage-collection subcommand
func GarbageCollection() *Command {
	cmd := &Command{
		Command: &cobra.Command{
			Use:     "garbage-collection",
			Aliases: []string{"gc", "g"},
			Short:   "Display commands for garbage collection for a container registry",
			Long:    "The subcommands of `doctl registry garbage-collection` start a garbage collection, retrieve or cancel a currently-active garbage collection, or list past garbage collections for a specified registry.",
		},
	}

	overrideNS := "registry.garbage-collection"

	runStartGarbageCollectionDesc := "Starts a garbage collection on a container registry. You can only have one active garbage collection at a time for a given registry."
	cmdStartGarbageCollection := CmdBuilder(
		cmd,
		RunStartGarbageCollection,
		"start",
		"Start garbage collection for a container registry",
		runStartGarbageCollectionDesc,
		Writer,
		aliasOpt("s"),
		displayerType(&displayers.GarbageCollection{}),
	)
	cmdStartGarbageCollection.overrideNS = overrideNS
	AddBoolFlag(cmdStartGarbageCollection, doctl.ArgGCIncludeUntaggedManifests, "", false,
		"Include untagged manifests in garbage collection.")
	AddBoolFlag(cmdStartGarbageCollection, doctl.ArgGCExcludeUnreferencedBlobs, "", false,
		"Exclude unreferenced blobs from garbage collection.")
	AddBoolFlag(cmdStartGarbageCollection, doctl.ArgForce, doctl.ArgShortForce, false, "Run garbage collection without confirmation prompt")
	cmdStartGarbageCollection.Example = `The following example starts a garbage collection on a registry named ` + "`" + `example-registry` + "`" + `: doctl registry garbage-collection start example-registry`

	gcInfoIncluded := `
  - UUID
  - Status
  - Registry name
  - Create time
  - Updated at time
  - Blobs deleted
  - Freed bytes
`

	runGetGarbageCollectionDesc := "Retrieves the currently-active garbage collection for a container registry, if any active garbage collection exists. Information included about the registry includes:" + gcInfoIncluded
	cmdGetGarbageCollection := CmdBuilder(
		cmd,
		RunGetGarbageCollection,
		"get-active",
		"Retrieve information about the currently-active garbage collection for a container registry",
		runGetGarbageCollectionDesc,
		Writer,
		aliasOpt("ga", "g"),
		displayerType(&displayers.GarbageCollection{}),
	)
	cmdGetGarbageCollection.overrideNS = overrideNS
	cmdGetGarbageCollection.Example = `The following example retrieves the currently-active garbage collection for a registry: doctl registry garbage-collection get-active
	
The following example retrieves the currently-active garbage collection for a registry named ` + "`" + `example-registry` + "`" + `: doctl registry garbage-collection get-active example-registry`

	runListGarbageCollectionsDesc := "Retrieves a list of past garbage collections for a registry. Information about each garbage collection includes:" + gcInfoIncluded
	cmdListGarbageCollections := CmdBuilder(
		cmd,
		RunListGarbageCollections,
		"list",
		"Retrieve information about past garbage collections for a container registry",
		runListGarbageCollectionsDesc,
		Writer,
		aliasOpt("ls", "l"),
		displayerType(&displayers.GarbageCollection{}),
	)
	cmdListGarbageCollections.overrideNS = overrideNS
	cmdListGarbageCollections.Example = `The following example retrieves a list of past garbage collections for a registry: doctl registry garbage-collection list
	
The following example retrieves a list of past garbage collections for a registry named ` + "`" + `example-registry` + "`" + `: doctl registry garbage-collection list example-registry`

	runCancelGarbageCollectionDesc := "Cancels the currently-active garbage collection for a container registry."
	cmdCancelGarbageCollection := CmdBuilder(
		cmd,
		RunCancelGarbageCollection,
		"cancel",
		"Cancel the currently-active garbage collection for a container registry",
		runCancelGarbageCollectionDesc,
		Writer,
		aliasOpt("c"),
	)
	cmdCancelGarbageCollection.overrideNS = overrideNS
	cmdCancelGarbageCollection.Example = `The following example cancels the garbage collection with the uuid` + "`" + `gc-uuid` + "`" + `for a registry: doctl registry garbage-collection cancel gc-uuid
	
The following example cancels the garbage collection with the uuid ` + "`" + `gc-uuid` + "`" + ` for a registry named ` + "`" + `example-registry` + "`" + `: doctl registry garbage-collection cancel example-registry gc-uuid`

	return cmd
}

// RegistryOptions creates the registry options subcommand
func RegistryOptions() *Command {
	cmd := &Command{
		Command: &cobra.Command{
			Use:     "options",
			Aliases: []string{"opts", "o"},
			Short:   "List available container registry options",
			Long:    "This command lists options available when creating or updating a container registry.",
		},
	}

	overrideNS := "registry.options"

	tiersDesc := "Lists available container registry subscription tiers"
	cmdRegistryOptionTiers := CmdBuilder(cmd, RunRegistryOptionsTiers, "subscription-tiers", tiersDesc, tiersDesc, Writer, aliasOpt("tiers"))
	cmdRegistryOptionTiers.overrideNS = overrideNS
	regionsDesc := "Lists available container registry regions"
	cmdGetRegistryOptionsRegions := CmdBuilder(cmd, RunGetRegistryOptionsRegions, "available-regions", regionsDesc, regionsDesc, Writer, aliasOpt("regions"))
	cmdGetRegistryOptionsRegions.overrideNS = overrideNS

	return cmd
}

// Registry Run Commands

// RunRegistryCreate creates a registry
func RunRegistryCreate(c *CmdConfig) error {
	err := ensureOneArg(c)
	if err != nil {
		return err
	}

	name := c.Args[0]
	subscriptionTier, err := c.Doit.GetString(c.NS, doctl.ArgSubscriptionTier)
	if err != nil {
		return err
	}
	region, err := c.Doit.GetString(c.NS, doctl.ArgRegionSlug)
	if err != nil {
		return err
	}

	rs := c.Registry()

	rcr := &godo.RegistryCreateRequest{
		Name:                 name,
		SubscriptionTierSlug: subscriptionTier,
		Region:               region,
	}
	r, err := rs.Create(rcr)
	if err != nil {
		return err
	}

	return displayRegistries(c, *r)
}

// RunRegistryGet returns the registry
func RunRegistryGet(c *CmdConfig) error {
	reg, err := c.Registry().Get()
	if err != nil {
		if is412Error(err) {
			// List available registries to show in the error message
			registries, listErr := c.Registry().List()
			if listErr == nil && len(registries) > 0 {
				names := make([]string, len(registries))
				for i, r := range registries {
					names[i] = r.Name
				}
				return fmt.Errorf("multiple registries found. Please use 'doctl registries get <registry-name>' instead.\nAvailable registries: %s", strings.Join(names, ", "))
			}
			return fmt.Errorf("multiple registries found. Please use 'doctl registries get <registry-name>' instead")
		}
		return err
	}

	return displayRegistries(c, *reg)
}

// RunRegistryDelete delete the registry
func RunRegistryDelete(c *CmdConfig) error {
	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
	if err != nil {
		return err
	}

	if !force && AskForConfirm("delete registry") != nil {
		return fmt.Errorf("operation aborted")
	}

	return c.Registry().Delete()
}

// store execCommand in a variable. Lets us override it while testing
var execCommand = exec.Command

// RunRegistryLogin logs in Docker to the registry
func RunRegistryLogin(c *CmdConfig) error {
	expirySeconds, err := c.Doit.GetInt(c.NS, doctl.ArgRegistryExpirySeconds)
	if err != nil {
		return err
	}
	neverExpire, err := c.Doit.GetBool(c.NS, doctl.ArgRegistryNeverExpire)
	if err != nil {
		return err
	}
	if neverExpire && expirySeconds > 0 {
		return errExpiryTimeAndNeverExpire
	}
	readOnly, err := c.Doit.GetBool(c.NS, doctl.ArgRegistryReadOnly)
	if err != nil {
		return err
	}

	regCredReq := godo.RegistryDockerCredentialsRequest{
		ReadWrite:     !readOnly,
		ExpirySeconds: godo.PtrTo(defaultRegistryAPITokenExpirySeconds),
	}
	if expirySeconds != 0 {
		regCredReq.ExpirySeconds = godo.PtrTo(expirySeconds)
	}
	if neverExpire {
		regCredReq.ExpirySeconds = nil
	}

	fmt.Printf("Logging Docker in to %s\n", c.Registry().Endpoint())
	creds, err := c.Registry().DockerCredentials(&regCredReq)
	if err != nil {
		return err
	}
	if expirySeconds == 0 && !neverExpire {
		notice("Login valid for 30 days. Use the --expiry-seconds flag to set a shorter expiration or --never-expire for no expiration.")
	}

	var dc dockerConfig
	err = json.Unmarshal(creds.DockerConfigJSON, &dc)
	if err != nil {
		return err
	}

	// read the login credentials from the docker config
	for host, conf := range dc.Auths {
		// decode and split into username + password
		creds, err := base64.StdEncoding.DecodeString(conf.Auth)
		if err != nil {
			return err
		}

		splitCreds := strings.Split(string(creds), ":")
		if len(splitCreds) != 2 {
			return fmt.Errorf("got invalid docker credentials")
		}
		user, pass := splitCreds[0], splitCreds[1]

		authconfig := configtypes.AuthConfig{
			Username:      user,
			Password:      pass,
			ServerAddress: host,
		}

		cf := dockerconf.LoadDefaultConfigFile(os.Stderr)
		dockerCreds := cf.GetCredentialsStore(authconfig.ServerAddress)
		err = dockerCreds.Store(authconfig)
		if err != nil {
			_, isSnap := os.LookupEnv("SNAP")
			if os.IsPermission(err) && isSnap {
				warn("Using the doctl Snap? Grant access to the doctl:dot-docker plug to use this command with: sudo snap connect doctl:dot-docker")
				return err
			}

			return err
		}

		err = cf.Save()
		if err != nil {
			return err
		}
	}

	return nil
}

// RunKubernetesManifest prints a Kubernetes manifest that provides read/pull access to the registry
func RunKubernetesManifest(c *CmdConfig) error {
	secretName, err := c.Doit.GetString(c.NS, doctl.ArgObjectName)
	if err != nil {
		return err
	}
	secretNamespace, err := c.Doit.GetString(c.NS, doctl.ArgObjectNamespace)
	if err != nil {
		return err
	}

	// if no secret name supplied, use the registry name
	if secretName == "" {
		reg, err := c.Registry().Get()
		if err != nil {
			return err
		}
		secretName = "registry-" + reg.Name
	}

	// fetch docker config
	dockerCreds, err := c.Registry().DockerCredentials(&godo.RegistryDockerCredentialsRequest{
		ReadWrite: false,
	})
	if err != nil {
		return err
	}
	annotations := map[string]string{}

	if secretNamespace == k8smetav1.NamespaceSystem {
		annotations[DOSecretOperatorAnnotation] = secretName
	}

	// create the manifest for the secret
	secret := &k8sapiv1.Secret{
		TypeMeta: k8smetav1.TypeMeta{
			Kind:       "Secret",
			APIVersion: "v1",
		},
		ObjectMeta: k8smetav1.ObjectMeta{
			Name:        secretName,
			Namespace:   secretNamespace,
			Annotations: annotations,
		},
		Type: k8sapiv1.SecretTypeDockerConfigJson,
		Data: map[string][]byte{
			".dockerconfigjson": dockerCreds.DockerConfigJSON,
		},
	}

	serializer := k8sjson.NewSerializerWithOptions(
		k8sjson.DefaultMetaFactory, nil, nil,
		k8sjson.SerializerOptions{
			Yaml:   true,
			Pretty: true,
			Strict: true,
		},
	)

	return serializer.Encode(secret, c.Out)
}

// RunDockerConfig generates credentials and prints a Docker config that can be
// used to authenticate a Docker client with the registry.
func RunDockerConfig(c *CmdConfig) error {
	readWrite, err := c.Doit.GetBool(c.NS, doctl.ArgReadWrite)
	if err != nil {
		return err
	}
	expirySeconds, err := c.Doit.GetInt(c.NS, doctl.ArgRegistryExpirySeconds)
	if err != nil {
		return err
	}
	regCredReq := godo.RegistryDockerCredentialsRequest{
		ReadWrite: readWrite,
	}
	if expirySeconds != 0 {
		regCredReq.ExpirySeconds = godo.PtrTo(expirySeconds)
	}

	dockerCreds, err := c.Registry().DockerCredentials(&regCredReq)
	if err != nil {
		return err
	}

	_, err = c.Out.Write(append(dockerCreds.DockerConfigJSON, '\n'))
	return err
}

// RunRegistryLogout logs Docker out of the registry
func RunRegistryLogout(c *CmdConfig) error {
	endpoint, err := c.Doit.GetString(c.NS, doctl.ArgRegistryAuthorizationServerEndpoint)
	if err != nil {
		return err
	}

	server := c.Registry().Endpoint()
	fmt.Printf("Removing login credentials for %s\n", server)

	cf := dockerconf.LoadDefaultConfigFile(os.Stderr)
	dockerCreds := cf.GetCredentialsStore(server)
	authConfig, err := dockerCreds.Get(server)
	if err != nil {
		return err
	}

	err = dockerCreds.Erase(server)
	if err != nil {
		_, isSnap := os.LookupEnv("SNAP")
		if os.IsPermission(err) && isSnap {
			warn("Using the doctl Snap? Grant access to the doctl:dot-docker plug to use this command with: sudo snap connect doctl:dot-docker")
			return err
		}

		return err
	}

	return c.Registry().RevokeOAuthToken(authConfig.Password, endpoint)
}

// Repository Run Commands

// RunListRepositories lists repositories for the registry
func RunListRepositories(c *CmdConfig) error {
	registryName, err := getRegistryNameWithFallback(c)
	if err != nil {
		return err
	}

	repositories, err := c.Registry().ListRepositories(registryName)
	if err != nil {
		return err
	}

	return displayRepositories(c, repositories...)
}

// RunListRepositoriesV2 lists repositories for the registry
func RunListRepositoriesV2(c *CmdConfig) error {
	registryName, err := getRegistryNameWithFallback(c)
	if err != nil {
		return err
	}

	repositories, err := c.Registry().ListRepositoriesV2(registryName)
	if err != nil {
		return err
	}

	return displayRepositoriesV2(c, repositories...)
}

// RunListRepositoryTags lists tags for the repository in a registry
func RunListRepositoryTags(c *CmdConfig) error {
	err := ensureOneArg(c)
	if err != nil {
		return err
	}

	registryName, err := getRegistryNameWithFallback(c)
	if err != nil {
		return err
	}

	tags, err := c.Registry().ListRepositoryTags(registryName, c.Args[0])
	if err != nil {
		return err
	}

	return displayRepositoryTags(c, tags...)
}

// RunListRepositoryManifests lists manifests for the repository in a registry
func RunListRepositoryManifests(c *CmdConfig) error {
	err := ensureOneArg(c)
	if err != nil {
		return err
	}

	registryName, err := getRegistryNameWithFallback(c)
	if err != nil {
		return err
	}

	manifests, err := c.Registry().ListRepositoryManifests(registryName, c.Args[0])
	if err != nil {
		return err
	}

	return displayRepositoryManifests(c, manifests...)
}

// RunRepositoryDeleteTag deletes one or more repository tags
func RunRepositoryDeleteTag(c *CmdConfig) error {
	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
	if err != nil {
		return err
	}

	if len(c.Args) < 2 {
		return doctl.NewMissingArgsErr(c.NS)
	}

	registryName, err := getRegistryNameWithFallback(c)
	if err != nil {
		return err
	}

	repository := c.Args[0]
	tags := c.Args[1:]

	if !force && AskForConfirm(fmt.Sprintf("delete %d repository tag(s)", len(tags))) != nil {
		return fmt.Errorf("operation aborted")
	}

	var errors []string
	for _, tag := range tags {
		if err := c.Registry().DeleteTag(registryName, repository, tag); err != nil {
			errors = append(errors, err.Error())
		}
	}

	if len(errors) > 0 {
		return fmt.Errorf("failed to delete all repository tags: \n%s", strings.Join(errors, "\n"))
	}

	return nil
}

// RunRepositoryDeleteManifest deletes one or more repository manifests by digest
func RunRepositoryDeleteManifest(c *CmdConfig) error {
	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
	if err != nil {
		return err
	}

	if len(c.Args) < 2 {
		return doctl.NewMissingArgsErr(c.NS)
	}

	registryName, err := getRegistryNameWithFallback(c)
	if err != nil {
		return err
	}

	repository := c.Args[0]
	digests := c.Args[1:]

	if !force && AskForConfirm(fmt.Sprintf("delete %d repository manifest(s) by digest (including associated tags)", len(digests))) != nil {
		return fmt.Errorf("operation aborted")
	}

	var errors []string
	for _, digest := range digests {
		if err := c.Registry().DeleteManifest(registryName, repository, digest); err != nil {
			errors = append(errors, err.Error())
		}
	}

	if len(errors) > 0 {
		return fmt.Errorf("failed to delete all repository manifests: \n%s", strings.Join(errors, "\n"))
	}

	return nil
}

// is412Error checks if an error is a 412 (Precondition Failed) response, which indicates
// multiple registries exist and a specific registry name is required
func is412Error(err error) bool {
	if err == nil {
		return false
	}
	// Check if it's a godo ErrorResponse with status 412
	if errResp, ok := err.(*godo.ErrorResponse); ok {
		return errResp.Response != nil && errResp.Response.StatusCode == 412
	}
	return false
}

// getRegistryNameWithFallback attempts to get a registry name from the flag first.
// If not provided, it falls back to using c.Registry().Get(), but provides a helpful
// error message if that fails due to multiple registries (412 error).
func getRegistryNameWithFallback(c *CmdConfig) (string, error) {
	registryName, err := c.Doit.GetString(c.NS, doctl.ArgRegistry)
	if err != nil {
		return "", err
	}

	if registryName != "" {
		return registryName, nil
	}

	// No registry name provided, try to get the default registry
	registry, err := c.Registry().Get()
	if err != nil {
		if is412Error(err) {
			// List available registries to show in the error message
			registries, listErr := c.Registry().List()
			if listErr == nil && len(registries) > 0 {
				names := make([]string, len(registries))
				for i, r := range registries {
					names[i] = r.Name
				}
				return "", fmt.Errorf("multiple registries found. Please specify one with --registry <name>.\nAvailable registries: %s", strings.Join(names, ", "))
			}
			return "", fmt.Errorf("multiple registries found. Please specify one with --registry <name>")
		}
		return "", fmt.Errorf("failed to get registry: %w", err)
	}

	return registry.Name, nil
}

// getRegistryNameFromArgs gets a registry name from command line args if provided,
// otherwise falls back to getting the default registry. This is used by commands
// that accept an optional positional registry argument.
func getRegistryNameFromArgs(c *CmdConfig, args []string, argIndex int) (string, error) {
	if len(args) > argIndex && args[argIndex] != "" {
		return args[argIndex], nil
	}

	// No registry name in args, try to get the default registry
	registry, err := c.Registry().Get()
	if err != nil {
		if is412Error(err) {
			// List available registries to show in the error message
			registries, listErr := c.Registry().List()
			if listErr == nil && len(registries) > 0 {
				names := make([]string, len(registries))
				for i, r := range registries {
					names[i] = r.Name
				}
				return "", fmt.Errorf("multiple registries found. Please specify registry as an argument.\nAvailable registries: %s", strings.Join(names, ", "))
			}
			return "", fmt.Errorf("multiple registries found. Please specify registry as an argument")
		}
		return "", fmt.Errorf("failed to get registry: %w", err)
	}

	return registry.Name, nil
}

func displayRegistries(c *CmdConfig, registries ...do.Registry) error {
	item := &displayers.Registry{
		Registries: registries,
	}
	return c.Display(item)
}

func displayRepositories(c *CmdConfig, repositories ...do.Repository) error {
	item := &displayers.Repository{
		Repositories: repositories,
	}
	return c.Display(item)
}

func displayRepositoriesV2(c *CmdConfig, repositories ...do.RepositoryV2) error {
	item := &displayers.RepositoryV2{
		Repositories: repositories,
	}
	return c.Display(item)
}

func displayRepositoryTags(c *CmdConfig, tags ...do.RepositoryTag) error {
	item := &displayers.RepositoryTag{
		Tags: tags,
	}
	return c.Display(item)
}

func displayRepositoryManifests(c *CmdConfig, manifests ...do.RepositoryManifest) error {
	item := &displayers.RepositoryManifest{
		Manifests: manifests,
	}
	return c.Display(item)
}

// Garbage Collection run commands

// RunStartGarbageCollection starts a garbage collection for the specified
// registry.
func RunStartGarbageCollection(c *CmdConfig) error {
	var registryName string
	var err error
	// we anticipate supporting multiple registries in the future by allowing the
	// user to specify a registry argument on the command line but defaulting to
	// the default registry returned by c.Registry().Get()
	if len(c.Args) == 0 {
		registryName, err = getRegistryNameFromArgs(c, c.Args, 0)
		if err != nil {
			return err
		}
	} else if len(c.Args) == 1 {
		registryName = c.Args[0]
	} else {
		return doctl.NewTooManyArgsErr(c.NS)
	}

	includeUntaggedManifests, err := c.Doit.GetBool(c.NS, doctl.ArgGCIncludeUntaggedManifests)
	if err != nil {
		return err
	}

	excludeUnreferencedBlobs, err := c.Doit.GetBool(c.NS, doctl.ArgGCExcludeUnreferencedBlobs)
	if err != nil {
		return err
	}

	gcStartRequest := &godo.StartGarbageCollectionRequest{}
	if includeUntaggedManifests && !excludeUnreferencedBlobs {
		gcStartRequest.Type = godo.GCTypeUntaggedManifestsAndUnreferencedBlobs
	} else if includeUntaggedManifests && excludeUnreferencedBlobs {
		gcStartRequest.Type = godo.GCTypeUntaggedManifestsOnly
	} else if !includeUntaggedManifests && !excludeUnreferencedBlobs {
		gcStartRequest.Type = godo.GCTypeUnreferencedBlobsOnly
	} else {
		return fmt.Errorf("incompatible combination of include-untagged-manifests and exclude-unreferenced-blobs flags")
	}

	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
	if err != nil {
		return err
	}

	msg := "run garbage collection -- this will put your registry in read-only mode until it finishes"

	if !force && AskForConfirm(msg) != nil {
		return errOperationAborted
	}

	gc, err := c.Registry().StartGarbageCollection(registryName, gcStartRequest)
	if err != nil {
		return err
	}

	return displayGarbageCollections(c, *gc)
}

// RunGetGarbageCollection gets the specified registry's currently-active
// garbage collection.
func RunGetGarbageCollection(c *CmdConfig) error {
	var registryName string
	var err error
	// we anticipate supporting multiple registries in the future by allowing the
	// user to specify a registry argument on the command line but defaulting to
	// the default registry returned by c.Registry().Get()
	if len(c.Args) == 0 {
		registryName, err = getRegistryNameFromArgs(c, c.Args, 0)
		if err != nil {
			return err
		}
	} else if len(c.Args) == 1 {
		registryName = c.Args[0]
	} else {
		return doctl.NewTooManyArgsErr(c.NS)
	}

	gc, err := c.Registry().GetGarbageCollection(registryName)
	if err != nil {
		return err
	}

	return displayGarbageCollections(c, *gc)
}

// RunListGarbageCollections gets the specified registry's currently-active
// garbage collection.
func RunListGarbageCollections(c *CmdConfig) error {
	var registryName string
	var err error
	// we anticipate supporting multiple registries in the future by allowing the
	// user to specify a registry argument on the command line but defaulting to
	// the default registry returned by c.Registry().Get()
	if len(c.Args) == 0 {
		registryName, err = getRegistryNameFromArgs(c, c.Args, 0)
		if err != nil {
			return err
		}
	} else if len(c.Args) == 1 {
		registryName = c.Args[0]
	} else {
		return doctl.NewTooManyArgsErr(c.NS)
	}

	gcs, err := c.Registry().ListGarbageCollections(registryName)
	if err != nil {
		return err
	}

	return displayGarbageCollections(c, gcs...)
}

// RunCancelGarbageCollection gets the specified registry's currently-active
// garbage collection.
func RunCancelGarbageCollection(c *CmdConfig) error {
	var (
		registryName string
		gcUUID       string
		err          error
	)

	if len(c.Args) == 0 {
		return doctl.NewMissingArgsErr(c.NS)
	} else if len(c.Args) == 1 { // <gc-uuid>
		gcUUID = c.Args[0]
	} else if len(c.Args) == 2 { // <registry-name> <gc-uuid>
		registryName = c.Args[0]
		gcUUID = c.Args[1]
	} else {
		return doctl.NewTooManyArgsErr(c.NS)
	}

	// we anticipate supporting multiple registries in the future by allowing the
	// user to specify a registry argument on the command line but defaulting to
	// the default registry returned by c.Registry().Get()
	if registryName == "" {
		registryName, err = getRegistryNameFromArgs(c, []string{}, 0)
		if err != nil {
			return err
		}
	}

	_, err = c.Registry().CancelGarbageCollection(registryName, gcUUID)
	if err != nil {
		return err
	}

	return nil
}

func displayGarbageCollections(c *CmdConfig, garbageCollections ...do.GarbageCollection) error {
	item := &displayers.GarbageCollection{
		GarbageCollections: garbageCollections,
	}
	return c.Display(item)
}

func RunRegistryOptionsTiers(c *CmdConfig) error {
	tiers, err := c.Registry().GetSubscriptionTiers()
	if err != nil {
		return err
	}

	item := &displayers.RegistrySubscriptionTiers{
		SubscriptionTiers: tiers,
	}
	return c.Display(item)
}

func RunGetRegistryOptionsRegions(c *CmdConfig) error {
	regions, err := c.Registry().GetAvailableRegions()
	if err != nil {
		return err
	}

	item := &displayers.RegistryAvailableRegions{
		Regions: regions,
	}
	return c.Display(item)
}

// Registries creates the registries sub-command
func Registries() *Command {
	cmd := &Command{
		Command: &cobra.Command{
			Use:     "registries",
			Aliases: []string{"regs", "rs"},
			Short:   "Display commands for working with multiple container registries",
			Long:    "The subcommands of `doctl registries` allow you to manage multiple registries.",
			GroupID: manageResourcesGroup,
		},
	}

	listRegistriesDesc := `Lists information about all registries in your account, including:
  - The registry name
  - The endpoint used to access it
  - The region where it resides
`
	cmdRunRegistriesList := CmdBuilder(
		cmd,
		RunRegistriesList, "list",
		"List all registries in your account", listRegistriesDesc,
		Writer, aliasOpt("ls"), displayerType(&displayers.Registry{}),
	)
	cmdRunRegistriesList.Example = `The following example lists all registries in your account: doctl registries list`

	createRegistriesDesc := `Creates a new registry in your account. The registry name must be unique and can contain only lowercase letters, numbers, and hyphens.`
	cmdRunRegistriesCreate := CmdBuilder(
		cmd,
		RunRegistriesCreate, "create <registry-name>",
		"Create a private container registry",
		createRegistriesDesc,
		Writer, aliasOpt("c"),
	)
	AddStringFlag(cmdRunRegistriesCreate, doctl.ArgSubscriptionTier, "", "basic",
		"Subscription tier for the new registry. For a list of possible values, use the `doctl registries options subscription-tiers` command.", requiredOpt())
	AddStringFlag(cmdRunRegistriesCreate, doctl.ArgRegionSlug, "", "",
		"A `slug` indicating which datacenter region the registry reside in. For a list of supported region slugs, use the `doctl registries options available-regions` command")
	cmdRunRegistriesCreate.Example = `The following example creates a registry named ` + "`" + `example-registry` + "`" + ` in the NYC3 region: doctl registries create example-registry --region=nyc3`

	getRegistryDesc := `Retrieves information about a registry in your account, including:
  - The registry name
  - The endpoint used to access it
  - The region where it resides
`
	cmdRunRegistriesGet := CmdBuilder(
		cmd,
		RunRegistriesGet, "get <registry-name>",
		"Get information about a registry",
		getRegistryDesc,
		Writer, aliasOpt("g"), displayerType(&displayers.Registry{}),
	)
	cmdRunRegistriesGet.Example = `The following example retrieves information about a registry named ` + "`" + `example-registry` + "`" + `: doctl registries get example-registry`

	deleteRegistryDesc := "Permanently deletes a registry from your account."
	cmdRunRegistriesDelete := CmdBuilder(
		cmd,
		RunRegistriesDelete, "delete <registry-name>",
		"Delete a registry",
		deleteRegistryDesc,
		Writer, aliasOpt("d", "del", "rm"),
	)
	AddBoolFlag(cmdRunRegistriesDelete, doctl.ArgForce, doctl.ArgShortForce, false, "Delete registry without confirmation prompt")
	cmdRunRegistriesDelete.Example = `The following example deletes a registry named ` + "`" + `example-registry` + "`" + `: doctl registries delete example-registry. Note that you can delete only one registry at a time.`

	dockerConfigDesc := `Outputs a JSON-formatted Docker configuration that you can use to configure a Docker client to authenticate with your private container registry. This configuration is useful for configuring third-party tools that need access to your registry.

By default this command generates read-only credentials. Use the ` + "`" + `--read-write` + "`" + ` flag to generate credentials that can push. The configuration produced by this command contains a DigitalOcean API token that can be used to access your account and should be treated as sensitive information.`

	cmdRunRegistriesDockerConfig := CmdBuilder(cmd, RunRegistriesDockerConfig, "docker-config <registry-name>",
		"Generate a Docker auth configuration for a registry",
		dockerConfigDesc, Writer, aliasOpt("config"))
	AddBoolFlag(cmdRunRegistriesDockerConfig, doctl.ArgReadWrite, "", false,
		"Generates credentials that can push to your registry")
	AddIntFlag(cmdRunRegistriesDockerConfig, doctl.ArgRegistryExpirySeconds, "", 0,
		"The length of time the registry credentials are valid for, in seconds. By default, the credentials do not expire.")
	cmdRunRegistriesDockerConfig.Example = `The following example generates a Docker configuration for a registry named ` + "`" + `example-registry` + "`" + ` and uses the ` + "`" + `--expiry-seconds` + "`" + ` to set the credentials to expire after one day: doctl registries docker-config example-registry --expiry-seconds=86400`

	// Add login command
	loginRegDesc := "Logs Docker into Container Registry making pull and push commands to your private container registry authenticated."
	cmdRegistriesLogin := CmdBuilder(cmd, RunRegistriesLogin, "login <registry-name>", "Log in Docker to a container registry",
		loginRegDesc, Writer)
	AddIntFlag(cmdRegistriesLogin, doctl.ArgRegistryExpirySeconds, "", 0,
		"The length of time the registry credentials are valid for, in seconds. By default, the credentials expire after 30 days.")
	AddBoolFlag(cmdRegistriesLogin, doctl.ArgRegistryReadOnly, "", false,
		"Sets the DigitalOcean API token generated by the login command to read-only, causing any push operations to fail. By default, the API token is read-write.")
	AddBoolFlag(cmdRegistriesLogin, doctl.ArgRegistryNeverExpire, "", false,
		"Sets the DigitalOcean API token generated by the login command to never expire. By default, this is set to false.")
	cmdRegistriesLogin.Example = `The following example logs Docker into a registry named ` + "`" + `example-registry` + "`" + ` and provides Docker with read-only credentials: doctl registries login example-registry --read-only=true`

	// Add logout command
	logoutRegDesc := "This command logs Docker out of the private container registry, revoking access to it."
	cmdRunRegistriesLogout := CmdBuilder(cmd, RunRegistriesLogout, "logout <registry-name>", "Log out Docker from a container registry",
		logoutRegDesc, Writer)
	AddStringFlag(cmdRunRegistriesLogout, doctl.ArgRegistryAuthorizationServerEndpoint, "", oauthTokenRevokeEndpoint, "The endpoint of the OAuth authorization server used to revoke credentials on logout.")
	cmdRunRegistriesLogout.Example = `The following example logs Docker out of a registry named ` + "`" + `example-registry` + "`" + `: doctl registries logout example-registry`

	// Add kubernetes-manifest command
	kubeManifestDesc := `Outputs a YAML-formatted Kubernetes secret manifest that can be used to grant a Kubernetes cluster pull access to your private container registry.

By default, the secret manifest is applied to all the namespaces for the Kubernetes cluster using the DOSecret operator. The DOSecret operator is available on clusters running version 1.15.12-do.2 or greater. For older clusters, or to restrict the secret to a specific namespace, use the ` + "`" + `--namespace` + "`" + ` flag.

You can redirect the command's output to a file to save the manifest for later use or pipe it directly to ` + "`" + `kubectl` + "`" + ` to create the secret in your cluster:

    doctl registries kubernetes-manifest example-registry | kubectl apply -f -
`
	cmdRunRegistriesKubernetesManifest := CmdBuilder(cmd, RunRegistriesKubernetesManifest, "kubernetes-manifest <registry-name>",
		"Generate a Kubernetes secret manifest for a registry.",
		kubeManifestDesc, Writer, aliasOpt("k8s"))
	AddStringFlag(cmdRunRegistriesKubernetesManifest, doctl.ArgObjectName, "", "",
		"The secret's name. Defaults to the registry name prefixed with \"registry-\"")
	AddStringFlag(cmdRunRegistriesKubernetesManifest, doctl.ArgObjectNamespace, "",
		"kube-system", "The Kubernetes namespace to hold the secret")
	cmdRunRegistriesKubernetesManifest.Example = `The following example generates a secret manifest for a registry named ` + "`" + `example-registry` + "`" + ` and applies it to the ` + "`" + `kube-system` + "`" + ` namespace: doctl registries kubernetes-manifest example-registry --namespace=kube-system`

	// Add sub-commands
	cmd.AddCommand(RegistriesRepository())
	cmd.AddCommand(RegistriesGarbageCollection())
	cmd.AddCommand(RegistriesOptions())

	return cmd
}

// RunRegistriesList lists all registries in the account.
func RunRegistriesList(c *CmdConfig) error {
	registries, err := c.Registries().List()
	if err != nil {
		return err
	}

	return displayRegistries(c, registries...)
}

// RunRegistriesCreate creates a new registry.
func RunRegistriesCreate(c *CmdConfig) error {
	err := ensureOneArg(c)
	if err != nil {
		return err
	}

	name := c.Args[0]
	subscriptionTier, err := c.Doit.GetString(c.NS, doctl.ArgSubscriptionTier)
	if err != nil {
		return err
	}
	region, err := c.Doit.GetString(c.NS, doctl.ArgRegionSlug)
	if err != nil {
		return err
	}

	createRequest := &godo.RegistryCreateRequest{
		Name:                 name,
		SubscriptionTierSlug: subscriptionTier,
		Region:               region,
	}
	registry, err := c.Registries().Create(createRequest)
	if err != nil {
		return err
	}

	return displayRegistries(c, *registry)
}

// RunRegistriesGet retrieves information about specific registries.
func RunRegistriesGet(c *CmdConfig) error {
	if len(c.Args) < 1 {
		return doctl.NewMissingArgsErr(c.NS)
	}

	var registries []do.Registry
	var errors []string

	for _, name := range c.Args {
		registry, err := c.Registries().Get(name)
		if err != nil {
			errors = append(errors, fmt.Sprintf("failed to fetch registry %s: %v", name, err))
			continue
		}
		registries = append(registries, *registry)
	}

	if len(registries) > 0 {
		if err := displayRegistries(c, registries...); err != nil {
			return err
		}
	}

	if len(errors) > 0 {
		return fmt.Errorf("%s", strings.Join(errors, "\n"))
	}

	return nil
}

// RunRegistriesDelete deletes a specific registry.
func RunRegistriesDelete(c *CmdConfig) error {
	err := ensureOneArg(c)
	if err != nil {
		return err
	}

	name := c.Args[0]
	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
	if err != nil {
		return err
	}

	if !force && AskForConfirm("delete this registry") != nil {
		return fmt.Errorf("operation aborted")
	}

	return c.Registries().Delete(name)
}

// RunRegistriesDockerConfig generates a Docker configuration for a registry.
func RunRegistriesDockerConfig(c *CmdConfig) error {
	err := ensureOneArg(c)
	if err != nil {
		return err
	}

	registryName := c.Args[0]
	readWrite, err := c.Doit.GetBool(c.NS, doctl.ArgReadWrite)
	if err != nil {
		return err
	}
	expirySeconds, err := c.Doit.GetInt(c.NS, doctl.ArgRegistryExpirySeconds)
	if err != nil {
		return err
	}
	regCredReq := godo.RegistryDockerCredentialsRequest{
		ReadWrite: readWrite,
	}
	if expirySeconds != 0 {
		regCredReq.ExpirySeconds = godo.PtrTo(expirySeconds)
	}

	dockerCreds, err := c.Registries().DockerCredentials(registryName, &regCredReq)
	if err != nil {
		return err
	}

	_, err = c.Out.Write(append(dockerCreds.DockerConfigJSON, '\n'))
	return err
}

// RunRegistriesLogin logs in Docker to a registry.
func RunRegistriesLogin(c *CmdConfig) error {
	err := ensureOneArg(c)
	if err != nil {
		return err
	}

	registryName := c.Args[0]
	expirySeconds, err := c.Doit.GetInt(c.NS, doctl.ArgRegistryExpirySeconds)
	if err != nil {
		return err
	}
	neverExpire, err := c.Doit.GetBool(c.NS, doctl.ArgRegistryNeverExpire)
	if err != nil {
		return err
	}
	if neverExpire && expirySeconds > 0 {
		return errExpiryTimeAndNeverExpire
	}
	readOnly, err := c.Doit.GetBool(c.NS, doctl.ArgRegistryReadOnly)
	if err != nil {
		return err
	}

	regCredReq := godo.RegistryDockerCredentialsRequest{
		ReadWrite:     !readOnly,
		ExpirySeconds: godo.PtrTo(defaultRegistryAPITokenExpirySeconds),
	}
	if expirySeconds != 0 {
		regCredReq.ExpirySeconds = godo.PtrTo(expirySeconds)
	}
	if neverExpire {
		regCredReq.ExpirySeconds = nil
	}

	reg, err := c.Registries().Get(registryName)
	if err != nil {
		return err
	}

	fmt.Printf("Logging Docker in to %s\n", reg.Endpoint())
	creds, err := c.Registries().DockerCredentials(registryName, &regCredReq)
	if err != nil {
		return err
	}
	if expirySeconds == 0 && !neverExpire {
		notice("Login valid for 30 days. Use the --expiry-seconds flag to set a shorter expiration or --never-expire for no expiration.")
	}

	var dc dockerConfig
	err = json.Unmarshal(creds.DockerConfigJSON, &dc)
	if err != nil {
		return err
	}

	// read the login credentials from the docker config
	for host, conf := range dc.Auths {
		// decode and split into username + password
		creds, err := base64.StdEncoding.DecodeString(conf.Auth)
		if err != nil {
			return err
		}

		splitCreds := strings.Split(string(creds), ":")
		if len(splitCreds) != 2 {
			return fmt.Errorf("got invalid docker credentials")
		}
		user, pass := splitCreds[0], splitCreds[1]

		authconfig := configtypes.AuthConfig{
			Username:      user,
			Password:      pass,
			ServerAddress: host,
		}

		cf := dockerconf.LoadDefaultConfigFile(os.Stderr)
		dockerCreds := cf.GetCredentialsStore(authconfig.ServerAddress)
		err = dockerCreds.Store(authconfig)
		if err != nil {
			_, isSnap := os.LookupEnv("SNAP")
			if os.IsPermission(err) && isSnap {
				warn("Using the doctl Snap? Grant access to the doctl:dot-docker plug to use this command with: sudo snap connect doctl:dot-docker")
				return err
			}

			return err
		}

		err = cf.Save()
		if err != nil {
			return err
		}
	}

	return nil
}

// RunRegistriesLogout logs Docker out of a registry.
func RunRegistriesLogout(c *CmdConfig) error {
	err := ensureOneArg(c)
	if err != nil {
		return err
	}

	registryName := c.Args[0]
	endpoint, err := c.Doit.GetString(c.NS, doctl.ArgRegistryAuthorizationServerEndpoint)
	if err != nil {
		return err
	}

	reg, err := c.Registries().Get(registryName)
	if err != nil {
		return err
	}

	server := reg.Endpoint()
	fmt.Printf("Removing login credentials for %s\n", server)

	// Use the base hostname for credential lookup (same as single registry)
	// Docker credentials are stored under registry.digitalocean.com, not registry.digitalocean.com/registry-name
	baseServer := do.RegistryHostname

	cf := dockerconf.LoadDefaultConfigFile(os.Stderr)
	dockerCreds := cf.GetCredentialsStore(baseServer)
	authConfig, err := dockerCreds.Get(baseServer)
	if err != nil {
		return err
	}

	err = dockerCreds.Erase(baseServer)
	if err != nil {
		_, isSnap := os.LookupEnv("SNAP")
		if os.IsPermission(err) && isSnap {
			warn("Using the doctl Snap? Grant access to the doctl:dot-docker plug to use this command with: sudo snap connect doctl:dot-docker")
			return err
		}

		return err
	}

	// For multi-registry logout, we need to use the RegistriesService equivalent
	// Since RegistriesService doesn't have RevokeOAuthToken, we'll use the Registry service
	rs := c.Registry()
	return rs.RevokeOAuthToken(authConfig.Password, endpoint)
}

// RunRegistriesKubernetesManifest prints a Kubernetes manifest for a registry.
func RunRegistriesKubernetesManifest(c *CmdConfig) error {
	err := ensureOneArg(c)
	if err != nil {
		return err
	}

	registryName := c.Args[0]
	secretName, err := c.Doit.GetString(c.NS, doctl.ArgObjectName)
	if err != nil {
		return err
	}
	namespace, err := c.Doit.GetString(c.NS, doctl.ArgObjectNamespace)
	if err != nil {
		return err
	}

	// if no secret name supplied, use the registry name
	if secretName == "" {
		secretName = "registry-" + registryName
	}

	// fetch docker config
	regCredReq := godo.RegistryDockerCredentialsRequest{
		ReadWrite: false, // k8s secrets should be read-only
	}

	dockerCreds, err := c.Registries().DockerCredentials(registryName, &regCredReq)
	if err != nil {
		return err
	}

	// create the manifest for the secret
	secret := k8sapiv1.Secret{
		TypeMeta: k8smetav1.TypeMeta{
			APIVersion: "v1",
			Kind:       "Secret",
		},
		ObjectMeta: k8smetav1.ObjectMeta{
			Name:      secretName,
			Namespace: namespace,
			Annotations: map[string]string{
				DOSecretOperatorAnnotation: registryName,
			},
		},
		Type: k8sapiv1.SecretTypeDockerConfigJson,
		Data: map[string][]byte{
			k8sapiv1.DockerConfigJsonKey: dockerCreds.DockerConfigJSON,
		},
	}

	serializer := k8sjson.NewYAMLSerializer(k8sjson.DefaultMetaFactory, nil, nil)
	return serializer.Encode(&secret, c.Out)
}

// RegistriesRepository creates the repository sub-command for multi-registry
func RegistriesRepository() *Command {
	cmd := &Command{
		Command: &cobra.Command{
			Use:     "repository",
			Aliases: []string{"repo", "r"},
			Short:   "Display commands for working with repositories in multiple container registries",
			Long:    "The subcommands of `doctl registries repository` allow you to manage repositories in multiple registries.",
		},
	}

	overrideNS := "registries.repository"

	listRepositoriesDesc := `Retrieves information about repositories in a registry, including:
  - The repository name
  - The latest tag for the repository
  - The compressed size for the latest tag
  - The manifest digest for the latest tag
  - The last updated timestamp
`
	cmdListRepositories := CmdBuilder(
		cmd,
		RunRegistriesListRepositories, "list <registry-name>",
		"List repositories for a container registry", listRepositoriesDesc,
		Writer, aliasOpt("ls"), displayerType(&displayers.Repository{}),
		hiddenCmd(),
	)
	cmdListRepositories.overrideNS = overrideNS
	cmdListRepositories.Example = `The following example lists repositories in a registry named ` + "`" + `example-registry` + "`" + ` and uses the ` + "`" + `--format` + "`" + ` flag to return only the name and update time of each repository: doctl registries repository list example-registry --format Name,UpdatedAt`

	listRepositoriesV2Desc := `Retrieves information about repositories in a registry, including:
  - The repository name
  - The latest manifest of the repository
  - The latest manifest's latest tag, if any
  - The number of tags in the repository
  - The number of manifests in the repository
`
	cmdListRepositoriesV2 := CmdBuilder(
		cmd,
		RunRegistriesListRepositoriesV2, "list-v2 <registry-name>",
		"List repositories for a container registry", listRepositoriesV2Desc,
		Writer, aliasOpt("ls2"), displayerType(&displayers.Repository{}),
	)
	cmdListRepositoriesV2.overrideNS = overrideNS
	cmdListRepositoriesV2.Example = `The following example lists repositories in a registry named ` + "`" + `example-registry` + "`" + ` and uses the ` + "`" + `--format` + "`" + ` flag to return only the name and update time of each repository: doctl registries repository list-v2 example-registry --format Name,UpdatedAt`

	listRepositoryTagsDesc := `Retrieves information about tags in a repository, including:
  - The tag name
  - The compressed size
  - The manifest digest
  - The last updated timestamp
`
	cmdListRepositoryTags := CmdBuilder(
		cmd,
		RunRegistriesListRepositoryTags, "list-tags <registry-name> <repository>",
		"List tags for a repository in a container registry", listRepositoryTagsDesc,
		Writer, aliasOpt("lt"), displayerType(&displayers.RepositoryTag{}),
	)
	cmdListRepositoryTags.overrideNS = overrideNS
	cmdListRepositoryTags.Example = `The following example lists tags in a repository named ` + "`" + `example-repository` + "`" + ` in a registry named ` + "`" + `example-registry` + "`" + `. The command also uses the ` + "`" + `--format` + "`" + ` flag to return only the tag name and manifest digest for each tag: doctl registries repository list-tags example-registry example-repository --format Tag,ManifestDigest`

	deleteTagDesc := "Permanently deletes one or more repository tags."
	cmdRunRepositoryDeleteTag := CmdBuilder(
		cmd,
		RunRegistriesRepositoryDeleteTag,
		"delete-tag <registry-name> <repository> <tag>...",
		"Delete one or more container repository tags",
		deleteTagDesc,
		Writer,
		aliasOpt("dt"),
	)
	cmdRunRepositoryDeleteTag.overrideNS = overrideNS
	AddBoolFlag(cmdRunRepositoryDeleteTag, doctl.ArgForce, doctl.ArgShortForce, false, "Delete tag without confirmation prompt")
	cmdRunRepositoryDeleteTag.Example = `The following example deletes a tag named ` + "`" + `web` + "`" + ` from a repository named ` + "`" + `example-repository` + "`" + ` in a registry named ` + "`" + `example-registry` + "`" + `: doctl registries repository delete-tag example-registry example-repository web`

	listRepositoryManifests := `Retrieves information about manifests in a repository, including:
  - The manifest digest
  - The compressed size
  - The uncompressed size
  - The last updated timestamp
  - The manifest tags
  - The manifest blobs (available in detailed output only)
`
	cmdListRepositoryManifests := CmdBuilder(
		cmd,
		RunRegistriesListRepositoryManifests, "list-manifests <registry-name> <repository>",
		"List manifests for a repository in a container registry", listRepositoryManifests,
		Writer, aliasOpt("lm"), displayerType(&displayers.RepositoryManifest{}),
	)
	cmdListRepositoryManifests.overrideNS = overrideNS
	cmdListRepositoryManifests.Example = `The following example lists manifests in a repository named ` + "`" + `example-repository` + "`" + ` in a registry named ` + "`" + `example-registry` + "`" + `. The command also uses the ` + "`" + `--format` + "`" + ` flag to return only the digest and update time for each manifest: doctl registries repository list-manifests example-registry example-repository --format Digest,UpdatedAt`

	deleteManifestDesc := "Permanently deletes one or more repository manifests by digest."
	cmdRunRepositoryDeleteManifest := CmdBuilder(
		cmd,
		RunRegistriesRepositoryDeleteManifest,
		"delete-manifest <registry-name> <repository> <manifest-digest>...",
		"Delete one or more container repository manifests by digest",
		deleteManifestDesc,
		Writer,
		aliasOpt("dm"),
	)
	cmdRunRepositoryDeleteManifest.overrideNS = overrideNS
	AddBoolFlag(cmdRunRepositoryDeleteManifest, doctl.ArgForce, doctl.ArgShortForce, false, "Deletes manifest without confirmation prompt")
	cmdRunRepositoryDeleteManifest.Example = `The following example deletes a manifest with digest ` + "`" + `sha256:1234567890abcdef` + "`" + ` from a repository named ` + "`" + `example-repository` + "`" + ` in a registry named ` + "`" + `example-registry` + "`" + `: doctl registries repository delete-manifest example-registry example-repository sha256:a67c20e45178d90cbe686575719bd81f279b06842dc77521690e292c1eea685`

	return cmd
}

// RegistriesGarbageCollection creates the garbage-collection sub-command for multi-registry
func RegistriesGarbageCollection() *Command {
	cmd := &Command{
		Command: &cobra.Command{
			Use:     "garbage-collection",
			Aliases: []string{"gc", "g"},
			Short:   "Display commands for garbage collection for multiple container registries",
			Long:    "The subcommands of `doctl registries garbage-collection` start a garbage collection, retrieve or cancel a currently-active garbage collection, or list past garbage collections for multiple registries.",
		},
	}

	overrideNS := "registries.garbage-collection"

	runStartGarbageCollectionDesc := "Starts a garbage collection on a container registry. You can only have one active garbage collection at a time for a given registry."
	cmdStartGarbageCollection := CmdBuilder(
		cmd,
		RunRegistriesStartGarbageCollection,
		"start <registry-name>",
		"Start garbage collection for a container registry",
		runStartGarbageCollectionDesc,
		Writer,
		aliasOpt("s"),
		displayerType(&displayers.GarbageCollection{}),
	)
	cmdStartGarbageCollection.overrideNS = overrideNS
	AddBoolFlag(cmdStartGarbageCollection, doctl.ArgGCIncludeUntaggedManifests, "", false,
		"Include untagged manifests in garbage collection.")
	AddBoolFlag(cmdStartGarbageCollection, doctl.ArgGCExcludeUnreferencedBlobs, "", false,
		"Exclude unreferenced blobs from garbage collection.")
	AddBoolFlag(cmdStartGarbageCollection, doctl.ArgForce, doctl.ArgShortForce, false, "Run garbage collection without confirmation prompt")
	cmdStartGarbageCollection.Example = `The following example starts a garbage collection on a registry named ` + "`" + `example-registry` + "`" + `: doctl registries garbage-collection start example-registry`

	gcInfoIncluded := `
  - UUID
  - Status
  - Registry name
  - Create time
  - Updated at time
  - Blobs deleted
  - Freed bytes
`

	runGetGarbageCollectionDesc := "Retrieves the currently-active garbage collection for a container registry, if any active garbage collection exists. Information included about the registry includes:" + gcInfoIncluded
	cmdGetGarbageCollection := CmdBuilder(
		cmd,
		RunRegistriesGetGarbageCollection,
		"get-active <registry-name>",
		"Retrieve information about the currently-active garbage collection for a container registry",
		runGetGarbageCollectionDesc,
		Writer,
		aliasOpt("ga", "g"),
		displayerType(&displayers.GarbageCollection{}),
	)
	cmdGetGarbageCollection.overrideNS = overrideNS
	cmdGetGarbageCollection.Example = `The following example retrieves the currently-active garbage collection for a registry named ` + "`" + `example-registry` + "`" + `: doctl registries garbage-collection get-active example-registry`

	runListGarbageCollectionsDesc := "Retrieves a list of past garbage collections for a registry. Information about each garbage collection includes:" + gcInfoIncluded
	cmdListGarbageCollections := CmdBuilder(
		cmd,
		RunRegistriesListGarbageCollections,
		"list <registry-name>",
		"Retrieve information about past garbage collections for a container registry",
		runListGarbageCollectionsDesc,
		Writer,
		aliasOpt("ls", "l"),
		displayerType(&displayers.GarbageCollection{}),
	)
	cmdListGarbageCollections.overrideNS = overrideNS
	cmdListGarbageCollections.Example = `The following example retrieves a list of past garbage collections for a registry named ` + "`" + `example-registry` + "`" + `: doctl registries garbage-collection list example-registry`

	runCancelGarbageCollectionDesc := "Cancels the currently-active garbage collection for a container registry."
	cmdCancelGarbageCollection := CmdBuilder(
		cmd,
		RunRegistriesCancelGarbageCollection,
		"cancel <registry-name> <gc-uuid>",
		"Cancel the currently-active garbage collection for a container registry",
		runCancelGarbageCollectionDesc,
		Writer,
		aliasOpt("c"),
	)
	cmdCancelGarbageCollection.overrideNS = overrideNS
	cmdCancelGarbageCollection.Example = `The following example cancels the garbage collection with the uuid ` + "`" + `gc-uuid` + "`" + ` for a registry named ` + "`" + `example-registry` + "`" + `: doctl registries garbage-collection cancel example-registry gc-uuid`

	return cmd
}

// RegistriesOptions creates the registry options subcommand for multi-registry
func RegistriesOptions() *Command {
	cmd := &Command{
		Command: &cobra.Command{
			Use:     "options",
			Aliases: []string{"opts", "o"},
			Short:   "List available container registry options for multiple registries",
			Long:    "This command lists options available when creating or updating container registries.",
		},
	}

	overrideNS := "registries.options"

	tiersDesc := "Lists available container registry subscription tiers"
	cmdRegistriesOptionsTiers := CmdBuilder(cmd, RunRegistriesOptionsTiers, "subscription-tiers", tiersDesc, tiersDesc, Writer, aliasOpt("tiers"))
	cmdRegistriesOptionsTiers.overrideNS = overrideNS
	regionsDesc := "Lists available container registry regions"
	cmdRegistriesOptionRegions := CmdBuilder(cmd, RunRegistriesOptionsRegions, "available-regions", regionsDesc, regionsDesc, Writer, aliasOpt("regions"))
	cmdRegistriesOptionRegions.overrideNS = overrideNS

	return cmd
}

// Registries Run Commands

// RunRegistriesListRepositories lists repositories for the specified registry
func RunRegistriesListRepositories(c *CmdConfig) error {
	if len(c.Args) < 1 {
		return doctl.NewMissingArgsErr(c.NS)
	}

	registryName := c.Args[0]

	repos, err := c.Registries().ListRepositories(registryName)
	if err != nil {
		return err
	}

	return displayRepositories(c, repos...)
}

// RunRegistriesListRepositoriesV2 lists repositories for the specified registry using V2 API
func RunRegistriesListRepositoriesV2(c *CmdConfig) error {
	if len(c.Args) < 1 {
		return doctl.NewMissingArgsErr(c.NS)
	}

	registryName := c.Args[0]

	repos, err := c.Registries().ListRepositoriesV2(registryName)
	if err != nil {
		return err
	}

	return displayRepositoriesV2(c, repos...)
}

// RunRegistriesListRepositoryTags lists tags for the repository in a registry
func RunRegistriesListRepositoryTags(c *CmdConfig) error {
	if len(c.Args) < 2 {
		return doctl.NewMissingArgsErr(c.NS)
	}

	registryName := c.Args[0]
	repositoryName := c.Args[1]

	tags, err := c.Registries().ListRepositoryTags(registryName, repositoryName)
	if err != nil {
		return err
	}

	return displayRepositoryTags(c, tags...)
}

// RunRegistriesRepositoryDeleteTag deletes one or more repository tags
func RunRegistriesRepositoryDeleteTag(c *CmdConfig) error {
	if len(c.Args) < 3 {
		return doctl.NewMissingArgsErr(c.NS)
	}

	registryName := c.Args[0]
	repositoryName := c.Args[1]
	tags := c.Args[2:]

	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
	if err != nil {
		return err
	}

	if !force {
		return fmt.Errorf("tag deletion is destructive and irreversible, please use --force flag to continue")
	}

	for _, tag := range tags {
		err := c.Registries().DeleteTag(registryName, repositoryName, tag)
		if err != nil {
			return err
		}
	}

	fmt.Fprintf(c.Out, "Successfully deleted %d tag(s)\n", len(tags))
	return nil
}

// RunRegistriesListRepositoryManifests lists manifests for the repository in a registry
func RunRegistriesListRepositoryManifests(c *CmdConfig) error {
	if len(c.Args) < 2 {
		return doctl.NewMissingArgsErr(c.NS)
	}

	registryName := c.Args[0]
	repositoryName := c.Args[1]

	manifests, err := c.Registries().ListRepositoryManifests(registryName, repositoryName)
	if err != nil {
		return err
	}

	return displayRepositoryManifests(c, manifests...)
}

// RunRegistriesRepositoryDeleteManifest deletes one or more repository manifests by digest
func RunRegistriesRepositoryDeleteManifest(c *CmdConfig) error {
	if len(c.Args) < 3 {
		return doctl.NewMissingArgsErr(c.NS)
	}

	registryName := c.Args[0]
	repositoryName := c.Args[1]
	digests := c.Args[2:]

	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
	if err != nil {
		return err
	}

	if !force {
		return fmt.Errorf("manifest deletion is destructive and irreversible, please use --force flag to continue")
	}

	for _, digest := range digests {
		err := c.Registries().DeleteManifest(registryName, repositoryName, digest)
		if err != nil {
			return err
		}
	}

	fmt.Fprintf(c.Out, "Successfully deleted %d manifest(s)\n", len(digests))
	return nil
}

// RunRegistriesStartGarbageCollection starts a garbage collection for the specified registry
func RunRegistriesStartGarbageCollection(c *CmdConfig) error {
	if len(c.Args) < 1 {
		return doctl.NewMissingArgsErr(c.NS)
	}

	registryName := c.Args[0]

	includeUntaggedManifests, err := c.Doit.GetBool(c.NS, doctl.ArgGCIncludeUntaggedManifests)
	if err != nil {
		return err
	}

	excludeUnreferencedBlobs, err := c.Doit.GetBool(c.NS, doctl.ArgGCExcludeUnreferencedBlobs)
	if err != nil {
		return err
	}

	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
	if err != nil {
		return err
	}

	if !force {
		confirmation := fmt.Sprintf("Warning: Garbage collection is irreversible and will delete unreferenced blobs from %s. Continue?", registryName)
		if err := AskForConfirm(confirmation); err != nil {
			return err
		}
	}

	var gcType godo.GarbageCollectionType
	switch {
	case includeUntaggedManifests && excludeUnreferencedBlobs:
		gcType = godo.GCTypeUntaggedManifestsOnly
	case !includeUntaggedManifests && !excludeUnreferencedBlobs:
		gcType = godo.GCTypeUnreferencedBlobsOnly
	default:
		gcType = godo.GCTypeUntaggedManifestsAndUnreferencedBlobs
	}

	req := &godo.StartGarbageCollectionRequest{
		Type: gcType,
	}

	gc, err := c.Registries().StartGarbageCollection(registryName, req)
	if err != nil {
		return err
	}

	return displayGarbageCollections(c, *gc)
}

// RunRegistriesGetGarbageCollection gets the specified registry's currently-active garbage collection
func RunRegistriesGetGarbageCollection(c *CmdConfig) error {
	if len(c.Args) < 1 {
		return doctl.NewMissingArgsErr(c.NS)
	}

	registryName := c.Args[0]

	gc, err := c.Registries().GetGarbageCollection(registryName)
	if err != nil {
		return err
	}

	return displayGarbageCollections(c, *gc)
}

// RunRegistriesListGarbageCollections lists garbage collections for a registry
func RunRegistriesListGarbageCollections(c *CmdConfig) error {
	if len(c.Args) < 1 {
		return doctl.NewMissingArgsErr(c.NS)
	}

	registryName := c.Args[0]

	gcs, err := c.Registries().ListGarbageCollections(registryName)
	if err != nil {
		return err
	}

	return displayGarbageCollections(c, gcs...)
}

// RunRegistriesCancelGarbageCollection cancels the currently-active garbage collection for a registry
func RunRegistriesCancelGarbageCollection(c *CmdConfig) error {
	if len(c.Args) < 2 {
		return doctl.NewMissingArgsErr(c.NS)
	}

	registryName := c.Args[0]
	gcUUID := c.Args[1]

	req := &godo.UpdateGarbageCollectionRequest{
		Cancel: true,
	}

	gc, err := c.Registries().UpdateGarbageCollection(registryName, gcUUID, req)
	if err != nil {
		return err
	}

	// Check for nil pointer before dereferencing
	if gc == nil {
		return fmt.Errorf("garbage collection cancellation completed but no details returned")
	}

	return displayGarbageCollections(c, *gc)
}

// RunRegistriesOptionsTiers lists available container registry subscription tiers
func RunRegistriesOptionsTiers(c *CmdConfig) error {
	tiers, err := c.Registries().GetSubscriptionTiers()
	if err != nil {
		return err
	}

	item := &displayers.RegistrySubscriptionTiers{
		SubscriptionTiers: tiers,
	}
	return c.Display(item)
}

// RunRegistriesOptionsRegions lists available container registry regions
func RunRegistriesOptionsRegions(c *CmdConfig) error {
	regions, err := c.Registries().GetAvailableRegions()
	if err != nil {
		return err
	}

	item := &displayers.RegistryAvailableRegions{
		Regions: regions,
	}
	return c.Display(item)
}
