DockerCLI/cmd/docker/builder.go

181 lines
5.7 KiB
Go

package main
import (
"fmt"
"io"
"os"
"strconv"
"strings"
pluginmanager "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
const (
builderDefaultPlugin = "buildx"
buildxMissingWarning = `DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
Install the buildx component to build images with BuildKit:
https://docs.docker.com/go/buildx/`
buildkitDisabledWarning = `DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
BuildKit is currently disabled; enable it by removing the DOCKER_BUILDKIT=0
environment-variable.`
buildxMissingError = `ERROR: BuildKit is enabled but the buildx component is missing or broken.
Install the buildx component to build images with BuildKit:
https://docs.docker.com/go/buildx/`
)
func newBuilderError(errorMsg string, pluginLoadErr error) error {
if pluginmanager.IsNotFound(pluginLoadErr) {
return errors.New(errorMsg)
}
if pluginLoadErr != nil {
return fmt.Errorf("%w\n\n%s", pluginLoadErr, errorMsg)
}
return errors.New(errorMsg)
}
//nolint:gocyclo
func processBuilder(dockerCli command.Cli, cmd *cobra.Command, args, osargs []string) ([]string, []string, []string, error) {
var buildKitDisabled, useBuilder, useAlias bool
var envs []string
// check DOCKER_BUILDKIT env var is not empty
// if it is assume we want to use the builder component
if v := os.Getenv("DOCKER_BUILDKIT"); v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return args, osargs, nil, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
}
if !enabled {
buildKitDisabled = true
} else {
useBuilder = true
}
}
// if a builder alias is defined, use it instead
// of the default one
builderAlias := builderDefaultPlugin
aliasMap := dockerCli.ConfigFile().Aliases
if v, ok := aliasMap[keyBuilderAlias]; ok {
useBuilder = true
useAlias = true
builderAlias = v
}
// is this a build that should be forwarded to the builder?
fwargs, fwosargs, fwcmdpath, forwarded := forwardBuilder(builderAlias, args, osargs)
if !forwarded {
return args, osargs, nil, nil
}
if !useBuilder {
// Builder is not explicitly configured as an alias for buildx.
// Detect whether we should use BuildKit, or fallback to the
// legacy builder.
if si := dockerCli.ServerInfo(); si.BuildkitVersion != types.BuilderBuildKit && si.OSType == "windows" {
// The daemon didn't advertise BuildKit as the preferred builder,
// so use the legacy builder, which is still the default for
// Windows / WCOW.
return args, osargs, nil, nil
}
}
if buildKitDisabled {
// When using a Linux daemon, print a warning that the legacy builder
// is deprecated. For Windows / WCOW, BuildKit is still experimental,
// so we don't print this warning, even if the daemon advertised that
// it supports BuildKit.
if dockerCli.ServerInfo().OSType != "windows" {
_, _ = fmt.Fprintf(dockerCli.Err(), "%s\n\n", buildkitDisabledWarning)
}
return args, osargs, nil, nil
}
// check plugin is available if cmd forwarded
plugin, perr := pluginmanager.GetPlugin(builderAlias, dockerCli, cmd.Root())
if perr == nil && plugin != nil {
perr = plugin.Err
}
if perr != nil {
// if builder is enforced with DOCKER_BUILDKIT=1, cmd must fail
// if the plugin is missing or broken.
if useBuilder {
return args, osargs, nil, newBuilderError(buildxMissingError, perr)
}
// otherwise, display warning and continue
_, _ = fmt.Fprintf(dockerCli.Err(), "%s\n\n", newBuilderError(buildxMissingWarning, perr))
return args, osargs, nil, nil
}
// If build subcommand is forwarded, user would expect "docker build" to
// always create a local docker image (default context builder). This is
// for better backward compatibility in case where a user could switch to
// a docker container builder with "docker buildx --use foo" which does
// not --load by default. Also makes sure that an arbitrary builder name
// is not being set in the command line or in the environment before
// setting the default context and keep "buildx install" behavior if being
// set (builder alias).
if forwarded && !useAlias && !hasBuilderName(args, os.Environ()) {
envs = append([]string{"BUILDX_BUILDER=" + dockerCli.CurrentContext()}, envs...)
}
// overwrite the command path for this plugin using the alias name.
cmd.Annotations[pluginmanager.CommandAnnotationPluginCommandPath] = strings.Join(append([]string{cmd.CommandPath()}, fwcmdpath...), " ")
return fwargs, fwosargs, envs, nil
}
func forwardBuilder(alias string, args, osargs []string) ([]string, []string, []string, bool) {
aliases := [][3][]string{
{
{"builder"},
{alias},
{"builder"},
},
{
{"build"},
{alias, "build"},
{},
},
{
{"image", "build"},
{alias, "build"},
{"image"},
},
}
for _, al := range aliases {
if fwargs, changed := command.StringSliceReplaceAt(args, al[0], al[1], 0); changed {
fwosargs, _ := command.StringSliceReplaceAt(osargs, al[0], al[1], -1)
fwcmdpath := al[2]
return fwargs, fwosargs, fwcmdpath, true
}
}
return args, osargs, nil, false
}
// hasBuilderName checks if a builder name is defined in args or env vars
func hasBuilderName(args []string, envs []string) bool {
var builder string
flagset := pflag.NewFlagSet("buildx", pflag.ContinueOnError)
flagset.Usage = func() {}
flagset.SetOutput(io.Discard)
flagset.StringVar(&builder, "builder", "", "")
_ = flagset.Parse(args)
if builder != "" {
return true
}
for _, e := range envs {
if strings.HasPrefix(e, "BUILDX_BUILDER=") && e != "BUILDX_BUILDER=" {
return true
}
}
return false
}