1
0
mirror of https://github.com/ko-build/ko.git synced 2025-03-17 20:47:51 +02:00

bump ggcr dep to v0.5.0 (#349)

This commit is contained in:
Jason Hall 2021-04-28 18:08:12 -04:00 committed by GitHub
parent 938bbcdbd4
commit 5395f992fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 1172 additions and 563 deletions

2
go.mod
View File

@ -10,7 +10,7 @@ require (
github.com/fsnotify/fsnotify v1.4.9
github.com/go-training/helloworld v0.0.0-20200225145412-ba5f4379d78b
github.com/google/go-cmp v0.5.4
github.com/google/go-containerregistry v0.4.1-0.20210216200643-d81088d9983e
github.com/google/go-containerregistry v0.5.0
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
github.com/mattmoor/dep-notify v0.0.0-20190205035814-a45dec370a17
github.com/onsi/gomega v1.10.3 // indirect

2
go.sum
View File

@ -185,6 +185,8 @@ github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-containerregistry v0.4.1-0.20210216200643-d81088d9983e h1:8jPkeUOUhBGSDpuIUyjKHsC0I5/nI4zvmv6DB2TACh4=
github.com/google/go-containerregistry v0.4.1-0.20210216200643-d81088d9983e/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=
github.com/google/go-containerregistry v0.5.0 h1:eb9sinv4PKm0AUwQGov0mvIdA4pyBGjRofxN4tWnMwM=
github.com/google/go-containerregistry v0.5.0/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=

View File

@ -43,8 +43,13 @@ func NewDaemon(namer Namer, tags []string) Interface {
return &demon{namer, tags}
}
// getOpts returns daemon.Options. It's a var to allow it to be overridden during tests.
var getOpts = func(ctx context.Context) []daemon.Option {
return []daemon.Option{daemon.WithContext(ctx)}
}
// Publish implements publish.Interface
func (d *demon) Publish(_ context.Context, br build.Result, s string) (name.Reference, error) {
func (d *demon) Publish(ctx context.Context, br build.Result, s string) (name.Reference, error) {
s = strings.TrimPrefix(s, build.StrictScheme)
// https://github.com/google/go-containerregistry/issues/212
s = strings.ToLower(s)
@ -100,7 +105,7 @@ func (d *demon) Publish(_ context.Context, br build.Result, s string) (name.Refe
}
log.Printf("Loading %v", digestTag)
if _, err := daemon.Write(digestTag, img); err != nil {
if _, err := daemon.Write(digestTag, img, getOpts(ctx)...); err != nil {
return nil, err
}
log.Printf("Loaded %v", digestTag)
@ -112,9 +117,7 @@ func (d *demon) Publish(_ context.Context, br build.Result, s string) (name.Refe
return nil, err
}
err = daemon.Tag(digestTag, tag)
if err != nil {
if err := daemon.Tag(digestTag, tag, getOpts(ctx)...); err != nil {
return nil, err
}
log.Printf("Added tag %v", tagName)

View File

@ -26,24 +26,30 @@ import (
"github.com/google/go-containerregistry/pkg/v1/random"
)
type MockImageLoader struct{}
type mockClient struct {
daemon.Client
}
var Tags []string
func (m *MockImageLoader) ImageLoad(_ context.Context, _ io.Reader, _ bool) (types.ImageLoadResponse, error) {
func (m *mockClient) NegotiateAPIVersion(context.Context) {}
func (m *mockClient) ImageLoad(context.Context, io.Reader, bool) (types.ImageLoadResponse, error) {
return types.ImageLoadResponse{
Body: ioutil.NopCloser(strings.NewReader("Loaded")),
}, nil
}
func (m *MockImageLoader) ImageTag(ctx context.Context, source, target string) error {
Tags = append(Tags, target)
func (m *mockClient) ImageTag(_ context.Context, _ string, tag string) error {
Tags = append(Tags, tag)
return nil
}
var Tags []string
func init() {
daemon.GetImageLoader = func() (daemon.ImageLoader, error) {
return &MockImageLoader{}, nil
getOpts = func(ctx context.Context) []daemon.Option {
return []daemon.Option{
daemon.WithContext(ctx),
daemon.WithClient(&mockClient{}),
}
}
}

View File

@ -15,10 +15,11 @@
package cmd
import (
"log"
"fmt"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/logs"
"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/spf13/cobra"
@ -33,35 +34,44 @@ func NewCmdAppend(options *[]crane.Option) *cobra.Command {
Use: "append",
Short: "Append contents of a tarball to a remote image",
Args: cobra.NoArgs,
Run: func(_ *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
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)
return fmt.Errorf("pulling %s: %v", baseRef, err)
}
}
img, err := crane.Append(base, newLayers...)
if err != nil {
log.Fatalf("appending %v: %v", newLayers, err)
return fmt.Errorf("appending %v: %v", newLayers, err)
}
if outFile != "" {
if err := crane.Save(img, newTag, outFile); err != nil {
log.Fatalf("writing output %q: %v", outFile, err)
return fmt.Errorf("writing output %q: %v", outFile, err)
}
} else {
if err := crane.Push(img, newTag, *options...); err != nil {
log.Fatalf("pushing image %s: %v", newTag, err)
return fmt.Errorf("pushing image %s: %v", newTag, err)
}
ref, err := name.ParseReference(newTag)
if err != nil {
return fmt.Errorf("parsing reference %s: %v", newTag, err)
}
d, err := img.Digest()
if err != nil {
return fmt.Errorf("digest: %v", err)
}
fmt.Println(ref.Context().Digest(d.String()))
}
return nil
},
}
appendCmd.Flags().StringVarP(&baseRef, "base", "b", "", "Name of base image to append to")

View File

@ -78,18 +78,18 @@ func NewCmdAuthGet(argv ...string) *cobra.Command {
Short: "Implements a credential helper",
Example: eg,
Args: cobra.NoArgs,
Run: func(_ *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
b, err := ioutil.ReadAll(os.Stdin)
if err != nil {
log.Fatal(err)
return err
}
reg, err := name.NewRegistry(strings.TrimSpace(string(b)))
if err != nil {
log.Fatal(err)
return err
}
authorizer, err := authn.DefaultKeychain.Resolve(reg)
if err != nil {
log.Fatal(err)
return err
}
// If we don't find any credentials, there's a magic error to return:
@ -103,15 +103,13 @@ func NewCmdAuthGet(argv ...string) *cobra.Command {
auth, err := authorizer.Authorization()
if err != nil {
log.Fatal(err)
return err
}
// Convert back to a form that credential helpers can parse so that this
// can act as a meta credential helper.
creds := toCreds(auth)
if err := json.NewEncoder(os.Stdout).Encode(creds); err != nil {
log.Fatal(err)
}
return json.NewEncoder(os.Stdout).Encode(creds)
},
}
}
@ -132,17 +130,15 @@ func NewCmdAuthLogin(argv ...string) *cobra.Command {
Short: "Log in to a registry",
Example: eg,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
reg, err := name.NewRegistry(args[0])
if err != nil {
log.Fatal(err)
return err
}
opts.serverAddress = reg.Name()
if err := login(opts); err != nil {
log.Fatal(err)
}
return login(opts)
},
}

View File

@ -15,8 +15,8 @@
package cmd
import (
"fmt"
"io"
"log"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/spf13/cobra"
@ -29,19 +29,20 @@ func NewCmdBlob(options *[]crane.Option) *cobra.Command {
Short: "Read a blob from the registry",
Example: "crane blob ubuntu@sha256:4c1d20cdee96111c8acf1858b62655a37ce81ae48648993542b7ac363ac5c0e5 > blob.tar.gz",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
src := args[0]
layer, err := crane.PullLayer(src, *options...)
if err != nil {
log.Fatalf("pulling layer %s: %v", src, err)
return fmt.Errorf("pulling layer %s: %v", src, err)
}
blob, err := layer.Compressed()
if err != nil {
log.Fatalf("fetching blob %s: %v", src, err)
return fmt.Errorf("fetching blob %s: %v", src, err)
}
if _, err := io.Copy(cmd.OutOrStdout(), blob); err != nil {
log.Fatalf("copying blob %s: %v", src, err)
return fmt.Errorf("copying blob %s: %v", src, err)
}
return nil
},
}
}

View File

@ -16,7 +16,6 @@ package cmd
import (
"fmt"
"log"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/spf13/cobra"
@ -28,16 +27,17 @@ func NewCmdCatalog(options *[]crane.Option) *cobra.Command {
Use: "catalog",
Short: "List the repos in a registry",
Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
reg := args[0]
repos, err := crane.Catalog(reg, *options...)
if err != nil {
log.Fatalf("reading repos for %s: %v", reg, err)
return fmt.Errorf("reading repos for %s: %v", reg, err)
}
for _, repo := range repos {
fmt.Println(repo)
}
return nil
},
}
}

View File

@ -16,7 +16,6 @@ package cmd
import (
"fmt"
"log"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/spf13/cobra"
@ -28,12 +27,13 @@ func NewCmdConfig(options *[]crane.Option) *cobra.Command {
Use: "config IMAGE",
Short: "Get the config of an image",
Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
cfg, err := crane.Config(args[0], *options...)
if err != nil {
log.Fatalf("fetching config: %v", err)
return fmt.Errorf("fetching config: %v", err)
}
fmt.Print(string(cfg))
return nil
},
}
}

View File

@ -15,8 +15,6 @@
package cmd
import (
"log"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/spf13/cobra"
)
@ -28,11 +26,9 @@ func NewCmdCopy(options *[]crane.Option) *cobra.Command {
Aliases: []string{"cp"},
Short: "Efficiently copy a remote image from src to dst",
Args: cobra.ExactArgs(2),
Run: func(_ *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
src, dst := args[0], args[1]
if err := crane.Copy(src, dst, *options...); err != nil {
log.Fatal(err)
}
return crane.Copy(src, dst, *options...)
},
}
}

View File

@ -15,8 +15,6 @@
package cmd
import (
"log"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/spf13/cobra"
)
@ -27,11 +25,9 @@ func NewCmdDelete(options *[]crane.Option) *cobra.Command {
Use: "delete IMAGE",
Short: "Delete an image reference from its registry",
Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
ref := args[0]
if err := crane.Delete(ref, *options...); err != nil {
log.Fatalf("deleting %s: %v", ref, err)
}
return crane.Delete(ref, *options...)
},
}
}

