From 6b13369c78f6b19b4a0be30b996156fe94731d1d Mon Sep 17 00:00:00 2001 From: Xing Ma Date: Fri, 29 Nov 2024 16:57:25 +0800 Subject: [PATCH] nydusify: introduce optimize subcommand of nydusify We can statically analyze the image entrypoint dependency, or use runtime dynamic analysis technologies such as ebpf, fanotify, metric, etc. to obtain the container file access pattern, and then build this part of data into an independent image layer: * preferentially fetch blob during the image startup phase to reduce network and disk IO. * avoid frequent image builds, allows for better local cache utilization. Implement optimize subcommand of nydusify to generate a new image, which references a new blob included prefetch file chunks. ``` nydusify optimize --policy separated-prefetch-blob \ --source $existed-nydus-image \ --target $new-nydus-image \ --prefetch-files /path/to/prefetch-files ``` More detailed process is as follows: 1. nydusify first downloads the source image and bootstrap, utilize nydus-image to output a new bootstrap along with an independent prefetchblob; 2. nydusify generate&push new meta layer including new bootstrap and the prefetch-files , also generates&push new manifest/config/prefetchblob, completing the incremental image build. Signed-off-by: Xing Ma --- contrib/nydusify/cmd/nydusify.go | 93 ++++ contrib/nydusify/pkg/optimizer/builder.go | 87 ++++ contrib/nydusify/pkg/optimizer/optimizer.go | 537 ++++++++++++++++++++ contrib/nydusify/pkg/utils/constant.go | 3 +- 4 files changed, 719 insertions(+), 1 deletion(-) create mode 100644 contrib/nydusify/pkg/optimizer/builder.go create mode 100644 contrib/nydusify/pkg/optimizer/optimizer.go diff --git a/contrib/nydusify/cmd/nydusify.go b/contrib/nydusify/cmd/nydusify.go index 82d597778ea..109dd65c4ff 100644 --- a/contrib/nydusify/cmd/nydusify.go +++ b/contrib/nydusify/cmd/nydusify.go @@ -16,6 +16,8 @@ import ( "runtime" "strings" + "github.com/dragonflyoss/nydus/contrib/nydusify/pkg/optimizer" + "github.com/containerd/containerd/reference/docker" "github.com/distribution/reference" "github.com/dustin/go-humanize" @@ -1160,6 +1162,97 @@ func main() { return copier.Copy(context.Background(), opt) }, }, + { + Name: "optimize", + Usage: "Optimize a source nydus image and push to the target", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "source", + Required: true, + Usage: "Source (Nydus) image reference", + EnvVars: []string{"SOURCE"}, + }, + &cli.StringFlag{ + Name: "target", + Required: true, + Usage: "Target (Nydus) image reference", + EnvVars: []string{"TARGET"}, + }, + &cli.BoolFlag{ + Name: "source-insecure", + Required: false, + Usage: "Skip verifying server certs for HTTPS source registry", + EnvVars: []string{"SOURCE_INSECURE"}, + }, + &cli.BoolFlag{ + Name: "target-insecure", + Required: false, + Usage: "Skip verifying server certs for HTTPS target registry", + EnvVars: []string{"TARGET_INSECURE"}, + }, + + &cli.StringFlag{ + Name: "policy", + Value: "separated-blob-with-prefetch-files", + Usage: "Specify the optimizing way", + EnvVars: []string{"OPTIMIZE_POLICY"}, + }, + &cli.StringFlag{ + Name: "prefetch-files", + Required: false, + Usage: "File path to include prefetch files for optimization", + EnvVars: []string{"PREFETCH_FILES"}, + }, + + &cli.StringFlag{ + Name: "work-dir", + Value: "./tmp", + Usage: "Working directory for image optimization", + EnvVars: []string{"WORK_DIR"}, + }, + + &cli.StringFlag{ + Name: "nydus-image", + Value: "nydus-image", + Usage: "Path to the nydus-image binary, default to search in PATH", + EnvVars: []string{"NYDUS_IMAGE"}, + }, + + &cli.StringFlag{ + Name: "push-chunk-size", + Value: "0MB", + Usage: "Chunk size for pushing a blob layer in chunked", + }, + }, + Action: func(c *cli.Context) error { + setupLogLevel(c) + + pushChunkSize, err := humanize.ParseBytes(c.String("push-chunk-size")) + if err != nil { + return errors.Wrap(err, "invalid --push-chunk-size option") + } + if pushChunkSize > 0 { + logrus.Infof("will push layer with chunk size %s", c.String("push-chunk-size")) + } + opt := optimizer.Opt{ + WorkDir: c.String("work-dir"), + NydusImagePath: c.String("nydus-image"), + + Source: c.String("source"), + Target: c.String("target"), + SourceInsecure: c.Bool("source-insecure"), + TargetInsecure: c.Bool("target-insecure"), + + AllPlatforms: c.Bool("all-platforms"), + Platforms: c.String("platform"), + + PushChunkSize: int64(pushChunkSize), + PrefetchFilesPath: c.String("prefetch-files"), + } + + return optimizer.Optimize(context.Background(), opt) + }, + }, { Name: "commit", Usage: "Create and push a new nydus image from a container's changes that use a nydus image", diff --git a/contrib/nydusify/pkg/optimizer/builder.go b/contrib/nydusify/pkg/optimizer/builder.go new file mode 100644 index 00000000000..66f26ac23d5 --- /dev/null +++ b/contrib/nydusify/pkg/optimizer/builder.go @@ -0,0 +1,87 @@ +package optimizer + +import ( + "context" + "encoding/json" + "os" + "os/exec" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +var logger = logrus.WithField("module", "optimizer") + +func isSignalKilled(err error) bool { + return strings.Contains(err.Error(), "signal: killed") +} + +type BuildOption struct { + BuilderPath string + PrefetchFilesPath string + BootstrapPath string + BlobDir string + OutputBootstrapPath string + OutputJSONPath string + Timeout *time.Duration +} + +type outputJSON struct { + Blobs []string `json:"blobs"` +} + +func Build(option BuildOption) (string, error) { + outputJSONPath := option.OutputJSONPath + args := []string{ + "optimize", + "--log-level", + "warn", + "--prefetch-files", + option.PrefetchFilesPath, + "--bootstrap", + option.BootstrapPath, + "--blob-dir", + option.BlobDir, + "--output-bootstrap", + option.OutputBootstrapPath, + "--output-json", + outputJSONPath, + } + + ctx := context.Background() + var cancel context.CancelFunc + if option.Timeout != nil { + ctx, cancel = context.WithTimeout(ctx, *option.Timeout) + defer cancel() + } + logrus.Debugf("\tCommand: %s %s", option.BuilderPath, strings.Join(args, " ")) + + cmd := exec.CommandContext(ctx, option.BuilderPath, args...) + cmd.Stdout = logger.Writer() + cmd.Stderr = logger.Writer() + + if err := cmd.Run(); err != nil { + if isSignalKilled(err) && option.Timeout != nil { + logrus.WithError(err).Errorf("fail to run %v %+v, possibly due to timeout %v", option.BuilderPath, args, *option.Timeout) + } else { + logrus.WithError(err).Errorf("fail to run %v %+v", option.BuilderPath, args) + } + return "", errors.Wrap(err, "run merge command") + } + + outputBytes, err := os.ReadFile(outputJSONPath) + if err != nil { + return "", errors.Wrapf(err, "read file %s", outputJSONPath) + } + var output outputJSON + err = json.Unmarshal(outputBytes, &output) + if err != nil { + return "", errors.Wrapf(err, "unmarshal output json file %s", outputJSONPath) + } + blobID := output.Blobs[len(output.Blobs)-1] + + logrus.Infof("build success for prefetch blob : %s", blobID) + return blobID, nil +} diff --git a/contrib/nydusify/pkg/optimizer/optimizer.go b/contrib/nydusify/pkg/optimizer/optimizer.go new file mode 100644 index 00000000000..84d35614e5f --- /dev/null +++ b/contrib/nydusify/pkg/optimizer/optimizer.go @@ -0,0 +1,537 @@ +// Copyright 2024 Nydus Developers. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +package optimizer + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "time" + + "github.com/goharbor/acceleration-service/pkg/platformutil" + + "github.com/containerd/containerd/content/local" + "github.com/containerd/containerd/reference/docker" + + "github.com/containerd/containerd/content" + "github.com/containerd/containerd/namespaces" + "github.com/dragonflyoss/nydus/contrib/nydusify/pkg/committer" + converterpvd "github.com/dragonflyoss/nydus/contrib/nydusify/pkg/converter/provider" + "github.com/dragonflyoss/nydus/contrib/nydusify/pkg/parser" + "github.com/dragonflyoss/nydus/contrib/nydusify/pkg/provider" + "github.com/dragonflyoss/nydus/contrib/nydusify/pkg/remote" + "github.com/dragonflyoss/nydus/contrib/nydusify/pkg/utils" + accerr "github.com/goharbor/acceleration-service/pkg/errdefs" + accremote "github.com/goharbor/acceleration-service/pkg/remote" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +const ( + EntryBootstrap = "image.boot" + EntryPrefetchFiles = "prefetch.files" +) + +type Opt struct { + WorkDir string + NydusImagePath string + + Source string + Target string + + SourceInsecure bool + TargetInsecure bool + + OptimizePolicy string + PrefetchFilesPath string + + AllPlatforms bool + Platforms string + + PushChunkSize int64 +} + +// the information generated during building +type BuildInfo struct { + SourceImage parser.Image + BuildDir string + BlobDir string + PrefetchBlobID string + NewBootstrapPath string +} + +type File struct { + Name string + Reader io.Reader + Size int64 +} + +type bootstrapInfo struct { + bootstrapDesc ocispec.Descriptor + bootstrapDiffID digest.Digest +} + +func hosts(opt Opt) accremote.HostFunc { + maps := map[string]bool{ + opt.Source: opt.SourceInsecure, + opt.Target: opt.TargetInsecure, + } + return func(ref string) (accremote.CredentialFunc, bool, error) { + return accremote.NewDockerConfigCredFunc(), maps[ref], nil + } +} + +func remoter(opt Opt) (*remote.Remote, error) { + targetRef, err := committer.ValidateRef(opt.Target) + if err != nil { + return nil, errors.Wrap(err, "validate target reference") + } + remoter, err := provider.DefaultRemote(targetRef, opt.TargetInsecure) + if err != nil { + return nil, errors.Wrap(err, "create remote") + } + return remoter, nil +} + +func makeDesc(x interface{}, oldDesc ocispec.Descriptor) ([]byte, *ocispec.Descriptor, error) { + data, err := json.MarshalIndent(x, "", " ") + if err != nil { + return nil, nil, errors.Wrap(err, "json marshal") + } + dgst := digest.SHA256.FromBytes(data) + + newDesc := oldDesc + newDesc.Size = int64(len(data)) + newDesc.Digest = dgst + + return data, &newDesc, nil +} + +// packToTar packs files to .tar(.gz) stream then return reader. +// +// ported from https://github.com/containerd/nydus-snapshotter/blob/5f948e4498151b51c742d2ee0b3f7b96f86a26f7/pkg/converter/utils.go#L92 +func packToTar(files []File, compress bool) io.ReadCloser { + dirHdr := &tar.Header{ + Name: "image", + Mode: 0755, + Typeflag: tar.TypeDir, + } + + pr, pw := io.Pipe() + + go func() { + // Prepare targz writer + var tw *tar.Writer + var gw *gzip.Writer + var err error + + if compress { + gw = gzip.NewWriter(pw) + tw = tar.NewWriter(gw) + } else { + tw = tar.NewWriter(pw) + } + + defer func() { + err1 := tw.Close() + var err2 error + if gw != nil { + err2 = gw.Close() + } + + var finalErr error + + // Return the first error encountered to the other end and ignore others. + switch { + case err != nil: + finalErr = err + case err1 != nil: + finalErr = err1 + case err2 != nil: + finalErr = err2 + } + + pw.CloseWithError(finalErr) + }() + + // Write targz stream + if err = tw.WriteHeader(dirHdr); err != nil { + return + } + + for _, file := range files { + hdr := tar.Header{ + Name: filepath.Join("image", file.Name), + Mode: 0444, + Size: file.Size, + } + if err = tw.WriteHeader(&hdr); err != nil { + return + } + if _, err = io.Copy(tw, file.Reader); err != nil { + return + } + } + }() + + return pr +} + +func getOriginalBlobLayers(nydusImage parser.Image) []ocispec.Descriptor { + originalBlobLayers := []ocispec.Descriptor{} + for idx := range nydusImage.Manifest.Layers { + layer := nydusImage.Manifest.Layers[idx] + if layer.MediaType == utils.MediaTypeNydusBlob { + originalBlobLayers = append(originalBlobLayers, layer) + } + } + return originalBlobLayers +} + +func fetchBlobs(ctx context.Context, opt Opt, buildDir string) error { + logrus.Infof("pulling source image") + start := time.Now() + platformMC, err := platformutil.ParsePlatforms(opt.AllPlatforms, opt.Platforms) + if err != nil { + return err + } + pvd, err := converterpvd.New(buildDir, hosts(opt), 200, "v1", platformMC, opt.PushChunkSize) + if err != nil { + return err + } + + sourceNamed, err := docker.ParseDockerRef(opt.Source) + if err != nil { + return errors.Wrap(err, "parse source reference") + } + source := sourceNamed.String() + + if err := pvd.Pull(ctx, source); err != nil { + if accerr.NeedsRetryWithHTTP(err) { + pvd.UsePlainHTTP() + if err := pvd.Pull(ctx, source); err != nil { + return errors.Wrap(err, "try to pull image") + } + } else { + return errors.Wrap(err, "pull source image") + } + } + logrus.Infof("pulled source image, elapsed: %s", time.Since(start)) + return nil +} + +// Optimize coverts and push a new optimized nydus image +func Optimize(ctx context.Context, opt Opt) error { + ctx = namespaces.WithNamespace(ctx, "nydusify") + + sourceRemote, err := provider.DefaultRemote(opt.Source, opt.SourceInsecure) + if err != nil { + return errors.Wrap(err, "Init source image parser") + } + sourceParser, err := parser.New(sourceRemote, runtime.GOARCH) + if err != nil { + return errors.Wrap(err, "failed to create parser") + } + + sourceParsed, err := sourceParser.Parse(ctx) + if err != nil { + return errors.Wrap(err, "parse source image") + } + sourceNydusImage := sourceParsed.NydusImage + + if _, err := os.Stat(opt.WorkDir); err != nil { + if errors.Is(err, os.ErrNotExist) { + if err := os.MkdirAll(opt.WorkDir, 0755); err != nil { + return errors.Wrap(err, "prepare work directory") + } + // We should only clean up when the work directory not exists + // before, otherwise it may delete user data by mistake. + defer os.RemoveAll(opt.WorkDir) + } else { + return errors.Wrap(err, "stat work directory") + } + } + buildDir, err := os.MkdirTemp(opt.WorkDir, "nydusify-") + if err != nil { + return errors.Wrap(err, "create temp directory") + } + defer os.RemoveAll(buildDir) + + if err := fetchBlobs(ctx, opt, buildDir); err != nil { + return errors.Wrap(err, "prepare nydus blobs") + } + + originalBootstrap := filepath.Join(buildDir, "nydus_bootstrap") + bootstrapDesc := parser.FindNydusBootstrapDesc(&sourceNydusImage.Manifest) + if bootstrapDesc == nil { + return fmt.Errorf("not found Nydus bootstrap layer in manifest") + } + bootstrapReader, err := sourceParser.Remote.Pull(ctx, *bootstrapDesc, true) + if err != nil { + return errors.Wrap(err, "pull Nydus originalBootstrap layer") + } + defer bootstrapReader.Close() + if err := utils.UnpackFile(bootstrapReader, utils.BootstrapFileNameInLayer, originalBootstrap); err != nil { + return errors.Wrap(err, "unpack Nydus originalBootstrap layer") + } + + compressAlgo := bootstrapDesc.Digest.Algorithm().String() + blobDir := filepath.Join(buildDir + "/content/blobs/" + compressAlgo) + outPutJSONPath := filepath.Join(buildDir, "output.json") + newBootstrapPath := filepath.Join(buildDir, "optimized_bootstrap") + builderOpt := BuildOption{ + BuilderPath: opt.NydusImagePath, + PrefetchFilesPath: opt.PrefetchFilesPath, + BootstrapPath: originalBootstrap, + BlobDir: blobDir, + OutputBootstrapPath: newBootstrapPath, + OutputJSONPath: outPutJSONPath, + } + logrus.Infof("begin to build new prefetch blob and bootstrap") + start := time.Now() + prefetchBlobID, err := Build(builderOpt) + if err != nil { + return errors.Wrap(err, "optimize nydus image") + } + logrus.Infof("builded new prefetch blob and bootstrap, elapsed: %s", time.Since(start)) + + buildInfo := BuildInfo{ + SourceImage: *sourceParsed.NydusImage, + BuildDir: buildDir, + BlobDir: blobDir, + PrefetchBlobID: prefetchBlobID, + NewBootstrapPath: newBootstrapPath, + } + + if err := pushNewImage(ctx, opt, buildInfo); err != nil { + return errors.Wrap(err, "push new image") + } + return nil +} + +// push blob +func pushBlob(ctx context.Context, opt Opt, buildInfo BuildInfo) (*ocispec.Descriptor, error) { + blobDir := buildInfo.BlobDir + blobID := buildInfo.PrefetchBlobID + remoter, err := remoter(opt) + if err != nil { + return nil, errors.Wrap(err, "create remote") + } + + blobRa, err := local.OpenReader(filepath.Join(blobDir, blobID)) + if err != nil { + return nil, errors.Wrap(err, "open reader for upper blob") + } + + blobDigest := digest.NewDigestFromEncoded(digest.SHA256, blobID) + blobDesc := ocispec.Descriptor{ + Digest: blobDigest, + Size: blobRa.Size(), + MediaType: utils.MediaTypeNydusBlob, + Annotations: map[string]string{ + utils.LayerAnnotationNydusBlob: "true", + }, + } + + if err := remoter.Push(ctx, blobDesc, true, io.NewSectionReader(blobRa, 0, blobRa.Size())); err != nil { + if utils.RetryWithHTTP(err) { + remoter.MaybeWithHTTP(err) + if err := remoter.Push(ctx, blobDesc, true, io.NewSectionReader(blobRa, 0, blobRa.Size())); err != nil { + return nil, errors.Wrap(err, "push blob") + } + } else { + return nil, errors.Wrap(err, "push blob") + } + } + return &blobDesc, nil +} + +func pushNewBootstrap(ctx context.Context, opt Opt, buildInfo BuildInfo) (*bootstrapInfo, error) { + remoter, err := remoter(opt) + if err != nil { + return nil, errors.Wrap(err, "create remote") + } + bootstrapRa, err := local.OpenReader(buildInfo.NewBootstrapPath) + if err != nil { + return nil, errors.Wrap(err, "open reader for bootstrap") + } + prefetchfilesRa, err := local.OpenReader(opt.PrefetchFilesPath) + if err != nil { + return nil, errors.Wrap(err, "open reader for prefetch files") + } + files := []File{ + { + Name: EntryBootstrap, + Reader: content.NewReader(bootstrapRa), + Size: bootstrapRa.Size(), + }, { + Name: EntryPrefetchFiles, + Reader: content.NewReader(prefetchfilesRa), + Size: prefetchfilesRa.Size(), + }, + } + rc := packToTar(files, false) + defer rc.Close() + + bootstrapTarPath := filepath.Join(buildInfo.BuildDir, "bootstrap.tar") + bootstrapTar, err := os.Create(bootstrapTarPath) + if err != nil { + return nil, errors.Wrap(err, "create bootstrap tar file") + } + defer bootstrapTar.Close() + + tarDigester := digest.SHA256.Digester() + if _, err := io.Copy(io.MultiWriter(bootstrapTar, tarDigester.Hash()), rc); err != nil { + return nil, errors.Wrap(err, "get tar digest") + } + bootstrapDiffID := tarDigester.Digest() + + bootstrapTarRa, err := os.Open(bootstrapTarPath) + if err != nil { + return nil, errors.Wrap(err, "open bootstrap tar file") + } + defer bootstrapTarRa.Close() + + bootstrapTarGzPath := filepath.Join(buildInfo.BuildDir, "bootstrap.tar.gz") + bootstrapTarGz, err := os.Create(bootstrapTarGzPath) + if err != nil { + return nil, errors.Wrap(err, "create bootstrap tar.gz file") + } + defer bootstrapTarGz.Close() + gzDigester := digest.SHA256.Digester() + gzWriter := gzip.NewWriter(io.MultiWriter(bootstrapTarGz, gzDigester.Hash())) + if _, err := io.Copy(gzWriter, bootstrapTarRa); err != nil { + return nil, errors.Wrap(err, "compress bootstrap & prefetchfiles to tar.gz") + } + if err := gzWriter.Close(); err != nil { + return nil, errors.Wrap(err, "close gzip writer") + } + + bootstrapTarGzRa, err := local.OpenReader(bootstrapTarGzPath) + if err != nil { + return nil, errors.Wrap(err, "open reader for upper blob") + } + defer bootstrapTarGzRa.Close() + + oldBootstrapDesc := parser.FindNydusBootstrapDesc(&buildInfo.SourceImage.Manifest) + if oldBootstrapDesc == nil { + return nil, fmt.Errorf("not found originial Nydus bootstrap layer in manifest") + } + + annotations := oldBootstrapDesc.Annotations + annotations[utils.LayerAnnotationNyudsPrefetchBlob] = buildInfo.PrefetchBlobID + + // push bootstrap + bootstrapDesc := ocispec.Descriptor{ + Digest: gzDigester.Digest(), + Size: bootstrapTarGzRa.Size(), + MediaType: ocispec.MediaTypeImageLayerGzip, + Annotations: annotations, + } + + bootstrapRc, err := os.Open(bootstrapTarGzPath) + if err != nil { + return nil, errors.Wrapf(err, "open bootstrap %s", bootstrapTarGzPath) + } + defer bootstrapRc.Close() + if err := remoter.Push(ctx, bootstrapDesc, true, bootstrapRc); err != nil { + return nil, errors.Wrap(err, "push bootstrap layer") + } + return &bootstrapInfo{ + bootstrapDesc: bootstrapDesc, + bootstrapDiffID: bootstrapDiffID, + }, nil +} + +func pushConfig(ctx context.Context, opt Opt, buildInfo BuildInfo, bootstrapDiffID digest.Digest) (*ocispec.Descriptor, error) { + nydusImage := buildInfo.SourceImage + remoter, err := remoter(opt) + if err != nil { + return nil, errors.Wrap(err, "create remote") + } + config := nydusImage.Config + + originalBlobLayers := getOriginalBlobLayers(nydusImage) + config.RootFS.DiffIDs = []digest.Digest{} + for idx := range originalBlobLayers { + config.RootFS.DiffIDs = append(config.RootFS.DiffIDs, originalBlobLayers[idx].Digest) + } + prefetchBlobDigest := digest.NewDigestFromEncoded(digest.SHA256, buildInfo.PrefetchBlobID) + config.RootFS.DiffIDs = append(config.RootFS.DiffIDs, prefetchBlobDigest) + // Note: bootstrap diffid is tar + config.RootFS.DiffIDs = append(config.RootFS.DiffIDs, bootstrapDiffID) + + configBytes, configDesc, err := makeDesc(config, nydusImage.Manifest.Config) + if err != nil { + return nil, errors.Wrap(err, "make config desc") + } + + if err := remoter.Push(ctx, *configDesc, true, bytes.NewReader(configBytes)); err != nil { + if utils.RetryWithHTTP(err) { + remoter.MaybeWithHTTP(err) + if err := remoter.Push(ctx, *configDesc, true, bytes.NewReader(configBytes)); err != nil { + return nil, errors.Wrap(err, "push image config") + } + } else { + return nil, errors.Wrap(err, "push image config") + } + } + + return configDesc, nil + +} + +func pushNewImage(ctx context.Context, opt Opt, buildInfo BuildInfo) error { + logrus.Infof("pushing new image") + start := time.Now() + + remoter, err := remoter(opt) + if err != nil { + return errors.Wrap(err, "create remote") + } + nydusImage := buildInfo.SourceImage + + prefetchBlob, err := pushBlob(ctx, opt, buildInfo) + if err != nil { + return errors.Wrap(err, "create and push hot blob desc") + } + + bootstrapInfo, err := pushNewBootstrap(ctx, opt, buildInfo) + if err != nil { + return errors.Wrap(err, "create and push bootstrap desc") + } + + configDesc, err := pushConfig(ctx, opt, buildInfo, bootstrapInfo.bootstrapDiffID) + if err != nil { + return errors.Wrap(err, "create and push bootstrap desc") + } + + // push image manifest + layers := getOriginalBlobLayers(nydusImage) + layers = append(layers, *prefetchBlob) + layers = append(layers, bootstrapInfo.bootstrapDesc) + nydusImage.Manifest.Config = *configDesc + nydusImage.Manifest.Layers = layers + + manifestBytes, manifestDesc, err := makeDesc(nydusImage.Manifest, nydusImage.Desc) + if err != nil { + return errors.Wrap(err, "make config desc") + } + if err := remoter.Push(ctx, *manifestDesc, false, bytes.NewReader(manifestBytes)); err != nil { + return errors.Wrap(err, "push image manifest") + } + logrus.Infof("pushed new image, elapsed: %s", time.Since(start)) + return nil +} diff --git a/contrib/nydusify/pkg/utils/constant.go b/contrib/nydusify/pkg/utils/constant.go index 87e6075c85a..78c6ea23eec 100644 --- a/contrib/nydusify/pkg/utils/constant.go +++ b/contrib/nydusify/pkg/utils/constant.go @@ -22,5 +22,6 @@ const ( LayerAnnotationUncompressed = "containerd.io/uncompressed" - LayerAnnotationNydusCommitBlobs = "containerd.io/snapshot/nydus-commit-blobs" + LayerAnnotationNydusCommitBlobs = "containerd.io/snapshot/nydus-commit-blobs" + LayerAnnotationNyudsPrefetchBlob = "containerd.io/snapshot/nydus-separated-blob-with-prefetch-files" )