Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -766,8 +766,12 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[
}
node := dp.Node().Driver
if node.IsMobyDriver() {
features, err := node.Features(ctx)
if err != nil {
return errors.Wrap(err, "failed to detect driver features")
}
for _, e := range so.Exports {
if e.Type == "moby" && e.Attrs["push"] != "" && !node.Features(ctx)[driver.DirectPush] {
if e.Type == "moby" && e.Attrs["push"] != "" && !features[driver.DirectPush] {
if ok, _ := strconv.ParseBool(e.Attrs["push"]); ok {
pushNames = e.Attrs["name"]
if pushNames == "" {
Expand Down
38 changes: 27 additions & 11 deletions build/opt.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ func isPolicyEvaluationError(policies []*policy.Policy, err error) bool {
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
driverFeatures, err := nodeDriver.Features(ctx)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to detect driver features")
}
defers := make([]func(error), 0, 2)
releaseF := func(inErr error) {
for _, f := range defers {
Expand All @@ -269,7 +273,7 @@ func toSolveOpt(ctx context.Context, np *noderesolver.ResolvedNode, multiDriver
}

for _, e := range opt.CacheTo {
if e.Type != "inline" && !nodeDriver.Features(ctx)[driver.CacheExport] {
if e.Type != "inline" && !driverFeatures[driver.CacheExport] {
return nil, nil, notSupported(driver.CacheExport, nodeDriver, "https://docs.docker.com/go/build-cache-backends/")
}
}
Expand Down Expand Up @@ -346,10 +350,10 @@ func toSolveOpt(ctx context.Context, np *noderesolver.ResolvedNode, multiDriver
}
}

supportAttestations := bopts.LLBCaps.Contains(apicaps.CapID("exporter.image.attestations")) && nodeDriver.Features(ctx)[driver.MultiPlatform]
supportAttestations := bopts.LLBCaps.Contains(apicaps.CapID("exporter.image.attestations")) && driverFeatures[driver.MultiPlatform]
if len(attests) > 0 {
if !supportAttestations {
if !nodeDriver.Features(ctx)[driver.MultiPlatform] {
if !driverFeatures[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")
Expand Down Expand Up @@ -391,7 +395,7 @@ func toSolveOpt(ctx context.Context, np *noderesolver.ResolvedNode, multiDriver
// 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] {
} else if driverFeatures[driver.DefaultLoad] {
opt.Exports = []client.ExportEntry{{Type: "docker", Attrs: map[string]string{}}}
}
}
Expand All @@ -402,7 +406,7 @@ func toSolveOpt(ctx context.Context, np *noderesolver.ResolvedNode, multiDriver
}

// 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] {
if len(opt.Exports) > 0 && opt.CallFunc == nil && len(opt.Annotations) > 0 && nodeDriver.IsMobyDriver() && !driverFeatures[driver.MultiPlatform] {
for _, exp := range opt.Exports {
if exp.Type == "image" || exp.Type == "docker" {
for ak := range opt.Annotations {
Expand Down Expand Up @@ -476,12 +480,24 @@ func toSolveOpt(ctx context.Context, np *noderesolver.ResolvedNode, multiDriver

// set up exporters
for i, e := range so.Exports {
if e.Type == "oci" && !nodeDriver.Features(ctx)[driver.OCIExporter] {
if e.Type == "oci" && !driverFeatures[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 {
var features map[dockerutil.Feature]bool
if e.Output == nil {
contextName := e.Attrs["context"]
if nodeDriver.IsMobyDriver() {
// The docker driver loads into its own daemon.
contextName = node.Endpoint
}
var err error
features, err = docker.Features(ctx, contextName)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to detect docker features")
}
}
if features[dockerutil.OCIImporter] {
// rely on oci importer if available (which supports
// multi-platform images), otherwise fall back to docker
so.Exports[i].Type = "oci"
Expand Down Expand Up @@ -511,14 +527,14 @@ func toSolveOpt(ctx context.Context, np *noderesolver.ResolvedNode, multiDriver
so.Exports[i].Attrs["prefer-image-digest"] = "true"
}
}
} else if !nodeDriver.Features(ctx)[driver.DockerExporter] {
} else if !driverFeatures[driver.DockerExporter] {
return nil, nil, notSupported(driver.DockerExporter, nodeDriver, "https://docs.docker.com/go/build-exporters/")
}
}
if e.Type == "image" && nodeDriver.IsMobyDriver() {
so.Exports[i].Type = "moby"
// The containerd image store resolves images by manifest or index digest.
if nodeDriver.Features(ctx)[driver.PreferImageDigest] {
if driverFeatures[driver.PreferImageDigest] {
so.Exports[i].Attrs["prefer-image-digest"] = "true"
}
if e.Attrs["push"] != "" {
Expand Down Expand Up @@ -605,7 +621,7 @@ func toSolveOpt(ctx context.Context, np *noderesolver.ResolvedNode, multiDriver
for i, p := range opt.Platforms {
pp[i] = platforms.FormatAll(p)
}
if len(pp) > 1 && !nodeDriver.Features(ctx)[driver.MultiPlatform] {
if len(pp) > 1 && !driverFeatures[driver.MultiPlatform] {
return nil, nil, notSupported(driver.MultiPlatform, nodeDriver, "https://docs.docker.com/go/build-multi-platform/")
}
so.FrontendAttrs["platform"] = strings.Join(pp, ",")
Expand Down
183 changes: 183 additions & 0 deletions build/opt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,26 @@ package build

import (
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"sync/atomic"
"testing"

noderesolver "github.com/docker/buildx/build/resolver"
"github.com/docker/buildx/builder"
"github.com/docker/buildx/driver"
"github.com/docker/buildx/policy"
"github.com/docker/buildx/store"
"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/progress"
"github.com/docker/cli/cli/command"
contextstore "github.com/docker/cli/cli/context/store"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/ociindex"
gateway "github.com/moby/buildkit/frontend/gateway/client"
Expand All @@ -21,6 +34,176 @@ import (
"github.com/stretchr/testify/require"
)

type exporterTestDriver struct {
driver.Driver
moby bool
features func(context.Context) (map[driver.Feature]bool, error)
}

func (d exporterTestDriver) Info(context.Context) (*driver.Info, error) {
return &driver.Info{Status: driver.Running}, nil
}

func (d exporterTestDriver) Client(context.Context, ...client.ClientOpt) (*client.Client, error) {
return nil, nil
}

func (d exporterTestDriver) IsMobyDriver() bool {
return d.moby
}

func (d exporterTestDriver) Features(ctx context.Context) (map[driver.Feature]bool, error) {
if d.features != nil {
return d.features(ctx)
}
return map[driver.Feature]bool{driver.DockerExporter: true}, nil
}

type exporterTestCLI struct {
command.Cli
store contextstore.Store
currentContext string
}

func (c exporterTestCLI) ContextStore() contextstore.Store {
return c.store
}

func (c exporterTestCLI) CurrentContext() string {
return c.currentContext
}

func TestDockerExporterFeatureProbe(t *testing.T) {
var goodCalls, badCalls atomic.Int32
newServer := func(available bool, calls *atomic.Int32) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/_ping") {
w.Header().Set("API-Version", "1.55")
return
}
if !strings.HasSuffix(r.URL.Path, "/info") {
http.NotFound(w, r)
return
}
calls.Add(1)
w.Header().Set("Content-Type", "application/json")
if !available {
w.WriteHeader(http.StatusServiceUnavailable)
_, _ = io.WriteString(w, `{"message":"daemon unavailable"}`)
return
}
_, _ = io.WriteString(w, `{}`)
}))
t.Cleanup(server.Close)
return server
}
good := newServer(true, &goodCalls)
bad := newServer(false, &badCalls)
for _, tt := range []struct {
name string
moby bool
endpoint string
currentContext string
exportContext string
tarball bool
wantError bool
}{
{name: "docker uses builder", moby: true, endpoint: good.URL, currentContext: bad.URL},
{name: "docker ignores output context", moby: true, endpoint: good.URL, currentContext: bad.URL, exportContext: bad.URL},
{name: "docker reports builder failure", moby: true, endpoint: bad.URL, currentContext: good.URL, wantError: true},
{name: "remote uses output context", endpoint: bad.URL, currentContext: bad.URL, exportContext: good.URL},
{name: "remote reports current context failure", endpoint: good.URL, currentContext: bad.URL, wantError: true},
{name: "tarball skips daemon probe", endpoint: bad.URL, currentContext: bad.URL, tarball: true},
} {
t.Run(tt.name, func(t *testing.T) {
goodCalls.Store(0)
badCalls.Store(0)
nodes, err := noderesolver.Resolve(t.Context(), []builder.Node{{
Node: store.Node{Endpoint: tt.endpoint},
Driver: &driver.DriverHandle{Driver: exporterTestDriver{moby: tt.moby}},
}}, nil, nil)
require.NoError(t, err)
require.Len(t, nodes, 1)
cli := exporterTestCLI{
store: contextstore.New(t.TempDir(), command.DefaultContextStoreConfig()),
currentContext: tt.currentContext,
}
export := client.ExportEntry{Type: "docker", Attrs: map[string]string{"context": tt.exportContext}}
if tt.tarball {
export.Output = func(map[string]string) (io.WriteCloser, error) { return nil, nil }
}
opt := &Options{
Inputs: Inputs{ContextPath: "https://example.com/context.tar.gz"},
Exports: []client.ExportEntry{export},
Policy: []buildflags.PolicyConfig{{Disabled: true}},
}
cfg := confutil.NewConfig(nil, confutil.WithDir(t.TempDir()))
so, release, err := toSolveOpt(t.Context(), nodes[0], false, opt, gateway.BuildOpts{}, cfg, testProgressWriter{}, dockerutil.NewClient(cli))
if tt.wantError {
require.ErrorContains(t, err, "failed to detect docker features")
require.ErrorContains(t, err, "daemon unavailable")
require.EqualValues(t, 1, badCalls.Load())
require.Zero(t, goodCalls.Load())
return
}
require.NoError(t, err)
defer release(nil)
require.Zero(t, badCalls.Load())
if tt.tarball {
require.Zero(t, goodCalls.Load())
} else {
require.EqualValues(t, 1, goodCalls.Load())
}
if tt.moby {
require.Equal(t, "moby", so.Exports[0].Type)
} else {
require.Equal(t, "docker", so.Exports[0].Type)
}
})
}
}

func TestDriverFeatureFailurePreservesProvenance(t *testing.T) {
t.Setenv(noDefaultAttestationsEnv, "false")
probeErr := errors.New("worker is starting")
calls := 0
d := exporterTestDriver{
moby: true,
features: func(context.Context) (map[driver.Feature]bool, error) {
calls++
if calls == 1 {
return nil, probeErr
}
return map[driver.Feature]bool{driver.MultiPlatform: true}, nil
},
}
nodes, err := noderesolver.Resolve(t.Context(), []builder.Node{{
Driver: &driver.DriverHandle{Driver: d},
}}, nil, nil)
require.NoError(t, err)
require.Len(t, nodes, 1)
opt := &Options{
Inputs: Inputs{ContextPath: "https://example.com/context.tar.gz"},
Exports: []client.ExportEntry{{Type: "image", Attrs: map[string]string{}}},
Platforms: []ocispecs.Platform{{OS: "linux", Architecture: "amd64"}, {OS: "linux", Architecture: "arm64"}},
Policy: []buildflags.PolicyConfig{{Disabled: true}},
}
cfg := confutil.NewConfig(nil, confutil.WithDir(t.TempDir()))
bopts := buildOptsWithCaps(apicaps.CapID("exporter.image.attestations"))
so, release, err := toSolveOpt(t.Context(), nodes[0], false, opt, bopts, cfg, testProgressWriter{}, nil)
require.ErrorIs(t, err, probeErr)
require.ErrorContains(t, err, "failed to detect driver features")
require.Nil(t, so)
require.Nil(t, release)
require.Equal(t, 1, calls)

so, release, err = toSolveOpt(t.Context(), nodes[0], false, opt, bopts, cfg, testProgressWriter{}, nil)
require.NoError(t, err)
defer release(nil)
require.Equal(t, "mode=min,inline-only=true", so.FrontendAttrs["attest:provenance"])
require.Equal(t, 2, calls)
}

func TestCacheOptions_DerivedVars(t *testing.T) {
t.Setenv("ACTIONS_RUNTIME_TOKEN", "sensitive_token")
t.Setenv("ACTIONS_CACHE_URL", "https://cache.github.com")
Expand Down
20 changes: 12 additions & 8 deletions commands/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,18 @@ func runInspect(ctx context.Context, dockerCli command.Cli, in inspectOptions) e
}
if debug.IsEnabled() {
fmt.Fprintf(w, "Features:\n")
features := nodes[i].Driver.Features(ctx)
featKeys := make([]string, 0, len(features))
for k := range features {
featKeys = append(featKeys, string(k))
}
sort.Strings(featKeys)
for _, k := range featKeys {
fmt.Fprintf(w, "\t%s:\t%t\n", k, features[driver.Feature(k)])
features, err := nodes[i].Driver.Features(timeoutCtx)
if err != nil {
fmt.Fprintf(w, "\tError:\t%s\n", err.Error())
} else {
featKeys := make([]string, 0, len(features))
for k := range features {
featKeys = append(featKeys, string(k))
}
sort.Strings(featKeys)
for _, k := range featKeys {
fmt.Fprintf(w, "\t%s:\t%t\n", k, features[driver.Feature(k)])
}
}
}
if len(nodes[i].Labels) > 0 {
Expand Down
4 changes: 2 additions & 2 deletions driver/cloud/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,15 @@ func (d *Driver) Client(ctx context.Context, opts ...client.ClientOpt) (*client.
return c, nil
}

func (d *Driver) Features(_ context.Context) map[driver.Feature]bool {
func (d *Driver) Features(_ context.Context) (map[driver.Feature]bool, error) {
return map[driver.Feature]bool{
driver.OCIExporter: true,
driver.DockerExporter: false,
driver.CacheExport: true,
driver.MultiPlatform: true,
driver.DirectPush: true,
driver.DefaultLoad: d.defaultLoad,
}
}, nil
}

func (d *Driver) Factory() driver.Factory {
Expand Down
4 changes: 2 additions & 2 deletions driver/docker-container/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -581,15 +581,15 @@ func (d *Driver) Factory() driver.Factory {
return d.factory
}

func (d *Driver) Features(ctx context.Context) map[driver.Feature]bool {
func (d *Driver) Features(ctx context.Context) (map[driver.Feature]bool, error) {
return map[driver.Feature]bool{
driver.OCIExporter: true,
driver.DockerExporter: true,
driver.CacheExport: true,
driver.MultiPlatform: true,
driver.DirectPush: true,
driver.DefaultLoad: d.defaultLoad,
}
}, nil
}

func (d *Driver) HostGatewayIP(ctx context.Context) (net.IP, error) {
Expand Down
Loading
Loading