mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-08-17 09:15:53 +08:00
Compare commits
14 Commits
v0.12.0-rc
...
v0.12.0-rc
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d537b9e418 | ||
![]() |
616fb3e55c | ||
![]() |
80aa28f75c | ||
![]() |
0408f3ac45 | ||
![]() |
7683ef9137 | ||
![]() |
3f423468df | ||
![]() |
ff8bca206b | ||
![]() |
08a70ecdcc | ||
![]() |
d83da63320 | ||
![]() |
639e0bc5ed | ||
![]() |
d0a9a81e2e | ||
![]() |
de1a560f07 | ||
![]() |
e168fd826c | ||
![]() |
abfc04f621 |
42
.github/workflows/codeql.yml
vendored
Normal file
42
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: codeql
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'v[0-9]*'
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.21.3
|
||||
|
||||
jobs:
|
||||
codeql:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: go
|
||||
-
|
||||
name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
-
|
||||
name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:go"
|
@@ -949,8 +949,10 @@ func (t *Target) GetEvalContexts(ectx *hcl.EvalContext, block *hcl.Block, loadDe
|
||||
for _, e := range ectxs {
|
||||
e2 := ectx.NewChild()
|
||||
e2.Variables = make(map[string]cty.Value)
|
||||
for k, v := range e.Variables {
|
||||
e2.Variables[k] = v
|
||||
if e != ectx {
|
||||
for k, v := range e.Variables {
|
||||
e2.Variables[k] = v
|
||||
}
|
||||
}
|
||||
e2.Variables[k] = v
|
||||
ectxs2 = append(ectxs2, e2)
|
||||
|
@@ -1113,6 +1113,27 @@ func TestHCLMatrixBadTypes(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestHCLMatrixWithGlobalTarget(t *testing.T) {
|
||||
dt := []byte(`
|
||||
target "x" {
|
||||
tags = ["a", "b"]
|
||||
}
|
||||
|
||||
target "default" {
|
||||
tags = target.x.tags
|
||||
matrix = {
|
||||
dummy = [""]
|
||||
}
|
||||
}
|
||||
`)
|
||||
c, err := ParseFile(dt, "docker-bake.hcl")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(c.Targets))
|
||||
require.Equal(t, "x", c.Targets[0].Name)
|
||||
require.Equal(t, "default", c.Targets[1].Name)
|
||||
require.Equal(t, []string{"a", "b"}, c.Targets[1].Tags)
|
||||
}
|
||||
|
||||
func TestJSONAttributes(t *testing.T) {
|
||||
dt := []byte(`{"FOO": "abc", "variable": {"BAR": {"default": "def"}}, "target": { "app": { "args": {"v1": "pre-${FOO}-${BAR}"}} } }`)
|
||||
|
||||
|
312
build/build.go
312
build/build.go
@@ -135,218 +135,6 @@ func filterAvailableNodes(nodes []builder.Node) ([]builder.Node, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type driverPair struct {
|
||||
driverIndex int
|
||||
platforms []specs.Platform
|
||||
so *client.SolveOpt
|
||||
bopts gateway.BuildOpts
|
||||
}
|
||||
|
||||
func driverIndexes(m map[string][]driverPair) []int {
|
||||
out := make([]int, 0, len(m))
|
||||
visited := map[int]struct{}{}
|
||||
for _, dp := range m {
|
||||
for _, d := range dp {
|
||||
if _, ok := visited[d.driverIndex]; ok {
|
||||
continue
|
||||
}
|
||||
visited[d.driverIndex] = struct{}{}
|
||||
out = append(out, d.driverIndex)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func allIndexes(l int) []int {
|
||||
out := make([]int, 0, l)
|
||||
for i := 0; i < l; i++ {
|
||||
out = append(out, i)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func ensureBooted(ctx context.Context, nodes []builder.Node, idxs []int, pw progress.Writer) ([]*client.Client, error) {
|
||||
clients := make([]*client.Client, len(nodes))
|
||||
|
||||
baseCtx := ctx
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
for _, i := range idxs {
|
||||
func(i int) {
|
||||
eg.Go(func() error {
|
||||
c, err := driver.Boot(ctx, baseCtx, nodes[i].Driver, pw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clients[i] = c
|
||||
return nil
|
||||
})
|
||||
}(i)
|
||||
}
|
||||
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return clients, nil
|
||||
}
|
||||
|
||||
func splitToDriverPairs(availablePlatforms map[string]int, opt map[string]Options) map[string][]driverPair {
|
||||
m := map[string][]driverPair{}
|
||||
for k, opt := range opt {
|
||||
mm := map[int][]specs.Platform{}
|
||||
for _, p := range opt.Platforms {
|
||||
k := platforms.Format(p)
|
||||
idx := availablePlatforms[k] // default 0
|
||||
pp := mm[idx]
|
||||
pp = append(pp, p)
|
||||
mm[idx] = pp
|
||||
}
|
||||
// if no platform is specified, use first driver
|
||||
if len(mm) == 0 {
|
||||
mm[0] = nil
|
||||
}
|
||||
dps := make([]driverPair, 0, 2)
|
||||
for idx, pp := range mm {
|
||||
dps = append(dps, driverPair{driverIndex: idx, platforms: pp})
|
||||
}
|
||||
m[k] = dps
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func resolveDrivers(ctx context.Context, nodes []builder.Node, opt map[string]Options, pw progress.Writer) (map[string][]driverPair, []*client.Client, error) {
|
||||
dps, clients, err := resolveDriversBase(ctx, nodes, opt, pw)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
bopts := make([]gateway.BuildOpts, len(clients))
|
||||
|
||||
span, ctx := tracing.StartSpan(ctx, "load buildkit capabilities", trace.WithSpanKind(trace.SpanKindInternal))
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
for i, c := range clients {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
func(i int, c *client.Client) {
|
||||
eg.Go(func() error {
|
||||
clients[i].Build(ctx, client.SolveOpt{
|
||||
Internal: true,
|
||||
}, "buildx", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
||||
bopts[i] = c.BuildOpts()
|
||||
return nil, nil
|
||||
}, nil)
|
||||
return nil
|
||||
})
|
||||
}(i, c)
|
||||
}
|
||||
|
||||
err = eg.Wait()
|
||||
tracing.FinishWithError(span, err)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for key := range dps {
|
||||
for i, dp := range dps[key] {
|
||||
dps[key][i].bopts = bopts[dp.driverIndex]
|
||||
}
|
||||
}
|
||||
|
||||
return dps, clients, nil
|
||||
}
|
||||
|
||||
func resolveDriversBase(ctx context.Context, nodes []builder.Node, opt map[string]Options, pw progress.Writer) (map[string][]driverPair, []*client.Client, error) {
|
||||
availablePlatforms := map[string]int{}
|
||||
for i, node := range nodes {
|
||||
for _, p := range node.Platforms {
|
||||
availablePlatforms[platforms.Format(p)] = i
|
||||
}
|
||||
}
|
||||
|
||||
undetectedPlatform := false
|
||||
allPlatforms := map[string]struct{}{}
|
||||
for _, opt := range opt {
|
||||
for _, p := range opt.Platforms {
|
||||
k := platforms.Format(p)
|
||||
allPlatforms[k] = struct{}{}
|
||||
if _, ok := availablePlatforms[k]; !ok {
|
||||
undetectedPlatform = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fast path
|
||||
if len(nodes) == 1 || len(allPlatforms) == 0 {
|
||||
m := map[string][]driverPair{}
|
||||
for k, opt := range opt {
|
||||
m[k] = []driverPair{{driverIndex: 0, platforms: opt.Platforms}}
|
||||
}
|
||||
clients, err := ensureBooted(ctx, nodes, driverIndexes(m), pw)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return m, clients, nil
|
||||
}
|
||||
|
||||
// map based on existing platforms
|
||||
if !undetectedPlatform {
|
||||
m := splitToDriverPairs(availablePlatforms, opt)
|
||||
clients, err := ensureBooted(ctx, nodes, driverIndexes(m), pw)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return m, clients, nil
|
||||
}
|
||||
|
||||
// boot all drivers in k
|
||||
clients, err := ensureBooted(ctx, nodes, allIndexes(len(nodes)), pw)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
workers := make([][]*client.WorkerInfo, len(clients))
|
||||
|
||||
for i, c := range clients {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
func(i int) {
|
||||
eg.Go(func() error {
|
||||
ww, err := clients[i].ListWorkers(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "listing workers")
|
||||
}
|
||||
workers[i] = ww
|
||||
return nil
|
||||
})
|
||||
|
||||
}(i)
|
||||
}
|
||||
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for i, ww := range workers {
|
||||
for _, w := range ww {
|
||||
for _, p := range w.Platforms {
|
||||
p = platforms.Normalize(p)
|
||||
ps := platforms.Format(p)
|
||||
|
||||
if _, ok := availablePlatforms[ps]; !ok {
|
||||
availablePlatforms[ps] = i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return splitToDriverPairs(availablePlatforms, opt), clients, nil
|
||||
}
|
||||
|
||||
func toRepoOnly(in string) (string, error) {
|
||||
m := map[string]struct{}{}
|
||||
p := strings.Split(in, ",")
|
||||
@@ -717,10 +505,14 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s
|
||||
}
|
||||
}
|
||||
|
||||
m, clients, err := resolveDrivers(ctx, nodes, opt, w)
|
||||
drivers, err := resolveDrivers(ctx, nodes, opt, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
driversSolveOpts := make(map[string][]*client.SolveOpt, len(drivers))
|
||||
for k, dps := range drivers {
|
||||
driversSolveOpts[k] = make([]*client.SolveOpt, len(dps))
|
||||
}
|
||||
|
||||
defers := make([]func(), 0, 2)
|
||||
defer func() {
|
||||
@@ -734,30 +526,33 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
for k, opt := range opt {
|
||||
multiDriver := len(m[k]) > 1
|
||||
multiDriver := len(drivers[k]) > 1
|
||||
hasMobyDriver := false
|
||||
gitattrs, err := getGitAttributes(ctx, opt.Inputs.ContextPath, opt.Inputs.DockerfilePath)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Warn("current commit information was not captured by the build")
|
||||
}
|
||||
for i, np := range m[k] {
|
||||
node := nodes[np.driverIndex]
|
||||
if node.Driver.IsMobyDriver() {
|
||||
for i, np := range drivers[k] {
|
||||
if np.Node().Driver.IsMobyDriver() {
|
||||
hasMobyDriver = true
|
||||
}
|
||||
opt.Platforms = np.platforms
|
||||
so, release, err := toSolveOpt(ctx, node, multiDriver, opt, np.bopts, configDir, w, docker)
|
||||
gatewayOpts, err := np.BuildOpts(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := saveLocalState(so, k, opt, node, configDir); err != nil {
|
||||
so, release, err := toSolveOpt(ctx, np.Node(), multiDriver, opt, gatewayOpts, configDir, w, docker)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := saveLocalState(so, k, opt, np.Node(), configDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range gitattrs {
|
||||
so.FrontendAttrs[k] = v
|
||||
}
|
||||
defers = append(defers, release)
|
||||
m[k][i].so = so
|
||||
driversSolveOpts[k][i] = so
|
||||
}
|
||||
for _, at := range opt.Session {
|
||||
if s, ok := at.(interface {
|
||||
@@ -771,8 +566,8 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s
|
||||
|
||||
// validate for multi-node push
|
||||
if hasMobyDriver && multiDriver {
|
||||
for _, dp := range m[k] {
|
||||
for _, e := range dp.so.Exports {
|
||||
for _, so := range driversSolveOpts[k] {
|
||||
for _, e := range so.Exports {
|
||||
if e.Type == "moby" {
|
||||
if ok, _ := strconv.ParseBool(e.Attrs["push"]); ok {
|
||||
return nil, errors.Errorf("multi-node push can't currently be performed with the docker driver, please switch to a different driver")
|
||||
@@ -785,12 +580,13 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s
|
||||
|
||||
// validate that all links between targets use same drivers
|
||||
for name := range opt {
|
||||
dps := m[name]
|
||||
for _, dp := range dps {
|
||||
for k, v := range dp.so.FrontendAttrs {
|
||||
dps := drivers[name]
|
||||
for i, dp := range dps {
|
||||
so := driversSolveOpts[name][i]
|
||||
for k, v := range so.FrontendAttrs {
|
||||
if strings.HasPrefix(k, "context:") && strings.HasPrefix(v, "target:") {
|
||||
k2 := strings.TrimPrefix(v, "target:")
|
||||
dps2, ok := m[k2]
|
||||
dps2, ok := drivers[k2]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("failed to find target %s for context %s", k2, strings.TrimPrefix(k, "context:")) // should be validated before already
|
||||
}
|
||||
@@ -814,12 +610,13 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s
|
||||
results := waitmap.New()
|
||||
|
||||
multiTarget := len(opt) > 1
|
||||
childTargets := calculateChildTargets(drivers, driversSolveOpts, opt)
|
||||
|
||||
for k, opt := range opt {
|
||||
err := func(k string) error {
|
||||
opt := opt
|
||||
dps := m[k]
|
||||
multiDriver := len(m[k]) > 1
|
||||
dps := drivers[k]
|
||||
multiDriver := len(drivers[k]) > 1
|
||||
|
||||
var span trace.Span
|
||||
ctx := ctx
|
||||
@@ -835,8 +632,9 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s
|
||||
var insecurePush bool
|
||||
|
||||
for i, dp := range dps {
|
||||
i, dp, so := i, dp, *dp.so
|
||||
node := nodes[dp.driverIndex]
|
||||
i, dp := i, dp
|
||||
node := dp.Node()
|
||||
so := driversSolveOpts[k][i]
|
||||
if multiDriver {
|
||||
for i, e := range so.Exports {
|
||||
switch e.Type {
|
||||
@@ -867,11 +665,14 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s
|
||||
|
||||
pw := progress.WithPrefix(w, k, multiTarget)
|
||||
|
||||
c := clients[dp.driverIndex]
|
||||
c, err := dp.Client(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
eg2.Go(func() error {
|
||||
pw = progress.ResetTime(pw)
|
||||
|
||||
if err := waitContextDeps(ctx, dp.driverIndex, results, &so); err != nil {
|
||||
if err := waitContextDeps(ctx, dp.driverIndex, results, so); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -944,16 +745,35 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s
|
||||
printRes = res.Metadata
|
||||
}
|
||||
|
||||
results.Set(resultKey(dp.driverIndex, k), res)
|
||||
rKey := resultKey(dp.driverIndex, k)
|
||||
results.Set(rKey, res)
|
||||
|
||||
if children, ok := childTargets[rKey]; ok && len(children) > 0 {
|
||||
// we need to wait until the child targets have completed before we can release
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
eg.Go(func() error {
|
||||
return res.EachRef(func(ref gateway.Reference) error {
|
||||
return ref.Evaluate(ctx)
|
||||
})
|
||||
})
|
||||
eg.Go(func() error {
|
||||
_, err := results.Get(ctx, children...)
|
||||
return err
|
||||
})
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
var rr *client.SolveResponse
|
||||
if resultHandleFunc != nil {
|
||||
var resultHandle *ResultHandle
|
||||
resultHandle, rr, err = NewResultHandle(ctx, cc, so, "buildx", buildFunc, ch)
|
||||
resultHandle, rr, err = NewResultHandle(ctx, cc, *so, "buildx", buildFunc, ch)
|
||||
resultHandleFunc(dp.driverIndex, resultHandle)
|
||||
} else {
|
||||
rr, err = c.Build(ctx, so, "buildx", buildFunc, ch)
|
||||
rr, err = c.Build(ctx, *so, "buildx", buildFunc, ch)
|
||||
}
|
||||
if desktop.BuildBackendEnabled() && node.Driver.HistoryAPISupported(ctx) {
|
||||
buildRef := fmt.Sprintf("%s/%s/%s", node.Builder, node.Name, so.Ref)
|
||||
@@ -977,7 +797,7 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s
|
||||
rr.ExporterResponse[k] = string(v)
|
||||
}
|
||||
|
||||
node := nodes[dp.driverIndex].Driver
|
||||
node := dp.Node().Driver
|
||||
if node.IsMobyDriver() {
|
||||
for _, e := range so.Exports {
|
||||
if e.Type == "moby" && e.Attrs["push"] != "" {
|
||||
@@ -1069,7 +889,7 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s
|
||||
if len(descs) > 0 {
|
||||
var imageopt imagetools.Opt
|
||||
for _, dp := range dps {
|
||||
imageopt = nodes[dp.driverIndex].ImageOpt
|
||||
imageopt = dp.Node().ImageOpt
|
||||
break
|
||||
}
|
||||
names := strings.Split(pushNames, ",")
|
||||
@@ -1482,6 +1302,24 @@ func resultKey(index int, name string) string {
|
||||
return fmt.Sprintf("%d-%s", index, name)
|
||||
}
|
||||
|
||||
// calculateChildTargets returns all the targets that depend on current target for reverse index
|
||||
func calculateChildTargets(drivers map[string][]*resolvedNode, driversSolveOpts map[string][]*client.SolveOpt, opt map[string]Options) map[string][]string {
|
||||
out := make(map[string][]string)
|
||||
for name := range opt {
|
||||
dps := drivers[name]
|
||||
for i, dp := range dps {
|
||||
so := driversSolveOpts[name][i]
|
||||
for k, v := range so.FrontendAttrs {
|
||||
if strings.HasPrefix(k, "context:") && strings.HasPrefix(v, "target:") {
|
||||
target := resultKey(dp.driverIndex, strings.TrimPrefix(v, "target:"))
|
||||
out[target] = append(out[target], resultKey(dp.driverIndex, name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func waitContextDeps(ctx context.Context, index int, results *waitmap.Map, so *client.SolveOpt) error {
|
||||
m := map[string]string{}
|
||||
for k, v := range so.FrontendAttrs {
|
||||
|
305
build/driver.go
Normal file
305
build/driver.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/containerd/containerd/platforms"
|
||||
"github.com/docker/buildx/builder"
|
||||
"github.com/docker/buildx/driver"
|
||||
"github.com/docker/buildx/util/progress"
|
||||
"github.com/moby/buildkit/client"
|
||||
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
||||
"github.com/moby/buildkit/util/flightcontrol"
|
||||
"github.com/moby/buildkit/util/tracing"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type resolvedNode struct {
|
||||
resolver *nodeResolver
|
||||
driverIndex int
|
||||
platforms []specs.Platform
|
||||
}
|
||||
|
||||
func (dp resolvedNode) Node() builder.Node {
|
||||
return dp.resolver.nodes[dp.driverIndex]
|
||||
}
|
||||
|
||||
func (dp resolvedNode) Client(ctx context.Context) (*client.Client, error) {
|
||||
clients, err := dp.resolver.boot(ctx, []int{dp.driverIndex}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clients[0], nil
|
||||
}
|
||||
|
||||
func (dp resolvedNode) BuildOpts(ctx context.Context) (gateway.BuildOpts, error) {
|
||||
opts, err := dp.resolver.opts(ctx, []int{dp.driverIndex}, nil)
|
||||
if err != nil {
|
||||
return gateway.BuildOpts{}, err
|
||||
}
|
||||
return opts[0], nil
|
||||
}
|
||||
|
||||
type matchMaker func(specs.Platform) platforms.MatchComparer
|
||||
|
||||
type nodeResolver struct {
|
||||
nodes []builder.Node
|
||||
clients flightcontrol.Group[*client.Client]
|
||||
opt flightcontrol.Group[gateway.BuildOpts]
|
||||
}
|
||||
|
||||
func resolveDrivers(ctx context.Context, nodes []builder.Node, opt map[string]Options, pw progress.Writer) (map[string][]*resolvedNode, error) {
|
||||
driverRes := newDriverResolver(nodes)
|
||||
drivers, err := driverRes.Resolve(ctx, opt, pw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return drivers, err
|
||||
}
|
||||
|
||||
func newDriverResolver(nodes []builder.Node) *nodeResolver {
|
||||
r := &nodeResolver{
|
||||
nodes: nodes,
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *nodeResolver) Resolve(ctx context.Context, opt map[string]Options, pw progress.Writer) (map[string][]*resolvedNode, error) {
|
||||
if len(r.nodes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
nodes := map[string][]*resolvedNode{}
|
||||
for k, opt := range opt {
|
||||
node, perfect, err := r.resolve(ctx, opt.Platforms, pw, platforms.OnlyStrict, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !perfect {
|
||||
break
|
||||
}
|
||||
nodes[k] = node
|
||||
}
|
||||
if len(nodes) != len(opt) {
|
||||
// if we didn't get a perfect match, we need to boot all drivers
|
||||
allIndexes := make([]int, len(r.nodes))
|
||||
for i := range allIndexes {
|
||||
allIndexes[i] = i
|
||||
}
|
||||
|
||||
clients, err := r.boot(ctx, allIndexes, pw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
eg, egCtx := errgroup.WithContext(ctx)
|
||||
workers := make([][]specs.Platform, len(clients))
|
||||
for i, c := range clients {
|
||||
i, c := i, c
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
eg.Go(func() error {
|
||||
ww, err := c.ListWorkers(egCtx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "listing workers")
|
||||
}
|
||||
|
||||
ps := make(map[string]specs.Platform, len(ww))
|
||||
for _, w := range ww {
|
||||
for _, p := range w.Platforms {
|
||||
pk := platforms.Format(platforms.Normalize(p))
|
||||
ps[pk] = p
|
||||
}
|
||||
}
|
||||
for _, p := range ps {
|
||||
workers[i] = append(workers[i], p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// then we can attempt to match against all the available platforms
|
||||
// (this time we don't care about imperfect matches)
|
||||
nodes = map[string][]*resolvedNode{}
|
||||
for k, opt := range opt {
|
||||
node, _, err := r.resolve(ctx, opt.Platforms, pw, platforms.Only, func(idx int, n builder.Node) []specs.Platform {
|
||||
return workers[idx]
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodes[k] = node
|
||||
}
|
||||
}
|
||||
|
||||
idxs := make([]int, 0, len(r.nodes))
|
||||
for _, nodes := range nodes {
|
||||
for _, node := range nodes {
|
||||
idxs = append(idxs, node.driverIndex)
|
||||
}
|
||||
}
|
||||
|
||||
// preload capabilities
|
||||
span, ctx := tracing.StartSpan(ctx, "load buildkit capabilities", trace.WithSpanKind(trace.SpanKindInternal))
|
||||
_, err := r.opts(ctx, idxs, pw)
|
||||
tracing.FinishWithError(span, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func (r *nodeResolver) resolve(ctx context.Context, ps []specs.Platform, pw progress.Writer, matcher matchMaker, additional func(idx int, n builder.Node) []specs.Platform) ([]*resolvedNode, bool, error) {
|
||||
if len(r.nodes) == 0 {
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
if len(ps) == 0 {
|
||||
ps = []specs.Platform{platforms.DefaultSpec()}
|
||||
}
|
||||
|
||||
perfect := true
|
||||
nodeIdxs := make([]int, 0)
|
||||
for _, p := range ps {
|
||||
idx := r.get(p, matcher, additional)
|
||||
if idx == -1 {
|
||||
idx = 0
|
||||
perfect = false
|
||||
}
|
||||
nodeIdxs = append(nodeIdxs, idx)
|
||||
}
|
||||
|
||||
var nodes []*resolvedNode
|
||||
for i, idx := range nodeIdxs {
|
||||
nodes = append(nodes, &resolvedNode{
|
||||
resolver: r,
|
||||
driverIndex: idx,
|
||||
platforms: []specs.Platform{ps[i]},
|
||||
})
|
||||
}
|
||||
nodes = recombineNodes(nodes)
|
||||
if _, err := r.boot(ctx, nodeIdxs, pw); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return nodes, perfect, nil
|
||||
}
|
||||
|
||||
func (r *nodeResolver) get(p specs.Platform, matcher matchMaker, additionalPlatforms func(int, builder.Node) []specs.Platform) int {
|
||||
best := -1
|
||||
bestPlatform := specs.Platform{}
|
||||
for i, node := range r.nodes {
|
||||
platforms := node.Platforms
|
||||
if additionalPlatforms != nil {
|
||||
platforms = append([]specs.Platform{}, platforms...)
|
||||
platforms = append(platforms, additionalPlatforms(i, node)...)
|
||||
}
|
||||
for _, p2 := range platforms {
|
||||
m := matcher(p2)
|
||||
if !m.Match(p) {
|
||||
continue
|
||||
}
|
||||
|
||||
if best == -1 {
|
||||
best = i
|
||||
bestPlatform = p2
|
||||
continue
|
||||
}
|
||||
if matcher(p2).Less(p, bestPlatform) {
|
||||
best = i
|
||||
bestPlatform = p2
|
||||
}
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
func (r *nodeResolver) boot(ctx context.Context, idxs []int, pw progress.Writer) ([]*client.Client, error) {
|
||||
clients := make([]*client.Client, len(idxs))
|
||||
|
||||
baseCtx := ctx
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
for i, idx := range idxs {
|
||||
i, idx := i, idx
|
||||
eg.Go(func() error {
|
||||
c, err := r.clients.Do(ctx, fmt.Sprint(idx), func(ctx context.Context) (*client.Client, error) {
|
||||
if r.nodes[idx].Driver == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return driver.Boot(ctx, baseCtx, r.nodes[idx].Driver, pw)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clients[i] = c
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return clients, nil
|
||||
}
|
||||
|
||||
func (r *nodeResolver) opts(ctx context.Context, idxs []int, pw progress.Writer) ([]gateway.BuildOpts, error) {
|
||||
clients, err := r.boot(ctx, idxs, pw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bopts := make([]gateway.BuildOpts, len(clients))
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
for i, idxs := range idxs {
|
||||
i, idx := i, idxs
|
||||
c := clients[i]
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
eg.Go(func() error {
|
||||
opt, err := r.opt.Do(ctx, fmt.Sprint(idx), func(ctx context.Context) (gateway.BuildOpts, error) {
|
||||
opt := gateway.BuildOpts{}
|
||||
_, err := c.Build(ctx, client.SolveOpt{
|
||||
Internal: true,
|
||||
}, "buildx", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
||||
opt = c.BuildOpts()
|
||||
return nil, nil
|
||||
}, nil)
|
||||
return opt, err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bopts[i] = opt
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bopts, nil
|
||||
}
|
||||
|
||||
// recombineDriverPairs recombines resolved nodes that are on the same driver
|
||||
// back together into a single node.
|
||||
func recombineNodes(nodes []*resolvedNode) []*resolvedNode {
|
||||
result := make([]*resolvedNode, 0, len(nodes))
|
||||
lookup := map[int]int{}
|
||||
for _, node := range nodes {
|
||||
if idx, ok := lookup[node.driverIndex]; ok {
|
||||
result[idx].platforms = append(result[idx].platforms, node.platforms...)
|
||||
} else {
|
||||
lookup[node.driverIndex] = len(result)
|
||||
result = append(result, node)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
313
build/driver_test.go
Normal file
313
build/driver_test.go
Normal file
@@ -0,0 +1,313 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/platforms"
|
||||
"github.com/docker/buildx/builder"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFindDriverSanity(t *testing.T) {
|
||||
r := makeTestResolver(map[string][]specs.Platform{
|
||||
"aaa": {platforms.DefaultSpec()},
|
||||
})
|
||||
|
||||
res, perfect, err := r.resolve(context.TODO(), []specs.Platform{platforms.DefaultSpec()}, nil, platforms.OnlyStrict, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, 0, res[0].driverIndex)
|
||||
require.Equal(t, "aaa", res[0].Node().Builder)
|
||||
}
|
||||
|
||||
func TestFindDriverEmpty(t *testing.T) {
|
||||
r := makeTestResolver(nil)
|
||||
|
||||
res, perfect, err := r.resolve(context.TODO(), []specs.Platform{platforms.DefaultSpec()}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Nil(t, res)
|
||||
}
|
||||
|
||||
func TestFindDriverWeirdName(t *testing.T) {
|
||||
r := makeTestResolver(map[string][]specs.Platform{
|
||||
"aaa": {platforms.MustParse("linux/amd64")},
|
||||
"bbb": {platforms.MustParse("linux/foobar")},
|
||||
})
|
||||
|
||||
// find first platform
|
||||
res, perfect, err := r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/foobar")}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, 1, res[0].driverIndex)
|
||||
require.Equal(t, "bbb", res[0].Node().Builder)
|
||||
}
|
||||
|
||||
func TestFindDriverUnknown(t *testing.T) {
|
||||
r := makeTestResolver(map[string][]specs.Platform{
|
||||
"aaa": {platforms.MustParse("linux/amd64")},
|
||||
})
|
||||
|
||||
res, perfect, err := r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/riscv64")}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.False(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, 0, res[0].driverIndex)
|
||||
require.Equal(t, "aaa", res[0].Node().Builder)
|
||||
}
|
||||
|
||||
func TestSelectNodeSinglePlatform(t *testing.T) {
|
||||
r := makeTestResolver(map[string][]specs.Platform{
|
||||
"aaa": {platforms.MustParse("linux/amd64")},
|
||||
"bbb": {platforms.MustParse("linux/riscv64")},
|
||||
})
|
||||
|
||||
// find first platform
|
||||
res, perfect, err := r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/amd64")}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, 0, res[0].driverIndex)
|
||||
require.Equal(t, "aaa", res[0].Node().Builder)
|
||||
|
||||
// find second platform
|
||||
res, perfect, err = r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/riscv64")}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, 1, res[0].driverIndex)
|
||||
require.Equal(t, "bbb", res[0].Node().Builder)
|
||||
|
||||
// find an unknown platform, should match the first driver
|
||||
res, perfect, err = r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/s390x")}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.False(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, 0, res[0].driverIndex)
|
||||
require.Equal(t, "aaa", res[0].Node().Builder)
|
||||
}
|
||||
|
||||
func TestSelectNodeMultiPlatform(t *testing.T) {
|
||||
r := makeTestResolver(map[string][]specs.Platform{
|
||||
"aaa": {platforms.MustParse("linux/amd64"), platforms.MustParse("linux/arm64")},
|
||||
"bbb": {platforms.MustParse("linux/riscv64")},
|
||||
})
|
||||
|
||||
res, perfect, err := r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/amd64")}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, 0, res[0].driverIndex)
|
||||
require.Equal(t, "aaa", res[0].Node().Builder)
|
||||
|
||||
res, perfect, err = r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/arm64")}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, 0, res[0].driverIndex)
|
||||
require.Equal(t, "aaa", res[0].Node().Builder)
|
||||
|
||||
res, perfect, err = r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/riscv64")}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, 1, res[0].driverIndex)
|
||||
require.Equal(t, "bbb", res[0].Node().Builder)
|
||||
}
|
||||
|
||||
func TestSelectNodeNonStrict(t *testing.T) {
|
||||
r := makeTestResolver(map[string][]specs.Platform{
|
||||
"aaa": {platforms.MustParse("linux/amd64")},
|
||||
"bbb": {platforms.MustParse("linux/arm64")},
|
||||
})
|
||||
|
||||
// arm64 should match itself
|
||||
res, perfect, err := r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/arm64")}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, "bbb", res[0].Node().Builder)
|
||||
|
||||
// arm64 may support arm/v8
|
||||
res, perfect, err = r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/arm/v8")}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, "bbb", res[0].Node().Builder)
|
||||
|
||||
// arm64 may support arm/v7
|
||||
res, perfect, err = r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/arm/v7")}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, "bbb", res[0].Node().Builder)
|
||||
}
|
||||
|
||||
func TestSelectNodeNonStrictARM(t *testing.T) {
|
||||
r := makeTestResolver(map[string][]specs.Platform{
|
||||
"aaa": {platforms.MustParse("linux/amd64")},
|
||||
"bbb": {platforms.MustParse("linux/arm64")},
|
||||
"ccc": {platforms.MustParse("linux/arm/v8")},
|
||||
})
|
||||
|
||||
res, perfect, err := r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/arm/v8")}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, "ccc", res[0].Node().Builder)
|
||||
|
||||
res, perfect, err = r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/arm/v7")}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, "ccc", res[0].Node().Builder)
|
||||
}
|
||||
|
||||
func TestSelectNodeNonStrictLower(t *testing.T) {
|
||||
r := makeTestResolver(map[string][]specs.Platform{
|
||||
"aaa": {platforms.MustParse("linux/amd64")},
|
||||
"bbb": {platforms.MustParse("linux/arm/v7")},
|
||||
})
|
||||
|
||||
// v8 can't be built on v7 (so we should select the default)...
|
||||
res, perfect, err := r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/arm/v8")}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.False(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, "aaa", res[0].Node().Builder)
|
||||
|
||||
// ...but v6 can be built on v8
|
||||
res, perfect, err = r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/arm/v6")}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, "bbb", res[0].Node().Builder)
|
||||
}
|
||||
|
||||
func TestSelectNodePreferStart(t *testing.T) {
|
||||
r := makeTestResolver(map[string][]specs.Platform{
|
||||
"aaa": {platforms.MustParse("linux/amd64")},
|
||||
"bbb": {platforms.MustParse("linux/riscv64")},
|
||||
"ccc": {platforms.MustParse("linux/riscv64")},
|
||||
})
|
||||
|
||||
res, perfect, err := r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/riscv64")}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, "bbb", res[0].Node().Builder)
|
||||
}
|
||||
|
||||
func TestSelectNodePreferExact(t *testing.T) {
|
||||
r := makeTestResolver(map[string][]specs.Platform{
|
||||
"aaa": {platforms.MustParse("linux/arm/v8")},
|
||||
"bbb": {platforms.MustParse("linux/arm/v7")},
|
||||
})
|
||||
|
||||
res, perfect, err := r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/arm/v7")}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, "bbb", res[0].Node().Builder)
|
||||
}
|
||||
|
||||
func TestSelectNodeCurrentPlatform(t *testing.T) {
|
||||
r := makeTestResolver(map[string][]specs.Platform{
|
||||
"aaa": {platforms.MustParse("linux/foobar")},
|
||||
"bbb": {platforms.DefaultSpec()},
|
||||
})
|
||||
|
||||
res, perfect, err := r.resolve(context.TODO(), []specs.Platform{}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, "bbb", res[0].Node().Builder)
|
||||
}
|
||||
|
||||
func TestSelectNodeAdditionalPlatforms(t *testing.T) {
|
||||
r := makeTestResolver(map[string][]specs.Platform{
|
||||
"aaa": {platforms.MustParse("linux/amd64")},
|
||||
"bbb": {platforms.MustParse("linux/arm/v8")},
|
||||
})
|
||||
|
||||
res, perfect, err := r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/arm/v7")}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, "bbb", res[0].Node().Builder)
|
||||
|
||||
res, perfect, err = r.resolve(context.TODO(), []specs.Platform{platforms.MustParse("linux/arm/v7")}, nil, platforms.Only, func(idx int, n builder.Node) []specs.Platform {
|
||||
if n.Builder == "aaa" {
|
||||
return []specs.Platform{platforms.MustParse("linux/arm/v7")}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, "aaa", res[0].Node().Builder)
|
||||
}
|
||||
|
||||
func TestSplitNodeMultiPlatform(t *testing.T) {
|
||||
r := makeTestResolver(map[string][]specs.Platform{
|
||||
"aaa": {platforms.MustParse("linux/amd64"), platforms.MustParse("linux/arm64")},
|
||||
"bbb": {platforms.MustParse("linux/riscv64")},
|
||||
})
|
||||
|
||||
res, perfect, err := r.resolve(context.TODO(), []specs.Platform{
|
||||
platforms.MustParse("linux/amd64"),
|
||||
platforms.MustParse("linux/arm64"),
|
||||
}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, "aaa", res[0].Node().Builder)
|
||||
|
||||
res, perfect, err = r.resolve(context.TODO(), []specs.Platform{
|
||||
platforms.MustParse("linux/amd64"),
|
||||
platforms.MustParse("linux/riscv64"),
|
||||
}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 2)
|
||||
require.Equal(t, "aaa", res[0].Node().Builder)
|
||||
require.Equal(t, "bbb", res[1].Node().Builder)
|
||||
}
|
||||
|
||||
func TestSplitNodeMultiPlatformNoUnify(t *testing.T) {
|
||||
r := makeTestResolver(map[string][]specs.Platform{
|
||||
"aaa": {platforms.MustParse("linux/amd64")},
|
||||
"bbb": {platforms.MustParse("linux/amd64"), platforms.MustParse("linux/riscv64")},
|
||||
})
|
||||
|
||||
// the "best" choice would be the node with both platforms, but we're using
|
||||
// a naive algorithm that doesn't try to unify the platforms
|
||||
res, perfect, err := r.resolve(context.TODO(), []specs.Platform{
|
||||
platforms.MustParse("linux/amd64"),
|
||||
platforms.MustParse("linux/riscv64"),
|
||||
}, nil, platforms.Only, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, perfect)
|
||||
require.Len(t, res, 2)
|
||||
require.Equal(t, "aaa", res[0].Node().Builder)
|
||||
require.Equal(t, "bbb", res[1].Node().Builder)
|
||||
}
|
||||
|
||||
func makeTestResolver(nodes map[string][]specs.Platform) *nodeResolver {
|
||||
var ns []builder.Node
|
||||
for name, platforms := range nodes {
|
||||
ns = append(ns, builder.Node{
|
||||
Builder: name,
|
||||
Platforms: platforms,
|
||||
})
|
||||
}
|
||||
sort.Slice(ns, func(i, j int) bool {
|
||||
return ns[i].Builder < ns[j].Builder
|
||||
})
|
||||
return newDriverResolver(ns)
|
||||
}
|
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/containerd/containerd/platforms"
|
||||
@@ -98,8 +100,6 @@ func runBake(dockerCli command.Cli, targets []string, in bakeOptions, cFlags com
|
||||
defer cancel()
|
||||
|
||||
var nodes []builder.Node
|
||||
var files []bake.File
|
||||
var inp *bake.Input
|
||||
var progressConsoleDesc, progressTextDesc string
|
||||
|
||||
// instance only needed for reading remote bake files or building
|
||||
@@ -147,18 +147,15 @@ func runBake(dockerCli command.Cli, targets []string, in bakeOptions, cFlags com
|
||||
}
|
||||
}()
|
||||
|
||||
if url != "" {
|
||||
files, inp, err = bake.ReadRemoteFiles(ctx, nodes, url, in.files, printer)
|
||||
} else {
|
||||
progress.Wrap("[internal] load local bake definitions", printer.Write, func(sub progress.SubLogger) error {
|
||||
files, err = bake.ReadLocalFiles(in.files, dockerCli.In(), sub)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
files, inp, err := readBakeFiles(ctx, nodes, url, in.files, dockerCli.In(), printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return errors.New("couldn't find a bake definition")
|
||||
}
|
||||
|
||||
tgts, grps, err := bake.ReadTargets(ctx, files, targets, overrides, map[string]string{
|
||||
// don't forget to update documentation if you add a new
|
||||
// built-in variable: docs/bake-reference.md#built-in-variables
|
||||
@@ -296,3 +293,42 @@ func saveLocalStateGroup(dockerCli command.Cli, ref string, lsg localstate.State
|
||||
}
|
||||
return l.SaveGroup(ref, lsg)
|
||||
}
|
||||
|
||||
func readBakeFiles(ctx context.Context, nodes []builder.Node, url string, names []string, stdin io.Reader, pw progress.Writer) (files []bake.File, inp *bake.Input, err error) {
|
||||
var lnames []string
|
||||
var rnames []string
|
||||
for _, v := range names {
|
||||
if strings.HasPrefix(v, "cwd://") {
|
||||
lnames = append(lnames, strings.TrimPrefix(v, "cwd://"))
|
||||
} else {
|
||||
rnames = append(rnames, v)
|
||||
}
|
||||
}
|
||||
|
||||
if url != "" {
|
||||
var rfiles []bake.File
|
||||
rfiles, inp, err = bake.ReadRemoteFiles(ctx, nodes, url, rnames, pw)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
files = append(files, rfiles...)
|
||||
}
|
||||
|
||||
if len(lnames) > 0 || url == "" {
|
||||
var lfiles []bake.File
|
||||
progress.Wrap("[internal] load local bake definitions", pw.Write, func(sub progress.SubLogger) error {
|
||||
if url != "" {
|
||||
lfiles, err = bake.ReadLocalFiles(lnames, stdin, sub)
|
||||
} else {
|
||||
lfiles, err = bake.ReadLocalFiles(append(lnames, rnames...), stdin, sub)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
files = append(files, lfiles...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
@@ -33,7 +33,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
volumeStateSuffix = "_state"
|
||||
volumeStateSuffix = "_state"
|
||||
buildkitdConfigFile = "buildkitd.toml"
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
@@ -111,9 +112,7 @@ func (d *Driver) create(ctx context.Context, l progress.SubLogger) error {
|
||||
Image: imageName,
|
||||
Env: d.env,
|
||||
}
|
||||
if d.InitConfig.BuildkitFlags != nil {
|
||||
cfg.Cmd = d.InitConfig.BuildkitFlags
|
||||
}
|
||||
cfg.Cmd = getBuildkitFlags(d.InitConfig)
|
||||
|
||||
useInit := true // let it cleanup exited processes created by BuildKit's container API
|
||||
return l.Wrap("creating container "+d.Name, func() error {
|
||||
@@ -250,7 +249,9 @@ func (d *Driver) copyToContainer(ctx context.Context, files map[string][]byte) e
|
||||
return err
|
||||
}
|
||||
defer srcArchive.Close()
|
||||
return d.DockerAPI.CopyToContainer(ctx, d.Name, "/", srcArchive, dockertypes.CopyToContainerOptions{})
|
||||
|
||||
baseDir := path.Dir(confutil.DefaultBuildKitConfigDir)
|
||||
return d.DockerAPI.CopyToContainer(ctx, d.Name, baseDir, srcArchive, dockertypes.CopyToContainerOptions{})
|
||||
}
|
||||
|
||||
func (d *Driver) exec(ctx context.Context, cmd []string) (string, net.Conn, error) {
|
||||
@@ -466,15 +467,34 @@ func writeConfigFiles(m map[string][]byte) (_ string, err error) {
|
||||
os.RemoveAll(tmpDir)
|
||||
}
|
||||
}()
|
||||
configDir := filepath.Base(confutil.DefaultBuildKitConfigDir)
|
||||
for f, dt := range m {
|
||||
f = path.Join(confutil.DefaultBuildKitConfigDir, f)
|
||||
p := filepath.Join(tmpDir, f)
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0700); err != nil {
|
||||
p := filepath.Join(tmpDir, configDir, f)
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.WriteFile(p, dt, 0600); err != nil {
|
||||
if err := os.WriteFile(p, dt, 0644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return tmpDir, nil
|
||||
}
|
||||
|
||||
func getBuildkitFlags(initConfig driver.InitConfig) []string {
|
||||
flags := initConfig.BuildkitFlags
|
||||
if _, ok := initConfig.Files[buildkitdConfigFile]; ok {
|
||||
// There's no way for us to determine the appropriate default configuration
|
||||
// path and the default path can vary depending on if the image is normal
|
||||
// or rootless.
|
||||
//
|
||||
// In order to ensure that --config works, copy to a specific path and
|
||||
// specify the location.
|
||||
//
|
||||
// This should be appended before the user-specified arguments
|
||||
// so that this option could be overwritten by the user.
|
||||
newFlags := make([]string, 0, len(flags)+2)
|
||||
newFlags = append(newFlags, "--config", path.Join("/etc/buildkit", buildkitdConfigFile))
|
||||
flags = append(newFlags, flags...)
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
@@ -23,12 +23,14 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){
|
||||
testBakeLocalMulti,
|
||||
testBakeRemote,
|
||||
testBakeRemoteCmdContext,
|
||||
testBakeRemoteLocalOverride,
|
||||
testBakeRemoteCmdContextOverride,
|
||||
testBakeRemoteContextSubdir,
|
||||
testBakeRemoteCmdContextEscapeRoot,
|
||||
testBakeRemoteCmdContextEscapeRelative,
|
||||
testBakeRemoteDockerfileCwd,
|
||||
testBakeRemoteLocalContextRemoteDockerfile,
|
||||
testBakeEmpty,
|
||||
}
|
||||
|
||||
func testBakeLocal(t *testing.T, sb integration.Sandbox) {
|
||||
@@ -46,6 +48,7 @@ target "default" {
|
||||
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||
fstest.CreateFile("foo", []byte("foo"), 0600),
|
||||
)
|
||||
|
||||
dirDest := t.TempDir()
|
||||
|
||||
cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--progress=plain", "--set", "*.output=type=local,dest="+dirDest))
|
||||
@@ -79,16 +82,23 @@ services:
|
||||
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||
fstest.CreateFile("foo", []byte("foo"), 0600),
|
||||
)
|
||||
|
||||
dirDest := t.TempDir()
|
||||
|
||||
cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--progress=plain", "--set", "*.output=type=local,dest="+dirDest))
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, out)
|
||||
require.Contains(t, string(out), `#1 [internal] load local bake definitions`)
|
||||
require.Contains(t, string(out), `#1 reading compose.yaml`)
|
||||
require.Contains(t, string(out), `#1 reading docker-bake.hcl`)
|
||||
|
||||
dt, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
require.Contains(t, string(dt), `#1 [internal] load local bake definitions`)
|
||||
require.Contains(t, string(dt), `#1 reading compose.yaml`)
|
||||
require.Contains(t, string(dt), `#1 reading docker-bake.hcl`)
|
||||
require.FileExists(t, filepath.Join(dirDest, "foo"))
|
||||
|
||||
dirDest2 := t.TempDir()
|
||||
|
||||
out, err := bakeCmd(sb, withDir(dir), withArgs("--file", "cwd://docker-bake.hcl", "--set", "*.output=type=local,dest="+dirDest2))
|
||||
require.NoError(t, err, out)
|
||||
|
||||
require.FileExists(t, filepath.Join(dirDest2, "foo"))
|
||||
}
|
||||
|
||||
func testBakeRemote(t *testing.T, sb integration.Sandbox) {
|
||||
@@ -121,6 +131,48 @@ EOT
|
||||
require.FileExists(t, filepath.Join(dirDest, "foo"))
|
||||
}
|
||||
|
||||
func testBakeRemoteLocalOverride(t *testing.T, sb integration.Sandbox) {
|
||||
remoteBakefile := []byte(`
|
||||
target "default" {
|
||||
dockerfile-inline = <<EOT
|
||||
FROM scratch
|
||||
COPY foo /foo
|
||||
EOT
|
||||
}
|
||||
`)
|
||||
localBakefile := []byte(`
|
||||
target "default" {
|
||||
dockerfile-inline = <<EOT
|
||||
FROM scratch
|
||||
COPY bar /bar
|
||||
EOT
|
||||
}
|
||||
`)
|
||||
dirSpec := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("docker-bake.hcl", remoteBakefile, 0600),
|
||||
fstest.CreateFile("bar", []byte("bar"), 0600),
|
||||
)
|
||||
dirSrc := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("local-docker-bake.hcl", localBakefile, 0600),
|
||||
)
|
||||
dirDest := t.TempDir()
|
||||
|
||||
git, err := gitutil.New(gitutil.WithWorkingDir(dirSpec))
|
||||
require.NoError(t, err)
|
||||
|
||||
gitutil.GitInit(git, t)
|
||||
gitutil.GitAdd(git, t, "docker-bake.hcl", "bar")
|
||||
gitutil.GitCommit(git, t, "initial commit")
|
||||
addr := gitutil.GitServeHTTP(git, t)
|
||||
|
||||
out, err := bakeCmd(sb, withDir(dirSrc), withArgs(addr, "--file", "cwd://local-docker-bake.hcl", "--set", "*.output=type=local,dest="+dirDest))
|
||||
require.NoError(t, err, out)
|
||||
|
||||
require.FileExists(t, filepath.Join(dirDest, "bar"))
|
||||
}
|
||||
|
||||
func testBakeRemoteCmdContext(t *testing.T, sb integration.Sandbox) {
|
||||
bakefile := []byte(`
|
||||
target "default" {
|
||||
@@ -426,3 +478,9 @@ COPY foo /foo
|
||||
require.Error(t, err, out)
|
||||
require.Contains(t, out, "reading a dockerfile for a remote build invocation is currently not supported")
|
||||
}
|
||||
|
||||
func testBakeEmpty(t *testing.T, sb integration.Sandbox) {
|
||||
out, err := bakeCmd(sb)
|
||||
require.Error(t, err, out)
|
||||
require.Contains(t, out, "couldn't find a bake definition")
|
||||
}
|
||||
|
Reference in New Issue
Block a user