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
+}