@@ -10,9 +10,11 @@ import (
1010 "maps"
1111 "reflect"
1212 "slices"
13+ "sort"
1314 "strings"
1415 "sync"
1516
17+ tarpatch "github.com/containers/tar-diff/pkg/tar-patch"
1618 digest "github.com/opencontainers/go-digest"
1719 imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
1820 "github.com/sirupsen/logrus"
@@ -448,6 +450,11 @@ func (ic *imageCopier) copyLayers(ctx context.Context) ([]compressiontypes.Algor
448450 srcInfosUpdated = true
449451 }
450452
453+ deltaLayers , err := types .ImageDeltaLayers (ic .src , ctx )
454+ if err != nil {
455+ return nil , err
456+ }
457+
451458 type copyLayerData struct {
452459 destInfo types.BlobInfo
453460 diffID digest.Digest
@@ -466,7 +473,7 @@ func (ic *imageCopier) copyLayers(ctx context.Context) ([]compressiontypes.Algor
466473 copyGroup := sync.WaitGroup {}
467474
468475 data := make ([]copyLayerData , len (srcInfos ))
469- copyLayerHelper := func (index int , srcLayer types.BlobInfo , toEncrypt bool , pool * mpb.Progress , srcRef reference.Named ) {
476+ copyLayerHelper := func (index int , srcLayer types.BlobInfo , toEncrypt bool , pool * mpb.Progress , srcRef reference.Named , deltaLayers []types. BlobInfo ) {
470477 defer ic .c .concurrentBlobCopiesSemaphore .Release (1 )
471478 defer copyGroup .Done ()
472479 cld := copyLayerData {}
@@ -481,7 +488,7 @@ func (ic *imageCopier) copyLayers(ctx context.Context) ([]compressiontypes.Algor
481488 logrus .Debugf ("Skipping foreign layer %q copy to %s" , cld .destInfo .Digest , ic .c .dest .Reference ().Transport ().Name ())
482489 }
483490 } else {
484- cld .destInfo , cld .diffID , cld .err = ic .copyLayer (ctx , srcLayer , toEncrypt , pool , index , srcRef , manifestLayerInfos [index ].EmptyLayer )
491+ cld .destInfo , cld .diffID , cld .err = ic .copyLayer (ctx , index , srcLayer , toEncrypt , pool , srcRef , manifestLayerInfos [index ].EmptyLayer , deltaLayers )
485492 }
486493 data [index ] = cld
487494 }
@@ -521,7 +528,7 @@ func (ic *imageCopier) copyLayers(ctx context.Context) ([]compressiontypes.Algor
521528 return fmt .Errorf ("copying layer: %w" , err )
522529 }
523530 copyGroup .Add (1 )
524- go copyLayerHelper (i , srcLayer , layersToEncrypt .Contains (i ), progressPool , ic .c .rawSource .Reference ().DockerReference ())
531+ go copyLayerHelper (i , srcLayer , layersToEncrypt .Contains (i ), progressPool , ic .c .rawSource .Reference ().DockerReference (), deltaLayers )
525532 }
526533
527534 // A call to copyGroup.Wait() is done at this point by the defer above.
@@ -690,10 +697,85 @@ func compressionEditsFromBlobInfo(srcInfo types.BlobInfo) (types.LayerCompressio
690697 }
691698}
692699
700+ // getMatchingDeltaLayers gets all the deltas that apply to this layer
701+ func (ic * imageCopier ) getMatchingDeltaLayers (ctx context.Context , srcIndex int , deltaLayers []types.BlobInfo ) (digest.Digest , []* types.BlobInfo ) {
702+ if deltaLayers == nil {
703+ return "" , nil
704+ }
705+ config , _ := ic .src .OCIConfig (ctx )
706+ if config == nil || config .RootFS .DiffIDs == nil || len (config .RootFS .DiffIDs ) <= srcIndex {
707+ return "" , nil
708+ }
709+
710+ layerDiffID := config .RootFS .DiffIDs [srcIndex ]
711+
712+ var matchingLayers []* types.BlobInfo
713+ for i := range deltaLayers {
714+ deltaLayer := & deltaLayers [i ]
715+ to := deltaLayer .Annotations ["io.github.containers.delta.to" ]
716+ if to == layerDiffID .String () {
717+ matchingLayers = append (matchingLayers , deltaLayer )
718+ }
719+ }
720+
721+ return layerDiffID , matchingLayers
722+ }
723+
724+ // resolveDeltaLayer looks at which of the matching delta froms have locally available data and picks the best one
725+ func (ic * imageCopier ) resolveDeltaLayer (ctx context.Context , matchingDeltas []* types.BlobInfo ) (io.ReadCloser , tarpatch.DataSource , types.BlobInfo , error ) {
726+ // Sort smallest deltas so we favour the smallest useable one
727+ sort .Slice (matchingDeltas , func (i , j int ) bool {
728+ return matchingDeltas [i ].Size < matchingDeltas [j ].Size
729+ })
730+
731+ for i := range matchingDeltas {
732+ matchingDelta := matchingDeltas [i ]
733+ from := matchingDelta .Annotations ["io.github.containers.delta.from" ]
734+ fromDigest , err := digest .Parse (from )
735+ if err != nil {
736+ continue // Silently ignore if server specified a weird format
737+ }
738+
739+ dataSource , err := types .ImageDestinationGetLayerDeltaData (ic .c .dest , ctx , fromDigest )
740+ if err != nil {
741+ return nil , nil , types.BlobInfo {}, err // Internal error
742+ }
743+ if dataSource == nil {
744+ continue // from layer doesn't exist
745+ }
746+
747+ logrus .Debugf ("Using delta %v for DiffID %v" , matchingDelta .Digest , fromDigest )
748+
749+ deltaStream , _ , err := ic .c .rawSource .GetBlob (ctx , * matchingDelta , ic .c .blobInfoCache )
750+ if err != nil {
751+ return nil , nil , types.BlobInfo {}, fmt .Errorf ("reading delta blob %s: %w" , matchingDelta .Digest , err )
752+ }
753+ return deltaStream , dataSource , * matchingDelta , nil
754+ }
755+ return nil , nil , types.BlobInfo {}, nil
756+ }
757+
758+ // canUseDeltas checks if deltas can be used for this layer
759+ func (ic * imageCopier ) canUseDeltas (srcInfo types.BlobInfo ) (bool , string ) {
760+ // Deltas rewrite the manifest to refer to the uncompressed digest, so we must be able to substitute blobs
761+ if ! ic .canSubstituteBlobs {
762+ return false , ""
763+ }
764+
765+ switch srcInfo .MediaType {
766+ case manifest .DockerV2Schema2LayerMediaType , manifest .DockerV2SchemaLayerMediaTypeUncompressed :
767+ return true , manifest .DockerV2SchemaLayerMediaTypeUncompressed
768+ case imgspecv1 .MediaTypeImageLayer , imgspecv1 .MediaTypeImageLayerGzip , imgspecv1 .MediaTypeImageLayerZstd :
769+ return true , imgspecv1 .MediaTypeImageLayer
770+ }
771+
772+ return false , ""
773+ }
774+
693775// copyLayer copies a layer with srcInfo (with known Digest and Annotations and possibly known Size) in src to dest, perhaps (de/re/)compressing it,
694776// and returns a complete blobInfo of the copied layer, and a value for LayerDiffIDs if diffIDIsNeeded
695777// srcRef can be used as an additional hint to the destination during checking whether a layer can be reused but srcRef can be nil.
696- 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 ) {
778+ func (ic * imageCopier ) copyLayer (ctx context.Context , srcIndex int , srcInfo types.BlobInfo , toEncrypt bool , pool * mpb.Progress , srcRef reference.Named , emptyLayer bool , deltaLayers []types. BlobInfo ) (types.BlobInfo , digest.Digest , error ) {
697779 // If the srcInfo doesn't contain compression information, try to compute it from the
698780 // MediaType, which was either read from a manifest by way of LayerInfos() or constructed
699781 // by LayerInfosForCopy(), if it was supplied at all. If we succeed in copying the blob,
@@ -712,6 +794,58 @@ func (ic *imageCopier) copyLayer(ctx context.Context, srcInfo types.BlobInfo, to
712794
713795 ic .c .printCopyInfo ("blob" , srcInfo )
714796
797+ // First look for a delta that matches this layer and substitute the result of that
798+ if ok , deltaResultMediaType := ic .canUseDeltas (srcInfo ); ok {
799+ // Get deltas going TO this layer
800+ deltaDiffID , matchingDeltas := ic .getMatchingDeltaLayers (ctx , srcIndex , deltaLayers )
801+ // Get best possible FROM delta
802+ deltaStream , deltaDataSource , matchingDelta , err := ic .resolveDeltaLayer (ctx , matchingDeltas )
803+ if err != nil {
804+ return types.BlobInfo {}, "" , err
805+ }
806+ if deltaStream != nil {
807+ bar , err := ic .c .createProgressBar (pool , false , matchingDelta , "delta" , "done" )
808+ if err != nil {
809+ return types.BlobInfo {}, "" , err
810+ }
811+
812+ wrappedDeltaStream := bar .ProxyReader (deltaStream )
813+
814+ // Convert deltaStream to uncompressed tar layer stream
815+ pr , pw := io .Pipe ()
816+ go func () {
817+ if err := tarpatch .Apply (wrappedDeltaStream , deltaDataSource , pw ); err != nil {
818+ // We will notice this error when failing to verify the digest, so leave it be
819+ logrus .Infof ("Failed to apply layer delta: %v" , err )
820+ }
821+ deltaDataSource .Close ()
822+ deltaStream .Close ()
823+ wrappedDeltaStream .Close ()
824+ pw .Close ()
825+ }()
826+ defer pr .Close ()
827+
828+ // Copy uncompressed tar layer to destination, verifying the diffID
829+ blobInfo , diffIDChan , err := ic .copyLayerFromStream (ctx , pr , types.BlobInfo {Digest : deltaDiffID , Size : - 1 , MediaType : deltaResultMediaType , Annotations : srcInfo .Annotations }, true , toEncrypt , bar , srcIndex , emptyLayer )
830+ if err != nil {
831+ return types.BlobInfo {}, "" , err
832+ }
833+
834+ // Wait for diffID verification
835+ diffIDResult := <- diffIDChan
836+ if diffIDResult .err != nil {
837+ return types.BlobInfo {}, "" , diffIDResult .err
838+ }
839+
840+ bar .SetTotal (matchingDelta .Size , true )
841+
842+ // Record the fact that this blob is uncompressed
843+ ic .c .blobInfoCache .RecordDigestUncompressedPair (diffIDResult .digest , diffIDResult .digest )
844+
845+ return blobInfo , diffIDResult .digest , nil
846+ }
847+ }
848+
715849 diffIDIsNeeded := false
716850 var cachedDiffID digest.Digest = ""
717851 if ic .diffIDsAreNeeded {
@@ -751,7 +885,7 @@ func (ic *imageCopier) copyLayer(ctx context.Context, srcInfo types.BlobInfo, to
751885 Cache : ic .c .blobInfoCache ,
752886 CanSubstitute : canSubstitute ,
753887 EmptyLayer : emptyLayer ,
754- LayerIndex : & layerIndex ,
888+ LayerIndex : & srcIndex ,
755889 SrcRef : srcRef ,
756890 PossibleManifestFormats : append ([]string {ic .manifestConversionPlan .preferredMIMEType }, ic .manifestConversionPlan .otherMIMETypeCandidates ... ),
757891 RequiredCompression : requiredCompression ,
@@ -813,7 +947,7 @@ func (ic *imageCopier) copyLayer(ctx context.Context, srcInfo types.BlobInfo, to
813947 uploadedBlob , err := ic .c .dest .PutBlobPartial (ctx , & proxy , srcInfo , private.PutBlobPartialOptions {
814948 Cache : ic .c .blobInfoCache ,
815949 EmptyLayer : emptyLayer ,
816- LayerIndex : layerIndex ,
950+ LayerIndex : srcIndex ,
817951 })
818952 if err == nil {
819953 if srcInfo .Size != - 1 {
@@ -856,7 +990,7 @@ func (ic *imageCopier) copyLayer(ctx context.Context, srcInfo types.BlobInfo, to
856990 }
857991 defer srcStream .Close ()
858992
859- blobInfo , diffIDChan , err := ic .copyLayerFromStream (ctx , srcStream , types.BlobInfo {Digest : srcInfo .Digest , Size : srcBlobSize , MediaType : srcInfo .MediaType , Annotations : srcInfo .Annotations }, diffIDIsNeeded , toEncrypt , bar , layerIndex , emptyLayer )
993+ blobInfo , diffIDChan , err := ic .copyLayerFromStream (ctx , srcStream , types.BlobInfo {Digest : srcInfo .Digest , Size : srcBlobSize , MediaType : srcInfo .MediaType , Annotations : srcInfo .Annotations }, diffIDIsNeeded , toEncrypt , bar , srcIndex , emptyLayer )
860994 if err != nil {
861995 return types.BlobInfo {}, "" , err
862996 }
0 commit comments