Skip to content

Commit d476a84

Browse files
author
cleverhu
committed
support copy multi arch instance
Signed-off-by: cleverhu <[email protected]>
1 parent 94e0c4f commit d476a84

File tree

5 files changed

+115
-10
lines changed

5 files changed

+115
-10
lines changed

copy/copy.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const (
6161
// only accept one image (i.e., it cannot accept lists), an error
6262
// should be returned.
6363
CopySpecificImages
64+
CopyCustomArchImages
6465
)
6566

6667
// ImageListSelection is one of CopySystemImage, CopyAllImages, or
@@ -92,8 +93,9 @@ type Options struct {
9293
PreserveDigests bool
9394
// manifest MIME type of image set by user. "" is default and means use the autodetection to the manifest MIME type
9495
ForceManifestMIMEType string
95-
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
96-
Instances []digest.Digest // if ImageListSelection is CopySpecificImages, copy only these instances and the list itself
96+
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
97+
ImageListPlatforms []manifest.Schema2PlatformSpec // if ImageListSelection is CopySpecificImages, copy only these target platforms
98+
Instances []digest.Digest // if ImageListSelection is CopySpecificImages, copy only these instances and the list itself, this is auto generated by ImageListPlatforms
9799
// Give priority to pulling gzip images if multiple images are present when configured to OptionalBoolTrue,
98100
// prefers the best compression if this is configured as OptionalBoolFalse. Choose automatically (and the choice may change over time)
99101
// 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,
325327
if !supportsMultipleImages(c.dest) {
326328
return nil, fmt.Errorf("copying multiple images: destination transport %q does not support copying multiple images as a group", destRef.Transport().Name())
327329
}
330+
328331
// Copy some or all of the images.
329332
switch c.options.ImageListSelection {
330333
case CopyAllImages:
@@ -365,7 +368,7 @@ func (c *copier) close() {
365368
// validateImageListSelection returns an error if the passed-in value is not one that we recognize as a valid ImageListSelection value
366369
func validateImageListSelection(selection ImageListSelection) error {
367370
switch selection {
368-
case CopySystemImage, CopyAllImages, CopySpecificImages:
371+
case CopySystemImage, CopyAllImages, CopySpecificImages, CopyCustomArchImages:
369372
return nil
370373
default:
371374
return fmt.Errorf("Invalid value for options.ImageListSelection: %d", selection)

copy/multiple.go

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type instanceCopyKind int
2626
const (
2727
instanceCopyCopy instanceCopyKind = iota
2828
instanceCopyClone
29+
instanceCopyDelete
2930
)
3031

3132
type instanceCopy struct {
@@ -60,8 +61,9 @@ func platformV1ToPlatformComparable(platform *imgspecv1.Platform) platformCompar
6061
}
6162
osFeatures := slices.Clone(platform.OSFeatures)
6263
sort.Strings(osFeatures)
63-
return platformComparable{architecture: platform.Architecture,
64-
os: platform.OS,
64+
return platformComparable{
65+
architecture: platform.Architecture,
66+
os: platform.OS,
6567
// This is strictly speaking ambiguous, fields of OSFeatures can contain a ','. Probably good enough for now.
6668
osFeatures: strings.Join(osFeatures, ","),
6769
osVersion: platform.OSVersion,
@@ -98,8 +100,64 @@ func validateCompressionVariantExists(input []OptionCompressionVariant) error {
98100
return nil
99101
}
100102

103+
func getInstanceDigestForPlatform(list internalManifest.List, platform manifest.Schema2PlatformSpec) (digest.Digest, error) {
104+
for _, instanceDigest := range list.Instances() {
105+
instance, err := list.Instance(instanceDigest)
106+
if err != nil {
107+
return "", err
108+
}
109+
110+
if instance.ReadOnly.Platform == nil {
111+
continue
112+
}
113+
114+
if instance.ReadOnly.Platform.OS == platform.OS &&
115+
instance.ReadOnly.Platform.Architecture == platform.Architecture {
116+
return instanceDigest, nil
117+
}
118+
}
119+
120+
return "", fmt.Errorf("no instance found for platform %s/%s", platform.OS, platform.Architecture)
121+
}
122+
123+
func filterInstancesByPlatforms(list internalManifest.List, platforms []manifest.Schema2PlatformSpec) ([]digest.Digest, error) {
124+
if len(platforms) == 0 {
125+
return list.Instances(), nil
126+
}
127+
128+
missingPlatforms := []manifest.Schema2PlatformSpec{}
129+
supportedInstance := set.New[digest.Digest]()
130+
// Check each requested platform
131+
for _, platform := range platforms {
132+
if digest, err := getInstanceDigestForPlatform(list, platform); err != nil {
133+
missingPlatforms = append(missingPlatforms, platform)
134+
} else {
135+
supportedInstance.Add(digest)
136+
}
137+
}
138+
139+
if len(missingPlatforms) > 0 {
140+
var platformStrings []string
141+
for _, p := range missingPlatforms {
142+
platformStr := fmt.Sprintf("%s/%s", p.OS, p.Architecture)
143+
if p.Variant != "" {
144+
platformStr += "/" + p.Variant
145+
}
146+
platformStrings = append(platformStrings, platformStr)
147+
}
148+
return nil, fmt.Errorf("requested platforms not found in image: %s", strings.Join(platformStrings, ", "))
149+
}
150+
151+
return supportedInstance.Values(), nil
152+
}
153+
101154
// prepareInstanceCopies prepares a list of instances which needs to copied to the manifest list.
102155
func prepareInstanceCopies(list internalManifest.List, instanceDigests []digest.Digest, options *Options) ([]instanceCopy, error) {
156+
filteredInstanceDigests, err := filterInstancesByPlatforms(list, options.ImageListPlatforms)
157+
if err != nil {
158+
return nil, err
159+
}
160+
103161
res := []instanceCopy{}
104162
if options.ImageListSelection == CopySpecificImages && len(options.EnsureCompressionVariantsExist) > 0 {
105163
// List can already contain compressed instance for a compression selected in `EnsureCompressionVariantsExist`
@@ -109,20 +167,33 @@ func prepareInstanceCopies(list internalManifest.List, instanceDigests []digest.
109167
// We might define the semantics and implement this in the future.
110168
return res, fmt.Errorf("EnsureCompressionVariantsExist is not implemented for CopySpecificImages")
111169
}
112-
err := validateCompressionVariantExists(options.EnsureCompressionVariantsExist)
170+
171+
err = validateCompressionVariantExists(options.EnsureCompressionVariantsExist)
113172
if err != nil {
114173
return res, err
115174
}
116175
compressionsByPlatform, err := platformCompressionMap(list, instanceDigests)
117176
if err != nil {
118177
return nil, err
119178
}
179+
120180
for i, instanceDigest := range instanceDigests {
121181
if options.ImageListSelection == CopySpecificImages &&
122182
!slices.Contains(options.Instances, instanceDigest) {
123183
logrus.Debugf("Skipping instance %s (%d/%d)", instanceDigest, i+1, len(instanceDigests))
124184
continue
125185
}
186+
187+
if options.ImageListSelection == CopyCustomArchImages &&
188+
!slices.Contains(filteredInstanceDigests, instanceDigest) {
189+
logrus.Debugf("Skipping instance %s (%d/%d)", instanceDigest, i+1, len(instanceDigests))
190+
res = append(res, instanceCopy{
191+
op: instanceCopyDelete,
192+
sourceDigest: instanceDigest,
193+
})
194+
continue
195+
}
196+
126197
instanceDetails, err := list.Instance(instanceDigest)
127198
if err != nil {
128199
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,
232303
if err != nil {
233304
return nil, fmt.Errorf("preparing instances for copy: %w", err)
234305
}
306+
235307
c.Printf("Copying %d images generated from %d images in list\n", len(instanceCopyList), len(instanceDigests))
236308
for i, instance := range instanceCopyList {
237309
// Update instances to be edited by their `ListOperation` and
@@ -252,15 +324,17 @@ func (c *copier) copyMultipleImages(ctx context.Context) (copiedManifest []byte,
252324
UpdateDigest: updated.manifestDigest,
253325
UpdateSize: int64(len(updated.manifest)),
254326
UpdateCompressionAlgorithms: updated.compressionAlgorithms,
255-
UpdateMediaType: updated.manifestMIMEType})
327+
UpdateMediaType: updated.manifestMIMEType,
328+
})
256329
case instanceCopyClone:
257330
logrus.Debugf("Replicating instance %s (%d/%d)", instance.sourceDigest, i+1, len(instanceCopyList))
258331
c.Printf("Replicating image %s (%d/%d)\n", instance.sourceDigest, i+1, len(instanceCopyList))
259332
unparsedInstance := image.UnparsedInstance(c.rawSource, &instanceCopyList[i].sourceDigest)
260333
updated, err := c.copySingleImage(ctx, unparsedInstance, &instanceCopyList[i].sourceDigest, copySingleImageOptions{
261334
requireCompressionFormatMatch: true,
262335
compressionFormat: &instance.cloneCompressionVariant.Algorithm,
263-
compressionLevel: instance.cloneCompressionVariant.Level})
336+
compressionLevel: instance.cloneCompressionVariant.Level,
337+
})
264338
if err != nil {
265339
return nil, fmt.Errorf("replicating image %d/%d from manifest list: %w", i+1, len(instanceCopyList), err)
266340
}
@@ -275,6 +349,13 @@ func (c *copier) copyMultipleImages(ctx context.Context) (copiedManifest []byte,
275349
AddAnnotations: instance.cloneAnnotations,
276350
AddCompressionAlgorithms: updated.compressionAlgorithms,
277351
})
352+
case instanceCopyDelete:
353+
logrus.Debugf("Deleting instance %s (%d/%d)", instance.sourceDigest, i+1, len(instanceCopyList))
354+
c.Printf("Deleting image %s (%d/%d)\n", instance.sourceDigest, i+1, len(instanceCopyList))
355+
instanceEdits = append(instanceEdits, internalManifest.ListEdit{
356+
ListOperation: internalManifest.ListOpRemove,
357+
UpdateOldDigest: instance.sourceDigest,
358+
})
278359
default:
279360
return nil, fmt.Errorf("copying image: invalid copy operation %d", instance.op)
280361
}

internal/manifest/docker_schema2_list.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ func (index *Schema2ListPublic) UpdateInstances(updates []ListUpdate) error {
8282
UpdateDigest: instance.Digest,
8383
UpdateSize: instance.Size,
8484
UpdateMediaType: instance.MediaType,
85-
ListOperation: ListOpUpdate})
85+
ListOperation: ListOpUpdate,
86+
})
8687
}
8788
return index.editInstances(editInstances)
8889
}
@@ -128,6 +129,14 @@ func (index *Schema2ListPublic) editInstances(editInstances []ListEdit) error {
128129
},
129130
schema2PlatformSpecFromOCIPlatform(*editInstance.AddPlatform),
130131
})
132+
case ListOpRemove:
133+
targetIndex := slices.IndexFunc(index.Manifests, func(m Schema2ManifestDescriptor) bool {
134+
return m.Digest == editInstance.UpdateOldDigest
135+
})
136+
if targetIndex == -1 {
137+
return fmt.Errorf("Schema2List.EditInstances: digest %s not found", editInstance.UpdateOldDigest)
138+
}
139+
index.Manifests = slices.Delete(index.Manifests, targetIndex, targetIndex+1)
131140
default:
132141
return fmt.Errorf("internal error: invalid operation: %d", editInstance.ListOperation)
133142
}

internal/manifest/list.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ const (
8383
listOpInvalid ListOp = iota
8484
ListOpAdd
8585
ListOpUpdate
86+
ListOpRemove
8687
)
8788

8889
// ListEdit includes the fields which a List's EditInstances() method will modify.

internal/manifest/oci_index.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ func (index *OCI1IndexPublic) UpdateInstances(updates []ListUpdate) error {
7979
UpdateDigest: instance.Digest,
8080
UpdateSize: instance.Size,
8181
UpdateMediaType: instance.MediaType,
82-
ListOperation: ListOpUpdate})
82+
ListOperation: ListOpUpdate,
83+
})
8384
}
8485
return index.editInstances(editInstances)
8586
}
@@ -166,6 +167,16 @@ func (index *OCI1IndexPublic) editInstances(editInstances []ListEdit) error {
166167
Platform: editInstance.AddPlatform,
167168
Annotations: annotations,
168169
})
170+
case ListOpRemove:
171+
targetIndex := slices.IndexFunc(index.Manifests, func(m imgspecv1.Descriptor) bool {
172+
return m.Digest == editInstance.UpdateOldDigest
173+
})
174+
175+
if targetIndex == -1 {
176+
return fmt.Errorf("OCI1Index.EditInstances: digest %s not found", editInstance.UpdateOldDigest)
177+
}
178+
179+
index.Manifests = slices.Delete(index.Manifests, targetIndex, targetIndex+1)
169180
default:
170181
return fmt.Errorf("internal error: invalid operation: %d", editInstance.ListOperation)
171182
}

0 commit comments

Comments
 (0)