// Copyright 2024, Pulumi Corporation. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package internal import ( "context" "errors" "fmt" "path/filepath" "strings" "sync" "time" "github.com/blang/semver" "github.com/docker/buildx/builder" "github.com/docker/buildx/store/storeutil" "github.com/docker/cli/cli/command" cfgtypes "github.com/docker/cli/cli/config/types" ) // host contains a host-level Docker CLI as well as a cache of initialized // builders. Operations on the host are serialized. type host struct { mu sync.Mutex cli command.Cli config *Config builders map[string]*cachedBuilder auths map[string]cfgtypes.AuthConfig // True if the buildkit daemon is at least v0.13. supportsMultipleExports bool } func newHost(_ context.Context, config *Config) (*host, error) { docker, err := newDockerCLI(config) if err != nil { return nil, err } // Load existing credentials into memory. auths, err := docker.ConfigFile().GetAllCredentials() if err != nil { return nil, err } h := &host{ cli: docker, config: config, builders: map[string]*cachedBuilder{}, auths: auths, supportsMultipleExports: false, // Determined when we boot the builder. } return h, err } // builderFor ensures a builder is available and running. This is guarded by a // mutex to ensure other resources don't attempt to use the builder until it's // ready. // // If the build doesn't specify a builder by name, we will iterate through all // available builders until we find one that we can connect to. func (h *host) builderFor(ctx context.Context, build Build) (*cachedBuilder, error) { h.mu.Lock() defer h.mu.Unlock() opts := build.BuildOptions() if b, ok := h.builders[opts.Builder]; ok { return b, nil } txn, release, err := storeutil.GetStore(h.cli) if err != nil { return nil, fmt.Errorf("getting store: %w", err) } defer release() contextPathHash := opts.ContextPath if absContextPath, err := filepath.Abs(contextPathHash); err == nil { contextPathHash = absContextPath } b, err := builder.New(h.cli, builder.WithName(opts.Builder), builder.WithContextPathHash(contextPathHash), builder.WithStore(txn), ) if err != nil && build.ShouldExec() && strings.HasPrefix(opts.Builder, "cloud-") { //nolint:revive // Human-readable. err = errors.Join(err, errors.New("Make sure you're logged in to Docker (`docker login`) if you're trying to use a cloud builder"), //nolint:lll,staticcheck errors.New("Make sure you have the correct buildx plugin installed (https://github.com/docker/buildx-desktop)"), //nolint:lll,staticcheck ) } if err != nil && build.ShouldExec() { //nolint:revive // Human-readable. err = errors.Join(err, errors.New( //nolint:staticcheck "Make sure your buildx plugin is executable (`docker buildx version`)"), ) } if err != nil { return nil, fmt.Errorf("new builder: %w", err) } // If we didn't request a particular builder, and we loaded a default // builder with an unsupported (docker) driver, then look for a builder we // do support. if b.Driver == "" && opts.Builder == "" { builders, err := builder.GetBuilders(h.cli, txn) if err != nil { return nil, fmt.Errorf("getting builders: %w", err) } nextbuilder: for _, bb := range builders { if bb.Driver == "" { continue } if err := bb.Validate(); err != nil { continue } if bb.Err() != nil { continue } nodes, err := bb.LoadNodes(ctx) if err != nil { continue } for idx := range nodes { n := nodes[idx] if n.Driver == nil { continue nextbuilder } if _, err := n.Driver.Dial(ctx); err != nil { continue nextbuilder } // TODO: Confirm the builder supports the requested platforms. } b = bb break } } if b.Driver == "" && opts.Builder == "" { // If we STILL don't have a builder, create a docker-container instance. b, err = builder.Create( ctx, txn, h.cli, builder.CreateOpts{Driver: "docker-container"}, ) if err != nil { return nil, fmt.Errorf("creating builder: %w", err) } ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() if _, err := b.Boot(ctx); err != nil { return nil, fmt.Errorf("booting builder: %w", err) } } // Attempt to load nodes in order to determine the builder's driver. Ignore // errors for "exec" builds because it's possible to request builders with // drivers that are unknown to us. nodes, err := b.LoadNodes(ctx, builder.WithData()) if err != nil && !build.ShouldExec() { if strings.Contains(err.Error(), "failed to find driver") { //nolint:revive // Human-readable. err = errors.Join(err, errors.New( //nolint:staticcheck "Use `exec: true` if you're trying to use Docker Build Cloud or other custom drivers", )) } return nil, fmt.Errorf("loading nodes: %w", err) } // Attempt to determine our builder's buildkit version. for idx := range nodes { if nodes[idx].Version == "" { continue } v, err := semver.ParseTolerant(nodes[idx].Version) if err != nil { return nil, fmt.Errorf("parsing buildkit version %q: %w", nodes[idx].Version, err) } h.supportsMultipleExports = v.GE(semver.MustParse("0.13.0")) break } cached := &cachedBuilder{name: b.Name, driver: b.Driver, nodes: nodes} h.builders[opts.Builder] = cached return cached, nil } // cachedBuilder caches the builders we've loaded. Repeatedly fetching them can // sometimes result in EOF errors from the daemon, especially when under load. type cachedBuilder struct { name string driver string nodes []builder.Node }