package build import ( "bufio" "bytes" "context" "crypto/rand" _ "crypto/sha256" // ensure digests can be computed "encoding/base64" "encoding/hex" "encoding/json" "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/docker/buildx/builder" "github.com/docker/buildx/driver" "github.com/docker/buildx/localstate" "github.com/docker/buildx/util/desktop" "github.com/docker/buildx/util/dockerutil" "github.com/docker/buildx/util/imagetools" "github.com/docker/buildx/util/progress" "github.com/docker/buildx/util/resolver" "github.com/docker/buildx/util/waitmap" "github.com/docker/cli/opts" "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" "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" "github.com/opencontainers/go-digest" specs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/trace" "golang.org/x/sync/errgroup" ) var ( errStdinConflict = errors.New("invalid argument: can't use stdin for both build context and dockerfile") errDockerfileConflict = errors.New("ambiguous Dockerfile source: both stdin and flag correspond to Dockerfiles") ) const ( printFallbackImage = "docker/dockerfile:1.5.2-labs@sha256:f2e91734a84c0922ff47aa4098ab775f1dfa932430d2888dd5cad5251fafdac4" ) type Options struct { Inputs Inputs Allow []entitlements.Entitlement Attests map[string]*string BuildArgs map[string]string CacheFrom []client.CacheOptionsEntry CacheTo []client.CacheOptionsEntry CgroupParent string Exports []client.ExportEntry ExtraHosts []string Labels map[string]string NetworkMode string NoCache bool NoCacheFilter []string Platforms []specs.Platform Pull bool ShmSize opts.MemBytes Tags []string Target string Ulimits *opts.UlimitOpt Session []session.Attachable // Linked marks this target as exclusively linked (not requested by the user). Linked bool PrintFunc *PrintFunc SourcePolicy *spb.Policy } type PrintFunc struct { Name string Format string } type Inputs struct { ContextPath string DockerfilePath string InStream io.Reader ContextState *llb.State DockerfileInline string NamedContexts map[string]NamedContext } type NamedContext struct { Path string State *llb.State } func filterAvailableNodes(nodes []builder.Node) ([]builder.Node, error) { out := make([]builder.Node, 0, len(nodes)) err := errors.Errorf("no drivers found") for _, n := range nodes { if n.Err == nil && n.Driver != nil { out = append(out, n) } if n.Err != nil { err = n.Err } } if len(out) > 0 { return out, nil } return nil, err } type driverPair struct { driverIndex int platforms []specs.Platform so *client.SolveOpt bopts gateway.BuildOpts } func driverIndexes(m map[string][]driverPair) []int { out := make([]int, 0, len(m)) visited := map[int]struct{}{} for _, dp := range m { for _, d := range dp { if _, ok := visited[d.driverIndex]; ok { continue } visited[d.driverIndex] = struct{}{} out = append(out, d.driverIndex) } } return out } func allIndexes(l int) []int { out := make([]int, 0, l) for i := 0; i < l; i++ { out = append(out, i) } return out } func ensureBooted(ctx context.Context, nodes []builder.Node, idxs []int, pw progress.Writer) ([]*client.Client, error) { clients := make([]*client.Client, len(nodes)) baseCtx := ctx eg, ctx := errgroup.WithContext(ctx) for _, i := range idxs { func(i int) { eg.Go(func() error { c, err := driver.Boot(ctx, baseCtx, nodes[i].Driver, pw) if err != nil { return err } clients[i] = c return nil }) }(i) } if err := eg.Wait(); err != nil { return nil, err } return clients, nil } func splitToDriverPairs(availablePlatforms map[string]int, opt map[string]Options) map[string][]driverPair { m := map[string][]driverPair{} for k, opt := range opt { mm := map[int][]specs.Platform{} for _, p := range opt.Platforms { k := platforms.Format(p) idx := availablePlatforms[k] // default 0 pp := mm[idx] pp = append(pp, p) mm[idx] = pp } // if no platform is specified, use first driver if len(mm) == 0 { mm[0] = nil } dps := make([]driverPair, 0, 2) for idx, pp := range mm { dps = append(dps, driverPair{driverIndex: idx, platforms: pp}) } m[k] = dps } return m } func resolveDrivers(ctx context.Context, nodes []builder.Node, opt map[string]Options, pw progress.Writer) (map[string][]driverPair, []*client.Client, error) { dps, clients, err := resolveDriversBase(ctx, nodes, opt, pw) if err != nil { return nil, nil, err } bopts := make([]gateway.BuildOpts, len(clients)) span, ctx := tracing.StartSpan(ctx, "load buildkit capabilities", trace.WithSpanKind(trace.SpanKindInternal)) eg, ctx := errgroup.WithContext(ctx) for i, c := range clients { if c == nil { continue } func(i int, c *client.Client) { eg.Go(func() error { clients[i].Build(ctx, client.SolveOpt{ Internal: true, }, "buildx", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { bopts[i] = c.BuildOpts() return nil, nil }, nil) return nil }) }(i, c) } err = eg.Wait() tracing.FinishWithError(span, err) if err != nil { return nil, nil, err } for key := range dps { for i, dp := range dps[key] { dps[key][i].bopts = bopts[dp.driverIndex] } } return dps, clients, nil } func resolveDriversBase(ctx context.Context, nodes []builder.Node, opt map[string]Options, pw progress.Writer) (map[string][]driverPair, []*client.Client, error) { availablePlatforms := map[string]int{} for i, node := range nodes { for _, p := range node.Platforms { availablePlatforms[platforms.Format(p)] = i } } undetectedPlatform := false allPlatforms := map[string]struct{}{} for _, opt := range opt { for _, p := range opt.Platforms { k := platforms.Format(p) allPlatforms[k] = struct{}{} if _, ok := availablePlatforms[k]; !ok { undetectedPlatform = true } } } // fast path if len(nodes) == 1 || len(allPlatforms) == 0 { m := map[string][]driverPair{} for k, opt := range opt { m[k] = []driverPair{{driverIndex: 0, platforms: opt.Platforms}} } clients, err := ensureBooted(ctx, nodes, driverIndexes(m), pw) if err != nil { return nil, nil, err } return m, clients, nil } // map based on existing platforms if !undetectedPlatform { m := splitToDriverPairs(availablePlatforms, opt) clients, err := ensureBooted(ctx, nodes, driverIndexes(m), pw) if err != nil { return nil, nil, err } return m, clients, nil } // boot all drivers in k clients, err := ensureBooted(ctx, nodes, allIndexes(len(nodes)), pw) if err != nil { return nil, nil, err } eg, ctx := errgroup.WithContext(ctx) workers := make([][]*client.WorkerInfo, len(clients)) for i, c := range clients { if c == nil { continue } func(i int) { eg.Go(func() error { ww, err := clients[i].ListWorkers(ctx) if err != nil { return errors.Wrap(err, "listing workers") } workers[i] = ww return nil }) }(i) } if err := eg.Wait(); err != nil { return nil, nil, err } for i, ww := range workers { for _, w := range ww { for _, p := range w.Platforms { p = platforms.Normalize(p) ps := platforms.Format(p) if _, ok := availablePlatforms[ps]; !ok { availablePlatforms[ps] = i } } } } return splitToDriverPairs(availablePlatforms, opt), clients, nil } func toRepoOnly(in string) (string, error) { m := map[string]struct{}{} p := strings.Split(in, ",") for _, pp := range p { n, err := reference.ParseNormalizedNamed(pp) if err != nil { return "", err } m[n.Name()] = struct{}{} } out := make([]string, 0, len(m)) for k := range m { out = append(out, k) } 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(nodeDriver, driver.CacheExport) } } 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{ Frontend: "dockerfile.v0", FrontendAttrs: map[string]string{}, LocalDirs: map[string]string{}, CacheExports: cacheTo, CacheImports: cacheFrom, AllowedEntitlements: opt.Allow, SourcePolicy: opt.SourcePolicy, } 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 } } supportsAttestations := bopts.LLBCaps.Contains(apicaps.CapID("exporter.image.attestations")) && nodeDriver.Features(ctx)[driver.MultiPlatform] if len(attests) > 0 { if !supportsAttestations { return nil, nil, errors.Errorf("attestations are not supported by the current buildkitd") } for k, v := range attests { so.FrontendAttrs["attest:"+k] = v } } if _, ok := opt.Attests["provenance"]; !ok && supportsAttestations { 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: return nil, nil, errors.Errorf("multiple outputs currently unsupported") } // 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(nodeDriver, driver.OCIExporter) } 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 = wrapWriteCloser(w) } } else if !nodeDriver.Features(ctx)[driver.DockerExporter] { return nil, nil, notSupported(nodeDriver, driver.DockerExporter) } } 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 + ":" + 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(nodeDriver, driver.MultiPlatform) } 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 } // remember local state like directory path that is not sent to buildkit so.Ref = identity.NewID() if err := saveLocalState(so, opt, node, configDir); err != nil { return nil, nil, err } 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) } func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[string]Options, docker *dockerutil.Client, configDir string, w progress.Writer, resultHandleFunc func(driverIndex int, rCtx *ResultHandle)) (resp map[string]*client.SolveResponse, err error) { if len(nodes) == 0 { return nil, errors.Errorf("driver required for build") } nodes, err = filterAvailableNodes(nodes) if err != nil { return nil, errors.Wrapf(err, "no valid drivers found") } var noMobyDriver driver.Driver for _, n := range nodes { if !n.Driver.IsMobyDriver() { noMobyDriver = n.Driver break } } if noMobyDriver != nil && !noDefaultLoad() && noPrintFunc(opt) { var noOutputTargets []string for name, opt := range opt { if !opt.Linked && len(opt.Exports) == 0 { noOutputTargets = append(noOutputTargets, name) } } if len(noOutputTargets) > 0 { var warnNoOutputBuf bytes.Buffer warnNoOutputBuf.WriteString("No output specified ") if len(noOutputTargets) == 1 && noOutputTargets[0] == "default" { warnNoOutputBuf.WriteString(fmt.Sprintf("with %s driver", noMobyDriver.Factory().Name())) } else { warnNoOutputBuf.WriteString(fmt.Sprintf("for %s target(s) with %s driver", strings.Join(noOutputTargets, ", "), noMobyDriver.Factory().Name())) } logrus.Warnf("%s. Build result will only remain in the build cache. To push result image into registry use --push or to load image into docker use --load", warnNoOutputBuf.String()) } } m, clients, err := resolveDrivers(ctx, nodes, opt, w) if err != nil { return nil, err } defers := make([]func(), 0, 2) defer func() { if err != nil { for _, f := range defers { f() } } }() eg, ctx := errgroup.WithContext(ctx) for k, opt := range opt { multiDriver := len(m[k]) > 1 hasMobyDriver := false gitattrs, err := getGitAttributes(ctx, opt.Inputs.ContextPath, opt.Inputs.DockerfilePath) if err != nil { logrus.WithError(err).Warn("current commit information was not captured by the build") } for i, np := range m[k] { node := nodes[np.driverIndex] if node.Driver.IsMobyDriver() { hasMobyDriver = true } opt.Platforms = np.platforms so, release, err := toSolveOpt(ctx, node, multiDriver, opt, np.bopts, configDir, w, docker) if err != nil { return nil, err } for k, v := range gitattrs { so.FrontendAttrs[k] = v } defers = append(defers, release) m[k][i].so = so } for _, at := range opt.Session { if s, ok := at.(interface { SetLogger(progresswriter.Logger) }); ok { s.SetLogger(func(s *client.SolveStatus) { w.Write(s) }) } } // validate for multi-node push if hasMobyDriver && multiDriver { for _, dp := range m[k] { for _, e := range dp.so.Exports { if e.Type == "moby" { if ok, _ := strconv.ParseBool(e.Attrs["push"]); ok { return nil, errors.Errorf("multi-node push can't currently be performed with the docker driver, please switch to a different driver") } } } } } } // validate that all links between targets use same drivers for name := range opt { dps := m[name] for _, dp := range dps { for k, v := range dp.so.FrontendAttrs { if strings.HasPrefix(k, "context:") && strings.HasPrefix(v, "target:") { k2 := strings.TrimPrefix(v, "target:") dps2, ok := m[k2] if !ok { return nil, errors.Errorf("failed to find target %s for context %s", k2, strings.TrimPrefix(k, "context:")) // should be validated before already } var found bool for _, dp2 := range dps2 { if dp2.driverIndex == dp.driverIndex { found = true break } } if !found { return nil, errors.Errorf("failed to use %s as context %s for %s because targets build with different drivers", k2, strings.TrimPrefix(k, "context:"), name) } } } } } resp = map[string]*client.SolveResponse{} var respMu sync.Mutex results := waitmap.New() multiTarget := len(opt) > 1 for k, opt := range opt { err := func(k string) error { opt := opt dps := m[k] multiDriver := len(m[k]) > 1 var span trace.Span ctx := ctx if multiTarget { span, ctx = tracing.StartSpan(ctx, k) } baseCtx := ctx res := make([]*client.SolveResponse, len(dps)) eg2, ctx := errgroup.WithContext(ctx) var pushNames string var insecurePush bool for i, dp := range dps { i, dp, so := i, dp, *dp.so node := nodes[dp.driverIndex] if multiDriver { for i, e := range so.Exports { switch e.Type { case "oci", "tar": return errors.Errorf("%s for multi-node builds currently not supported", e.Type) case "image": if pushNames == "" && e.Attrs["push"] != "" { if ok, _ := strconv.ParseBool(e.Attrs["push"]); ok { pushNames = e.Attrs["name"] if pushNames == "" { return errors.Errorf("tag is needed when pushing to registry") } names, err := toRepoOnly(e.Attrs["name"]) if err != nil { return err } if ok, _ := strconv.ParseBool(e.Attrs["registry.insecure"]); ok { insecurePush = true } e.Attrs["name"] = names e.Attrs["push-by-digest"] = "true" so.Exports[i].Attrs = e.Attrs } } } } } pw := progress.WithPrefix(w, k, multiTarget) c := clients[dp.driverIndex] eg2.Go(func() error { pw = progress.ResetTime(pw) if err := waitContextDeps(ctx, dp.driverIndex, results, &so); err != nil { return err } frontendInputs := make(map[string]*pb.Definition) for key, st := range so.FrontendInputs { def, err := st.Marshal(ctx) if err != nil { return err } frontendInputs[key] = def.ToPB() } req := gateway.SolveRequest{ Frontend: so.Frontend, FrontendInputs: frontendInputs, FrontendOpt: make(map[string]string), } for k, v := range so.FrontendAttrs { req.FrontendOpt[k] = v } so.Frontend = "" so.FrontendInputs = nil ch, done := progress.NewChannel(pw) defer func() { <-done }() cc := c var printRes map[string][]byte buildFunc := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { if opt.PrintFunc != nil { if _, ok := req.FrontendOpt["frontend.caps"]; !ok { req.FrontendOpt["frontend.caps"] = "moby.buildkit.frontend.subrequests+forward" } else { req.FrontendOpt["frontend.caps"] += ",moby.buildkit.frontend.subrequests+forward" } req.FrontendOpt["requestid"] = "frontend." + opt.PrintFunc.Name } res, err := c.Solve(ctx, req) if err != nil { fallback := false var reqErr *errdefs.UnsupportedSubrequestError if errors.As(err, &reqErr) { switch reqErr.Name { case "frontend.outline", "frontend.targets": fallback = true default: return nil, err } } else { return nil, err } // buildkit v0.8 vendored in Docker 20.10 does not support typed errors if strings.Contains(err.Error(), "unsupported request frontend.outline") || strings.Contains(err.Error(), "unsupported request frontend.targets") { fallback = true } if fallback { req.FrontendOpt["build-arg:BUILDKIT_SYNTAX"] = printFallbackImage res2, err2 := c.Solve(ctx, req) if err2 != nil { return nil, err } res = res2 } else { return nil, err } } if opt.PrintFunc != nil { printRes = res.Metadata } results.Set(resultKey(dp.driverIndex, k), res) return res, nil } var rr *client.SolveResponse if resultHandleFunc != nil { var resultHandle *ResultHandle resultHandle, rr, err = NewResultHandle(ctx, cc, so, "buildx", buildFunc, ch) resultHandleFunc(dp.driverIndex, resultHandle) } else { rr, err = c.Build(ctx, so, "buildx", buildFunc, ch) } if desktop.BuildBackendEnabled() && node.Driver.HistoryAPISupported(ctx) { buildRef := fmt.Sprintf("%s/%s/%s", node.Builder, node.Name, so.Ref) if err != nil { return &desktop.ErrorWithBuildRef{ Ref: buildRef, Err: err, } } progress.WriteBuildRef(w, k, buildRef) } if err != nil { return err } res[i] = rr if rr.ExporterResponse == nil { rr.ExporterResponse = map[string]string{} } for k, v := range printRes { rr.ExporterResponse[k] = string(v) } node := nodes[dp.driverIndex].Driver if node.IsMobyDriver() { for _, e := range so.Exports { if e.Type == "moby" && e.Attrs["push"] != "" { if ok, _ := strconv.ParseBool(e.Attrs["push"]); ok { pushNames = e.Attrs["name"] if pushNames == "" { return errors.Errorf("tag is needed when pushing to registry") } pw := progress.ResetTime(pw) pushList := strings.Split(pushNames, ",") for _, name := range pushList { if err := progress.Wrap(fmt.Sprintf("pushing %s with docker", name), pw.Write, func(l progress.SubLogger) error { return pushWithMoby(ctx, node, name, l) }); err != nil { return err } } remoteDigest, err := remoteDigestWithMoby(ctx, node, pushList[0]) if err == nil && remoteDigest != "" { // old daemons might not have containerimage.config.digest set // in response so use containerimage.digest value for it if available if _, ok := rr.ExporterResponse[exptypes.ExporterImageConfigDigestKey]; !ok { if v, ok := rr.ExporterResponse[exptypes.ExporterImageDigestKey]; ok { rr.ExporterResponse[exptypes.ExporterImageConfigDigestKey] = v } } rr.ExporterResponse[exptypes.ExporterImageDigestKey] = remoteDigest } else if err != nil { return err } } } } } return nil }) } eg.Go(func() (err error) { ctx := baseCtx defer func() { if span != nil { tracing.FinishWithError(span, err) } }() pw := progress.WithPrefix(w, "default", false) if err := eg2.Wait(); err != nil { return err } respMu.Lock() resp[k] = res[0] respMu.Unlock() if len(res) == 1 { return nil } if pushNames != "" { progress.Write(pw, fmt.Sprintf("merging manifest list %s", pushNames), func() error { descs := make([]specs.Descriptor, 0, len(res)) for _, r := range res { s, ok := r.ExporterResponse[exptypes.ExporterImageDescriptorKey] if ok { dt, err := base64.StdEncoding.DecodeString(s) if err != nil { return err } var desc specs.Descriptor if err := json.Unmarshal(dt, &desc); err != nil { return errors.Wrapf(err, "failed to unmarshal descriptor %s", s) } descs = append(descs, desc) continue } // This is fallback for some very old buildkit versions. // Note that the mediatype isn't really correct as most of the time it is image manifest and // not manifest list but actually both are handled because for Docker mediatypes the // mediatype value in the Accpet header does not seem to matter. s, ok = r.ExporterResponse[exptypes.ExporterImageDigestKey] if ok { descs = append(descs, specs.Descriptor{ Digest: digest.Digest(s), MediaType: images.MediaTypeDockerSchema2ManifestList, Size: -1, }) } } if len(descs) > 0 { var imageopt imagetools.Opt for _, dp := range dps { imageopt = nodes[dp.driverIndex].ImageOpt break } names := strings.Split(pushNames, ",") if insecurePush { insecureTrue := true httpTrue := true nn, err := reference.ParseNormalizedNamed(names[0]) if err != nil { return err } imageopt.RegistryConfig = map[string]resolver.RegistryConfig{ reference.Domain(nn): { Insecure: &insecureTrue, PlainHTTP: &httpTrue, }, } } itpull := imagetools.New(imageopt) ref, err := reference.ParseNormalizedNamed(names[0]) if err != nil { return err } ref = reference.TagNameOnly(ref) srcs := make([]*imagetools.Source, len(descs)) for i, desc := range descs { srcs[i] = &imagetools.Source{ Desc: desc, Ref: ref, } } dt, desc, err := itpull.Combine(ctx, srcs, nil) if err != nil { return err } itpush := imagetools.New(imageopt) for _, n := range names { nn, err := reference.ParseNormalizedNamed(n) if err != nil { return err } if err := itpush.Push(ctx, nn, desc, dt); err != nil { return err } } respMu.Lock() resp[k] = &client.SolveResponse{ ExporterResponse: map[string]string{ exptypes.ExporterImageDigestKey: desc.Digest.String(), }, } respMu.Unlock() } return nil }) } return nil }) return nil }(k) if err != nil { return nil, err } } if err := eg.Wait(); err != nil { return nil, err } return resp, nil } func pushWithMoby(ctx context.Context, d driver.Driver, name string, l progress.SubLogger) error { api := d.Config().DockerAPI if api == nil { return errors.Errorf("invalid empty Docker API reference") // should never happen } creds, err := imagetools.RegistryAuthForRef(name, d.Config().Auth) if err != nil { return err } rc, err := api.ImagePush(ctx, name, types.ImagePushOptions{ RegistryAuth: creds, }) if err != nil { return err } started := map[string]*client.VertexStatus{} defer func() { for _, st := range started { if st.Completed == nil { now := time.Now() st.Completed = &now l.SetStatus(st) } } }() dec := json.NewDecoder(rc) var parsedError error for { var jm jsonmessage.JSONMessage if err := dec.Decode(&jm); err != nil { if parsedError != nil { return parsedError } if err == io.EOF { break } return err } if jm.ID != "" { id := "pushing layer " + jm.ID st, ok := started[id] if !ok { if jm.Progress != nil || jm.Status == "Pushed" { now := time.Now() st = &client.VertexStatus{ ID: id, Started: &now, } started[id] = st } else { continue } } st.Timestamp = time.Now() if jm.Progress != nil { st.Current = jm.Progress.Current st.Total = jm.Progress.Total } if jm.Error != nil { now := time.Now() st.Completed = &now } if jm.Status == "Pushed" { now := time.Now() st.Completed = &now st.Current = st.Total } l.SetStatus(st) } if jm.Error != nil { parsedError = jm.Error } } return nil } func remoteDigestWithMoby(ctx context.Context, d driver.Driver, name string) (string, error) { api := d.Config().DockerAPI if api == nil { return "", errors.Errorf("invalid empty Docker API reference") // should never happen } creds, err := imagetools.RegistryAuthForRef(name, d.Config().Auth) if err != nil { return "", err } image, _, err := api.ImageInspectWithRaw(ctx, name) if err != nil { return "", err } if len(image.RepoDigests) == 0 { return "", nil } remoteImage, err := api.DistributionInspect(ctx, name, creds) if err != nil { return "", err } 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 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 } 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) } func waitContextDeps(ctx context.Context, index int, results *waitmap.Map, so *client.SolveOpt) error { m := map[string]string{} for k, v := range so.FrontendAttrs { if strings.HasPrefix(k, "context:") && strings.HasPrefix(v, "target:") { target := resultKey(index, strings.TrimPrefix(v, "target:")) m[target] = k } } if len(m) == 0 { return nil } keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } res, err := results.Get(ctx, keys...) if err != nil { return err } for k, v := range m { r, ok := res[k] if !ok { continue } rr, ok := r.(*gateway.Result) if !ok { return errors.Errorf("invalid result type %T", rr) } if so.FrontendAttrs == nil { so.FrontendAttrs = map[string]string{} } if so.FrontendInputs == nil { so.FrontendInputs = map[string]llb.State{} } if len(rr.Refs) > 0 { for platform, r := range rr.Refs { st, err := r.ToState() if err != nil { return err } so.FrontendInputs[k+"::"+platform] = st so.FrontendAttrs[v+"::"+platform] = "input:" + k + "::" + platform metadata := make(map[string][]byte) if dt, ok := rr.Metadata[exptypes.ExporterImageConfigKey+"/"+platform]; ok { metadata[exptypes.ExporterImageConfigKey] = dt } if dt, ok := rr.Metadata["containerimage.buildinfo/"+platform]; ok { metadata["containerimage.buildinfo"] = dt } if len(metadata) > 0 { dt, err := json.Marshal(metadata) if err != nil { return err } so.FrontendAttrs["input-metadata:"+k+"::"+platform] = string(dt) } } delete(so.FrontendAttrs, v) } if rr.Ref != nil { st, err := rr.Ref.ToState() if err != nil { return err } so.FrontendInputs[k] = st so.FrontendAttrs[v] = "input:" + k metadata := make(map[string][]byte) if dt, ok := rr.Metadata[exptypes.ExporterImageConfigKey]; ok { metadata[exptypes.ExporterImageConfigKey] = dt } if dt, ok := rr.Metadata["containerimage.buildinfo"]; ok { metadata["containerimage.buildinfo"] = dt } if len(metadata) > 0 { dt, err := json.Marshal(metadata) if err != nil { return err } so.FrontendAttrs["input-metadata:"+k] = string(dt) } } } return nil } func notSupported(d driver.Driver, f driver.Feature) error { return errors.Errorf("%s feature is currently not supported for %s driver. Please switch to a different driver (eg. \"docker buildx create --use\")", f, d.Factory().Name()) } 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 wrapWriteCloser(wc io.WriteCloser) func(map[string]string) (io.WriteCloser, error) { return func(map[string]string) (io.WriteCloser, error) { return wc, nil } } var nodeIdentifierMu sync.Mutex func tryNodeIdentifier(configDir string) (out string) { nodeIdentifierMu.Lock() defer nodeIdentifierMu.Unlock() sessionFile := filepath.Join(configDir, ".buildNodeID") if _, err := os.Lstat(sessionFile); err != nil { if os.IsNotExist(err) { // create a new file with stored randomness b := make([]byte, 8) if _, err := rand.Read(b); err != nil { return out } if err := os.WriteFile(sessionFile, []byte(hex.EncodeToString(b)), 0600); err != nil { return out } } } dt, err := os.ReadFile(sessionFile) if err == nil { return string(dt) } return } func noPrintFunc(opt map[string]Options) bool { for _, v := range opt { if v.PrintFunc != nil { return false } } return true } func saveLocalState(so client.SolveOpt, opt Options, node builder.Node, configDir string) error { var err error if so.Ref == "" { return nil } lp := opt.Inputs.ContextPath dp := opt.Inputs.DockerfilePath if lp != "" || dp != "" { if lp != "" { lp, err = filepath.Abs(lp) if err != nil { return err } } if dp != "" { dp, err = filepath.Abs(dp) if err != nil { return err } } ls, err := localstate.New(configDir) if err != nil { return err } if err := ls.SaveRef(node.Builder, node.Name, so.Ref, localstate.State{ LocalPath: lp, DockerfilePath: dp, }); err != nil { return err } } return nil } // ReadSourcePolicy reads a source policy from a file. // The file path is taken from EXPERIMENTAL_BUILDKIT_SOURCE_POLICY env var. // if the env var is not set, this `returns nil, nil` func ReadSourcePolicy() (*spb.Policy, error) { p := os.Getenv("EXPERIMENTAL_BUILDKIT_SOURCE_POLICY") if p == "" { return nil, nil } data, err := os.ReadFile(p) if err != nil { return nil, errors.Wrap(err, "failed to read policy file") } var pol spb.Policy if err := json.Unmarshal(data, &pol); err != nil { // maybe it's in protobuf format? e2 := pol.Unmarshal(data) if e2 != nil { return nil, errors.Wrap(err, "failed to parse source policy") } } return &pol, nil }