diff --git a/copy/copy.go b/copy/copy.go index bf8f4015b6..1223e726b6 100644 --- a/copy/copy.go +++ b/copy/copy.go @@ -23,6 +23,7 @@ import ( "github.com/containers/image/v5/types" encconfig "github.com/containers/ocicrypt/config" digest "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" "golang.org/x/exp/slices" "golang.org/x/sync/semaphore" @@ -91,8 +92,9 @@ type Options struct { PreserveDigests bool // manifest MIME type of image set by user. "" is default and means use the autodetection to the manifest MIME type ForceManifestMIMEType string - ImageListSelection ImageListSelection // set to either CopySystemImage (the default), CopyAllImages, or CopySpecificImages to control which instances we copy when the source reference is a list; ignored if the source reference is not a list - Instances []digest.Digest // if ImageListSelection is CopySpecificImages, copy only these instances and the list itself + ImageListSelection ImageListSelection // set to either CopySystemImage (the default), CopyAllImages, or CopySpecificImages to control which instances we copy when the source reference is a list; ignored if the source reference is not a list + Instances []digest.Digest // if ImageListSelection is CopySpecificImages, copy only these instances, instances matching the InstancePlatforms list, and the list itself + InstancePlatforms []imgspecv1.Platform // if ImageListSelection is CopySpecificImages, copy only matching instances, instances listed in the Instances list, and the list itself // Give priority to pulling gzip images if multiple images are present when configured to OptionalBoolTrue, // prefers the best compression if this is configured as OptionalBoolFalse. Choose automatically (and the choice may change over time) // if this is set to OptionalBoolUndefined (which is the default behavior, and recommended for most callers). diff --git a/copy/multiple.go b/copy/multiple.go index 097a18855e..f6e006aecb 100644 --- a/copy/multiple.go +++ b/copy/multiple.go @@ -10,11 +10,13 @@ import ( "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/internal/image" internalManifest "github.com/containers/image/v5/internal/manifest" + "github.com/containers/image/v5/internal/set" "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/signature" + "github.com/containers/image/v5/types" + digest "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" - "golang.org/x/exp/slices" ) // copyMultipleImages copies some or all of an image list's instances, using @@ -89,15 +91,19 @@ func (c *copier) copyMultipleImages(ctx context.Context, policyContext *signatur // Copy each image, or just the ones we want to copy, in turn. instanceDigests := updatedList.Instances() imagesToCopy := len(instanceDigests) + var specificImages *set.Set[digest.Digest] if options.ImageListSelection == CopySpecificImages { - imagesToCopy = len(options.Instances) + if specificImages, err = determineSpecificImages(options, updatedList); err != nil { + return nil, err + } + imagesToCopy = specificImages.Size() } c.Printf("Copying %d of %d images in list\n", imagesToCopy, len(instanceDigests)) updates := make([]manifest.ListUpdate, len(instanceDigests)) instancesCopied := 0 for i, instanceDigest := range instanceDigests { if options.ImageListSelection == CopySpecificImages && - !slices.Contains(options.Instances, instanceDigest) { + !specificImages.Contains(instanceDigest) { update, err := updatedList.Instance(instanceDigest) if err != nil { return nil, err @@ -196,3 +202,30 @@ func (c *copier) copyMultipleImages(ctx context.Context, policyContext *signatur return manifestList, nil } + +// determineSpecificImages returns a set of images to copy based on the +// Instances and InstancePlatforms fields of the passed-in options structure +func determineSpecificImages(options *Options, updatedList manifest.List) (*set.Set[digest.Digest], error) { + // Start with the instances that were listed by digest. + specificImages := set.New[digest.Digest]() + for _, instanceDigest := range options.Instances { + specificImages.Add(instanceDigest) + } + if len(options.InstancePlatforms) > 0 { + // Choose the best match for each platform we were asked to + // also copy, and add it to the set of instances to copy. + for _, platform := range options.InstancePlatforms { + platformContext := types.SystemContext{ + OSChoice: platform.OS, + ArchitectureChoice: platform.Architecture, + VariantChoice: platform.Variant, + } + instanceDigest, err := updatedList.ChooseInstance(&platformContext) + if err != nil { + return nil, fmt.Errorf("While choosing the instance for platform spec %v: %w", platform, err) + } + specificImages.Add(instanceDigest) + } + } + return specificImages, nil +} diff --git a/copy/multiple_test.go b/copy/multiple_test.go new file mode 100644 index 0000000000..09dbdbda47 --- /dev/null +++ b/copy/multiple_test.go @@ -0,0 +1,195 @@ +package copy + +import ( + "io/ioutil" + "reflect" + "strings" + "testing" + + "github.com/containers/image/v5/manifest" + digest "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +func TestDetermineSpecificImages(t *testing.T) { + testCases := []struct { + id string + fixture string + instanceDigests []digest.Digest + instancePlatforms []imgspecv1.Platform + expected []digest.Digest + expectedErrIncludes string + }{ + { + id: "no inputs no outputs", + fixture: "../manifest/fixtures/v2list.manifest.json", + }, + { + id: "instances only out of order", + fixture: "../manifest/fixtures/v2list.manifest.json", + instanceDigests: []digest.Digest{ + "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2", + "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783", + }, + expected: []digest.Digest{ + "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783", + "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2", + }, + }, + { + id: "instances only in order", + fixture: "../manifest/fixtures/v2list.manifest.json", + instanceDigests: []digest.Digest{ + "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783", + "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2", + }, + expected: []digest.Digest{ + "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783", + "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2", + }, + }, + { + id: "platforms only", + fixture: "../manifest/fixtures/v2list.manifest.json", + instancePlatforms: []imgspecv1.Platform{ + { + OS: "linux", + Architecture: "s390x", + }, + { + OS: "linux", + Architecture: "ppc64le", + }, + }, + expected: []digest.Digest{ + "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783", + "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2", + }, + }, + { + id: "platforms only in order", + fixture: "../manifest/fixtures/v2list.manifest.json", + instancePlatforms: []imgspecv1.Platform{ + { + OS: "linux", + Architecture: "ppc64le", + }, + { + OS: "linux", + Architecture: "s390x", + }, + }, + expected: []digest.Digest{ + "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783", + "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2", + }, + }, + { + id: "platforms only out of order", + fixture: "../manifest/fixtures/v2list.manifest.json", + instancePlatforms: []imgspecv1.Platform{ + { + OS: "linux", + Architecture: "s390x", + }, + { + OS: "linux", + Architecture: "ppc64le", + }, + }, + expected: []digest.Digest{ + "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783", + "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2", + }, + }, + { + id: "mixed without duplicates", + fixture: "../manifest/fixtures/v2list.manifest.json", + instancePlatforms: []imgspecv1.Platform{ + { + OS: "linux", + Architecture: "s390x", + }, + }, + instanceDigests: []digest.Digest{ + "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783", + }, + expected: []digest.Digest{ + "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783", + "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2", + }, + }, + { + id: "mixed with duplicates", + fixture: "../manifest/fixtures/v2list.manifest.json", + instancePlatforms: []imgspecv1.Platform{ + { + OS: "linux", + Architecture: "ppc64le", + }, + { + OS: "linux", + Architecture: "s390x", + }, + }, + instanceDigests: []digest.Digest{ + "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783", + "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2", + }, + expected: []digest.Digest{ + "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783", + "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2", + }, + }, + { + id: "no such platform", + fixture: "../manifest/fixtures/v2list.manifest.json", + instancePlatforms: []imgspecv1.Platform{ + { + OS: "windows", + Architecture: "amd64", + }, + { + OS: "darwin", + Architecture: "arm64", + }, + }, + expectedErrIncludes: "no image found in manifest list for", + }, + } + for _, tc := range testCases { + t.Run(tc.id, func(t *testing.T) { + listBlob, err := ioutil.ReadFile(tc.fixture) + if err != nil { + t.Fatalf("unexpected error reading fixture %q: %v", tc.fixture, err) + } + list, err := manifest.ListFromBlob(listBlob, manifest.GuessMIMEType(listBlob)) + if err != nil { + t.Fatalf("unexpected error parsing fixture %q: %v", tc.fixture, err) + } + options := Options{ + Instances: tc.instanceDigests, + InstancePlatforms: tc.instancePlatforms, + } + specific, err := determineSpecificImages(&options, list) + if err != nil { + if tc.expectedErrIncludes != "" { + if strings.Contains(err.Error(), tc.expectedErrIncludes) { + // okay + return + } + } + t.Fatalf("unexpected error selecting instances: %v", err) + } + var selected []digest.Digest + for _, instanceDigest := range list.Instances() { + if specific.Contains(instanceDigest) { + selected = append(selected, instanceDigest) + } + } + if !reflect.DeepEqual(selected, tc.expected) { + t.Fatalf("given instance list %#v and platforms list %#v, expected to select %#v, but selected %#v", tc.instanceDigests, tc.instancePlatforms, tc.expected, selected) + } + }) + } +} diff --git a/internal/set/set.go b/internal/set/set.go index 5c7bcabef8..bb5b74e527 100644 --- a/internal/set/set.go +++ b/internal/set/set.go @@ -44,3 +44,7 @@ func (s *Set[E]) Empty() bool { func (s *Set[E]) Values() []E { return maps.Keys(s.m) } + +func (s Set[E]) Size() int { + return len(s.m) +}