Repository: google/ko
Pull request #136 (2020-02-14 19:43:10)

Update ggcr
Need to grab some fixes from pkg/v1/mutate


Patch:
diff --git a/go.mod b/go.mod
index dcb3c9be..2862a801 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960
 	github.com/fsnotify/fsnotify v1.4.7
 	github.com/google/go-cmp v0.3.0
-	github.com/google/go-containerregistry v0.0.0-20200115190719-e8e9aa676278
+	github.com/google/go-containerregistry v0.0.0-20200212224832-c629a66d7231
 	github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
 	github.com/mattmoor/dep-notify v0.0.0-20190205035814-a45dec370a17
 	github.com/spf13/cobra v0.0.5
diff --git a/go.sum b/go.sum
index fa7747de..ecf20e10 100644
--- a/go.sum
+++ b/go.sum
@@ -136,6 +136,8 @@ github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-containerregistry v0.0.0-20200115190719-e8e9aa676278 h1:jzLpEQ2Is2AOHtwZvILBecSxSxq9v3KpZU6fIQIvYYg=
 github.com/google/go-containerregistry v0.0.0-20200115190719-e8e9aa676278/go.mod h1:Wtl/v6YdQxv397EREtzwgd9+Ud7Q5D8XMbi3Zazgkrs=
+github.com/google/go-containerregistry v0.0.0-20200212224832-c629a66d7231 h1:zoj6E1dzY9aeZw1CGJv1hffxgyunrLpjI0SZWK7ynzg=
+github.com/google/go-containerregistry v0.0.0-20200212224832-c629a66d7231/go.mod h1:Wtl/v6YdQxv397EREtzwgd9+Ud7Q5D8XMbi3Zazgkrs=
 github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
 github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