View File

@ -15,8 +15,8 @@
package cmd
import (
"errors"
"fmt"
"log"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/spf13/cobra"
@ -24,16 +24,52 @@ import (
// NewCmdDigest creates a new cobra.Command for the digest subcommand.
func NewCmdDigest(options *[]crane.Option) *cobra.Command {
return &cobra.Command{
var tarball string
cmd := &cobra.Command{
Use: "digest IMAGE",
Short: "Get the digest of an image",
Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) {
digest, err := crane.Digest(args[0], *options...)
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if tarball == "" && len(args) == 0 {
cmd.Help()
return errors.New("image reference required without --tarball")
}
digest, err := getDigest(tarball, args, options)
if err != nil {
log.Fatalf("computing digest: %v", err)
return err
}
fmt.Println(digest)
return nil
},
}
cmd.Flags().StringVar(&tarball, "tarball", "", "(Optional) path to tarball containing the image")
return cmd
}
func getDigest(tarball string, args []string, options *[]crane.Option) (string, error) {
if tarball != "" {
return getTarballDigest(tarball, args, options)
}
return crane.Digest(args[0], *options...)
}
func getTarballDigest(tarball string, args []string, options *[]crane.Option) (string, error) {
tag := ""
if len(args) > 0 {
tag = args[0]
}
img, err := crane.LoadTag(tarball, tag, *options...)
if err != nil {
return "", fmt.Errorf("loading image from %q: %v", tarball, err)
}
digest, err := img.Digest()
if err != nil {
return "", fmt.Errorf("computing digest: %v", err)
}
return digest.String(), nil
}

View File

@ -15,7 +15,7 @@
package cmd
import (
"log"
"fmt"
"os"
"github.com/google/go-containerregistry/pkg/crane"
@ -33,23 +33,21 @@ func NewCmdExport(options *[]crane.Option) *cobra.Command {
# Write tarball to file
crane export ubuntu ubuntu.tar`,
Args: cobra.ExactArgs(2),
Run: func(_ *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
src, dst := args[0], args[1]
f, err := openFile(dst)
if err != nil {
log.Fatalf("failed to open %s: %v", dst, err)
return fmt.Errorf("failed to open %s: %v", dst, err)
}
defer f.Close()
img, err := crane.Pull(src, *options...)
if err != nil {
log.Fatal(err)
return fmt.Errorf("pulling %s: %v", src, err)
}
if err := crane.Export(img, f); err != nil {
log.Fatalf("exporting %s: %v", src, err)
}
return crane.Export(img, f)
},
}
}

View File

@ -16,7 +16,6 @@ package cmd
import (
"fmt"
"log"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/spf13/cobra"
@ -28,16 +27,17 @@ func NewCmdList(options *[]crane.Option) *cobra.Command {
Use: "ls REPO",
Short: "List the tags in a repo",
Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
repo := args[0]
tags, err := crane.ListTags(repo, *options...)
if err != nil {
log.Fatalf("reading tags for %s: %v", repo, err)
return fmt.Errorf("reading tags for %s: %v", repo, err)
}
for _, tag := range tags {
fmt.Println(tag)
}
return nil
},
}
}

View File

@ -16,7 +16,6 @@ package cmd
import (
"fmt"
"log"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/spf13/cobra"
@ -28,13 +27,14 @@ func NewCmdManifest(options *[]crane.Option) *cobra.Command {
Use: "manifest IMAGE",
Short: "Get the manifest of an image",
Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
src := args[0]
manifest, err := crane.Manifest(src, *options...)
if err != nil {
log.Fatalf("fetching manifest %s: %v", src, err)
return fmt.Errorf("fetching manifest %s: %v", src, err)
}
fmt.Print(string(manifest))
return nil
},
}
}

View File

@ -0,0 +1,107 @@
// Copyright 2021 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"log"
"strings"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/spf13/cobra"
)
// NewCmdMutate creates a new cobra.Command for the mutate subcommand.
func NewCmdMutate(options *[]crane.Option) *cobra.Command {
var lbls []string
var entrypoint string
var newRef string
mutateCmd := &cobra.Command{
Use: "mutate",
Short: "Modify image labels and annotations",
Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) {
// Pull image and get config.
ref := args[0]
img, err := crane.Pull(ref, *options...)
if err != nil {
log.Fatalf("pulling %s: %v", ref, err)
}
cfg, err := img.ConfigFile()
if err != nil {
log.Fatalf("getting config: %v", err)
}
cfg = cfg.DeepCopy()
// Set labels.
if cfg.Config.Labels == nil {
cfg.Config.Labels = map[string]string{}
}
labels := map[string]string{}
for _, l := range lbls {
parts := strings.SplitN(l, "=", 2)
if len(parts) == 1 {
log.Fatalf("parsing label %q, not enough parts", l)
}
labels[parts[0]] = parts[1]
}
for k, v := range labels {
cfg.Config.Labels[k] = v
}
// Set entrypoint.
if entrypoint != "" {
// NB: This doesn't attempt to do anything smart about splitting the string into multiple entrypoint elements.
cfg.Config.Entrypoint = []string{entrypoint}
}
// Mutate and write image.
img, err = mutate.Config(img, cfg.Config)
if err != nil {
log.Fatalf("mutating config: %v", err)
}
// If the new ref isn't provided, write over the original image.
// If that ref was provided by digest (e.g., output from
// another crane command), then strip that and push the
// mutated image by digest instead.
if newRef == "" {
newRef = ref
}
digest, err := img.Digest()
if err != nil {
log.Fatalf("digesting new image: %v", err)
}
r, err := name.ParseReference(newRef)
if err != nil {
log.Fatalf("parsing %s: %v", newRef, err)
}
if _, ok := r.(name.Digest); ok {
newRef = r.Context().Digest(digest.String()).String()
}
if err := crane.Push(img, newRef, *options...); err != nil {
log.Fatalf("pushing %s: %v", newRef, err)
}
fmt.Println(r.Context().Digest(digest.String()))
},
}
mutateCmd.Flags().StringSliceVarP(&lbls, "label", "l", nil, "New labels to add")
mutateCmd.Flags().StringVar(&entrypoint, "entrypoint", "", "New entrypoing to set")
mutateCmd.Flags().StringVarP(&newRef, "tag", "t", "", "New tag to apply to mutated image. If not provided, push by digest to the original image repository.")
return mutateCmd
}

View File

@ -15,8 +15,6 @@
package cmd
import (
"log"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/spf13/cobra"
)
@ -31,11 +29,9 @@ func NewCmdOptimize(options *[]crane.Option) *cobra.Command {
Aliases: []string{"opt"},
Short: "Optimize a remote container image from src to dst",
Args: cobra.ExactArgs(2),
Run: func(_ *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
src, dst := args[0], args[1]
if err := crane.Optimize(src, dst, files, *options...); err != nil {
log.Fatal(err)
}
return crane.Optimize(src, dst, files, *options...)
},
}

View File

@ -16,9 +16,9 @@ package cmd
import (
"fmt"
"log"
"github.com/google/go-containerregistry/pkg/crane"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/cache"
"github.com/spf13/cobra"
)
@ -29,34 +29,40 @@ func NewCmdPull(options *[]crane.Option) *cobra.Command {
cmd := &cobra.Command{
Use: "pull IMAGE TARBALL",
Short: "Pull a remote image by reference and store its contents in a tarball",
Args: cobra.ExactArgs(2),
Run: func(_ *cobra.Command, args []string) {
src, path := args[0], args[1]
img, err := crane.Pull(src, *options...)
if err != nil {
log.Fatal(err)
}
if cachePath != "" {
img = cache.Image(img, cache.NewFilesystemCache(cachePath))
Short: "Pull remote images by reference and store their contents in a tarball",
Args: cobra.MinimumNArgs(2),
RunE: func(_ *cobra.Command, args []string) error {
imageMap := map[string]v1.Image{}
srcList, path := args[:len(args)-1], args[len(args)-1]
for _, src := range srcList {
img, err := crane.Pull(src, *options...)
if err != nil {
return fmt.Errorf("pulling %s: %v", src, err)
}
if cachePath != "" {
img = cache.Image(img, cache.NewFilesystemCache(cachePath))
}
imageMap[src] = img
}
switch format {
case "tarball":
if err := crane.Save(img, src, path); err != nil {
log.Fatalf("saving tarball %s: %v", path, err)
if err := crane.MultiSave(imageMap, path); err != nil {
return fmt.Errorf("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)
if err := crane.MultiSaveLegacy(imageMap, path); err != nil {
return fmt.Errorf("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)
if err := crane.MultiSaveOCI(imageMap, path); err != nil {
return fmt.Errorf("saving oci image layout %s: %v", path, err)
}
default:
log.Fatalf("unexpected --format: %q (valid values are: tarball, legacy, and oci)", format)
return fmt.Errorf("unexpected --format: %q (valid values are: tarball, legacy, and oci)", format)
}
return nil
},
}
cmd.Flags().StringVarP(&cachePath, "cache_path", "c", "", "Path to cache image layers")

View File

@ -15,7 +15,7 @@
package cmd
import (
"log"
"fmt"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/spf13/cobra"
@ -27,16 +27,14 @@ func NewCmdPush(options *[]crane.Option) *cobra.Command {
Use: "push TARBALL IMAGE",
Short: "Push image contents as a tarball to a remote registry",
Args: cobra.ExactArgs(2),
Run: func(_ *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
path, tag := args[0], args[1]
img, err := crane.Load(path)
if err != nil {
log.Fatalf("loading %s as tarball: %v", path, err)
return fmt.Errorf("loading %s as tarball: %v", path, err)
}
if err := crane.Push(img, tag, *options...); err != nil {
log.Fatalf("pushing %s: %v", tag, err)
}
return crane.Push(img, tag, *options...)
},
}
}

View File

@ -16,7 +16,6 @@ package cmd
import (
"fmt"
"log"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/v1/mutate"
@ -31,36 +30,37 @@ func NewCmdRebase(options *[]crane.Option) *cobra.Command {
Use: "rebase",
Short: "Rebase an image onto a new base image",
Args: cobra.NoArgs,
Run: func(*cobra.Command, []string) {
RunE: func(*cobra.Command, []string) error {
origImg, err := crane.Pull(orig, *options...)
if err != nil {
log.Fatalf("pulling %s: %v", orig, err)
return fmt.Errorf("pulling %s: %v", orig, err)
}
oldBaseImg, err := crane.Pull(oldBase, *options...)
if err != nil {
log.Fatalf("pulling %s: %v", oldBase, err)
return fmt.Errorf("pulling %s: %v", oldBase, err)
}
newBaseImg, err := crane.Pull(newBase, *options...)
if err != nil {
log.Fatalf("pulling %s: %v", newBase, err)
return fmt.Errorf("pulling %s: %v", newBase, err)
}
img, err := mutate.Rebase(origImg, oldBaseImg, newBaseImg)
if err != nil {
log.Fatalf("rebasing: %v", err)
return fmt.Errorf("rebasing: %v", err)
}
if err := crane.Push(img, rebased, *options...); err != nil {
log.Fatalf("pushing %s: %v", rebased, err)
return fmt.Errorf("pushing %s: %v", rebased, err)
}
digest, err := img.Digest()
if err != nil {
log.Fatalf("digesting rebased: %v", err)
return fmt.Errorf("digesting rebased: %v", err)
}
fmt.Println(digest.String())
return nil
},
}
rebaseCmd.Flags().StringVarP(&orig, "original", "", "", "Original image to rebase")

View File

@ -93,6 +93,7 @@ func New(use, short string, options []crane.Option) *cobra.Command {
NewCmdTag(&options),
NewCmdValidate(&options),
NewCmdVersion(),
NewCmdMutate(&options),
}
root.AddCommand(commands...)

View File

@ -15,8 +15,6 @@
package cmd
import (
"log"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/spf13/cobra"
)
@ -38,11 +36,9 @@ crane tag registry.example.com/library/ubuntu:v0 v1
Example: `# Add a v1 tag to ubuntu
crane tag ubuntu v1`,
Args: cobra.ExactArgs(2),
Run: func(_ *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
img, tag := args[0], args[1]
if err := crane.Tag(img, tag, *options...); err != nil {
log.Fatal(err)
}
return crane.Tag(img, tag, *options...)
},
}
}

View File

@ -16,7 +16,6 @@ package cmd
import (
"fmt"
"log"
"github.com/google/go-containerregistry/pkg/crane"
v1 "github.com/google/go-containerregistry/pkg/v1"
@ -33,7 +32,7 @@ func NewCmdValidate(options *[]crane.Option) *cobra.Command {
Use: "validate",
Short: "Validate that an image is well-formed",
Args: cobra.ExactArgs(0),
Run: func(_ *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
for flag, maker := range map[string]func(string, ...crane.Option) (v1.Image, error){
tarballPath: makeTarball,
remoteRef: crane.Pull,
@ -43,7 +42,7 @@ func NewCmdValidate(options *[]crane.Option) *cobra.Command {
}
img, err := maker(flag, *options...)
if err != nil {
log.Fatalf("failed to read image %s: %v", flag, err)
return fmt.Errorf("failed to read image %s: %v", flag, err)
}
if err := validate.Image(img); err != nil {
@ -52,6 +51,7 @@ func NewCmdValidate(options *[]crane.Option) *cobra.Command {
fmt.Printf("PASS: %s\n", flag)
}
}
return nil
},
}
validateCmd.Flags().StringVar(&tarballPath, "tarball", "", "Path to tarball to validate")

View File

@ -20,7 +20,7 @@ import (
"compress/gzip"
"io"
"github.com/google/go-containerregistry/pkg/v1/internal/and"
"github.com/google/go-containerregistry/internal/and"
)
var gzipMagicHeader = []byte{'\x1f', '\x8b'}

View File

@ -0,0 +1,57 @@
// Copyright 2019 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 legacy provides methods for interacting with legacy image formats.
package legacy
import (
"bytes"
"encoding/json"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
)
// CopySchema1 allows `[g]crane cp` to work with old images without adding
// full support for schema 1 images to this package.
func CopySchema1(desc *remote.Descriptor, srcRef, dstRef name.Reference, opts ...remote.Option) error {
m := schema1{}
if err := json.NewDecoder(bytes.NewReader(desc.Manifest)).Decode(&m); err != nil {
return err
}
for _, layer := range m.FSLayers {
src := srcRef.Context().Digest(layer.BlobSum)
dst := dstRef.Context().Digest(layer.BlobSum)
blob, err := remote.Layer(src, opts...)
if err != nil {
return err
}
if err := remote.WriteLayer(dst.Context(), blob, opts...); err != nil {
return err
}
}
return remote.Put(dstRef, desc, opts...)
}
type fslayer struct {
BlobSum string `json:"blobSum"`
}
type schema1 struct {
FSLayers []fslayer `json:"fsLayers"`
}

View File

@ -20,7 +20,7 @@ import (
"context"
"fmt"
"github.com/google/go-containerregistry/pkg/internal/retry/wait"
"github.com/google/go-containerregistry/internal/retry/wait"
)
// Backoff is an alias of our own wait.Backoff to avoid name conflicts with

View File

@ -20,8 +20,8 @@ import (
"hash"
"io"
"github.com/google/go-containerregistry/internal/and"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/internal/and"
)
type verifyReader struct {

View File

@ -40,20 +40,33 @@ func Append(base v1.Image, paths ...string) (v1.Image, error) {
}
func getLayer(path string) (v1.Layer, error) {
f, err := streamFile(path)
if err != nil {
return nil, err
}
if f != nil {
return stream.NewLayer(f), nil
}
return tarball.LayerFromFile(path)
}
// If we're dealing with a named pipe, trying to open it multiple times will
// fail, so we need to do a streaming upload.
//
// returns nil, nil for non-streaming files
func streamFile(path string) (*os.File, error) {
if path == "-" {
return os.Stdin, nil
}
fi, err := os.Stat(path)
if err != nil {
return nil, err
}
// If we're dealing with a named pipe, trying to open it multiple times will
// fail, so we need to do a streaming upload.
if !fi.Mode().IsRegular() {
rc, err := os.Open(path)
if err != nil {
return nil, err
}
return stream.NewLayer(rc), nil
return os.Open(path)
}
return tarball.LayerFromFile(path)
return nil, nil
}

View File

@ -17,8 +17,7 @@ package crane
import (
"fmt"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/internal/legacy"
"github.com/google/go-containerregistry/internal/legacy"
"github.com/google/go-containerregistry/pkg/logs"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
@ -59,7 +58,7 @@ func Copy(src, dst string, opt ...Option) error {
}
case types.DockerManifestSchema1, types.DockerManifestSchema1Signed:
// Handle schema 1 images separately.
if err := copySchema1(desc, srcRef, dstRef); err != nil {
if err := legacy.CopySchema1(desc, srcRef, dstRef, o.remote...); err != nil {
return fmt.Errorf("failed to copy schema 1 image: %v", err)
}
default:
@ -87,16 +86,3 @@ func copyIndex(desc *remote.Descriptor, dstRef name.Reference, o options) error
}
return remote.WriteIndex(dstRef, idx, o.remote...)
}
func copySchema1(desc *remote.Descriptor, srcRef, dstRef name.Reference) error {
srcAuth, err := authn.DefaultKeychain.Resolve(srcRef.Context().Registry)
if err != nil {
return err
}
dstAuth, err := authn.DefaultKeychain.Resolve(dstRef.Context().Registry)
if err != nil {
return err
}
return legacy.CopySchema1(desc, srcRef, dstRef, srcAuth, dstAuth)
}

View File

@ -14,6 +14,8 @@
package crane
import "github.com/google/go-containerregistry/pkg/logs"
// Digest returns the sha256 hash of the remote image at ref.
func Digest(ref string, opt ...Option) (string, error) {
o := makeOptions(opt...)
@ -39,7 +41,12 @@ func Digest(ref string, opt ...Option) (string, error) {
}
desc, err := head(ref, opt...)
if err != nil {
return "", err
logs.Warn.Printf("HEAD request failed, falling back on GET: %v", err)
rdesc, err := getManifest(ref, opt...)
if err != nil {
return "", err
}
return rdesc.Digest.String(), nil
}
return desc.Digest.String(), nil
}

View File

@ -37,7 +37,7 @@ func Pull(src string, opt ...Option) (v1.Image, error) {
o := makeOptions(opt...)
ref, err := name.ParseReference(src, o.name...)
if err != nil {
return nil, fmt.Errorf("parsing tag %q: %v", src, err)
return nil, fmt.Errorf("parsing reference %q: %v", src, err)
}
return remote.Image(ref, o.remote...)
@ -45,26 +45,36 @@ func Pull(src string, opt ...Option) (v1.Image, error) {
// Save writes the v1.Image img as a tarball at path with tag src.
func Save(img v1.Image, src, path string) error {
ref, err := name.ParseReference(src)
if err != nil {
return fmt.Errorf("parsing ref %q: %v", src, err)
}
imgMap := map[string]v1.Image{src: img}
return MultiSave(imgMap, path)
}
// WriteToFile wants a tag to write to the tarball, but we might have
// been given a digest.
// If the original ref was a tag, use that. Otherwise, if it was a
// digest, tag the image with :i-was-a-digest instead.
tag, ok := ref.(name.Tag)
if !ok {
d, ok := ref.(name.Digest)
if !ok {
return fmt.Errorf("ref wasn't a tag or digest")
// MultiSave writes collection of v1.Image img with tag as a tarball.
func MultiSave(imgMap map[string]v1.Image, path string) error {
tagToImage := map[name.Tag]v1.Image{}
for src, img := range imgMap {
ref, err := name.ParseReference(src)
if err != nil {
return fmt.Errorf("parsing ref %q: %v", src, err)
}
tag = d.Repository.Tag(iWasADigestTag)
}
// WriteToFile wants a tag to write to the tarball, but we might have
// been given a digest.
// If the original ref was a tag, use that. Otherwise, if it was a
// digest, tag the image with :i-was-a-digest instead.
tag, ok := ref.(name.Tag)
if !ok {
d, ok := ref.(name.Digest)
if !ok {
return fmt.Errorf("ref wasn't a tag or digest")
}
tag = d.Repository.Tag(iWasADigestTag)
}
tagToImage[tag] = img
}
// no progress channel (for now)
return tarball.WriteToFile(path, tag, img)
return tarball.MultiWriteToFile(path, tagToImage)
}
// PullLayer returns the given layer from a registry.
@ -80,9 +90,20 @@ func PullLayer(ref string, opt ...Option) (v1.Layer, error) {
// 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)
imgMap := map[string]v1.Image{src: img}
return MultiSave(imgMap, path)
}
// MultiSaveLegacy writes collection of v1.Image img with tag as a legacy tarball.
func MultiSaveLegacy(imgMap map[string]v1.Image, path string) error {
refToImage := map[name.Reference]v1.Image{}
for src, img := range imgMap {
ref, err := name.ParseReference(src)
if err != nil {
return fmt.Errorf("parsing ref %q: %v", src, err)
}
refToImage[ref] = img
}
w, err := os.Create(path)
@ -91,12 +112,19 @@ func SaveLegacy(img v1.Image, src, path string) error {
}
defer w.Close()
return legacy.Write(ref, img, w)
return legacy.MultiWrite(refToImage, 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 {
imgMap := map[string]v1.Image{"": img}
return MultiSaveOCI(imgMap, path)
}
// MultiSaveOCI writes collection of 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 MultiSaveOCI(imgMap map[string]v1.Image, path string) error {
p, err := layout.FromPath(path)
if err != nil {
p, err = layout.Write(path, empty.Index)
@ -104,5 +132,10 @@ func SaveOCI(img v1.Image, path string) error {
return err
}
}
return p.AppendImage(img)
for _, img := range imgMap {
if err = p.AppendImage(img); err != nil {
return err
}
}
return nil
}

View File

@ -24,17 +24,31 @@ import (
)
// Load reads the tarball at path as a v1.Image.
func Load(path string) (v1.Image, error) {
// TODO: Allow tag?
return tarball.ImageFromPath(path, nil)
func Load(path string, opt ...Option) (v1.Image, error) {
return LoadTag(path, "")
}
// LoadTag reads a tag from the tarball at path as a v1.Image.
// If tag is "", will attempt to read the tarball as a single image.
func LoadTag(path, tag string, opt ...Option) (v1.Image, error) {
if tag == "" {
return tarball.ImageFromPath(path, nil)
}
o := makeOptions(opt...)
t, err := name.NewTag(tag, o.name...)
if err != nil {
return nil, fmt.Errorf("parsing tag %q: %v", tag, err)
}
return tarball.ImageFromPath(path, &t)
}
// Push pushes the v1.Image img to a registry as dst.
func Push(img v1.Image, dst string, opt ...Option) error {
o := makeOptions(opt...)
tag, err := name.NewTag(dst, o.name...)
tag, err := name.ParseReference(dst, o.name...)
if err != nil {
return fmt.Errorf("parsing tag %q: %v", dst, err)
return fmt.Errorf("parsing reference %q: %v", dst, err)
}
return remote.Write(tag, img, o.remote...)
}

View File

@ -1,102 +0,0 @@
// Copyright 2019 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 legacy provides methods for interacting with legacy image formats.
package legacy
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/logs"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
)
// CopySchema1 allows `[g]crane cp` to work with old images without adding
// full support for schema 1 images to this package.
func CopySchema1(desc *remote.Descriptor, srcRef, dstRef name.Reference, srcAuth, dstAuth authn.Authenticator) error {
m := schema1{}
if err := json.NewDecoder(bytes.NewReader(desc.Manifest)).Decode(&m); err != nil {
return err
}
for _, layer := range m.FSLayers {
src := srcRef.Context().Digest(layer.BlobSum)
dst := dstRef.Context().Digest(layer.BlobSum)
blob, err := remote.Layer(src, remote.WithAuth(srcAuth))
if err != nil {
return err
}
if err := remote.WriteLayer(dst.Context(), blob, remote.WithAuth(dstAuth)); err != nil {
return err
}
}
return putManifest(desc, dstRef, dstAuth)
}
// TODO: perhaps expose this in remote?
func putManifest(desc *remote.Descriptor, dstRef name.Reference, dstAuth authn.Authenticator) error {
reg := dstRef.Context().Registry
scopes := []string{dstRef.Scope(transport.PushScope)}
// TODO(jonjohnsonjr): Use NewWithContext.
tr, err := transport.New(reg, dstAuth, http.DefaultTransport, scopes)
if err != nil {
return err
}
client := &http.Client{Transport: tr}
u := url.URL{
Scheme: dstRef.Context().Registry.Scheme(),
Host: dstRef.Context().RegistryStr(),
Path: fmt.Sprintf("/v2/%s/manifests/%s", dstRef.Context().RepositoryStr(), dstRef.Identifier()),
}
req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewBuffer(desc.Manifest))
if err != nil {
return err
}
req.Header.Set("Content-Type", string(desc.MediaType))
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if err := transport.CheckError(resp, http.StatusOK, http.StatusCreated, http.StatusAccepted); err != nil {
return err
}
// The image was successfully pushed!
logs.Progress.Printf("%v: digest: %v size: %d", dstRef, desc.Digest, len(desc.Manifest))
return nil
}
type fslayer struct {
BlobSum string `json:"blobSum"`
}
type schema1 struct {
FSLayers []fslayer `json:"fsLayers"`
}

View File

@ -47,3 +47,30 @@ func ParseReference(s string, opts ...Option) (Reference, error) {
return nil, NewErrBadName("could not parse reference: " + s)
}
type stringConst string
// MustParseReference behaves like ParseReference, but panics instead of
// returning an error. It's intended for use in tests, or when a value is
// expected to be valid at code authoring time.
//
// To discourage its use in scenarios where the value is not known at code
// authoring time, it must be passed a string constant:
//
// const str = "valid/string"
// MustParseReference(str)
// MustParseReference("another/valid/string")
// MustParseReference(str + "/and/more")
//
// These will not compile:
//
// var str = "valid/string"
// MustParseReference(str)
// MustParseReference(strings.Join([]string{"valid", "string"}, "/"))
func MustParseReference(s stringConst, opts ...Option) Reference {
ref, err := ParseReference(string(s), opts...)
if err != nil {
panic(err)
}
return ref
}

View File

@ -21,6 +21,7 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"sort"
"strconv"
@ -31,6 +32,10 @@ import (
"github.com/google/go-containerregistry/pkg/v1/types"
)
type catalog struct {
Repos []string `json:"repositories"`
}
type listTags struct {
Name string `json:"name"`
Tags []string `json:"tags"`
@ -45,6 +50,7 @@ type manifests struct {
// maps repo -> manifest tag/digest -> manifest
manifests map[string]map[string]manifest
lock sync.Mutex
log *log.Logger
}
func isManifest(req *http.Request) bool {
@ -65,6 +71,16 @@ func isTags(req *http.Request) bool {
return elems[len(elems)-2] == "tags"
}
func isCatalog(req *http.Request) bool {
elems := strings.Split(req.URL.Path, "/")
elems = elems[1:]
if len(elems) < 2 {
return false
}
return elems[len(elems)-1] == "_catalog"
}
// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-an-image-manifest
// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-an-image
func (m *manifests) handle(resp http.ResponseWriter, req *http.Request) *regError {
@ -172,6 +188,7 @@ func (m *manifests) handle(resp http.ResponseWriter, req *http.Request) *regErro
}
} else {
// TODO: Probably want to do an existence check for blobs.
m.log.Printf("TODO: Check blobs for %q", desc.Digest)
}
}
}
@ -273,3 +290,45 @@ func (m *manifests) handleTags(resp http.ResponseWriter, req *http.Request) *reg
Message: "We don't understand your method + url",
}
}
func (m *manifests) handleCatalog(resp http.ResponseWriter, req *http.Request) *regError {
query := req.URL.Query()
nStr := query.Get("n")
n := 10000
if nStr != "" {
n, _ = strconv.Atoi(nStr)
}
if req.Method == "GET" {
m.lock.Lock()
defer m.lock.Unlock()
var repos []string
countRepos := 0
// TODO: implement pagination
for key := range m.manifests {
if countRepos >= n {
break
}
countRepos++
repos = append(repos, key)
}
repositoriesToList := catalog{
Repos: repos,
}
msg, _ := json.Marshal(repositoriesToList)
resp.Header().Set("Content-Length", fmt.Sprint(len(msg)))
resp.WriteHeader(http.StatusOK)
io.Copy(resp, bytes.NewReader([]byte(msg)))
return nil
}
return &regError{
Status: http.StatusBadRequest,
Code: "METHOD_UNKNOWN",
Message: "We don't understand your method + url",
}
}

View File

@ -47,6 +47,9 @@ func (r *registry) v2(resp http.ResponseWriter, req *http.Request) *regError {
if isTags(req) {
return r.manifests.handleTags(resp, req)
}
if isCatalog(req) {
return r.manifests.handleCatalog(resp, req)
}
resp.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
if req.URL.Path != "/v2/" && req.URL.Path != "/v2" {
return &regError{
@ -79,6 +82,7 @@ func New(opts ...Option) http.Handler {
},
manifests: manifests{
manifests: map[string]map[string]manifest{},
log: log.New(os.Stderr, "", log.LstdFlags),
},
}
for _, o := range opts {
@ -95,5 +99,6 @@ type Option func(r *registry)
func Logger(l *log.Logger) Option {
return func(r *registry) {
r.log = l
r.manifests.log = l
}
}

View File

@ -17,7 +17,7 @@ package registry
import (
"net/http/httptest"
ggcrtest "github.com/google/go-containerregistry/pkg/internal/httptest"
ggcrtest "github.com/google/go-containerregistry/internal/httptest"
)
// TLS returns an httptest server, with an http client that has been configured to

View File

@ -3,9 +3,11 @@ package cache
import (
"errors"
"io"
"github.com/google/go-containerregistry/pkg/logs"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/types"
)
// Cache encapsulates methods to interact with cached layers.
@ -55,46 +57,66 @@ func (i *image) Layers() ([]v1.Layer, error) {
var out []v1.Layer
for _, l := range ls {
// Check if this layer is present in the cache in compressed
// form.
digest, err := l.Digest()
if err != nil {
return nil, err
}
if cl, err := i.c.Get(digest); err == nil {
// Layer found in the cache.
logs.Progress.Printf("Layer %s found (compressed) in cache", digest)
out = append(out, cl)
continue
} else if err != nil && err != ErrNotFound {
return nil, err
}
// Check if this layer is present in the cache in
// uncompressed form.
diffID, err := l.DiffID()
if err != nil {
return nil, err
}
if cl, err := i.c.Get(diffID); err == nil {
// Layer found in the cache.
logs.Progress.Printf("Layer %s found (uncompressed) in cache", diffID)
out = append(out, cl)
} else if err != nil && err != ErrNotFound {
return nil, err
}
// Not cached, fall through to real layer.
l, err = i.c.Put(l)
if err != nil {
return nil, err
}
out = append(out, l)
out = append(out, &lazyLayer{inner: l, c: i.c})
}
return out, nil
}
type lazyLayer struct {
inner v1.Layer
c Cache
}
func (l *lazyLayer) Compressed() (io.ReadCloser, error) {
digest, err := l.inner.Digest()
if err != nil {
return nil, err
}
if cl, err := l.c.Get(digest); err == nil {
// Layer found in the cache.
logs.Progress.Printf("Layer %s found (compressed) in cache", digest)
return cl.Compressed()
} else if err != nil && err != ErrNotFound {
return nil, err
}
// Not cached, pull and return the real layer.
logs.Progress.Printf("Layer %s not found (compressed) in cache, getting", digest)
rl, err := l.c.Put(l.inner)
if err != nil {
return nil, err
}
return rl.Compressed()
}
func (l *lazyLayer) Uncompressed() (io.ReadCloser, error) {
diffID, err := l.inner.DiffID()
if err != nil {
return nil, err
}
if cl, err := l.c.Get(diffID); err == nil {
// Layer found in the cache.
logs.Progress.Printf("Layer %s found (uncompressed) in cache", diffID)
return cl.Uncompressed()
} else if err != nil && err != ErrNotFound {
return nil, err
}
// Not cached, pull and return the real layer.
logs.Progress.Printf("Layer %s not found (uncompressed) in cache, getting", diffID)
rl, err := l.c.Put(l.inner)
if err != nil {
return nil, err
}
return rl.Uncompressed()
}
func (l *lazyLayer) Size() (int64, error) { return l.inner.Size() }
func (l *lazyLayer) DiffID() (v1.Hash, error) { return l.inner.DiffID() }
func (l *lazyLayer) Digest() (v1.Hash, error) { return l.inner.Digest() }
func (l *lazyLayer) MediaType() (types.MediaType, error) { return l.inner.MediaType() }
func (i *image) LayerByDigest(h v1.Hash) (v1.Layer, error) {
l, err := i.c.Get(h)
if err == ErrNotFound {

View File

@ -19,103 +19,71 @@ import (
"context"
"io"
"io/ioutil"
"sync"
"github.com/docker/docker/client"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/tarball"
)
// image accesses an image from a docker daemon
type image struct {
v1.Image
}
var _ v1.Image = (*image)(nil)
type imageOpener struct {
ref name.Reference
ref name.Reference
ctx context.Context
buffered bool
client Client
once sync.Once
bytes []byte
err error
}
// ImageOption is a functional option for Image.
type ImageOption func(*imageOpener) error
func (i *imageOpener) Open() (v1.Image, error) {
var opener tarball.Opener
var err error
if i.buffered {
opener, err = i.bufferedOpener(i.ref)
} else {
opener, err = i.unbufferedOpener(i.ref)
}
if err != nil {
return nil, err
}
tb, err := tarball.Image(opener, nil)
if err != nil {
return nil, err
}
img := &image{
Image: tb,
}
return img, nil
func (i *imageOpener) saveImage() (io.ReadCloser, error) {
return i.client.ImageSave(i.ctx, []string{i.ref.Name()})
}
func (i *imageOpener) saveImage(ref name.Reference) (io.ReadCloser, error) {
return i.client.ImageSave(context.Background(), []string{ref.Name()})
}
func (i *imageOpener) bufferedOpener(ref name.Reference) (tarball.Opener, error) {
func (i *imageOpener) bufferedOpener() (io.ReadCloser, error) {
// Store the tarball in memory and return a new reader into the bytes each time we need to access something.
rc, err := i.saveImage(ref)
if err != nil {
return nil, err
}
defer rc.Close()
i.once.Do(func() {
i.bytes, i.err = func() ([]byte, error) {
rc, err := i.saveImage()
if err != nil {
return nil, err
}
defer rc.Close()
imageBytes, err := ioutil.ReadAll(rc)
if err != nil {
return nil, err
}
// The tarball interface takes a function that it can call to return an opened reader-like object.
// Daemon comes from a set of bytes, so wrap them in a ReadCloser so it looks like an opened file.
return func() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewReader(imageBytes)), nil
}, nil
return ioutil.ReadAll(rc)
}()
})
// Wrap the bytes in a ReadCloser so it looks like an opened file.
return ioutil.NopCloser(bytes.NewReader(i.bytes)), i.err
}
func (i *imageOpener) unbufferedOpener(ref name.Reference) (tarball.Opener, error) {
func (i *imageOpener) opener() tarball.Opener {
if i.buffered {
return i.bufferedOpener
}
// To avoid storing the tarball in memory, do a save every time we need to access something.
return func() (io.ReadCloser, error) {
return i.saveImage(ref)
}, nil
return i.saveImage
}
// Image provides access to an image reference from the Docker daemon,
// applying functional options to the underlying imageOpener before
// resolving the reference into a v1.Image.
func Image(ref name.Reference, options ...ImageOption) (v1.Image, error) {
func Image(ref name.Reference, options ...Option) (v1.Image, error) {
o, err := makeOptions(options...)
if err != nil {
return nil, err
}
i := &imageOpener{
ref: ref,
buffered: true, // buffer by default
}
for _, option := range options {
if err := option(i); err != nil {
return nil, err
}
buffered: o.buffered,
client: o.client,
ctx: o.ctx,
}
if i.client == nil {
var err error
i.client, err = client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, err
}
}
i.client.NegotiateAPIVersion(context.Background())
return i.Open()
return tarball.Image(i.opener(), nil)
}

View File

@ -19,34 +19,76 @@ import (
"io"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
// ImageOption is an alias for Option.
// Deprecated: Use Option instead.
type ImageOption Option
// Option is a functional option for daemon operations.
type Option func(*options)
type options struct {
ctx context.Context
client Client
buffered bool
}
var defaultClient = func() (Client, error) {
return client.NewClientWithOpts(client.FromEnv)
}
func makeOptions(opts ...Option) (*options, error) {
o := &options{
buffered: true,
ctx: context.Background(),
}
for _, opt := range opts {
opt(o)
}
if o.client == nil {
client, err := defaultClient()
if err != nil {
return nil, err
}
o.client = client
}
o.client.NegotiateAPIVersion(o.ctx)
return o, nil
}
// WithBufferedOpener buffers the image.
func WithBufferedOpener() ImageOption {
return func(i *imageOpener) error {
return i.setBuffered(true)
func WithBufferedOpener() Option {
return func(o *options) {
o.buffered = true
}
}
// WithUnbufferedOpener streams the image to avoid buffering.
func WithUnbufferedOpener() ImageOption {
return func(i *imageOpener) error {
return i.setBuffered(false)
func WithUnbufferedOpener() Option {
return func(o *options) {
o.buffered = false
}
}
func (i *imageOpener) setBuffered(buffer bool) error {
i.buffered = buffer
return nil
}
// WithClient is a functional option to allow injecting a docker client.
//
// By default, github.com/docker/docker/client.FromEnv is used.
func WithClient(client Client) ImageOption {
return func(i *imageOpener) error {
i.client = client
return nil
func WithClient(client Client) Option {
return func(o *options) {
o.client = client
}
}
// WithContext is a functional option to pass through a context.Context.
//
// By default, context.Background() is used.
func WithContext(ctx context.Context) Option {
return func(o *options) {
o.ctx = ctx
}
}

View File

@ -15,47 +15,28 @@
package daemon
import (
"context"
"fmt"
"io"
"io/ioutil"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/tarball"
)
// ImageLoader is an interface for testing.
type ImageLoader interface {
ImageLoad(context.Context, io.Reader, bool) (types.ImageLoadResponse, error)
ImageTag(context.Context, string, string) error
}
// GetImageLoader is a variable so we can override in tests.
var GetImageLoader = func() (ImageLoader, error) {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, err
}
cli.NegotiateAPIVersion(context.Background())
return cli, nil
}
// Tag adds a tag to an already existent image.
func Tag(src, dest name.Tag) error {
cli, err := GetImageLoader()
func Tag(src, dest name.Tag, options ...Option) error {
o, err := makeOptions(options...)
if err != nil {
return err
}
return cli.ImageTag(context.Background(), src.String(), dest.String())
return o.client.ImageTag(o.ctx, src.String(), dest.String())
}
// Write saves the image into the daemon as the given tag.
func Write(tag name.Tag, img v1.Image) (string, error) {
cli, err := GetImageLoader()
func Write(tag name.Tag, img v1.Image, options ...Option) (string, error) {
o, err := makeOptions(options...)
if err != nil {
return "", err
}
@ -66,14 +47,14 @@ func Write(tag name.Tag, img v1.Image) (string, error) {
}()
// write the image in docker save format first, then load it
resp, err := cli.ImageLoad(context.Background(), pr, false)
resp, err := o.client.ImageLoad(o.ctx, pr, false)
if err != nil {
return "", fmt.Errorf("error loading image: %v", err)
}
defer resp.Body.Close()
b, readErr := ioutil.ReadAll(resp.Body)
b, err := ioutil.ReadAll(resp.Body)
response := string(b)
if readErr != nil {
if err != nil {
return response, fmt.Errorf("error reading load response body: %v", err)
}
return response, nil

View File

@ -87,7 +87,7 @@ func Hasher(name string) (hash.Hash, error) {
func (h *Hash) parse(unquoted string) error {
parts := strings.Split(unquoted, ":")
if len(parts) != 2 {
return fmt.Errorf("too many parts in hash: %s", unquoted)
return fmt.Errorf("cannot parse hash: %q", unquoted)
}
rest := strings.TrimLeft(parts[1], "0123456789abcdef")

View File

@ -3,40 +3,55 @@ 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
type Option func(*options)
type options struct {
descOpts []descriptorOption
}
func makeOptions(opts ...Option) *options {
o := &options{
descOpts: []descriptorOption{},
}
for _, apply := range opts {
apply(o)
}
return o
}
type descriptorOption func(*v1.Descriptor)
// 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
return func(o *options) {
o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) {
if desc.Annotations == nil {
desc.Annotations = make(map[string]string)
}
for k, v := range annotations {
desc.Annotations[k] = v
}
})
}
}
// 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
return func(o *options) {
o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) {
if desc.URLs == nil {
desc.URLs = []string{}
}
desc.URLs = append(desc.URLs, urls...)
})
}
}
// 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
return func(o *options) {
o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) {
desc.Platform = &platform
})
}
}

View File

@ -62,10 +62,9 @@ func (l Path) AppendImage(img v1.Image, options ...Option) error {
Digest: d,
}
for _, opt := range options {
if err := opt(&desc); err != nil {
return err
}
o := makeOptions(options...)
for _, opt := range o.descOpts {
opt(&desc)
}
return l.AppendDescriptor(desc)
@ -99,10 +98,9 @@ func (l Path) AppendIndex(ii v1.ImageIndex, options ...Option) error {
Digest: d,
}
for _, opt := range options {
if err := opt(&desc); err != nil {
return err
}
o := makeOptions(options...)
for _, opt := range o.descOpts {
opt(&desc)
}
return l.AppendDescriptor(desc)
@ -163,10 +161,9 @@ func (l Path) replaceDescriptor(append mutate.Appendable, matcher match.Matcher,
return err
}
for _, opt := range options {
if err := opt(desc); err != nil {
return err
}
o := makeOptions(options...)
for _, opt := range o.descOpts {
opt(desc)
}
add := mutate.IndexAddendum{
@ -368,9 +365,9 @@ func (l Path) writeIndexToFile(indexFile string, ii v1.ImageIndex) error {
var blob io.ReadCloser
// Workaround for #819.
if wl, ok := ii.(withLayer); ok {
layer, err := wl.Layer(desc.Digest)
if err != nil {
return err
layer, lerr := wl.Layer(desc.Digest)
if lerr != nil {
return lerr
}
blob, err = layer.Compressed()
} else if wb, ok := ii.(withBlob); ok {

View File

@ -23,8 +23,8 @@ import (
// Manifest represents the OCI image manifest in a structured way.
type Manifest struct {
SchemaVersion int64 `json:"schemaVersion,omitempty"`
MediaType types.MediaType `json:"mediaType"`
SchemaVersion int64 `json:"schemaVersion"`
MediaType types.MediaType `json:"mediaType,omitempty"`
Config Descriptor `json:"config"`
Layers []Descriptor `json:"layers"`
Annotations map[string]string `json:"annotations,omitempty"`

View File

@ -24,9 +24,9 @@ import (
"strings"
"time"
"github.com/google/go-containerregistry/internal/gzip"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/internal/gzip"
"github.com/google/go-containerregistry/pkg/v1/match"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/google/go-containerregistry/pkg/v1/types"

View File

@ -17,8 +17,8 @@ package partial
import (
"io"
"github.com/google/go-containerregistry/internal/gzip"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/internal/gzip"
"github.com/google/go-containerregistry/pkg/v1/types"
)

View File

@ -19,8 +19,8 @@ import (
"io"
"sync"
"github.com/google/go-containerregistry/internal/gzip"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/internal/gzip"
"github.com/google/go-containerregistry/pkg/v1/types"
)

View File

@ -25,10 +25,10 @@ import (
"strconv"
"strings"
"github.com/google/go-containerregistry/internal/verify"
"github.com/google/go-containerregistry/pkg/logs"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/internal/verify"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
"github.com/google/go-containerregistry/pkg/v1/types"
@ -324,14 +324,26 @@ func (f *fetcher) headManifest(ref name.Reference, acceptable []types.MediaType)
return nil, err
}
mediaType := types.MediaType(resp.Header.Get("Content-Type"))
mth := resp.Header.Get("Content-Type")
if mth == "" {
return nil, fmt.Errorf("HEAD %s: response did not include Content-Type header", u.String())
}
mediaType := types.MediaType(mth)
size, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
lh := resp.Header.Get("Content-Length")
if lh == "" {
return nil, fmt.Errorf("HEAD %s: response did not include Content-Length header", u.String())
}
size, err := strconv.ParseInt(lh, 10, 64)
if err != nil {
return nil, err
}
digest, err := v1.NewHash(resp.Header.Get("Docker-Content-Digest"))
dh := resp.Header.Get("Docker-Content-Digest")
if dh == "" {
return nil, fmt.Errorf("HEAD %s: response did not include Docker-Content-Digest header", u.String())
}
digest, err := v1.NewHash(dh)
if err != nil {
return nil, err
}

View File

@ -21,10 +21,10 @@ import (
"net/url"
"sync"
"github.com/google/go-containerregistry/pkg/internal/redact"
"github.com/google/go-containerregistry/internal/redact"
"github.com/google/go-containerregistry/internal/verify"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/internal/verify"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
"github.com/google/go-containerregistry/pkg/v1/types"

View File

@ -17,7 +17,7 @@ package remote
import (
"io"
"github.com/google/go-containerregistry/pkg/internal/redact"
"github.com/google/go-containerregistry/internal/redact"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"

View File

@ -33,7 +33,7 @@ import (
// Current limitations:
// - All refs must share the same repository.
// - Images cannot consist of stream.Layers.
func MultiWrite(m map[name.Reference]Taggable, options ...Option) error {
func MultiWrite(m map[name.Reference]Taggable, options ...Option) (rerr error) {
// Determine the repository being pushed to; if asked to push to
// multiple repositories, give up.
var repo, zero name.Repository
@ -86,14 +86,54 @@ func MultiWrite(m map[name.Reference]Taggable, options ...Option) error {
return err
}
w := writer{
repo: repo,
client: &http.Client{Transport: tr},
context: o.context,
repo: repo,
client: &http.Client{Transport: tr},
context: o.context,
updates: o.updates,
lastUpdate: &v1.Update{},
}
// Collect the total size of blobs and manifests we're about to write.
if o.updates != nil {
defer close(o.updates)
defer func() { sendError(o.updates, rerr) }()
for _, b := range blobs {
size, err := b.Size()
if err != nil {
return err
}
w.lastUpdate.Total += size
}
countManifest := func(t Taggable) error {
b, err := t.RawManifest()
if err != nil {
return err
}
w.lastUpdate.Total += int64(len(b))
return nil
}
for _, i := range images {
if err := countManifest(i); err != nil {
return err
}
}
for _, nm := range newManifests {
for _, i := range nm {
if err := countManifest(i); err != nil {
return err
}
}
}
for _, i := range indexes {
if err := countManifest(i); err != nil {
return err
}
}
}
// Upload individual blobs and collect any errors.
blobChan := make(chan v1.Layer, 2*o.jobs)
var g errgroup.Group
g, ctx := errgroup.WithContext(o.context)
for i := 0; i < o.jobs; i++ {
// Start N workers consuming blobs to upload.
g.Go(func() error {
@ -105,12 +145,17 @@ func MultiWrite(m map[name.Reference]Taggable, options ...Option) error {
return nil
})
}
go func() {
g.Go(func() error {
defer close(blobChan)
for _, b := range blobs {
blobChan <- b
select {
case blobChan <- b:
case <-ctx.Done():
return ctx.Err()
}
}
close(blobChan)
}()
return nil
})
if err := g.Wait(); err != nil {
return err
}
@ -155,8 +200,8 @@ func MultiWrite(m map[name.Reference]Taggable, options ...Option) error {
}
// Push originally requested index manifests, which might depend on
// newly discovered manifests.
return commitMany(indexes)
return commitMany(indexes)
}
// addIndexBlobs adds blobs to the set of blobs we intend to upload, and

View File

@ -37,6 +37,7 @@ type options struct {
jobs int
userAgent string
allowNondistributableArtifacts bool
updates chan<- v1.Update
}
var defaultPlatform = v1.Platform{
@ -66,9 +67,6 @@ func makeOptions(target authn.Resource, opts ...Option) (*options, error) {
if err != nil {
return nil, err
}
if auth == authn.Anonymous {
logs.Warn.Printf("No matching credentials were found for %q, falling back on anonymous", target)
}
o.auth = auth
}
@ -184,3 +182,14 @@ func WithNondistributable(o *options) error {
o.allowNondistributableArtifacts = true
return nil
}
// WithProgress takes a channel that will receive progress updates as bytes are written.
//
// Sending updates to an unbuffered channel will block writes, so callers
// should provide a buffered channel to avoid potential deadlocks.
func WithProgress(updates chan<- v1.Update) Option {
return func(o *options) error {
o.updates = updates
return nil
}
}

View File

@ -25,8 +25,9 @@ import (
"strings"
authchallenge "github.com/docker/distribution/registry/client/auth/challenge"
"github.com/google/go-containerregistry/internal/redact"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/internal/redact"
"github.com/google/go-containerregistry/pkg/logs"
"github.com/google/go-containerregistry/pkg/name"
)
@ -262,6 +263,7 @@ func (bt *bearerTransport) refreshOauth(ctx context.Context) ([]byte, error) {
defer resp.Body.Close()
if err := CheckError(resp, http.StatusOK); err != nil {
logs.Warn.Printf("No matching credentials were found for %q", bt.registry)
return nil, err
}
@ -301,6 +303,7 @@ func (bt *bearerTransport) refreshBasic(ctx context.Context) ([]byte, error) {
defer resp.Body.Close()
if err := CheckError(resp, http.StatusOK); err != nil {
logs.Warn.Printf("No matching credentials were found for %q", bt.registry)
return nil, err
}

View File

@ -100,7 +100,7 @@ func (e *Error) Temporary() bool {
return true
}
// TODO(jonjohnsonjr): Consider moving to pkg/internal/redact.
// TODO(jonjohnsonjr): Consider moving to internal/redact.
func redactURL(original *url.URL) *url.URL {
qs := original.Query()
for k, v := range qs {
@ -163,6 +163,7 @@ var temporaryErrorCodes = map[ErrorCode]struct{}{
}
var temporaryStatusCodes = map[int]struct{}{
http.StatusRequestTimeout: {},
http.StatusInternalServerError: {},
http.StatusBadGateway: {},
http.StatusServiceUnavailable: {},

View File

@ -20,7 +20,7 @@ import (
"net/http/httputil"
"time"
"github.com/google/go-containerregistry/pkg/internal/redact"
"github.com/google/go-containerregistry/internal/redact"
"github.com/google/go-containerregistry/pkg/logs"
)

View File

@ -18,7 +18,7 @@ import (
"net/http"
"time"
"github.com/google/go-containerregistry/pkg/internal/retry"
"github.com/google/go-containerregistry/internal/retry"
)
// Sleep for 0.1, 0.3, 0.9, 2.7 seconds. This should cover networking blips.

View File

@ -23,15 +23,17 @@ import (
"net/http"
"net/url"
"strings"
"sync/atomic"
"time"
"github.com/google/go-containerregistry/pkg/internal/redact"
"github.com/google/go-containerregistry/pkg/internal/retry"
"github.com/google/go-containerregistry/internal/redact"
"github.com/google/go-containerregistry/internal/retry"
"github.com/google/go-containerregistry/pkg/logs"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
"github.com/google/go-containerregistry/pkg/v1/stream"
"github.com/google/go-containerregistry/pkg/v1/types"
"golang.org/x/sync/errgroup"
)
@ -42,61 +44,97 @@ type Taggable interface {
}
// Write pushes the provided img to the specified image reference.
func Write(ref name.Reference, img v1.Image, options ...Option) error {
ls, err := img.Layers()
if err != nil {
return err
}
func Write(ref name.Reference, img v1.Image, options ...Option) (rerr error) {
o, err := makeOptions(ref.Context(), options...)
if err != nil {
return err
}
var lastUpdate *v1.Update
if o.updates != nil {
lastUpdate = &v1.Update{}
lastUpdate.Total, err = countImage(img, o.allowNondistributableArtifacts)
if err != nil {
return err
}
defer close(o.updates)
defer func() { sendError(o.updates, rerr) }()
}
return writeImage(ref, img, o, lastUpdate)
}
func writeImage(ref name.Reference, img v1.Image, o *options, lastUpdate *v1.Update) error {
ls, err := img.Layers()
if err != nil {
return err
}
scopes := scopesForUploadingImage(ref.Context(), ls)
tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, scopes)
if err != nil {
return err
}
w := writer{
repo: ref.Context(),
client: &http.Client{Transport: tr},
context: o.context,
repo: ref.Context(),
client: &http.Client{Transport: tr},
context: o.context,
updates: o.updates,
lastUpdate: lastUpdate,
}
// Upload individual blobs and collect any errors.
blobChan := make(chan v1.Layer, 2*o.jobs)
g, ctx := errgroup.WithContext(o.context)
for i := 0; i < o.jobs; i++ {
// Start N workers consuming blobs to upload.
g.Go(func() error {
for b := range blobChan {
if err := w.uploadOne(b); err != nil {
return err
}
}
return nil
})
}
// Upload individual layers in goroutines and collect any errors.
// If we can dedupe by the layer digest, try to do so. If we can't determine
// the digest for whatever reason, we can't dedupe and might re-upload.
var g errgroup.Group
uploaded := map[v1.Hash]bool{}
for _, l := range ls {
l := l
g.Go(func() error {
defer close(blobChan)
uploaded := map[v1.Hash]bool{}
for _, l := range ls {
l := l
// Handle foreign layers.
mt, err := l.MediaType()
if err != nil {
return err
}
if !mt.IsDistributable() && !o.allowNondistributableArtifacts {
continue
}
// Streaming layers calculate their digests while uploading them. Assume
// an error here indicates we need to upload the layer.
h, err := l.Digest()
if err == nil {
// If we can determine the layer's digest ahead of
// time, use it to dedupe uploads.
if uploaded[h] {
continue // Already uploading.
// Handle foreign layers.
mt, err := l.MediaType()
if err != nil {
return err
}
if !mt.IsDistributable() && !o.allowNondistributableArtifacts {
continue
}
uploaded[h] = true
}
// TODO(#803): Pipe through remote.WithJobs and upload these in parallel.
g.Go(func() error {
return w.uploadOne(l)
})
// Streaming layers calculate their digests while uploading them. Assume
// an error here indicates we need to upload the layer.
h, err := l.Digest()
if err == nil {
// If we can determine the layer's digest ahead of
// time, use it to dedupe uploads.
if uploaded[h] {
continue // Already uploading.
}
uploaded[h] = true
}
select {
case blobChan <- l:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
})
if err := g.Wait(); err != nil {
return err
}
if l, err := partial.ConfigLayer(img); err != nil {
@ -137,6 +175,16 @@ type writer struct {
repo name.Repository
client *http.Client
context context.Context
updates chan<- v1.Update
lastUpdate *v1.Update
}
func sendError(ch chan<- v1.Update, err error) error {
if err != nil && ch != nil {
ch <- v1.Update{Error: err}
}
return err
}
// url returns a url.Url for the specified path in the context of this remote image reference.
@ -259,10 +307,49 @@ func (w *writer) initiateUpload(from, mount string) (location string, mounted bo
}
}
type progressReader struct {
rc io.ReadCloser
count *int64 // number of bytes this reader has read, to support resetting on retry.
updates chan<- v1.Update
lastUpdate *v1.Update
}
func (r *progressReader) Read(b []byte) (int, error) {
n, err := r.rc.Read(b)
if err != nil {
return n, err
}
atomic.AddInt64(r.count, int64(n))
// TODO: warn/debug log if sending takes too long, or if sending is blocked while context is cancelled.
r.updates <- v1.Update{
Total: r.lastUpdate.Total,
Complete: atomic.AddInt64(&r.lastUpdate.Complete, int64(n)),
}
return n, nil
}
func (r *progressReader) Close() error { return r.rc.Close() }
// streamBlob streams the contents of the blob to the specified location.
// On failure, this will return an error. On success, this will return the location
// header indicating how to commit the streamed blob.
func (w *writer) streamBlob(ctx context.Context, blob io.ReadCloser, streamLocation string) (commitLocation string, err error) {
func (w *writer) streamBlob(ctx context.Context, blob io.ReadCloser, streamLocation string) (commitLocation string, rerr error) {
reset := func() {}
defer func() {
if rerr != nil {
reset()
}
}()
if w.updates != nil {
var count int64
blob = &progressReader{rc: blob, updates: w.updates, lastUpdate: w.lastUpdate, count: &count}
reset = func() {
atomic.AddInt64(&w.lastUpdate.Complete, -count)
w.updates <- *w.lastUpdate
}
}
req, err := http.NewRequest(http.MethodPatch, streamLocation, blob)
if err != nil {
return "", err
@ -308,6 +395,17 @@ func (w *writer) commitBlob(location, digest string) error {
return transport.CheckError(resp, http.StatusCreated)
}
// incrProgress increments and sends a progress update, if WithProgress is used.
func (w *writer) incrProgress(written int64) {
if w.updates == nil {
return
}
w.updates <- v1.Update{
Total: w.lastUpdate.Total,
Complete: atomic.AddInt64(&w.lastUpdate.Complete, int64(written)),
}
}
// uploadOne performs a complete upload of a single layer.
func (w *writer) uploadOne(l v1.Layer) error {
var from, mount string
@ -319,6 +417,11 @@ func (w *writer) uploadOne(l v1.Layer) error {
return err
}
if existing {
size, err := l.Size()
if err != nil {
return err
}
w.incrProgress(size)
logs.Progress.Printf("existing blob: %v", h)
return nil
}
@ -338,6 +441,11 @@ func (w *writer) uploadOne(l v1.Layer) error {
if err != nil {
return err
} else if mounted {
size, err := l.Size()
if err != nil {
return err
}
w.incrProgress(size)
h, err := l.Digest()
if err != nil {
return err
@ -400,6 +508,11 @@ func (w *writer) writeIndex(ref name.Reference, ii v1.ImageIndex, options ...Opt
return err
}
o, err := makeOptions(ref.Context(), options...)
if err != nil {
return err
}
// TODO(#803): Pipe through remote.WithJobs and upload these in parallel.
for _, desc := range index.Manifests {
ref := ref.Context().Digest(desc.Digest.String())
@ -418,7 +531,6 @@ func (w *writer) writeIndex(ref name.Reference, ii v1.ImageIndex, options ...Opt
if err != nil {
return err
}
if err := w.writeIndex(ref, ii); err != nil {
return err
}
@ -427,10 +539,7 @@ func (w *writer) writeIndex(ref name.Reference, ii v1.ImageIndex, options ...Opt
if err != nil {
return err
}
// TODO: Ideally we could reuse this writer, but we need to know
// scopes before we do the token exchange. To be lazy here, just
// re-do the token exchange. MultiWrite fixes this.
if err := Write(ref, img, options...); err != nil {
if err := writeImage(ref, img, o, w.lastUpdate); err != nil {
return err
}
default:
@ -522,6 +631,7 @@ func (w *writer) commitManifest(t Taggable, ref name.Reference) error {
// The image was successfully pushed!
logs.Progress.Printf("%v: digest: %v size: %d", ref, desc.Digest, desc.Size)
w.incrProgress(int64(len(raw)))
return nil
}
@ -553,11 +663,12 @@ func scopesForUploadingImage(repo name.Repository, layers []v1.Layer) []string {
// WriteIndex pushes the provided ImageIndex to the specified image reference.
// WriteIndex will attempt to push all of the referenced manifests before
// attempting to push the ImageIndex, to retain referential integrity.
func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) error {
func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) (rerr error) {
o, err := makeOptions(ref.Context(), options...)
if err != nil {
return err
}
scopes := []string{ref.Scope(transport.PushScope)}
tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, scopes)
if err != nil {
@ -567,12 +678,132 @@ func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) error {
repo: ref.Context(),
client: &http.Client{Transport: tr},
context: o.context,
updates: o.updates,
}
if o.updates != nil {
w.lastUpdate = &v1.Update{}
w.lastUpdate.Total, err = countIndex(ii, o.allowNondistributableArtifacts)
if err != nil {
return err
}
defer close(o.updates)
defer func() { sendError(o.updates, rerr) }()
}
return w.writeIndex(ref, ii, options...)
}
// countImage counts the total size of all layers + config blob + manifest for
// an image. It de-dupes duplicate layers.
func countImage(img v1.Image, allowNondistributableArtifacts bool) (int64, error) {
var total int64
ls, err := img.Layers()
if err != nil {
return 0, err
}
seen := map[v1.Hash]bool{}
for _, l := range ls {
// Handle foreign layers.
mt, err := l.MediaType()
if err != nil {
return 0, err
}
if !mt.IsDistributable() && !allowNondistributableArtifacts {
continue
}
// TODO: support streaming layers which update the total count as they write.
if _, ok := l.(*stream.Layer); ok {
return 0, errors.New("cannot use stream.Layer and WithProgress")
}
// Dedupe layers.
d, err := l.Digest()
if err != nil {
return 0, err
}
if seen[d] {
continue
}
seen[d] = true
size, err := l.Size()
if err != nil {
return 0, err
}
total += size
}
b, err := img.RawConfigFile()
if err != nil {
return 0, err
}
total += int64(len(b))
size, err := img.Size()
if err != nil {
return 0, err
}
total += size
return total, nil
}
// countIndex counts the total size of all images + sub-indexes for an index.
// It does not attempt to de-dupe duplicate images, etc.
func countIndex(idx v1.ImageIndex, allowNondistributableArtifacts bool) (int64, error) {
var total int64
mf, err := idx.IndexManifest()
if err != nil {
return 0, err
}
for _, desc := range mf.Manifests {
switch desc.MediaType {
case types.OCIImageIndex, types.DockerManifestList:
sidx, err := idx.ImageIndex(desc.Digest)
if err != nil {
return 0, err
}
size, err := countIndex(sidx, allowNondistributableArtifacts)
if err != nil {
return 0, err
}
total += size
case types.OCIManifestSchema1, types.DockerManifestSchema2:
simg, err := idx.Image(desc.Digest)
if err != nil {
return 0, err
}
size, err := countImage(simg, allowNondistributableArtifacts)
if err != nil {
return 0, err
}
total += size
default:
// Workaround for #819.
if wl, ok := idx.(withLayer); ok {
layer, err := wl.Layer(desc.Digest)
if err != nil {
return 0, err
}
size, err := layer.Size()
if err != nil {
return 0, err
}
total += size
}
}
}
size, err := idx.Size()
if err != nil {
return 0, err
}
total += size
return total, nil
}
// WriteLayer uploads the provided Layer to the specified repo.
func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) error {
func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) (rerr error) {
o, err := makeOptions(repo, options...)
if err != nil {
return err
@ -586,8 +817,23 @@ func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) error {
repo: repo,
client: &http.Client{Transport: tr},
context: o.context,
updates: o.updates,
}
if o.updates != nil {
defer close(o.updates)
defer func() { sendError(o.updates, rerr) }()
// TODO: support streaming layers which update the total count as they write.
if _, ok := layer.(*stream.Layer); ok {
return errors.New("cannot use stream.Layer and WithProgress")
}
size, err := layer.Size()
if err != nil {
return err
}
w.lastUpdate = &v1.Update{Total: size}
}
return w.uploadOne(layer)
}
@ -603,11 +849,26 @@ func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) error {
// should ensure that all blobs or manifests that are referenced by t exist
// in the target registry.
func Tag(tag name.Tag, t Taggable, options ...Option) error {
o, err := makeOptions(tag.Context(), options...)
return Put(tag, t, options...)
}
// Put adds a manifest from the given Taggable via PUT /v1/.../manifest/<ref>
//
// Notable implementations of Taggable are v1.Image, v1.ImageIndex, and
// remote.Descriptor.
//
// If t implements MediaType, we will use that for the Content-Type, otherwise
// we will default to types.DockerManifestSchema2.
//
// Put does not attempt to write anything other than the manifest, so callers
// should ensure that all blobs or manifests that are referenced by t exist
// in the target registry.
func Put(ref name.Reference, t Taggable, options ...Option) error {
o, err := makeOptions(ref.Context(), options...)
if err != nil {
return err
}
scopes := []string{tag.Scope(transport.PushScope)}
scopes := []string{ref.Scope(transport.PushScope)}
// TODO: This *always* does a token exchange. For some registries,
// that's pretty slow. Some ideas;
@ -615,15 +876,15 @@ func Tag(tag name.Tag, t Taggable, options ...Option) error {
// * Allow callers to pass in a transport.Transport, typecheck
// it to allow them to reuse the transport across multiple calls.
// * WithTag option to do multiple manifest PUTs in commitManifest.
tr, err := transport.NewWithContext(o.context, tag.Context().Registry, o.auth, o.transport, scopes)
tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, scopes)
if err != nil {
return err
}
w := writer{
repo: tag.Context(),
repo: ref.Context(),
client: &http.Client{Transport: tr},
context: o.context,
}
return w.commitManifest(t, tag)
return w.commitManifest(t, ref)
}

View File

@ -23,11 +23,13 @@ import (
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"sync"
"github.com/google/go-containerregistry/internal/gzip"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/internal/gzip"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/types"
)
@ -216,6 +218,10 @@ func extractFileFromTar(opener Opener, filePath string) (io.ReadCloser, error) {
return nil, err
}
if hdr.Name == filePath {
if hdr.Typeflag == tar.TypeSymlink || hdr.Typeflag == tar.TypeLink {
currentDir := filepath.Dir(filePath)
return extractFileFromTar(opener, path.Join(currentDir, hdr.Linkname))
}
close = false
return tarFile{
Reader: tf,

View File

@ -23,10 +23,10 @@ import (
"sync"
"github.com/containerd/stargz-snapshotter/estargz"
"github.com/google/go-containerregistry/internal/and"
gestargz "github.com/google/go-containerregistry/internal/estargz"
ggzip "github.com/google/go-containerregistry/internal/gzip"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/internal/and"
gestargz "github.com/google/go-containerregistry/pkg/v1/internal/estargz"
ggzip "github.com/google/go-containerregistry/pkg/v1/internal/gzip"
"github.com/google/go-containerregistry/pkg/v1/types"
)

20
vendor/modules.txt vendored
View File

@ -105,16 +105,20 @@ 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.4.1-0.20210216200643-d81088d9983e
# github.com/google/go-containerregistry v0.5.0
## explicit
github.com/google/go-containerregistry/cmd/crane/cmd
github.com/google/go-containerregistry/internal/and
github.com/google/go-containerregistry/internal/estargz
github.com/google/go-containerregistry/internal/gzip
github.com/google/go-containerregistry/internal/httptest
github.com/google/go-containerregistry/internal/legacy
github.com/google/go-containerregistry/internal/redact
github.com/google/go-containerregistry/internal/retry
github.com/google/go-containerregistry/internal/retry/wait
github.com/google/go-containerregistry/internal/verify
github.com/google/go-containerregistry/pkg/authn
github.com/google/go-containerregistry/pkg/crane
github.com/google/go-containerregistry/pkg/internal/httptest
github.com/google/go-containerregistry/pkg/internal/legacy
github.com/google/go-containerregistry/pkg/internal/redact
github.com/google/go-containerregistry/pkg/internal/retry
github.com/google/go-containerregistry/pkg/internal/retry/wait
github.com/google/go-containerregistry/pkg/legacy
github.com/google/go-containerregistry/pkg/legacy/tarball
github.com/google/go-containerregistry/pkg/logs
@ -124,10 +128,6 @@ github.com/google/go-containerregistry/pkg/v1
github.com/google/go-containerregistry/pkg/v1/cache
github.com/google/go-containerregistry/pkg/v1/daemon
github.com/google/go-containerregistry/pkg/v1/empty
github.com/google/go-containerregistry/pkg/v1/internal/and
github.com/google/go-containerregistry/pkg/v1/internal/estargz
github.com/google/go-containerregistry/pkg/v1/internal/gzip
github.com/google/go-containerregistry/pkg/v1/internal/verify
github.com/google/go-containerregistry/pkg/v1/layout
github.com/google/go-containerregistry/pkg/v1/match
github.com/google/go-containerregistry/pkg/v1/mutate