From 4b516af1f6258e19e6eb1b201d2793deb3da5773 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 6 Mar 2024 10:01:00 +0100 Subject: [PATCH] build: move funcs related to solve opts handling Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- build/build.go | 605 ------------------------------------------------ build/opt.go | 609 +++++++++++++++++++++++++++++++++++++++++++++++++ build/utils.go | 21 ++ 3 files changed, 630 insertions(+), 605 deletions(-) create mode 100644 build/opt.go diff --git a/build/build.go b/build/build.go index 9e1c18c3..9df07a90 100644 --- a/build/build.go +++ b/build/build.go @@ -1,7 +1,6 @@ package build import ( - "bufio" "bytes" "context" _ "crypto/sha256" // ensure digests can be computed @@ -10,44 +9,32 @@ import ( "fmt" "io" "os" - "path/filepath" "strconv" "strings" "sync" - "syscall" "time" - "github.com/containerd/containerd/content" - "github.com/containerd/containerd/content/local" "github.com/containerd/containerd/images" - "github.com/containerd/containerd/platforms" "github.com/distribution/reference" "github.com/docker/buildx/builder" "github.com/docker/buildx/driver" - "github.com/docker/buildx/util/confutil" "github.com/docker/buildx/util/desktop" "github.com/docker/buildx/util/dockerutil" "github.com/docker/buildx/util/imagetools" - "github.com/docker/buildx/util/osutil" "github.com/docker/buildx/util/progress" "github.com/docker/buildx/util/resolver" "github.com/docker/buildx/util/waitmap" "github.com/docker/cli/opts" imagetypes "github.com/docker/docker/api/types/image" - "github.com/docker/docker/builder/remotecontext/urlutil" "github.com/docker/docker/pkg/jsonmessage" "github.com/moby/buildkit/client" "github.com/moby/buildkit/client/llb" - "github.com/moby/buildkit/client/ociindex" "github.com/moby/buildkit/exporter/containerimage/exptypes" gateway "github.com/moby/buildkit/frontend/gateway/client" - "github.com/moby/buildkit/identity" "github.com/moby/buildkit/session" - "github.com/moby/buildkit/session/upload/uploadprovider" "github.com/moby/buildkit/solver/errdefs" "github.com/moby/buildkit/solver/pb" spb "github.com/moby/buildkit/sourcepolicy/pb" - "github.com/moby/buildkit/util/apicaps" "github.com/moby/buildkit/util/entitlements" "github.com/moby/buildkit/util/progress/progresswriter" "github.com/moby/buildkit/util/tracing" @@ -157,324 +144,6 @@ func toRepoOnly(in string) (string, error) { return strings.Join(out, ","), nil } -func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt Options, bopts gateway.BuildOpts, configDir string, pw progress.Writer, docker *dockerutil.Client) (solveOpt *client.SolveOpt, release func(), err error) { - nodeDriver := node.Driver - defers := make([]func(), 0, 2) - releaseF := func() { - for _, f := range defers { - f() - } - } - - defer func() { - if err != nil { - releaseF() - } - }() - - // inline cache from build arg - if v, ok := opt.BuildArgs["BUILDKIT_INLINE_CACHE"]; ok { - if v, _ := strconv.ParseBool(v); v { - opt.CacheTo = append(opt.CacheTo, client.CacheOptionsEntry{ - Type: "inline", - Attrs: map[string]string{}, - }) - } - } - - for _, e := range opt.CacheTo { - if e.Type != "inline" && !nodeDriver.Features(ctx)[driver.CacheExport] { - return nil, nil, notSupported(driver.CacheExport, nodeDriver, "https://docs.docker.com/go/build-cache-backends/") - } - } - - cacheTo := make([]client.CacheOptionsEntry, 0, len(opt.CacheTo)) - for _, e := range opt.CacheTo { - if e.Type == "gha" { - if !bopts.LLBCaps.Contains(apicaps.CapID("cache.gha")) { - continue - } - } else if e.Type == "s3" { - if !bopts.LLBCaps.Contains(apicaps.CapID("cache.s3")) { - continue - } - } - cacheTo = append(cacheTo, e) - } - - cacheFrom := make([]client.CacheOptionsEntry, 0, len(opt.CacheFrom)) - for _, e := range opt.CacheFrom { - if e.Type == "gha" { - if !bopts.LLBCaps.Contains(apicaps.CapID("cache.gha")) { - continue - } - } else if e.Type == "s3" { - if !bopts.LLBCaps.Contains(apicaps.CapID("cache.s3")) { - continue - } - } - cacheFrom = append(cacheFrom, e) - } - - so := client.SolveOpt{ - Ref: opt.Ref, - Frontend: "dockerfile.v0", - FrontendAttrs: map[string]string{}, - LocalDirs: map[string]string{}, - CacheExports: cacheTo, - CacheImports: cacheFrom, - AllowedEntitlements: opt.Allow, - SourcePolicy: opt.SourcePolicy, - } - - if so.Ref == "" { - so.Ref = identity.NewID() - } - - if opt.CgroupParent != "" { - so.FrontendAttrs["cgroup-parent"] = opt.CgroupParent - } - - if v, ok := opt.BuildArgs["BUILDKIT_MULTI_PLATFORM"]; ok { - if v, _ := strconv.ParseBool(v); v { - so.FrontendAttrs["multi-platform"] = "true" - } - } - - if multiDriver { - // force creation of manifest list - so.FrontendAttrs["multi-platform"] = "true" - } - - attests := make(map[string]string) - for k, v := range opt.Attests { - if v != nil { - attests[k] = *v - } - } - - supportAttestations := bopts.LLBCaps.Contains(apicaps.CapID("exporter.image.attestations")) && nodeDriver.Features(ctx)[driver.MultiPlatform] - if len(attests) > 0 { - if !supportAttestations { - if !nodeDriver.Features(ctx)[driver.MultiPlatform] { - return nil, nil, notSupported("Attestation", nodeDriver, "https://docs.docker.com/go/attestations/") - } - return nil, nil, errors.Errorf("Attestations are not supported by the current BuildKit daemon") - } - for k, v := range attests { - so.FrontendAttrs["attest:"+k] = v - } - } - - if _, ok := opt.Attests["provenance"]; !ok && supportAttestations { - const noAttestEnv = "BUILDX_NO_DEFAULT_ATTESTATIONS" - var noProv bool - if v, ok := os.LookupEnv(noAttestEnv); ok { - noProv, err = strconv.ParseBool(v) - if err != nil { - return nil, nil, errors.Wrap(err, "invalid "+noAttestEnv) - } - } - if !noProv { - so.FrontendAttrs["attest:provenance"] = "mode=min,inline-only=true" - } - } - - switch len(opt.Exports) { - case 1: - // valid - case 0: - if nodeDriver.IsMobyDriver() && !noDefaultLoad() { - // backwards compat for docker driver only: - // this ensures the build results in a docker image. - opt.Exports = []client.ExportEntry{{Type: "image", Attrs: map[string]string{}}} - } - default: - if err := bopts.LLBCaps.Supports(pb.CapMultipleExporters); err != nil { - return nil, nil, errors.Errorf("multiple outputs currently unsupported by the current BuildKit daemon, please upgrade to version v0.13+ or use a single output") - } - } - - // fill in image exporter names from tags - if len(opt.Tags) > 0 { - tags := make([]string, len(opt.Tags)) - for i, tag := range opt.Tags { - ref, err := reference.Parse(tag) - if err != nil { - return nil, nil, errors.Wrapf(err, "invalid tag %q", tag) - } - tags[i] = ref.String() - } - for i, e := range opt.Exports { - switch e.Type { - case "image", "oci", "docker": - opt.Exports[i].Attrs["name"] = strings.Join(tags, ",") - } - } - } else { - for _, e := range opt.Exports { - if e.Type == "image" && e.Attrs["name"] == "" && e.Attrs["push"] != "" { - if ok, _ := strconv.ParseBool(e.Attrs["push"]); ok { - return nil, nil, errors.Errorf("tag is needed when pushing to registry") - } - } - } - } - - // cacheonly is a fake exporter to opt out of default behaviors - exports := make([]client.ExportEntry, 0, len(opt.Exports)) - for _, e := range opt.Exports { - if e.Type != "cacheonly" { - exports = append(exports, e) - } - } - opt.Exports = exports - - // set up exporters - for i, e := range opt.Exports { - if e.Type == "oci" && !nodeDriver.Features(ctx)[driver.OCIExporter] { - return nil, nil, notSupported(driver.OCIExporter, nodeDriver, "https://docs.docker.com/go/build-exporters/") - } - if e.Type == "docker" { - features := docker.Features(ctx, e.Attrs["context"]) - if features[dockerutil.OCIImporter] && e.Output == nil { - // rely on oci importer if available (which supports - // multi-platform images), otherwise fall back to docker - opt.Exports[i].Type = "oci" - } else if len(opt.Platforms) > 1 || len(attests) > 0 { - if e.Output != nil { - return nil, nil, errors.Errorf("docker exporter does not support exporting manifest lists, use the oci exporter instead") - } - return nil, nil, errors.Errorf("docker exporter does not currently support exporting manifest lists") - } - if e.Output == nil { - if nodeDriver.IsMobyDriver() { - e.Type = "image" - } else { - w, cancel, err := docker.LoadImage(ctx, e.Attrs["context"], pw) - if err != nil { - return nil, nil, err - } - defers = append(defers, cancel) - opt.Exports[i].Output = func(_ map[string]string) (io.WriteCloser, error) { - return w, nil - } - } - } else if !nodeDriver.Features(ctx)[driver.DockerExporter] { - return nil, nil, notSupported(driver.DockerExporter, nodeDriver, "https://docs.docker.com/go/build-exporters/") - } - } - if e.Type == "image" && nodeDriver.IsMobyDriver() { - opt.Exports[i].Type = "moby" - if e.Attrs["push"] != "" { - if ok, _ := strconv.ParseBool(e.Attrs["push"]); ok { - if ok, _ := strconv.ParseBool(e.Attrs["push-by-digest"]); ok { - return nil, nil, errors.Errorf("push-by-digest is currently not implemented for docker driver, please create a new builder instance") - } - } - } - } - if e.Type == "docker" || e.Type == "image" || e.Type == "oci" { - // inline buildinfo attrs from build arg - if v, ok := opt.BuildArgs["BUILDKIT_INLINE_BUILDINFO_ATTRS"]; ok { - e.Attrs["buildinfo-attrs"] = v - } - } - } - - so.Exports = opt.Exports - so.Session = opt.Session - - releaseLoad, err := LoadInputs(ctx, nodeDriver, opt.Inputs, pw, &so) - if err != nil { - return nil, nil, err - } - defers = append(defers, releaseLoad) - - if sharedKey := so.LocalDirs["context"]; sharedKey != "" { - if p, err := filepath.Abs(sharedKey); err == nil { - sharedKey = filepath.Base(p) - } - so.SharedKey = sharedKey + ":" + confutil.TryNodeIdentifier(configDir) - } - - if opt.Pull { - so.FrontendAttrs["image-resolve-mode"] = pb.AttrImageResolveModeForcePull - } else if nodeDriver.IsMobyDriver() { - // moby driver always resolves local images by default - so.FrontendAttrs["image-resolve-mode"] = pb.AttrImageResolveModePreferLocal - } - if opt.Target != "" { - so.FrontendAttrs["target"] = opt.Target - } - if len(opt.NoCacheFilter) > 0 { - so.FrontendAttrs["no-cache"] = strings.Join(opt.NoCacheFilter, ",") - } - if opt.NoCache { - so.FrontendAttrs["no-cache"] = "" - } - for k, v := range opt.BuildArgs { - so.FrontendAttrs["build-arg:"+k] = v - } - for k, v := range opt.Labels { - so.FrontendAttrs["label:"+k] = v - } - - for k, v := range node.ProxyConfig { - if _, ok := opt.BuildArgs[k]; !ok { - so.FrontendAttrs["build-arg:"+k] = v - } - } - - // set platforms - if len(opt.Platforms) != 0 { - pp := make([]string, len(opt.Platforms)) - for i, p := range opt.Platforms { - pp[i] = platforms.Format(p) - } - if len(pp) > 1 && !nodeDriver.Features(ctx)[driver.MultiPlatform] { - return nil, nil, notSupported(driver.MultiPlatform, nodeDriver, "https://docs.docker.com/go/build-multi-platform/") - } - so.FrontendAttrs["platform"] = strings.Join(pp, ",") - } - - // setup networkmode - switch opt.NetworkMode { - case "host": - so.FrontendAttrs["force-network-mode"] = opt.NetworkMode - so.AllowedEntitlements = append(so.AllowedEntitlements, entitlements.EntitlementNetworkHost) - case "none": - so.FrontendAttrs["force-network-mode"] = opt.NetworkMode - case "", "default": - default: - return nil, nil, errors.Errorf("network mode %q not supported by buildkit - you can define a custom network for your builder using the network driver-opt in buildx create", opt.NetworkMode) - } - - // setup extrahosts - extraHosts, err := toBuildkitExtraHosts(ctx, opt.ExtraHosts, nodeDriver) - if err != nil { - return nil, nil, err - } - if len(extraHosts) > 0 { - so.FrontendAttrs["add-hosts"] = extraHosts - } - - // setup shm size - if opt.ShmSize.Value() > 0 { - so.FrontendAttrs["shm-size"] = strconv.FormatInt(opt.ShmSize.Value(), 10) - } - - // setup ulimits - ulimits, err := toBuildkitUlimits(opt.Ulimits) - if err != nil { - return nil, nil, err - } else if len(ulimits) > 0 { - so.FrontendAttrs["ulimit"] = ulimits - } - - return &so, releaseF, nil -} - func Build(ctx context.Context, nodes []builder.Node, opt map[string]Options, docker *dockerutil.Client, configDir string, w progress.Writer) (resp map[string]*client.SolveResponse, err error) { return BuildWithResultHandler(ctx, nodes, opt, docker, configDir, w, nil) } @@ -1095,231 +764,6 @@ func remoteDigestWithMoby(ctx context.Context, d driver.Driver, name string) (st return remoteImage.Descriptor.Digest.String(), nil } -func createTempDockerfile(r io.Reader) (string, error) { - dir, err := os.MkdirTemp("", "dockerfile") - if err != nil { - return "", err - } - f, err := os.Create(filepath.Join(dir, "Dockerfile")) - if err != nil { - return "", err - } - defer f.Close() - if _, err := io.Copy(f, r); err != nil { - return "", err - } - return dir, err -} - -func LoadInputs(ctx context.Context, d *driver.DriverHandle, inp Inputs, pw progress.Writer, target *client.SolveOpt) (func(), error) { - if inp.ContextPath == "" { - return nil, errors.New("please specify build context (e.g. \".\" for the current directory)") - } - - // TODO: handle stdin, symlinks, remote contexts, check files exist - - var ( - err error - dockerfileReader io.Reader - dockerfileDir string - dockerfileName = inp.DockerfilePath - toRemove []string - ) - - switch { - case inp.ContextState != nil: - if target.FrontendInputs == nil { - target.FrontendInputs = make(map[string]llb.State) - } - target.FrontendInputs["context"] = *inp.ContextState - target.FrontendInputs["dockerfile"] = *inp.ContextState - case inp.ContextPath == "-": - if inp.DockerfilePath == "-" { - return nil, errStdinConflict - } - - buf := bufio.NewReader(inp.InStream) - magic, err := buf.Peek(archiveHeaderSize * 2) - if err != nil && err != io.EOF { - return nil, errors.Wrap(err, "failed to peek context header from STDIN") - } - if !(err == io.EOF && len(magic) == 0) { - if isArchive(magic) { - // stdin is context - up := uploadprovider.New() - target.FrontendAttrs["context"] = up.Add(buf) - target.Session = append(target.Session, up) - } else { - if inp.DockerfilePath != "" { - return nil, errDockerfileConflict - } - // stdin is dockerfile - dockerfileReader = buf - inp.ContextPath, _ = os.MkdirTemp("", "empty-dir") - toRemove = append(toRemove, inp.ContextPath) - target.LocalDirs["context"] = inp.ContextPath - } - } - case osutil.IsLocalDir(inp.ContextPath): - target.LocalDirs["context"] = inp.ContextPath - switch inp.DockerfilePath { - case "-": - dockerfileReader = inp.InStream - case "": - dockerfileDir = inp.ContextPath - default: - dockerfileDir = filepath.Dir(inp.DockerfilePath) - dockerfileName = filepath.Base(inp.DockerfilePath) - } - case IsRemoteURL(inp.ContextPath): - if inp.DockerfilePath == "-" { - dockerfileReader = inp.InStream - } else if filepath.IsAbs(inp.DockerfilePath) { - dockerfileDir = filepath.Dir(inp.DockerfilePath) - dockerfileName = filepath.Base(inp.DockerfilePath) - target.FrontendAttrs["dockerfilekey"] = "dockerfile" - } - target.FrontendAttrs["context"] = inp.ContextPath - default: - return nil, errors.Errorf("unable to prepare context: path %q not found", inp.ContextPath) - } - - if inp.DockerfileInline != "" { - dockerfileReader = strings.NewReader(inp.DockerfileInline) - } - - if dockerfileReader != nil { - dockerfileDir, err = createTempDockerfile(dockerfileReader) - if err != nil { - return nil, err - } - toRemove = append(toRemove, dockerfileDir) - dockerfileName = "Dockerfile" - target.FrontendAttrs["dockerfilekey"] = "dockerfile" - } - if urlutil.IsURL(inp.DockerfilePath) { - dockerfileDir, err = createTempDockerfileFromURL(ctx, d, inp.DockerfilePath, pw) - if err != nil { - return nil, err - } - toRemove = append(toRemove, dockerfileDir) - dockerfileName = "Dockerfile" - target.FrontendAttrs["dockerfilekey"] = "dockerfile" - delete(target.FrontendInputs, "dockerfile") - } - - if dockerfileName == "" { - dockerfileName = "Dockerfile" - } - - if dockerfileDir != "" { - target.LocalDirs["dockerfile"] = dockerfileDir - dockerfileName = handleLowercaseDockerfile(dockerfileDir, dockerfileName) - } - - target.FrontendAttrs["filename"] = dockerfileName - - for k, v := range inp.NamedContexts { - target.FrontendAttrs["frontend.caps"] = "moby.buildkit.frontend.contexts+forward" - if v.State != nil { - target.FrontendAttrs["context:"+k] = "input:" + k - if target.FrontendInputs == nil { - target.FrontendInputs = make(map[string]llb.State) - } - target.FrontendInputs[k] = *v.State - continue - } - - if IsRemoteURL(v.Path) || strings.HasPrefix(v.Path, "docker-image://") || strings.HasPrefix(v.Path, "target:") { - target.FrontendAttrs["context:"+k] = v.Path - continue - } - - // handle OCI layout - if strings.HasPrefix(v.Path, "oci-layout://") { - pathAlone := strings.TrimPrefix(v.Path, "oci-layout://") - localPath := pathAlone - localPath, dig, hasDigest := strings.Cut(localPath, "@") - localPath, tag, hasTag := strings.Cut(localPath, ":") - if !hasTag { - tag = "latest" - hasTag = true - } - idx := ociindex.NewStoreIndex(localPath) - if !hasDigest { - // lookup by name - desc, err := idx.Get(tag) - if err != nil { - return nil, err - } - if desc != nil { - dig = string(desc.Digest) - hasDigest = true - } - } - if !hasDigest { - // lookup single - desc, err := idx.GetSingle() - if err != nil { - return nil, err - } - if desc != nil { - dig = string(desc.Digest) - hasDigest = true - } - } - if !hasDigest { - return nil, errors.Errorf("oci-layout reference %q could not be resolved", v.Path) - } - _, err := digest.Parse(dig) - if err != nil { - return nil, errors.Wrapf(err, "invalid oci-layout digest %s", dig) - } - - store, err := local.NewStore(localPath) - if err != nil { - return nil, errors.Wrapf(err, "invalid store at %s", localPath) - } - storeName := identity.NewID() - if target.OCIStores == nil { - target.OCIStores = map[string]content.Store{} - } - target.OCIStores[storeName] = store - - layout := "oci-layout://" + storeName - if hasTag { - layout += ":" + tag - } - if hasDigest { - layout += "@" + dig - } - - target.FrontendAttrs["context:"+k] = layout - continue - } - st, err := os.Stat(v.Path) - if err != nil { - return nil, errors.Wrapf(err, "failed to get build context %v", k) - } - if !st.IsDir() { - return nil, errors.Wrapf(syscall.ENOTDIR, "failed to get build context path %v", v) - } - localName := k - if k == "context" || k == "dockerfile" { - localName = "_" + k // underscore to avoid collisions - } - target.LocalDirs[localName] = v.Path - target.FrontendAttrs["context:"+k] = "local:" + localName - } - - release := func() { - for _, dir := range toRemove { - os.RemoveAll(dir) - } - } - return release, nil -} - func resultKey(index int, name string) string { return fmt.Sprintf("%d-%s", index, name) } @@ -1428,55 +872,6 @@ func waitContextDeps(ctx context.Context, index int, results *waitmap.Map, so *c return nil } -func notSupported(f driver.Feature, d driver.Driver, docs string) error { - return errors.Errorf(`%s is not supported for the %s driver. -Switch to a different driver, or turn on the containerd image store, and try again. -Learn more at %s`, f, d.Factory().Name(), docs) -} - -func noDefaultLoad() bool { - v, ok := os.LookupEnv("BUILDX_NO_DEFAULT_LOAD") - if !ok { - return false - } - b, err := strconv.ParseBool(v) - if err != nil { - logrus.Warnf("invalid non-bool value for BUILDX_NO_DEFAULT_LOAD: %s", v) - } - return b -} - -// handle https://github.com/moby/moby/pull/10858 -func handleLowercaseDockerfile(dir, p string) string { - if filepath.Base(p) != "Dockerfile" { - return p - } - - f, err := os.Open(filepath.Dir(filepath.Join(dir, p))) - if err != nil { - return p - } - - names, err := f.Readdirnames(-1) - if err != nil { - return p - } - - foundLowerCase := false - for _, n := range names { - if n == "Dockerfile" { - return p - } - if n == "dockerfile" { - foundLowerCase = true - } - } - if foundLowerCase { - return filepath.Join(filepath.Dir(p), "dockerfile") - } - return p -} - func noPrintFunc(opt map[string]Options) bool { for _, v := range opt { if v.PrintFunc != nil { diff --git a/build/opt.go b/build/opt.go new file mode 100644 index 00000000..d0733bd9 --- /dev/null +++ b/build/opt.go @@ -0,0 +1,609 @@ +package build + +import ( + "bufio" + "context" + "io" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" + + "github.com/containerd/containerd/content" + "github.com/containerd/containerd/content/local" + "github.com/containerd/containerd/platforms" + "github.com/distribution/reference" + "github.com/docker/buildx/builder" + "github.com/docker/buildx/driver" + "github.com/docker/buildx/util/confutil" + "github.com/docker/buildx/util/dockerutil" + "github.com/docker/buildx/util/osutil" + "github.com/docker/buildx/util/progress" + "github.com/docker/docker/builder/remotecontext/urlutil" + "github.com/moby/buildkit/client" + "github.com/moby/buildkit/client/llb" + "github.com/moby/buildkit/client/ociindex" + gateway "github.com/moby/buildkit/frontend/gateway/client" + "github.com/moby/buildkit/identity" + "github.com/moby/buildkit/session/upload/uploadprovider" + "github.com/moby/buildkit/solver/pb" + "github.com/moby/buildkit/util/apicaps" + "github.com/moby/buildkit/util/entitlements" + "github.com/opencontainers/go-digest" + "github.com/pkg/errors" +) + +func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt Options, bopts gateway.BuildOpts, configDir string, pw progress.Writer, docker *dockerutil.Client) (solveOpt *client.SolveOpt, release func(), err error) { + nodeDriver := node.Driver + defers := make([]func(), 0, 2) + releaseF := func() { + for _, f := range defers { + f() + } + } + + defer func() { + if err != nil { + releaseF() + } + }() + + // inline cache from build arg + if v, ok := opt.BuildArgs["BUILDKIT_INLINE_CACHE"]; ok { + if v, _ := strconv.ParseBool(v); v { + opt.CacheTo = append(opt.CacheTo, client.CacheOptionsEntry{ + Type: "inline", + Attrs: map[string]string{}, + }) + } + } + + for _, e := range opt.CacheTo { + if e.Type != "inline" && !nodeDriver.Features(ctx)[driver.CacheExport] { + return nil, nil, notSupported(driver.CacheExport, nodeDriver, "https://docs.docker.com/go/build-cache-backends/") + } + } + + cacheTo := make([]client.CacheOptionsEntry, 0, len(opt.CacheTo)) + for _, e := range opt.CacheTo { + if e.Type == "gha" { + if !bopts.LLBCaps.Contains(apicaps.CapID("cache.gha")) { + continue + } + } else if e.Type == "s3" { + if !bopts.LLBCaps.Contains(apicaps.CapID("cache.s3")) { + continue + } + } + cacheTo = append(cacheTo, e) + } + + cacheFrom := make([]client.CacheOptionsEntry, 0, len(opt.CacheFrom)) + for _, e := range opt.CacheFrom { + if e.Type == "gha" { + if !bopts.LLBCaps.Contains(apicaps.CapID("cache.gha")) { + continue + } + } else if e.Type == "s3" { + if !bopts.LLBCaps.Contains(apicaps.CapID("cache.s3")) { + continue + } + } + cacheFrom = append(cacheFrom, e) + } + + so := client.SolveOpt{ + Ref: opt.Ref, + Frontend: "dockerfile.v0", + FrontendAttrs: map[string]string{}, + LocalDirs: map[string]string{}, + CacheExports: cacheTo, + CacheImports: cacheFrom, + AllowedEntitlements: opt.Allow, + SourcePolicy: opt.SourcePolicy, + } + + if so.Ref == "" { + so.Ref = identity.NewID() + } + + if opt.CgroupParent != "" { + so.FrontendAttrs["cgroup-parent"] = opt.CgroupParent + } + + if v, ok := opt.BuildArgs["BUILDKIT_MULTI_PLATFORM"]; ok { + if v, _ := strconv.ParseBool(v); v { + so.FrontendAttrs["multi-platform"] = "true" + } + } + + if multiDriver { + // force creation of manifest list + so.FrontendAttrs["multi-platform"] = "true" + } + + attests := make(map[string]string) + for k, v := range opt.Attests { + if v != nil { + attests[k] = *v + } + } + + supportAttestations := bopts.LLBCaps.Contains(apicaps.CapID("exporter.image.attestations")) && nodeDriver.Features(ctx)[driver.MultiPlatform] + if len(attests) > 0 { + if !supportAttestations { + if !nodeDriver.Features(ctx)[driver.MultiPlatform] { + return nil, nil, notSupported("Attestation", nodeDriver, "https://docs.docker.com/go/attestations/") + } + return nil, nil, errors.Errorf("Attestations are not supported by the current BuildKit daemon") + } + for k, v := range attests { + so.FrontendAttrs["attest:"+k] = v + } + } + + if _, ok := opt.Attests["provenance"]; !ok && supportAttestations { + const noAttestEnv = "BUILDX_NO_DEFAULT_ATTESTATIONS" + var noProv bool + if v, ok := os.LookupEnv(noAttestEnv); ok { + noProv, err = strconv.ParseBool(v) + if err != nil { + return nil, nil, errors.Wrap(err, "invalid "+noAttestEnv) + } + } + if !noProv { + so.FrontendAttrs["attest:provenance"] = "mode=min,inline-only=true" + } + } + + switch len(opt.Exports) { + case 1: + // valid + case 0: + if nodeDriver.IsMobyDriver() && !noDefaultLoad() { + // backwards compat for docker driver only: + // this ensures the build results in a docker image. + opt.Exports = []client.ExportEntry{{Type: "image", Attrs: map[string]string{}}} + } + default: + if err := bopts.LLBCaps.Supports(pb.CapMultipleExporters); err != nil { + return nil, nil, errors.Errorf("multiple outputs currently unsupported by the current BuildKit daemon, please upgrade to version v0.13+ or use a single output") + } + } + + // fill in image exporter names from tags + if len(opt.Tags) > 0 { + tags := make([]string, len(opt.Tags)) + for i, tag := range opt.Tags { + ref, err := reference.Parse(tag) + if err != nil { + return nil, nil, errors.Wrapf(err, "invalid tag %q", tag) + } + tags[i] = ref.String() + } + for i, e := range opt.Exports { + switch e.Type { + case "image", "oci", "docker": + opt.Exports[i].Attrs["name"] = strings.Join(tags, ",") + } + } + } else { + for _, e := range opt.Exports { + if e.Type == "image" && e.Attrs["name"] == "" && e.Attrs["push"] != "" { + if ok, _ := strconv.ParseBool(e.Attrs["push"]); ok { + return nil, nil, errors.Errorf("tag is needed when pushing to registry") + } + } + } + } + + // cacheonly is a fake exporter to opt out of default behaviors + exports := make([]client.ExportEntry, 0, len(opt.Exports)) + for _, e := range opt.Exports { + if e.Type != "cacheonly" { + exports = append(exports, e) + } + } + opt.Exports = exports + + // set up exporters + for i, e := range opt.Exports { + if e.Type == "oci" && !nodeDriver.Features(ctx)[driver.OCIExporter] { + return nil, nil, notSupported(driver.OCIExporter, nodeDriver, "https://docs.docker.com/go/build-exporters/") + } + if e.Type == "docker" { + features := docker.Features(ctx, e.Attrs["context"]) + if features[dockerutil.OCIImporter] && e.Output == nil { + // rely on oci importer if available (which supports + // multi-platform images), otherwise fall back to docker + opt.Exports[i].Type = "oci" + } else if len(opt.Platforms) > 1 || len(attests) > 0 { + if e.Output != nil { + return nil, nil, errors.Errorf("docker exporter does not support exporting manifest lists, use the oci exporter instead") + } + return nil, nil, errors.Errorf("docker exporter does not currently support exporting manifest lists") + } + if e.Output == nil { + if nodeDriver.IsMobyDriver() { + e.Type = "image" + } else { + w, cancel, err := docker.LoadImage(ctx, e.Attrs["context"], pw) + if err != nil { + return nil, nil, err + } + defers = append(defers, cancel) + opt.Exports[i].Output = func(_ map[string]string) (io.WriteCloser, error) { + return w, nil + } + } + } else if !nodeDriver.Features(ctx)[driver.DockerExporter] { + return nil, nil, notSupported(driver.DockerExporter, nodeDriver, "https://docs.docker.com/go/build-exporters/") + } + } + if e.Type == "image" && nodeDriver.IsMobyDriver() { + opt.Exports[i].Type = "moby" + if e.Attrs["push"] != "" { + if ok, _ := strconv.ParseBool(e.Attrs["push"]); ok { + if ok, _ := strconv.ParseBool(e.Attrs["push-by-digest"]); ok { + return nil, nil, errors.Errorf("push-by-digest is currently not implemented for docker driver, please create a new builder instance") + } + } + } + } + if e.Type == "docker" || e.Type == "image" || e.Type == "oci" { + // inline buildinfo attrs from build arg + if v, ok := opt.BuildArgs["BUILDKIT_INLINE_BUILDINFO_ATTRS"]; ok { + e.Attrs["buildinfo-attrs"] = v + } + } + } + + so.Exports = opt.Exports + so.Session = opt.Session + + releaseLoad, err := LoadInputs(ctx, nodeDriver, opt.Inputs, pw, &so) + if err != nil { + return nil, nil, err + } + defers = append(defers, releaseLoad) + + if sharedKey := so.LocalDirs["context"]; sharedKey != "" { + if p, err := filepath.Abs(sharedKey); err == nil { + sharedKey = filepath.Base(p) + } + so.SharedKey = sharedKey + ":" + confutil.TryNodeIdentifier(configDir) + } + + if opt.Pull { + so.FrontendAttrs["image-resolve-mode"] = pb.AttrImageResolveModeForcePull + } else if nodeDriver.IsMobyDriver() { + // moby driver always resolves local images by default + so.FrontendAttrs["image-resolve-mode"] = pb.AttrImageResolveModePreferLocal + } + if opt.Target != "" { + so.FrontendAttrs["target"] = opt.Target + } + if len(opt.NoCacheFilter) > 0 { + so.FrontendAttrs["no-cache"] = strings.Join(opt.NoCacheFilter, ",") + } + if opt.NoCache { + so.FrontendAttrs["no-cache"] = "" + } + for k, v := range opt.BuildArgs { + so.FrontendAttrs["build-arg:"+k] = v + } + for k, v := range opt.Labels { + so.FrontendAttrs["label:"+k] = v + } + + for k, v := range node.ProxyConfig { + if _, ok := opt.BuildArgs[k]; !ok { + so.FrontendAttrs["build-arg:"+k] = v + } + } + + // set platforms + if len(opt.Platforms) != 0 { + pp := make([]string, len(opt.Platforms)) + for i, p := range opt.Platforms { + pp[i] = platforms.Format(p) + } + if len(pp) > 1 && !nodeDriver.Features(ctx)[driver.MultiPlatform] { + return nil, nil, notSupported(driver.MultiPlatform, nodeDriver, "https://docs.docker.com/go/build-multi-platform/") + } + so.FrontendAttrs["platform"] = strings.Join(pp, ",") + } + + // setup networkmode + switch opt.NetworkMode { + case "host": + so.FrontendAttrs["force-network-mode"] = opt.NetworkMode + so.AllowedEntitlements = append(so.AllowedEntitlements, entitlements.EntitlementNetworkHost) + case "none": + so.FrontendAttrs["force-network-mode"] = opt.NetworkMode + case "", "default": + default: + return nil, nil, errors.Errorf("network mode %q not supported by buildkit - you can define a custom network for your builder using the network driver-opt in buildx create", opt.NetworkMode) + } + + // setup extrahosts + extraHosts, err := toBuildkitExtraHosts(ctx, opt.ExtraHosts, nodeDriver) + if err != nil { + return nil, nil, err + } + if len(extraHosts) > 0 { + so.FrontendAttrs["add-hosts"] = extraHosts + } + + // setup shm size + if opt.ShmSize.Value() > 0 { + so.FrontendAttrs["shm-size"] = strconv.FormatInt(opt.ShmSize.Value(), 10) + } + + // setup ulimits + ulimits, err := toBuildkitUlimits(opt.Ulimits) + if err != nil { + return nil, nil, err + } else if len(ulimits) > 0 { + so.FrontendAttrs["ulimit"] = ulimits + } + + return &so, releaseF, nil +} + +func LoadInputs(ctx context.Context, d *driver.DriverHandle, inp Inputs, pw progress.Writer, target *client.SolveOpt) (func(), error) { + if inp.ContextPath == "" { + return nil, errors.New("please specify build context (e.g. \".\" for the current directory)") + } + + // TODO: handle stdin, symlinks, remote contexts, check files exist + + var ( + err error + dockerfileReader io.Reader + dockerfileDir string + dockerfileName = inp.DockerfilePath + toRemove []string + ) + + switch { + case inp.ContextState != nil: + if target.FrontendInputs == nil { + target.FrontendInputs = make(map[string]llb.State) + } + target.FrontendInputs["context"] = *inp.ContextState + target.FrontendInputs["dockerfile"] = *inp.ContextState + case inp.ContextPath == "-": + if inp.DockerfilePath == "-" { + return nil, errStdinConflict + } + + buf := bufio.NewReader(inp.InStream) + magic, err := buf.Peek(archiveHeaderSize * 2) + if err != nil && err != io.EOF { + return nil, errors.Wrap(err, "failed to peek context header from STDIN") + } + if !(err == io.EOF && len(magic) == 0) { + if isArchive(magic) { + // stdin is context + up := uploadprovider.New() + target.FrontendAttrs["context"] = up.Add(buf) + target.Session = append(target.Session, up) + } else { + if inp.DockerfilePath != "" { + return nil, errDockerfileConflict + } + // stdin is dockerfile + dockerfileReader = buf + inp.ContextPath, _ = os.MkdirTemp("", "empty-dir") + toRemove = append(toRemove, inp.ContextPath) + target.LocalDirs["context"] = inp.ContextPath + } + } + case osutil.IsLocalDir(inp.ContextPath): + target.LocalDirs["context"] = inp.ContextPath + switch inp.DockerfilePath { + case "-": + dockerfileReader = inp.InStream + case "": + dockerfileDir = inp.ContextPath + default: + dockerfileDir = filepath.Dir(inp.DockerfilePath) + dockerfileName = filepath.Base(inp.DockerfilePath) + } + case IsRemoteURL(inp.ContextPath): + if inp.DockerfilePath == "-" { + dockerfileReader = inp.InStream + } else if filepath.IsAbs(inp.DockerfilePath) { + dockerfileDir = filepath.Dir(inp.DockerfilePath) + dockerfileName = filepath.Base(inp.DockerfilePath) + target.FrontendAttrs["dockerfilekey"] = "dockerfile" + } + target.FrontendAttrs["context"] = inp.ContextPath + default: + return nil, errors.Errorf("unable to prepare context: path %q not found", inp.ContextPath) + } + + if inp.DockerfileInline != "" { + dockerfileReader = strings.NewReader(inp.DockerfileInline) + } + + if dockerfileReader != nil { + dockerfileDir, err = createTempDockerfile(dockerfileReader) + if err != nil { + return nil, err + } + toRemove = append(toRemove, dockerfileDir) + dockerfileName = "Dockerfile" + target.FrontendAttrs["dockerfilekey"] = "dockerfile" + } + if urlutil.IsURL(inp.DockerfilePath) { + dockerfileDir, err = createTempDockerfileFromURL(ctx, d, inp.DockerfilePath, pw) + if err != nil { + return nil, err + } + toRemove = append(toRemove, dockerfileDir) + dockerfileName = "Dockerfile" + target.FrontendAttrs["dockerfilekey"] = "dockerfile" + delete(target.FrontendInputs, "dockerfile") + } + + if dockerfileName == "" { + dockerfileName = "Dockerfile" + } + + if dockerfileDir != "" { + target.LocalDirs["dockerfile"] = dockerfileDir + dockerfileName = handleLowercaseDockerfile(dockerfileDir, dockerfileName) + } + + target.FrontendAttrs["filename"] = dockerfileName + + for k, v := range inp.NamedContexts { + target.FrontendAttrs["frontend.caps"] = "moby.buildkit.frontend.contexts+forward" + if v.State != nil { + target.FrontendAttrs["context:"+k] = "input:" + k + if target.FrontendInputs == nil { + target.FrontendInputs = make(map[string]llb.State) + } + target.FrontendInputs[k] = *v.State + continue + } + + if IsRemoteURL(v.Path) || strings.HasPrefix(v.Path, "docker-image://") || strings.HasPrefix(v.Path, "target:") { + target.FrontendAttrs["context:"+k] = v.Path + continue + } + + // handle OCI layout + if strings.HasPrefix(v.Path, "oci-layout://") { + pathAlone := strings.TrimPrefix(v.Path, "oci-layout://") + localPath := pathAlone + localPath, dig, hasDigest := strings.Cut(localPath, "@") + localPath, tag, hasTag := strings.Cut(localPath, ":") + if !hasTag { + tag = "latest" + hasTag = true + } + idx := ociindex.NewStoreIndex(localPath) + if !hasDigest { + // lookup by name + desc, err := idx.Get(tag) + if err != nil { + return nil, err + } + if desc != nil { + dig = string(desc.Digest) + hasDigest = true + } + } + if !hasDigest { + // lookup single + desc, err := idx.GetSingle() + if err != nil { + return nil, err + } + if desc != nil { + dig = string(desc.Digest) + hasDigest = true + } + } + if !hasDigest { + return nil, errors.Errorf("oci-layout reference %q could not be resolved", v.Path) + } + _, err := digest.Parse(dig) + if err != nil { + return nil, errors.Wrapf(err, "invalid oci-layout digest %s", dig) + } + + store, err := local.NewStore(localPath) + if err != nil { + return nil, errors.Wrapf(err, "invalid store at %s", localPath) + } + storeName := identity.NewID() + if target.OCIStores == nil { + target.OCIStores = map[string]content.Store{} + } + target.OCIStores[storeName] = store + + layout := "oci-layout://" + storeName + if hasTag { + layout += ":" + tag + } + if hasDigest { + layout += "@" + dig + } + + target.FrontendAttrs["context:"+k] = layout + continue + } + st, err := os.Stat(v.Path) + if err != nil { + return nil, errors.Wrapf(err, "failed to get build context %v", k) + } + if !st.IsDir() { + return nil, errors.Wrapf(syscall.ENOTDIR, "failed to get build context path %v", v) + } + localName := k + if k == "context" || k == "dockerfile" { + localName = "_" + k // underscore to avoid collisions + } + target.LocalDirs[localName] = v.Path + target.FrontendAttrs["context:"+k] = "local:" + localName + } + + release := func() { + for _, dir := range toRemove { + os.RemoveAll(dir) + } + } + return release, nil +} + +func createTempDockerfile(r io.Reader) (string, error) { + dir, err := os.MkdirTemp("", "dockerfile") + if err != nil { + return "", err + } + f, err := os.Create(filepath.Join(dir, "Dockerfile")) + if err != nil { + return "", err + } + defer f.Close() + if _, err := io.Copy(f, r); err != nil { + return "", err + } + return dir, err +} + +// handle https://github.com/moby/moby/pull/10858 +func handleLowercaseDockerfile(dir, p string) string { + if filepath.Base(p) != "Dockerfile" { + return p + } + + f, err := os.Open(filepath.Dir(filepath.Join(dir, p))) + if err != nil { + return p + } + + names, err := f.Readdirnames(-1) + if err != nil { + return p + } + + foundLowerCase := false + for _, n := range names { + if n == "Dockerfile" { + return p + } + if n == "dockerfile" { + foundLowerCase = true + } + } + if foundLowerCase { + return filepath.Join(filepath.Dir(p), "dockerfile") + } + return p +} diff --git a/build/utils.go b/build/utils.go index cf585387..0f319728 100644 --- a/build/utils.go +++ b/build/utils.go @@ -5,6 +5,8 @@ import ( "bytes" "context" "net" + "os" + "strconv" "strings" "github.com/docker/buildx/driver" @@ -12,6 +14,7 @@ import ( "github.com/docker/docker/builder/remotecontext/urlutil" "github.com/moby/buildkit/util/gitutil" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) const ( @@ -101,3 +104,21 @@ func toBuildkitUlimits(inp *opts.UlimitOpt) (string, error) { } return strings.Join(ulimits, ","), nil } + +func notSupported(f driver.Feature, d driver.Driver, docs string) error { + return errors.Errorf(`%s is not supported for the %s driver. +Switch to a different driver, or turn on the containerd image store, and try again. +Learn more at %s`, f, d.Factory().Name(), docs) +} + +func noDefaultLoad() bool { + v, ok := os.LookupEnv("BUILDX_NO_DEFAULT_LOAD") + if !ok { + return false + } + b, err := strconv.ParseBool(v) + if err != nil { + logrus.Warnf("invalid non-bool value for BUILDX_NO_DEFAULT_LOAD: %s", v) + } + return b +}