Repository: google/go-containerregistry (go)

Issue:
Add different formats for crane pull
There are multiple reasonable on-disk representations of an image:
* [v1/layout](https://github.com/google/go-containerregistry/tree/master/pkg/v1/layout)
* [v1/tarball](https://github.com/google/go-containerregistry/tree/master/pkg/v1/tarball)
* [legacy/tarball](https://github.com/google/go-containerregistry/tree/master/pkg/legacy/tarball)

Crane currently just pulls v1/tarball for everything, which is compatible with `docker load`, but apparently incompatible with [dive](https://github.com/wagoodman/dive). It would be nice if we could specify the format when pulling to be broadly compatible with different tools.

What I'm thinking:

```
# v1/tarball (also default)
crane pull --format=tarball ubuntu ubuntu.tar

# legacy/tarball
crane pull --format=legacy ubuntu ubuntu.tar

# v1/layout
crane pull --format=oci ubuntu ubuntu/
```

Pull request description:
Add --format flag to `crane pull`
Fixes #621

This adds the --format flag with values "tarball" (default), "legacy"
(for saving pkg/legacy/tarball images, for compatibility with some
software that assumes the old format), and "oci", which saves the image
as an OCI image layout.

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/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_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_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/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/legacy/tarball/write.go b/pkg/legacy/tarball/write.go
index e5555fb4..710ff067 100644
--- a/pkg/legacy/tarball/write.go
+++ b/pkg/legacy/tarball/write.go
@@ -22,7 +22,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"strings"
 
 	"github.com/google/go-containerregistry/pkg/legacy"
@@ -249,21 +248,23 @@ func MultiWrite(refToImage map[name.Reference]v1.Image, w io.Writer) error {
 				return err
 			}
 			layerFiles[i] = fmt.Sprintf("%s/layer.tar", cur.config.ID)
-			u, err := l.Uncompressed()
+
+			// If the v1.Layer implements UncompressedSize efficiently, use that
+			// for the tar header. Otherwise, this iterates over Uncompressed().
+			// NOTE: If using a streaming layer, this may consume the layer.
+			size, err := partial.UncompressedSize(l)
 			if err != nil {
 				return err
 			}
-			defer u.Close()
-			// Reads the entire uncompressed blob into memory! Can be avoided
-			// for some layer implementations where the uncompressed blob is
-			// stored on disk and the layer can just stat the file.
-			uncompressedBlob, err := ioutil.ReadAll(u)
+			u, err := l.Uncompressed()
 			if err != nil {
 				return err
 			}
-			if err := writeTarEntry(tf, layerFiles[i], bytes.NewReader(uncompressedBlob), int64(len(uncompressedBlob))); err != nil {
+			defer u.Close()
+			if err := writeTarEntry(tf, layerFiles[i], u, size); err != nil {
 				return err
 			}
+
 			j, err := cur.json()
 			if 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/list.go b/pkg/v1/google/list.go
index e5a530bd..3b40b36f 100644
--- a/pkg/v1/google/list.go
+++ b/pkg/v1/google/list.go
@@ -121,6 +121,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:  toUnixMs(m.Uploaded),
+		Tags:      m.Tags,
+	})
+}
+
 // UnmarshalJSON implements json.Unmarshaler
 func (m *ManifestInfo) UnmarshalJSON(data []byte) error {
 	raw := rawManifestInfo{}
diff --git a/pkg/v1/partial/with.go b/pkg/v1/partial/with.go
index c75d17a0..08bdce3e 100644
--- a/pkg/v1/partial/with.go
+++ b/pkg/v1/partial/with.go
@@ -129,12 +129,6 @@ func RawConfigFile(i WithConfigFile) ([]byte, error) {
 	return json.Marshal(cfg)
 }
 
-// WithUncompressedLayer defines the subset of v1.Image used by these helper methods
-type WithUncompressedLayer interface {
-	// UncompressedLayer is like UncompressedBlob, but takes the "diff id".
-	UncompressedLayer(v1.Hash) (io.ReadCloser, error)
-}
-
 // WithRawManifest defines the subset of v1.Image used by these helper methods
 type WithRawManifest interface {
 	// RawManifest returns the serialized bytes of this image's config file.
@@ -348,3 +342,41 @@ func Descriptor(d Describable) (*v1.Descriptor, error) {
 
 	return &desc, nil
 }
+
+type withUncompressedSize interface {
+	UncompressedSize() (int64, error)
+}
+
+// UncompressedSize returns the size of the Uncompressed layer. If the
+// underlying implementation doesn't implement UncompressedSize directly,
+// this will compute the uncompressedSize by reading everything returned
+// by Compressed(). This is potentially expensive and may consume the contents
+// for streaming layers.
+func UncompressedSize(l v1.Layer) (int64, error) {
+	// If the layer implements UncompressedSize itself, return that.
+	if wus, ok := l.(withUncompressedSize); ok {
+		return wus.UncompressedSize()
+	}
+
+	// Otherwise, try to unwrap any partial implementations to see
+	// if the wrapped struct implements UncompressedSize.
+	if ule, ok := l.(*uncompressedLayerExtender); ok {
+		if wus, ok := ule.UncompressedLayer.(withUncompressedSize); ok {
+			return wus.UncompressedSize()
+		}
+	}
+	if cle, ok := l.(*compressedLayerExtender); ok {
+		if wus, ok := cle.CompressedLayer.(withUncompressedSize); ok {
+			return wus.UncompressedSize()
+		}
+	}
+
+	// The layer doesn't implement UncompressedSize, we need to compute it.
+	rc, err := l.Uncompressed()
+	if err != nil {
+		return -1, err
+	}
+	defer rc.Close()
+
+	return io.Copy(ioutil.Discard, rc)
+}
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{


Test patch:
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/legacy/tarball/write_test.go b/pkg/legacy/tarball/write_test.go
index 3b9ce495..717ee558 100644
--- a/pkg/legacy/tarball/write_test.go
+++ b/pkg/legacy/tarball/write_test.go
@@ -24,6 +24,7 @@ import (
 	"github.com/google/go-containerregistry/pkg/name"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/google/go-containerregistry/pkg/v1/mutate"
+	"github.com/google/go-containerregistry/pkg/v1/partial"
 	"github.com/google/go-containerregistry/pkg/v1/random"
 	"github.com/google/go-containerregistry/pkg/v1/tarball"
 	"github.com/google/go-containerregistry/pkg/v1/types"
@@ -401,3 +402,57 @@ func TestMultiWriteMismatchedHistory(t *testing.T) {
 		t.Errorf("Got unexpected error when writing image with mismatched history & layer, got %v, want substring %q", err, want)
 	}
 }
+
+type fastSizeLayer struct {
+	v1.Layer
+	size   int64
+	called bool
+}
+
+func (l *fastSizeLayer) UncompressedSize() (int64, error) {
+	l.called = true
+	return l.size, nil
+}
+
+func TestUncompressedSize(t *testing.T) {
+	// Make a random image
+	img, err := random.Image(256, 8)
+	if err != nil {
+		t.Fatalf("Error creating random image: %v", err)
+	}
+
+	rand, err := random.Layer(1000, types.DockerLayer)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	size, err := partial.UncompressedSize(rand)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	l := &fastSizeLayer{Layer: rand, size: size}
+
+	img, err = mutate.AppendLayers(img, l)
+	if err != nil {
+		t.Fatal(err)
+	}
+	tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
+	if err != nil {
+		t.Fatalf("Error creating test tag: %v", err)
+	}
+	// Make a tempfile for tarball writes.
+	fp, err := ioutil.TempFile("", "")
+	if err != nil {
+		t.Fatalf("Error creating temp file: %v", err)
+	}
+	t.Log(fp.Name())
+	defer fp.Close()
+	defer os.Remove(fp.Name())
+	if err := Write(tag, img, fp); err != nil {
+		t.Fatalf("Write(): %v", err)
+	}
+	if !l.called {
+		t.Errorf("expected UncompressedSize to be called, but it wasn't")
+	}
+}
diff --git a/pkg/v1/google/list_test.go b/pkg/v1/google/list_test.go
index e7c71623..709aaf97 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,35 @@ 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)
+	}
+}
+
 func TestList(t *testing.T) {
 	cases := []struct {
 		name         string
diff --git a/pkg/v1/partial/compressed_test.go b/pkg/v1/partial/compressed_test.go
index 005f50cd..2167ea6d 100644
--- a/pkg/v1/partial/compressed_test.go
+++ b/pkg/v1/partial/compressed_test.go
@@ -97,6 +97,12 @@ func (l *noDiffID) Size() (int64, error) {
 func (l *noDiffID) MediaType() (types.MediaType, error) {
 	return l.l.MediaType()
 }
+func (l *noDiffID) Descriptor() (*v1.Descriptor, error) {
+	return partial.Descriptor(l.l)
+}
+func (l *noDiffID) UncompressedSize() (int64, error) {
+	return partial.UncompressedSize(l.l)
+}
 
 func TestCompressedLayerExtender(t *testing.T) {
 	rnd, err := random.Layer(1000, types.OCILayer)
@@ -111,4 +117,55 @@ func TestCompressedLayerExtender(t *testing.T) {
 	if err := compare.Layers(rnd, l); err != nil {
 		t.Fatalf("compare.Layers: %v", err)
 	}
+	if _, err := partial.Descriptor(l); err != nil {
+		t.Fatalf("partial.Descriptor: %v", err)
+	}
+	if _, err := partial.UncompressedSize(l); err != nil {
+		t.Fatalf("partial.UncompressedSize: %v", err)
+	}
+}
+
+type compressedImage struct {
+	img v1.Image
+}
+
+func (i *compressedImage) RawConfigFile() ([]byte, error) {
+	return i.img.RawConfigFile()
+}
+
+func (i *compressedImage) MediaType() (types.MediaType, error) {
+	return i.img.MediaType()
+}
+
+func (i *compressedImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) {
+	return i.img.LayerByDigest(h)
+}
+
+func (i *compressedImage) RawManifest() ([]byte, error) {
+	return i.img.RawManifest()
+}
+
+func (i *compressedImage) Descriptor() (*v1.Descriptor, error) {
+	return partial.Descriptor(i.img)
+}
+
+func TestCompressed(t *testing.T) {
+	rnd, err := random.Image(1024, 1)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	core := &compressedImage{rnd}
+
+	img, err := partial.CompressedToImage(core)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if err := validate.Image(img); err != nil {
+		t.Fatalf("validate.Image: %v", err)
+	}
+	if _, err := partial.Descriptor(img); err != nil {
+		t.Fatalf("partial.Descriptor: %v", err)
+	}
 }
diff --git a/pkg/v1/partial/uncompressed_test.go b/pkg/v1/partial/uncompressed_test.go
index 7605c66b..3b8cedad 100644
--- a/pkg/v1/partial/uncompressed_test.go
+++ b/pkg/v1/partial/uncompressed_test.go
@@ -74,7 +74,11 @@ func (l *foreignLayer) Descriptor() (*v1.Descriptor, error) {
 	}, nil
 }
 
-func TestDescriptor(t *testing.T) {
+func (l *foreignLayer) UncompressedSize() (int64, error) {
+	return partial.UncompressedSize(l.wrapped)
+}
+
+func TestUncompressedLayer(t *testing.T) {
 	randLayer, err := random.Layer(1024, types.DockerForeignLayer)
 	if err != nil {
 		t.Fatal(err)
@@ -89,6 +93,18 @@ func TestDescriptor(t *testing.T) {
 	if want, got := desc.URLs[0], "http://example.com"; want != got {
 		t.Errorf("URLs[0] = %s != %s", got, want)
 	}
+
+	layer, err := partial.UncompressedToLayer(l)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if err := validate.Layer(layer); err != nil {
+		t.Errorf("validate.Layer: %v", err)
+	}
+	if _, err := partial.UncompressedSize(layer); err != nil {
+		t.Errorf("partial.UncompressedSize: %v", err)
+	}
 }
 
 // legacy/tarball.Write + tarball.Image leverages a lot of uncompressed partials.
@@ -175,6 +191,10 @@ func (i *uncompressedImage) LayerByDiffID(h v1.Hash) (partial.UncompressedLayer,
 	return i.img.LayerByDiffID(h)
 }
 
+func (i *uncompressedImage) Descriptor() (*v1.Descriptor, error) {
+	return partial.Descriptor(i.img)
+}
+
 func TestUncompressed(t *testing.T) {
 	rnd, err := random.Image(1024, 1)
 	if err != nil {
@@ -191,4 +211,7 @@ func TestUncompressed(t *testing.T) {
 	if err := validate.Image(img); err != nil {
 		t.Fatalf("validate.Image: %v", err)
 	}
+	if _, err := partial.Descriptor(img); err != nil {
+		t.Fatalf("partial.Descriptor: %v", err)
+	}
 }
diff --git a/pkg/v1/partial/with_test.go b/pkg/v1/partial/with_test.go
index b9c85c6f..139c72bd 100644
--- a/pkg/v1/partial/with_test.go
+++ b/pkg/v1/partial/with_test.go
@@ -21,6 +21,7 @@ import (
 	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/google/go-containerregistry/pkg/v1/partial"
 	"github.com/google/go-containerregistry/pkg/v1/random"
+	"github.com/google/go-containerregistry/pkg/v1/types"
 )
 
 func TestRawConfigFile(t *testing.T) {
@@ -193,3 +194,26 @@ func TestBlobSize(t *testing.T) {
 		t.Errorf("expected err, got nil")
 	}
 }
+
+type fastpathLayer struct {
+	v1.Layer
+}
+
+func (l *fastpathLayer) UncompressedSize() (int64, error) {
+	return 100, nil
+}
+
+func TestUncompressedSize(t *testing.T) {
+	randLayer, err := random.Layer(1024, types.DockerLayer)
+	if err != nil {
+		t.Fatal(err)
+	}
+	fpl := &fastpathLayer{randLayer}
+	us, err := partial.UncompressedSize(fpl)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if got, want := us, int64(100); got != want {
+		t.Errorf("UncompressedSize() = %d != %d", got, want)
+	}
+}
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)