File: builder.go

package info (click to toggle)
docker.io 27.5.1%2Bdfsg4-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 67,384 kB
  • sloc: sh: 5,847; makefile: 1,146; ansic: 664; python: 162; asm: 133
file content (180 lines) | stat: -rw-r--r-- 5,876 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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
}