Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support copy multi arch instance #2612

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions copy/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
91 changes: 86 additions & 5 deletions copy/multiple.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type instanceCopyKind int
const (
instanceCopyCopy instanceCopyKind = iota
instanceCopyClone
instanceCopyDelete
)

type instanceCopy struct {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
cleverhu marked this conversation as resolved.
Show resolved Hide resolved
filteredInstanceDigests, err := filterInstancesByPlatforms(list, options.ImageListPlatforms)
cleverhu marked this conversation as resolved.
Show resolved Hide resolved
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`
Expand All @@ -109,20 +167,33 @@ 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
}
compressionsByPlatform, err := platformCompressionMap(list, instanceDigests)
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)
Expand Down Expand Up @@ -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
Expand All @@ -252,15 +324,17 @@ 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))
unparsedInstance := image.UnparsedInstance(c.rawSource, &instanceCopyList[i].sourceDigest)
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)
}
Expand All @@ -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)
}
Expand Down
11 changes: 10 additions & 1 deletion internal/manifest/docker_schema2_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
1 change: 1 addition & 0 deletions internal/manifest/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const (
listOpInvalid ListOp = iota
ListOpAdd
ListOpUpdate
ListOpRemove
)

// ListEdit includes the fields which a List's EditInstances() method will modify.
Expand Down
13 changes: 12 additions & 1 deletion internal/manifest/oci_index.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down