15
15
package nydus
16
16
17
17
import (
18
+ "archive/tar"
18
19
"bytes"
20
+ "compress/gzip"
19
21
"context"
22
+ "encoding/json"
23
+ "time"
20
24
21
25
"fmt"
22
26
@@ -41,6 +45,7 @@ import (
41
45
nydusutils "github.com/goharbor/acceleration-service/pkg/driver/nydus/utils"
42
46
"github.com/goharbor/acceleration-service/pkg/errdefs"
43
47
"github.com/goharbor/acceleration-service/pkg/utils"
48
+ "github.com/opencontainers/go-digest"
44
49
specs "github.com/opencontainers/image-spec/specs-go"
45
50
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
46
51
"github.com/pkg/errors"
@@ -56,6 +61,9 @@ const (
56
61
annotationFsVersion = "containerd.io/snapshot/nydus-fs-version"
57
62
// annotationBuilderVersion indicates the nydus builder (nydus-image) version.
58
63
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" )
59
67
)
60
68
61
69
var builderVersion string
@@ -344,6 +352,11 @@ func (d *Driver) makeManifestIndex(ctx context.Context, cs content.Store, oci, n
344
352
if err != nil {
345
353
return nil , errors .Wrap (err , "get oci image manifest list" )
346
354
}
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
+ ociDescs [idx ] = desc
359
+ }
347
360
348
361
nydusDescs , err := utils .GetManifests (ctx , cs , nydus , d .platformMC )
349
362
if err != nil {
@@ -426,3 +439,127 @@ func (d *Driver) getChunkDict(ctx context.Context, provider accelcontent.Provide
426
439
427
440
return & chunkDict , nil
428
441
}
442
+
443
+ // PrependEmptyLayer modifies the original image manifest and config to prepend an empty layer
444
+ // This is done on purpose to force new shas for all the subsequent layers when unpacked by runtimes
445
+ // So that no layer reuse can be possible between stock OCI images and nydus-converted OCI images
446
+ // It returns the updated manifest descriptor
447
+ func PrependEmptyLayer (ctx context.Context , cs content.Store , manifestDesc ocispec.Descriptor ) (ocispec.Descriptor , error ) {
448
+ // Read existing OCI manifest
449
+ manifestBytes , err := content .ReadBlob (ctx , cs , manifestDesc )
450
+ if err != nil {
451
+ return ocispec.Descriptor {}, errors .Wrap (err , "read manifest" )
452
+ }
453
+
454
+ var manifest ocispec.Manifest
455
+ if err := json .Unmarshal (manifestBytes , & manifest ); err != nil {
456
+ return ocispec.Descriptor {}, errors .Wrap (err , "unmarshal manifest" )
457
+ }
458
+
459
+ // Read existing OCI config
460
+ configBytes , err := content .ReadBlob (ctx , cs , manifest .Config )
461
+ if err != nil {
462
+ return ocispec.Descriptor {}, errors .Wrap (err , "read config" )
463
+ }
464
+
465
+ var config ocispec.Image
466
+ if err := json .Unmarshal (configBytes , & config ); err != nil {
467
+ return ocispec.Descriptor {}, errors .Wrap (err , "unmarshal config" )
468
+ }
469
+
470
+ // Rebuild the layer list with an empty layer at the beginning
471
+ // This will force new shas for all the subsequent layers
472
+ var (
473
+ emptyLayerMediaType string
474
+ configDescriptorMediaType string
475
+ )
476
+
477
+ switch manifest .MediaType {
478
+ case ocispec .MediaTypeImageManifest :
479
+ emptyLayerMediaType = ocispec .MediaTypeImageLayerGzip
480
+ configDescriptorMediaType = ocispec .MediaTypeImageConfig
481
+ case images .MediaTypeDockerSchema2Manifest , images .MediaTypeDockerSchema1Manifest :
482
+ emptyLayerMediaType = images .MediaTypeDockerSchema2LayerGzip
483
+ configDescriptorMediaType = images .MediaTypeDockerSchema2Config
484
+ }
485
+ emptyDescriptorBytes := generateDockerEmptyLayer ()
486
+ emptyDescriptor := ocispec.Descriptor {
487
+ MediaType : emptyLayerMediaType ,
488
+ Digest : digest .FromBytes (emptyDescriptorBytes ),
489
+ Size : int64 (len (emptyDescriptorBytes )),
490
+ }
491
+
492
+ manifest .Layers = append ([]ocispec.Descriptor {emptyDescriptor }, manifest .Layers ... )
493
+ if manifest .Annotations == nil {
494
+ manifest .Annotations = map [string ]string {}
495
+ }
496
+ manifest .Annotations [annotationSourceDigest ] = manifestDesc .Digest .String ()
497
+ // Add an empty diff_id at the beginning of the config
498
+ config .RootFS .DiffIDs = append ([]digest.Digest {emptyTarGzipUnpackedDigest }, config .RootFS .DiffIDs ... )
499
+ // Rewrite history to add an entry for the empty layer
500
+ createdTime := time .Now ()
501
+ emptyLayerHistory := ocispec.History {
502
+ Created : & createdTime ,
503
+ CreatedBy : "Nydus Converter" ,
504
+ Comment : "Nydus Empty Layer" ,
505
+ }
506
+ config .History = append ([]ocispec.History {emptyLayerHistory }, config .History ... )
507
+
508
+ newConfigDesc , newConfigBytes , err := nydusutils .MarshalToDesc (config , configDescriptorMediaType )
509
+ if err != nil {
510
+ return ocispec.Descriptor {}, errors .Wrap (err , "marshal modified config" )
511
+ }
512
+ if newConfigDesc .Annotations == nil {
513
+ newConfigDesc .Annotations = map [string ]string {}
514
+ }
515
+ newConfigDesc .Annotations [annotationSourceDigest ] = manifest .Config .Digest .String ()
516
+
517
+ manifest .Config = * newConfigDesc
518
+ newManifestDesc , newManifestBytes , err := nydusutils .MarshalToDesc (manifest , manifest .MediaType )
519
+ if err != nil {
520
+ return ocispec.Descriptor {}, errors .Wrap (err , "marshal modified manifest" )
521
+ }
522
+ // Add back the original information of the manifest descriptor
523
+ newManifestDesc .Platform = manifestDesc .Platform
524
+ newManifestDesc .URLs = manifestDesc .URLs
525
+ newManifestDesc .ArtifactType = manifestDesc .ArtifactType
526
+ newManifestDesc .Annotations = manifestDesc .Annotations
527
+
528
+ if newManifestDesc .Annotations == nil {
529
+ newManifestDesc .Annotations = map [string ]string {}
530
+ }
531
+ newManifestDesc .Annotations [annotationSourceDigest ] = manifestDesc .Digest .String ()
532
+
533
+ // Write modified config
534
+ if err := content .WriteBlob (
535
+ ctx , cs , newConfigDesc .Digest .String (), bytes .NewReader (newConfigBytes ), * newConfigDesc ,
536
+ ); err != nil {
537
+ return ocispec.Descriptor {}, errors .Wrap (err , "write modified config" )
538
+ }
539
+
540
+ // Write empty blob
541
+ if err := content .WriteBlob (
542
+ ctx , cs , emptyDescriptor .Digest .String (), bytes .NewReader (emptyDescriptorBytes ), emptyDescriptor ,
543
+ ); err != nil {
544
+ return ocispec.Descriptor {}, errors .Wrap (err , "write empty json blob" )
545
+ }
546
+
547
+ // Write modified manifest
548
+ if err := content .WriteBlob (
549
+ ctx , cs , newManifestDesc .Digest .String (), bytes .NewReader (newManifestBytes ), * newManifestDesc ,
550
+ ); err != nil {
551
+ return ocispec.Descriptor {}, errors .Wrap (err , "write modified manifest" )
552
+ }
553
+
554
+ return * newManifestDesc , nil
555
+ }
556
+
557
+ // Empty gzip-compressed tar file that can be used as an empty layer content
558
+ func generateDockerEmptyLayer () []byte {
559
+ var buf bytes.Buffer
560
+ gzw := gzip .NewWriter (& buf )
561
+ tw := tar .NewWriter (gzw )
562
+ tw .Close ()
563
+ gzw .Close ()
564
+ return buf .Bytes ()
565
+ }
0 commit comments