Repository: google/go-containerregistry
Pull request #643 (2019-12-20 19:44:25)

Add a --json flag to gcrane ls
Sometimes, I want structured output or more info than just digest +
tags. This just returns everything so that you can use e.g. jq to
process the results.

codecov-io: # [Codecov](https://codecov.io/gh/google/go-containerregistry/pull/643?src=pr&el=h1) Report
> Merging [#643](https://codecov.io/gh/google/go-containerregistry/pull/643?src=pr&el=desc) into [master](https://codecov.io/gh/google/go-containerregistry/commit/34fb8ff33bed0fab41a607d21da5860b924ff694?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/google/go-containerregistry/pull/643/graphs/tree.svg?width=650&token=wG7iiDCdZ4&height=150&src=pr)](https://codecov.io/gh/google/go-containerregistry/pull/643?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master     #643   +/-   ##
=======================================
  Coverage   76.53%   76.53%           
=======================================
  Files         101      101           
  Lines        4483     4483           
=======================================
  Hits         3431     3431           
  Misses        636      636           
  Partials      416      416
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/google/go-containerregistry/pull/643?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/google/go-containerregistry/pull/643?src=pr&el=footer). Last update [34fb8ff...4e2d71e](https://codecov.io/gh/google/go-containerregistry/pull/643?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/cmd/crane/cmd/append.go b/cmd/crane/cmd/append.go
index 29b18785..1c3f9e71 100644
--- a/cmd/crane/cmd/append.go
+++ b/cmd/crane/cmd/append.go
@@ -18,6 +18,9 @@ import (
 	"log"
 
 	"github.com/google/go-containerregistry/pkg/crane"
+	"github.com/google/go-containerregistry/pkg/logs"
+	v1 "github.com/google/go-containerregistry/pkg/v1"
+	"github.com/google/go-containerregistry/pkg/v1/empty"
 	"github.com/spf13/cobra"
 )
 
@@ -25,21 +28,31 @@ func init() { Root.AddCommand(NewCmdAppend()) }
 
 // NewCmdAppend creates a new cobra.Command for the append subcommand.
 func NewCmdAppend() *cobra.Command {
-	var baseRef, newTag, newLayer, outFile string
+	var baseRef, newTag, outFile string
+	var newLayers []string
 
 	appendCmd := &cobra.Command{
 		Use:   "append",
 		Short: "Append contents of a tarball to a remote image",
 		Args:  cobra.NoArgs,
 		Run: func(_ *cobra.Command, args []string) {
-			base, err := crane.Pull(baseRef, options...)
-			if err != nil {
-				log.Fatalf("pulling %s: %v", baseRef, err)
+			var base v1.Image
+			var err error
+
+			if baseRef == "" {
+				logs.Warn.Printf("base unspecified, using empty image")
+				base = empty.Image
+
+			} else {
+				base, err = crane.Pull(baseRef, options...)
+				if err != nil {
+					log.Fatalf("pulling %s: %v", baseRef, err)
+				}
 			}
 
-			img, err := crane.Append(base, newLayer)
+			img, err := crane.Append(base, newLayers...)
 			if err != nil {
-				log.Fatalf("appending %s: %v", newLayer, err)
+				log.Fatalf("appending %v: %v", newLayers, err)
 			}
 
 			if outFile != "" {
@@ -55,10 +68,9 @@ func NewCmdAppend() *cobra.Command {
 	}
 	appendCmd.Flags().StringVarP(&baseRef, "base", "b", "", "Name of base image to append to")
 	appendCmd.Flags().StringVarP(&newTag, "new_tag", "t", "", "Tag to apply to resulting image")
-	appendCmd.Flags().StringVarP(&newLayer, "new_layer", "f", "", "Path to tarball to append to image")
+	appendCmd.Flags().StringSliceVarP(&newLayers, "new_layer", "f", []string{}, "Path to tarball to append to image")
 	appendCmd.Flags().StringVarP(&outFile, "output", "o", "", "Path to new tarball of resulting image")
 
-	appendCmd.MarkFlagRequired("base")
 	appendCmd.MarkFlagRequired("new_tag")
 	appendCmd.MarkFlagRequired("new_layer")
 	return appendCmd
diff --git a/cmd/crane/cmd/auth.go b/cmd/crane/cmd/auth.go
new file mode 100644
index 00000000..29033db4
--- /dev/null
+++ b/cmd/crane/cmd/auth.go
@@ -0,0 +1,151 @@
+// Copyright 2020 Google LLC 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 cmd
+
+import (
+	"encoding/json"
+	"errors"
+	"io/ioutil"
+	"log"
+	"os"
+	"strings"
+
+	"github.com/docker/cli/cli/config"
+	"github.com/docker/cli/cli/config/types"
+	"github.com/google/go-containerregistry/pkg/authn"
+	"github.com/google/go-containerregistry/pkg/name"
+	"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{
+		Use:   "auth",
+		Short: "Log in or access credentials",
+		Args:  cobra.NoArgs,
+		Run: func(cmd *cobra.Command, args []string) {
+			cmd.Usage()
+		},
+	}
+	cmd.AddCommand(NewCmdAuthGet(), NewCmdAuthLogin())
+	return cmd
+}
+
+// NewCmdAuthGet creates a new `crane auth get` command.
+func NewCmdAuthGet() *cobra.Command {
+	return &cobra.Command{
+		Use:   "get",
+		Short: "Implements a credential helper",
+		Example: `  # Read configured credentials for reg.example.com
+  echo "reg.example.com" | crane auth get
+  {"username":"AzureDiamond","password":"hunter2"}`,
+		Args: cobra.NoArgs,
+		Run: func(_ *cobra.Command, args []string) {
+			b, err := ioutil.ReadAll(os.Stdin)
+			if err != nil {
+				log.Fatal(err)
+			}
+			reg, err := name.NewRegistry(strings.TrimSpace(string(b)))
+			if err != nil {
+				log.Fatal(err)
+			}
+			auther, err := authn.DefaultKeychain.Resolve(reg)
+			if err != nil {
+				log.Fatal(err)
+			}
+			auth, err := auther.Authorization()
+			if err != nil {
+				log.Fatal(err)
+			}
+			if err := json.NewEncoder(os.Stdout).Encode(auth); err != nil {
+				log.Fatal(err)
+			}
+		},
+	}
+}
+
+// NewCmdAuthLogin creates a new `crane auth login` command.
+func NewCmdAuthLogin() *cobra.Command {
+	var opts loginOptions
+
+	cmd := &cobra.Command{
+		Use:   "login [OPTIONS] [SERVER]",
+		Short: "Log in to a registry",
+		Example: `  # Log in to reg.example.com
+  crane auth login reg.example.com -u AzureDiamond -p hunter2`,
+		Args: cobra.ExactArgs(1),
+		Run: func(cmd *cobra.Command, args []string) {
+			reg, err := name.NewRegistry(args[0])
+			if err != nil {
+				log.Fatal(err)
+			}
+
+			opts.serverAddress = reg.Name()
+
+			if err := login(opts); err != nil {
+				log.Fatal(err)
+			}
+		},
+	}
+
+	flags := cmd.Flags()
+
+	flags.StringVarP(&opts.user, "username", "u", "", "Username")
+	flags.StringVarP(&opts.password, "password", "p", "", "Password")
+	flags.BoolVarP(&opts.passwordStdin, "password-stdin", "", false, "Take the password from stdin")
+
+	return cmd
+}
+
+type loginOptions struct {
+	serverAddress string
+	user          string
+	password      string
+	passwordStdin bool
+}
+
+func login(opts loginOptions) error {
+	if opts.passwordStdin {
+		contents, err := ioutil.ReadAll(os.Stdin)
+		if err != nil {
+			return err
+		}
+
+		opts.password = strings.TrimSuffix(string(contents), "\n")
+		opts.password = strings.TrimSuffix(opts.password, "\r")
+	}
+	if opts.user == "" && opts.password == "" {
+		return errors.New("username and password required")
+	}
+	cf, err := config.Load(os.Getenv("DOCKER_CONFIG"))
+	if err != nil {
+		return err
+	}
+	creds := cf.GetCredentialsStore(opts.serverAddress)
+	if opts.serverAddress == name.DefaultRegistry {
+		opts.serverAddress = authn.DefaultAuthKey
+	}
+	if err := creds.Store(types.AuthConfig{
+		ServerAddress: opts.serverAddress,
+		Username:      opts.user,
+		Password:      opts.password,
+	}); err != nil {
+		return err
+	}
+
+	return cf.Save()
+}
diff --git a/cmd/crane/cmd/pull.go b/cmd/crane/cmd/pull.go
index bb5cc767..daa181f5 100644
--- a/cmd/crane/cmd/pull.go
+++ b/cmd/crane/cmd/pull.go
@@ -15,6 +15,7 @@
 package cmd
 
 import (
+	"fmt"
 	"log"
 
 	"github.com/google/go-containerregistry/pkg/crane"
@@ -26,7 +27,7 @@ func init() { Root.AddCommand(NewCmdPull()) }
 
 // NewCmdPull creates a new cobra.Command for the pull subcommand.
 func NewCmdPull() *cobra.Command {
-	var cachePath string
+	var cachePath, format string
 
 	cmd := &cobra.Command{
 		Use:   "pull IMAGE TARBALL",
@@ -41,12 +42,27 @@ func NewCmdPull() *cobra.Command {
 			if cachePath != "" {
 				img = cache.Image(img, cache.NewFilesystemCache(cachePath))
 			}
-			if err := crane.Save(img, src, path); err != nil {
-				log.Fatalf("saving tarball %s: %v", path, err)
+
+			switch format {
+			case "tarball":
+				if err := crane.Save(img, src, path); err != nil {
+					log.Fatalf("saving tarball %s: %v", path, err)
+				}
+			case "legacy":
+				if err := crane.SaveLegacy(img, src, path); err != nil {
+					log.Fatalf("saving legacy tarball %s: %v", path, err)
+				}
+			case "oci":
+				if err := crane.SaveOCI(img, path); err != nil {
+					log.Fatalf("saving oci image layout %s: %v", path, err)
+				}
+			default:
+				log.Fatalf("unexpected --format: %q (valid values are: tarball, legacy, and oci)", format)
 			}
 		},
 	}
 	cmd.Flags().StringVarP(&cachePath, "cache_path", "c", "", "Path to cache image layers")
+	cmd.Flags().StringVar(&format, "format", "tarball", fmt.Sprintf("Format in which to save images (%q, %q, or %q)", "tarball", "legacy", "oci"))
 
 	return cmd
 }
diff --git a/cmd/crane/cmd/root.go b/cmd/crane/cmd/root.go
index ae022405..3a272772 100644
--- a/cmd/crane/cmd/root.go
+++ b/cmd/crane/cmd/root.go
@@ -13,8 +13,10 @@
 package cmd
 
 import (
+	"net/http"
 	"os"
 
+	"github.com/docker/cli/cli/config"
 	"github.com/google/go-containerregistry/pkg/crane"
 	"github.com/google/go-containerregistry/pkg/logs"
 	"github.com/spf13/cobra"
@@ -45,6 +47,36 @@ var (
 			if insecure {
 				options = append(options, crane.Insecure)
 			}
+
+			// Add any http headers if they are set in the config file.
+			cf, err := config.Load(os.Getenv("DOCKER_CONFIG"))
+			if err != nil {
+				logs.Debug.Printf("failed to read config file: %v", err)
+			} else if len(cf.HTTPHeaders) != 0 {
+				options = append(options, crane.WithTransport(&headerTransport{
+					inner:       http.DefaultTransport,
+					httpHeaders: cf.HTTPHeaders,
+				}))
+			}
 		},
 	}
 )
+
+// headerTransport sets headers on outgoing requests.
+type headerTransport struct {
+	httpHeaders map[string]string
+	inner       http.RoundTripper
+}
+
+// RoundTrip implements http.RoundTripper.
+func (ht *headerTransport) RoundTrip(in *http.Request) (*http.Response, error) {
+	for k, v := range ht.httpHeaders {
+		if http.CanonicalHeaderKey(k) == "User-Agent" {
+			// Docker sets this, which is annoying, since we're not docker.
+			// We might want to revisit completely ignoring this.
+			continue
+		}
+		in.Header.Set(k, v)
+	}
+	return ht.inner.RoundTrip(in)
+}
diff --git a/cmd/crane/doc/crane.md b/cmd/crane/doc/crane.md
index 29932ceb..d25000e6 100644
--- a/cmd/crane/doc/crane.md
+++ b/cmd/crane/doc/crane.md
@@ -21,6 +21,7 @@ crane [flags]
 ### SEE ALSO
 
 * [crane append](crane_append.md)	 - Append contents of a tarball to a remote image
+* [crane auth](crane_auth.md)	 - Log in or access credentials
 * [crane blob](crane_blob.md)	 - Read a blob from the registry
 * [crane catalog](crane_catalog.md)	 - List the repos in a registry
 * [crane config](crane_config.md)	 - Get the config of an image
diff --git a/cmd/crane/doc/crane_append.md b/cmd/crane/doc/crane_append.md
index 40f8b951..01013c2c 100644
--- a/cmd/crane/doc/crane_append.md
+++ b/cmd/crane/doc/crane_append.md
@@ -13,11 +13,11 @@ crane append [flags]
 ### Options
 
 ```
-  -b, --base string        Name of base image to append to
-  -h, --help               help for append
-  -f, --new_layer string   Path to tarball to append to image
-  -t, --new_tag string     Tag to apply to resulting image
-  -o, --output string      Path to new tarball of resulting image
+  -b, --base string         Name of base image to append to
+  -h, --help                help for append
+  -f, --new_layer strings   Path to tarball to append to image
+  -t, --new_tag string      Tag to apply to resulting image
+  -o, --output string       Path to new tarball of resulting image
 ```
 
 ### Options inherited from parent commands
diff --git a/cmd/crane/doc/crane_auth.md b/cmd/crane/doc/crane_auth.md
new file mode 100644
index 00000000..c983205a
--- /dev/null
+++ b/cmd/crane/doc/crane_auth.md
@@ -0,0 +1,31 @@
+## crane auth
+
+Log in or access credentials
+
+### Synopsis
+
+Log in or access credentials
+
+```
+crane auth [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for auth
+```
+
+### Options inherited from parent commands
+
+```
+      --insecure   Allow image references to be fetched without TLS
+  -v, --verbose    Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md)	 - Crane is a tool for managing container images
+* [crane auth get](crane_auth_get.md)	 - Implements a credential helper
+* [crane auth login](crane_auth_login.md)	 - Log in to a registry
+
diff --git a/cmd/crane/doc/crane_auth_get.md b/cmd/crane/doc/crane_auth_get.md
new file mode 100644
index 00000000..d560bd22
--- /dev/null
+++ b/cmd/crane/doc/crane_auth_get.md
@@ -0,0 +1,37 @@
+## crane auth get
+
+Implements a credential helper
+
+### Synopsis
+
+Implements a credential helper
+
+```
+crane auth get [flags]
+```
+
+### Examples
+
+```
+  # Read configured credentials for reg.example.com
+  echo "reg.example.com" | crane auth get
+  {"username":"AzureDiamond","password":"hunter2"}
+```
+
+### Options
+
+```
+  -h, --help   help for get
+```
+
+### Options inherited from parent commands
+
+```
+      --insecure   Allow image references to be fetched without TLS
+  -v, --verbose    Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane auth](crane_auth.md)	 - Log in or access credentials
+
diff --git a/cmd/crane/doc/crane_auth_login.md b/cmd/crane/doc/crane_auth_login.md
new file mode 100644
index 00000000..e771c413
--- /dev/null
+++ b/cmd/crane/doc/crane_auth_login.md
@@ -0,0 +1,39 @@
+## crane auth login
+
+Log in to a registry
+
+### Synopsis
+
+Log in to a registry
+
+```
+crane auth login [OPTIONS] [SERVER] [flags]
+```
+
+### Examples
+
+```
+  # Log in to reg.example.com
+  crane auth login reg.example.com -u AzureDiamond -p hunter2
+```
+
+### Options
+
+```
+  -h, --help              help for login
+  -p, --password string   Password
+      --password-stdin    Take the password from stdin
+  -u, --username string   Username
+```
+
+### Options inherited from parent commands
+
+```
+      --insecure   Allow image references to be fetched without TLS
+  -v, --verbose    Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane auth](crane_auth.md)	 - Log in or access credentials
+
diff --git a/cmd/crane/doc/crane_pull.md b/cmd/crane/doc/crane_pull.md
index f1a9bc83..c5e8db01 100644
--- a/cmd/crane/doc/crane_pull.md
+++ b/cmd/crane/doc/crane_pull.md
@@ -14,6 +14,7 @@ crane pull IMAGE TARBALL [flags]
 
 ```
   -c, --cache_path string   Path to cache image layers
+      --format string       Format in which to save images ("tarball", "legacy", or "oci") (default "tarball")
   -h, --help                help for pull
 ```
 
diff --git a/cmd/gcrane/cmd/list.go b/cmd/gcrane/cmd/list.go
index beb712e4..98842532 100644
--- a/cmd/gcrane/cmd/list.go
+++ b/cmd/gcrane/cmd/list.go
@@ -15,6 +15,7 @@
 package cmd
 
 import (
+	"encoding/json"
 	"fmt"
 	"log"
 
@@ -28,28 +29,30 @@ func init() { Root.AddCommand(NewCmdList()) }
 // NewCmdList creates a new cobra.Command for the ls subcommand.
 func NewCmdList() *cobra.Command {
 	recursive := false
+	json := false
 	cmd := &cobra.Command{
 		Use:   "ls REPO",
 		Short: "List the contents of a repo",
 		Args:  cobra.ExactArgs(1),
 		Run: func(_ *cobra.Command, args []string) {
-			ls(args[0], recursive)
+			ls(args[0], recursive, json)
 		},
 	}
 
 	cmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "Whether to recurse through repos")
+	cmd.Flags().BoolVar(&json, "json", false, "Format the response from the registry as JSON, one line per repo")
 
 	return cmd
 }
 
-func ls(root string, recursive bool) {
+func ls(root string, recursive, j bool) {
 	repo, err := name.NewRepository(root)
 	if err != nil {
 		log.Fatalln(err)
 	}
 
 	if recursive {
-		if err := google.Walk(repo, printImages, google.WithAuthFromKeychain(google.Keychain)); err != nil {
+		if err := google.Walk(repo, printImages(j), google.WithAuthFromKeychain(google.Keychain)); err != nil {
 			log.Fatalln(err)
 		}
 		return
@@ -60,36 +63,49 @@ func ls(root string, recursive bool) {
 		log.Fatalln(err)
 	}
 
-	if len(tags.Manifests) == 0 && len(tags.Children) == 0 {
-		// If we didn't see any GCR extensions, just list the tags like normal.
-		for _, tag := range tags.Tags {
-			fmt.Printf("%s:%s\n", repo, tag)
+	if !j {
+		if len(tags.Manifests) == 0 && len(tags.Children) == 0 {
+			// If we didn't see any GCR extensions, just list the tags like normal.
+			for _, tag := range tags.Tags {
+				fmt.Printf("%s:%s\n", repo, tag)
+			}
+			return
 		}
-		return
-	}
 
-	// Since we're not recursing, print the subdirectories too.
-	for _, child := range tags.Children {
-		fmt.Printf("%s/%s\n", repo, child)
+		// Since we're not recursing, print the subdirectories too.
+		for _, child := range tags.Children {
+			fmt.Printf("%s/%s\n", repo, child)
+		}
 	}
 
-	if err := printImages(repo, tags, err); err != nil {
+	if err := printImages(j)(repo, tags, err); err != nil {
 		log.Fatalln(err)
 	}
 }
 
-func printImages(repo name.Repository, tags *google.Tags, err error) error {
-	if err != nil {
-		return err
-	}
+func printImages(j bool) google.WalkFunc {
+	return func(repo name.Repository, tags *google.Tags, err error) error {
+		if err != nil {
+			return err
+		}
+
+		if j {
+			b, err := json.Marshal(tags)
+			if err != nil {
+				return err
+			}
+			fmt.Printf("%s\n", b)
+			return nil
+		}
 
-	for digest, manifest := range tags.Manifests {
-		fmt.Printf("%s@%s\n", repo, digest)
+		for digest, manifest := range tags.Manifests {
+			fmt.Printf("%s@%s\n", repo, digest)
 
-		for _, tag := range manifest.Tags {
-			fmt.Printf("%s:%s\n", repo, tag)
+			for _, tag := range manifest.Tags {
+				fmt.Printf("%s:%s\n", repo, tag)
+			}
 		}
-	}
 
-	return nil
+		return nil
+	}
 }
diff --git a/go.mod b/go.mod
index f55faa68..8ae36dfb 100644
--- a/go.mod
+++ b/go.mod
@@ -17,15 +17,15 @@ require (
 	github.com/morikuni/aec v1.0.0 // indirect
 	github.com/spf13/cobra v0.0.5
 	github.com/vdemeester/k8s-pkg-credentialprovider v0.0.0-20200107171650-7c61ffa44238
-	golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect
+	golang.org/x/lint v0.0.0-20200130185559-910be7a94367 // indirect
 	golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
 	golang.org/x/sync v0.0.0-20190423024810-112230192c58
 	golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect
-	golang.org/x/tools v0.0.0-20200115165105-de0b1760071a // indirect
+	golang.org/x/tools v0.0.0-20200210192313-1ace956b0e17 // indirect
 	google.golang.org/grpc v1.24.0 // indirect
 	honnef.co/go/tools v0.0.1-2019.2.3 // indirect
 	k8s.io/api v0.17.0
 	k8s.io/apimachinery v0.17.0
 	k8s.io/client-go v0.17.0
-	k8s.io/code-generator v0.17.1
+	k8s.io/code-generator v0.17.2
 )
diff --git a/go.sum b/go.sum
index 5bbf6479..aa464d54 100644
--- a/go.sum
+++ b/go.sum
@@ -342,8 +342,11 @@ golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jK
 golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE=
 golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367 h1:0IiAsCRByjO2QjX7ZPkw5oU9x+n1YqRL802rjC0c3Aw=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -429,7 +432,11 @@ golang.org/x/tools v0.0.0-20191205215504-7b8c8591a921 h1:a2kzKBu/PCzjVEtbXNEyM2K
 golang.org/x/tools v0.0.0-20191205215504-7b8c8591a921/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200115165105-de0b1760071a h1:bEJ3JL2YUH3tt9KX9dsy0WUF3WOrhjtNjK93o0svepY=
 golang.org/x/tools v0.0.0-20200115165105-de0b1760071a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200210192313-1ace956b0e17 h1:a/Fd23DJvg1CaeDH0dYHahE+hCI0v9rFgxSNIThoUcM=
+golang.org/x/tools v0.0.0-20200210192313-1ace956b0e17/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
 gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
@@ -498,6 +505,8 @@ k8s.io/code-generator v0.0.0-20191121015212-c4c8f8345c7e h1:HB9Zu5ZUvJfNpLiTPhz+
 k8s.io/code-generator v0.0.0-20191121015212-c4c8f8345c7e/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s=
 k8s.io/code-generator v0.17.1 h1:e3B1UqRzRUWygp7WD+QTRT3ZUahPIaRKF0OFa7duQwI=
 k8s.io/code-generator v0.17.1/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s=
+k8s.io/code-generator v0.17.2 h1:pTwl3rLB1fUyxmvEzmVPMM0tBSdUehd7z+bDzpj4lPE=
+k8s.io/code-generator v0.17.2/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s=
 k8s.io/component-base v0.17.0 h1:BnDFcmBDq+RPpxXjmuYnZXb59XNN9CaFrX8ba9+3xrA=
 k8s.io/component-base v0.17.0/go.mod h1:rKuRAokNMY2nn2A6LP/MiwpoaMRHpfRnrPaUJJj1Yoc=
 k8s.io/csi-translation-lib v0.17.0/go.mod h1:HEF7MEz7pOLJCnxabi45IPkhSsE/KmxPQksuCrHKWls=
diff --git a/pkg/authn/keychain.go b/pkg/authn/keychain.go
index ce999e8e..7bafc2fa 100644
--- a/pkg/authn/keychain.go
+++ b/pkg/authn/keychain.go
@@ -52,7 +52,9 @@ var (
 )
 
 const (
-	defaultAuthKey = "https://" + name.DefaultRegistry + "/v1/"
+	// DefaultAuthKey is the key used for dockerhub in config files, which
+	// is hardcoded for historical reasons.
+	DefaultAuthKey = "https://" + name.DefaultRegistry + "/v1/"
 )
 
 // Resolve implements Keychain.
@@ -67,7 +69,7 @@ func (dk *defaultKeychain) Resolve(target Resource) (Authenticator, error) {
 	// https://github.com/moby/moby/blob/fc01c2b481097a6057bec3cd1ab2d7b4488c50c4/registry/config.go#L397-L404
 	key := target.RegistryStr()
 	if key == name.DefaultRegistry {
-		key = defaultAuthKey
+		key = DefaultAuthKey
 	}
 
 	cfg, err := cf.GetAuthConfig(key)
diff --git a/pkg/crane/append.go b/pkg/crane/append.go
index 86269123..5e2b5422 100644
--- a/pkg/crane/append.go
+++ b/pkg/crane/append.go
@@ -23,11 +23,15 @@ import (
 )
 
 // Append reads a layer from path and appends it the the v1.Image base.
-func Append(base v1.Image, path string) (v1.Image, error) {
-	layer, err := tarball.LayerFromFile(path)
-	if err != nil {
-		return nil, fmt.Errorf("reading tar %q: %v", path, err)
+func Append(base v1.Image, paths ...string) (v1.Image, error) {
+	layers := make([]v1.Layer, 0, len(paths))
+	for _, path := range paths {
+		layer, err := tarball.LayerFromFile(path)
+		if err != nil {
+			return nil, fmt.Errorf("reading tar %q: %v", path, err)
+		}
+		layers = append(layers, layer)
 	}
 
-	return mutate.AppendLayers(base, layer)
+	return mutate.AppendLayers(base, layers...)
 }
diff --git a/pkg/crane/pull.go b/pkg/crane/pull.go
index a591b1ca..bbdf1fa7 100644
--- a/pkg/crane/pull.go
+++ b/pkg/crane/pull.go
@@ -16,9 +16,13 @@ package crane
 
 import (
 	"fmt"
+	"os"
 
+	legacy "github.com/google/go-containerregistry/pkg/legacy/tarball"
 	"github.com/google/go-containerregistry/pkg/name"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
+	"github.com/google/go-containerregistry/pkg/v1/empty"
+	"github.com/google/go-containerregistry/pkg/v1/layout"
 	"github.com/google/go-containerregistry/pkg/v1/remote"
 	"github.com/google/go-containerregistry/pkg/v1/tarball"
 )
@@ -72,3 +76,32 @@ func PullLayer(ref string, opt ...Option) (v1.Layer, error) {
 
 	return remote.Layer(digest, o.remote...)
 }
+
+// SaveLegacy writes the v1.Image img as a legacy tarball at path with tag src.
+func SaveLegacy(img v1.Image, src, path string) error {
+	ref, err := name.ParseReference(src)
+	if err != nil {
+		return fmt.Errorf("parsing ref %q: %v", src, err)
+	}
+
+	w, err := os.Create(path)
+	if err != nil {
+		return err
+	}
+	defer w.Close()
+
+	return legacy.Write(ref, img, w)
+}
+
+// SaveOCI writes the v1.Image img as an OCI Image Layout at path. If a layout
+// already exists at that path, it will add the image to the index.
+func SaveOCI(img v1.Image, path string) error {
+	p, err := layout.FromPath(path)
+	if err != nil {
+		p, err = layout.Write(path, empty.Index)
+		if err != nil {
+			return err
+		}
+	}
+	return p.AppendImage(img)
+}
diff --git a/pkg/gcrane/copy.go b/pkg/gcrane/copy.go
index d1b2a0df..ddf9e262 100644
--- a/pkg/gcrane/copy.go
+++ b/pkg/gcrane/copy.go
@@ -126,12 +126,13 @@ func recursiveCopy(ctx context.Context, src, dst string, jobs int) error {
 	g, ctx := errgroup.WithContext(ctx)
 	walkFn := func(repo name.Repository, tags *google.Tags, err error) error {
 		if err != nil {
+			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))
 				return err
 			}); err != nil {
-				return fmt.Errorf("failed walkFn for repo %s: %v", repo, err)
+				return fmt.Errorf("failed List for repo %s: %v", repo, err)
 			}
 		}
 
diff --git a/pkg/internal/legacy/copy.go b/pkg/internal/legacy/copy.go
index 9b073d22..ba8f96ab 100644
--- a/pkg/internal/legacy/copy.go
+++ b/pkg/internal/legacy/copy.go
@@ -46,7 +46,7 @@ func CopySchema1(desc *remote.Descriptor, srcRef, dstRef name.Reference, srcAuth
 			return err
 		}
 
-		if err := remote.WriteLayer(dst, blob, remote.WithAuth(dstAuth)); err != nil {
+		if err := remote.WriteLayer(dst.Context(), blob, remote.WithAuth(dstAuth)); err != nil {
 			return err
 		}
 	}
diff --git a/pkg/registry/tls.go b/pkg/registry/tls.go
index 70bcc33d..d5e86192 100644
--- a/pkg/registry/tls.go
+++ b/pkg/registry/tls.go
@@ -15,19 +15,9 @@
 package registry
 
 import (
-	"bytes"
-	"context"
-	"crypto/ecdsa"
-	"crypto/elliptic"
-	"crypto/rand"
-	"crypto/tls"
-	"crypto/x509"
-	"encoding/pem"
-	"math/big"
-	"net"
-	"net/http"
 	"net/http/httptest"
-	"time"
+
+	ggcrtest "github.com/google/go-containerregistry/pkg/internal/httptest"
 )
 
 // TLS returns an httptest server, with an http client that has been configured to
@@ -35,70 +25,5 @@ import (
 // which should correspond to the domain the image is stored in.
 // If you need a transport, Client().Transport is correctly configured.
 func TLS(domain string) (*httptest.Server, error) {
-	s := httptest.NewUnstartedServer(New())
-
-	template := x509.Certificate{
-		SerialNumber: big.NewInt(1),
-		NotBefore:    time.Now().Add(-1 * time.Hour),
-		NotAfter:     time.Now().Add(time.Hour),
-		IPAddresses: []net.IP{
-			net.IPv4(127, 0, 0, 1),
-			net.IPv6loopback,
-		},
-		DNSNames: []string{domain},
-
-		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
-		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
-		BasicConstraintsValid: true,
-		IsCA:                  true,
-	}
-
-	priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
-	if err != nil {
-		return nil, err
-	}
-
-	b, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
-	if err != nil {
-		return nil, err
-	}
-
-	pc := &bytes.Buffer{}
-	if err := pem.Encode(pc, &pem.Block{Type: "CERTIFICATE", Bytes: b}); err != nil {
-		return nil, err
-	}
-
-	ek, err := x509.MarshalECPrivateKey(priv)
-	if err != nil {
-		return nil, err
-	}
-
-	pk := &bytes.Buffer{}
-	if err := pem.Encode(pk, &pem.Block{Type: "EC PRIVATE KEY", Bytes: ek}); err != nil {
-		return nil, err
-	}
-
-	c, err := tls.X509KeyPair(pc.Bytes(), pk.Bytes())
-	if err != nil {
-		return nil, err
-	}
-	s.TLS = &tls.Config{
-		Certificates: []tls.Certificate{c},
-	}
-	s.StartTLS()
-
-	certpool := x509.NewCertPool()
-	certpool.AddCert(s.Certificate())
-
-	t := &http.Transport{
-		TLSClientConfig: &tls.Config{
-			RootCAs: certpool,
-		},
-		DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
-			return net.Dial(s.Listener.Addr().Network(), s.Listener.Addr().String())
-		},
-	}
-	s.Client().Transport = t
-
-	return s, nil
+	return ggcrtest.NewTLSServer(domain, New())
 }
diff --git a/pkg/v1/google/keychain.go b/pkg/v1/google/keychain.go
index f4384978..36727041 100644
--- a/pkg/v1/google/keychain.go
+++ b/pkg/v1/google/keychain.go
@@ -51,8 +51,8 @@ type googleKeychain struct {
 // In general, we don't worry about that here because we expect to use the same
 // gcloud configuration in the scope of this one process.
 func (gk *googleKeychain) Resolve(target authn.Resource) (authn.Authenticator, error) {
-	// Only authenticate GCR so it works with authn.NewMultiKeychain to fallback.
-	if !strings.HasSuffix(target.RegistryStr(), "gcr.io") {
+	// Only authenticate GCR and AR so it works with authn.NewMultiKeychain to fallback.
+	if !strings.HasSuffix(target.RegistryStr(), "gcr.io") && !strings.HasSuffix(target.RegistryStr(), "pkg.dev") {
 		return authn.Anonymous, nil
 	}
 
diff --git a/pkg/v1/google/list.go b/pkg/v1/google/list.go
index e5a530bd..cad92a56 100644
--- a/pkg/v1/google/list.go
+++ b/pkg/v1/google/list.go
@@ -98,12 +98,14 @@ func (l *lister) list(repo name.Repository) (*Tags, error) {
 	return &tags, nil
 }
 
+// Uploaded uses json.Number to work around GCR returning timeUploaded
+// as a string, while AR returns the same field as int64.
 type rawManifestInfo struct {
-	Size      string   `json:"imageSizeBytes"`
-	MediaType string   `json:"mediaType"`
-	Created   string   `json:"timeCreatedMs"`
-	Uploaded  string   `json:"timeUploadedMs"`
-	Tags      []string `json:"tag"`
+	Size      string      `json:"imageSizeBytes"`
+	MediaType string      `json:"mediaType"`
+	Created   string      `json:"timeCreatedMs"`
+	Uploaded  json.Number `json:"timeUploadedMs"`
+	Tags      []string    `json:"tag"`
 }
 
 // ManifestInfo is a Manifests entry is the output of List and Walk.
@@ -121,6 +123,21 @@ func fromUnixMs(ms int64) time.Time {
 	return time.Unix(sec, ns)
 }
 
+func toUnixMs(t time.Time) string {
+	return strconv.FormatInt(t.UnixNano()/1000000, 10)
+}
+
+// MarshalJSON implements json.Marshaler
+func (m ManifestInfo) MarshalJSON() ([]byte, error) {
+	return json.Marshal(rawManifestInfo{
+		Size:      strconv.FormatUint(m.Size, 10),
+		MediaType: m.MediaType,
+		Created:   toUnixMs(m.Created),
+		Uploaded:  json.Number(toUnixMs(m.Uploaded)),
+		Tags:      m.Tags,
+	})
+}
+
 // UnmarshalJSON implements json.Unmarshaler
 func (m *ManifestInfo) UnmarshalJSON(data []byte) error {
 	raw := rawManifestInfo{}
diff --git a/pkg/v1/layout/write.go b/pkg/v1/layout/write.go
index 2abb9a58..5c047001 100644
--- a/pkg/v1/layout/write.go
+++ b/pkg/v1/layout/write.go
@@ -124,15 +124,19 @@ func (l Path) AppendDescriptor(desc v1.Descriptor) error {
 		return err
 	}
 
-	return l.writeFile("index.json", rawIndex)
+	return l.WriteFile("index.json", rawIndex, os.ModePerm)
 }
 
-func (l Path) writeFile(name string, data []byte) error {
+// WriteFile write a file with arbitrary data at an arbitrary location in a v1
+// layout. Used mostly internally to write files like "oci-layout" and
+// "index.json", also can be used to write other arbitrary files. Do *not* use
+// this to write blobs. Use only WriteBlob() for that.
+func (l Path) WriteFile(name string, data []byte, perm os.FileMode) error {
 	if err := os.MkdirAll(l.path(), os.ModePerm); err != nil && !os.IsExist(err) {
 		return err
 	}
 
-	return ioutil.WriteFile(l.path(name), data, os.ModePerm)
+	return ioutil.WriteFile(l.path(name), data, perm)
 
 }
 
@@ -259,12 +263,12 @@ func (l Path) writeIndexToFile(indexFile string, ii v1.ImageIndex) error {
 		return err
 	}
 
-	return l.writeFile(indexFile, rawIndex)
+	return l.WriteFile(indexFile, rawIndex, os.ModePerm)
 }
 
 func (l Path) writeIndex(ii v1.ImageIndex) error {
 	// Always just write oci-layout file, since it's small.
-	if err := l.writeFile("oci-layout", []byte(layoutFile)); err != nil {
+	if err := l.WriteFile("oci-layout", []byte(layoutFile), os.ModePerm); err != nil {
 		return err
 	}
 
@@ -291,7 +295,7 @@ func (l Path) writeIndex(ii v1.ImageIndex) error {
 func Write(path string, ii v1.ImageIndex) (Path, error) {
 	lp := Path(path)
 	// Always just write oci-layout file, since it's small.
-	if err := lp.writeFile("oci-layout", []byte(layoutFile)); err != nil {
+	if err := lp.WriteFile("oci-layout", []byte(layoutFile), os.ModePerm); err != nil {
 		return "", err
 	}
 
diff --git a/pkg/v1/mutate/image.go b/pkg/v1/mutate/image.go
index 382d139e..e13ef1f8 100644
--- a/pkg/v1/mutate/image.go
+++ b/pkg/v1/mutate/image.go
@@ -120,8 +120,12 @@ func (i *image) compute() error {
 
 	// With OCI media types, this should not be set, see discussion:
 	// https://github.com/opencontainers/image-spec/pull/795
-	if i.mediaType != nil && strings.Contains(string(*i.mediaType), types.OCIVendorPrefix) {
-		manifest.MediaType = ""
+	if i.mediaType != nil {
+		if strings.Contains(string(*i.mediaType), types.OCIVendorPrefix) {
+			manifest.MediaType = ""
+		} else if strings.Contains(string(*i.mediaType), types.DockerVendorPrefix) {
+			manifest.MediaType = *i.mediaType
+		}
 	}
 
 	i.configFile = configFile
diff --git a/pkg/v1/mutate/index.go b/pkg/v1/mutate/index.go
index 21ccf50c..b8f88c3b 100644
--- a/pkg/v1/mutate/index.go
+++ b/pkg/v1/mutate/index.go
@@ -24,31 +24,33 @@ import (
 	"github.com/google/go-containerregistry/pkg/v1/types"
 )
 
-func computeDescriptor(desc v1.Descriptor, add Appendable) (*v1.Descriptor, error) {
-	d, err := add.Digest()
+func computeDescriptor(ia IndexAddendum) (*v1.Descriptor, error) {
+	desc, err := partial.Descriptor(ia.Add)
 	if err != nil {
 		return nil, err
 	}
-	mt, err := add.MediaType()
-	if err != nil {
-		return nil, err
+
+	// The IndexAddendum allows overriding Descriptor values.
+	if ia.Descriptor.Size != 0 {
+		desc.Size = ia.Descriptor.Size
 	}
-	sz, err := add.Size()
-	if err != nil {
-		return nil, err
+	if string(ia.Descriptor.MediaType) != "" {
+		desc.MediaType = ia.Descriptor.MediaType
 	}
-
-	// The IndexAddendum allows overriding These values.
-	if desc.Size == 0 {
-		desc.Size = sz
+	if ia.Descriptor.Digest != (v1.Hash{}) {
+		desc.Digest = ia.Descriptor.Digest
+	}
+	if ia.Descriptor.Platform != nil {
+		desc.Platform = ia.Descriptor.Platform
 	}
-	if string(desc.MediaType) == "" {
-		desc.MediaType = mt
+	if len(ia.Descriptor.URLs) != 0 {
+		desc.URLs = ia.Descriptor.URLs
 	}
-	if desc.Digest == (v1.Hash{}) {
-		desc.Digest = d
+	if len(ia.Descriptor.Annotations) != 0 {
+		desc.Annotations = ia.Descriptor.Annotations
 	}
-	return &desc, nil
+
+	return desc, nil
 }
 
 type index struct {
@@ -89,7 +91,7 @@ func (i *index) compute() error {
 	manifest := m.DeepCopy()
 	manifests := manifest.Manifests
 	for _, add := range i.adds {
-		desc, err := computeDescriptor(add.Descriptor, add.Add)
+		desc, err := computeDescriptor(add)
 		if err != nil {
 			return err
 		}
@@ -107,8 +109,12 @@ func (i *index) compute() error {
 
 	// With OCI media types, this should not be set, see discussion:
 	// https://github.com/opencontainers/image-spec/pull/795
-	if i.mediaType != nil && strings.Contains(string(*i.mediaType), types.OCIVendorPrefix) {
-		manifest.MediaType = ""
+	if i.mediaType != nil {
+		if strings.Contains(string(*i.mediaType), types.OCIVendorPrefix) {
+			manifest.MediaType = ""
+		} else if strings.Contains(string(*i.mediaType), types.DockerVendorPrefix) {
+			manifest.MediaType = *i.mediaType
+		}
 	}
 
 	i.manifest = manifest
diff --git a/pkg/v1/remote/check.go b/pkg/v1/remote/check.go
index da0fa24c..65ee4e7f 100644
--- a/pkg/v1/remote/check.go
+++ b/pkg/v1/remote/check.go
@@ -34,7 +34,7 @@ func CheckPushPermission(ref name.Reference, kc authn.Keychain, t http.RoundTrip
 	// authorize a push. Figure out how to return early here when we can,
 	// to avoid a roundtrip for spec-compliant registries.
 	w := writer{
-		ref:    ref,
+		repo:   ref.Context(),
 		client: &http.Client{Transport: tr},
 	}
 	loc, _, err := w.initiateUpload("", "")
diff --git a/pkg/v1/remote/transport/error.go b/pkg/v1/remote/transport/error.go
index 35e5d798..a215e794 100644
--- a/pkg/v1/remote/transport/error.go
+++ b/pkg/v1/remote/transport/error.go
@@ -19,9 +19,27 @@ import (
 	"fmt"
 	"io/ioutil"
 	"net/http"
+	"net/url"
 	"strings"
 )
 
+// The set of query string keys that we expect to send as part of the registry
+// protocol. Anything else is potentially dangerous to leak, as it's probably
+// from a redirect. These redirects often included tokens or signed URLs.
+var paramWhitelist = map[string]struct{}{
+	// Token exchange
+	"scope":   struct{}{},
+	"service": struct{}{},
+	// Cross-repo mounting
+	"mount": struct{}{},
+	"from":  struct{}{},
+	// Layer PUT
+	"digest": struct{}{},
+	// Listing tags and catalog
+	"n":    struct{}{},
+	"last": struct{}{},
+}
+
 // Error implements error to support the following error specification:
 // https://github.com/docker/distribution/blob/master/docs/spec/api.md#errors
 type Error struct {
@@ -30,6 +48,8 @@ type Error struct {
 	StatusCode int
 	// The raw body if we couldn't understand it.
 	rawBody string
+	// The request that failed.
+	request *http.Request
 }
 
 // Check that Error implements error
@@ -37,6 +57,14 @@ var _ error = (*Error)(nil)
 
 // Error implements error
 func (e *Error) Error() string {
+	prefix := ""
+	if e.request != nil {
+		prefix = fmt.Sprintf("%s %s: ", e.request.Method, redact(e.request.URL))
+	}
+	return prefix + e.responseErr()
+}
+
+func (e *Error) responseErr() string {
 	switch len(e.Errors) {
 	case 0:
 		if len(e.rawBody) == 0 {
@@ -51,7 +79,7 @@ func (e *Error) Error() string {
 			errors = append(errors, d.String())
 		}
 		return fmt.Sprintf("multiple errors returned: %s",
-			strings.Join(errors, ";"))
+			strings.Join(errors, "; "))
 	}
 }
 
@@ -69,6 +97,21 @@ func (e *Error) Temporary() bool {
 	return true
 }
 
+func redact(original *url.URL) *url.URL {
+	qs := original.Query()
+	for k, v := range qs {
+		for i := range v {
+			if _, ok := paramWhitelist[k]; !ok {
+				// key is not in the whitelist
+				v[i] = "REDACTED"
+			}
+		}
+	}
+	redacted := *original
+	redacted.RawQuery = qs.Encode()
+	return &redacted
+}
+
 // Diagnostic represents a single error returned by a Docker registry interaction.
 type Diagnostic struct {
 	Code    ErrorCode   `json:"code"`
@@ -127,5 +170,6 @@ func CheckError(resp *http.Response, codes ...int) error {
 		structuredError.rawBody = string(b)
 	}
 	structuredError.StatusCode = resp.StatusCode
+	structuredError.request = resp.Request
 	return structuredError
 }
diff --git a/pkg/v1/remote/write.go b/pkg/v1/remote/write.go
index 7be851f0..7b431c07 100644
--- a/pkg/v1/remote/write.go
+++ b/pkg/v1/remote/write.go
@@ -50,13 +50,13 @@ func Write(ref name.Reference, img v1.Image, options ...Option) error {
 		return err
 	}
 
-	scopes := scopesForUploadingImage(ref, ls)
+	scopes := scopesForUploadingImage(ref.Context(), ls)
 	tr, err := transport.New(ref.Context().Registry, o.auth, o.transport, scopes)
 	if err != nil {
 		return err
 	}
 	w := writer{
-		ref:    ref,
+		repo:   ref.Context(),
 		client: &http.Client{Transport: tr},
 	}
 
@@ -125,20 +125,20 @@ func Write(ref name.Reference, img v1.Image, options ...Option) error {
 
 	// With all of the constituent elements uploaded, upload the manifest
 	// to commit the image.
-	return w.commitImage(img)
+	return w.commitImage(img, ref)
 }
 
 // writer writes the elements of an image to a remote image reference.
 type writer struct {
-	ref    name.Reference
+	repo   name.Repository
 	client *http.Client
 }
 
 // url returns a url.Url for the specified path in the context of this remote image reference.
 func (w *writer) url(path string) url.URL {
 	return url.URL{
-		Scheme: w.ref.Context().Registry.Scheme(),
-		Host:   w.ref.Context().RegistryStr(),
+		Scheme: w.repo.Registry.Scheme(),
+		Host:   w.repo.RegistryStr(),
 		Path:   path,
 	}
 }
@@ -164,7 +164,7 @@ func (w *writer) nextLocation(resp *http.Response) (string, error) {
 // initiation if "mount" is specified, even if no "from" sources are specified.
 // However, this is not broadly applicable to all registries, e.g. ECR.
 func (w *writer) checkExistingBlob(h v1.Hash) (bool, error) {
-	u := w.url(fmt.Sprintf("/v2/%s/blobs/%s", w.ref.Context().RepositoryStr(), h.String()))
+	u := w.url(fmt.Sprintf("/v2/%s/blobs/%s", w.repo.RepositoryStr(), h.String()))
 
 	resp, err := w.client.Head(u.String())
 	if err != nil {
@@ -182,7 +182,7 @@ func (w *writer) checkExistingBlob(h v1.Hash) (bool, error) {
 // checkExistingManifest checks if a manifest exists already in the repository
 // by making a HEAD request to the manifest API.
 func (w *writer) checkExistingManifest(h v1.Hash, mt types.MediaType) (bool, error) {
-	u := w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.ref.Context().RepositoryStr(), h.String()))
+	u := w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.repo.RepositoryStr(), h.String()))
 
 	req, err := http.NewRequest(http.MethodHead, u.String(), nil)
 	if err != nil {
@@ -210,7 +210,7 @@ func (w *writer) checkExistingManifest(h v1.Hash, mt types.MediaType) (bool, err
 // upload was initiated and the body of that blob should be sent to the returned
 // location.
 func (w *writer) initiateUpload(from, mount string) (location string, mounted bool, err error) {
-	u := w.url(fmt.Sprintf("/v2/%s/blobs/uploads/", w.ref.Context().RepositoryStr()))
+	u := w.url(fmt.Sprintf("/v2/%s/blobs/uploads/", w.repo.RepositoryStr()))
 	uv := url.Values{}
 	if mount != "" && from != "" {
 		// Quay will fail if we specify a "mount" without a "from".
@@ -313,7 +313,7 @@ func (w *writer) uploadOne(l v1.Layer) error {
 		mount = h.String()
 	}
 	if ml, ok := l.(*MountableLayer); ok {
-		if w.ref.Context().RegistryStr() == ml.Reference.Context().RegistryStr() {
+		if w.repo.RegistryStr() == ml.Reference.Context().RegistryStr() {
 			from = ml.Reference.Context().RepositoryStr()
 		}
 	}
@@ -407,7 +407,7 @@ func unpackTaggable(t Taggable) (*v1.Descriptor, error) {
 }
 
 // commitImage does a PUT of the image's manifest.
-func (w *writer) commitImage(t Taggable) error {
+func (w *writer) commitImage(t Taggable, ref name.Reference) error {
 	raw, err := t.RawManifest()
 	if err != nil {
 		return err
@@ -417,7 +417,7 @@ func (w *writer) commitImage(t Taggable) error {
 		return err
 	}
 
-	u := w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.ref.Context().RepositoryStr(), w.ref.Identifier()))
+	u := w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.repo.RepositoryStr(), ref.Identifier()))
 
 	// Make the request to PUT the serialized manifest
 	req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewBuffer(raw))
@@ -437,11 +437,11 @@ func (w *writer) commitImage(t Taggable) error {
 	}
 
 	// The image was successfully pushed!
-	logs.Progress.Printf("%v: digest: %v size: %d", w.ref, desc.Digest, len(raw))
+	logs.Progress.Printf("%v: digest: %v size: %d", ref, desc.Digest, len(raw))
 	return nil
 }
 
-func scopesForUploadingImage(ref name.Reference, layers []v1.Layer) []string {
+func scopesForUploadingImage(repo name.Repository, layers []v1.Layer) []string {
 	// use a map as set to remove duplicates scope strings
 	scopeSet := map[string]struct{}{}
 
@@ -449,7 +449,7 @@ func scopesForUploadingImage(ref name.Reference, layers []v1.Layer) []string {
 		if ml, ok := l.(*MountableLayer); ok {
 			// we will add push scope for ref.Context() after the loop.
 			// for now we ask pull scope for references of the same registry
-			if ml.Reference.Context() != ref.Context() && ml.Reference.Context().Registry == ref.Context().Registry {
+			if ml.Reference.Context() != repo && ml.Reference.Context().Registry == repo.Registry {
 				scopeSet[ml.Reference.Scope(transport.PullScope)] = struct{}{}
 			}
 		}
@@ -457,7 +457,7 @@ func scopesForUploadingImage(ref name.Reference, layers []v1.Layer) []string {
 
 	scopes := make([]string, 0)
 	// Push scope should be the first element because a few registries just look at the first scope to determine access.
-	scopes = append(scopes, ref.Scope(transport.PushScope))
+	scopes = append(scopes, repo.Scope(transport.PushScope))
 
 	for scope := range scopeSet {
 		scopes = append(scopes, scope)
@@ -485,7 +485,7 @@ func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) error {
 		return err
 	}
 	w := writer{
-		ref:    ref,
+		repo:   ref.Context(),
 		client: &http.Client{Transport: tr},
 	}
 
@@ -523,22 +523,22 @@ func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) error {
 
 	// With all of the constituent elements uploaded, upload the manifest
 	// to commit the image.
-	return w.commitImage(ii)
+	return w.commitImage(ii, ref)
 }
 
-// WriteLayer uploads the provided Layer to the specified name.Digest.
-func WriteLayer(ref name.Digest, layer v1.Layer, options ...Option) error {
-	o, err := makeOptions(ref.Context(), options...)
+// WriteLayer uploads the provided Layer to the specified repo.
+func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) error {
+	o, err := makeOptions(repo, options...)
 	if err != nil {
 		return err
 	}
-	scopes := scopesForUploadingImage(ref, []v1.Layer{layer})
-	tr, err := transport.New(ref.Context().Registry, o.auth, o.transport, scopes)
+	scopes := scopesForUploadingImage(repo, []v1.Layer{layer})
+	tr, err := transport.New(repo.Registry, o.auth, o.transport, scopes)
 	if err != nil {
 		return err
 	}
 	w := writer{
-		ref:    ref,
+		repo:   repo,
 		client: &http.Client{Transport: tr},
 	}
 
@@ -564,9 +564,9 @@ func Tag(tag name.Tag, t Taggable, options ...Option) error {
 		return err
 	}
 	w := writer{
-		ref:    tag,
+		repo:   tag.Context(),
 		client: &http.Client{Transport: tr},
 	}
 
-	return w.commitImage(t)
+	return w.commitImage(t, tag)
 }
diff --git a/pkg/v1/tarball/image.go b/pkg/v1/tarball/image.go
index 0d36125d..67a5d437 100644
--- a/pkg/v1/tarball/image.go
+++ b/pkg/v1/tarball/image.go
@@ -79,15 +79,17 @@ func Image(opener Opener, tag *name.Tag) (v1.Image, error) {
 	}
 
 	// Peek at the first layer and see if it's compressed.
-	compressed, err := img.areLayersCompressed()
-	if err != nil {
-		return nil, err
-	}
-	if compressed {
-		c := compressedImage{
-			image: img,
+	if len(img.imgDescriptor.Layers) > 0 {
+		compressed, err := img.areLayersCompressed()
+		if err != nil {
+			return nil, err
+		}
+		if compressed {
+			c := compressedImage{
+				image: img,
+			}
+			return partial.CompressedToImage(&c)
 		}
-		return partial.CompressedToImage(&c)
 	}
 
 	uc := uncompressedImage{
diff --git a/pkg/v1/tarball/layer.go b/pkg/v1/tarball/layer.go
index 314a51f1..23729af4 100644
--- a/pkg/v1/tarball/layer.go
+++ b/pkg/v1/tarball/layer.go
@@ -122,7 +122,7 @@ func LayerFromOpener(opener Opener, opts ...LayerOption) (v1.Layer, error) {
 }
 
 // LayerFromReader returns a v1.Layer given a io.Reader.
-func LayerFromReader(reader io.Reader) (v1.Layer, error) {
+func LayerFromReader(reader io.Reader, opts ...LayerOption) (v1.Layer, error) {
 	// Buffering due to Opener requiring multiple calls.
 	a, err := ioutil.ReadAll(reader)
 	if err != nil {
@@ -130,7 +130,7 @@ func LayerFromReader(reader io.Reader) (v1.Layer, error) {
 	}
 	return LayerFromOpener(func() (io.ReadCloser, error) {
 		return ioutil.NopCloser(bytes.NewReader(a)), nil
-	})
+	}, opts...)
 }
 
 func computeDigest(opener Opener, compressed bool, compression int) (v1.Hash, int64, error) {


Test patch:
diff --git a/pkg/authn/keychain_test.go b/pkg/authn/keychain_test.go
index feb882ef..646c8ab5 100644
--- a/pkg/authn/keychain_test.go
+++ b/pkg/authn/keychain_test.go
@@ -107,7 +107,7 @@ func TestVariousPaths(t *testing.T) {
 		},
 	}, {
 		target:  defaultRegistry,
-		content: fmt.Sprintf(`{"auths": {"%s": {"auth": %q}}}`, defaultAuthKey, encode("foo", "bar")),
+		content: fmt.Sprintf(`{"auths": {"%s": {"auth": %q}}}`, DefaultAuthKey, encode("foo", "bar")),
 		cfg: &AuthConfig{
 			Username: "foo",
 			Password: "bar",
diff --git a/pkg/crane/crane_test.go b/pkg/crane/crane_test.go
index f6bedc40..6bf14d5e 100644
--- a/pkg/crane/crane_test.go
+++ b/pkg/crane/crane_test.go
@@ -23,6 +23,7 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"net/url"
+	"os"
 	"testing"
 
 	"github.com/google/go-containerregistry/pkg/crane"
@@ -199,6 +200,8 @@ func TestCraneTarball(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
+	defer os.Remove(tmp.Name())
+
 	img, err := random.Image(1024, 5)
 	if err != nil {
 		t.Fatal(err)
@@ -228,6 +231,43 @@ func TestCraneTarball(t *testing.T) {
 	}
 }
 
+func TestCraneSaveLegacy(t *testing.T) {
+	t.Parallel()
+	// Write an image as a legacy tarball.
+	tmp, err := ioutil.TempFile("", "")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.Remove(tmp.Name())
+
+	img, err := random.Image(1024, 5)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if err := crane.SaveLegacy(img, "test/crane", tmp.Name()); err != nil {
+		t.Errorf("SaveOCI: %v", err)
+	}
+}
+
+func TestCraneSaveOCI(t *testing.T) {
+	t.Parallel()
+	// Write an image as an OCI image layout.
+	tmp, err := ioutil.TempDir("", "")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(tmp)
+
+	img, err := random.Image(1024, 5)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if err := crane.SaveOCI(img, tmp); err != nil {
+		t.Errorf("SaveLegacy: %v", err)
+	}
+}
+
 func TestCraneFilesystem(t *testing.T) {
 	t.Parallel()
 	tmp, err := ioutil.TempFile("", "")
@@ -288,7 +328,7 @@ func TestCraneFilesystem(t *testing.T) {
 
 func TestBadInputs(t *testing.T) {
 	t.Parallel()
-	invalid := "@@@@@@"
+	invalid := "/dev/null/@@@@@@"
 
 	// Create a valid image reference that will fail with not found.
 	s := httptest.NewServer(http.NotFoundHandler())
@@ -312,6 +352,9 @@ func TestBadInputs(t *testing.T) {
 		{"Delete(invalid)", crane.Delete(invalid)},
 		{"Delete: 404", crane.Delete(valid404)},
 		{"Save(_, invalid)", crane.Save(nil, invalid, "")},
+		{"SaveLegacy(_, invalid)", crane.SaveLegacy(nil, invalid, "")},
+		{"SaveLegacy(_, invalid)", crane.SaveLegacy(nil, valid404, invalid)},
+		{"SaveOCI(_, invalid)", crane.SaveOCI(nil, "")},
 		{"Copy(invalid, invalid)", crane.Copy(invalid, invalid)},
 		{"Copy(404, invalid)", crane.Copy(valid404, invalid)},
 		{"Copy(404, 404)", crane.Copy(valid404, valid404)},
@@ -321,7 +364,7 @@ func TestBadInputs(t *testing.T) {
 		// These return multiple values, which are hard to use as expressions.
 		{"Pull(invalid)", e(crane.Pull(invalid))},
 		{"Digest(invalid)", e(crane.Digest(invalid))},
-		{"Manifet(invalid)", e(crane.Manifest(invalid))},
+		{"Manifest(invalid)", e(crane.Manifest(invalid))},
 		{"Config(invalid)", e(crane.Config(invalid))},
 		{"Config(404)", e(crane.Config(valid404))},
 		{"ListTags(invalid)", e(crane.ListTags(invalid))},
diff --git a/pkg/gcrane/copy_test.go b/pkg/gcrane/copy_test.go
index fcaec5bc..553173b3 100644
--- a/pkg/gcrane/copy_test.go
+++ b/pkg/gcrane/copy_test.go
@@ -17,30 +17,193 @@ package gcrane
 import (
 	"bytes"
 	"context"
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
+	"os"
+	"path"
+	"strings"
 	"testing"
 	"time"
 
 	"github.com/google/go-cmp/cmp"
+	ggcrtest "github.com/google/go-containerregistry/pkg/internal/httptest"
 	"github.com/google/go-containerregistry/pkg/internal/retry"
 	"github.com/google/go-containerregistry/pkg/logs"
 	"github.com/google/go-containerregistry/pkg/name"
+	"github.com/google/go-containerregistry/pkg/registry"
 	"github.com/google/go-containerregistry/pkg/v1/google"
+	"github.com/google/go-containerregistry/pkg/v1/partial"
+	"github.com/google/go-containerregistry/pkg/v1/random"
+	"github.com/google/go-containerregistry/pkg/v1/remote"
 	"github.com/google/go-containerregistry/pkg/v1/remote/transport"
 	"github.com/google/go-containerregistry/pkg/v1/types"
 )
 
 func mustRepo(s string) name.Repository {
-	repo, err := name.NewRepository(s, name.WeakValidation)
+	repo, err := name.NewRepository(s)
 	if err != nil {
 		panic(err)
 	}
 	return repo
 }
 
+type fakeGCR struct {
+	h     http.Handler
+	repos map[string]google.Tags
+	t     *testing.T
+}
+
+func (gcr *fakeGCR) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	gcr.t.Logf("%s %s", r.Method, r.URL)
+	if strings.HasPrefix(r.URL.Path, "/v2/") && strings.HasSuffix(r.URL.Path, "/tags/list") {
+		repo := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/v2/"), "/tags/list")
+		if tags, ok := gcr.repos[repo]; !ok {
+			w.WriteHeader(http.StatusNotFound)
+		} else {
+			gcr.t.Logf("%+v", tags)
+			json.NewEncoder(w).Encode(tags)
+		}
+	} else {
+		gcr.h.ServeHTTP(w, r)
+	}
+}
+
+func newFakeGCR(stuff map[name.Reference]partial.Describable, t *testing.T) (*fakeGCR, error) {
+	h := registry.New()
+
+	repos := make(map[string]google.Tags)
+
+	for ref, thing := range stuff {
+		repo := ref.Context().RepositoryStr()
+		tags, ok := repos[repo]
+		if !ok {
+			tags = google.Tags{
+				Name:     repo,
+				Children: []string{},
+			}
+		}
+
+		// Populate the "child" field.
+		for parentPath := repo; parentPath != "."; parentPath = path.Dir(parentPath) {
+			child, parent := path.Base(parentPath), path.Dir(parentPath)
+			tags, ok := repos[parent]
+			if !ok {
+				tags = google.Tags{}
+			}
+			for _, c := range repos[parent].Children {
+				if c == child {
+					break
+				}
+			}
+			tags.Children = append(tags.Children, child)
+			repos[parent] = tags
+		}
+
+		// Populate the "manifests" and "tags" field.
+		d, err := thing.Digest()
+		if err != nil {
+			return nil, err
+		}
+		mt, err := thing.MediaType()
+		if err != nil {
+			return nil, err
+		}
+		if tags.Manifests == nil {
+			tags.Manifests = make(map[string]google.ManifestInfo)
+		}
+		mi, ok := tags.Manifests[d.String()]
+		if !ok {
+			mi = google.ManifestInfo{
+				MediaType: string(mt),
+				Tags:      []string{},
+			}
+		}
+		if tag, ok := ref.(name.Tag); ok {
+			tags.Tags = append(tags.Tags, tag.Identifier())
+			mi.Tags = append(mi.Tags, tag.Identifier())
+		}
+		tags.Manifests[d.String()] = mi
+		repos[repo] = tags
+	}
+
+	return &fakeGCR{h: h, t: t, repos: repos}, nil
+}
+
+func TestCopy(t *testing.T) {
+	logs.Warn.SetOutput(os.Stderr)
+	src := "gcr.io/test/gcrane"
+	dst := "gcr.io/test/gcrane/copy"
+
+	oneTag, err := random.Image(1024, 5)
+	if err != nil {
+		t.Fatal(err)
+	}
+	twoTags, err := random.Image(1024, 5)
+	if err != nil {
+		t.Fatal(err)
+	}
+	noTags, err := random.Image(1024, 3)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	latestRef, err := name.ParseReference(src)
+	if err != nil {
+		t.Fatal(err)
+	}
+	oneTagRef := latestRef.Context().Tag("bar")
+
+	d, err := noTags.Digest()
+	if err != nil {
+		t.Fatal(err)
+	}
+	noTagsRef := latestRef.Context().Digest(d.String())
+	fooRef := latestRef.Context().Tag("foo")
+
+	// Set up a fake GCR.
+	h, err := newFakeGCR(map[name.Reference]partial.Describable{
+		oneTagRef: oneTag,
+		latestRef: twoTags,
+		fooRef:    twoTags,
+		noTagsRef: noTags,
+	}, t)
+	if err != nil {
+		t.Fatal(err)
+	}
+	s, err := ggcrtest.NewTLSServer("gcr.io", h)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer s.Close()
+
+	// Make sure we don't actually talk to GCR.
+	http.DefaultTransport = s.Client().Transport
+
+	if err := remote.Write(latestRef, twoTags); err != nil {
+		t.Fatal(err)
+	}
+	if err := remote.Write(fooRef, twoTags); err != nil {
+		t.Fatal(err)
+	}
+	if err := remote.Write(oneTagRef, oneTag); err != nil {
+		t.Fatal(err)
+	}
+	if err := remote.Write(noTagsRef, noTags); err != nil {
+		t.Fatal(err)
+	}
+
+	if err := Copy(src, dst); err != nil {
+		t.Fatal(err)
+	}
+
+	if err := CopyRepository(context.Background(), src, dst); err != nil {
+		t.Fatal(err)
+	}
+}
+
 func TestRename(t *testing.T) {
 	c := copier{
 		srcRepo: mustRepo("gcr.io/foo"),
diff --git a/pkg/internal/httptest/httptest.go b/pkg/internal/httptest/httptest.go
new file mode 100644
index 00000000..52983112
--- /dev/null
+++ b/pkg/internal/httptest/httptest.go
@@ -0,0 +1,103 @@
+// Copyright 2020 Google LLC 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 httptest
+
+import (
+	"bytes"
+	"context"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/pem"
+	"math/big"
+	"net"
+	"net/http"
+	"net/http/httptest"
+	"time"
+)
+
+// NewTLSServer returns an httptest server, with an http client that has been configured to
+// send all requests to the returned server. The TLS certs are generated for the given domain.
+// If you need a transport, Client().Transport is correctly configured.
+func NewTLSServer(domain string, handler http.Handler) (*httptest.Server, error) {
+	s := httptest.NewUnstartedServer(handler)
+
+	template := x509.Certificate{
+		SerialNumber: big.NewInt(1),
+		NotBefore:    time.Now().Add(-1 * time.Hour),
+		NotAfter:     time.Now().Add(time.Hour),
+		IPAddresses: []net.IP{
+			net.IPv4(127, 0, 0, 1),
+			net.IPv6loopback,
+		},
+		DNSNames: []string{domain},
+
+		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+		BasicConstraintsValid: true,
+		IsCA:                  true,
+	}
+
+	priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
+	if err != nil {
+		return nil, err
+	}
+
+	b, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
+	if err != nil {
+		return nil, err
+	}
+
+	pc := &bytes.Buffer{}
+	if err := pem.Encode(pc, &pem.Block{Type: "CERTIFICATE", Bytes: b}); err != nil {
+		return nil, err
+	}
+
+	ek, err := x509.MarshalECPrivateKey(priv)
+	if err != nil {
+		return nil, err
+	}
+
+	pk := &bytes.Buffer{}
+	if err := pem.Encode(pk, &pem.Block{Type: "EC PRIVATE KEY", Bytes: ek}); err != nil {
+		return nil, err
+	}
+
+	c, err := tls.X509KeyPair(pc.Bytes(), pk.Bytes())
+	if err != nil {
+		return nil, err
+	}
+	s.TLS = &tls.Config{
+		Certificates: []tls.Certificate{c},
+	}
+	s.StartTLS()
+
+	certpool := x509.NewCertPool()
+	certpool.AddCert(s.Certificate())
+
+	t := &http.Transport{
+		TLSClientConfig: &tls.Config{
+			RootCAs: certpool,
+		},
+		DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+			return net.Dial(s.Listener.Addr().Network(), s.Listener.Addr().String())
+		},
+	}
+	s.Client().Transport = t
+
+	return s, nil
+}
diff --git a/pkg/internal/legacy/copy_test.go b/pkg/internal/legacy/copy_test.go
index 414e3c4c..49c0c58f 100644
--- a/pkg/internal/legacy/copy_test.go
+++ b/pkg/internal/legacy/copy_test.go
@@ -66,7 +66,7 @@ func TestCopySchema1(t *testing.T) {
 	}
 
 	// Populate the registry with a layer and a schema 1 manifest referencing it.
-	if err := remote.WriteLayer(layerRef, layer); err != nil {
+	if err := remote.WriteLayer(layerRef.Context(), layer); err != nil {
 		t.Fatal(err)
 	}
 	manifest := schema1{
diff --git a/pkg/v1/google/auth_test.go b/pkg/v1/google/auth_test.go
index 7801669f..4ba5c1dc 100644
--- a/pkg/v1/google/auth_test.go
+++ b/pkg/v1/google/auth_test.go
@@ -160,7 +160,7 @@ func TestKeychainDockerHub(t *testing.T) {
 	}
 }
 
-func TestKeychainGCR(t *testing.T) {
+func TestKeychainGCRandAR(t *testing.T) {
 	cases := []string{
 		"gcr.io",
 		"us.gcr.io",
@@ -168,6 +168,10 @@ func TestKeychainGCR(t *testing.T) {
 		"eu.gcr.io",
 		"staging-k8s.gcr.io",
 		"global.gcr.io",
+		"us-docker.pkg.dev",
+		"asia-docker.pkg.dev",
+		"europe-docker.pkg.dev",
+		"us-central1-docker.pkg.dev",
 	}
 
 	// Env should fail.
@@ -184,18 +188,18 @@ func TestKeychainGCR(t *testing.T) {
 			GetGcloudCmd = newGcloudCmdMock("success")
 
 			if auth, err := Keychain.Resolve(mustRegistry(tc)); err != nil {
-				t.Errorf("expected success, got: %v", err)
+				t.Errorf("expected success for %v, got: %v", tc, err)
 			} else if auth == authn.Anonymous {
-				t.Errorf("expected not anonymous auth, got: %v", auth)
+				t.Errorf("expected not anonymous auth for %v, got: %v", tc, auth)
 			}
 
 			// Make gcloud fail to test that caching works.
 			GetGcloudCmd = newGcloudCmdMock("badoutput")
 
 			if auth, err := Keychain.Resolve(mustRegistry(tc)); err != nil {
-				t.Errorf("expected success, got: %v", err)
+				t.Errorf("expected success for %v, got: %v", tc, err)
 			} else if auth == authn.Anonymous {
-				t.Errorf("expected not anonymous auth, got: %v", auth)
+				t.Errorf("expected not anonymous auth for %v, got: %v", tc, auth)
 			}
 		})
 	}
diff --git a/pkg/v1/google/list_test.go b/pkg/v1/google/list_test.go
index e7c71623..19186175 100644
--- a/pkg/v1/google/list_test.go
+++ b/pkg/v1/google/list_test.go
@@ -16,6 +16,7 @@ package google
 
 import (
 	"bytes"
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"net/http/httptest"
@@ -37,6 +38,93 @@ func mustParseDuration(t *testing.T, d string) time.Duration {
 	return dur
 }
 
+func TestRoundtrip(t *testing.T) {
+	raw := rawManifestInfo{
+		Size:      "100",
+		MediaType: "hi",
+		Created:   "12345678",
+		Uploaded:  "23456789",
+		Tags:      []string{"latest"},
+	}
+
+	og, err := json.Marshal(raw)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	parsed := ManifestInfo{}
+	if err := json.Unmarshal(og, &parsed); err != nil {
+		t.Fatal(err)
+	}
+
+	roundtripped, err := json.Marshal(parsed)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if diff := cmp.Diff(og, roundtripped); diff != "" {
+		t.Errorf("ManifestInfo can't roundtrip: (-want +got) = %s", diff)
+	}
+}
+
+// GCR returns timeUploaded as string
+func TestTimeUploadedMsAsString(t *testing.T) {
+	data := []byte(`
+		{
+			"imageSizeBytes": "100",
+			"mediaType": "hi",
+	  		"tag": ["latest"],
+	  		"timeCreatedMs": "12345678",
+	  		"timeUploadedMs": "23456789"
+		}
+	`)
+
+	raw := rawManifestInfo{}
+	if err := json.Unmarshal(data, &raw); err != nil {
+		t.Fatal(err)
+	}
+
+	expectedRaw := rawManifestInfo{
+		Size:      "100",
+		MediaType: "hi",
+		Created:   "12345678",
+		Uploaded:  "23456789",
+		Tags:      []string{"latest"},
+	}
+
+	if diff := cmp.Diff(expectedRaw, raw); diff != "" {
+		t.Errorf("Can't unmarshal rawManifestInfo with string timeUploadedMs: (-want +got) = %s", diff)
+	}
+}
+
+// AR returns timeUploaded as int64, and timeCreatedMs is missing
+func TestTimeUploadedMsAsInt64(t *testing.T) {
+	data := []byte(`
+		{
+			"imageSizeBytes": "100",
+			"mediaType": "hi",
+	  		"tag": ["latest"],
+	  		"timeUploadedMs": 23456789
+		}
+	`)
+
+	raw := rawManifestInfo{}
+	if err := json.Unmarshal(data, &raw); err != nil {
+		t.Fatal(err)
+	}
+
+	expectedRaw := rawManifestInfo{
+		Size:      "100",
+		MediaType: "hi",
+		Uploaded:  "23456789",
+		Tags:      []string{"latest"},
+	}
+
+	if diff := cmp.Diff(expectedRaw, raw); diff != "" {
+		t.Errorf("Can't unmarshal rawManifestInfo with int64 timeUploadedMs: (-want +got) = %s", diff)
+	}
+}
+
 func TestList(t *testing.T) {
 	cases := []struct {
 		name         string
diff --git a/pkg/v1/mutate/index_test.go b/pkg/v1/mutate/index_test.go
index 7ebf0be0..65930e74 100644
--- a/pkg/v1/mutate/index_test.go
+++ b/pkg/v1/mutate/index_test.go
@@ -16,6 +16,7 @@ package mutate_test
 
 import (
 	"log"
+	"strings"
 	"testing"
 
 	v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -44,6 +45,11 @@ func TestAppendIndex(t *testing.T) {
 		t.Fatal(err)
 	}
 
+	weirdHash := v1.Hash{
+		Algorithm: "sha256",
+		Hex:       strings.Repeat("0", 64),
+	}
+
 	add := mutate.AppendManifests(base, mutate.IndexAddendum{
 		Add: idx,
 		Descriptor: v1.Descriptor{
@@ -52,12 +58,20 @@ func TestAppendIndex(t *testing.T) {
 	}, mutate.IndexAddendum{
 		Add: img,
 		Descriptor: v1.Descriptor{
-			URLs: []string{"image.example.com"},
+			MediaType: types.MediaType("application/xml"),
+			URLs:      []string{"image.example.com"},
 		},
 	}, mutate.IndexAddendum{
 		Add: l,
 		Descriptor: v1.Descriptor{
-			URLs: []string{"layer.example.com"},
+			URLs:   []string{"layer.example.com"},
+			Size:   1337,
+			Digest: weirdHash,
+			Platform: &v1.Platform{
+				OS:           "haiku",
+				Architecture: "toaster",
+			},
+			Annotations: map[string]string{"weird": "true"},
 		},
 	})
 
@@ -93,6 +107,10 @@ func TestAppendIndex(t *testing.T) {
 		}
 	}
 
+	if got, want := m.Manifests[4].MediaType, types.MediaType("application/xml"); got != want {
+		t.Errorf("wrong MediaType for layer: %s != %s", got, want)
+	}
+
 	if got, want := m.Manifests[5].MediaType, types.OCIUncompressedRestrictedLayer; got != want {
 		t.Errorf("wrong MediaType for layer: %s != %s", got, want)
 	}
diff --git a/pkg/v1/mutate/mutate_test.go b/pkg/v1/mutate/mutate_test.go
index 3f0a4917..16a4fe85 100644
--- a/pkg/v1/mutate/mutate_test.go
+++ b/pkg/v1/mutate/mutate_test.go
@@ -315,6 +315,23 @@ func TestMutateMediaType(t *testing.T) {
 		t.Errorf("MediaType should not be set for OCI media types: %v", manifest.MediaType)
 	}
 
+	want = types.DockerManifestSchema2
+	img = mutate.MediaType(img, want)
+	got, err = img.MediaType()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if want != got {
+		t.Errorf("%q != %q", want, got)
+	}
+	manifest, err = img.Manifest()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if manifest.MediaType != want {
+		t.Errorf("MediaType should be set for Docker media types: %v", manifest.MediaType)
+	}
+
 	want = types.OCIImageIndex
 	idx := mutate.IndexMediaType(empty.Index, want)
 	got, err = idx.MediaType()
@@ -331,6 +348,23 @@ func TestMutateMediaType(t *testing.T) {
 	if im.MediaType != "" {
 		t.Errorf("MediaType should not be set for OCI media types: %v", im.MediaType)
 	}
+
+	want = types.DockerManifestList
+	idx = mutate.IndexMediaType(idx, want)
+	got, err = idx.MediaType()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if want != got {
+		t.Errorf("%q != %q", want, got)
+	}
+	im, err = idx.IndexManifest()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if im.MediaType != want {
+		t.Errorf("MediaType should be set for Docker media types: %v", im.MediaType)
+	}
 }
 
 func TestAppendStreamableLayer(t *testing.T) {
diff --git a/pkg/v1/remote/layer_test.go b/pkg/v1/remote/layer_test.go
index 4708d83d..70c5ef75 100644
--- a/pkg/v1/remote/layer_test.go
+++ b/pkg/v1/remote/layer_test.go
@@ -56,7 +56,7 @@ func TestRemoteLayer(t *testing.T) {
 	}
 
 	t.Log(ref)
-	if err := WriteLayer(ref, layer); err != nil {
+	if err := WriteLayer(ref.Context(), layer); err != nil {
 		t.Fatalf("failed to WriteLayer: %v", err)
 	}
 
diff --git a/pkg/v1/remote/transport/error_test.go b/pkg/v1/remote/transport/error_test.go
index 4ca84708..85bd1279 100644
--- a/pkg/v1/remote/transport/error_test.go
+++ b/pkg/v1/remote/transport/error_test.go
@@ -17,8 +17,10 @@ package transport
 import (
 	"bytes"
 	"encoding/json"
+	"errors"
 	"io/ioutil"
 	"net/http"
+	"net/url"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
@@ -79,9 +81,10 @@ func TestCheckErrorNil(t *testing.T) {
 
 func TestCheckErrorNotError(t *testing.T) {
 	tests := []struct {
-		code int
-		body string
-		msg  string
+		code    int
+		body    string
+		msg     string
+		request *http.Request
 	}{{
 		code: http.StatusBadRequest,
 		body: "",
@@ -89,13 +92,27 @@ func TestCheckErrorNotError(t *testing.T) {
 	}, {
 		code: http.StatusUnauthorized,
 		body: "Not JSON",
-		msg:  "unsupported status code 401; body: Not JSON",
+		msg:  "GET https://example.com/somepath?access_token=REDACTED&scope=foo&service=bar: unsupported status code 401; body: Not JSON",
+		request: &http.Request{
+			Method: http.MethodGet,
+			URL: &url.URL{
+				Scheme: "https",
+				Host:   "example.com",
+				Path:   "somepath",
+				RawQuery: url.Values{
+					"scope":        []string{"foo"},
+					"service":      []string{"bar"},
+					"access_token": []string{"hunter2"},
+				}.Encode(),
+			},
+		},
 	}}
 
 	for _, test := range tests {
 		resp := &http.Response{
 			StatusCode: test.code,
 			Body:       ioutil.NopCloser(bytes.NewBufferString(test.body)),
+			Request:    test.request,
 		}
 
 		err := CheckError(resp, http.StatusOK)
@@ -151,7 +168,7 @@ func TestCheckErrorWithError(t *testing.T) {
 			}},
 			StatusCode: 400,
 		},
-		msg: "multiple errors returned: NAME_INVALID: a message for you;SIZE_INVALID: another message for you; with some details",
+		msg: "multiple errors returned: NAME_INVALID: a message for you; SIZE_INVALID: another message for you; with some details",
 	}}
 
 	for _, test := range tests {
@@ -175,3 +192,28 @@ func TestCheckErrorWithError(t *testing.T) {
 		}
 	}
 }
+
+func TestBodyError(t *testing.T) {
+	expectedErr := errors.New("whoops")
+	resp := &http.Response{
+		StatusCode: http.StatusOK,
+		Body:       &errReadCloser{expectedErr},
+	}
+	if err := CheckError(resp, http.StatusNotFound); err == nil {
+		t.Errorf("CheckError() = nil, wanted error %v", expectedErr)
+	} else if err != expectedErr {
+		t.Errorf("CheckError() = %v, wanted %v", err, expectedErr)
+	}
+}
+
+type errReadCloser struct {
+	err error
+}
+
+func (e *errReadCloser) Read(p []byte) (int, error) {
+	return 0, e.err
+}
+
+func (e *errReadCloser) Close() error {
+	return e.err
+}
diff --git a/pkg/v1/remote/write_test.go b/pkg/v1/remote/write_test.go
index 89161487..40b6cfef 100644
--- a/pkg/v1/remote/write_test.go
+++ b/pkg/v1/remote/write_test.go
@@ -70,7 +70,7 @@ func TestUrl(t *testing.T) {
 
 	for _, test := range tests {
 		w := &writer{
-			ref: mustNewTag(t, test.tag),
+			repo: mustNewTag(t, test.tag).Context(),
 		}
 		if got, want := w.url(test.path), test.url; got.String() != want {
 			t.Errorf("url(%v) = %v, want %v", test.path, got.String(), want)
@@ -92,7 +92,7 @@ func TestNextLocation(t *testing.T) {
 
 	ref := mustNewTag(t, "gcr.io/foo/bar:latest")
 	w := &writer{
-		ref: ref,
+		repo: ref.Context(),
 	}
 
 	for _, test := range tests {
@@ -165,7 +165,7 @@ func setupWriterWithServer(server *httptest.Server, repo string) (*writer, close
 	}
 
 	return &writer{
-		ref:    tag,
+		repo:   tag.Context(),
 		client: http.DefaultClient,
 	}, server, nil
 }
@@ -701,7 +701,7 @@ func TestUploadOne(t *testing.T) {
 	}
 	ml := &MountableLayer{
 		Layer:     l,
-		Reference: w.ref,
+		Reference: w.repo.Digest(h.String()),
 	}
 	if err := w.uploadOne(ml); err != nil {
 		t.Errorf("uploadOne() = %v", err)
@@ -811,8 +811,8 @@ func TestCommitImage(t *testing.T) {
 	}
 	defer closer.Close()
 
-	if err := w.commitImage(img); err != nil {
-		t.Errorf("commitBlob() = %v", err)
+	if err := w.commitImage(img, w.repo.Tag("latest")); err != nil {
+		t.Errorf("commitImage() = %v", err)
 	}
 }
 
@@ -1069,7 +1069,7 @@ func TestScopesForUploadingImage(t *testing.T) {
 	}
 
 	for _, tc := range testCases {
-		actual := scopesForUploadingImage(tc.reference, tc.layers)
+		actual := scopesForUploadingImage(tc.reference.Context(), tc.layers)
 
 		if want, got := tc.expected[0], actual[0]; want != got {
 			t.Errorf("TestScopesForUploadingImage() %s: Wrong first scope; want %v, got %v", tc.name, want, got)