1515package nydus
1616
1717import (
18+ "archive/tar"
1819 "bytes"
20+ "compress/gzip"
1921 "context"
22+ "encoding/json"
23+ "time"
2024
2125 "fmt"
2226
@@ -41,6 +45,7 @@ import (
4145 nydusutils "github.com/goharbor/acceleration-service/pkg/driver/nydus/utils"
4246 "github.com/goharbor/acceleration-service/pkg/errdefs"
4347 "github.com/goharbor/acceleration-service/pkg/utils"
48+ "github.com/opencontainers/go-digest"
4449 specs "github.com/opencontainers/image-spec/specs-go"
4550 ocispec "github.com/opencontainers/image-spec/specs-go/v1"
4651 "github.com/pkg/errors"
@@ -56,6 +61,9 @@ const (
5661 annotationFsVersion = "containerd.io/snapshot/nydus-fs-version"
5762 // annotationBuilderVersion indicates the nydus builder (nydus-image) version.
5863 annotationBuilderVersion = "containerd.io/snapshot/nydus-builder-version"
64+ // emptyTarGzipUnpackedDigest is the canonical sha256 digest of empty tar file (1024 NULL bytes).
65+ // Can be used as the diffID of an empty layer tar.gz layer.
66+ emptyTarGzipUnpackedDigest = digest .Digest ("sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" )
5967)
6068
6169var builderVersion string
@@ -344,6 +352,14 @@ func (d *Driver) makeManifestIndex(ctx context.Context, cs content.Store, oci, n
344352 if err != nil {
345353 return nil , errors .Wrap (err , "get oci image manifest list" )
346354 }
355+ for idx , desc := range ociDescs {
356+ // Modify initial OCI image to prevent layer reuse with non-nydus OCI images
357+ desc , err = PrependEmptyLayer (ctx , cs , desc )
358+ if err != nil {
359+ return nil , errors .Wrap (err , "prepend empty layer" )
360+ }
361+ ociDescs [idx ] = desc
362+ }
347363
348364 nydusDescs , err := utils .GetManifests (ctx , cs , nydus , d .platformMC )
349365 if err != nil {
@@ -426,3 +442,127 @@ func (d *Driver) getChunkDict(ctx context.Context, provider accelcontent.Provide
426442
427443 return & chunkDict , nil
428444}
445+
446+ // PrependEmptyLayer modifies the original image manifest and config to prepend an empty layer
447+ // This is done on purpose to force new shas for all the subsequent layers when unpacked by runtimes
448+ // So that no layer reuse can be possible between stock OCI images and nydus-converted OCI images
449+ // It returns the updated manifest descriptor
450+ func PrependEmptyLayer (ctx context.Context , cs content.Store , manifestDesc ocispec.Descriptor ) (ocispec.Descriptor , error ) {
451+ // Read existing OCI manifest
452+ manifestBytes , err := content .ReadBlob (ctx , cs , manifestDesc )
453+ if err != nil {
454+ return ocispec.Descriptor {}, errors .Wrap (err , "read manifest" )
455+ }
456+
457+ var manifest ocispec.Manifest
458+ if err := json .Unmarshal (manifestBytes , & manifest ); err != nil {
459+ return ocispec.Descriptor {}, errors .Wrap (err , "unmarshal manifest" )
460+ }
461+
462+ // Read existing OCI config
463+ configBytes , err := content .ReadBlob (ctx , cs , manifest .Config )
464+ if err != nil {
465+ return ocispec.Descriptor {}, errors .Wrap (err , "read config" )
466+ }
467+
468+ var config ocispec.Image
469+ if err := json .Unmarshal (configBytes , & config ); err != nil {
470+ return ocispec.Descriptor {}, errors .Wrap (err , "unmarshal config" )
471+ }
472+
473+ // Rebuild the layer list with an empty layer at the beginning
474+ // This will force new shas for all the subsequent layers
475+ var (
476+ emptyLayerMediaType string
477+ configDescriptorMediaType string
478+ )
479+
480+ switch manifest .MediaType {
481+ case ocispec .MediaTypeImageManifest :
482+ emptyLayerMediaType = ocispec .MediaTypeImageLayerGzip
483+ configDescriptorMediaType = ocispec .MediaTypeImageConfig
484+ case images .MediaTypeDockerSchema2Manifest , images .MediaTypeDockerSchema1Manifest :
485+ emptyLayerMediaType = images .MediaTypeDockerSchema2LayerGzip
486+ configDescriptorMediaType = images .MediaTypeDockerSchema2Config
487+ }
488+ emptyDescriptorBytes := generateDockerEmptyLayer ()
489+ emptyDescriptor := ocispec.Descriptor {
490+ MediaType : emptyLayerMediaType ,
491+ Digest : digest .FromBytes (emptyDescriptorBytes ),
492+ Size : int64 (len (emptyDescriptorBytes )),
493+ }
494+
495+ manifest .Layers = append ([]ocispec.Descriptor {emptyDescriptor }, manifest .Layers ... )
496+ if manifest .Annotations == nil {
497+ manifest .Annotations = map [string ]string {}
498+ }
499+ manifest .Annotations [annotationSourceDigest ] = manifestDesc .Digest .String ()
500+ // Add an empty diff_id at the beginning of the config
501+ config .RootFS .DiffIDs = append ([]digest.Digest {emptyTarGzipUnpackedDigest }, config .RootFS .DiffIDs ... )
502+ // Rewrite history to add an entry for the empty layer
503+ createdTime := time .Now ()
504+ emptyLayerHistory := ocispec.History {
505+ Created : & createdTime ,
506+ CreatedBy : "Nydus Converter" ,
507+ Comment : "Nydus Empty Layer" ,
508+ }
509+ config .History = append ([]ocispec.History {emptyLayerHistory }, config .History ... )
510+
511+ newConfigDesc , newConfigBytes , err := nydusutils .MarshalToDesc (config , configDescriptorMediaType )
512+ if err != nil {
513+ return ocispec.Descriptor {}, errors .Wrap (err , "marshal modified config" )
514+ }
515+ if newConfigDesc .Annotations == nil {
516+ newConfigDesc .Annotations = map [string ]string {}
517+ }
518+ newConfigDesc .Annotations [annotationSourceDigest ] = manifest .Config .Digest .String ()
519+
520+ manifest .Config = * newConfigDesc
521+ newManifestDesc , newManifestBytes , err := nydusutils .MarshalToDesc (manifest , manifest .MediaType )
522+ if err != nil {
523+ return ocispec.Descriptor {}, errors .Wrap (err , "marshal modified manifest" )
524+ }
525+ // Add back the original information of the manifest descriptor
526+ newManifestDesc .Platform = manifestDesc .Platform
527+ newManifestDesc .URLs = manifestDesc .URLs
528+ newManifestDesc .ArtifactType = manifestDesc .ArtifactType
529+ newManifestDesc .Annotations = manifestDesc .Annotations
530+
531+ if newManifestDesc .Annotations == nil {
532+ newManifestDesc .Annotations = map [string ]string {}
533+ }
534+ newManifestDesc .Annotations [annotationSourceDigest ] = manifestDesc .Digest .String ()
535+
536+ // Write modified config
537+ if err := content .WriteBlob (
538+ ctx , cs , newConfigDesc .Digest .String (), bytes .NewReader (newConfigBytes ), * newConfigDesc ,
539+ ); err != nil {
540+ return ocispec.Descriptor {}, errors .Wrap (err , "write modified config" )
541+ }
542+
543+ // Write empty blob
544+ if err := content .WriteBlob (
545+ ctx , cs , emptyDescriptor .Digest .String (), bytes .NewReader (emptyDescriptorBytes ), emptyDescriptor ,
546+ ); err != nil {
547+ return ocispec.Descriptor {}, errors .Wrap (err , "write empty json blob" )
548+ }
549+
550+ // Write modified manifest
551+ if err := content .WriteBlob (
552+ ctx , cs , newManifestDesc .Digest .String (), bytes .NewReader (newManifestBytes ), * newManifestDesc ,
553+ ); err != nil {
554+ return ocispec.Descriptor {}, errors .Wrap (err , "write modified manifest" )
555+ }
556+
557+ return * newManifestDesc , nil
558+ }
559+
560+ // Empty gzip-compressed tar file that can be used as an empty layer content
561+ func generateDockerEmptyLayer () []byte {
562+ var buf bytes.Buffer
563+ gzw := gzip .NewWriter (& buf )
564+ tw := tar .NewWriter (gzw )
565+ tw .Close ()
566+ gzw .Close ()
567+ return buf .Bytes ()
568+ }
0 commit comments