@@ -3,6 +3,7 @@ package copy
33import (
44 "bytes"
55 "context"
6+ "encoding/json"
67 "errors"
78 "fmt"
89 "maps"
@@ -16,9 +17,12 @@ import (
1617 "go.podman.io/image/v5/docker/reference"
1718 "go.podman.io/image/v5/internal/image"
1819 internalManifest "go.podman.io/image/v5/internal/manifest"
20+ "go.podman.io/image/v5/internal/private"
1921 "go.podman.io/image/v5/internal/set"
2022 "go.podman.io/image/v5/manifest"
2123 "go.podman.io/image/v5/pkg/compression"
24+ "go.podman.io/image/v5/types"
25+ chunkedToc "go.podman.io/storage/pkg/chunked/toc"
2226)
2327
2428type instanceCopyKind int
@@ -28,6 +32,14 @@ const (
2832 instanceCopyClone
2933)
3034
35+ // copiedInstanceData stores info about a successfully copied instance,
36+ // used for creating sentinel variants.
37+ type copiedInstanceData struct {
38+ sourceDigest digest.Digest
39+ result copySingleImageResult
40+ platform * imgspecv1.Platform
41+ }
42+
3143type instanceCopy struct {
3244 op instanceCopyKind
3345 sourceDigest digest.Digest
@@ -283,6 +295,8 @@ func (c *copier) copyMultipleImages(ctx context.Context) (copiedManifest []byte,
283295 if err != nil {
284296 return nil , fmt .Errorf ("preparing instances for copy: %w" , err )
285297 }
298+ var copiedInstances []copiedInstanceData
299+
286300 c .Printf ("Copying %d images generated from %d images in list\n " , len (instanceCopyList ), len (instanceDigests ))
287301 for i , instance := range instanceCopyList {
288302 // Update instances to be edited by their `ListOperation` and
@@ -305,6 +319,15 @@ func (c *copier) copyMultipleImages(ctx context.Context) (copiedManifest []byte,
305319 UpdateCompressionAlgorithms : updated .compressionAlgorithms ,
306320 UpdateMediaType : updated .manifestMIMEType ,
307321 })
322+ // Capture instance data for sentinel variant creation
323+ instanceDetails , detailsErr := updatedList .Instance (instance .sourceDigest )
324+ if detailsErr == nil {
325+ copiedInstances = append (copiedInstances , copiedInstanceData {
326+ sourceDigest : instance .sourceDigest ,
327+ result : updated ,
328+ platform : instanceDetails .ReadOnly .Platform ,
329+ })
330+ }
308331 case instanceCopyClone :
309332 logrus .Debugf ("Replicating instance %s (%d/%d)" , instance .sourceDigest , i + 1 , len (instanceCopyList ))
310333 c .Printf ("Replicating image %s (%d/%d)\n " , instance .sourceDigest , i + 1 , len (instanceCopyList ))
@@ -333,6 +356,15 @@ func (c *copier) copyMultipleImages(ctx context.Context) (copiedManifest []byte,
333356 }
334357 }
335358
359+ // Create zstd:chunked sentinel variants for instances where any layer uses zstd:chunked.
360+ if cannotModifyManifestListReason == "" {
361+ sentinelEdits , err := c .createZstdChunkedSentinelVariants (ctx , copiedInstances , updatedList )
362+ if err != nil {
363+ return nil , fmt .Errorf ("creating zstd:chunked sentinel variants: %w" , err )
364+ }
365+ instanceEdits = append (instanceEdits , sentinelEdits ... )
366+ }
367+
336368 // Now reset the digest/size/types of the manifests in the list to account for any conversions that we made.
337369 if err = updatedList .EditInstances (instanceEdits , cannotModifyManifestListReason != "" ); err != nil {
338370 return nil , fmt .Errorf ("updating manifest list: %w" , err )
@@ -405,3 +437,244 @@ func (c *copier) copyMultipleImages(ctx context.Context) (copiedManifest []byte,
405437
406438 return manifestList , nil
407439}
440+
441+ // hasZstdChunkedLayers returns true if any non-empty layer in the manifest has
442+ // zstd:chunked TOC annotations.
443+ func hasZstdChunkedLayers (ociMan * manifest.OCI1 ) bool {
444+ for _ , l := range ociMan .LayerInfos () {
445+ if l .EmptyLayer {
446+ continue
447+ }
448+ d , err := chunkedToc .GetTOCDigest (l .Annotations )
449+ if err == nil && d != nil {
450+ return true
451+ }
452+ }
453+ return false
454+ }
455+
456+ // pushSentinelVariant creates and pushes a sentinel variant of the given OCI manifest.
457+ // It prepends a sentinel layer and DiffID, creates a new config, and pushes everything
458+ // to the destination. Returns the serialized sentinel manifest and its digest.
459+ func (c * copier ) pushSentinelVariant (ctx context.Context , ociMan * manifest.OCI1 , ociConfig * imgspecv1.Image ) ([]byte , digest.Digest , error ) {
460+ sentinelContent := []byte (chunkedToc .ZstdChunkedSentinelContent )
461+
462+ // Push sentinel blob (content-addressed, so idempotent if already pushed).
463+ _ , err := c .dest .PutBlobWithOptions (ctx , bytes .NewReader (sentinelContent ),
464+ types.BlobInfo {
465+ Digest : chunkedToc .ZstdChunkedSentinelDigest ,
466+ Size : int64 (len (sentinelContent )),
467+ }, private.PutBlobOptions {Cache : c .blobInfoCache })
468+ if err != nil {
469+ return nil , "" , fmt .Errorf ("pushing sentinel blob: %w" , err )
470+ }
471+
472+ // Create new config with sentinel DiffID prepended.
473+ newDiffIDs := make ([]digest.Digest , 0 , len (ociConfig .RootFS .DiffIDs )+ 1 )
474+ newDiffIDs = append (newDiffIDs , chunkedToc .ZstdChunkedSentinelDigest )
475+ newDiffIDs = append (newDiffIDs , ociConfig .RootFS .DiffIDs ... )
476+ ociConfig .RootFS .DiffIDs = newDiffIDs
477+ configBlob , err := json .Marshal (ociConfig )
478+ if err != nil {
479+ return nil , "" , fmt .Errorf ("marshaling sentinel config: %w" , err )
480+ }
481+ configDigest := digest .FromBytes (configBlob )
482+
483+ // Push new config.
484+ _ , err = c .dest .PutBlobWithOptions (ctx , bytes .NewReader (configBlob ),
485+ types.BlobInfo {
486+ Digest : configDigest ,
487+ Size : int64 (len (configBlob )),
488+ MediaType : imgspecv1 .MediaTypeImageConfig ,
489+ }, private.PutBlobOptions {Cache : c .blobInfoCache , IsConfig : true })
490+ if err != nil {
491+ return nil , "" , fmt .Errorf ("pushing sentinel config: %w" , err )
492+ }
493+
494+ // Build sentinel manifest: sentinel layer + original layers.
495+ newLayers := make ([]imgspecv1.Descriptor , 0 , len (ociMan .Layers )+ 1 )
496+ newLayers = append (newLayers , imgspecv1.Descriptor {
497+ MediaType : imgspecv1 .MediaTypeImageLayerZstd ,
498+ Digest : chunkedToc .ZstdChunkedSentinelDigest ,
499+ Size : int64 (len (sentinelContent )),
500+ })
501+ newLayers = append (newLayers , ociMan .Layers ... )
502+
503+ sentinelOCI := manifest .OCI1FromComponents (imgspecv1.Descriptor {
504+ MediaType : imgspecv1 .MediaTypeImageConfig ,
505+ Digest : configDigest ,
506+ Size : int64 (len (configBlob )),
507+ }, newLayers )
508+ sentinelManifestBlob , err := sentinelOCI .Serialize ()
509+ if err != nil {
510+ return nil , "" , fmt .Errorf ("serializing sentinel manifest: %w" , err )
511+ }
512+ sentinelManifestDigest := digest .FromBytes (sentinelManifestBlob )
513+
514+ // Push sentinel manifest.
515+ if err := c .dest .PutManifest (ctx , sentinelManifestBlob , & sentinelManifestDigest ); err != nil {
516+ return nil , "" , fmt .Errorf ("pushing sentinel manifest: %w" , err )
517+ }
518+
519+ return sentinelManifestBlob , sentinelManifestDigest , nil
520+ }
521+
522+ // createZstdChunkedSentinelVariants creates sentinel variants for instances
523+ // where any layer uses zstd:chunked. The sentinel variant has a non-tar sentinel
524+ // layer prepended, signaling aware clients to skip the full-digest mitigation.
525+ // It returns ListEdit entries to add the sentinel variants to the manifest list.
526+ func (c * copier ) createZstdChunkedSentinelVariants (ctx context.Context , copiedInstances []copiedInstanceData , updatedList internalManifest.List ) ([]internalManifest.ListEdit , error ) {
527+ var edits []internalManifest.ListEdit
528+
529+ // Check which platforms already have a sentinel variant in the source.
530+ platformsWithSentinel := set .New [platformComparable ]()
531+ for _ , d := range updatedList .Instances () {
532+ details , err := updatedList .Instance (d )
533+ if err != nil {
534+ continue
535+ }
536+ if details .ReadOnly .Annotations [internalManifest .OCI1InstanceAnnotationZstdChunkedSentinel ] == internalManifest .OCI1InstanceAnnotationZstdChunkedSentinelValue {
537+ platformsWithSentinel .Add (platformV1ToPlatformComparable (details .ReadOnly .Platform ))
538+ }
539+ }
540+
541+ for _ , ci := range copiedInstances {
542+ // Only handle OCI manifests (zstd:chunked is OCI-only).
543+ if ci .result .manifestMIMEType != imgspecv1 .MediaTypeImageManifest {
544+ continue
545+ }
546+
547+ // Skip if this platform already has a sentinel variant.
548+ if platformsWithSentinel .Contains (platformV1ToPlatformComparable (ci .platform )) {
549+ continue
550+ }
551+
552+ ociMan , err := manifest .OCI1FromManifest (ci .result .manifest )
553+ if err != nil {
554+ logrus .Debugf ("Cannot parse manifest for sentinel variant: %v" , err )
555+ continue
556+ }
557+
558+ if ! hasZstdChunkedLayers (ociMan ) {
559+ continue
560+ }
561+
562+ // Get source config to read DiffIDs.
563+ srcDigest := ci .sourceDigest
564+ unparsed := image .UnparsedInstance (c .rawSource , & srcDigest )
565+ srcImage , err := image .FromUnparsedImage (ctx , nil , unparsed )
566+ if err != nil {
567+ logrus .Debugf ("Cannot read source image for sentinel variant: %v" , err )
568+ continue
569+ }
570+ ociConfig , err := srcImage .OCIConfig (ctx )
571+ if err != nil {
572+ logrus .Debugf ("Cannot get OCI config for sentinel variant: %v" , err )
573+ continue
574+ }
575+
576+ sentinelManifestBlob , sentinelManifestDigest , err := c .pushSentinelVariant (ctx , ociMan , ociConfig )
577+ if err != nil {
578+ return nil , err
579+ }
580+
581+ edits = append (edits , internalManifest.ListEdit {
582+ ListOperation : internalManifest .ListOpAdd ,
583+ AddDigest : sentinelManifestDigest ,
584+ AddSize : int64 (len (sentinelManifestBlob )),
585+ AddMediaType : imgspecv1 .MediaTypeImageManifest ,
586+ AddPlatform : ci .platform ,
587+ AddAnnotations : map [string ]string {
588+ internalManifest .OCI1InstanceAnnotationZstdChunkedSentinel : internalManifest .OCI1InstanceAnnotationZstdChunkedSentinelValue ,
589+ internalManifest .OCI1InstanceAnnotationCompressionZSTD : internalManifest .OCI1InstanceAnnotationCompressionZSTDValue ,
590+ },
591+ AddCompressionAlgorithms : ci .result .compressionAlgorithms ,
592+ })
593+
594+ platformsWithSentinel .Add (platformV1ToPlatformComparable (ci .platform ))
595+ }
596+
597+ return edits , nil
598+ }
599+
600+ // createSingleImageSentinelIndex checks if a single copied image has zstd:chunked
601+ // layers, and if so, creates a sentinel variant and wraps both in an OCI index.
602+ // Returns the serialized index manifest, or nil if no sentinel was needed.
603+ func (c * copier ) createSingleImageSentinelIndex (ctx context.Context , single copySingleImageResult ) ([]byte , error ) {
604+ if single .manifestMIMEType != imgspecv1 .MediaTypeImageManifest {
605+ return nil , nil
606+ }
607+
608+ ociMan , err := manifest .OCI1FromManifest (single .manifest )
609+ if err != nil {
610+ return nil , nil
611+ }
612+
613+ if ! hasZstdChunkedLayers (ociMan ) {
614+ return nil , nil
615+ }
616+
617+ // Get source config to read DiffIDs.
618+ srcImage , err := image .FromUnparsedImage (ctx , nil , c .unparsedToplevel )
619+ if err != nil {
620+ logrus .Debugf ("Cannot read source image for single-image sentinel: %v" , err )
621+ return nil , nil
622+ }
623+ ociConfig , err := srcImage .OCIConfig (ctx )
624+ if err != nil {
625+ logrus .Debugf ("Cannot get OCI config for single-image sentinel: %v" , err )
626+ return nil , nil
627+ }
628+
629+ sentinelManifestBlob , sentinelManifestDigest , err := c .pushSentinelVariant (ctx , ociMan , ociConfig )
630+ if err != nil {
631+ return nil , err
632+ }
633+
634+ // Extract platform from config.
635+ var platform * imgspecv1.Platform
636+ if ociConfig .OS != "" || ociConfig .Architecture != "" {
637+ platform = & imgspecv1.Platform {
638+ OS : ociConfig .OS ,
639+ Architecture : ociConfig .Architecture ,
640+ Variant : ociConfig .Variant ,
641+ OSVersion : ociConfig .OSVersion ,
642+ }
643+ }
644+
645+ // Build OCI index with: original manifest first, sentinel variant last.
646+ // Both entries get the zstd annotation so that old clients (which prefer
647+ // zstd over gzip) fall through to position-based selection and pick the
648+ // original at position 0 instead of the sentinel.
649+ index := manifest .OCI1IndexFromComponents ([]imgspecv1.Descriptor {
650+ {
651+ MediaType : imgspecv1 .MediaTypeImageManifest ,
652+ Digest : single .manifestDigest ,
653+ Size : int64 (len (single .manifest )),
654+ Platform : platform ,
655+ Annotations : map [string ]string {
656+ internalManifest .OCI1InstanceAnnotationCompressionZSTD : internalManifest .OCI1InstanceAnnotationCompressionZSTDValue ,
657+ },
658+ },
659+ {
660+ MediaType : imgspecv1 .MediaTypeImageManifest ,
661+ Digest : sentinelManifestDigest ,
662+ Size : int64 (len (sentinelManifestBlob )),
663+ Platform : platform ,
664+ Annotations : map [string ]string {
665+ internalManifest .OCI1InstanceAnnotationZstdChunkedSentinel : internalManifest .OCI1InstanceAnnotationZstdChunkedSentinelValue ,
666+ internalManifest .OCI1InstanceAnnotationCompressionZSTD : internalManifest .OCI1InstanceAnnotationCompressionZSTDValue ,
667+ },
668+ },
669+ }, nil )
670+ indexBlob , err := index .Serialize ()
671+ if err != nil {
672+ return nil , fmt .Errorf ("serializing sentinel index: %w" , err )
673+ }
674+
675+ if err := c .dest .PutManifest (ctx , indexBlob , nil ); err != nil {
676+ return nil , fmt .Errorf ("pushing sentinel index: %w" , err )
677+ }
678+
679+ return indexBlob , nil
680+ }
0 commit comments