diff --git a/pkg/commands/apply.go b/pkg/commands/apply.go
index 204ef553..d3f98ac6 100644
--- a/pkg/commands/apply.go
+++ b/pkg/commands/apply.go
@@ -30,10 +30,8 @@ import (
 // addApply augments our CLI surface with apply.
 func addApply(topLevel *cobra.Command) {
 	koApplyFlags := []string{}
-	lo := &options.LocalOptions{}
-	no := &options.NameOptions{}
+	po := &options.PublishOptions{}
 	fo := &options.FilenameOptions{}
-	ta := &options.TagsOptions{}
 	so := &options.SelectorOptions{}
 	sto := &options.StrictOptions{}
 	bo := &options.BuildOptions{}
@@ -75,10 +73,11 @@ func addApply(topLevel *cobra.Command) {
 			if err != nil {
 				log.Fatalf("error creating builder: %v", err)
 			}
-			publisher, err := makePublisher(no, lo, ta)
+			publisher, err := makePublisher(po)
 			if err != nil {
 				log.Fatalf("error creating publisher: %v", err)
 			}
+			defer publisher.Close()
 			// Create a set of ko-specific flags to ignore when passing through
 			// kubectl global flags.
 			ignoreSet := make(map[string]struct{})
@@ -145,10 +144,8 @@ func addApply(topLevel *cobra.Command) {
 			}
 		},
 	}
-	options.AddLocalArg(apply, lo)
-	options.AddNamingArgs(apply, no)
+	options.AddPublishArg(apply, po)
 	options.AddFileArg(apply, fo)
-	options.AddTagsArg(apply, ta)
 	options.AddSelectorArg(apply, so)
 	options.AddStrictArg(apply, sto)
 	options.AddBuildOptions(apply, bo)
diff --git a/pkg/commands/create.go b/pkg/commands/create.go
index e46bf1bc..1bd0350c 100644
--- a/pkg/commands/create.go
+++ b/pkg/commands/create.go
@@ -30,10 +30,8 @@ import (
 // addCreate augments our CLI surface with apply.
 func addCreate(topLevel *cobra.Command) {
 	koCreateFlags := []string{}
-	lo := &options.LocalOptions{}
-	no := &options.NameOptions{}
+	po := &options.PublishOptions{}
 	fo := &options.FilenameOptions{}
-	ta := &options.TagsOptions{}
 	so := &options.SelectorOptions{}
 	sto := &options.StrictOptions{}
 	bo := &options.BuildOptions{}
@@ -75,10 +73,11 @@ func addCreate(topLevel *cobra.Command) {
 			if err != nil {
 				log.Fatalf("error creating builder: %v", err)
 			}
-			publisher, err := makePublisher(no, lo, ta)
+			publisher, err := makePublisher(po)
 			if err != nil {
 				log.Fatalf("error creating publisher: %v", err)
 			}
+			defer publisher.Close()
 			// Create a set of ko-specific flags to ignore when passing through
 			// kubectl global flags.
 			ignoreSet := make(map[string]struct{})
@@ -145,10 +144,8 @@ func addCreate(topLevel *cobra.Command) {
 			}
 		},
 	}
-	options.AddLocalArg(create, lo)
-	options.AddNamingArgs(create, no)
+	options.AddPublishArg(create, po)
 	options.AddFileArg(create, fo)
-	options.AddTagsArg(create, ta)
 	options.AddSelectorArg(create, so)
 	options.AddStrictArg(create, sto)
 	options.AddBuildOptions(create, bo)
diff --git a/pkg/commands/options/flatname.go b/pkg/commands/options/publish.go
similarity index 52%
rename from pkg/commands/options/flatname.go
rename to pkg/commands/options/publish.go
index 98574750..2e8b4253 100644
--- a/pkg/commands/options/flatname.go
+++ b/pkg/commands/options/publish.go
@@ -23,18 +23,43 @@ import (
 	"github.com/spf13/cobra"
 )
 
-// NameOptions represents options for the ko binary.
-type NameOptions struct {
+// PublishOptions encapsulates options when publishing.
+type PublishOptions struct {
+	Tags []string
+
+	// Push publishes images to a registry.
+	Push bool
+
+	// Local publishes images to a local docker daemon.
+	Local            bool
+	InsecureRegistry bool
+
+	OCILayoutPath string
+	TarballFile   string
+
 	// PreserveImportPaths preserves the full import path after KO_DOCKER_REPO.
 	PreserveImportPaths bool
 	// BaseImportPaths uses the base path without MD5 hash after KO_DOCKER_REPO.
 	BaseImportPaths bool
 }
 
-func AddNamingArgs(cmd *cobra.Command, no *NameOptions) {
-	cmd.Flags().BoolVarP(&no.PreserveImportPaths, "preserve-import-paths", "P", no.PreserveImportPaths,
+func AddPublishArg(cmd *cobra.Command, po *PublishOptions) {
+	cmd.Flags().StringSliceVarP(&po.Tags, "tags", "t", []string{"latest"},
+		"Which tags to use for the produced image instead of the default 'latest' tag.")
+
+	cmd.Flags().BoolVar(&po.Push, "push", true, "Push images to KO_DOCKER_REPO")
+
+	cmd.Flags().BoolVarP(&po.Local, "local", "L", po.Local,
+		"Load into images to local docker daemon.")
+	cmd.Flags().BoolVar(&po.InsecureRegistry, "insecure-registry", po.InsecureRegistry,
+		"Whether to skip TLS verification on the registry")
+
+	cmd.Flags().StringVar(&po.OCILayoutPath, "oci-layout-path", "", "Path to save the OCI image layout of the built images")
+	cmd.Flags().StringVar(&po.TarballFile, "tarball", "", "File to save images tarballs")
+
+	cmd.Flags().BoolVarP(&po.PreserveImportPaths, "preserve-import-paths", "P", po.PreserveImportPaths,
 		"Whether to preserve the full import path after KO_DOCKER_REPO.")
-	cmd.Flags().BoolVarP(&no.BaseImportPaths, "base-import-paths", "B", no.BaseImportPaths,
+	cmd.Flags().BoolVarP(&po.BaseImportPaths, "base-import-paths", "B", po.BaseImportPaths,
 		"Whether to use the base path without MD5 hash after KO_DOCKER_REPO.")
 }
 
@@ -52,10 +77,10 @@ func baseImportPaths(importpath string) string {
 	return filepath.Base(importpath)
 }
 
-func MakeNamer(no *NameOptions) publish.Namer {
-	if no.PreserveImportPaths {
+func MakeNamer(po *PublishOptions) publish.Namer {
+	if po.PreserveImportPaths {
 		return preserveImportPath
-	} else if no.BaseImportPaths {
+	} else if po.BaseImportPaths {
 		return baseImportPaths
 	}
 	return packageWithMD5
diff --git a/pkg/commands/publish.go b/pkg/commands/publish.go
index 7a1280c9..2c651a1c 100644
--- a/pkg/commands/publish.go
+++ b/pkg/commands/publish.go
@@ -24,9 +24,7 @@ import (
 
 // addPublish augments our CLI surface with publish.
 func addPublish(topLevel *cobra.Command) {
-	lo := &options.LocalOptions{}
-	no := &options.NameOptions{}
-	ta := &options.TagsOptions{}
+	po := &options.PublishOptions{}
 	bo := &options.BuildOptions{}
 
 	publish := &cobra.Command{
@@ -64,10 +62,11 @@ func addPublish(topLevel *cobra.Command) {
 			if err != nil {
 				log.Fatalf("error creating builder: %v", err)
 			}
-			publisher, err := makePublisher(no, lo, ta)
+			publisher, err := makePublisher(po)
 			if err != nil {
 				log.Fatalf("error creating publisher: %v", err)
 			}
+			defer publisher.Close()
 			ctx := createCancellableContext()
 			images, err := publishImages(ctx, args, publisher, builder)
 			if err != nil {
@@ -78,9 +77,7 @@ func addPublish(topLevel *cobra.Command) {
 			}
 		},
 	}
-	options.AddLocalArg(publish, lo)
-	options.AddNamingArgs(publish, no)
-	options.AddTagsArg(publish, ta)
+	options.AddPublishArg(publish, po)
 	options.AddBuildOptions(publish, bo)
 	topLevel.AddCommand(publish)
 }
diff --git a/pkg/commands/resolve.go b/pkg/commands/resolve.go
index 71ac1f2e..485d255b 100644
--- a/pkg/commands/resolve.go
+++ b/pkg/commands/resolve.go
@@ -24,10 +24,8 @@ import (
 
 // addResolve augments our CLI surface with resolve.
 func addResolve(topLevel *cobra.Command) {
-	lo := &options.LocalOptions{}
-	no := &options.NameOptions{}
+	po := &options.PublishOptions{}
 	fo := &options.FilenameOptions{}
-	ta := &options.TagsOptions{}
 	so := &options.SelectorOptions{}
 	sto := &options.StrictOptions{}
 	bo := &options.BuildOptions{}
@@ -62,20 +60,19 @@ func addResolve(topLevel *cobra.Command) {
 			if err != nil {
 				log.Fatalf("error creating builder: %v", err)
 			}
-			publisher, err := makePublisher(no, lo, ta)
+			publisher, err := makePublisher(po)
 			if err != nil {
 				log.Fatalf("error creating publisher: %v", err)
 			}
+			defer publisher.Close()
 			ctx := createCancellableContext()
 			if err := resolveFilesToWriter(ctx, builder, publisher, fo, so, sto, os.Stdout); err != nil {
 				log.Fatal(err)
 			}
 		},
 	}
-	options.AddLocalArg(resolve, lo)
-	options.AddNamingArgs(resolve, no)
+	options.AddPublishArg(resolve, po)
 	options.AddFileArg(resolve, fo)
-	options.AddTagsArg(resolve, ta)
 	options.AddSelectorArg(resolve, so)
 	options.AddStrictArg(resolve, sto)
 	options.AddBuildOptions(resolve, bo)
diff --git a/pkg/commands/resolver.go b/pkg/commands/resolver.go
index 342961e7..49502e8f 100644
--- a/pkg/commands/resolver.go
+++ b/pkg/commands/resolver.go
@@ -111,16 +111,19 @@ func makeBuilder(bo *options.BuildOptions) (*build.Caching, error) {
 	return build.NewCaching(innerBuilder)
 }
 
-func makePublisher(no *options.NameOptions, lo *options.LocalOptions, ta *options.TagsOptions) (publish.Interface, error) {
+func makePublisher(po *options.PublishOptions) (publish.Interface, error) {
 	// Create the publish.Interface that we will use to publish image references
 	// to either a docker daemon or a container image registry.
 	innerPublisher, err := func() (publish.Interface, error) {
-		namer := options.MakeNamer(no)
-
 		repoName := os.Getenv("KO_DOCKER_REPO")
-		if lo.Local || repoName == publish.LocalDomain {
-			return publish.NewDaemon(namer, ta.Tags), nil
+		namer := options.MakeNamer(po)
+		if repoName == publish.LocalDomain || po.Local {
+			// TODO(jonjohnsonjr): I'm assuming that nobody will
+			// use local with other publishers, but that might
+			// not be true.
+			return publish.NewDaemon(namer, po.Tags), nil
 		}
+
 		if repoName == "" {
 			return nil, errors.New("KO_DOCKER_REPO environment variable is unset")
 		}
@@ -130,12 +133,31 @@ func makePublisher(no *options.NameOptions, lo *options.LocalOptions, ta *option
 			}
 		}
 
-		return publish.NewDefault(repoName,
-			publish.WithTransport(defaultTransport()),
-			publish.WithAuthFromKeychain(authn.DefaultKeychain),
-			publish.WithNamer(namer),
-			publish.WithTags(ta.Tags),
-			publish.Insecure(lo.InsecureRegistry))
+		publishers := []publish.Interface{}
+		if po.OCILayoutPath != "" {
+			lp, err := publish.NewLayout(po.OCILayoutPath)
+			if err != nil {
+				return nil, fmt.Errorf("failed to create LayoutPublisher for %q: %v", po.OCILayoutPath, err)
+			}
+			publishers = append(publishers, lp)
+		}
+		if po.TarballFile != "" {
+			tp := publish.NewTarball(po.TarballFile, repoName, namer, po.Tags)
+			publishers = append(publishers, tp)
+		}
+		if po.Push {
+			dp, err := publish.NewDefault(repoName,
+				publish.WithTransport(defaultTransport()),
+				publish.WithAuthFromKeychain(authn.DefaultKeychain),
+				publish.WithNamer(namer),
+				publish.WithTags(po.Tags),
+				publish.Insecure(po.InsecureRegistry))
+			if err != nil {
+				return nil, err
+			}
+			publishers = append(publishers, dp)
+		}
+		return publish.MultiPublisher(publishers...), nil
 	}()
 	if err != nil {
 		return nil, err
diff --git a/pkg/commands/run.go b/pkg/commands/run.go
index 184fd193..2b43de26 100644
--- a/pkg/commands/run.go
+++ b/pkg/commands/run.go
@@ -27,9 +27,7 @@ import (
 
 // addRun augments our CLI surface with run.
 func addRun(topLevel *cobra.Command) {
-	lo := &options.LocalOptions{}
-	no := &options.NameOptions{}
-	ta := &options.TagsOptions{}
+	po := &options.PublishOptions{}
 	bo := &options.BuildOptions{}
 
 	run := &cobra.Command{
@@ -69,10 +67,11 @@ func addRun(topLevel *cobra.Command) {
 			if err != nil {
 				log.Fatalf("error creating builder: %v", err)
 			}
-			publisher, err := makePublisher(no, lo, ta)
+			publisher, err := makePublisher(po)
 			if err != nil {
 				log.Fatalf("error creating publisher: %v", err)
 			}
+			defer publisher.Close()
 
 			if len(os.Args) < 3 {
 				log.Fatalf("usage: %s run <package>", os.Args[0])
@@ -137,9 +136,7 @@ func addRun(topLevel *cobra.Command) {
 			UnknownFlags: true,
 		},
 	}
-	options.AddLocalArg(run, lo)
-	options.AddNamingArgs(run, no)
-	options.AddTagsArg(run, ta)
+	options.AddPublishArg(run, po)
 	options.AddBuildOptions(run, bo)
 
 	topLevel.AddCommand(run)
diff --git a/pkg/publish/daemon.go b/pkg/publish/daemon.go
index c8019bbc..00cf0f9a 100644
--- a/pkg/publish/daemon.go
+++ b/pkg/publish/daemon.go
@@ -78,3 +78,7 @@ func (d *demon) Publish(img v1.Image, s string) (name.Reference, error) {
 
 	return &digestTag, nil
 }
+
+func (d *demon) Close() error {
+	return nil
+}
diff --git a/pkg/publish/default.go b/pkg/publish/default.go
index 8d5cf33c..23604b00 100644
--- a/pkg/publish/default.go
+++ b/pkg/publish/default.go
@@ -140,3 +140,7 @@ func (d *defalt) Publish(img v1.Image, s string) (name.Reference, error) {
 	log.Printf("Published %v", dig)
 	return &dig, nil
 }
+
+func (d *defalt) Close() error {
+	return nil
+}
diff --git a/pkg/publish/layout.go b/pkg/publish/layout.go
new file mode 100644
index 00000000..6ea15966
--- /dev/null
+++ b/pkg/publish/layout.go
@@ -0,0 +1,66 @@
+// 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 publish
+
+import (
+	"fmt"
+	"log"
+
+	"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"
+)
+
+type LayoutPublisher struct {
+	p layout.Path
+}
+
+// NewLayout returns a new publish.Interface that saves images to an OCI Image Layout.
+func NewLayout(path string) (Interface, error) {
+	p, err := layout.FromPath(path)
+	if err != nil {
+		p, err = layout.Write(path, empty.Index)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return &LayoutPublisher{p}, nil
+}
+
+// Publish implements publish.Interface.
+func (l *LayoutPublisher) Publish(img v1.Image, s string) (name.Reference, error) {
+	log.Printf("Saving %v", s)
+	if err := l.p.AppendImage(img); err != nil {
+		return nil, err
+	}
+	log.Printf("Saved %v", s)
+
+	h, err := img.Digest()
+	if err != nil {
+		return nil, err
+	}
+
+	dig, err := name.NewDigest(fmt.Sprintf("%s@%s", l.p, h))
+	if err != nil {
+		return nil, err
+	}
+
+	return dig, nil
+}
+
+func (l *LayoutPublisher) Close() error {
+	return nil
+}
diff --git a/pkg/publish/multi.go b/pkg/publish/multi.go
new file mode 100644
index 00000000..d808a062
--- /dev/null
+++ b/pkg/publish/multi.go
@@ -0,0 +1,54 @@
+// 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 publish
+
+import (
+	"github.com/google/go-containerregistry/pkg/name"
+	v1 "github.com/google/go-containerregistry/pkg/v1"
+)
+
+// MultiPublisher creates a publisher that publishes to all
+// the provided publishers, similar to the Unix tee(1) command.
+//
+// When calling Publish, the name.Reference returned will be the return value
+// of the last publisher passed to MultiPublisher (last one wins).
+func MultiPublisher(publishers ...Interface) Interface {
+	return &multiPublisher{publishers}
+}
+
+type multiPublisher struct {
+	publishers []Interface
+}
+
+// Publish implements publish.Interface.
+func (p *multiPublisher) Publish(img v1.Image, s string) (ref name.Reference, err error) {
+	for _, pub := range p.publishers {
+		ref, err = pub.Publish(img, s)
+		if err != nil {
+			return
+		}
+	}
+
+	return
+}
+
+func (p *multiPublisher) Close() (err error) {
+	for _, pub := range p.publishers {
+		if perr := pub.Close(); perr != nil {
+			err = perr
+		}
+	}
+	return
+}
diff --git a/pkg/publish/publish.go b/pkg/publish/publish.go
index c6002ef9..1c61693e 100644
--- a/pkg/publish/publish.go
+++ b/pkg/publish/publish.go
@@ -25,4 +25,8 @@ type Interface interface {
 	// provided string into the image's repository name.  Returns the digest
 	// of the published image.
 	Publish(v1.Image, string) (name.Reference, error)
+
+	// Close exists for the tarball implementation so we can
+	// do the whole thing in one write.
+	Close() error
 }
diff --git a/pkg/publish/shared.go b/pkg/publish/shared.go
index edd2832b..1a2af98f 100644
--- a/pkg/publish/shared.go
+++ b/pkg/publish/shared.go
@@ -74,3 +74,7 @@ func (c *caching) Publish(img v1.Image, ref string) (name.Reference, error) {
 
 	return f.Get()
 }
+
+func (c *caching) Close() error {
+	return c.inner.Close()
+}
diff --git a/pkg/publish/tarball.go b/pkg/publish/tarball.go
new file mode 100644
index 00000000..17ba8dde
--- /dev/null
+++ b/pkg/publish/tarball.go
@@ -0,0 +1,95 @@
+// 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 publish
+
+import (
+	"fmt"
+	"log"
+	"strings"
+
+	"github.com/google/go-containerregistry/pkg/name"
+	v1 "github.com/google/go-containerregistry/pkg/v1"
+	"github.com/google/go-containerregistry/pkg/v1/tarball"
+)
+
+type TarballPublisher struct {
+	file  string
+	base  string
+	namer Namer
+	tags  []string
+	refs  map[name.Reference]v1.Image
+}
+
+// NewTarball returns a new publish.Interface that saves images to a tarball.
+func NewTarball(file, base string, namer Namer, tags []string) *TarballPublisher {
+	return &TarballPublisher{
+		file:  file,
+		base:  base,
+		namer: namer,
+		tags:  tags,
+		refs:  make(map[name.Reference]v1.Image),
+	}
+}
+
+// Publish implements publish.Interface.
+func (t *TarballPublisher) Publish(img v1.Image, s string) (name.Reference, error) {
+	// https://github.com/google/go-containerregistry/issues/212
+	s = strings.ToLower(s)
+
+	for _, tagName := range t.tags {
+		tag, err := name.ParseReference(fmt.Sprintf("%s/%s:%s", t.base, t.namer(s), tagName))
+		if err != nil {
+			return nil, err
+		}
+		t.refs[tag] = img
+	}
+
+	h, err := img.Digest()
+	if err != nil {
+		return nil, err
+	}
+
+	if len(t.tags) == 0 {
+		ref, err := name.ParseReference(fmt.Sprintf("%s/%s@%s", t.base, t.namer(s), h))
+		if err != nil {
+			return nil, err
+		}
+		t.refs[ref] = img
+	}
+
+	ref := fmt.Sprintf("%s/%s@%s", t.base, t.namer(s), h)
+	if len(t.tags) == 1 && t.tags[0] != defaultTags[0] {
+		// If a single tag is explicitly set (not latest), then this
+		// is probably a release, so include the tag in the reference.
+		ref = fmt.Sprintf("%s/%s:%s@%s", t.base, t.namer(s), t.tags[0], h)
+	}
+	dig, err := name.NewDigest(ref)
+	if err != nil {
+		return nil, err
+	}
+
+	return &dig, nil
+}
+
+func (t *TarballPublisher) Close() error {
+	log.Printf("Saving %v", t.file)
+	if err := tarball.MultiRefWriteToFile(t.file, t.refs); err != nil {
+		// Bad practice, but we log  this here because right now we just defer the Close.
+		log.Printf("failed to save %q: %v", t.file, err)
+		return err
+	}
+	log.Printf("Saved %v", t.file)
+	return nil
+}
diff --git a/pkg/commands/options/local.go b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/blob.go
similarity index 53%
rename from pkg/commands/options/local.go
rename to vendor/github.com/google/go-containerregistry/pkg/v1/layout/blob.go
index 967c8926..ba90d4cd 100644
--- a/pkg/commands/options/local.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/blob.go
@@ -12,22 +12,27 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package options
+package layout
 
 import (
-	"github.com/spf13/cobra"
+	"io"
+	"io/ioutil"
+	"os"
+
+	v1 "github.com/google/go-containerregistry/pkg/v1"
 )
 
-// LocalOptions represents options for the ko binary.
-type LocalOptions struct {
-	// Local publishes images to a local docker daemon.
-	Local            bool
-	InsecureRegistry bool
+// Blob returns a blob with the given hash from the Path.
+func (l Path) Blob(h v1.Hash) (io.ReadCloser, error) {
+	return os.Open(l.blobPath(h))
+}
+
+// Bytes is a convenience function to return a blob from the Path as
+// a byte slice.
+func (l Path) Bytes(h v1.Hash) ([]byte, error) {
+	return ioutil.ReadFile(l.blobPath(h))
 }
 
-func AddLocalArg(cmd *cobra.Command, lo *LocalOptions) {
-	cmd.Flags().BoolVarP(&lo.Local, "local", "L", lo.Local,
-		"Whether to publish images to a local docker daemon vs. a registry.")
-	cmd.Flags().BoolVar(&lo.InsecureRegistry, "insecure-registry", lo.InsecureRegistry,
-		"Whether to skip TLS verification on the registry")
+func (l Path) blobPath(h v1.Hash) string {
+	return l.path("blobs", h.Algorithm, h.Hex)
 }
diff --git a/pkg/commands/options/tags.go b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/doc.go
similarity index 62%
rename from pkg/commands/options/tags.go
rename to vendor/github.com/google/go-containerregistry/pkg/v1/layout/doc.go
index 22c1d980..d80d2736 100644
--- a/pkg/commands/options/tags.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/doc.go
@@ -12,18 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package options
-
-import (
-	"github.com/spf13/cobra"
-)
-
-// TagsOptions holds the list of tags to tag the built image
-type TagsOptions struct {
-	Tags []string
-}
-
-func AddTagsArg(cmd *cobra.Command, ta *TagsOptions) {
-	cmd.Flags().StringSliceVarP(&ta.Tags, "tags", "t", []string{"latest"},
-		"Which tags to use for the produced image instead of the default 'latest' tag.")
-}
+// Package layout provides facilities for reading/writing artifacts from/to
+// an OCI image layout on disk, see:
+//
+// https://github.com/opencontainers/image-spec/blob/master/image-layout.md
+package layout
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/layout/image.go b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/image.go
new file mode 100644
index 00000000..7c76a10c
--- /dev/null
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/image.go
@@ -0,0 +1,131 @@
+// Copyright 2018 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 layout
+
+import (
+	"fmt"
+	"io"
+	"sync"
+
+	v1 "github.com/google/go-containerregistry/pkg/v1"
+	"github.com/google/go-containerregistry/pkg/v1/partial"
+	"github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+type layoutImage struct {
+	path         Path
+	desc         v1.Descriptor
+	manifestLock sync.Mutex // Protects rawManifest
+	rawManifest  []byte
+}
+
+var _ partial.CompressedImageCore = (*layoutImage)(nil)
+
+// Image reads a v1.Image with digest h from the Path.
+func (l Path) Image(h v1.Hash) (v1.Image, error) {
+	ii, err := l.ImageIndex()
+	if err != nil {
+		return nil, err
+	}
+
+	return ii.Image(h)
+}
+
+func (li *layoutImage) MediaType() (types.MediaType, error) {
+	return li.desc.MediaType, nil
+}
+
+// Implements WithManifest for partial.Blobset.
+func (li *layoutImage) Manifest() (*v1.Manifest, error) {
+	return partial.Manifest(li)
+}
+
+func (li *layoutImage) RawManifest() ([]byte, error) {
+	li.manifestLock.Lock()
+	defer li.manifestLock.Unlock()
+	if li.rawManifest != nil {
+		return li.rawManifest, nil
+	}
+
+	b, err := li.path.Bytes(li.desc.Digest)
+	if err != nil {
+		return nil, err
+	}
+
+	li.rawManifest = b
+	return li.rawManifest, nil
+}
+
+func (li *layoutImage) RawConfigFile() ([]byte, error) {
+	manifest, err := li.Manifest()
+	if err != nil {
+		return nil, err
+	}
+
+	return li.path.Bytes(manifest.Config.Digest)
+}
+
+func (li *layoutImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) {
+	manifest, err := li.Manifest()
+	if err != nil {
+		return nil, err
+	}
+
+	if h == manifest.Config.Digest {
+		return partial.CompressedLayer(&compressedBlob{
+			path: li.path,
+			desc: manifest.Config,
+		}), nil
+	}
+
+	for _, desc := range manifest.Layers {
+		if h == desc.Digest {
+			switch desc.MediaType {
+			case types.OCILayer, types.DockerLayer:
+				return partial.CompressedToLayer(&compressedBlob{
+					path: li.path,
+					desc: desc,
+				})
+			default:
+				// TODO: We assume everything is a compressed blob, but that might not be true.
+				// TODO: Handle foreign layers.
+				return nil, fmt.Errorf("unexpected media type: %v for layer: %v", desc.MediaType, desc.Digest)
+			}
+		}
+	}
+
+	return nil, fmt.Errorf("could not find layer in image: %s", h)
+}
+
+type compressedBlob struct {
+	path Path
+	desc v1.Descriptor
+}
+
+func (b *compressedBlob) Digest() (v1.Hash, error) {
+	return b.desc.Digest, nil
+}
+
+func (b *compressedBlob) Compressed() (io.ReadCloser, error) {
+	return b.path.Blob(b.desc.Digest)
+}
+
+func (b *compressedBlob) Size() (int64, error) {
+	return b.desc.Size, nil
+}
+
+func (b *compressedBlob) MediaType() (types.MediaType, error) {
+	return b.desc.MediaType, nil
+}
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/layout/index.go b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/index.go
new file mode 100644
index 00000000..8ae3a7bb
--- /dev/null
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/index.go
@@ -0,0 +1,153 @@
+// Copyright 2018 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 layout
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+
+	v1 "github.com/google/go-containerregistry/pkg/v1"
+	"github.com/google/go-containerregistry/pkg/v1/partial"
+	"github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+var _ v1.ImageIndex = (*layoutIndex)(nil)
+
+type layoutIndex struct {
+	mediaType types.MediaType
+	path      Path
+	rawIndex  []byte
+}
+
+// ImageIndexFromPath is a convenience function which constructs a Path and returns its v1.ImageIndex.
+func ImageIndexFromPath(path string) (v1.ImageIndex, error) {
+	lp, err := FromPath(path)
+	if err != nil {
+		return nil, err
+	}
+	return lp.ImageIndex()
+}
+
+// ImageIndex returns a v1.ImageIndex for the Path.
+func (l Path) ImageIndex() (v1.ImageIndex, error) {
+	rawIndex, err := ioutil.ReadFile(l.path("index.json"))
+	if err != nil {
+		return nil, err
+	}
+
+	idx := &layoutIndex{
+		mediaType: types.OCIImageIndex,
+		path:      l,
+		rawIndex:  rawIndex,
+	}
+
+	return idx, nil
+}
+
+func (i *layoutIndex) MediaType() (types.MediaType, error) {
+	return i.mediaType, nil
+}
+
+func (i *layoutIndex) Digest() (v1.Hash, error) {
+	return partial.Digest(i)
+}
+
+func (i *layoutIndex) Size() (int64, error) {
+	return partial.Size(i)
+}
+
+func (i *layoutIndex) IndexManifest() (*v1.IndexManifest, error) {
+	var index v1.IndexManifest
+	err := json.Unmarshal(i.rawIndex, &index)
+	return &index, err
+}
+
+func (i *layoutIndex) RawManifest() ([]byte, error) {
+	return i.rawIndex, nil
+}
+
+func (i *layoutIndex) Image(h v1.Hash) (v1.Image, error) {
+	// Look up the digest in our manifest first to return a better error.
+	desc, err := i.findDescriptor(h)
+	if err != nil {
+		return nil, err
+	}
+
+	if !isExpectedMediaType(desc.MediaType, types.OCIManifestSchema1, types.DockerManifestSchema2) {
+		return nil, fmt.Errorf("unexpected media type for %v: %s", h, desc.MediaType)
+	}
+
+	img := &layoutImage{
+		path: i.path,
+		desc: *desc,
+	}
+	return partial.CompressedToImage(img)
+}
+
+func (i *layoutIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) {
+	// Look up the digest in our manifest first to return a better error.
+	desc, err := i.findDescriptor(h)
+	if err != nil {
+		return nil, err
+	}
+
+	if !isExpectedMediaType(desc.MediaType, types.OCIImageIndex, types.DockerManifestList) {
+		return nil, fmt.Errorf("unexpected media type for %v: %s", h, desc.MediaType)
+	}
+
+	rawIndex, err := i.path.Bytes(h)
+	if err != nil {
+		return nil, err
+	}
+
+	return &layoutIndex{
+		mediaType: desc.MediaType,
+		path:      i.path,
+		rawIndex:  rawIndex,
+	}, nil
+}
+
+func (i *layoutIndex) Blob(h v1.Hash) (io.ReadCloser, error) {
+	return i.path.Blob(h)
+}
+
+func (i *layoutIndex) findDescriptor(h v1.Hash) (*v1.Descriptor, error) {
+	im, err := i.IndexManifest()
+	if err != nil {
+		return nil, err
+	}
+
+	for _, desc := range im.Manifests {
+		if desc.Digest == h {
+			return &desc, nil
+		}
+	}
+
+	return nil, fmt.Errorf("could not find descriptor in index: %s", h)
+}
+
+// TODO: Pull this out into methods on types.MediaType? e.g. instead, have:
+// * mt.IsIndex()
+// * mt.IsImage()
+func isExpectedMediaType(mt types.MediaType, expected ...types.MediaType) bool {
+	for _, allowed := range expected {
+		if mt == allowed {
+			return true
+		}
+	}
+	return false
+}
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/layout/layoutpath.go b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/layoutpath.go
new file mode 100644
index 00000000..a031ff5a
--- /dev/null
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/layoutpath.go
@@ -0,0 +1,25 @@
+// Copyright 2019 The original author or authors
+//
+// 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 layout
+
+import "path/filepath"
+
+// Path represents an OCI image layout rooted in a file system path
+type Path string
+
+func (l Path) path(elem ...string) string {
+	complete := []string{string(l)}
+	return filepath.Join(append(complete, elem...)...)
+}
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/layout/options.go b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/options.go
new file mode 100644
index 00000000..5569e51d
--- /dev/null
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/options.go
@@ -0,0 +1,42 @@
+package layout
+
+import v1 "github.com/google/go-containerregistry/pkg/v1"
+
+// Option is a functional option for Layout.
+//
+// TODO: We'll need to change this signature to support Sparse/Thin images.
+// Or, alternatively, wrap it in a sparse.Image that returns an empty list for layers?
+type Option func(*v1.Descriptor) error
+
+// WithAnnotations adds annotations to the artifact descriptor.
+func WithAnnotations(annotations map[string]string) Option {
+	return func(desc *v1.Descriptor) error {
+		if desc.Annotations == nil {
+			desc.Annotations = make(map[string]string)
+		}
+		for k, v := range annotations {
+			desc.Annotations[k] = v
+		}
+
+		return nil
+	}
+}
+
+// WithURLs adds urls to the artifact descriptor.
+func WithURLs(urls []string) Option {
+	return func(desc *v1.Descriptor) error {
+		if desc.URLs == nil {
+			desc.URLs = []string{}
+		}
+		desc.URLs = append(desc.URLs, urls...)
+		return nil
+	}
+}
+
+// WithPlatform sets the platform of the artifact descriptor.
+func WithPlatform(platform v1.Platform) Option {
+	return func(desc *v1.Descriptor) error {
+		desc.Platform = &platform
+		return nil
+	}
+}
diff --git a/pkg/commands/options/binary.go b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/read.go
similarity index 58%
rename from pkg/commands/options/binary.go
rename to vendor/github.com/google/go-containerregistry/pkg/v1/layout/read.go
index c44a32fa..796abc7d 100644
--- a/pkg/commands/options/binary.go
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/read.go
@@ -1,4 +1,4 @@
-// Copyright 2018 Google LLC All Rights Reserved.
+// Copyright 2019 The original author or authors
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,19 +12,21 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package options
+package layout
 
 import (
-	"github.com/spf13/cobra"
+	"os"
+	"path/filepath"
 )
 
-// PublishOptions represents options for the ko binary.
-type PublishOptions struct {
-	// Path is the import path of the binary to publish.
-	Path string
-}
+// FromPath reads an OCI image layout at path and constructs a layout.Path.
+func FromPath(path string) (Path, error) {
+	// TODO: check oci-layout exists
+
+	_, err := os.Stat(filepath.Join(path, "index.json"))
+	if err != nil {
+		return "", err
+	}
 
-func AddImageArg(cmd *cobra.Command, lo *PublishOptions) {
-	cmd.Flags().StringVarP(&lo.Path, "image", "i", lo.Path,
-		"The import path of the binary to publish.")
+	return Path(path), nil
 }
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/layout/write.go b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/write.go
new file mode 100644
index 00000000..2abb9a58
--- /dev/null
+++ b/vendor/github.com/google/go-containerregistry/pkg/v1/layout/write.go
@@ -0,0 +1,301 @@
+// Copyright 2018 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 layout
+
+import (
+	"bytes"
+	"encoding/json"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	v1 "github.com/google/go-containerregistry/pkg/v1"
+	"github.com/google/go-containerregistry/pkg/v1/types"
+	"golang.org/x/sync/errgroup"
+)
+
+var layoutFile = `{
+    "imageLayoutVersion": "1.0.0"
+}`
+
+// AppendImage writes a v1.Image to the Path and updates
+// the index.json to reference it.
+func (l Path) AppendImage(img v1.Image, options ...Option) error {
+	if err := l.writeImage(img); err != nil {
+		return err
+	}
+
+	mt, err := img.MediaType()
+	if err != nil {
+		return err
+	}
+
+	d, err := img.Digest()
+	if err != nil {
+		return err
+	}
+
+	manifest, err := img.RawManifest()
+	if err != nil {
+		return err
+	}
+
+	desc := v1.Descriptor{
+		MediaType: mt,
+		Size:      int64(len(manifest)),
+		Digest:    d,
+	}
+
+	for _, opt := range options {
+		if err := opt(&desc); err != nil {
+			return err
+		}
+	}
+
+	return l.AppendDescriptor(desc)
+}
+
+// AppendIndex writes a v1.ImageIndex to the Path and updates
+// the index.json to reference it.
+func (l Path) AppendIndex(ii v1.ImageIndex, options ...Option) error {
+	if err := l.writeIndex(ii); err != nil {
+		return err
+	}
+
+	mt, err := ii.MediaType()
+	if err != nil {
+		return err
+	}
+
+	d, err := ii.Digest()
+	if err != nil {
+		return err
+	}
+
+	manifest, err := ii.RawManifest()
+	if err != nil {
+		return err
+	}
+
+	desc := v1.Descriptor{
+		MediaType: mt,
+		Size:      int64(len(manifest)),
+		Digest:    d,
+	}
+
+	for _, opt := range options {
+		if err := opt(&desc); err != nil {
+			return err
+		}
+	}
+
+	return l.AppendDescriptor(desc)
+}
+
+// AppendDescriptor adds a descriptor to the index.json of the Path.
+func (l Path) AppendDescriptor(desc v1.Descriptor) error {
+	ii, err := l.ImageIndex()
+	if err != nil {
+		return err
+	}
+
+	index, err := ii.IndexManifest()
+	if err != nil {
+		return err
+	}
+
+	index.Manifests = append(index.Manifests, desc)
+
+	rawIndex, err := json.MarshalIndent(index, "", "   ")
+	if err != nil {
+		return err
+	}
+
+	return l.writeFile("index.json", rawIndex)
+}
+
+func (l Path) writeFile(name string, data []byte) 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)
+
+}
+
+// WriteBlob copies a file to the blobs/ directory in the Path from the given ReadCloser at
+// blobs/{hash.Algorithm}/{hash.Hex}.
+func (l Path) WriteBlob(hash v1.Hash, r io.ReadCloser) error {
+	dir := l.path("blobs", hash.Algorithm)
+	if err := os.MkdirAll(dir, os.ModePerm); err != nil && !os.IsExist(err) {
+		return err
+	}
+
+	file := filepath.Join(dir, hash.Hex)
+	if _, err := os.Stat(file); err == nil {
+		// Blob already exists, that's fine.
+		return nil
+	}
+	w, err := os.Create(file)
+	if err != nil {
+		return err
+	}
+	defer w.Close()
+
+	_, err = io.Copy(w, r)
+	return err
+}
+
+// TODO: A streaming version of WriteBlob so we don't have to know the hash
+// before we write it.
+
+// TODO: For streaming layers we should write to a tmp file then Rename to the
+// final digest.
+func (l Path) writeLayer(layer v1.Layer) error {
+	d, err := layer.Digest()
+	if err != nil {
+		return err
+	}
+
+	r, err := layer.Compressed()
+	if err != nil {
+		return err
+	}
+
+	return l.WriteBlob(d, r)
+}
+
+func (l Path) writeImage(img v1.Image) error {
+	layers, err := img.Layers()
+	if err != nil {
+		return err
+	}
+
+	// Write the layers concurrently.
+	var g errgroup.Group
+	for _, layer := range layers {
+		layer := layer
+		g.Go(func() error {
+			return l.writeLayer(layer)
+		})
+	}
+	if err := g.Wait(); err != nil {
+		return err
+	}
+
+	// Write the config.
+	cfgName, err := img.ConfigName()
+	if err != nil {
+		return err
+	}
+	cfgBlob, err := img.RawConfigFile()
+	if err != nil {
+		return err
+	}
+	if err := l.WriteBlob(cfgName, ioutil.NopCloser(bytes.NewReader(cfgBlob))); err != nil {
+		return err
+	}
+
+	// Write the img manifest.
+	d, err := img.Digest()
+	if err != nil {
+		return err
+	}
+	manifest, err := img.RawManifest()
+	if err != nil {
+		return err
+	}
+
+	return l.WriteBlob(d, ioutil.NopCloser(bytes.NewReader(manifest)))
+}
+
+func (l Path) writeIndexToFile(indexFile string, ii v1.ImageIndex) error {
+	index, err := ii.IndexManifest()
+	if err != nil {
+		return err
+	}
+
+	// Walk the descriptors and write any v1.Image or v1.ImageIndex that we find.
+	// If we come across something we don't expect, just write it as a blob.
+	for _, desc := range index.Manifests {
+		switch desc.MediaType {
+		case types.OCIImageIndex, types.DockerManifestList:
+			ii, err := ii.ImageIndex(desc.Digest)
+			if err != nil {
+				return err
+			}
+			if err := l.writeIndex(ii); err != nil {
+				return err
+			}
+		case types.OCIManifestSchema1, types.DockerManifestSchema2:
+			img, err := ii.Image(desc.Digest)
+			if err != nil {
+				return err
+			}
+			if err := l.writeImage(img); err != nil {
+				return err
+			}
+		default:
+			// TODO: The layout could reference arbitrary things, which we should
+			// probably just pass through.
+		}
+	}
+
+	rawIndex, err := ii.RawManifest()
+	if err != nil {
+		return err
+	}
+
+	return l.writeFile(indexFile, rawIndex)
+}
+
+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 {
+		return err
+	}
+
+	h, err := ii.Digest()
+	if err != nil {
+		return err
+	}
+
+	indexFile := filepath.Join("blobs", h.Algorithm, h.Hex)
+	return l.writeIndexToFile(indexFile, ii)
+
+}
+
+// Write constructs a Path at path from an ImageIndex.
+//
+// The contents are written in the following format:
+// At the top level, there is:
+//   One oci-layout file containing the version of this image-layout.
+//   One index.json file listing descriptors for the contained images.
+// Under blobs/, there is, for each image:
+//   One file for each layer, named after the layer's SHA.
+//   One file for each config blob, named after its SHA.
+//   One file for each manifest blob, named after its SHA.
+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 {
+		return "", err
+	}
+
+	// TODO create blobs/ in case there is a blobs file which would prevent the directory from being created
+
+	return lp, lp.writeIndexToFile("index.json", ii)
+}
diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/mutate/image.go b/vendor/github.com/google/go-containerregistry/pkg/v1/mutate/image.go
index 382d139e..e13ef1f8 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/mutate/image.go
+++ b/vendor/github.com/google/go-containerregistry/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/vendor/github.com/google/go-containerregistry/pkg/v1/mutate/index.go b/vendor/github.com/google/go-containerregistry/pkg/v1/mutate/index.go
index 21ccf50c..b8f88c3b 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/mutate/index.go
+++ b/vendor/github.com/google/go-containerregistry/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/vendor/github.com/google/go-containerregistry/pkg/v1/partial/with.go b/vendor/github.com/google/go-containerregistry/pkg/v1/partial/with.go
index c75d17a0..08bdce3e 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/partial/with.go
+++ b/vendor/github.com/google/go-containerregistry/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/vendor/github.com/google/go-containerregistry/pkg/v1/remote/check.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/check.go
index da0fa24c..65ee4e7f 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/check.go
+++ b/vendor/github.com/google/go-containerregistry/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/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/error.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/error.go
index 35e5d798..a215e794 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/error.go
+++ b/vendor/github.com/google/go-containerregistry/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/vendor/github.com/google/go-containerregistry/pkg/v1/remote/write.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/write.go
index 7be851f0..7b431c07 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/write.go
+++ b/vendor/github.com/google/go-containerregistry/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/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/image.go b/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/image.go
index 0d36125d..67a5d437 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/image.go
+++ b/vendor/github.com/google/go-containerregistry/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/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/layer.go b/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/layer.go
index 314a51f1..23729af4 100644
--- a/vendor/github.com/google/go-containerregistry/pkg/v1/tarball/layer.go
+++ b/vendor/github.com/google/go-containerregistry/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) {
diff --git a/vendor/github.com/sirupsen/logrus/go.mod b/vendor/github.com/sirupsen/logrus/go.mod
index ea266226..12fdf989 100644
--- a/vendor/github.com/sirupsen/logrus/go.mod
+++ b/vendor/github.com/sirupsen/logrus/go.mod
@@ -8,5 +8,3 @@ require (
 	github.com/stretchr/testify v1.2.2
 	golang.org/x/sys v0.0.0-20190422165155-953cdadca894
 )
-
-go 1.13
diff --git a/vendor/modules.txt b/vendor/modules.txt
index eeba453d..48df743a 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -84,7 +84,7 @@ github.com/google/go-cmp/cmp/internal/diff
 github.com/google/go-cmp/cmp/internal/flags
 github.com/google/go-cmp/cmp/internal/function
 github.com/google/go-cmp/cmp/internal/value
-# github.com/google/go-containerregistry v0.0.0-20200115190719-e8e9aa676278
+# github.com/google/go-containerregistry v0.0.0-20200212224832-c629a66d7231
 github.com/google/go-containerregistry/pkg/authn
 github.com/google/go-containerregistry/pkg/internal/retry
 github.com/google/go-containerregistry/pkg/internal/retry/wait
@@ -93,6 +93,7 @@ github.com/google/go-containerregistry/pkg/name
 github.com/google/go-containerregistry/pkg/v1
 github.com/google/go-containerregistry/pkg/v1/daemon
 github.com/google/go-containerregistry/pkg/v1/empty
+github.com/google/go-containerregistry/pkg/v1/layout
 github.com/google/go-containerregistry/pkg/v1/mutate
 github.com/google/go-containerregistry/pkg/v1/partial
 github.com/google/go-containerregistry/pkg/v1/random


Test patch:
diff --git a/pkg/internal/testing/fixed.go b/pkg/internal/testing/fixed.go
index 879a46b7..472b0bb9 100644
--- a/pkg/internal/testing/fixed.go
+++ b/pkg/internal/testing/fixed.go
@@ -72,6 +72,10 @@ func (f *fixedPublish) Publish(_ v1.Image, s string) (name.Reference, error) {
 	return &d, nil
 }
 
+func (f *fixedPublish) Close() error {
+	return nil
+}
+
 func ComputeDigest(base name.Repository, ref string, h v1.Hash) string {
 	d, err := name.NewDigest(fmt.Sprintf("%s/%s@%s", base, ref, h))
 	if err != nil {
diff --git a/pkg/publish/layout_test.go b/pkg/publish/layout_test.go
new file mode 100644
index 00000000..c15c2a99
--- /dev/null
+++ b/pkg/publish/layout_test.go
@@ -0,0 +1,48 @@
+// 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 publish
+
+import (
+	"io/ioutil"
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/google/go-containerregistry/pkg/v1/random"
+)
+
+func TestLayout(t *testing.T) {
+	img, err := random.Image(1024, 1)
+	if err != nil {
+		t.Fatalf("random.Image() = %v", err)
+	}
+	importpath := "github.com/Google/go-containerregistry/cmd/crane"
+
+	tmp, err := ioutil.TempDir("", "ko")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(tmp)
+
+	lp, err := NewLayout(tmp)
+	if err != nil {
+		t.Errorf("NewLayout() = %v", err)
+	}
+	if d, err := lp.Publish(img, importpath); err != nil {
+		t.Errorf("Publish() = %v", err)
+	} else if !strings.HasPrefix(d.String(), tmp) {
+		t.Errorf("Publish() = %v, wanted prefix %v", d, tmp)
+	}
+}
diff --git a/pkg/publish/multi_test.go b/pkg/publish/multi_test.go
new file mode 100644
index 00000000..46ed2120
--- /dev/null
+++ b/pkg/publish/multi_test.go
@@ -0,0 +1,63 @@
+// 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 publish
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"testing"
+
+	"github.com/google/go-containerregistry/pkg/v1/random"
+)
+
+func TestMulti(t *testing.T) {
+	img, err := random.Image(1024, 1)
+	if err != nil {
+		t.Fatalf("random.Image() = %v", err)
+	}
+	base := "blah"
+	repoName := fmt.Sprintf("%s/%s", "example.com", base)
+	importpath := "github.com/Google/go-containerregistry/cmd/crane"
+
+	fp, err := ioutil.TempFile("", "")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer fp.Close()
+	defer os.Remove(fp.Name())
+
+	tp := NewTarball(fp.Name(), repoName, md5Hash, []string{})
+
+	tmp, err := ioutil.TempDir("", "ko")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(tmp)
+
+	lp, err := NewLayout(tmp)
+	if err != nil {
+		t.Errorf("NewLayout() = %v", err)
+	}
+
+	p := MultiPublisher(lp, tp)
+	if _, err := p.Publish(img, importpath); err != nil {
+		t.Errorf("Publish() = %v", err)
+	}
+
+	if err := p.Close(); err != nil {
+		t.Errorf("Close() = %v", err)
+	}
+}
diff --git a/pkg/publish/shared_test.go b/pkg/publish/shared_test.go
index cde0d186..858a786b 100644
--- a/pkg/publish/shared_test.go
+++ b/pkg/publish/shared_test.go
@@ -30,17 +30,21 @@ type slowpublish struct {
 // slowpublish implements Interface
 var _ Interface = (*slowpublish)(nil)
 
-func (sb *slowpublish) Publish(img v1.Image, ref string) (name.Reference, error) {
-	time.Sleep(sb.sleep)
+func (sp *slowpublish) Publish(img v1.Image, ref string) (name.Reference, error) {
+	time.Sleep(sp.sleep)
 	return makeRef()
 }
 
+func (sp *slowpublish) Close() error {
+	return nil
+}
+
 func TestCaching(t *testing.T) {
 	duration := 100 * time.Millisecond
 	ref := "foo"
 
-	sb := &slowpublish{duration}
-	cb, _ := NewCaching(sb)
+	sp := &slowpublish{duration}
+	cb, _ := NewCaching(sp)
 
 	previousDigest := "not-a-digest"
 	// Each iteration, we test that the first publish is slow and subsequent
diff --git a/pkg/publish/tarball_test.go b/pkg/publish/tarball_test.go
new file mode 100644
index 00000000..07d7e897
--- /dev/null
+++ b/pkg/publish/tarball_test.go
@@ -0,0 +1,69 @@
+// 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 publish
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/google/go-containerregistry/pkg/name"
+	"github.com/google/go-containerregistry/pkg/v1/random"
+)
+
+func TestTarball(t *testing.T) {
+	img, err := random.Image(1024, 1)
+	if err != nil {
+		t.Fatalf("random.Image() = %v", err)
+	}
+	base := "blah"
+	importpath := "github.com/Google/go-containerregistry/cmd/crane"
+
+	fp, err := ioutil.TempFile("", "")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer fp.Close()
+	defer os.Remove(fp.Name())
+
+	expectedRepo := fmt.Sprintf("%s/%s", base, md5Hash(strings.ToLower(importpath)))
+
+	tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", "example.com", expectedRepo))
+	if err != nil {
+		t.Fatalf("NewTag() = %v", err)
+	}
+
+	repoName := fmt.Sprintf("%s/%s", "example.com", base)
+	tagss := [][]string{{
+		// no tags
+	}, {
+		// one tag
+		"v0.1.0",
+	}, {
+		// multiple tags
+		"latest",
+		"debug",
+	}}
+	for _, tags := range tagss {
+		tp := NewTarball(fp.Name(), repoName, md5Hash, tags)
+		if d, err := tp.Publish(img, importpath); err != nil {
+			t.Errorf("Publish() = %v", err)
+		} else if !strings.HasPrefix(d.String(), tag.Repository.String()) {
+			t.Errorf("Publish() = %v, wanted prefix %v", d, tag.Repository)
+		}
+	}
+}