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,14 @@ 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
+ if err != nil {
359
+ return nil , errors .Wrap (err , "prepend empty layer" )
360
+ }
361
+ ociDescs [idx ] = desc
362
+ }
347
363
348
364
nydusDescs , err := utils .GetManifests (ctx , cs , nydus , d .platformMC )
349
365
if err != nil {
@@ -426,3 +442,127 @@ func (d *Driver) getChunkDict(ctx context.Context, provider accelcontent.Provide
426
442
427
443
return & chunkDict , nil
428
444
}
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