Repository: google/go-containerregistry
Pull request #845 (2020-11-26 17:10:47)

Refactor [g]crane to make threading options easier
I've split out some of this from https://github.com/google/go-containerregistry/pull/837

This addresses a few things:

1. `gcrane` now inherits the options/flags from `crane` (e.g. `--insecure`, `--platform`, `-v`, the `headerTransport` thing). Some of these were duplicated code, others were just broken in `gcrane`.
2. Drop most use of `init` for the cmd packages
3. Allow passing in a keychain or auth to crane (Fixes https://github.com/google/go-containerregistry/pull/653).
4. Create and thread `gcrane.Keychain` through as the default keychain for gcrane (Fixes https://github.com/google/go-containerregistry/pull/721).

I ended up passing a pointer to a slice of options through the command init logic, which feels kind of gross, but a lot less gross than having package-level globals... let me know if you have a better suggestion. I _think_ we need to do this because `PersistentPreRun` has to modify the options, but we've already created each command by the time that runs... :/ 


Patch:
diff --git a/cmd/crane/cmd/append.go b/cmd/crane/cmd/append.go
index 1c3f9e71..89b0bb25 100644
--- a/cmd/crane/cmd/append.go
+++ b/cmd/crane/cmd/append.go
@@ -24,10 +24,8 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdAppend()) }
-
 // NewCmdAppend creates a new cobra.Command for the append subcommand.
