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

copy: replicate manifest list instances with zstd compression on ReplicateZstd:true #1875

Closed
wants to merge 1 commit into from
Closed
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
165 changes: 111 additions & 54 deletions copy/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ type Options struct {
// if this is set to OptionalBoolUndefined (which is the default behavior, and recommended for most callers).
// This only affects CopySystemImage.
PreferGzipInstances types.OptionalBool
// While Copying entire manifest list from src to dest, allow each image to be replicated with compression as zstd
// and appropriate annotations configured for each newly compressed image the manifest list.
Copy link
Member

Choose a reason for hiding this comment

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

nit of a nit

Suggested change
// and appropriate annotations configured for each newly compressed image the manifest list.
// and appropriate annotations configured for each newly compressed image in the manifest list.

ReplicateZstd bool
mtrmac marked this conversation as resolved.
Show resolved Hide resolved

// If OciEncryptConfig is non-nil, it indicates that an image should be encrypted.
// The encryption options is derived from the construction of EncryptConfig object.
Expand Down Expand Up @@ -313,7 +316,7 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef,

if !multiImage {
Copy link
Collaborator

@mtrmac mtrmac Mar 7, 2023

Choose a reason for hiding this comment

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

The non-copyMultipleImages paths should at least fail with the new option. (We can’t represent multiple variants on this path, and SystemContext.CompressionFormat is already sufficient for conversion.)

It could be necessary to support converting a non-multi-arch single-image image into a new manifest list with two compression variants; in that case the new feature will need to be applicable to copyOneImage, i.e. now we need to either implement it or clearly refuse it, so that the behavior doesn’t silently change in the future.

Copy link
Collaborator

Choose a reason for hiding this comment

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

converting a non-multi-arch single-image image into a new manifest list with two compression variants

BTW I think this the case necessary for a simple buildah build && buildah push. So that will need to happen — but it might be a separate future PR. (Alternatively, buildah could create a single-instance manifest list itself, but that seems unnecessarily redundant.)

// The simple case: just copy a single image.
if copiedManifest, _, _, err = c.copyOneImage(ctx, policyContext, options, unparsedToplevel, unparsedToplevel, nil); err != nil {
if copiedManifest, _, _, _, err = c.copyOneImage(ctx, policyContext, options, unparsedToplevel, unparsedToplevel, nil); err != nil {
return nil, err
}
} else if options.ImageListSelection == CopySystemImage {
Expand All @@ -334,7 +337,7 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef,
logrus.Debugf("Source is a manifest list; copying (only) instance %s for current system", instanceDigest)
unparsedInstance := image.UnparsedInstance(rawSource, &instanceDigest)

if copiedManifest, _, _, err = c.copyOneImage(ctx, policyContext, options, unparsedToplevel, unparsedInstance, nil); err != nil {
if copiedManifest, _, _, _, err = c.copyOneImage(ctx, policyContext, options, unparsedToplevel, unparsedInstance, nil); err != nil {
return nil, fmt.Errorf("copying system image from manifest list: %w", err)
}
} else { /* options.ImageListSelection == CopyAllImages or options.ImageListSelection == CopySpecificImages, */
Expand Down Expand Up @@ -487,41 +490,84 @@ func (c *copier) copyMultipleImages(ctx context.Context, policyContext *signatur
// Copy each image, or just the ones we want to copy, in turn.
instanceDigests := updatedList.Instances()
imagesToCopy := len(instanceDigests)
// In case we are copying a manifest list from src to dest and
// zstd replication is set then all images will have one more
// copy with compression format as zstd.
if options.ReplicateZstd {
imagesToCopy = imagesToCopy * 2
mtrmac marked this conversation as resolved.
Show resolved Hide resolved
}
if options.ImageListSelection == CopySpecificImages {
imagesToCopy = len(options.Instances)
}
c.Printf("Copying %d of %d images in list\n", imagesToCopy, len(instanceDigests))
updates := make([]manifest.ListUpdate, len(instanceDigests))
c.Printf("Copying %d of %d images in list\n", imagesToCopy, imagesToCopy)
updates := make([]manifest.ListUpdate, imagesToCopy)
instancesCopied := 0
for i, instanceDigest := range instanceDigests {
if options.ImageListSelection == CopySpecificImages &&
!slices.Contains(options.Instances, instanceDigest) {
update, err := updatedList.Instance(instanceDigest)
processDigestsTimes := 1
if options.ReplicateZstd {
processDigestsTimes = 2
}
for iter := 0; iter < processDigestsTimes; iter++ {
for i, instanceDigest := range instanceDigests {
if iter == 1 && options.ReplicateZstd {
i = i + 2
// Change compression to zstd
options.DestinationCtx.CompressionFormat = &compression.Zstd
mtrmac marked this conversation as resolved.
Show resolved Hide resolved
level := 20
mtrmac marked this conversation as resolved.
Show resolved Hide resolved
options.DestinationCtx.CompressionLevel = &level
c.compressionFormat = &compression.Zstd
c.compressionLevel = &level
mtrmac marked this conversation as resolved.
Show resolved Hide resolved
}
if options.ImageListSelection == CopySpecificImages &&
mtrmac marked this conversation as resolved.
Show resolved Hide resolved
!slices.Contains(options.Instances, instanceDigest) {
update, err := updatedList.Instance(instanceDigest)
if err != nil {
return nil, err
}
logrus.Debugf("Skipping instance %s (%d/%d)", instanceDigest, i+1, len(instanceDigests))
// Record the digest/size/type of the manifest that we didn't copy.
updates[i] = update
continue
}
logrus.Debugf("Copying instance %s (%d/%d)", instanceDigest, i+1, len(instanceDigests))
c.Printf("Copying image %s (%d/%d)\n", instanceDigest, instancesCopied+1, imagesToCopy)
unparsedInstance := image.UnparsedInstance(c.rawSource, &instanceDigest)
updatedManifest, updatedManifestType, updatedManifestDigest, parsedImage, err := c.copyOneImage(ctx, policyContext, options, unparsedToplevel, unparsedInstance, &instanceDigest)
if err != nil {
return nil, err
return nil, fmt.Errorf("copying image %d/%d from manifest list: %w", instancesCopied+1, imagesToCopy, err)
}
instancesCopied++
// Record the result of a possible conversion here.
update := manifest.ListUpdate{
Digest: updatedManifestDigest,
Size: int64(len(updatedManifest)),
MediaType: updatedManifestType,
}
if iter == 1 && options.ReplicateZstd {
srcImage, err := parsedImage.OCIConfig(ctx)
if err != nil {
return nil, err
}
blobInfo := parsedImage.ConfigInfo()
updatedPlatform := imgspecv1.Platform{Architecture: srcImage.Architecture, OS: srcImage.OS, OSVersion: srcImage.OSVersion, OSFeatures: srcImage.OSFeatures, Variant: srcImage.Variant}
mtrmac marked this conversation as resolved.
Show resolved Hide resolved
instancesCopied++
annotations := make(map[string]string)
for k, v := range blobInfo.Annotations {
mtrmac marked this conversation as resolved.
Show resolved Hide resolved
annotations[k] = v
}
// set the standard zstd annotations if annotation for zstd replication was set
annotations[internalManifest.OCI1InstanceAnnotationCompressionZSTD] = "true"
mtrmac marked this conversation as resolved.
Show resolved Hide resolved
// Record the result of a possible conversion here with updated platform and annotations
update = manifest.ListUpdate{
Digest: updatedManifestDigest,
Size: int64(len(updatedManifest)),
MediaType: updatedManifestType,
Platform: &updatedPlatform,
Annotations: annotations,
}
}
logrus.Debugf("Skipping instance %s (%d/%d)", instanceDigest, i+1, len(instanceDigests))
// Record the digest/size/type of the manifest that we didn't copy.
updates[i] = update
continue
}
logrus.Debugf("Copying instance %s (%d/%d)", instanceDigest, i+1, len(instanceDigests))
c.Printf("Copying image %s (%d/%d)\n", instanceDigest, instancesCopied+1, imagesToCopy)
unparsedInstance := image.UnparsedInstance(c.rawSource, &instanceDigest)
updatedManifest, updatedManifestType, updatedManifestDigest, err := c.copyOneImage(ctx, policyContext, options, unparsedToplevel, unparsedInstance, &instanceDigest)
if err != nil {
return nil, fmt.Errorf("copying image %d/%d from manifest list: %w", instancesCopied+1, imagesToCopy, err)
}
instancesCopied++
// Record the result of a possible conversion here.
update := manifest.ListUpdate{
Digest: updatedManifestDigest,
Size: int64(len(updatedManifest)),
MediaType: updatedManifestType,
}
updates[i] = update
}

// Now reset the digest/size/types of the manifests in the list to account for any conversions that we made.
if err = updatedList.UpdateInstances(updates); err != nil {
return nil, fmt.Errorf("updating manifest list: %w", err)
Expand Down Expand Up @@ -597,28 +643,36 @@ func (c *copier) copyMultipleImages(ctx context.Context, policyContext *signatur

// copyOneImage copies a single (non-manifest-list) image unparsedImage, using policyContext to validate
// source image admissibility.
func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.PolicyContext, options *Options, unparsedToplevel, unparsedImage *image.UnparsedImage, targetInstance *digest.Digest) (retManifest []byte, retManifestType string, retManifestDigest digest.Digest, retErr error) {
func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.PolicyContext, options *Options, unparsedToplevel, unparsedImage *image.UnparsedImage, targetInstance *digest.Digest) (retManifest []byte, retManifestType string, retManifestDigest digest.Digest, retParsedImage types.Image, retErr error) {
var parsedImage types.Image
forceProcessLayer := false
// We will need to repush replicated image with different digest hence while
// copying layers we must force process layers
if options.ReplicateZstd {
forceProcessLayer = true
mtrmac marked this conversation as resolved.
Show resolved Hide resolved
}
// The caller is handling manifest lists; this could happen only if a manifest list contains a manifest list.
// Make sure we fail cleanly in such cases.
multiImage, err := isMultiImage(ctx, unparsedImage)
if err != nil {
// FIXME FIXME: How to name a reference for the sub-image?
return nil, "", "", fmt.Errorf("determining manifest MIME type for %s: %w", transports.ImageName(unparsedImage.Reference()), err)
return nil, "", "", parsedImage, fmt.Errorf("determining manifest MIME type for %s: %w", transports.ImageName(unparsedImage.Reference()), err)
mtrmac marked this conversation as resolved.
Show resolved Hide resolved
}
if multiImage {
return nil, "", "", fmt.Errorf("Unexpectedly received a manifest list instead of a manifest for a single image")
return nil, "", "", parsedImage, fmt.Errorf("Unexpectedly received a manifest list instead of a manifest for a single image")
}

// Please keep this policy check BEFORE reading any other information about the image.
// (The multiImage check above only matches the MIME type, which we have received anyway.
// Actual parsing of anything should be deferred.)
if allowed, err := policyContext.IsRunningImageAllowed(ctx, unparsedImage); !allowed || err != nil { // Be paranoid and fail if either return value indicates so.
return nil, "", "", fmt.Errorf("Source image rejected: %w", err)
return nil, "", "", parsedImage, fmt.Errorf("Source image rejected: %w", err)
}
src, err := image.FromUnparsedImage(ctx, options.SourceCtx, unparsedImage)
if err != nil {
return nil, "", "", fmt.Errorf("initializing image from source %s: %w", transports.ImageName(c.rawSource.Reference()), err)
return nil, "", "", parsedImage, fmt.Errorf("initializing image from source %s: %w", transports.ImageName(c.rawSource.Reference()), err)
}
parsedImage = src

// If the destination is a digested reference, make a note of that, determine what digest value we're
// expecting, and check that the source manifest matches it. If the source manifest doesn't, but it's
Expand All @@ -629,33 +683,33 @@ func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.Poli
destIsDigestedReference = true
matches, err := manifest.MatchesDigest(src.ManifestBlob, digested.Digest())
if err != nil {
return nil, "", "", fmt.Errorf("computing digest of source image's manifest: %w", err)
return nil, "", "", parsedImage, fmt.Errorf("computing digest of source image's manifest: %w", err)
}
if !matches {
manifestList, _, err := unparsedToplevel.Manifest(ctx)
if err != nil {
return nil, "", "", fmt.Errorf("reading manifest from source image: %w", err)
return nil, "", "", parsedImage, fmt.Errorf("reading manifest from source image: %w", err)
}
matches, err = manifest.MatchesDigest(manifestList, digested.Digest())
if err != nil {
return nil, "", "", fmt.Errorf("computing digest of source image's manifest: %w", err)
return nil, "", "", parsedImage, fmt.Errorf("computing digest of source image's manifest: %w", err)
}
if !matches {
return nil, "", "", errors.New("Digest of source image's manifest would not match destination reference")
return nil, "", "", parsedImage, errors.New("Digest of source image's manifest would not match destination reference")
}
}
}
}

if err := checkImageDestinationForCurrentRuntime(ctx, options.DestinationCtx, src, c.dest); err != nil {
return nil, "", "", err
return nil, "", "", parsedImage, err
}

sigs, err := c.sourceSignatures(ctx, src, options,
"Getting image source signatures",
"Checking if image destination supports signatures")
if err != nil {
return nil, "", "", err
return nil, "", "", parsedImage, err
}

// Determine if we're allowed to modify the manifest.
Expand Down Expand Up @@ -691,7 +745,7 @@ func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.Poli
ic.canSubstituteBlobs = ic.cannotModifyManifestReason == "" && len(c.signers) == 0

if err := ic.updateEmbeddedDockerReference(); err != nil {
return nil, "", "", err
return nil, "", "", parsedImage, err
}

destRequiresOciEncryption := (isEncrypted(src) && ic.c.ociDecryptConfig != nil) || options.OciEncryptLayers != nil
Expand All @@ -704,7 +758,7 @@ func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.Poli
cannotModifyManifestReason: ic.cannotModifyManifestReason,
})
if err != nil {
return nil, "", "", err
return nil, "", "", parsedImage, err
}
// We set up this part of ic.manifestUpdates quite early, not just around the
// code that calls copyUpdatedConfigAndManifest, so that other parts of the copy code
Expand All @@ -727,18 +781,18 @@ func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.Poli
isSrcDestManifestEqual, retManifest, retManifestType, retManifestDigest, err := compareImageDestinationManifestEqual(ctx, options, src, targetInstance, c.dest)
if err != nil {
logrus.Warnf("Failed to compare destination image manifest: %v", err)
return nil, "", "", err
return nil, "", "", parsedImage, err
}

if isSrcDestManifestEqual {
c.Printf("Skipping: image already present at destination\n")
return retManifest, retManifestType, retManifestDigest, nil
return retManifest, retManifestType, retManifestDigest, parsedImage, nil
}
}
}

if err := ic.copyLayers(ctx); err != nil {
return nil, "", "", err
if err := ic.copyLayers(ctx, forceProcessLayer); err != nil {
return nil, "", "", parsedImage, err
}

// With docker/distribution registries we do not know whether the registry accepts schema2 or schema1 only;
Expand All @@ -762,14 +816,14 @@ func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.Poli
// We don’t have other options.
// In principle the code below would handle this as well, but the resulting error message is fairly ugly.
// Don’t bother the user with MIME types if we have no choice.
return nil, "", "", err
return nil, "", "", parsedImage, err
}
// If the original MIME type is acceptable, determineManifestConversion always uses it as manifestConversionPlan.preferredMIMEType.
// So if we are here, we will definitely be trying to convert the manifest.
// With ic.cannotModifyManifestReason != "", that would just be a string of repeated failures for the same reason,
// so let’s bail out early and with a better error message.
if ic.cannotModifyManifestReason != "" {
return nil, "", "", fmt.Errorf("writing manifest failed and we cannot try conversions: %q: %w", cannotModifyManifestReason, err)
return nil, "", "", parsedImage, fmt.Errorf("writing manifest failed and we cannot try conversions: %q: %w", cannotModifyManifestReason, err)
}

// errs is a list of errors when trying various manifest types. Also serves as an "upload succeeded" flag when set to nil.
Expand All @@ -792,7 +846,7 @@ func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.Poli
break
}
if errs != nil {
return nil, "", "", fmt.Errorf("Uploading manifest failed, attempted the following formats: %s", strings.Join(errs, ", "))
return nil, "", "", parsedImage, fmt.Errorf("Uploading manifest failed, attempted the following formats: %s", strings.Join(errs, ", "))
}
}
if targetInstance != nil {
Expand All @@ -801,16 +855,16 @@ func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.Poli

newSigs, err := c.createSignatures(ctx, manifestBytes, options.SignIdentity)
if err != nil {
return nil, "", "", err
return nil, "", "", parsedImage, err
}
sigs = append(sigs, newSigs...)

c.Printf("Storing signatures\n")
if err := c.dest.PutSignaturesWithFormat(ctx, sigs, targetInstance); err != nil {
return nil, "", "", fmt.Errorf("writing signatures: %w", err)
return nil, "", "", parsedImage, fmt.Errorf("writing signatures: %w", err)
}

return manifestBytes, retManifestType, retManifestDigest, nil
return manifestBytes, retManifestType, retManifestDigest, parsedImage, nil
}

// Printf writes a formatted string to c.reportWriter.
Expand Down Expand Up @@ -888,7 +942,7 @@ func isTTY(w io.Writer) bool {
}

// copyLayers copies layers from ic.src/ic.c.rawSource to dest, using and updating ic.manifestUpdates if necessary and ic.cannotModifyManifestReason == "".
func (ic *imageCopier) copyLayers(ctx context.Context) error {
func (ic *imageCopier) copyLayers(ctx context.Context, forceProcessLayers bool) error {
srcInfos := ic.src.LayerInfos()
numLayers := len(srcInfos)
updatedSrcInfos, err := ic.src.LayerInfosForCopy(ctx)
Expand Down Expand Up @@ -938,7 +992,7 @@ func (ic *imageCopier) copyLayers(ctx context.Context) error {
logrus.Debugf("Skipping foreign layer %q copy to %s", cld.destInfo.Digest, ic.c.dest.Reference().Transport().Name())
}
} else {
cld.destInfo, cld.diffID, cld.err = ic.copyLayer(ctx, srcLayer, toEncrypt, pool, index, srcRef, manifestLayerInfos[index].EmptyLayer)
cld.destInfo, cld.diffID, cld.err = ic.copyLayer(ctx, srcLayer, toEncrypt, pool, index, srcRef, manifestLayerInfos[index].EmptyLayer, forceProcessLayers)
}
data[index] = cld
}
Expand Down Expand Up @@ -1111,7 +1165,7 @@ type diffIDResult struct {
// copyLayer copies a layer with srcInfo (with known Digest and Annotations and possibly known Size) in src to dest, perhaps (de/re/)compressing it,
// and returns a complete blobInfo of the copied layer, and a value for LayerDiffIDs if diffIDIsNeeded
// srcRef can be used as an additional hint to the destination during checking whether a layer can be reused but srcRef can be nil.
func (ic *imageCopier) copyLayer(ctx context.Context, srcInfo types.BlobInfo, toEncrypt bool, pool *mpb.Progress, layerIndex int, srcRef reference.Named, emptyLayer bool) (types.BlobInfo, digest.Digest, error) {
func (ic *imageCopier) copyLayer(ctx context.Context, srcInfo types.BlobInfo, toEncrypt bool, pool *mpb.Progress, layerIndex int, srcRef reference.Named, emptyLayer bool, forceProcessLayer bool) (types.BlobInfo, digest.Digest, error) {
// If the srcInfo doesn't contain compression information, try to compute it from the
// MediaType, which was either read from a manifest by way of LayerInfos() or constructed
// by LayerInfosForCopy(), if it was supplied at all. If we succeed in copying the blob,
Expand Down Expand Up @@ -1141,6 +1195,9 @@ func (ic *imageCopier) copyLayer(ctx context.Context, srcInfo types.BlobInfo, to
// but it’s not trivially safe to do such things, so until someone takes the effort to make a comprehensive argument, let’s not.
encryptingOrDecrypting := toEncrypt || (isOciEncrypted(srcInfo.MediaType) && ic.c.ociDecryptConfig != nil)
canAvoidProcessingCompleteLayer := !diffIDIsNeeded && !encryptingOrDecrypting
if forceProcessLayer {
canAvoidProcessingCompleteLayer = false
mtrmac marked this conversation as resolved.
Show resolved Hide resolved
}

// Don’t read the layer from the source if we already have the blob, and optimizations are acceptable.
if canAvoidProcessingCompleteLayer {
Expand Down
8 changes: 5 additions & 3 deletions internal/manifest/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,11 @@ type List interface {
// ListUpdate includes the fields which a List's UpdateInstances() method will modify.
// This is publicly visible as c/image/manifest.ListUpdate.
type ListUpdate struct {
Digest digest.Digest
Size int64
MediaType string
Digest digest.Digest
Size int64
MediaType string
Platform *imgspecv1.Platform
Annotations map[string]string
mtrmac marked this conversation as resolved.
Show resolved Hide resolved
}

// ListPublicFromBlob parses a list of manifests.
Expand Down
Loading