-
Notifications
You must be signed in to change notification settings - Fork 76
copy: add option to strip sparse manifest lists #655
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -71,6 +71,22 @@ const ( | |
| // specific images from the source reference. | ||
| type ImageListSelection int | ||
|
|
||
| const ( | ||
| // KeepSparseManifestList is the default value which, when set in | ||
| // Options.SparseManifestListAction, indicates that the manifest is kept | ||
| // as is even though some images from the list may be missing. Some | ||
| // registries may not support this. | ||
| KeepSparseManifestList SparseManifestListAction = iota | ||
|
|
||
| // StripSparseManifestList will strip missing images from the manifest | ||
| // list. When images are stripped the digest will differ from the original. | ||
| StripSparseManifestList | ||
| ) | ||
|
|
||
| // SparseManifestListAction is one of KeepSparseManifestList or StripSparseManifestList | ||
| // to control the behavior when only a subset of images from a manifest list is copied | ||
| type SparseManifestListAction int | ||
|
|
||
| // Options allows supplying non-default configuration modifying the behavior of CopyImage. | ||
| type Options struct { | ||
| RemoveSignatures bool // Remove any pre-existing signatures. Signers and SignBy… will still add a new signature. | ||
|
|
@@ -129,6 +145,10 @@ type Options struct { | |
| // to not indicate "nondistributable". | ||
| DownloadForeignLayers bool | ||
|
|
||
| // When only a subset of images of a list is copied, this action indicates if the manifest should be kept or stripped. | ||
| // See CopySpecificImages. | ||
| SparseManifestListAction SparseManifestListAction | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this would be better placed closer to the |
||
|
|
||
| // Contains slice of OptionCompressionVariant, where copy will ensure that for each platform | ||
| // in the manifest list, a variant with the requested compression will exist. | ||
| // Invalid when copying a non-multi-architecture image. That will probably | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -289,6 +289,41 @@ func (c *copier) copyMultipleImages(ctx context.Context) (copiedManifest []byte, | |
| return nil, fmt.Errorf("updating manifest list: %w", err) | ||
| } | ||
|
|
||
| // Remove skipped instances from the manifest list if StripSparseManifestList is enabled | ||
| if c.options.ImageListSelection == CopySpecificImages && c.options.SparseManifestListAction == StripSparseManifestList { | ||
| // Build a set of digests that were copied | ||
| copiedDigests := set.New[digest.Digest]() | ||
| for _, instance := range instanceCopyList { | ||
| copiedDigests.Add(instance.sourceDigest) | ||
| } | ||
|
Comment on lines
+294
to
+298
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if the same digest is included multiple times in the index? That’s not very likely with per-platform images, but with
Also, |
||
|
|
||
| // Find which indices were skipped | ||
| var indicesToDelete []int | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (If the structure remains: I think this variable is unnecessary — we can do something like |
||
| for i, instanceDigest := range instanceDigests { | ||
| if !copiedDigests.Contains(instanceDigest) { | ||
| indicesToDelete = append(indicesToDelete, i) | ||
| } | ||
| } | ||
|
|
||
| // Build delete operations for skipped instances | ||
| // Delete from highest to lowest index to avoid shifting | ||
| var deleteEdits []internalManifest.ListEdit | ||
| for i := len(indicesToDelete) - 1; i >= 0; i-- { | ||
| deleteEdits = append(deleteEdits, internalManifest.ListEdit{ | ||
| ListOperation: internalManifest.ListOpDelete, | ||
| DeleteIndex: indicesToDelete[i], | ||
| }) | ||
| } | ||
|
|
||
| // Remove skipped instances from the manifest list using EditInstances | ||
| if len(deleteEdits) > 0 { | ||
| logrus.Debugf("Removing %d instances from manifest list", len(deleteEdits)) | ||
| if err := updatedList.EditInstances(deleteEdits, false); err != nil { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can only call |
||
| return nil, fmt.Errorf("stripping sparse manifest list: %w", err) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Iterate through supported list types, preferred format first. | ||
| c.Printf("Writing manifest list to image destination\n") | ||
| var errs []string | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -132,6 +132,12 @@ func (list *Schema2ListPublic) editInstances(editInstances []ListEdit, cannotMod | |
| }, | ||
| schema2PlatformSpecFromOCIPlatform(*editInstance.AddPlatform), | ||
| }) | ||
| case ListOpDelete: | ||
| if editInstance.DeleteIndex < 0 || editInstance.DeleteIndex >= len(list.Manifests) { | ||
| return fmt.Errorf("Schema2List.EditInstances: invalid delete index %d (list has %d instances)", editInstance.DeleteIndex, len(list.Manifests)) | ||
| } | ||
| // Remove the element by appending slices before and after the target index | ||
| list.Manifests = append(list.Manifests[:editInstance.DeleteIndex], list.Manifests[editInstance.DeleteIndex+1:]...) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| default: | ||
| return fmt.Errorf("internal error: invalid operation: %d", editInstance.ListOperation) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -159,3 +159,40 @@ func TestChooseInstance(t *testing.T) { | |
| } | ||
| } | ||
| } | ||
|
|
||
| // TestListDeleteInstances tests the ListOpDelete functionality for both OCI and Docker manifest formats. | ||
| func TestListDeleteInstances(t *testing.T) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I’d prefer this to be tested in the existing |
||
| manifestFiles := []string{ | ||
| "ociv1.image.index.json", | ||
| "v2list.manifest.json", | ||
| } | ||
|
|
||
| for _, manifestFile := range manifestFiles { | ||
| t.Run(manifestFile, func(t *testing.T) { | ||
| validManifest, err := os.ReadFile(filepath.Join("testdata", manifestFile)) | ||
| require.NoError(t, err) | ||
| list, err := ListFromBlob(validManifest, GuessMIMEType(validManifest)) | ||
| require.NoError(t, err) | ||
|
|
||
| originalInstances := list.Instances() | ||
| require.GreaterOrEqual(t, len(originalInstances), 1) | ||
|
|
||
| // Test successful deletion | ||
| err = list.EditInstances([]ListEdit{{ | ||
| ListOperation: ListOpDelete, | ||
| DeleteIndex: 0, | ||
| }}, false) | ||
| require.NoError(t, err) | ||
| assert.Equal(t, len(originalInstances)-1, len(list.Instances())) | ||
| assert.Equal(t, originalInstances[1:], list.Instances()) | ||
|
|
||
| // Test error on invalid index | ||
| err = list.EditInstances([]ListEdit{{ | ||
| ListOperation: ListOpDelete, | ||
| DeleteIndex: 999, | ||
| }}, false) | ||
| assert.Error(t, err) | ||
| assert.Contains(t, err.Error(), "invalid delete index") | ||
| }) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC, otherwise it doesn’t have the specialized type.