From b9ce2af602646d30238e5a1eedfccb1cb529fae6 Mon Sep 17 00:00:00 2001 From: "Zheao.Li" Date: Mon, 2 Jan 2023 23:33:55 +0800 Subject: [PATCH] [Refactor] Refactor the build subcommand flagging process Signed-off-by: Zheao.Li --- .gitignore | 2 +- cmd/nerdctl/build.go | 446 ++++++----------------------------- pkg/api/types/build_types.go | 58 +++++ pkg/cmd/build/build.go | 415 ++++++++++++++++++++++++++++++++ 4 files changed, 548 insertions(+), 373 deletions(-) create mode 100644 pkg/api/types/build_types.go create mode 100644 pkg/cmd/build/build.go diff --git a/.gitignore b/.gitignore index 33c9733d860..646cd932d8f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ _output # golangci-lint -build +/build # vagrant /.vagrant diff --git a/cmd/nerdctl/build.go b/cmd/nerdctl/build.go index 7fa6e7a8822..4a77105debf 100644 --- a/cmd/nerdctl/build.go +++ b/cmd/nerdctl/build.go @@ -17,27 +17,17 @@ package main import ( - "encoding/json" "errors" "fmt" - "io" "os" - "os/exec" - "strconv" "strings" - "path/filepath" - - "github.com/containerd/containerd/errdefs" - dockerreference "github.com/containerd/containerd/reference/docker" "github.com/containerd/nerdctl/pkg/api/types" "github.com/containerd/nerdctl/pkg/buildkitutil" - "github.com/containerd/nerdctl/pkg/clientutil" + "github.com/containerd/nerdctl/pkg/cmd/build" "github.com/containerd/nerdctl/pkg/defaults" - "github.com/containerd/nerdctl/pkg/platformutil" "github.com/containerd/nerdctl/pkg/strutil" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -78,424 +68,136 @@ If Dockerfile is not present and -f is not specified, it will look for Container return buildCommand } -func getBuildkitHost(cmd *cobra.Command, ns string) (string, error) { - if cmd.Flags().Changed("buildkit-host") || os.Getenv("BUILDKIT_HOST") != "" { - // If address is explicitly specified, use it. - buildkitHost, err := cmd.Flags().GetString("buildkit-host") - if err != nil { - return "", err - } - if err := buildkitutil.PingBKDaemon(buildkitHost); err != nil { - return "", err - } - return buildkitHost, nil - } - return buildkitutil.GetBuildkitHost(ns) -} - -func isImageSharable(buildkitHost string, namespace, uuid, snapshotter string, platform []string) (bool, error) { - labels, err := buildkitutil.GetWorkerLabels(buildkitHost) +func processBuildCommandFlag(cmd *cobra.Command, args []string) (*types.BuildCommandOptions, error) { + globalOptions, err := processRootCmdFlags(cmd) if err != nil { - return false, err - } - logrus.Debugf("worker labels: %+v", labels) - executor, ok := labels["org.mobyproject.buildkit.worker.executor"] - if !ok { - return false, nil - } - containerdUUID, ok := labels["org.mobyproject.buildkit.worker.containerd.uuid"] - if !ok { - return false, nil - } - containerdNamespace, ok := labels["org.mobyproject.buildkit.worker.containerd.namespace"] - if !ok { - return false, nil + return nil, err } - workerSnapshotter, ok := labels["org.mobyproject.buildkit.worker.snapshotter"] - if !ok { - return false, nil - } - // NOTE: It's possible that BuildKit doesn't download the base image of non-default platform (e.g. when the provided - // Dockerfile doesn't contain instructions require base images like RUN) even if `--output type=image,unpack=true` - // is passed to BuildKit. Thus, we need to use `type=docker` or `type=oci` when nerdctl builds non-default platform - // image using `platform` option. - return executor == "containerd" && containerdUUID == uuid && containerdNamespace == namespace && workerSnapshotter == snapshotter && len(platform) == 0, nil -} - -func buildAction(cmd *cobra.Command, args []string) error { - globalOptions, err := processRootCmdFlags(cmd) + buildKitHost, err := getBuildkitHost(cmd, globalOptions.Namespace) if err != nil { - return err + return nil, err } platform, err := cmd.Flags().GetStringSlice("platform") if err != nil { - return err + return nil, err } platform = strutil.DedupeStrSlice(platform) - buildkitHost, err := getBuildkitHost(cmd, globalOptions.Namespace) - if err != nil { - return err - } - - buildctlBinary, buildctlArgs, needsLoading, metaFile, tags, cleanup, err := generateBuildctlArgs(cmd, globalOptions, buildkitHost, platform, args) - if err != nil { - return err - } - if cleanup != nil { - defer cleanup() - } - - quiet, err := cmd.Flags().GetBool("quiet") - if err != nil { - return err - } - - logrus.Debugf("running %s %v", buildctlBinary, buildctlArgs) - buildctlCmd := exec.Command(buildctlBinary, buildctlArgs...) - buildctlCmd.Env = os.Environ() - - var buildctlStdout io.Reader - if needsLoading { - buildctlStdout, err = buildctlCmd.StdoutPipe() - if err != nil { - return err - } - } else { - buildctlCmd.Stdout = cmd.OutOrStdout() - } - if !quiet { - buildctlCmd.Stderr = cmd.ErrOrStderr() - } - - if err := buildctlCmd.Start(); err != nil { - return err - } - - if needsLoading { - platMC, err := platformutil.NewMatchComparer(false, platform) - if err != nil { - return err - } - if err = loadImage(buildctlStdout, cmd, globalOptions, platMC, quiet); err != nil { - return err - } - } - - if err = buildctlCmd.Wait(); err != nil { - return err - } - - iidFile, _ := cmd.Flags().GetString("iidfile") - if iidFile != "" { - id, err := getDigestFromMetaFile(metaFile) - if err != nil { - return err - } - if err := os.WriteFile(iidFile, []byte(id), 0600); err != nil { - return err - } - } - client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) - if len(tags) > 1 { - logrus.Debug("Found more than 1 tag") - if err != nil { - return fmt.Errorf("unable to tag images: %s", err) - } - defer cancel() - imageService := client.ImageService() - image, err := imageService.Get(ctx, tags[0]) - if err != nil { - return fmt.Errorf("unable to tag image: %s", err) - } - for _, targetRef := range tags[1:] { - image.Name = targetRef - if _, err := imageService.Create(ctx, image); err != nil { - // if already exists; skip. - if errors.Is(err, errdefs.ErrAlreadyExists) { - continue - } - return fmt.Errorf("unable to tag image: %s", err) - } - } - } - - return nil -} - -func generateBuildctlArgs(cmd *cobra.Command, globalOptions *types.GlobalCommandOptions, buildkitHost string, platform, args []string) (buildCtlBinary string, - buildctlArgs []string, needsLoading bool, metaFile string, tags []string, cleanup func(), err error) { if len(args) < 1 { - return "", nil, false, "", nil, nil, errors.New("context needs to be specified") + return nil, errors.New("context needs to be specified") } buildContext := args[0] if buildContext == "-" || strings.Contains(buildContext, "://") { - return "", nil, false, "", nil, nil, fmt.Errorf("unsupported build context: %q", buildContext) + return nil, fmt.Errorf("unsupported build context: %q", buildContext) } - - buildctlBinary, err := buildkitutil.BuildctlBinary() if err != nil { - return "", nil, false, "", nil, nil, err + return nil, err } - output, err := cmd.Flags().GetString("output") if err != nil { - return "", nil, false, "", nil, nil, err - } - client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) - if output == "" { - if err != nil { - return "", nil, false, "", nil, nil, err - } - defer cancel() - info, err := client.Server(ctx) - if err != nil { - return "", nil, false, "", nil, nil, err - } - sharable, err := isImageSharable(buildkitHost, globalOptions.Namespace, info.UUID, globalOptions.Snapshotter, platform) - if err != nil { - return "", nil, false, "", nil, nil, err - } - if sharable { - output = "type=image,unpack=true" // ensure the target stage is unlazied (needed for any snapshotters) - } else { - output = "type=docker" - if len(platform) > 1 { - // For avoiding `error: failed to solve: docker exporter does not currently support exporting manifest lists` - // TODO: consider using type=oci for single-platform build too - output = "type=oci" - } - needsLoading = true - } - } else { - if !strings.Contains(output, "type=") { - // should accept --output as an alias of --output - // type=local,dest= - output = fmt.Sprintf("type=local,dest=%s", output) - } - if strings.Contains(output, "type=docker") || strings.Contains(output, "type=oci") { - needsLoading = true - } + return nil, err } tagValue, err := cmd.Flags().GetStringArray("tag") if err != nil { - return "", nil, false, "", nil, nil, err + return nil, err } - if tags = strutil.DedupeStrSlice(tagValue); len(tags) > 0 { - ref := tags[0] - named, err := dockerreference.ParseNormalizedNamed(ref) - if err != nil { - return "", nil, false, "", nil, nil, err - } - output += ",name=" + dockerreference.TagNameOnly(named).String() - - // pick the first tag and add it to output - for idx, tag := range tags { - named, err := dockerreference.ParseNormalizedNamed(tag) - if err != nil { - return "", nil, false, "", nil, nil, err - } - tags[idx] = dockerreference.TagNameOnly(named).String() - } - } else if len(tags) == 0 { - output = output + ",dangling-name-prefix=" - } - - buildctlArgs = buildkitutil.BuildctlBaseArgs(buildkitHost) - - progressValue, err := cmd.Flags().GetString("progress") + progress, err := cmd.Flags().GetString("progress") if err != nil { - return "", nil, false, "", nil, nil, err + return nil, err } - - buildctlArgs = append(buildctlArgs, []string{ - "build", - "--progress=" + progressValue, - "--frontend=dockerfile.v0", - "--local=context=" + buildContext, - "--output=" + output, - }...) - filename, err := cmd.Flags().GetString("file") if err != nil { - return "", nil, false, "", nil, nil, err + return nil, err } - - dir := buildContext - file := buildkitutil.DefaultDockerfileName - if filename != "" { - if filename == "-" { - var err error - dir, err = buildkitutil.WriteTempDockerfile(cmd.InOrStdin()) - if err != nil { - return "", nil, false, "", nil, nil, err - } - cleanup = func() { - os.RemoveAll(dir) - } - } else { - dir, file = filepath.Split(filename) - } - - if dir == "" { - dir = "." - } - } - dir, file, err = buildkitutil.BuildKitFile(dir, file) - if err != nil { - return "", nil, false, "", nil, nil, err - } - - buildctlArgs = append(buildctlArgs, "--local=dockerfile="+dir) - buildctlArgs = append(buildctlArgs, "--opt=filename="+file) - target, err := cmd.Flags().GetString("target") if err != nil { - return "", nil, false, "", nil, cleanup, err - } - if target != "" { - buildctlArgs = append(buildctlArgs, "--opt=target="+target) + return nil, err } - - if len(platform) > 0 { - buildctlArgs = append(buildctlArgs, "--opt=platform="+strings.Join(platform, ",")) - } - - buildArgsValue, err := cmd.Flags().GetStringArray("build-arg") + buildArgs, err := cmd.Flags().GetStringArray("build-arg") if err != nil { - return "", nil, false, "", nil, cleanup, err + return nil, err } - for _, ba := range strutil.DedupeStrSlice(buildArgsValue) { - arr := strings.Split(ba, "=") - if len(arr) == 1 && len(arr[0]) > 0 { - // Avoid masking default build arg value from Dockerfile if environment variable is not set - // https://github.com/moby/moby/issues/24101 - val, ok := os.LookupEnv(arr[0]) - if ok { - buildctlArgs = append(buildctlArgs, fmt.Sprintf("--opt=build-arg:%s=%s", ba, val)) - } else { - logrus.Debugf("ignoring unset build arg %q", ba) - } - } else if len(arr) > 1 && len(arr[0]) > 0 { - buildctlArgs = append(buildctlArgs, "--opt=build-arg:"+ba) - - // Support `--build-arg BUILDKIT_INLINE_CACHE=1` for compatibility with `docker buildx build` - // https://github.com/docker/buildx/blob/v0.6.3/docs/reference/buildx_build.md#-export-build-cache-to-an-external-cache-destination---cache-to - if strings.HasPrefix(ba, "BUILDKIT_INLINE_CACHE=") { - bic := strings.TrimPrefix(ba, "BUILDKIT_INLINE_CACHE=") - bicParsed, err := strconv.ParseBool(bic) - if err == nil { - if bicParsed { - buildctlArgs = append(buildctlArgs, "--export-cache=type=inline") - } - } else { - logrus.WithError(err).Warnf("invalid BUILDKIT_INLINE_CACHE: %q", bic) - } - } - } else { - return "", nil, false, "", nil, nil, fmt.Errorf("invalid build arg %q", ba) - } - } - - labels, err := cmd.Flags().GetStringArray("label") + label, err := cmd.Flags().GetStringArray("label") if err != nil { - return "", nil, false, "", nil, nil, err + return nil, err } - labels = strutil.DedupeStrSlice(labels) - for _, l := range labels { - buildctlArgs = append(buildctlArgs, "--opt=label:"+l) - } - noCache, err := cmd.Flags().GetBool("no-cache") if err != nil { - return "", nil, false, "", nil, cleanup, err - } - if noCache { - buildctlArgs = append(buildctlArgs, "--no-cache") + return nil, err } - - secretValue, err := cmd.Flags().GetStringArray("secret") + secret, err := cmd.Flags().GetStringArray("secret") if err != nil { - return "", nil, false, "", nil, cleanup, err - } - for _, s := range strutil.DedupeStrSlice(secretValue) { - buildctlArgs = append(buildctlArgs, "--secret="+s) + return nil, err } - - sshValue, err := cmd.Flags().GetStringArray("ssh") + ssh, err := cmd.Flags().GetStringArray("ssh") if err != nil { - return "", nil, false, "", nil, cleanup, err - } - for _, s := range strutil.DedupeStrSlice(sshValue) { - buildctlArgs = append(buildctlArgs, "--ssh="+s) + return nil, err } - cacheFrom, err := cmd.Flags().GetStringArray("cache-from") if err != nil { - return "", nil, false, "", nil, cleanup, err - } - for _, s := range strutil.DedupeStrSlice(cacheFrom) { - if !strings.Contains(s, "type=") { - s = "type=registry,ref=" + s - } - buildctlArgs = append(buildctlArgs, "--import-cache="+s) + return nil, err } - cacheTo, err := cmd.Flags().GetStringArray("cache-to") if err != nil { - return "", nil, false, "", nil, cleanup, err + return nil, err } - for _, s := range strutil.DedupeStrSlice(cacheTo) { - if !strings.Contains(s, "type=") { - s = "type=registry,ref=" + s - } - buildctlArgs = append(buildctlArgs, "--export-cache="+s) - } - rm, err := cmd.Flags().GetBool("rm") if err != nil { - return "", nil, false, "", nil, cleanup, err + return nil, err } - if !rm { - logrus.Warn("ignoring deprecated flag: '--rm=false'") - } - - iidFile, err := cmd.Flags().GetString("iidfile") + iidfile, err := cmd.Flags().GetString("iidfile") if err != nil { - return "", nil, false, "", nil, cleanup, err + return nil, err } - if iidFile != "" { - file, err := os.CreateTemp("", "buildkit-meta-*") + quiet, err := cmd.Flags().GetBool("quiet") + if err != nil { + return nil, err + } + result := &types.BuildCommandOptions{ + GOptions: globalOptions, + BuildKitHost: buildKitHost, + BuildContext: buildContext, + Output: output, + Tag: tagValue, + Progress: progress, + File: filename, + Target: target, + BuildArgs: buildArgs, + Label: label, + NoCache: noCache, + Secret: secret, + SSH: ssh, + CacheFrom: cacheFrom, + CacheTo: cacheTo, + Rm: rm, + IidFile: iidfile, + Quiet: quiet, + Platform: platform, + } + return result, nil +} + +func getBuildkitHost(cmd *cobra.Command, namespace string) (string, error) { + if cmd.Flags().Changed("buildkit-host") || os.Getenv("BUILDKIT_HOST") != "" { + // If address is explicitly specified, use it. + buildkitHost, err := cmd.Flags().GetString("buildkit-host") if err != nil { - return "", nil, false, "", nil, cleanup, err + return "", err + } + if err := buildkitutil.PingBKDaemon(buildkitHost); err != nil { + return "", err } - defer file.Close() - metaFile = file.Name() - buildctlArgs = append(buildctlArgs, "--metadata-file="+metaFile) + return buildkitHost, nil } - - return buildctlBinary, buildctlArgs, needsLoading, metaFile, tags, cleanup, nil + return buildkitutil.GetBuildkitHost(namespace) } -func getDigestFromMetaFile(path string) (string, error) { - data, err := os.ReadFile(path) +func buildAction(cmd *cobra.Command, args []string) error { + options, err := processBuildCommandFlag(cmd, args) if err != nil { - return "", err - } - defer os.Remove(path) - - metadata := map[string]json.RawMessage{} - if err := json.Unmarshal(data, &metadata); err != nil { - logrus.WithError(err).Errorf("failed to unmarshal metadata file %s", path) - return "", err - } - digestRaw, ok := metadata["containerimage.digest"] - if !ok { - return "", errors.New("failed to find containerimage.digest in metadata file") + return err } - var digest string - if err := json.Unmarshal(digestRaw, &digest); err != nil { - logrus.WithError(err).Errorf("failed to unmarshal digset") - return "", err + if err := build.Build(cmd.Context(), options, cmd.InOrStdin(), cmd.OutOrStdout(), cmd.ErrOrStderr()); err != nil { + return err } - return digest, nil + return nil } diff --git a/pkg/api/types/build_types.go b/pkg/api/types/build_types.go new file mode 100644 index 00000000000..7784f2527f2 --- /dev/null +++ b/pkg/api/types/build_types.go @@ -0,0 +1,58 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package types + +type BuildCommandOptions struct { + // GOptions is the global options + GOptions *GlobalCommandOptions + // BuildKitHost is the buildkit host + BuildKitHost string + // Tag is the tag of the image + Tag []string + // File Name of the Dockerfile + File string + // Target is the target of the build + Target string + // BuildArgs is the build-time variables + BuildArgs []string + // NoCache disables cache + NoCache bool + // Output is the output destination + Output string + // Progress Set type of progress output (auto, plain, tty). Use plain to show container output + Progress string + // Secret file to expose to the build: id=mysecret,src=/local/secret + Secret []string + // SSH agent socket or keys to expose to the build (format: default|[=|[,]]) + SSH []string + // Quiet suppress the build output and print image ID on success + Quiet bool + // CacheFrom external cache sources (eg. user/app:cache, type=local,src=path/to/dir) + CacheFrom []string + // CacheTo cache export destinations (eg. user/app:cache, type=local,dest=path/to/dir) + CacheTo []string + // Rm remove intermediate containers after a successful build + Rm bool + // Platform set target platform for build (e.g., "amd64", "arm64") + Platform []string + // IidFile write the image ID to the file + IidFile string + // Label is the metadata for an image + Label []string + // BuildContext is the build context + BuildContext string +} diff --git a/pkg/cmd/build/build.go b/pkg/cmd/build/build.go new file mode 100644 index 00000000000..87be0610d00 --- /dev/null +++ b/pkg/cmd/build/build.go @@ -0,0 +1,415 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package build + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/images" + "github.com/containerd/containerd/images/archive" + "github.com/containerd/containerd/platforms" + dockerreference "github.com/containerd/containerd/reference/docker" + "github.com/containerd/nerdctl/pkg/api/types" + "github.com/containerd/nerdctl/pkg/buildkitutil" + "github.com/containerd/nerdctl/pkg/clientutil" + "github.com/containerd/nerdctl/pkg/platformutil" + "github.com/containerd/nerdctl/pkg/strutil" + "github.com/sirupsen/logrus" +) + +func Build(ctx context.Context, options *types.BuildCommandOptions, stdin io.Reader, stdout, stderr io.Writer) error { + buildctlBinary, buildctlArgs, needsLoading, metaFile, tags, cleanup, err := generateBuildctlArgs(ctx, stdin, options) + if err != nil { + return err + } + if cleanup != nil { + defer cleanup() + } + + logrus.Debugf("running %s %v", buildctlBinary, buildctlArgs) + buildctlCmd := exec.Command(buildctlBinary, buildctlArgs...) + buildctlCmd.Env = os.Environ() + + var buildctlStdout io.Reader + if needsLoading { + buildctlStdout, err = buildctlCmd.StdoutPipe() + if err != nil { + return err + } + } else { + buildctlCmd.Stdout = stdout + } + if !options.Quiet { + buildctlCmd.Stderr = stderr + } + + if err := buildctlCmd.Start(); err != nil { + return err + } + + if needsLoading { + platMC, err := platformutil.NewMatchComparer(false, options.Platform) + if err != nil { + return err + } + if err = loadImage(ctx, buildctlStdout, options.GOptions.Namespace, options.GOptions.Address, options.GOptions.Snapshotter, stdout, platMC, options.Quiet); err != nil { + return err + } + } + + if err = buildctlCmd.Wait(); err != nil { + return err + } + + if options.IidFile != "" { + id, err := getDigestFromMetaFile(metaFile) + if err != nil { + return err + } + if err := os.WriteFile(options.IidFile, []byte(id), 0600); err != nil { + return err + } + } + + if len(tags) > 1 { + client, ctx, cancel, err := clientutil.NewClient(ctx, options.GOptions.Namespace, options.GOptions.Address) + logrus.Debug("Found more than 1 tag") + if err != nil { + return fmt.Errorf("unable to tag images: %s", err) + } + defer cancel() + imageService := client.ImageService() + image, err := imageService.Get(ctx, tags[0]) + if err != nil { + return fmt.Errorf("unable to tag image: %s", err) + } + for _, targetRef := range tags[1:] { + image.Name = targetRef + if _, err := imageService.Create(ctx, image); err != nil { + // if already exists; skip. + if errors.Is(err, errdefs.ErrAlreadyExists) { + continue + } + return fmt.Errorf("unable to tag image: %s", err) + } + } + } + + return nil +} + +// TODO: This struct and `loadImage` are duplicated with the code in `cmd/load.go`, remove it after `load.go` has been refactor +type readCounter struct { + io.Reader + N int +} + +func loadImage(ctx context.Context, in io.Reader, namespace, address, snapshotter string, output io.Writer, platMC platforms.MatchComparer, quiet bool) error { + // In addition to passing WithImagePlatform() to client.Import(), we also need to pass WithDefaultPlatform() to NewClient(). + // Otherwise unpacking may fail. + client, ctx, cancel, err := clientutil.NewClient(ctx, namespace, address, containerd.WithDefaultPlatform(platMC)) + if err != nil { + return err + } + defer cancel() + + r := &readCounter{Reader: in} + imgs, err := client.Import(ctx, r, containerd.WithDigestRef(archive.DigestTranslator(snapshotter)), containerd.WithSkipDigestRef(func(name string) bool { return name != "" }), containerd.WithImportPlatform(platMC)) + if err != nil { + if r.N == 0 { + // Avoid confusing "unrecognized image format" + return errors.New("no image was built") + } + if errors.Is(err, images.ErrEmptyWalk) { + err = fmt.Errorf("%w (Hint: set `--platform=PLATFORM` or `--all-platforms`)", err) + } + return err + } + for _, img := range imgs { + image := containerd.NewImageWithPlatform(client, img, platMC) + + // TODO: Show unpack status + if !quiet { + fmt.Fprintf(output, "unpacking %s (%s)...\n", img.Name, img.Target.Digest) + } + err = image.Unpack(ctx, snapshotter) + if err != nil { + return err + } + if quiet { + fmt.Fprintln(output, img.Target.Digest) + } else { + fmt.Fprintf(output, "Loaded image: %s\n", img.Name) + } + } + + return nil +} + +func generateBuildctlArgs(ctx context.Context, stdin io.Reader, options *types.BuildCommandOptions) (buildCtlBinary string, + buildctlArgs []string, needsLoading bool, metaFile string, tags []string, cleanup func(), err error) { + + buildctlBinary, err := buildkitutil.BuildctlBinary() + if err != nil { + return "", nil, false, "", nil, nil, err + } + + output := options.Output + if output == "" { + client, ctx, cancel, err := clientutil.NewClient(ctx, options.GOptions.Namespace, options.GOptions.Address) + if err != nil { + return "", nil, false, "", nil, nil, err + } + defer cancel() + info, err := client.Server(ctx) + if err != nil { + return "", nil, false, "", nil, nil, err + } + sharable, err := isImageSharable(options.BuildKitHost, options.GOptions.Namespace, info.UUID, options.GOptions.Snapshotter, options.Platform) + if err != nil { + return "", nil, false, "", nil, nil, err + } + if sharable { + output = "type=image,unpack=true" // ensure the target stage is unlazied (needed for any snapshotters) + } else { + output = "type=docker" + if len(options.Platform) > 1 { + // For avoiding `error: failed to solve: docker exporter does not currently support exporting manifest lists` + // TODO: consider using type=oci for single-options.Platform build too + output = "type=oci" + } + needsLoading = true + } + } else { + if !strings.Contains(output, "type=") { + // should accept --output as an alias of --output + // type=local,dest= + output = fmt.Sprintf("type=local,dest=%s", output) + } + if strings.Contains(output, "type=docker") || strings.Contains(output, "type=oci") { + needsLoading = true + } + } + if tags = strutil.DedupeStrSlice(options.Tag); len(tags) > 0 { + ref := tags[0] + named, err := dockerreference.ParseNormalizedNamed(ref) + if err != nil { + return "", nil, false, "", nil, nil, err + } + output += ",name=" + dockerreference.TagNameOnly(named).String() + + // pick the first tag and add it to output + for idx, tag := range tags { + named, err := dockerreference.ParseNormalizedNamed(tag) + if err != nil { + return "", nil, false, "", nil, nil, err + } + tags[idx] = dockerreference.TagNameOnly(named).String() + } + } else if len(tags) == 0 { + output = output + ",dangling-name-prefix=" + } + + buildctlArgs = buildkitutil.BuildctlBaseArgs(options.BuildKitHost) + + buildctlArgs = append(buildctlArgs, []string{ + "build", + "--progress=" + options.Progress, + "--frontend=dockerfile.v0", + "--local=context=" + options.BuildContext, + "--output=" + output, + }...) + + dir := options.BuildContext + file := buildkitutil.DefaultDockerfileName + if options.File != "" { + if options.File == "-" { + // Super Warning: this is a special trick to update the dir variable, Don't move this line!!!!!! + var err error + dir, err = buildkitutil.WriteTempDockerfile(stdin) + if err != nil { + return "", nil, false, "", nil, nil, err + } + cleanup = func() { + os.RemoveAll(dir) + } + } else { + dir, file = filepath.Split(options.File) + } + + if dir == "" { + dir = "." + } + } + dir, file, err = buildkitutil.BuildKitFile(dir, file) + if err != nil { + return "", nil, false, "", nil, nil, err + } + + buildctlArgs = append(buildctlArgs, "--local=dockerfile="+dir) + buildctlArgs = append(buildctlArgs, "--opt=filename="+file) + + if options.Target != "" { + buildctlArgs = append(buildctlArgs, "--opt=target="+options.Target) + } + + if len(options.Platform) > 0 { + buildctlArgs = append(buildctlArgs, "--opt=platform="+strings.Join(options.Platform, ",")) + } + + for _, ba := range strutil.DedupeStrSlice(options.BuildArgs) { + arr := strings.Split(ba, "=") + if len(arr) == 1 && len(arr[0]) > 0 { + // Avoid masking default build arg value from Dockerfile if environment variable is not set + // https://github.com/moby/moby/issues/24101 + val, ok := os.LookupEnv(arr[0]) + if ok { + buildctlArgs = append(buildctlArgs, fmt.Sprintf("--opt=build-arg:%s=%s", ba, val)) + } else { + logrus.Debugf("ignoring unset build arg %q", ba) + } + } else if len(arr) > 1 && len(arr[0]) > 0 { + buildctlArgs = append(buildctlArgs, "--opt=build-arg:"+ba) + + // Support `--build-arg BUILDKIT_INLINE_CACHE=1` for compatibility with `docker buildx build` + // https://github.com/docker/buildx/blob/v0.6.3/docs/reference/buildx_build.md#-export-build-cache-to-an-external-cache-destination---cache-to + if strings.HasPrefix(ba, "BUILDKIT_INLINE_CACHE=") { + bic := strings.TrimPrefix(ba, "BUILDKIT_INLINE_CACHE=") + bicParsed, err := strconv.ParseBool(bic) + if err == nil { + if bicParsed { + buildctlArgs = append(buildctlArgs, "--export-cache=type=inline") + } + } else { + logrus.WithError(err).Warnf("invalid BUILDKIT_INLINE_CACHE: %q", bic) + } + } + } else { + return "", nil, false, "", nil, nil, fmt.Errorf("invalid build arg %q", ba) + } + } + + for _, l := range strutil.DedupeStrSlice(options.Label) { + buildctlArgs = append(buildctlArgs, "--opt=label:"+l) + } + + if options.NoCache { + buildctlArgs = append(buildctlArgs, "--no-cache") + } + + for _, s := range strutil.DedupeStrSlice(options.Secret) { + buildctlArgs = append(buildctlArgs, "--secret="+s) + } + + for _, s := range strutil.DedupeStrSlice(options.SSH) { + buildctlArgs = append(buildctlArgs, "--ssh="+s) + } + + for _, s := range strutil.DedupeStrSlice(options.CacheFrom) { + if !strings.Contains(s, "type=") { + s = "type=registry,ref=" + s + } + buildctlArgs = append(buildctlArgs, "--import-cache="+s) + } + + for _, s := range strutil.DedupeStrSlice(options.CacheTo) { + if !strings.Contains(s, "type=") { + s = "type=registry,ref=" + s + } + buildctlArgs = append(buildctlArgs, "--export-cache="+s) + } + + if !options.Rm { + logrus.Warn("ignoring deprecated flag: '--rm=false'") + } + + if options.IidFile != "" { + file, err := os.CreateTemp("", "buildkit-meta-*") + if err != nil { + return "", nil, false, "", nil, cleanup, err + } + defer file.Close() + metaFile = file.Name() + buildctlArgs = append(buildctlArgs, "--metadata-file="+metaFile) + } + + return buildctlBinary, buildctlArgs, needsLoading, metaFile, tags, cleanup, nil +} + +func getDigestFromMetaFile(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + defer os.Remove(path) + + metadata := map[string]json.RawMessage{} + if err := json.Unmarshal(data, &metadata); err != nil { + logrus.WithError(err).Errorf("failed to unmarshal metadata file %s", path) + return "", err + } + digestRaw, ok := metadata["containerimage.digest"] + if !ok { + return "", errors.New("failed to find containerimage.digest in metadata file") + } + var digest string + if err := json.Unmarshal(digestRaw, &digest); err != nil { + logrus.WithError(err).Errorf("failed to unmarshal digset") + return "", err + } + return digest, nil +} + +func isImageSharable(buildkitHost, namespace, uuid, snapshotter string, platform []string) (bool, error) { + labels, err := buildkitutil.GetWorkerLabels(buildkitHost) + if err != nil { + return false, err + } + logrus.Debugf("worker labels: %+v", labels) + executor, ok := labels["org.mobyproject.buildkit.worker.executor"] + if !ok { + return false, nil + } + containerdUUID, ok := labels["org.mobyproject.buildkit.worker.containerd.uuid"] + if !ok { + return false, nil + } + containerdNamespace, ok := labels["org.mobyproject.buildkit.worker.containerd.namespace"] + if !ok { + return false, nil + } + workerSnapshotter, ok := labels["org.mobyproject.buildkit.worker.snapshotter"] + if !ok { + return false, nil + } + // NOTE: It's possible that BuildKit doesn't download the base image of non-default platform (e.g. when the provided + // Dockerfile doesn't contain instructions require base images like RUN) even if `--output type=image,unpack=true` + // is passed to BuildKit. Thus, we need to use `type=docker` or `type=oci` when nerdctl builds non-default platform + // image using `platform` option. + return executor == "containerd" && containerdUUID == uuid && containerdNamespace == namespace && workerSnapshotter == snapshotter && len(platform) == 0, nil +}