diff --git a/copy/copy.go b/copy/copy.go index 867ba73c7c..02cbceecf7 100644 --- a/copy/copy.go +++ b/copy/copy.go @@ -61,6 +61,7 @@ const ( // only accept one image (i.e., it cannot accept lists), an error // should be returned. CopySpecificImages + CopyCustomArchImages ) // ImageListSelection is one of CopySystemImage, CopyAllImages, or @@ -92,8 +93,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 + ImageListPlatforms []manifest.Schema2PlatformSpec // if ImageListSelection is CopySpecificImages, copy only these target platforms + Instances []digest.Digest // if ImageListSelection is CopySpecificImages, copy only these instances and the list itself, this is auto generated by ImageListPlatforms // 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). @@ -325,6 +327,7 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef, if !supportsMultipleImages(c.dest) { return nil, fmt.Errorf("copying multiple images: destination transport %q does not support copying multiple images as a group", destRef.Transport().Name()) } + // Copy some or all of the images. switch c.options.ImageListSelection { case CopyAllImages: @@ -365,7 +368,7 @@ func (c *copier) close() { // validateImageListSelection returns an error if the passed-in value is not one that we recognize as a valid ImageListSelection value func validateImageListSelection(selection ImageListSelection) error { switch selection { - case CopySystemImage, CopyAllImages, CopySpecificImages: + case CopySystemImage, CopyAllImages, CopySpecificImages, CopyCustomArchImages: return nil default: return fmt.Errorf("Invalid value for options.ImageListSelection: %d", selection) diff --git a/copy/multiple.go b/copy/multiple.go index 009a067ced..b7c03ad7b9 100644 --- a/copy/multiple.go +++ b/copy/multiple.go @@ -26,6 +26,7 @@ type instanceCopyKind int const ( instanceCopyCopy instanceCopyKind = iota instanceCopyClone + instanceCopyDelete ) type instanceCopy struct { @@ -60,8 +61,9 @@ func platformV1ToPlatformComparable(platform *imgspecv1.Platform) platformCompar } osFeatures := slices.Clone(platform.OSFeatures) sort.Strings(osFeatures) - return platformComparable{architecture: platform.Architecture, - os: platform.OS, + return platformComparable{ + architecture: platform.Architecture, + os: platform.OS, // This is strictly speaking ambiguous, fields of OSFeatures can contain a ','. Probably good enough for now. osFeatures: strings.Join(osFeatures, ","), osVersion: platform.OSVersion, @@ -98,8 +100,64 @@ func validateCompressionVariantExists(input []OptionCompressionVariant) error { return nil } +func getInstanceDigestForPlatform(list internalManifest.List, platform manifest.Schema2PlatformSpec) (digest.Digest, error) { + for _, instanceDigest := range list.Instances() { + instance, err := list.Instance(instanceDigest) + if err != nil { + return "", err + } + + if instance.ReadOnly.Platform == nil { + continue + } + + if instance.ReadOnly.Platform.OS == platform.OS && + instance.ReadOnly.Platform.Architecture == platform.Architecture { + return instanceDigest, nil + } + } + + return "", fmt.Errorf("no instance found for platform %s/%s", platform.OS, platform.Architecture) +} + +func filterInstancesByPlatforms(list internalManifest.List, platforms []manifest.Schema2PlatformSpec) ([]digest.Digest, error) { + if len(platforms) == 0 { + return list.Instances(), nil + } + + missingPlatforms := []manifest.Schema2PlatformSpec{} + supportedInstance := set.New[digest.Digest]() + // Check each requested platform + for _, platform := range platforms { + if digest, err := getInstanceDigestForPlatform(list, platform); err != nil { + missingPlatforms = append(missingPlatforms, platform) + } else { + supportedInstance.Add(digest) + } + } + + if len(missingPlatforms) > 0 { + var platformStrings []string + for _, p := range missingPlatforms { + platformStr := fmt.Sprintf("%s/%s", p.OS, p.Architecture) + if p.Variant != "" { + platformStr += "/" + p.Variant + } + platformStrings = append(platformStrings, platformStr) + } + return nil, fmt.Errorf("requested platforms not found in image: %s", strings.Join(platformStrings, ", ")) + } + + return supportedInstance.Values(), nil +} + // prepareInstanceCopies prepares a list of instances which needs to copied to the manifest list. func prepareInstanceCopies(list internalManifest.List, instanceDigests []digest.Digest, options *Options) ([]instanceCopy, error) { + filteredInstanceDigests, err := filterInstancesByPlatforms(list, options.ImageListPlatforms) + if err != nil { + return nil, err + } + res := []instanceCopy{} if options.ImageListSelection == CopySpecificImages && len(options.EnsureCompressionVariantsExist) > 0 { // List can already contain compressed instance for a compression selected in `EnsureCompressionVariantsExist` @@ -109,7 +167,8 @@ func prepareInstanceCopies(list internalManifest.List, instanceDigests []digest. // We might define the semantics and implement this in the future. return res, fmt.Errorf("EnsureCompressionVariantsExist is not implemented for CopySpecificImages") } - err := validateCompressionVariantExists(options.EnsureCompressionVariantsExist) + + err = validateCompressionVariantExists(options.EnsureCompressionVariantsExist) if err != nil { return res, err } @@ -117,12 +176,24 @@ func prepareInstanceCopies(list internalManifest.List, instanceDigests []digest. if err != nil { return nil, err } + for i, instanceDigest := range instanceDigests { if options.ImageListSelection == CopySpecificImages && !slices.Contains(options.Instances, instanceDigest) { logrus.Debugf("Skipping instance %s (%d/%d)", instanceDigest, i+1, len(instanceDigests)) continue } + + if options.ImageListSelection == CopyCustomArchImages && + !slices.Contains(filteredInstanceDigests, instanceDigest) { + logrus.Debugf("Skipping instance %s (%d/%d)", instanceDigest, i+1, len(instanceDigests)) + res = append(res, instanceCopy{ + op: instanceCopyDelete, + sourceDigest: instanceDigest, + }) + continue + } + instanceDetails, err := list.Instance(instanceDigest) if err != nil { return res, fmt.Errorf("getting details for instance %s: %w", instanceDigest, err) @@ -232,6 +303,7 @@ func (c *copier) copyMultipleImages(ctx context.Context) (copiedManifest []byte, if err != nil { return nil, fmt.Errorf("preparing instances for copy: %w", err) } + c.Printf("Copying %d images generated from %d images in list\n", len(instanceCopyList), len(instanceDigests)) for i, instance := range instanceCopyList { // Update instances to be edited by their `ListOperation` and @@ -252,7 +324,8 @@ func (c *copier) copyMultipleImages(ctx context.Context) (copiedManifest []byte, UpdateDigest: updated.manifestDigest, UpdateSize: int64(len(updated.manifest)), UpdateCompressionAlgorithms: updated.compressionAlgorithms, - UpdateMediaType: updated.manifestMIMEType}) + UpdateMediaType: updated.manifestMIMEType, + }) case instanceCopyClone: logrus.Debugf("Replicating instance %s (%d/%d)", instance.sourceDigest, i+1, len(instanceCopyList)) c.Printf("Replicating image %s (%d/%d)\n", instance.sourceDigest, i+1, len(instanceCopyList)) @@ -260,7 +333,8 @@ func (c *copier) copyMultipleImages(ctx context.Context) (copiedManifest []byte, updated, err := c.copySingleImage(ctx, unparsedInstance, &instanceCopyList[i].sourceDigest, copySingleImageOptions{ requireCompressionFormatMatch: true, compressionFormat: &instance.cloneCompressionVariant.Algorithm, - compressionLevel: instance.cloneCompressionVariant.Level}) + compressionLevel: instance.cloneCompressionVariant.Level, + }) if err != nil { return nil, fmt.Errorf("replicating image %d/%d from manifest list: %w", i+1, len(instanceCopyList), err) } @@ -275,6 +349,13 @@ func (c *copier) copyMultipleImages(ctx context.Context) (copiedManifest []byte, AddAnnotations: instance.cloneAnnotations, AddCompressionAlgorithms: updated.compressionAlgorithms, }) + case instanceCopyDelete: + logrus.Debugf("Deleting instance %s (%d/%d)", instance.sourceDigest, i+1, len(instanceCopyList)) + c.Printf("Deleting image %s (%d/%d)\n", instance.sourceDigest, i+1, len(instanceCopyList)) + instanceEdits = append(instanceEdits, internalManifest.ListEdit{ + ListOperation: internalManifest.ListOpRemove, + UpdateOldDigest: instance.sourceDigest, + }) default: return nil, fmt.Errorf("copying image: invalid copy operation %d", instance.op) } diff --git a/internal/manifest/docker_schema2_list.go b/internal/manifest/docker_schema2_list.go index 07922ceceb..0bbc7f4c23 100644 --- a/internal/manifest/docker_schema2_list.go +++ b/internal/manifest/docker_schema2_list.go @@ -82,7 +82,8 @@ func (index *Schema2ListPublic) UpdateInstances(updates []ListUpdate) error { UpdateDigest: instance.Digest, UpdateSize: instance.Size, UpdateMediaType: instance.MediaType, - ListOperation: ListOpUpdate}) + ListOperation: ListOpUpdate, + }) } return index.editInstances(editInstances) } @@ -128,6 +129,14 @@ func (index *Schema2ListPublic) editInstances(editInstances []ListEdit) error { }, schema2PlatformSpecFromOCIPlatform(*editInstance.AddPlatform), }) + case ListOpRemove: + targetIndex := slices.IndexFunc(index.Manifests, func(m Schema2ManifestDescriptor) bool { + return m.Digest == editInstance.UpdateOldDigest + }) + if targetIndex == -1 { + return fmt.Errorf("Schema2List.EditInstances: digest %s not found", editInstance.UpdateOldDigest) + } + index.Manifests = slices.Delete(index.Manifests, targetIndex, targetIndex+1) default: return fmt.Errorf("internal error: invalid operation: %d", editInstance.ListOperation) } diff --git a/internal/manifest/list.go b/internal/manifest/list.go index 1c614d1246..895e2a1a0f 100644 --- a/internal/manifest/list.go +++ b/internal/manifest/list.go @@ -83,6 +83,7 @@ const ( listOpInvalid ListOp = iota ListOpAdd ListOpUpdate + ListOpRemove ) // ListEdit includes the fields which a List's EditInstances() method will modify. diff --git a/internal/manifest/oci_index.go b/internal/manifest/oci_index.go index 6a0f88d3a6..c0d8deb13f 100644 --- a/internal/manifest/oci_index.go +++ b/internal/manifest/oci_index.go @@ -79,7 +79,8 @@ func (index *OCI1IndexPublic) UpdateInstances(updates []ListUpdate) error { UpdateDigest: instance.Digest, UpdateSize: instance.Size, UpdateMediaType: instance.MediaType, - ListOperation: ListOpUpdate}) + ListOperation: ListOpUpdate, + }) } return index.editInstances(editInstances) } @@ -166,6 +167,16 @@ func (index *OCI1IndexPublic) editInstances(editInstances []ListEdit) error { Platform: editInstance.AddPlatform, Annotations: annotations, }) + case ListOpRemove: + targetIndex := slices.IndexFunc(index.Manifests, func(m imgspecv1.Descriptor) bool { + return m.Digest == editInstance.UpdateOldDigest + }) + + if targetIndex == -1 { + return fmt.Errorf("OCI1Index.EditInstances: digest %s not found", editInstance.UpdateOldDigest) + } + + index.Manifests = slices.Delete(index.Manifests, targetIndex, targetIndex+1) default: return fmt.Errorf("internal error: invalid operation: %d", editInstance.ListOperation) }