Skip to content

Conversation

@aguidirh
Copy link

Add option to strip sparse manifest lists when copying specific images

This PR adds the ability to strip non-copied instances from manifest lists when using CopySpecificImages, implementing the functionality proposed in containers/image#1707.

Motivation

Relates to #227

When copying a subset of images from a multi-architecture manifest list using CopySpecificImages, the current behavior always produces a sparse manifest list - a manifest list that references platforms that weren't actually copied. While this maintains the original digest, many registries reject sparse manifests with "blob unknown to registry" errors.

This PR adds a new SparseManifestListAction option that gives users control over this behavior:

  1. KeepSparseManifestList (default, existing behavior) - keeps all references even for non-copied images
  2. StripSparseManifestList (new) - removes non-copied instances and generates a manifest list with only copied instances

This is particularly important for use cases like selective mirroring where only specific architectures need to be copied to registries that don't support sparse manifests.

Changes

1. New SparseManifestListAction option in image/copy

  • Added SparseManifestListAction enum with two values:
    • KeepSparseManifestList (default): preserves existing behavior - keeps all manifest references
    • StripSparseManifestList (new): removes non-copied instances from the manifest list
  • Integrated into Options struct to give users explicit control

2. New ListOpDelete operation in image/internal/manifest

  • Extended ListEdit with ListOpDelete operation and DeleteIndex field to support removing instances
  • Implemented deletion logic in both OCI1Index and Schema2List
  • Uses index-based deletion (not digest-based) to handle cases where multiple platforms share the same digest (e.g., amd64 variants v2/v3)
  • Requires deletion from highest to lowest index to prevent index shifting issues

3. Sparse manifest stripping in copyMultipleImages (image/copy/multiple.go)

  • Implemented stripping logic that activates when SparseManifestListAction == StripSparseManifestList
  • Identifies which instances were not copied by comparing against the original manifest list
  • Removes skipped instances using the new EditInstances API with ListOpDelete

4. Comprehensive test coverage

  • Added TestListDeleteInstances covering both OCI and Docker manifest formats
  • Tests both successful deletion and error handling for invalid indices
  • Validates high-to-low deletion order requirement

Testing

All existing tests pass. New tests verify:

  • ✅ Successful deletion of instances by index
  • ✅ Error handling for invalid indices
  • ✅ Both OCI Index and Docker Schema2 List formats
  • ✅ Manually tested with oc-mirror and local registry

Credits

This implementation is based on the original work by @bertbaron and @mtrmac in containers/image#1707, adapted for the container-libs monorepo structure with the following changes:

  • Moved from manifest package to internal/manifest (per container-libs directory structure)
  • Switched from digest-based to index-based deletion (per review feedback in original PR about platform variants)
  • Simplified test coverage while maintaining comprehensive validation of both OCI and Docker formats
  • Integrated with the existing EditInstances API pattern used throughout the codebase

Add the ability to strip non-copied instances from manifest lists when
using CopySpecificImages. This implements the functionality originally
proposed in containers/image#1707.

When copying a subset of images from a multi-architecture manifest list
using CopySpecificImages, the current behavior always produces a sparse
manifest list - a manifest list that references platforms that weren't
actually copied. While this maintains the original digest, many
registries reject sparse manifests with "blob unknown to registry"
errors.

This commit adds a new SparseManifestListAction option that gives users
control over this behavior:
- KeepSparseManifestList (default): preserves existing behavior
- StripSparseManifestList (new): removes non-copied instances

The implementation includes:
1. New SparseManifestListAction enum in image/copy
2. New ListOpDelete operation in image/internal/manifest with support
   for both OCI1Index and Schema2List formats
3. Index-based deletion (not digest-based) to handle platform variants
   that share the same digest
4. Stripping logic in copyMultipleImages that activates when
   StripSparseManifestList is set
5. Comprehensive test coverage for deletion operations

Based on original work by @bertbaron and @mtrmac in
containers/image#1707, adapted for the container-libs monorepo
structure.

Relates to containers#227

Signed-off-by: Alex Guidi <[email protected]>
@github-actions github-actions bot added the image Related to "image" package label Feb 11, 2026
@packit-as-a-service
Copy link

Packit jobs failed. @containers/packit-build please check.

Copy link
Contributor

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Just a brief first look: generally the structure and choice of affected sub packages look correct.

This is impressively small, I was expecting a that a larger change to internal/manifest would be necessary.

}

// Find which indices were skipped
var indicesToDelete []int
Copy link
Contributor

Choose a reason for hiding this comment

The 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 range slices.Backwards(instanceDigests) { if !copiedDigests.Contains(…) { deleteEdits = append(…) } in one loop)

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:]...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slices.Delete would be a bit better, (also in the other copy)

}

// TestListDeleteInstances tests the ListOpDelete functionality for both OCI and Docker manifest formats.
func TestListDeleteInstances(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d prefer this to be tested in the existing Test…EditInstances methods (but the two can share a helper if that is beneficial)

// 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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can only call EditInstances once, appending to existing instanceEdits.

Comment on lines +294 to +298
// Build a set of digests that were copied
copiedDigests := set.New[digest.Digest]()
for _, instance := range instanceCopyList {
copiedDigests.Add(instance.sourceDigest)
}
Copy link
Contributor

Choose a reason for hiding this comment

The 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 CopySpecificImages, the caller can choose to copy one of the duplicates and not the other.

prepareInstanceCopies knows exactly which instances are copied and which are not, so I think it can also prepare some version of the deletion instructions.

Also, prepareInstanceCopies is intentionally a ~pure function, so that it is unit-testable. Unit tests for the logic here, ensuring we ask for deleting the correct instances, seem worthwhile.


// StripSparseManifestList will strip missing images from the manifest
// list. When images are stripped the digest will differ from the original.
StripSparseManifestList
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
StripSparseManifestList
StripSparseManifestList SparseManifestListAction

IIRC, otherwise it doesn’t have the specialized type.


// 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be better placed closer to the ImageListSelection field.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

image Related to "image" package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants