Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
5 changes: 5 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ area/driver:
- changed-files:
- any-glob-to-any-file: 'driver/**'

# Add 'area/driver/cloud' label to changes in the cloud driver
area/driver/cloud:
- changed-files:
- any-glob-to-any-file: 'driver/cloud/**'

# Add 'area/driver/docker' label to changes in the docker driver
area/driver/docker:
- changed-files:
Expand Down
1 change: 1 addition & 0 deletions PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Area or component of the project affected. Please note that the table below may
| `area/dockerfile` | Any | `dockerfile` |
| `area/docs` | Any | `docs` |
| `area/driver` | Any | `driver` |
| `area/driver/cloud` | Any | `driver/cloud` |
| `area/driver/docker` | Any | `driver/docker` |
| `area/driver/docker-container` | Any | `driver/docker-container` |
| `area/driver/kubernetes` | Any | `driver/kubernetes` |
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ feature sets.

We currently support the following drivers:
- The `docker` driver ([manual](https://docs.docker.com/build/builders/drivers/docker/))
- The `cloud` driver ([manual](https://docs.docker.com/build-cloud/))
- The `docker-container` driver ([manual](https://docs.docker.com/build/builders/drivers/docker-container/))
- The `kubernetes` driver ([manual](https://docs.docker.com/build/drivers/kubernetes/))
- The `remote` driver ([manual](https://docs.docker.com/build/builders/drivers/remote/))
Expand Down Expand Up @@ -217,9 +218,11 @@ When you invoke a build, you can set the `--platform` flag to specify the target
platform for the build output, (for example, `linux/amd64`, `linux/arm64`, or
`darwin/amd64`).

When the current builder instance is backed by the `docker-container` or
`kubernetes` driver, you can specify multiple platforms together. In this case,
it builds a manifest list which contains images for all specified architectures.
When the current builder instance is backed by the `cloud`, `docker-container`,
`kubernetes` or `remote` driver, you can specify multiple platforms together.
In this case, it builds a manifest list which contains images for all specified
architectures.

When you use this image in [`docker run`](https://docs.docker.com/reference/cli/docker/container/run/)
or [`docker service`](https://docs.docker.com/reference/cli/docker/service/),
Docker picks the correct image based on the node's platform.
Expand Down
26 changes: 17 additions & 9 deletions build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,26 +261,35 @@ func findNonMobyDriver(nodes []builder.Node) *driver.DriverHandle {
return nil
}

// warnOnNoOutput will check if the given nodes and options would result in an output
// and prints a warning if it would not.
func warnOnNoOutput(ctx context.Context, nodes []builder.Node, opts map[string]Options) {
// warnOnNoOutput checks if the prepared build requests would result in an
// output and prints a warning if they would not.
func warnOnNoOutput(nodes []builder.Node, opts map[string]Options, reqForNodes map[string][]*reqForNode) {
// Return immediately if default load is explicitly disabled or a call
// function is used.
if noDefaultLoad() || !noCallFunc(opts) {
return
}

// Find the first non-moby driver and return if it either doesn't exist
// or if the driver has default load enabled.
// Find the first non-moby driver and return if it doesn't exist.
noMobyDriver := findNonMobyDriver(nodes)
if noMobyDriver == nil || noMobyDriver.Features(ctx)[driver.DefaultLoad] {
if noMobyDriver == nil {
return
}

// Produce a warning describing the targets affected.
var noOutputTargets []string
for name, opt := range opts {
if !opt.Linked && len(opt.Exports) == 0 {
if opt.Linked || len(opt.Exports) > 0 {
continue
}
hasOutput := false
for _, req := range reqForNodes[name] {
if len(req.so.Exports) > 0 || req.so.EnableSessionExporter {
hasOutput = true
break
}
}
if !hasOutput {
noOutputTargets = append(noOutputTargets, name)
}
}
Expand Down Expand Up @@ -490,8 +499,6 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[
if err != nil {
return nil, errors.Wrapf(err, "no valid drivers found")
}
warnOnNoOutput(ctx, nodes, opts)

optPlatforms := make(map[string][]ocispecs.Platform, len(opts))
for k, opt := range opts {
optPlatforms[k] = opt.Platforms
Expand All @@ -509,6 +516,7 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[
defer func() {
release(err)
}()
warnOnNoOutput(nodes, opts, reqForNodes)

// validate that all links between targets use same drivers
if err := validateTargetLinks(reqForNodes, drivers, opts); err != nil {
Expand Down
184 changes: 184 additions & 0 deletions build/build_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package build

import (
"bytes"
"testing"

"github.com/docker/buildx/builder"
"github.com/docker/buildx/driver"
"github.com/moby/buildkit/client"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

type warnOutputFactory struct {
driver.Factory
name string
}

func (f warnOutputFactory) Name() string {
return f.name
}

type warnOutputDriver struct {
driver.Driver
factory driver.Factory
moby bool
}

func (d warnOutputDriver) Factory() driver.Factory {
return d.factory
}

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

func TestWarnOnNoOutput(t *testing.T) {
cloudNodes := []builder.Node{{Driver: newWarnOutputDriver("cloud", false)}}
mobyNodes := []builder.Node{{Driver: newWarnOutputDriver("docker", true)}}
defaultOpts := map[string]Options{"default": {}}

tests := []struct {
name string
nodes []builder.Node
opts map[string]Options
reqForNodes map[string][]*reqForNode
wantWarning string
}{
{
name: "NoNonMobyDriver",
opts: defaultOpts,
},
{
name: "MobyDriver",
nodes: mobyNodes,
opts: defaultOpts,
},
{
name: "NoRequests",
nodes: cloudNodes,
opts: defaultOpts,
wantWarning: "No output specified with cloud driver.",
},
{
name: "NoOutput",
nodes: cloudNodes,
opts: defaultOpts,
reqForNodes: reqForDefaultTarget(&client.SolveOpt{}),
wantWarning: "No output specified with cloud driver.",
},
{
name: "DefaultLoadPreparedExporter",
nodes: cloudNodes,
opts: defaultOpts,
reqForNodes: reqForDefaultTarget(&client.SolveOpt{
Exports: []client.ExportEntry{{Type: "docker"}},
}),
},
{
name: "SessionExporter",
nodes: cloudNodes,
opts: defaultOpts,
reqForNodes: reqForDefaultTarget(&client.SolveOpt{
EnableSessionExporter: true,
}),
},
{
name: "OutputOnOneNode",
nodes: cloudNodes,
opts: defaultOpts,
reqForNodes: reqForDefaultTarget(
&client.SolveOpt{},
&client.SolveOpt{Exports: []client.ExportEntry{{Type: "image"}}},
),
},
{
name: "Linked",
nodes: cloudNodes,
opts: map[string]Options{"default": {Linked: true}},
},
{
name: "ExplicitCacheOnly",
nodes: cloudNodes,
opts: map[string]Options{"default": {
Exports: []client.ExportEntry{{Type: "cacheonly"}},
}},
},
{
name: "CallFunc",
nodes: cloudNodes,
opts: map[string]Options{"default": {CallFunc: &CallFunc{Name: "outline"}}},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("BUILDX_NO_DEFAULT_LOAD", "false")

var buf bytes.Buffer
restoreLogrus := captureLogrusWarnings(&buf)
defer restoreLogrus()

warnOnNoOutput(tt.nodes, tt.opts, tt.reqForNodes)

if tt.wantWarning == "" {
assert.Empty(t, buf.String())
return
}
assert.Contains(t, buf.String(), tt.wantWarning)
assert.Contains(t, buf.String(), "Build result will only remain in the build cache.")
})
}
}

func reqForDefaultTarget(solveOpts ...*client.SolveOpt) map[string][]*reqForNode {
reqs := make([]*reqForNode, 0, len(solveOpts))
for _, so := range solveOpts {
reqs = append(reqs, &reqForNode{so: so})
}
return map[string][]*reqForNode{"default": reqs}
}

func newWarnOutputDriver(name string, moby bool) *driver.DriverHandle {
return &driver.DriverHandle{
Driver: warnOutputDriver{
factory: warnOutputFactory{name: name},
moby: moby,
},
}
}

func captureLogrusWarnings(w *bytes.Buffer) func() {
logger := logrus.StandardLogger()
oldOut := logger.Out
oldFormatter := logger.Formatter
oldLevel := logger.Level

logger.SetOutput(w)
logger.SetFormatter(&logrus.TextFormatter{DisableTimestamp: true, DisableColors: true})
logger.SetLevel(logrus.WarnLevel)

return func() {
logger.SetOutput(oldOut)
logger.SetFormatter(oldFormatter)
logger.SetLevel(oldLevel)
}
}

func TestWarnOnNoOutputDisabledByEnv(t *testing.T) {
t.Setenv("BUILDX_NO_DEFAULT_LOAD", "true")

var buf bytes.Buffer
restoreLogrus := captureLogrusWarnings(&buf)
defer restoreLogrus()

warnOnNoOutput([]builder.Node{{
Driver: newWarnOutputDriver("cloud", false),
}}, map[string]Options{
"default": {},
}, nil)

require.Empty(t, buf.String())
}
Loading
Loading