-func NewCmdAppend() *cobra.Command {
+func NewCmdAppend(options *[]crane.Option) *cobra.Command {
 	var baseRef, newTag, outFile string
 	var newLayers []string
 
@@ -44,7 +42,7 @@ func NewCmdAppend() *cobra.Command {
 				base = empty.Image
 
 			} else {
-				base, err = crane.Pull(baseRef, options...)
+				base, err = crane.Pull(baseRef, *options...)
 				if err != nil {
 					log.Fatalf("pulling %s: %v", baseRef, err)
 				}
@@ -60,7 +58,7 @@ func NewCmdAppend() *cobra.Command {
 					log.Fatalf("writing output %q: %v", outFile, err)
 				}
 			} else {
-				if err := crane.Push(img, newTag, options...); err != nil {
+				if err := crane.Push(img, newTag, *options...); err != nil {
 					log.Fatalf("pushing image %s: %v", newTag, err)
 				}
 			}
diff --git a/cmd/crane/cmd/auth.go b/cmd/crane/cmd/auth.go
index 2d4e0e37..9151401b 100644
--- a/cmd/crane/cmd/auth.go
+++ b/cmd/crane/cmd/auth.go
@@ -29,8 +29,6 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdAuth()) }
-
 // NewCmdAuth creates a new cobra.Command for the auth subcommand.
 func NewCmdAuth() *cobra.Command {
 	cmd := &cobra.Command{
diff --git a/cmd/crane/cmd/blob.go b/cmd/crane/cmd/blob.go
index 24bef194..7ddc6be3 100644
--- a/cmd/crane/cmd/blob.go
+++ b/cmd/crane/cmd/blob.go
@@ -22,10 +22,8 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdBlob()) }
-
 // NewCmdBlob creates a new cobra.Command for the blob subcommand.
-func NewCmdBlob() *cobra.Command {
+func NewCmdBlob(options *[]crane.Option) *cobra.Command {
 	return &cobra.Command{
 		Use:     "blob BLOB",
 		Short:   "Read a blob from the registry",
@@ -33,7 +31,7 @@ func NewCmdBlob() *cobra.Command {
 		Args:    cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
 			src := args[0]
-			layer, err := crane.PullLayer(src, options...)
+			layer, err := crane.PullLayer(src, *options...)
 			if err != nil {
 				log.Fatalf("pulling layer %s: %v", src, err)
 			}
diff --git a/cmd/crane/cmd/catalog.go b/cmd/crane/cmd/catalog.go
index 336a4fff..2c82af57 100644
--- a/cmd/crane/cmd/catalog.go
+++ b/cmd/crane/cmd/catalog.go
@@ -22,17 +22,15 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdCatalog()) }
-
 // NewCmdCatalog creates a new cobra.Command for the repos subcommand.
-func NewCmdCatalog() *cobra.Command {
+func NewCmdCatalog(options *[]crane.Option) *cobra.Command {
 	return &cobra.Command{
 		Use:   "catalog",
 		Short: "List the repos in a registry",
 		Args:  cobra.ExactArgs(1),
 		Run: func(_ *cobra.Command, args []string) {
 			reg := args[0]
-			repos, err := crane.Catalog(reg, options...)
+			repos, err := crane.Catalog(reg, *options...)
 			if err != nil {
 				log.Fatalf("reading repos for %s: %v", reg, err)
 			}
diff --git a/cmd/crane/cmd/config.go b/cmd/crane/cmd/config.go
index c678e134..d2339c1c 100644
--- a/cmd/crane/cmd/config.go
+++ b/cmd/crane/cmd/config.go
@@ -22,16 +22,14 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdConfig()) }
-
 // NewCmdConfig creates a new cobra.Command for the config subcommand.
-func NewCmdConfig() *cobra.Command {
+func NewCmdConfig(options *[]crane.Option) *cobra.Command {
 	return &cobra.Command{
 		Use:   "config IMAGE",
 		Short: "Get the config of an image",
 		Args:  cobra.ExactArgs(1),
 		Run: func(_ *cobra.Command, args []string) {
-			cfg, err := crane.Config(args[0], options...)
+			cfg, err := crane.Config(args[0], *options...)
 			if err != nil {
 				log.Fatalf("fetching config: %v", err)
 			}
diff --git a/cmd/crane/cmd/copy.go b/cmd/crane/cmd/copy.go
index 17d07679..993233a8 100644
--- a/cmd/crane/cmd/copy.go
+++ b/cmd/crane/cmd/copy.go
@@ -21,10 +21,8 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdCopy()) }
-
 // NewCmdCopy creates a new cobra.Command for the copy subcommand.
-func NewCmdCopy() *cobra.Command {
+func NewCmdCopy(options *[]crane.Option) *cobra.Command {
 	return &cobra.Command{
 		Use:     "copy SRC DST",
 		Aliases: []string{"cp"},
@@ -32,7 +30,7 @@ func NewCmdCopy() *cobra.Command {
 		Args:    cobra.ExactArgs(2),
 		Run: func(_ *cobra.Command, args []string) {
 			src, dst := args[0], args[1]
-			if err := crane.Copy(src, dst, options...); err != nil {
+			if err := crane.Copy(src, dst, *options...); err != nil {
 				log.Fatal(err)
 			}
 		},
diff --git a/cmd/crane/cmd/delete.go b/cmd/crane/cmd/delete.go
index ba535f06..c5839abf 100644
--- a/cmd/crane/cmd/delete.go
+++ b/cmd/crane/cmd/delete.go
@@ -21,17 +21,15 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdDelete()) }
-
 // NewCmdDelete creates a new cobra.Command for the delete subcommand.
-func NewCmdDelete() *cobra.Command {
+func NewCmdDelete(options *[]crane.Option) *cobra.Command {
 	return &cobra.Command{
 		Use:   "delete IMAGE",
 		Short: "Delete an image reference from its registry",
 		Args:  cobra.ExactArgs(1),
 		Run: func(_ *cobra.Command, args []string) {
 			ref := args[0]
-			if err := crane.Delete(ref, options...); err != nil {
+			if err := crane.Delete(ref, *options...); err != nil {
 				log.Fatalf("deleting %s: %v", ref, err)
 			}
 		},
diff --git a/cmd/crane/cmd/digest.go b/cmd/crane/cmd/digest.go
index 3076c82a..24659e41 100644
--- a/cmd/crane/cmd/digest.go
+++ b/cmd/crane/cmd/digest.go
@@ -22,16 +22,14 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdDigest()) }
-
 // NewCmdDigest creates a new cobra.Command for the digest subcommand.
-func NewCmdDigest() *cobra.Command {
+func NewCmdDigest(options *[]crane.Option) *cobra.Command {
 	return &cobra.Command{
 		Use:   "digest IMAGE",
 		Short: "Get the digest of an image",
 		Args:  cobra.ExactArgs(1),
 		Run: func(_ *cobra.Command, args []string) {
-			digest, err := crane.Digest(args[0], options...)
+			digest, err := crane.Digest(args[0], *options...)
 			if err != nil {
 				log.Fatalf("computing digest: %v", err)
 			}
diff --git a/cmd/crane/cmd/export.go b/cmd/crane/cmd/export.go
index b8ba3fbe..1c04da1b 100644
--- a/cmd/crane/cmd/export.go
+++ b/cmd/crane/cmd/export.go
@@ -22,10 +22,8 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdExport()) }
-
 // NewCmdExport creates a new cobra.Command for the export subcommand.
-func NewCmdExport() *cobra.Command {
+func NewCmdExport(options *[]crane.Option) *cobra.Command {
 	return &cobra.Command{
 		Use:   "export IMAGE TARBALL",
 		Short: "Export contents of a remote image as a tarball",
@@ -44,7 +42,7 @@ func NewCmdExport() *cobra.Command {
 			}
 			defer f.Close()
 
-			img, err := crane.Pull(src, options...)
+			img, err := crane.Pull(src, *options...)
 			if err != nil {
 				log.Fatal(err)
 			}
diff --git a/cmd/crane/cmd/list.go b/cmd/crane/cmd/list.go
index 00d356a4..f8667dbd 100644
--- a/cmd/crane/cmd/list.go
+++ b/cmd/crane/cmd/list.go
@@ -22,17 +22,15 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdList()) }
-
 // NewCmdList creates a new cobra.Command for the ls subcommand.
-func NewCmdList() *cobra.Command {
+func NewCmdList(options *[]crane.Option) *cobra.Command {
 	return &cobra.Command{
 		Use:   "ls REPO",
 		Short: "List the tags in a repo",
 		Args:  cobra.ExactArgs(1),
 		Run: func(_ *cobra.Command, args []string) {
 			repo := args[0]
-			tags, err := crane.ListTags(repo, options...)
+			tags, err := crane.ListTags(repo, *options...)
 			if err != nil {
 				log.Fatalf("reading tags for %s: %v", repo, err)
 			}
diff --git a/cmd/crane/cmd/manifest.go b/cmd/crane/cmd/manifest.go
index 1929e7b6..35df558c 100644
--- a/cmd/crane/cmd/manifest.go
+++ b/cmd/crane/cmd/manifest.go
@@ -22,17 +22,15 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdManifest()) }
-
 // NewCmdManifest creates a new cobra.Command for the manifest subcommand.
-func NewCmdManifest() *cobra.Command {
+func NewCmdManifest(options *[]crane.Option) *cobra.Command {
 	return &cobra.Command{
 		Use:   "manifest IMAGE",
 		Short: "Get the manifest of an image",
 		Args:  cobra.ExactArgs(1),
 		Run: func(_ *cobra.Command, args []string) {
 			src := args[0]
-			manifest, err := crane.Manifest(src, options...)
+			manifest, err := crane.Manifest(src, *options...)
 			if err != nil {
 				log.Fatalf("fetching manifest %s: %v", src, err)
 			}
diff --git a/cmd/crane/cmd/pull.go b/cmd/crane/cmd/pull.go
index daa181f5..b5559926 100644
--- a/cmd/crane/cmd/pull.go
+++ b/cmd/crane/cmd/pull.go
@@ -23,10 +23,8 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdPull()) }
-
 // NewCmdPull creates a new cobra.Command for the pull subcommand.
-func NewCmdPull() *cobra.Command {
+func NewCmdPull(options *[]crane.Option) *cobra.Command {
 	var cachePath, format string
 
 	cmd := &cobra.Command{
@@ -35,7 +33,7 @@ func NewCmdPull() *cobra.Command {
 		Args:  cobra.ExactArgs(2),
 		Run: func(_ *cobra.Command, args []string) {
 			src, path := args[0], args[1]
-			img, err := crane.Pull(src, options...)
+			img, err := crane.Pull(src, *options...)
 			if err != nil {
 				log.Fatal(err)
 			}
diff --git a/cmd/crane/cmd/push.go b/cmd/crane/cmd/push.go
index 29546503..fbdc6146 100644
--- a/cmd/crane/cmd/push.go
+++ b/cmd/crane/cmd/push.go
@@ -21,10 +21,8 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdPush()) }
-
 // NewCmdPush creates a new cobra.Command for the push subcommand.
-func NewCmdPush() *cobra.Command {
+func NewCmdPush(options *[]crane.Option) *cobra.Command {
 	return &cobra.Command{
 		Use:   "push TARBALL IMAGE",
 		Short: "Push image contents as a tarball to a remote registry",
@@ -36,7 +34,7 @@ func NewCmdPush() *cobra.Command {
 				log.Fatalf("loading %s as tarball: %v", path, err)
 			}
 
-			if err := crane.Push(img, tag, options...); err != nil {
+			if err := crane.Push(img, tag, *options...); err != nil {
 				log.Fatalf("pushing %s: %v", tag, err)
 			}
 		},
diff --git a/cmd/crane/cmd/rebase.go b/cmd/crane/cmd/rebase.go
index d4fdfc2f..4226223f 100644
--- a/cmd/crane/cmd/rebase.go
+++ b/cmd/crane/cmd/rebase.go
@@ -23,10 +23,8 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdRebase()) }
-
 // NewCmdRebase creates a new cobra.Command for the rebase subcommand.
-func NewCmdRebase() *cobra.Command {
+func NewCmdRebase(options *[]crane.Option) *cobra.Command {
 	var orig, oldBase, newBase, rebased string
 
 	rebaseCmd := &cobra.Command{
@@ -34,17 +32,17 @@ func NewCmdRebase() *cobra.Command {
 		Short: "Rebase an image onto a new base image",
 		Args:  cobra.NoArgs,
 		Run: func(*cobra.Command, []string) {
-			origImg, err := crane.Pull(orig, options...)
+			origImg, err := crane.Pull(orig, *options...)
 			if err != nil {
 				log.Fatalf("pulling %s: %v", orig, err)
 			}
 
-			oldBaseImg, err := crane.Pull(oldBase, options...)
+			oldBaseImg, err := crane.Pull(oldBase, *options...)
 			if err != nil {
 				log.Fatalf("pulling %s: %v", oldBase, err)
 			}
 
-			newBaseImg, err := crane.Pull(newBase, options...)
+			newBaseImg, err := crane.Pull(newBase, *options...)
 			if err != nil {
 				log.Fatalf("pulling %s: %v", newBase, err)
 			}
@@ -54,7 +52,7 @@ func NewCmdRebase() *cobra.Command {
 				log.Fatalf("rebasing: %v", err)
 			}
 
-			if err := crane.Push(img, rebased, options...); err != nil {
+			if err := crane.Push(img, rebased, *options...); err != nil {
 				log.Fatalf("pushing %s: %v", rebased, err)
 			}
 
diff --git a/cmd/crane/cmd/root.go b/cmd/crane/cmd/root.go
index 8acbad1f..75ecf892 100644
--- a/cmd/crane/cmd/root.go
+++ b/cmd/crane/cmd/root.go
@@ -22,27 +22,27 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() {
-	Root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable debug logs")
-	Root.PersistentFlags().BoolVar(&insecure, "insecure", false, "Allow image references to be fetched without TLS")
-	Root.PersistentFlags().Var(platform, "platform", "Specifies the platform in the form os/arch[/variant] (e.g. linux/amd64).")
-}
+const (
+	use   = "crane"
+	short = "Crane is a tool for managing container images"
+)
 
-var (
-	verbose  = false
-	insecure = false
-	platform = &platformValue{}
+var Root = New(use, short, []crane.Option{})
 
-	// Crane options for this invocation.
-	options = []crane.Option{}
+// New returns a top-level command for crane. This is mostly exposed
+// to share code with gcrane.
+func New(use, short string, options []crane.Option) *cobra.Command {
+	verbose := false
+	insecure := false
+	platform := &platformValue{}
 
-	// Root is the top-level cobra.Command for crane.
-	Root = &cobra.Command{
-		Use:               "crane",
-		Short:             "Crane is a tool for managing container images",
+	root := &cobra.Command{
+		Use:               use,
+		Short:             short,
 		Run:               func(cmd *cobra.Command, _ []string) { cmd.Usage() },
 		DisableAutoGenTag: true,
 		PersistentPreRun: func(cmd *cobra.Command, args []string) {
+			// TODO(jonjohnsonjr): crane.Verbose option?
 			if verbose {
 				logs.Debug.SetOutput(os.Stderr)
 			}
@@ -64,7 +64,35 @@ var (
 			}
 		},
 	}
-)
+
+	commands := []*cobra.Command{
+		NewCmdAppend(&options),
+		NewCmdBlob(&options),
+		NewCmdAuth(),
+		NewCmdCatalog(&options),
+		NewCmdConfig(&options),
+		NewCmdCopy(&options),
+		NewCmdDelete(&options),
+		NewCmdDigest(&options),
+		NewCmdExport(&options),
+		NewCmdList(&options),
+		NewCmdManifest(&options),
+		NewCmdPull(&options),
+		NewCmdPush(&options),
+		NewCmdRebase(&options),
+		NewCmdTag(&options),
+		NewCmdValidate(&options),
+		NewCmdVersion(),
+	}
+
+	root.AddCommand(commands...)
+
+	root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable debug logs")
+	root.PersistentFlags().BoolVar(&insecure, "insecure", false, "Allow image references to be fetched without TLS")
+	root.PersistentFlags().Var(platform, "platform", "Specifies the platform in the form os/arch[/variant] (e.g. linux/amd64).")
+
+	return root
+}
 
 // headerTransport sets headers on outgoing requests.
 type headerTransport struct {
diff --git a/cmd/crane/cmd/tag.go b/cmd/crane/cmd/tag.go
index 0b2411fa..9e3338f7 100644
--- a/cmd/crane/cmd/tag.go
+++ b/cmd/crane/cmd/tag.go
@@ -21,10 +21,8 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdTag()) }
-
 // NewCmdTag creates a new cobra.Command for the tag subcommand.
-func NewCmdTag() *cobra.Command {
+func NewCmdTag(options *[]crane.Option) *cobra.Command {
 	return &cobra.Command{
 		Use:   "tag IMG TAG",
 		Short: "Efficiently tag a remote image",
@@ -42,7 +40,7 @@ crane tag ubuntu v1`,
 		Args: cobra.ExactArgs(2),
 		Run: func(_ *cobra.Command, args []string) {
 			img, tag := args[0], args[1]
-			if err := crane.Tag(img, tag, options...); err != nil {
+			if err := crane.Tag(img, tag, *options...); err != nil {
 				log.Fatal(err)
 			}
 		},
diff --git a/cmd/crane/cmd/validate.go b/cmd/crane/cmd/validate.go
index 3edcdade..c3edc88f 100644
--- a/cmd/crane/cmd/validate.go
+++ b/cmd/crane/cmd/validate.go
@@ -25,10 +25,8 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdValidate()) }
-
 // NewCmdValidate creates a new cobra.Command for the validate subcommand.
-func NewCmdValidate() *cobra.Command {
+func NewCmdValidate(options *[]crane.Option) *cobra.Command {
 	var tarballPath, remoteRef string
 
 	validateCmd := &cobra.Command{
@@ -43,7 +41,7 @@ func NewCmdValidate() *cobra.Command {
 				if flag == "" {
 					continue
 				}
-				img, err := maker(flag, options...)
+				img, err := maker(flag, *options...)
 				if err != nil {
 					log.Fatalf("failed to read image %s: %v", flag, err)
 				}
diff --git a/cmd/crane/cmd/version.go b/cmd/crane/cmd/version.go
index 286108e5..b16a4de5 100644
--- a/cmd/crane/cmd/version.go
+++ b/cmd/crane/cmd/version.go
@@ -11,8 +11,6 @@ import (
 // -ldflags="-X 'github.com/google/go-containerregistry/cmd/crane/cmd.Version=$TAG'"
 var Version string
 
-func init() { Root.AddCommand(NewCmdVersion()) }
-
 // NewCmdVersion creates a new cobra.Command for the version subcommand.
 func NewCmdVersion() *cobra.Command {
 	return &cobra.Command{
diff --git a/cmd/gcrane/cmd/copy.go b/cmd/gcrane/cmd/copy.go
index 8fc6c6ef..0dc13411 100644
--- a/cmd/gcrane/cmd/copy.go
+++ b/cmd/gcrane/cmd/copy.go
@@ -23,8 +23,6 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdCopy()) }
-
 // NewCmdCopy creates a new cobra.Command for the copy subcommand.
 func NewCmdCopy() *cobra.Command {
 	recursive := false
diff --git a/cmd/gcrane/cmd/gc.go b/cmd/gcrane/cmd/gc.go
index 21867b34..8085f0f9 100644
--- a/cmd/gcrane/cmd/gc.go
+++ b/cmd/gcrane/cmd/gc.go
@@ -24,8 +24,6 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdGc()) }
-
 // NewCmdGc creates a new cobra.Command for the gc subcommand.
 func NewCmdGc() *cobra.Command {
 	recursive := false
diff --git a/cmd/gcrane/cmd/list.go b/cmd/gcrane/cmd/list.go
index 98842532..f43ec80c 100644
--- a/cmd/gcrane/cmd/list.go
+++ b/cmd/gcrane/cmd/list.go
@@ -24,8 +24,6 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func init() { Root.AddCommand(NewCmdList()) }
-
 // NewCmdList creates a new cobra.Command for the ls subcommand.
 func NewCmdList() *cobra.Command {
 	recursive := false
diff --git a/cmd/gcrane/cmd/root.go b/cmd/gcrane/cmd/root.go
deleted file mode 100644
index d1f2e256..00000000
--- a/cmd/gcrane/cmd/root.go
+++ /dev/null
@@ -1,41 +0,0 @@
-// 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 cmd
-
-import (
-	"os"
-
-	"github.com/google/go-containerregistry/pkg/logs"
-	"github.com/spf13/cobra"
-)
-
-func init() {
-	Root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable debug logs")
-}
-
-var (
-	verbose = false
-
-	// Root is the top-level cobra.Command for gcrane.
-	Root = &cobra.Command{
-		Use:               "gcrane",
-		Short:             "gcrane is a tool for managing container images on gcr.io",
-		Run:               func(cmd *cobra.Command, _ []string) { cmd.Usage() },
-		DisableAutoGenTag: true,
-		PersistentPreRun: func(cmd *cobra.Command, args []string) {
-			if verbose {
-				logs.Debug.SetOutput(os.Stderr)
-			}
-		},
-	}
-)
diff --git a/cmd/gcrane/main.go b/cmd/gcrane/main.go
index 73831fe3..62bb4a95 100644
--- a/cmd/gcrane/main.go
+++ b/cmd/gcrane/main.go
@@ -18,9 +18,12 @@ import (
 	"fmt"
 	"os"
 
-	crane "github.com/google/go-containerregistry/cmd/crane/cmd"
-	gcrane "github.com/google/go-containerregistry/cmd/gcrane/cmd"
+	"github.com/google/go-containerregistry/cmd/crane/cmd"
+	gcmd "github.com/google/go-containerregistry/cmd/gcrane/cmd"
+	"github.com/google/go-containerregistry/pkg/crane"
+	"github.com/google/go-containerregistry/pkg/gcrane"
 	"github.com/google/go-containerregistry/pkg/logs"
+	"github.com/spf13/cobra"
 )
 
 func init() {
@@ -28,21 +31,37 @@ func init() {
 	logs.Progress.SetOutput(os.Stderr)
 }
 
+const (
+	use   = "gcrane"
+	short = "gcrane is a tool for managing container images on gcr.io and pkg.dev"
+)
+
 func main() {
+	// Same as crane, but override usage and keychain.
+	root := cmd.New(use, short, []crane.Option{crane.WithAuthFromKeychain(gcrane.Keychain)})
+
+	// Add or override commands.
+	gcraneCmds := []*cobra.Command{gcmd.NewCmdList(), gcmd.NewCmdGc(), gcmd.NewCmdCopy()}
+
 	// Maintain a map of google-specific commands that we "override".
-	gcrCmds := make(map[string]bool)
-	for _, cmd := range gcrane.Root.Commands() {
-		gcrCmds[cmd.Use] = true
+	used := make(map[string]bool)
+	for _, cmd := range gcraneCmds {
+		used[cmd.Use] = true
 	}
 
-	// Use crane for everything else so that this can be a drop-in replacement.
-	for _, cmd := range crane.Root.Commands() {
-		if _, ok := gcrCmds[cmd.Use]; !ok {
-			gcrane.Root.AddCommand(cmd)
+	// Remove those from crane's set of commands.
+	for _, cmd := range root.Commands() {
+		if _, ok := used[cmd.Use]; ok {
+			root.RemoveCommand(cmd)
 		}
 	}
 
-	if err := gcrane.Root.Execute(); err != nil {
+	// Add our own.
+	for _, cmd := range gcraneCmds {
+		root.AddCommand(cmd)
+	}
+
+	if err := root.Execute(); err != nil {
 		fmt.Println(err)
 		os.Exit(1)
 	}
diff --git a/go.mod b/go.mod
index 3e618eb1..4e13809e 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
 	github.com/aws/aws-sdk-go v1.31.6 // indirect
 	github.com/containerd/containerd v1.3.0 // indirect
 	github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017
-	github.com/docker/distribution v2.7.1+incompatible // indirect
+	github.com/docker/distribution v2.7.1+incompatible
 	github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7
 	github.com/docker/docker-credential-helpers v0.6.3 // indirect
 	github.com/docker/go-connections v0.4.0 // indirect
diff --git a/pkg/crane/options.go b/pkg/crane/options.go
index 23aed96c..80852ef9 100644
--- a/pkg/crane/options.go
+++ b/pkg/crane/options.go
@@ -66,3 +66,26 @@ func WithPlatform(platform *v1.Platform) Option {
 		o.platform = platform
 	}
 }
+
+// WithAuthFromKeychain is a functional option for overriding the default
+// authenticator for remote operations, using an authn.Keychain to find
+// credentials.
+//
+// By default, crane will use authn.DefaultKeychain.
+func WithAuthFromKeychain(keys authn.Keychain) Option {
+	return func(o *options) {
+		// Replace the default keychain at position 0.
+		o.remote[0] = remote.WithAuthFromKeychain(keys)
+	}
+}
+
+// WithAuth is a functional option for overriding the default authenticator
+// for remote operations.
+//
+// By default, crane will use authn.DefaultKeychain.
+func WithAuth(auth authn.Authenticator) Option {
+	return func(o *options) {
+		// Replace the default keychain at position 0.
+		o.remote[0] = remote.WithAuth(auth)
+	}
+}
diff --git a/pkg/gcrane/copy.go b/pkg/gcrane/copy.go
index ddf9e262..d3de1aa8 100644
--- a/pkg/gcrane/copy.go
+++ b/pkg/gcrane/copy.go
@@ -32,9 +32,9 @@ import (
 	"golang.org/x/sync/errgroup"
 )
 
-// gcraneKeychain tries to use google-specific credential sources, falling back to
+// Keychain tries to use google-specific credential sources, falling back to
 // the DefaultKeychain (config-file based).
-var gcraneKeychain = authn.NewMultiKeychain(google.Keychain, authn.DefaultKeychain)
+var Keychain = authn.NewMultiKeychain(google.Keychain, authn.DefaultKeychain)
 
 // GCRBackoff returns a retry.Backoff that is suitable for use with gcr.io.
 //
@@ -61,26 +61,13 @@ func GCRBackoff() retry.Backoff {
 
 // Copy copies a remote image or index from src to dst.
 func Copy(src, dst string) error {
-	// This is a bit of a hack, but we want to use crane's copy
-	// logic with gcrane's google credentials magic.
-	original := authn.DefaultKeychain
-	authn.DefaultKeychain = gcraneKeychain
-	defer func() {
-		authn.DefaultKeychain = original
-	}()
-	return crane.Copy(src, dst)
+	// Just reuse crane's copy logic with gcrane's credential logic.
+	return crane.Copy(src, dst, crane.WithAuthFromKeychain(Keychain))
 }
 
 // CopyRepository copies everything from the src GCR repository to the
 // dst GCR repository.
 func CopyRepository(ctx context.Context, src, dst string, opts ...Option) error {
-	// This is a bit of a hack, but we want to use crane's copy
-	// logic with gcrane's google credentials magic.
-	original := authn.DefaultKeychain
-	authn.DefaultKeychain = gcraneKeychain
-	defer func() {
-		authn.DefaultKeychain = original
-	}()
 	o := makeOptions(opts...)
 	return recursiveCopy(ctx, src, dst, o.jobs)
 }
@@ -129,7 +116,7 @@ func recursiveCopy(ctx context.Context, src, dst string, jobs int) error {
 			logs.Warn.Printf("failed walkFn for repo %s: %v", repo, err)
 			// If we hit an error when listing the repo, try re-listing with backoff.
 			if err := backoffErrors(GCRBackoff(), func() error {
-				tags, err = google.List(repo, google.WithAuthFromKeychain(google.Keychain))
+				tags, err = google.List(repo, google.WithAuthFromKeychain(Keychain))
 				return err
 			}); err != nil {
 				return fmt.Errorf("failed List for repo %s: %v", repo, err)
@@ -149,7 +136,7 @@ func recursiveCopy(ctx context.Context, src, dst string, jobs int) error {
 	// Start walking the repo, enqueuing items in c.tasks.
 	g.Go(func() error {
 		defer close(c.tasks)
-		if err := google.Walk(c.srcRepo, walkFn, google.WithAuthFromKeychain(google.Keychain)); err != nil {
+		if err := google.Walk(c.srcRepo, walkFn, google.WithAuthFromKeychain(Keychain)); err != nil {
 			return fmt.Errorf("failed to Walk: %v", err)
 		}
 		return nil
@@ -186,7 +173,7 @@ func (c *copier) copyRepo(ctx context.Context, oldRepo name.Repository, tags *go
 	// Figure out what we actually need to copy.
 	want := tags.Manifests
 	have := make(map[string]google.ManifestInfo)
-	haveTags, err := google.List(newRepo, google.WithAuthFromKeychain(google.Keychain))
+	haveTags, err := google.List(newRepo, google.WithAuthFromKeychain(Keychain))
 	if err != nil {
 		if !hasStatusCode(err, http.StatusNotFound) {
 			return err
@@ -224,7 +211,7 @@ func (c *copier) copyImages(ctx context.Context, t task) error {
 		srcImg := fmt.Sprintf("%s@%s", t.oldRepo, t.digest)
 		dstImg := fmt.Sprintf("%s@%s", t.newRepo, t.digest)
 
-		return crane.Copy(srcImg, dstImg)
+		return Copy(srcImg, dstImg)
 	}
 
 	// We only need to push the whole image once.
@@ -232,7 +219,7 @@ func (c *copier) copyImages(ctx context.Context, t task) error {
 	srcImg := fmt.Sprintf("%s:%s", t.oldRepo, tag)
 	dstImg := fmt.Sprintf("%s:%s", t.newRepo, tag)
 
-	if err := crane.Copy(srcImg, dstImg); err != nil {
+	if err := Copy(srcImg, dstImg); err != nil {
 		return err
 	}
 
@@ -246,7 +233,7 @@ func (c *copier) copyImages(ctx context.Context, t task) error {
 	if err != nil {
 		return err
 	}
-	desc, err := remote.Get(srcRef, remote.WithAuthFromKeychain(google.Keychain))
+	desc, err := remote.Get(srcRef, remote.WithAuthFromKeychain(Keychain))
 	if err != nil {
 		return err
 	}
@@ -254,7 +241,7 @@ func (c *copier) copyImages(ctx context.Context, t task) error {
 	for _, tag := range t.manifest.Tags[1:] {
 		dstImg := t.newRepo.Tag(tag)
 
-		if err := remote.Tag(dstImg, desc, remote.WithAuthFromKeychain(google.Keychain)); err != nil {
+		if err := remote.Tag(dstImg, desc, remote.WithAuthFromKeychain(Keychain)); err != nil {
 			return err
 		}
 	}
diff --git a/pkg/v1/remote/image.go b/pkg/v1/remote/image.go
index 84634f72..35fee776 100644
--- a/pkg/v1/remote/image.go
+++ b/pkg/v1/remote/image.go
@@ -21,6 +21,7 @@ import (
 	"net/url"
 	"sync"
 
+	"github.com/google/go-containerregistry/pkg/internal/redact"
 	"github.com/google/go-containerregistry/pkg/name"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/google/go-containerregistry/pkg/v1/partial"
@@ -142,6 +143,9 @@ func (rl *remoteImageLayer) Compressed() (io.ReadCloser, error) {
 		return nil, err
 	}
 
+	// We don't want to log binary layers -- this can break terminals.
+	ctx := redact.NewContext(rl.ri.context, "omitting binary blobs from logs")
+
 	for _, s := range d.URLs {
 		u, err := url.Parse(s)
 		if err != nil {
@@ -161,7 +165,7 @@ func (rl *remoteImageLayer) Compressed() (io.ReadCloser, error) {
 			return nil, err
 		}
 
-		resp, err := rl.ri.Client.Do(req.WithContext(rl.ri.context))
+		resp, err := rl.ri.Client.Do(req.WithContext(ctx))
 		if err != nil {
 			lastErr = err
 			continue
diff --git a/pkg/v1/remote/transport/bearer.go b/pkg/v1/remote/transport/bearer.go
index e83819f7..5f40329a 100644
--- a/pkg/v1/remote/transport/bearer.go
+++ b/pkg/v1/remote/transport/bearer.go
@@ -24,6 +24,7 @@ import (
 	"net/url"
 	"strings"
 
+	authchallenge "github.com/docker/distribution/registry/client/auth/challenge"
 	"github.com/google/go-containerregistry/pkg/authn"
 	"github.com/google/go-containerregistry/pkg/internal/redact"
 	"github.com/google/go-containerregistry/pkg/name"
@@ -54,6 +55,14 @@ var portMap = map[string]string{
 	"https": "443",
 }
 
+func stringSet(ss []string) map[string]struct{} {
+	set := make(map[string]struct{})
+	for _, s := range ss {
+		set[s] = struct{}{}
+	}
+	return set
+}
+
 // RoundTrip implements http.RoundTripper
 func (bt *bearerTransport) RoundTrip(in *http.Request) (*http.Response, error) {
 	sendRequest := func() (*http.Response, error) {
@@ -74,8 +83,31 @@ func (bt *bearerTransport) RoundTrip(in *http.Request) (*http.Response, error) {
 		return nil, err
 	}
 
-	// Perform a token refresh() and retry the request in case the token has expired
-	if res.StatusCode == http.StatusUnauthorized {
+	// If we hit a WWW-Authenticate challenge, it might be due to expired tokens or insufficient scope.
+	if challenges := authchallenge.ResponseChallenges(res); len(challenges) != 0 {
+		for _, wac := range challenges {
+			// TODO(jonjohnsonjr): Should we also update "realm" or "service"?
+			if scope, ok := wac.Parameters["scope"]; ok {
+				// From https://tools.ietf.org/html/rfc6750#section-3
+				// The "scope" attribute is defined in Section 3.3 of [RFC6749].  The
+				// "scope" attribute is a space-delimited list of case-sensitive scope
+				// values indicating the required scope of the access token for
+				// accessing the requested resource.
+				scopes := strings.Split(scope, " ")
+
+				// Add any scopes that we don't already request.
+				got := stringSet(bt.scopes)
+				for _, want := range scopes {
+					if _, ok := got[want]; !ok {
+						bt.scopes = append(bt.scopes, want)
+					}
+				}
+			}
+		}
+
+		// TODO(jonjohnsonjr): Teach transport.Error about "error" and "error_description" from challenge.
+
+		// Retry the request to attempt to get a valid token.
 		if err = bt.refresh(in.Context()); err != nil {
 			return nil, err
 		}
diff --git a/pkg/v1/remote/transport/ping.go b/pkg/v1/remote/transport/ping.go
index 77e13a1e..6ed1d464 100644
--- a/pkg/v1/remote/transport/ping.go
+++ b/pkg/v1/remote/transport/ping.go
@@ -22,6 +22,7 @@ import (
 	"net/http"
 	"strings"
 
+	authchallenge "github.com/docker/distribution/registry/client/auth/challenge"
 	"github.com/google/go-containerregistry/pkg/name"
 )
 
@@ -105,18 +106,18 @@ func ping(ctx context.Context, reg name.Registry, t http.RoundTripper) (*pingRes
 				scheme:    scheme,
 			}, nil
 		case http.StatusUnauthorized:
-			wac := resp.Header.Get("WWW-Authenticate")
-			if parts := strings.SplitN(wac, " ", 2); len(parts) == 2 {
-				// If there are two parts, then parse the challenge parameters.
+			if challenges := authchallenge.ResponseChallenges(resp); len(challenges) != 0 {
+				// If we hit more than one, I'm not even sure what to do.
+				wac := challenges[0]
 				return &pingResp{
-					challenge:  challenge(parts[0]).Canonical(),
-					parameters: parseChallenge(parts[1]),
+					challenge:  challenge(wac.Scheme).Canonical(),
+					parameters: wac.Parameters,
 					scheme:     scheme,
 				}, nil
 			}
 			// Otherwise, just return the challenge without parameters.
 			return &pingResp{
-				challenge: challenge(wac).Canonical(),
+				challenge: challenge(resp.Header.Get("WWW-Authenticate")).Canonical(),
 				scheme:    scheme,
 			}, nil
 		default:
diff --git a/vendor/github.com/docker/distribution/registry/client/auth/challenge/addr.go b/vendor/github.com/docker/distribution/registry/client/auth/challenge/addr.go
new file mode 100644
index 00000000..2c3ebe16
--- /dev/null
+++ b/vendor/github.com/docker/distribution/registry/client/auth/challenge/addr.go
@@ -0,0 +1,27 @@
+package challenge
+
+import (
+	"net/url"
+	"strings"
+)
+
+// FROM: https://golang.org/src/net/http/http.go
+// Given a string of the form "host", "host:port", or "[ipv6::address]:port",
+// return true if the string includes a port.
+func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") }
+
+// FROM: http://golang.org/src/net/http/transport.go
+var portMap = map[string]string{
+	"http":  "80",
+	"https": "443",
+}
+
+// canonicalAddr returns url.Host but always with a ":port" suffix
+// FROM: http://golang.org/src/net/http/transport.go
+func canonicalAddr(url *url.URL) string {
+	addr := url.Host
+	if !hasPort(addr) {
+		return addr + ":" + portMap[url.Scheme]
+	}
+	return addr
+}
diff --git a/vendor/github.com/docker/distribution/registry/client/auth/challenge/authchallenge.go b/vendor/github.com/docker/distribution/registry/client/auth/challenge/authchallenge.go
new file mode 100644
index 00000000..6e3f1ccc
--- /dev/null
+++ b/vendor/github.com/docker/distribution/registry/client/auth/challenge/authchallenge.go
@@ -0,0 +1,237 @@
+package challenge
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+	"sync"
+)
+
+// Challenge carries information from a WWW-Authenticate response header.
+// See RFC 2617.
+type Challenge struct {
+	// Scheme is the auth-scheme according to RFC 2617
+	Scheme string
+
+	// Parameters are the auth-params according to RFC 2617
+	Parameters map[string]string
+}
+
+// Manager manages the challenges for endpoints.
+// The challenges are pulled out of HTTP responses. Only
+// responses which expect challenges should be added to
+// the manager, since a non-unauthorized request will be
+// viewed as not requiring challenges.
+type Manager interface {
+	// GetChallenges returns the challenges for the given
+	// endpoint URL.
+	GetChallenges(endpoint url.URL) ([]Challenge, error)
+
+	// AddResponse adds the response to the challenge
+	// manager. The challenges will be parsed out of
+	// the WWW-Authenicate headers and added to the
+	// URL which was produced the response. If the
+	// response was authorized, any challenges for the
+	// endpoint will be cleared.
+	AddResponse(resp *http.Response) error
+}
+
+// NewSimpleManager returns an instance of
+// Manger which only maps endpoints to challenges
+// based on the responses which have been added the
+// manager. The simple manager will make no attempt to
+// perform requests on the endpoints or cache the responses
+// to a backend.
+func NewSimpleManager() Manager {
+	return &simpleManager{
+		Challenges: make(map[string][]Challenge),
+	}
+}
+
+type simpleManager struct {
+	sync.RWMutex
+	Challenges map[string][]Challenge
+}
+
+func normalizeURL(endpoint *url.URL) {
+	endpoint.Host = strings.ToLower(endpoint.Host)
+	endpoint.Host = canonicalAddr(endpoint)
+}
+
+func (m *simpleManager) GetChallenges(endpoint url.URL) ([]Challenge, error) {
+	normalizeURL(&endpoint)
+
+	m.RLock()
+	defer m.RUnlock()
+	challenges := m.Challenges[endpoint.String()]
+	return challenges, nil
+}
+
+func (m *simpleManager) AddResponse(resp *http.Response) error {
+	challenges := ResponseChallenges(resp)
+	if resp.Request == nil {
+		return fmt.Errorf("missing request reference")
+	}
+	urlCopy := url.URL{
+		Path:   resp.Request.URL.Path,
+		Host:   resp.Request.URL.Host,
+		Scheme: resp.Request.URL.Scheme,
+	}
+	normalizeURL(&urlCopy)
+
+	m.Lock()
+	defer m.Unlock()
+	m.Challenges[urlCopy.String()] = challenges
+	return nil
+}
+
+// Octet types from RFC 2616.
+type octetType byte
+
+var octetTypes [256]octetType
+
+const (
+	isToken octetType = 1 << iota
+	isSpace
+)
+
+func init() {
+	// OCTET      = <any 8-bit sequence of data>
+	// CHAR       = <any US-ASCII character (octets 0 - 127)>
+	// CTL        = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
+	// CR         = <US-ASCII CR, carriage return (13)>
+	// LF         = <US-ASCII LF, linefeed (10)>
+	// SP         = <US-ASCII SP, space (32)>
+	// HT         = <US-ASCII HT, horizontal-tab (9)>
+	// <">        = <US-ASCII double-quote mark (34)>
+	// CRLF       = CR LF
+	// LWS        = [CRLF] 1*( SP | HT )
+	// TEXT       = <any OCTET except CTLs, but including LWS>
+	// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
+	//              | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
+	// token      = 1*<any CHAR except CTLs or separators>
+	// qdtext     = <any TEXT except <">>
+
+	for c := 0; c < 256; c++ {
+		var t octetType
+		isCtl := c <= 31 || c == 127
+		isChar := 0 <= c && c <= 127
+		isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
+		if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
+			t |= isSpace
+		}
+		if isChar && !isCtl && !isSeparator {
+			t |= isToken
+		}
+		octetTypes[c] = t
+	}
+}
+
+// ResponseChallenges returns a list of authorization challenges
+// for the given http Response. Challenges are only checked if
+// the response status code was a 401.
+func ResponseChallenges(resp *http.Response) []Challenge {
+	if resp.StatusCode == http.StatusUnauthorized {
+		// Parse the WWW-Authenticate Header and store the challenges
+		// on this endpoint object.
+		return parseAuthHeader(resp.Header)
+	}
+
+	return nil
+}
+
+func parseAuthHeader(header http.Header) []Challenge {
+	challenges := []Challenge{}
+	for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] {
+		v, p := parseValueAndParams(h)
+		if v != "" {
+			challenges = append(challenges, Challenge{Scheme: v, Parameters: p})
+		}
+	}
+	return challenges
+}
+
+func parseValueAndParams(header string) (value string, params map[string]string) {
+	params = make(map[string]string)
+	value, s := expectToken(header)
+	if value == "" {
+		return
+	}
+	value = strings.ToLower(value)
+	s = "," + skipSpace(s)
+	for strings.HasPrefix(s, ",") {
+		var pkey string
+		pkey, s = expectToken(skipSpace(s[1:]))
+		if pkey == "" {
+			return
+		}
+		if !strings.HasPrefix(s, "=") {
+			return
+		}
+		var pvalue string
+		pvalue, s = expectTokenOrQuoted(s[1:])
+		if pvalue == "" {
+			return
+		}
+		pkey = strings.ToLower(pkey)
+		params[pkey] = pvalue
+		s = skipSpace(s)
+	}
+	return
+}
+
+func skipSpace(s string) (rest string) {
+	i := 0
+	for ; i < len(s); i++ {
+		if octetTypes[s[i]]&isSpace == 0 {
+			break
+		}
+	}
+	return s[i:]
+}
+
+func expectToken(s string) (token, rest string) {
+	i := 0
+	for ; i < len(s); i++ {
+		if octetTypes[s[i]]&isToken == 0 {
+			break
+		}
+	}
+	return s[:i], s[i:]
+}
+
+func expectTokenOrQuoted(s string) (value string, rest string) {
+	if !strings.HasPrefix(s, "\"") {
+		return expectToken(s)
+	}
+	s = s[1:]
+	for i := 0; i < len(s); i++ {
+		switch s[i] {
+		case '"':
+			return s[:i], s[i+1:]
+		case '\\':
+			p := make([]byte, len(s)-1)
+			j := copy(p, s[:i])
+			escape := true
+			for i = i + 1; i < len(s); i++ {
+				b := s[i]
+				switch {
+				case escape:
+					escape = false
+					p[j] = b
+					j++
+				case b == '\\':
+					escape = true
+				case b == '"':
+					return string(p[:j]), s[i+1:]
+				default:
+					p[j] = b
+					j++
+				}
+			}
+			return "", ""
+		}
+	}
+	return "", ""
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 2748b327..9cf9f334 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -85,6 +85,7 @@ github.com/docker/cli/cli/config/types
 github.com/docker/distribution/digestset
 github.com/docker/distribution/reference
 github.com/docker/distribution/registry/api/errcode
+github.com/docker/distribution/registry/client/auth/challenge
 # github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7
 ## explicit
 github.com/docker/docker/api


Test patch:
diff --git a/pkg/v1/remote/transport/bearer_test.go b/pkg/v1/remote/transport/bearer_test.go
index c0f707ad..b130d664 100644
--- a/pkg/v1/remote/transport/bearer_test.go
+++ b/pkg/v1/remote/transport/bearer_test.go
@@ -159,6 +159,7 @@ func TestBearerTransportTokenRefresh(t *testing.T) {
 				w.Write([]byte(fmt.Sprintf(`{"token": %q}`, refreshedToken)))
 			}
 
+			w.Header().Set("WWW-Authenticate", "scope=foo")
 			w.WriteHeader(http.StatusUnauthorized)
 		}))
 	defer server.Close()
@@ -238,6 +239,7 @@ func TestBearerTransportOauthRefresh(t *testing.T) {
 				return
 			}
 
+			w.Header().Set("WWW-Authenticate", "scope=foo")
 			w.WriteHeader(http.StatusUnauthorized)
 		}))
 	defer server.Close()
@@ -302,6 +304,7 @@ func TestBearerTransportOauth404Fallback(t *testing.T) {
 				return
 			}
 
+			w.Header().Set("WWW-Authenticate", "scope=foo")
 			w.WriteHeader(http.StatusUnauthorized)
 		}))
 	defer server.Close()
@@ -491,3 +494,66 @@ func TestCanonicalAddressResolution(t *testing.T) {
 		}
 	}
 }
+
+func TestInsufficientScope(t *testing.T) {
+	wrong := "the-wrong-scope"
+	right := "the-right-scope"
+	realm := ""
+	expectedService := "my-service.io"
+	passed := false
+
+	server := httptest.NewServer(
+		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			query := r.URL.Query()
+
+			if scopes := query["scope"]; len(scopes) == 0 {
+				if !passed {
+					w.Header().Set("WWW-Authenticate", fmt.Sprintf("Bearer realm=%q,scope=%q", realm, right))
+					w.WriteHeader(http.StatusUnauthorized)
+				}
+			} else if len(scopes) == 1 {
+				w.Write([]byte(`{"token": "arbitrary-token"}`))
+			} else if len(scopes) == 2 && scopes[1] == right {
+				passed = true
+				w.Write([]byte(`{"token": "arbitrary-token-2"}`))
+			}
+		}))
+	defer server.Close()
+
+	basic := &authn.Basic{Username: "foo", Password: "bar"}
+	u, err := url.Parse(server.URL)
+	if err != nil {
+		t.Error("Unexpected error during url.Parse: ", err)
+	}
+	realm = u.Host
+
+	registry, err := name.NewRegistry(expectedService, name.WeakValidation)
+	if err != nil {
+		t.Error("Unexpected error during NewRegistry: ", err)
+	}
+
+	bt := &bearerTransport{
+		inner:    http.DefaultTransport,
+		basic:    basic,
+		registry: registry,
+		realm:    server.URL,
+		scopes:   []string{wrong},
+		service:  expectedService,
+		scheme:   "http",
+	}
+
+	client := http.Client{Transport: bt}
+
+	res, err := client.Get(fmt.Sprintf("http://%s/v2/foo/bar/blobs/blah", u.Host))
+	if err != nil {
+		t.Error("Unexpected error during client.Get: ", err)
+		return
+	}
+	if res.StatusCode != http.StatusOK {
+		t.Errorf("client.Get final StatusCode got %v, want: %v", res.StatusCode, http.StatusOK)
+	}
+
+	if !passed {
+		t.Error("didn't refresh insufficient scope")
+	}
+}
diff --git a/pkg/v1/remote/transport/ping_test.go b/pkg/v1/remote/transport/ping_test.go
index 4d17e810..6131cf28 100644
--- a/pkg/v1/remote/transport/ping_test.go
+++ b/pkg/v1/remote/transport/ping_test.go
@@ -124,7 +124,7 @@ func TestPingBasicChallengeNoParams(t *testing.T) {
 func TestPingBearerChallengeWithParams(t *testing.T) {
 	server := httptest.NewServer(
 		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			w.Header().Set("WWW-Authenticate", `Bearer realm="http://auth.foo.io/token`)
+			w.Header().Set("WWW-Authenticate", `Bearer realm="http://auth.example.com/token"`)
 			http.Error(w, "Unauthorized", http.StatusUnauthorized)
 		}))
 	defer server.Close()
@@ -149,7 +149,7 @@ func TestPingBearerChallengeWithParams(t *testing.T) {
 func TestUnsupportedStatus(t *testing.T) {
 	server := httptest.NewServer(
 		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			w.Header().Set("WWW-Authenticate", `Bearer realm="http://auth.foo.io/token`)
+			w.Header().Set("WWW-Authenticate", `Bearer realm="http://auth.example.com/token`)
 			http.Error(w, "Forbidden", http.StatusForbidden)
 		}))
 	defer server.Close()