mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-28 08:27:42 +08:00

This regression was introduced in 616fb3e55cbc85647026f6e409af17e1011a85c4, with the node resolution refactor. The core issue here is just that we would unconditionally set the solve opt's platform to the default current platform, which was incorrect. We can prevent this easily by having a special case for the default case, like we had before, and then not setting the platforms field on this (which keeping the resolution behavior which was introduced). Signed-off-by: Justin Chadwell <me@jedevc.com>
316 lines
7.6 KiB
Go
316 lines
7.6 KiB
Go
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 {
|
|
SolveOpt *client.SolveOpt
|
|
|
|
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
|
|
}
|
|
|
|
perfect := true
|
|
nodeIdxs := make([]int, 0)
|
|
if len(ps) == 0 {
|
|
idx := r.get(platforms.DefaultSpec(), matcher, additional)
|
|
if idx == -1 {
|
|
idx = 0
|
|
perfect = false
|
|
}
|
|
nodeIdxs = append(nodeIdxs, idx)
|
|
} else {
|
|
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 {
|
|
node := &resolvedNode{
|
|
resolver: r,
|
|
driverIndex: idx,
|
|
}
|
|
if len(ps) > 0 {
|
|
node.platforms = []specs.Platform{ps[i]}
|
|
}
|
|
nodes = append(nodes, node)
|
|
}
|
|
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
|
|
}
|