Skip to content

Commit 7413411

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 7413411

File tree

2 files changed

+450
-0
lines changed

2 files changed

+450
-0
lines changed

pkg/driver/nydus/nydus.go

Lines changed: 140 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,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

Comments
 (0)