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>
1565 lines
42 KiB
Go
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
|
|
}
|