Skip to content

Commit

Permalink
support copy multi arch instance
Browse files Browse the repository at this point in the history
Signed-off-by: cleverhu <[email protected]>
  • Loading branch information
cleverhu committed Nov 27, 2024
1 parent 94e0c4f commit d476a84
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 10 deletions.
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) {
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`
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

0 comments on commit d476a84

Please sign in to comment.