diff --git a/cmd/crane/cmd/pull.go b/cmd/crane/cmd/pull.go index 41c6e95cd..ccfc216a9 100644 --- a/cmd/crane/cmd/pull.go +++ b/cmd/crane/cmd/pull.go @@ -101,7 +101,7 @@ func NewCmdPull(options *[]crane.Option) *cobra.Command { return err } opts = append(opts, layout.WithAnnotations(map[string]string{ - "org.opencontainers.image.ref.name": parsed.Name(), + ociAnnotationImageRefName: parsed.Name(), })) } if err = p.AppendImage(img, opts...); err != nil { @@ -117,7 +117,7 @@ func NewCmdPull(options *[]crane.Option) *cobra.Command { return err } opts = append(opts, layout.WithAnnotations(map[string]string{ - "org.opencontainers.image.ref.name": parsed.Name(), + ociAnnotationImageRefName: parsed.Name(), })) } if err := p.AppendIndex(idx, opts...); err != nil { diff --git a/cmd/crane/cmd/push.go b/cmd/crane/cmd/push.go index 05eedf16f..8ea4842ff 100644 --- a/cmd/crane/cmd/push.go +++ b/cmd/crane/cmd/push.go @@ -15,8 +15,11 @@ package cmd import ( + "context" "fmt" "os" + "strings" + "sync" "github.com/google/go-containerregistry/pkg/crane" "github.com/google/go-containerregistry/pkg/name" @@ -24,66 +27,126 @@ import ( "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/google/go-containerregistry/pkg/v1/partial" "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/spf13/cobra" ) +type imageWithRef struct { + image partial.WithRawManifest + ref name.Reference +} + // NewCmdPush creates a new cobra.Command for the push subcommand. func NewCmdPush(options *[]crane.Option) *cobra.Command { - index := false - imageRefs := "" + var ( + imageRefs string + index, annotateRef bool + ) cmd := &cobra.Command{ Use: "push PATH IMAGE", Short: "Push local image contents to a remote registry", Long: `If the PATH is a directory, it will be read as an OCI image layout. Otherwise, PATH is assumed to be a docker-style tarball.`, - Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - path, tag := args[0], args[1] + if !annotateRef { + if err := cobra.ExactArgs(2)(cmd, args); err != nil { + return err + } + path, tag := args[0], args[1] + img, err := loadImage(path, index) + if err != nil { + return err + } - img, err := loadImage(path, index) - if err != nil { - return err - } + o := crane.GetOptions(*options...) + ref, err := name.ParseReference(tag, o.Name...) + if err != nil { + return err + } - o := crane.GetOptions(*options...) - ref, err := name.ParseReference(tag, o.Name...) - if err != nil { - return err - } - var h v1.Hash - switch t := img.(type) { - case v1.Image: - if err := remote.Write(ref, t, o.Remote...); err != nil { + digest, err := writeImage(ref, img, o.Remote...) + if err != nil { return err } - if h, err = t.Digest(); err != nil { + if imageRefs != "" { + if err := os.WriteFile(imageRefs, []byte(digest.String()), 0600); err != nil { + return fmt.Errorf("failed to write image refs to %s: %w", imageRefs, err) + } + } + + // Print the digest of the pushed image to stdout to facilitate command composition. + fmt.Fprintln(cmd.OutOrStdout(), digest) + } else { + if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil { return err } - case v1.ImageIndex: - if err := remote.WriteIndex(ref, t, o.Remote...); err != nil { + path := args[0] + var registry *name.Registry + if len(args) == 2 { + r, err := name.NewRegistry(args[1]) + if err != nil { + return err + } + registry = &r + } + imgRefs, err := loadImageWithRef(path, index) + if err != nil { return err } - if h, err = t.Digest(); err != nil { + pusher, err := remote.NewPusher() + if err != nil { return err } - default: - return fmt.Errorf("cannot push type (%T) to registry", img) - } + ctx, cancel := context.WithCancel(cmd.Context()) + defer cancel() + o := crane.GetOptions(*options...) + o.Remote = append(o.Remote, remote.WithContext(ctx), remote.Reuse[*remote.Pusher](pusher)) + wg := sync.WaitGroup{} + var digests []string + for i := range imgRefs { + wg.Add(1) + go func(imgRef imageWithRef) (err error) { + defer func() { + if err != nil { + fmt.Println(err) + cancel() + } + wg.Done() + }() + if registry != nil { + switch t := imgRef.ref.(type) { + case name.Tag: + t.Registry = *registry + imgRef.ref = t + case name.Digest: + t.Registry = *registry + imgRef.ref = t + } + } + digest, err := writeImage(imgRef.ref, imgRef.image, o.Remote...) + if err != nil { + return err + } + + if imageRefs != "" { + digests = append(digests, digest.String()) + } - digest := ref.Context().Digest(h.String()) - if imageRefs != "" { - if err := os.WriteFile(imageRefs, []byte(digest.String()), 0600); err != nil { - return fmt.Errorf("failed to write image refs to %s: %w", imageRefs, err) + // Print the digest of the pushed image to stdout to facilitate command composition. + fmt.Fprintln(cmd.OutOrStdout(), digest) + return nil + }(imgRefs[i]) + } + wg.Wait() + if imageRefs != "" { + return os.WriteFile(imageRefs, []byte(strings.Join(digests, "\n")), 0600) } } - - // Print the digest of the pushed image to stdout to facilitate command composition. - fmt.Fprintln(cmd.OutOrStdout(), digest) - return nil }, } cmd.Flags().BoolVar(&index, "index", false, "push a collection of images as a single index, currently required if PATH contains multiple images") cmd.Flags().StringVar(&imageRefs, "image-refs", "", "path to file where a list of the published image references will be written") + cmd.Flags().BoolVar(&annotateRef, "annotate-ref", false, "use image reference to push bundle") return cmd } @@ -127,3 +190,99 @@ func loadImage(path string, index bool) (partial.WithRawManifest, error) { return nil, fmt.Errorf("layout contains non-image (mediaType: %q), consider --index", desc.MediaType) } + +func loadImageWithRef(path string, index bool) ([]imageWithRef, error) { + stat, err := os.Stat(path) + if err != nil { + return nil, err + } + + if !stat.IsDir() { + imgs, err := tarball.ImageAllFromPath(path) + if err != nil { + return nil, fmt.Errorf("loading %s as tarball: %w", path, err) + } + var imgRefs []imageWithRef + for _, img := range imgs { + imgTagged, ok := img.(partial.WithRepoTags) + if !ok || len(imgTagged.RepoTags()) == 0 { + return nil, fmt.Errorf("image %s has no tag", path) + } + for _, repoTag := range imgTagged.RepoTags() { + ref, err := name.ParseReference(repoTag, name.StrictValidation) + if err != nil { + return nil, fmt.Errorf("parsing %s repoTag: %w", path, err) + } + imgRefs = append(imgRefs, imageWithRef{img, ref}) + } + } + return imgRefs, nil + } + + l, err := layout.ImageIndexFromPath(path) + if err != nil { + return nil, fmt.Errorf("loading %s as OCI layout: %w", path, err) + } + + m, err := l.IndexManifest() + if err != nil { + return nil, err + } + + if index { + refName := m.Annotations[ociAnnotationImageRefName] + ref, err := name.ParseReference(refName, name.StrictValidation) + if err != nil { + return nil, fmt.Errorf("parsing %s repoTag: %w", path, err) + } + return []imageWithRef{{l, ref}}, err + } + + imgRefs := make([]imageWithRef, len(m.Manifests)) + for i := range m.Manifests { + refName := m.Manifests[i].Annotations[ociAnnotationImageRefName] + ref, err := name.ParseReference(refName, name.StrictValidation) + if err != nil { + return nil, fmt.Errorf("parsing %s repoTag: %w", path, err) + } + if m.Manifests[i].MediaType.IsImage() { + img, err := l.Image(m.Manifests[i].Digest) + if err != nil { + return nil, err + } + imgRefs[i] = imageWithRef{img, ref} + } else if m.Manifests[i].MediaType.IsIndex() { + img, err := l.ImageIndex(m.Manifests[i].Digest) + if err != nil { + return nil, err + } + imgRefs[i] = imageWithRef{img, ref} + } else { + return nil, fmt.Errorf("layout contains unexpected mediaType: %q", m.Manifests[i].MediaType) + } + } + return imgRefs, nil +} + +func writeImage(ref name.Reference, img partial.WithRawManifest, options ...remote.Option) (digest name.Digest, err error) { + var h v1.Hash + switch t := img.(type) { + case v1.Image: + if err = remote.Write(ref, t, options...); err != nil { + return + } + if h, err = t.Digest(); err != nil { + return + } + case v1.ImageIndex: + if err = remote.WriteIndex(ref, t, options...); err != nil { + return + } + if h, err = t.Digest(); err != nil { + return + } + default: + return digest, fmt.Errorf("cannot push type (%T) to registry", img) + } + return ref.Context().Digest(h.String()), nil +} diff --git a/cmd/crane/cmd/util.go b/cmd/crane/cmd/util.go index f4ff6514a..b55e106c8 100644 --- a/cmd/crane/cmd/util.go +++ b/cmd/crane/cmd/util.go @@ -17,9 +17,12 @@ package cmd import ( "strings" + "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" ) +const ociAnnotationImageRefName = "org.opencontainers.image.ref.name" + type platformsValue struct { platforms []v1.Platform } @@ -84,3 +87,11 @@ func parsePlatform(platform string) (*v1.Platform, error) { return v1.ParsePlatform(platform) } + +func referenceMapToStringMap(m map[name.Reference]v1.Image) map[string]v1.Image { + out := make(map[string]v1.Image, len(m)) + for k, v := range m { + out[k.String()] = v + } + return out +} diff --git a/cmd/crane/doc/crane_push.md b/cmd/crane/doc/crane_push.md index 64bacf60e..2d7a0855b 100644 --- a/cmd/crane/doc/crane_push.md +++ b/cmd/crane/doc/crane_push.md @@ -13,6 +13,7 @@ crane push PATH IMAGE [flags] ### Options ``` + --annotate-ref use image reference to push bundle -h, --help help for push --image-refs string path to file where a list of the published image references will be written --index push a collection of images as a single index, currently required if PATH contains multiple images diff --git a/pkg/crane/pull.go b/pkg/crane/pull.go index 7e6e5b7b6..1d7278506 100644 --- a/pkg/crane/pull.go +++ b/pkg/crane/pull.go @@ -51,28 +51,9 @@ func Save(img v1.Image, src, path string) error { // MultiSave writes collection of v1.Image img with tag as a tarball. func MultiSave(imgMap map[string]v1.Image, path string, opt ...Option) error { - o := makeOptions(opt...) - tagToImage := map[name.Tag]v1.Image{} - - for src, img := range imgMap { - ref, err := name.ParseReference(src, o.Name...) - if err != nil { - return fmt.Errorf("parsing ref %q: %w", src, err) - } - - // 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 + tagToImage, err := handleImgMap(imgMap, opt...) + if err != nil { + return err } // no progress channel (for now) return tarball.MultiWriteToFile(path, tagToImage) @@ -96,15 +77,14 @@ func SaveLegacy(img v1.Image, src, path string) error { } // 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: %w", src, err) - } - refToImage[ref] = img +func MultiSaveLegacy(imgMap map[string]v1.Image, path string, opt ...Option) error { + tagToImage, err := handleImgMap(imgMap, opt...) + if err != nil { + return err + } + refToImage := make(map[name.Reference]v1.Image, len(tagToImage)) + for tag, img := range tagToImage { + refToImage[tag] = img } w, err := os.Create(path) @@ -116,6 +96,33 @@ func MultiSaveLegacy(imgMap map[string]v1.Image, path string) error { return legacy.MultiWrite(refToImage, w) } +func handleImgMap(imgMap map[string]v1.Image, opt ...Option) (map[name.Tag]v1.Image, error) { + o := makeOptions(opt...) + tagToImage := map[name.Tag]v1.Image{} + + for src, img := range imgMap { + ref, err := name.ParseReference(src, o.Name...) + if err != nil { + return nil, fmt.Errorf("parsing ref %q: %w", src, err) + } + + // 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 nil, fmt.Errorf("ref wasn't a tag or digest") + } + tag = d.Repository.Tag(iWasADigestTag) + } + tagToImage[tag] = img + } + return tagToImage, nil +} + // 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 { diff --git a/pkg/v1/partial/compressed.go b/pkg/v1/partial/compressed.go index 44989ac96..aefbd742b 100644 --- a/pkg/v1/partial/compressed.go +++ b/pkg/v1/partial/compressed.go @@ -186,3 +186,11 @@ func CompressedToImage(cic CompressedImageCore) (v1.Image, error) { CompressedImageCore: cic, }, nil } + +// RepoTags try get repo tags +func (i *compressedImageExtender) RepoTags() []string { + if it, ok := i.CompressedImageCore.(WithRepoTags); ok { + return it.RepoTags() + } + return nil +} diff --git a/pkg/v1/partial/uncompressed.go b/pkg/v1/partial/uncompressed.go index df20d3aa9..1a7db8e6e 100644 --- a/pkg/v1/partial/uncompressed.go +++ b/pkg/v1/partial/uncompressed.go @@ -221,3 +221,11 @@ func (i *uncompressedImageExtender) LayerByDigest(h v1.Hash) (v1.Layer, error) { } return i.LayerByDiffID(diffID) } + +// RepoTags try get repo tags +func (i *uncompressedImageExtender) RepoTags() []string { + if it, ok := i.UncompressedImageCore.(WithRepoTags); ok { + return it.RepoTags() + } + return nil +} diff --git a/pkg/v1/partial/with.go b/pkg/v1/partial/with.go index c8b22b3f9..9d8145818 100644 --- a/pkg/v1/partial/with.go +++ b/pkg/v1/partial/with.go @@ -147,6 +147,10 @@ type WithRawManifest interface { RawManifest() ([]byte, error) } +type WithRepoTags interface { + RepoTags() []string +} + // Digest is a helper for implementing v1.Image func Digest(i WithRawManifest) (v1.Hash, error) { mb, err := i.RawManifest() diff --git a/pkg/v1/tarball/image.go b/pkg/v1/tarball/image.go index aba609dea..5ec45ad93 100644 --- a/pkg/v1/tarball/image.go +++ b/pkg/v1/tarball/image.go @@ -70,6 +70,11 @@ func ImageFromPath(path string, tag *name.Tag) (v1.Image, error) { return Image(pathOpener(path), tag) } +// ImageAllFromPath returns a v1.Image from a tarball located on path. +func ImageAllFromPath(path string) ([]v1.Image, error) { + return ImageAll(pathOpener(path)) +} + // LoadManifest load manifest func LoadManifest(opener Opener) (Manifest, error) { m, err := extractFileFromTar(opener, "manifest.json") @@ -86,6 +91,33 @@ func LoadManifest(opener Opener) (Manifest, error) { return manifest, nil } +// ImageAll exposes all images from the tarball at the provided path. +func ImageAll(opener Opener) ([]v1.Image, error) { + parentImage := &image{ + opener: opener, + } + if err := parentImage.loadManifest(); err != nil { + return nil, err + } + var images []v1.Image + for _, desc := range *parentImage.manifest { + for _, tagStr := range desc.RepoTags { + repoTag, err := name.NewTag(tagStr) + if err != nil { + return nil, err + } + + // may extract manifest and cfg many times, but optimizing it will make breaking changes + img, err := Image(opener, &repoTag) + if err != nil { + return nil, err + } + images = append(images, img) + } + } + return images, nil +} + // Image exposes an image from the tarball at the provided path. func Image(opener Opener, tag *name.Tag) (v1.Image, error) { img := &image{ @@ -120,6 +152,10 @@ func (i *image) MediaType() (types.MediaType, error) { return types.DockerManifestSchema2, nil } +func (i *image) RepoTags() []string { + return i.imgDescriptor.RepoTags +} + // Descriptor stores the manifest data for a single image inside a `docker save` tarball. type Descriptor struct { Config string @@ -175,7 +211,7 @@ func (i *image) areLayersCompressed() (bool, error) { return cp != compression.None, nil } -func (i *image) loadTarDescriptorAndConfig() error { +func (i *image) loadManifest() error { m, err := extractFileFromTar(i.opener, "manifest.json") if err != nil { return err @@ -189,6 +225,14 @@ func (i *image) loadTarDescriptorAndConfig() error { if i.manifest == nil { return errors.New("no valid manifest.json in tarball") } + return nil +} + +func (i *image) loadTarDescriptorAndConfig() error { + err := i.loadManifest() + if err != nil { + return err + } i.imgDescriptor, err = i.manifest.findDescriptor(i.tag) if err != nil {