Files
buildx/build/opt.go
T
Paweł Gronowski 21de7d2078 build: Fix iidfile for containerd-backed Docker driver
Images loaded into Docker's containerd image store are identified by
their manifest or index digest, while the legacy graphdriver store uses
the config digest.

buildx prefers containerimage.config.digest when the Moby exporter
returns both digests.
Some exporter responses still produce a usable iidfile, but affected
builds, such as those with attestations disabled, write a config digest
that Docker image commands cannot resolve.

Expose the Docker driver's image-store mode as a feature and set
prefer-image-digest for containerd-backed Moby exports.

Signed-off-by: Paweł Gronowski <git@grono.dev>
2026-07-13 20:01:34 +02:00

1565 lines
42 KiB
Go

package build
import (
"bytes"
"context"
"fmt"
"io"
"maps"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
"syscall"
"time"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/containerd/console"
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/plugins/content/local"
"github.com/containerd/platforms"
"github.com/distribution/reference"
noderesolver "github.com/docker/buildx/build/resolver"
"github.com/docker/buildx/driver"
"github.com/docker/buildx/policy"
"github.com/docker/buildx/util/buildflags"
"github.com/docker/buildx/util/confutil"
"github.com/docker/buildx/util/dockerutil"
"github.com/docker/buildx/util/ocilayout"
"github.com/docker/buildx/util/osutil"
"github.com/docker/buildx/util/progress"
"github.com/docker/buildx/util/sourcemeta"
"github.com/docker/buildx/util/urlutil"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/client/ociindex"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
"github.com/moby/buildkit/frontend/dockerfile/dfgitutil"
"github.com/moby/buildkit/frontend/dockerui"
gateway "github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/identity"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/secrets/secretsprovider"
"github.com/moby/buildkit/session/sshforward/sshprovider"
"github.com/moby/buildkit/session/upload/uploadprovider"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/sourcepolicy/policysession"
"github.com/moby/buildkit/util/apicaps"
"github.com/moby/buildkit/util/entitlements"
"github.com/moby/buildkit/util/gitutil"
"github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/tonistiigi/fsutil"
)
var sendGitQueryAsInput = sync.OnceValue(func() bool {
if v, ok := os.LookupEnv("BUILDX_SEND_GIT_QUERY_AS_INPUT"); ok {
if vv, err := strconv.ParseBool(v); err == nil {
return vv
}
}
return false
})
// defaultPolicyEnabled reports whether the builtin default source policy is
// enabled via the BUILDX_DEFAULT_POLICY environment variable. It is opt-in
// for now; a future release may flip the default to on.
var defaultPolicyEnabled = sync.OnceValue(func() bool {
if v, ok := os.LookupEnv("BUILDX_DEFAULT_POLICY"); ok {
if vv, err := strconv.ParseBool(v); err == nil {
return vv
}
}
return false
})
// policyExplicitlyDisabled reports whether the user passed `--policy
// disabled=true`, which suppresses both user-defined and builtin default
// policies.
func policyExplicitlyDisabled(configs []buildflags.PolicyConfig) bool {
for _, cfg := range configs {
if cfg.Disabled {
return true
}
}
return false
}
type policyProgressLogger struct {
ch chan *client.SolveStatus
done chan struct{}
dgst digest.Digest
name string
started time.Time
mu sync.Mutex
timer *time.Timer
window int
open bool
closed bool
}
const policyProgressWindow = 500 * time.Millisecond
func newPolicyProgressLogger(pw progress.Writer, name string) *policyProgressLogger {
if pw == nil {
return nil
}
ch, done := progress.NewChannel(pw)
dgst := digest.FromBytes([]byte(identity.NewID()))
return &policyProgressLogger{
ch: ch,
done: done,
dgst: dgst,
name: name,
}
}
func (l *policyProgressLogger) Log(msg string) {
if l == nil || msg == "" {
return
}
needStart := false
var started time.Time
var window int
l.mu.Lock()
if l.closed {
l.mu.Unlock()
return
}
if !l.open {
needStart = true
l.open = true
l.window++
window = l.window
started = time.Now()
l.started = started
} else {
window = l.window
}
if l.timer != nil {
l.timer.Stop()
}
l.timer = time.AfterFunc(policyProgressWindow, func() {
l.completeWindow(window, nil)
})
if needStart {
l.sendVertexStart(started)
}
if !strings.HasSuffix(msg, "\n") {
msg += "\n"
}
l.ch <- &client.SolveStatus{
Logs: []*client.VertexLog{{
Vertex: l.dgst,
Stream: 1,
Data: []byte(msg),
Timestamp: time.Now(),
}},
}
l.mu.Unlock()
}
func (l *policyProgressLogger) Write(p []byte) (int, error) {
if len(p) > 0 {
l.Log(string(p))
}
return len(p), nil
}
func (l *policyProgressLogger) Close(err error) {
if l == nil {
return
}
shouldComplete := false
var started time.Time
l.mu.Lock()
if l.closed {
l.mu.Unlock()
return
}
l.closed = true
if l.open {
shouldComplete = true
started = l.started
l.open = false
} else if err != nil && !l.started.IsZero() {
shouldComplete = true
started = l.started
}
l.window++
if l.timer != nil {
l.timer.Stop()
l.timer = nil
}
if shouldComplete {
l.sendVertexComplete(started, err)
}
l.mu.Unlock()
close(l.ch)
<-l.done
}
func (l *policyProgressLogger) completeWindow(window int, err error) {
l.mu.Lock()
if l.closed || !l.open || window != l.window {
l.mu.Unlock()
return
}
started := l.started
l.open = false
l.sendVertexComplete(started, err)
l.mu.Unlock()
}
func (l *policyProgressLogger) sendVertexStart(started time.Time) {
vtx := client.Vertex{
Digest: l.dgst,
Name: l.name,
Started: &started,
}
l.ch <- &client.SolveStatus{Vertexes: []*client.Vertex{&vtx}}
}
func (l *policyProgressLogger) sendVertexComplete(started time.Time, err error) {
tm := time.Now()
vtx := client.Vertex{
Digest: l.dgst,
Name: l.name,
Started: &started,
Completed: &tm,
}
if err != nil {
vtx.Error = err.Error()
}
l.ch <- &client.SolveStatus{Vertexes: []*client.Vertex{&vtx}}
}
func isPolicyEvaluationError(policies []*policy.Policy, err error) bool {
for _, p := range policies {
if p != nil && p.IsPolicyError(err) {
return true
}
}
return false
}
func toSolveOpt(ctx context.Context, np *noderesolver.ResolvedNode, multiDriver bool, opt *Options, bopts gateway.BuildOpts, cfg *confutil.Config, pw progress.Writer, docker *dockerutil.Client) (_ *client.SolveOpt, release func(error), err error) {
node := np.Node()
nodeDriver := node.Driver
defers := make([]func(error), 0, 2)
releaseF := func(inErr error) {
for _, f := range defers {
f(inErr)
}
}
defer func() {
if err != nil {
releaseF(err)
}
}()
// 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{},
LocalMounts: map[string]fsutil.FS{},
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_SYNTAX"]; ok {
cmdline := strings.TrimSpace(v)
if cmdline == "" {
return nil, nil, errors.Errorf("empty BUILDKIT_SYNTAX build-arg is invalid, use --build-arg BUILDKIT_SYNTAX without '=' for optional behavior")
}
p := strings.SplitN(cmdline, " ", 2)
so.Frontend = "gateway.v0"
so.FrontendAttrs["source"] = p[0]
so.FrontendAttrs["cmdline"] = v
}
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 !noDefaultLoad() && opt.CallFunc == nil {
if nodeDriver.IsMobyDriver() {
// 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{}}}
} else if nodeDriver.Features(ctx)[driver.DefaultLoad] {
opt.Exports = []client.ExportEntry{{Type: "docker", 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")
}
}
// check if index annotations are supported by docker driver
if len(opt.Exports) > 0 && opt.CallFunc == nil && len(opt.Annotations) > 0 && nodeDriver.IsMobyDriver() && !nodeDriver.Features(ctx)[driver.MultiPlatform] {
for _, exp := range opt.Exports {
if exp.Type == "image" || exp.Type == "docker" {
for ak := range opt.Annotations {
switch ak.Type {
case exptypes.AnnotationIndex, exptypes.AnnotationIndexDescriptor:
return nil, nil, errors.New("index annotations not supported for single platform export")
}
}
}
}
}
// 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, func(error) {
cancel()
})
opt.Exports[i].Output = func(_ map[string]string) (io.WriteCloser, error) {
return w, nil
}
// if docker is using the containerd snapshotter, prefer to export the image digest
// (rather than the image config digest). See https://github.com/moby/moby/issues/45458.
if features[dockerutil.OCIImporter] {
opt.Exports[i].Attrs["prefer-image-digest"] = "true"
}
}
} 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"
// The containerd image store resolves images by manifest or index digest.
if nodeDriver.Features(ctx)[driver.PreferImageDigest] {
opt.Exports[i].Attrs["prefer-image-digest"] = "true"
}
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 {
opt.Exports[i].Attrs["buildinfo-attrs"] = v
}
}
}
so.Exports = opt.Exports
so.Session = slices.Clone(opt.Session)
for k, v := range opt.BuildArgs {
so.FrontendAttrs["build-arg:"+k] = v
}
for k, v := range opt.Labels {
so.FrontendAttrs["label:"+k] = v
}
releaseLoad, err := loadInputs(ctx, nodeDriver, &opt.Inputs, pw, &so)
if err != nil {
return nil, nil, err
}
defers = append(defers, func(error) {
releaseLoad()
})
policyDefers, err := configureSourcePolicy(ctx, np, opt, cfg, bopts, &so, pw)
if err != nil {
return nil, nil, err
}
defers = append(defers, policyDefers...)
// add node identifier to shared key if one was specified
nodeID := cfg.TryNodeIdentifier()
if so.SharedKey != "" {
so.SharedKey += ":" + nodeID
}
for k, v := range so.FrontendAttrs {
if strings.HasPrefix(k, "sharedkey:localdir:") {
so.FrontendAttrs[k] = v + ":" + nodeID
}
}
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 node.ProxyConfig {
if !proxyArgKeyExists(opt.BuildArgs, k) {
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.String())
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
}
// setup per-step resource limits
addResourceLimits(opt.ResourceLimits, so.FrontendAttrs)
// mark call request as internal
if opt.CallFunc != nil {
so.Internal = true
}
return &so, releaseF, nil
}
func proxyArgKeyExists(buildArgs map[string]string, key string) bool {
for k := range buildArgs {
if strings.EqualFold(k, key) {
return true
}
}
return false
}
func configureSourcePolicy(ctx context.Context, np *noderesolver.ResolvedNode, opt *Options, cfg *confutil.Config, bopts gateway.BuildOpts, so *client.SolveOpt, pw progress.Writer) (_ []func(error), err error) {
if opt.Inputs.policy == nil {
if len(opt.Policy) > 0 {
return nil, errors.New("policy file specified but no policy FS in build context")
}
so.SourcePolicyProvider = nil
return nil, nil
}
env := policy.Env{}
for k, v := range opt.BuildArgs {
if env.Args == nil {
env.Args = map[string]*string{}
}
env.Args[k] = &v
}
env.Filename = policyEnvFilename(opt.Inputs)
env.Target = opt.Target
env.Labels = opt.Labels
popts, err := withPolicyConfig(*opt.Inputs.policy, opt.Policy)
if err != nil {
return nil, err
}
// Prepend the builtin default policy when enabled and not explicitly
// disabled. The default policy verifies trust for Docker-managed images
// (docker/dockerfile, docker/dockerfile-upstream) that may be implicitly
// loaded during a build, and passes through any other source so user
// policies retain full control.
if defaultPolicyEnabled() && !policyExplicitlyDisabled(opt.Policy) {
builtin := policyOpt{
Files: []policyFileSpec{{
Filename: policy.DefaultPolicyFilename,
Data: policy.DefaultPolicyData(),
}},
}
builtin.SkipCaps = true
popts = append([]policyOpt{builtin}, popts...)
}
if len(popts) == 0 {
so.SourcePolicyProvider = nil
return nil, nil
}
c, err := np.Client(ctx)
if err != nil {
return nil, err
}
sourceResolver := sourcemeta.NewResolver(c, sourcemeta.WithProgressWriter(pw), sourcemeta.WithSession(so.Session))
defers := []func(error){
func(error) {
_ = sourceResolver.Close()
},
}
defer func() {
if err == nil {
return
}
for _, f := range defers {
f(err)
}
defers = nil
}()
loadedOpts, err := resolvePolicyOpts(ctx, popts, sourceResolver)
if err != nil {
return nil, err
}
var policyFiles []string
for _, popt := range loadedOpts {
for _, f := range popt.Files {
if f.Filename != "" {
policyFiles = append(policyFiles, f.Filename)
}
}
}
var policyLogger *policyProgressLogger
if len(policyFiles) > 0 {
policyLogger = newPolicyProgressLogger(pw, fmt.Sprintf("loading policies %s", strings.Join(policyFiles, ", ")))
}
var policies []*policy.Policy
if policyLogger != nil {
defers = append(defers, func(inErr error) {
if len(policysession.DenyMessages(inErr)) > 0 || isPolicyEvaluationError(policies, inErr) {
policyLogger.Close(inErr)
return
}
policyLogger.Close(nil)
})
}
var cbs []policysession.PolicyCallback
for _, popt := range loadedOpts {
policyLevel := logrus.GetLevel()
if popt.LogLevel != nil {
policyLevel = *popt.LogLevel
}
logf := func(level logrus.Level, msg string) {
if policyLogger == nil || level > policyLevel {
return
}
policyLogger.Log(msg)
}
p := policy.NewPolicy(policy.Opt{
Files: popt.Files,
Env: env,
Log: logf,
FS: popt.FS,
VerifierProvider: policy.SignatureVerifier(cfg),
DefaultPlatform: defaultPlatform(bopts),
SourceResolver: sourceResolver,
})
if !popt.SkipCaps {
if err := applyPolicyCaps(ctx, p, bopts, so); err != nil {
return nil, err
}
}
policies = append(policies, p)
cbs = append(cbs, p.CheckPolicy)
if popt.Strict {
if bopts.LLBCaps.Supports(pb.CapSourcePolicySession) != nil {
return nil, errors.New("strict policy is not supported by the current BuildKit daemon, please upgrade to version v0.27+")
}
}
}
if so.ProxyNetwork {
if policyLogger != nil {
policyLogger.Log("policy enabled network proxy")
}
}
so.SourcePolicyProvider = policysession.NewPolicyProvider(policy.MultiPolicyCallback(cbs...))
return defers, nil
}
func applyPolicyCaps(ctx context.Context, p *policy.Policy, bopts gateway.BuildOpts, so *client.SolveOpt) error {
caps, err := p.CheckCaps(ctx)
if err != nil {
return errors.Wrap(err, "failed to evaluate policy caps")
}
if !caps[policy.CapExecProxy] {
return nil
}
if err := bopts.LLBCaps.Supports(pb.CapExecMetaNetworkProxy); err != nil {
return errors.New("network proxy requested by policy is not supported by the current BuildKit daemon, please upgrade to version v0.31+")
}
so.ProxyNetwork = true
return nil
}
func policyEnvFilename(inp Inputs) string {
base := filepath.Base(filepath.Clean(inp.DockerfilePath))
if base != "." && base != string(filepath.Separator) {
return base
}
return "Dockerfile"
}
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.ReadCloser
contextDir string
remoteContext bool
remotePolicyState *llb.State
dockerfileDir string
dockerfileName = inp.DockerfilePath
dockerfileSrcName = inp.DockerfilePath
toRemove []string
caps = map[string]struct{}{}
)
switch {
case inp.ContextState != nil:
remotePolicyState = inp.ContextState
if target.FrontendInputs == nil {
target.FrontendInputs = make(map[string]llb.State)
}
target.FrontendInputs["context"] = *inp.ContextState
target.FrontendInputs["dockerfile"] = *inp.ContextState
if _, ok, _ := dfgitutil.ParseGitRef(inp.ContextPath); ok {
target.FrontendAttrs["input:context"] = inp.ContextPath
}
case inp.ContextPath == "-":
if inp.DockerfilePath == "-" {
return nil, errors.Errorf("invalid argument: can't use stdin for both build context and dockerfile")
}
rc := inp.InStream.NewReadCloser()
magic, err := inp.InStream.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(rc)
target.Session = append(target.Session, up)
} else {
if inp.DockerfilePath != "" {
return nil, errors.Errorf("ambiguous Dockerfile source: both stdin and flag correspond to Dockerfiles")
}
// stdin is dockerfile
dockerfileReader = rc
inp.ContextPath, _ = os.MkdirTemp("", "empty-dir")
toRemove = append(toRemove, inp.ContextPath)
if err := setLocalMount("context", inp.ContextPath, target); err != nil {
return nil, err
}
contextDir = inp.ContextPath
}
}
case osutil.IsLocalDir(inp.ContextPath):
if err := setLocalMount("context", inp.ContextPath, target); err != nil {
return nil, err
}
contextDir = inp.ContextPath
sharedKey := inp.ContextPath
if p, err := filepath.Abs(sharedKey); err == nil {
sharedKey = filepath.Base(p)
}
target.SharedKey = sharedKey
switch inp.DockerfilePath {
case "-":
dockerfileReader = inp.InStream.NewReadCloser()
case "":
dockerfileDir = inp.ContextPath
default:
dockerfileDir = filepath.Dir(inp.DockerfilePath)
dockerfileName = filepath.Base(inp.DockerfilePath)
}
case urlutil.IsRemoteURL(inp.ContextPath):
remoteContext = true
if inp.DockerfilePath == "-" {
dockerfileReader = inp.InStream.NewReadCloser()
} 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
if err := processGitURL(inp.ContextPath, "context", target, caps); err != nil {
return nil, err
}
if st, ok := target.FrontendInputs["context"]; ok {
remotePolicyState = &st
if dockerfileReader == nil && !filepath.IsAbs(inp.DockerfilePath) {
target.FrontendInputs["dockerfile"] = st
}
}
default:
return nil, errors.Errorf("unable to prepare context: path %q not found", inp.ContextPath)
}
if inp.DockerfileInline != "" {
dockerfileReader = io.NopCloser(strings.NewReader(inp.DockerfileInline))
dockerfileSrcName = "inline"
} else if inp.DockerfilePath == "-" {
dockerfileSrcName = "stdin"
} else if inp.DockerfilePath == "" {
dockerfileSrcName = filepath.Join(inp.ContextPath, "Dockerfile")
}
if dockerfileReader != nil {
dockerfileDir, err = createTempDockerfile(dockerfileReader, inp.InStream)
if err != nil {
return nil, err
}
toRemove = append(toRemove, dockerfileDir)
dockerfileName = "Dockerfile"
target.FrontendAttrs["dockerfilekey"] = "dockerfile"
}
if urlutil.IsHTTPURL(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"
}
p := &policyOpt{
ContextDir: contextDir,
}
p.ContextState = remotePolicyState
if p.ContextState == nil && remoteContext {
p.ContextState = resolveRemotePolicyContextState(inp.ContextPath, target)
}
if dockerfileDir != "" {
if err := setLocalMount("dockerfile", dockerfileDir, target); err != nil {
return nil, err
}
dockerfileName = handleLowercaseDockerfile(dockerfileDir, dockerfileName)
}
defaultPolicyFilename := dockerfileName + ".rego"
if dockerfileDir != "" {
defaultPolicyFilename = filepath.Join(dockerfileDir, defaultPolicyFilename)
}
defaultPolicy := policyFileSpec{
Filename: defaultPolicyFilename,
Optional: true,
}
includeDefaultPolicy := true
if dockerfileDir != "" && p.ContextState == nil {
dt, err := os.ReadFile(defaultPolicyFilename)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return nil, errors.Wrapf(err, "failed to read policy file %s", defaultPolicyFilename)
}
includeDefaultPolicy = false
} else {
defaultPolicy.Data = dt
}
}
if includeDefaultPolicy {
p.Files = append(p.Files, defaultPolicy)
}
inp.policy = p
target.FrontendAttrs["filename"] = dockerfileName
for k, v := range inp.NamedContexts {
caps["moby.buildkit.frontend.contexts+forward"] = struct{}{}
if v.State != nil {
target.FrontendAttrs["context:"+k] = "input:" + k
if _, ok, _ := dfgitutil.ParseGitRef(v.Path); ok {
target.FrontendAttrs["input:git_state_"+k] = v.Path
}
if target.FrontendInputs == nil {
target.FrontendInputs = make(map[string]llb.State)
}
target.FrontendInputs[k] = *v.State
continue
}
if urlutil.IsRemoteURL(v.Path) || strings.HasPrefix(v.Path, "docker-image://") || strings.HasPrefix(v.Path, "target:") {
target.FrontendAttrs["context:"+k] = v.Path
processGitURL(v.Path, "context:"+k, target, caps)
continue
}
// handle OCI layout
if ref, ok, err := ocilayout.Parse(v.Path); ok {
if err != nil {
return nil, err
}
localPath := ref.Path
if ref.Digest == "" {
dig, err := resolveDigest(localPath, ref.Tag)
if err != nil {
return nil, errors.Wrapf(err, "oci-layout reference %q could not be resolved", v.Path)
}
ref.Digest = digest.Digest(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
ref.Path = storeName
target.FrontendAttrs["context:"+k] = ref.String()
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
}
if err := setLocalMount(localName, v.Path, target); err != nil {
return nil, err
}
target.FrontendAttrs["context:"+k] = "local:" + localName
sharedKey := v.Path
if p, err := filepath.Abs(sharedKey); err == nil {
sharedKey = filepath.Base(p)
} else {
sharedKey = filepath.Base(sharedKey)
}
target.FrontendAttrs["sharedkey:localdir:"+k] = sharedKey
}
release := func() {
for _, dir := range toRemove {
_ = os.RemoveAll(dir)
}
}
if len(caps) > 0 {
keys := slices.Collect(maps.Keys(caps))
slices.Sort(keys)
target.FrontendAttrs["frontend.caps"] = strings.Join(keys, ",")
}
inp.DockerfileMappingSrc = dockerfileSrcName
inp.DockerfileMappingDst = dockerfileName
return release, nil
}
func resolveRemotePolicyContextState(contextPath string, target *client.SolveOpt) *llb.State {
if target != nil && target.FrontendInputs != nil {
if st, ok := target.FrontendInputs["context"]; ok {
return &st
}
}
keepGitDir := false
if st, ok, _ := dockerui.DetectGitContext(contextPath, &keepGitDir); ok {
return st
}
st, filename, ok := dockerui.DetectHTTPContext(contextPath)
if !ok || filename == "" {
return nil
}
bc := llb.Scratch().File(llb.Copy(*st, filename, "/", &llb.CopyInfo{
AttemptUnpack: true,
}))
return &bc
}
func resolveDigest(localPath, tag string) (dig string, _ error) {
idx := ociindex.NewStoreIndex(localPath)
// lookup by name
desc, err := idx.Get(tag)
if err != nil {
return "", err
}
if desc == nil {
// lookup single
desc, err = idx.GetSingle()
if err != nil {
return "", err
}
}
if desc == nil {
return "", errors.New("failed to resolve digest")
}
dig = string(desc.Digest)
_, err = digest.Parse(dig)
if err != nil {
return "", errors.Wrapf(err, "invalid digest %s", dig)
}
return dig, nil
}
func setLocalMount(name, dir string, so *client.SolveOpt) error {
lm, err := fsutil.NewFS(dir)
if err != nil {
return err
}
if so.LocalMounts == nil {
so.LocalMounts = map[string]fsutil.FS{}
}
so.LocalMounts[name] = &fsMount{FS: lm, dir: dir}
return nil
}
func createTempDockerfile(r io.Reader, multiReader *SyncMultiReader) (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 multiReader != nil {
dt, err := io.ReadAll(r)
if err != nil {
return "", err
}
multiReader.Reset(dt)
r = bytes.NewReader(dt)
}
if _, err := io.Copy(f, r); err != nil {
return "", err
}
return dir, err
}
func processGitURL(url string, name string, target *client.SolveOpt, caps map[string]struct{}) error {
gitRef, err := gitutil.ParseURL(url)
if err != nil {
return err
}
if len(gitRef.Query) == 0 {
return nil
}
if !sendGitQueryAsInput() {
capName := "moby.buildkit.frontend.gitquerystring"
if name != "context" {
capName += "+forward"
}
caps[capName] = struct{}{}
return nil
}
var keepGitDir *bool
if name == "context" {
if v, ok := target.FrontendAttrs["build-arg:BUILDKIT_CONTEXT_KEEP_GIT_DIR"]; ok {
if vv, err := strconv.ParseBool(v); err == nil {
keepGitDir = &vv
}
}
}
st, ok, err := dockerui.DetectGitContext(url, keepGitDir)
if err != nil {
return err
}
if !ok {
return nil
}
if target.FrontendInputs == nil {
target.FrontendInputs = make(map[string]llb.State)
}
if name == "context" {
target.FrontendInputs["context"] = *st
delete(target.FrontendAttrs, "context")
target.FrontendAttrs["input:context"] = url
} else {
inputName := "git_state_" + name
target.FrontendInputs[inputName] = *st
target.FrontendAttrs[name] = "input:" + inputName
target.FrontendAttrs["input:"+name] = url
}
return nil
}
// 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
}
type fsMount struct {
fsutil.FS
dir string
}
var _ fsutil.FS = &fsMount{}
func CreateSSH(ssh []*buildflags.SSH) (session.Attachable, error) {
configs := make([]sshprovider.AgentConfig, 0, len(ssh))
for _, ssh := range ssh {
cfg := sshprovider.AgentConfig{
ID: ssh.ID,
Paths: slices.Clone(ssh.Paths),
}
configs = append(configs, cfg)
}
return sshprovider.NewSSHAgentProvider(configs)
}
func CreateSecrets(secrets []*buildflags.Secret) (session.Attachable, error) {
fs := make([]secretsprovider.Source, 0, len(secrets))
for _, secret := range secrets {
fs = append(fs, secretsprovider.Source{
ID: secret.ID,
FilePath: secret.FilePath,
Env: secret.Env,
})
}
store, err := secretsprovider.NewStore(fs)
if err != nil {
return nil, err
}
return secretsprovider.NewSecretProvider(store), nil
}
func CreateExports(entries []*buildflags.ExportEntry) ([]client.ExportEntry, []string, error) {
var outs []client.ExportEntry
var localPaths []string
if len(entries) == 0 {
return nil, nil, nil
}
var stdoutUsed bool
for _, entry := range entries {
if entry.Type == "" {
return nil, nil, errors.Errorf("type is required for output")
}
out := client.ExportEntry{
Type: entry.Type,
Attrs: map[string]string{},
}
maps.Copy(out.Attrs, entry.Attrs)
supportFile := false
supportDir := false
switch out.Type {
case client.ExporterLocal:
supportDir = true
case client.ExporterTar:
supportFile = true
case client.ExporterOCI, client.ExporterDocker:
tar, err := strconv.ParseBool(out.Attrs["tar"])
if err != nil {
tar = true
}
supportFile = tar
supportDir = !tar
case "registry":
out.Type = client.ExporterImage
out.Attrs["push"] = "true"
// Skip unpacking when only pushing to registry (unless explicitly set)
if _, ok := out.Attrs["unpack"]; !ok {
out.Attrs["unpack"] = "false"
}
}
if supportDir {
if entry.Destination == "" {
return nil, nil, errors.Errorf("dest is required for %s exporter", out.Type)
}
if entry.Destination == "-" {
return nil, nil, errors.Errorf("dest cannot be stdout for %s exporter", out.Type)
}
fi, err := os.Stat(entry.Destination)
if err != nil && !os.IsNotExist(err) {
return nil, nil, errors.Wrapf(err, "invalid destination directory: %s", entry.Destination)
}
if err == nil && !fi.IsDir() {
return nil, nil, errors.Errorf("destination directory %s is a file", entry.Destination)
}
out.OutputDir = entry.Destination
localPaths = append(localPaths, entry.Destination)
}
if supportFile {
if entry.Destination == "" && out.Type != client.ExporterDocker {
entry.Destination = "-"
}
if entry.Destination == "-" {
if stdoutUsed {
return nil, nil, errors.Errorf("multiple outputs configured to write to stdout")
}
if _, err := console.ConsoleFromFile(os.Stdout); err == nil {
return nil, nil, errors.Errorf("dest file is required for %s exporter. refusing to write to console", out.Type)
}
out.Output = wrapWriteCloser(os.Stdout)
stdoutUsed = true
} else if entry.Destination != "" {
fi, err := os.Stat(entry.Destination)
if err != nil && !os.IsNotExist(err) {
return nil, nil, errors.Wrapf(err, "invalid destination file: %s", entry.Destination)
}
if err == nil && fi.IsDir() {
return nil, nil, errors.Errorf("destination file %s is a directory", entry.Destination)
}
out.Output = wrapWriteCloserLazy(entry.Destination)
localPaths = append(localPaths, entry.Destination)
}
}
outs = append(outs, out)
}
return outs, localPaths, nil
}
func ValidateLocalExportDelete(outputs []client.ExportEntry, allowDelete bool) error {
for _, ex := range outputs {
if ex.Type != client.ExporterLocal {
continue
}
mode, err := client.ParseLocalExporterMode(ex.Attrs["mode"])
if err != nil {
return err
}
if mode != client.LocalExporterModeDelete || allowDelete {
continue
}
ok, err := isSafeLocalDeleteDest(ex.OutputDir)
if err != nil {
return err
}
if !ok {
return errors.Errorf("local output mode=delete for destination %q requires --allow=%s", ex.OutputDir, buildflags.EntitlementBuildxLocalDelete)
}
}
return nil
}
func isSafeLocalDeleteDest(dest string) (bool, error) {
wd, err := os.Getwd()
if err != nil {
return false, errors.Wrap(err, "failed to get current working directory")
}
wd, err = resolveOutputPath(wd)
if err != nil {
return false, errors.Wrap(err, "failed to evaluate current working directory")
}
dest, err = resolveOutputPath(dest)
if err != nil {
return false, errors.Wrapf(err, "failed to evaluate local output destination %q", dest)
}
rel, err := filepath.Rel(wd, dest)
if err != nil {
return false, nil
}
if rel == "." || rel == ".." || filepath.IsAbs(rel) || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return false, nil
}
return true, nil
}
func resolveOutputPath(p string) (string, error) {
p, rest, err := osutil.EvaluateToExistingPath(p)
if err != nil {
return "", err
}
p, err = osutil.GetLongPathName(p)
if err != nil {
return "", err
}
if rest != "" {
p = filepath.Join(p, rest)
}
return filepath.Clean(p), nil
}
func wrapWriteCloser(wc io.WriteCloser) func(map[string]string) (io.WriteCloser, error) {
return func(map[string]string) (io.WriteCloser, error) {
return wc, nil
}
}
type lazyFileWriter struct {
path string
file *os.File
}
func (w *lazyFileWriter) Write(p []byte) (int, error) {
if w.file == nil {
if err := os.MkdirAll(filepath.Dir(w.path), 0o755); err != nil {
return 0, err
}
f, err := os.Create(w.path)
if err != nil {
return 0, err
}
w.file = f
}
return w.file.Write(p)
}
func (w *lazyFileWriter) Close() error {
if w.file != nil {
return w.file.Close()
}
return nil
}
func wrapWriteCloserLazy(path string) func(map[string]string) (io.WriteCloser, error) {
return func(map[string]string) (io.WriteCloser, error) {
return &lazyFileWriter{path: path}, nil
}
}
func CreateCaches(entries []*buildflags.CacheOptionsEntry) []client.CacheOptionsEntry {
var outs []client.CacheOptionsEntry
if len(entries) == 0 {
return nil
}
for _, entry := range entries {
out := client.CacheOptionsEntry{
Type: entry.Type,
Attrs: map[string]string{},
}
maps.Copy(out.Attrs, entry.Attrs)
addGithubToken(&out)
addAwsCredentials(&out)
if !isActive(&out) {
continue
}
outs = append(outs, out)
}
return outs
}
func addGithubToken(ci *client.CacheOptionsEntry) {
if ci.Type != "gha" {
return
}
version, ok := ci.Attrs["version"]
if !ok {
// https://github.com/actions/toolkit/blob/2b08dc18f261b9fdd978b70279b85cbef81af8bc/packages/cache/src/internal/config.ts#L19
if v, ok := os.LookupEnv("ACTIONS_CACHE_SERVICE_V2"); ok {
if b, err := strconv.ParseBool(v); err == nil && b {
version = "2"
}
}
}
if _, ok := ci.Attrs["token"]; !ok {
if v, ok := os.LookupEnv("ACTIONS_RUNTIME_TOKEN"); ok {
ci.Attrs["token"] = v
}
}
if _, ok := ci.Attrs["url_v2"]; !ok && version == "2" {
// https://github.com/actions/toolkit/blob/2b08dc18f261b9fdd978b70279b85cbef81af8bc/packages/cache/src/internal/config.ts#L34-L35
if v, ok := os.LookupEnv("ACTIONS_RESULTS_URL"); ok {
ci.Attrs["url_v2"] = v
}
}
if _, ok := ci.Attrs["url"]; !ok {
// https://github.com/actions/toolkit/blob/2b08dc18f261b9fdd978b70279b85cbef81af8bc/packages/cache/src/internal/config.ts#L28-L33
if v, ok := os.LookupEnv("ACTIONS_CACHE_URL"); ok {
ci.Attrs["url"] = v
} else if v, ok := os.LookupEnv("ACTIONS_RESULTS_URL"); ok {
ci.Attrs["url"] = v
}
}
}
func addAwsCredentials(ci *client.CacheOptionsEntry) {
if ci.Type != "s3" {
return
}
_, okAccessKeyID := ci.Attrs["access_key_id"]
_, okSecretAccessKey := ci.Attrs["secret_access_key"]
// If the user provides access_key_id, secret_access_key, do not override the session token.
if okAccessKeyID && okSecretAccessKey {
return
}
ctx := context.TODO()
awsConfig, err := awsconfig.LoadDefaultConfig(ctx)
if err != nil {
return
}
credentials, err := awsConfig.Credentials.Retrieve(ctx)
if err != nil {
return
}
if !okAccessKeyID && credentials.AccessKeyID != "" {
ci.Attrs["access_key_id"] = credentials.AccessKeyID
}
if !okSecretAccessKey && credentials.SecretAccessKey != "" {
ci.Attrs["secret_access_key"] = credentials.SecretAccessKey
}
if _, ok := ci.Attrs["session_token"]; !ok && credentials.SessionToken != "" {
ci.Attrs["session_token"] = credentials.SessionToken
}
}
func isActive(ce *client.CacheOptionsEntry) bool {
// Always active if not gha.
if ce.Type != "gha" {
return true
}
return ce.Attrs["token"] != "" && (ce.Attrs["url"] != "" || ce.Attrs["url_v2"] != "")
}
func defaultPlatform(bopts gateway.BuildOpts) *ocispecs.Platform {
pl := bopts.Workers[0].Platforms
if len(pl) == 0 {
return nil
}
p := platforms.Normalize(pl[0])
return &p
}