From d94015466f3dc47cff2027638cc6bd9135961cd5 Mon Sep 17 00:00:00 2001 From: James Hewitt Date: Fri, 26 Nov 2021 11:51:07 +0000 Subject: [PATCH] Add an option to allow copying image indexes alone The new --multi-arch option allows the user to select between copying the image associated with the system platform, all images in the index, or just the index itself without attempting to copy the images. Signed-off-by: James Hewitt --- cmd/skopeo/copy.go | 33 +++++++++++++++++++++++++++++++++ completions/bash/skopeo | 1 + docs/skopeo-copy.1.md | 11 +++++++++++ integration/copy_test.go | 39 +++++++++++++++++++++++++++------------ 4 files changed, 72 insertions(+), 12 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 3ffce4dba6..b61d2a9e49 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -32,6 +32,7 @@ type copyOptions struct { format commonFlag.OptionalString // Force conversion of the image to a specified format quiet bool // Suppress output information when copying images all bool // Copy all of the images if the source is a list + multiArch commonFlag.OptionalString // How to handle multi architecture images encryptLayer []int // The list of layers to encrypt encryptionKeys []string // Keys needed to encrypt the image decryptionKeys []string // Keys needed to decrypt the image @@ -72,6 +73,7 @@ See skopeo(1) section "IMAGE NAMES" for the expected format flags.StringSliceVar(&opts.additionalTags, "additional-tag", []string{}, "additional tags (supports docker-archive)") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress output information when copying images") flags.BoolVarP(&opts.all, "all", "a", false, "Copy all images if SOURCE-IMAGE is a list") + flags.Var(commonFlag.NewOptionalStringValue(&opts.multiArch), "multi-arch", `How to handle multi-architecture images (system, all, or index-only)`) flags.BoolVar(&opts.removeSignatures, "remove-signatures", false, "Do not copy signatures from SOURCE-IMAGE") flags.StringVar(&opts.signByFingerprint, "sign-by", "", "Sign the image using a GPG key with the specified `FINGERPRINT`") flags.StringVar(&opts.digestFile, "digestfile", "", "Write the digest of the pushed image to the specified file") @@ -82,6 +84,27 @@ See skopeo(1) section "IMAGE NAMES" for the expected format return cmd } +// parseMultiArch parses the list processing selection +// It returns the copy.ImageListSelection to use with image.Copy option +func parseMultiArch(multiArch string) (copy.ImageListSelection, error) { + switch multiArch { + case "system": + return copy.CopySystemImage, nil + case "all": + return copy.CopyAllImages, nil + // There is no CopyNoImages value in copy.ImageListSelection, but because we + // don't provide an option to select a set of images to copy, we can use + // CopySpecificImages. + case "index-only": + return copy.CopySpecificImages, nil + // We don't expose CopySpecificImages other than index-only above, because + // we currently don't provide an option to choose the images to copy. That + // could be added in the future. + default: + return copy.CopySystemImage, fmt.Errorf("unknown multi-arch option %q. Choose one of the supported options: 'system', 'all', or 'index-only'", multiArch) + } +} + func (opts *copyOptions) run(args []string, stdout io.Writer) error { if len(args) != 2 { return errorShouldDisplayUsage{errors.New("Exactly two arguments expected")} @@ -143,7 +166,17 @@ func (opts *copyOptions) run(args []string, stdout io.Writer) error { if opts.quiet { stdout = nil } + imageListSelection := copy.CopySystemImage + if opts.multiArch.Present() && opts.all { + return fmt.Errorf("Cannot use --all and --multi-arch flags together") + } + if opts.multiArch.Present() { + imageListSelection, err = parseMultiArch(opts.multiArch.Value()) + if err != nil { + return err + } + } if opts.all { imageListSelection = copy.CopyAllImages } diff --git a/completions/bash/skopeo b/completions/bash/skopeo index ad3d8d29ca..2614054774 100644 --- a/completions/bash/skopeo +++ b/completions/bash/skopeo @@ -40,6 +40,7 @@ _skopeo_copy() { --src-authfile --dest-authfile --format -f + --multi-arch --sign-by --src-creds --screds --src-cert-dir diff --git a/docs/skopeo-copy.1.md b/docs/skopeo-copy.1.md index e9c2159228..d24ca7cfc4 100644 --- a/docs/skopeo-copy.1.md +++ b/docs/skopeo-copy.1.md @@ -66,6 +66,17 @@ MANIFEST TYPE (oci, v2s1, or v2s2) to use in the destination (default is manifes Print usage statement +**--multi-arch** + +Control what is copied if _source-image_ refers to a multi-architecture image. Default is system. + +Options: +- system: Copy only the image that matches the system architecture +- all: Copy the full multi-architecture image +- index-only: Copy only the index + +The index-only option usually fails unless the referenced per-architecture images are already present in the destination, or the target registry supports sparse indexes. + **--quiet**, **-q** Suppress output information when copying images. diff --git a/integration/copy_test.go b/integration/copy_test.go index 2c21ae56a2..a8f0171fc7 100644 --- a/integration/copy_test.go +++ b/integration/copy_test.go @@ -123,10 +123,10 @@ func (s *CopySuite) TestCopyAllWithManifestListRoundTrip(c *check.C) { dir2, err := ioutil.TempDir("", "copy-all-manifest-list-dir") c.Assert(err, check.IsNil) defer os.RemoveAll(dir2) - assertSkopeoSucceeds(c, "", "copy", "--all", knownListImage, "oci:"+oci1) - assertSkopeoSucceeds(c, "", "copy", "--all", "oci:"+oci1, "dir:"+dir1) - assertSkopeoSucceeds(c, "", "copy", "--all", "dir:"+dir1, "oci:"+oci2) - assertSkopeoSucceeds(c, "", "copy", "--all", "oci:"+oci2, "dir:"+dir2) + assertSkopeoSucceeds(c, "", "copy", "--multi-arch=all", knownListImage, "oci:"+oci1) + assertSkopeoSucceeds(c, "", "copy", "--multi-arch=all", "oci:"+oci1, "dir:"+dir1) + assertSkopeoSucceeds(c, "", "copy", "--multi-arch=all", "dir:"+dir1, "oci:"+oci2) + assertSkopeoSucceeds(c, "", "copy", "--multi-arch=all", "oci:"+oci2, "dir:"+dir2) assertDirImagesAreEqual(c, dir1, dir2) out := combinedOutputOfCommand(c, "diff", "-urN", oci1, oci2) c.Assert(out, check.Equals, "") @@ -145,15 +145,30 @@ func (s *CopySuite) TestCopyAllWithManifestListConverge(c *check.C) { dir2, err := ioutil.TempDir("", "copy-all-manifest-list-dir") c.Assert(err, check.IsNil) defer os.RemoveAll(dir2) - assertSkopeoSucceeds(c, "", "copy", "--all", knownListImage, "oci:"+oci1) - assertSkopeoSucceeds(c, "", "copy", "--all", "oci:"+oci1, "dir:"+dir1) - assertSkopeoSucceeds(c, "", "copy", "--all", "--format", "oci", knownListImage, "dir:"+dir2) - assertSkopeoSucceeds(c, "", "copy", "--all", "dir:"+dir2, "oci:"+oci2) + assertSkopeoSucceeds(c, "", "copy", "--multi-arch=all", knownListImage, "oci:"+oci1) + assertSkopeoSucceeds(c, "", "copy", "--multi-arch=all", "oci:"+oci1, "dir:"+dir1) + assertSkopeoSucceeds(c, "", "copy", "--multi-arch=all", "--format", "oci", knownListImage, "dir:"+dir2) + assertSkopeoSucceeds(c, "", "copy", "--multi-arch=all", "dir:"+dir2, "oci:"+oci2) assertDirImagesAreEqual(c, dir1, dir2) out := combinedOutputOfCommand(c, "diff", "-urN", oci1, oci2) c.Assert(out, check.Equals, "") } +func (s *CopySuite) TestCopyNoneWithManifestList(c *check.C) { + dir1, err := ioutil.TempDir("", "copy-all-manifest-list-dir") + c.Assert(err, check.IsNil) + defer os.RemoveAll(dir1) + assertSkopeoSucceeds(c, "", "copy", "--multi-arch=index-only", knownListImage, "dir:"+dir1) + + manifestPath := filepath.Join(dir1, "manifest.json") + readManifest, err := ioutil.ReadFile(manifestPath) + c.Assert(err, check.IsNil) + mimeType := manifest.GuessMIMEType(readManifest) + c.Assert(mimeType, check.Equals, "application/vnd.docker.distribution.manifest.list.v2+json") + out := combinedOutputOfCommand(c, "ls", "-1", dir1) + c.Assert(out, check.Equals, "manifest.json\nversion\n") +} + func (s *CopySuite) TestCopyWithManifestListConverge(c *check.C) { oci1, err := ioutil.TempDir("", "copy-all-manifest-list-oci") c.Assert(err, check.IsNil) @@ -168,9 +183,9 @@ func (s *CopySuite) TestCopyWithManifestListConverge(c *check.C) { c.Assert(err, check.IsNil) defer os.RemoveAll(dir2) assertSkopeoSucceeds(c, "", "copy", knownListImage, "oci:"+oci1) - assertSkopeoSucceeds(c, "", "copy", "--all", "oci:"+oci1, "dir:"+dir1) + assertSkopeoSucceeds(c, "", "copy", "--multi-arch=all", "oci:"+oci1, "dir:"+dir1) assertSkopeoSucceeds(c, "", "copy", "--format", "oci", knownListImage, "dir:"+dir2) - assertSkopeoSucceeds(c, "", "copy", "--all", "dir:"+dir2, "oci:"+oci2) + assertSkopeoSucceeds(c, "", "copy", "--multi-arch=all", "dir:"+dir2, "oci:"+oci2) assertDirImagesAreEqual(c, dir1, dir2) out := combinedOutputOfCommand(c, "diff", "-urN", oci1, oci2) c.Assert(out, check.Equals, "") @@ -181,7 +196,7 @@ func (s *CopySuite) TestCopyAllWithManifestListStorageFails(c *check.C) { c.Assert(err, check.IsNil) defer os.RemoveAll(storage) storage = fmt.Sprintf("[vfs@%s/root+%s/runroot]", storage, storage) - assertSkopeoFails(c, `.*destination transport .* does not support copying multiple images as a group.*`, "copy", "--all", knownListImage, "containers-storage:"+storage+"test") + assertSkopeoFails(c, `.*destination transport .* does not support copying multiple images as a group.*`, "copy", "--multi-arch=all", knownListImage, "containers-storage:"+storage+"test") } func (s *CopySuite) TestCopyWithManifestListStorage(c *check.C) { @@ -239,7 +254,7 @@ func (s *CopySuite) TestCopyWithManifestListDigest(c *check.C) { c.Assert(err, check.IsNil) digest := manifestDigest.String() assertSkopeoSucceeds(c, "", "copy", knownListImage+"@"+digest, "dir:"+dir1) - assertSkopeoSucceeds(c, "", "copy", "--all", knownListImage+"@"+digest, "dir:"+dir2) + assertSkopeoSucceeds(c, "", "copy", "--multi-arch=all", knownListImage+"@"+digest, "dir:"+dir2) assertSkopeoSucceeds(c, "", "copy", "dir:"+dir1, "oci:"+oci1) assertSkopeoSucceeds(c, "", "copy", "dir:"+dir2, "oci:"+oci2) out := combinedOutputOfCommand(c, "diff", "-urN", oci1, oci2)