Skip to content

Commit 9ad6d04

Browse files
committed
[nydus] Add layer in OCI image when merging platforms
Add an empty tar.gz layer at the beginning of the OCI image that's referrenced in the nydus merged-manifest compared to the original OCI one. This empty layer, being the first layer, will force the OCI image to have a completely different chainID from its original counterpart. This is done so that containerd and other runtimes don't try to reuse the OCI layers for other images that share layers with the original OCI variant. The drawback being that nydus-merged images can't share layers with pure OCI images when they are pulled on non-nydus clients. The choice to use an empty tar.gz layer as the new first layer is because: - this is a format understood by every runtime (on the other hand, docker doesn't understand layers that use `application/vnd.oci.empty.v1+json`) - an empty layer won't change the unpacked digest of the final image - it shouldn't be possible for a regular image (not built using nydus merge-manifest) to start wtih an empty tar.gzip. That's because they would need to be based on `scratch` and any layer they would add will necessary change something on the filesystem so it won't be an empty change Signed-off-by: Baptiste Girard-Carrabin <baptiste.girardcarrabin@datadoghq.com>
1 parent 91fc394 commit 9ad6d04

File tree

2 files changed

+447
-0
lines changed

2 files changed

+447
-0
lines changed

pkg/driver/nydus/nydus.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@
1515
package nydus
1616

1717
import (
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

6169
var builderVersion string
@@ -344,6 +352,11 @@ 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+
ociDescs[idx] = desc
359+
}
347360

348361
nydusDescs, err := utils.GetManifests(ctx, cs, nydus, d.platformMC)
349362
if err != nil {
@@ -426,3 +439,127 @@ func (d *Driver) getChunkDict(ctx context.Context, provider accelcontent.Provide
426439

427440
return &chunkDict, nil
428441
}
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

Comments
 (0)