Skip to content

Commit 1ffff4c

Browse files
committed
Merge pull request moby#15182 from mapuri/build-arg
Support for passing build-time variables in build context
2 parents 4dfa996 + 8cfcd87 commit 1ffff4c

18 files changed

Lines changed: 1101 additions & 57 deletions

File tree

api/client/build.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"github.com/docker/docker/pkg/units"
3636
"github.com/docker/docker/pkg/urlutil"
3737
"github.com/docker/docker/registry"
38+
"github.com/docker/docker/runconfig"
3839
"github.com/docker/docker/utils"
3940
)
4041

@@ -64,6 +65,8 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
6465
flCPUSetCpus := cmd.String([]string{"-cpuset-cpus"}, "", "CPUs in which to allow execution (0-3, 0,1)")
6566
flCPUSetMems := cmd.String([]string{"-cpuset-mems"}, "", "MEMs in which to allow execution (0-3, 0,1)")
6667
flCgroupParent := cmd.String([]string{"-cgroup-parent"}, "", "Optional parent cgroup for the container")
68+
flBuildArg := opts.NewListOpts(opts.ValidateEnv)
69+
cmd.Var(&flBuildArg, []string{"-build-arg"}, "Set build-time variables")
6770

6871
ulimits := make(map[string]*ulimit.Ulimit)
6972
flUlimits := opts.NewUlimitOpt(&ulimits)
@@ -257,6 +260,14 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
257260
}
258261
v.Set("ulimits", string(ulimitsJSON))
259262

263+
// collect all the build-time environment variables for the container
264+
buildArgs := runconfig.ConvertKVStringsToMap(flBuildArg.GetAll())
265+
buildArgsJSON, err := json.Marshal(buildArgs)
266+
if err != nil {
267+
return err
268+
}
269+
v.Set("buildargs", string(buildArgsJSON))
270+
260271
headers := http.Header(make(map[string][]string))
261272
buf, err := json.Marshal(cli.configFile.AuthConfigs)
262273
if err != nil {

api/server/image.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,15 @@ func (s *Server) postBuild(ctx context.Context, w http.ResponseWriter, r *http.R
323323
buildConfig.Ulimits = buildUlimits
324324
}
325325

326+
var buildArgs = map[string]string{}
327+
buildArgsJSON := r.FormValue("buildargs")
328+
if buildArgsJSON != "" {
329+
if err := json.NewDecoder(strings.NewReader(buildArgsJSON)).Decode(&buildArgs); err != nil {
330+
return err
331+
}
332+
}
333+
buildConfig.BuildArgs = buildArgs
334+
326335
// Job cancellation. Note: not all job types support this.
327336
if closeNotifier, ok := w.(http.CloseNotifier); ok {
328337
finished := make(chan struct{})

builder/command/command.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const (
1818
Volume = "volume"
1919
User = "user"
2020
StopSignal = "stopsignal"
21+
Arg = "arg"
2122
)
2223

2324
// Commands is list of all Dockerfile commands
@@ -37,4 +38,5 @@ var Commands = map[string]struct{}{
3738
Volume: {},
3839
User: {},
3940
StopSignal: {},
41+
Arg: {},
4042
}

builder/dispatchers.go

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -327,15 +327,59 @@ func run(b *builder, args []string, attributes map[string]bool, original string)
327327
return err
328328
}
329329

330+
// stash the cmd
330331
cmd := b.Config.Cmd
331-
// set Cmd manually, this is special case only for Dockerfiles
332-
b.Config.Cmd = config.Cmd
333332
runconfig.Merge(b.Config, config)
333+
// stash the config environment
334+
env := b.Config.Env
334335

335336
defer func(cmd *stringutils.StrSlice) { b.Config.Cmd = cmd }(cmd)
337+
defer func(env []string) { b.Config.Env = env }(env)
338+
339+
// derive the net build-time environment for this run. We let config
340+
// environment override the build time environment.
341+
// This means that we take the b.buildArgs list of env vars and remove
342+
// any of those variables that are defined as part of the container. In other
343+
// words, anything in b.Config.Env. What's left is the list of build-time env
344+
// vars that we need to add to each RUN command - note the list could be empty.
345+
//
346+
// We don't persist the build time environment with container's config
347+
// environment, but just sort and prepend it to the command string at time
348+
// of commit.
349+
// This helps with tracing back the image's actual environment at the time
350+
// of RUN, without leaking it to the final image. It also aids cache
351+
// lookup for same image built with same build time environment.
352+
cmdBuildEnv := []string{}
353+
configEnv := runconfig.ConvertKVStringsToMap(b.Config.Env)
354+
for key, val := range b.buildArgs {
355+
if !b.isBuildArgAllowed(key) {
356+
// skip build-args that are not in allowed list, meaning they have
357+
// not been defined by an "ARG" Dockerfile command yet.
358+
// This is an error condition but only if there is no "ARG" in the entire
359+
// Dockerfile, so we'll generate any necessary errors after we parsed
360+
// the entire file (see 'leftoverArgs' processing in evaluator.go )
361+
continue
362+
}
363+
if _, ok := configEnv[key]; !ok {
364+
cmdBuildEnv = append(cmdBuildEnv, fmt.Sprintf("%s=%s", key, val))
365+
}
366+
}
336367

337-
logrus.Debugf("[BUILDER] Command to be executed: %v", b.Config.Cmd)
368+
// derive the command to use for probeCache() and to commit in this container.
369+
// Note that we only do this if there are any build-time env vars. Also, we
370+
// use the special argument "|#" at the start of the args array. This will
371+
// avoid conflicts with any RUN command since commands can not
372+
// start with | (vertical bar). The "#" (number of build envs) is there to
373+
// help ensure proper cache matches. We don't want a RUN command
374+
// that starts with "foo=abc" to be considered part of a build-time env var.
375+
saveCmd := config.Cmd
376+
if len(cmdBuildEnv) > 0 {
377+
sort.Strings(cmdBuildEnv)
378+
tmpEnv := append([]string{fmt.Sprintf("|%d", len(cmdBuildEnv))}, cmdBuildEnv...)
379+
saveCmd = stringutils.NewStrSlice(append(tmpEnv, saveCmd.Slice()...)...)
380+
}
338381

382+
b.Config.Cmd = saveCmd
339383
hit, err := b.probeCache()
340384
if err != nil {
341385
return err
@@ -344,6 +388,13 @@ func run(b *builder, args []string, attributes map[string]bool, original string)
344388
return nil
345389
}
346390

391+
// set Cmd manually, this is special case only for Dockerfiles
392+
b.Config.Cmd = config.Cmd
393+
// set build-time environment for 'run'.
394+
b.Config.Env = append(b.Config.Env, cmdBuildEnv...)
395+
396+
logrus.Debugf("[BUILDER] Command to be executed: %v", b.Config.Cmd)
397+
347398
c, err := b.create()
348399
if err != nil {
349400
return err
@@ -358,6 +409,12 @@ func run(b *builder, args []string, attributes map[string]bool, original string)
358409
if err != nil {
359410
return err
360411
}
412+
413+
// revert to original config environment and set the command string to
414+
// have the build-time env vars in it (if any) so that future cache look-ups
415+
// properly match it.
416+
b.Config.Env = env
417+
b.Config.Cmd = saveCmd
361418
if err := b.commit(c.ID, cmd, "run"); err != nil {
362419
return err
363420
}
@@ -557,3 +614,47 @@ func stopSignal(b *builder, args []string, attributes map[string]bool, original
557614
b.Config.StopSignal = sig
558615
return b.commit("", b.Config.Cmd, fmt.Sprintf("STOPSIGNAL %v", args))
559616
}
617+
618+
// ARG name[=value]
619+
//
620+
// Adds the variable foo to the trusted list of variables that can be passed
621+
// to builder using the --build-arg flag for expansion/subsitution or passing to 'run'.
622+
// Dockerfile author may optionally set a default value of this variable.
623+
func arg(b *builder, args []string, attributes map[string]bool, original string) error {
624+
if len(args) != 1 {
625+
return fmt.Errorf("ARG requires exactly one argument definition")
626+
}
627+
628+
var (
629+
name string
630+
value string
631+
hasDefault bool
632+
)
633+
634+
arg := args[0]
635+
// 'arg' can just be a name or name-value pair. Note that this is different
636+
// from 'env' that handles the split of name and value at the parser level.
637+
// The reason for doing it differently for 'arg' is that we support just
638+
// defining an arg and not assign it a value (while 'env' always expects a
639+
// name-value pair). If possible, it will be good to harmonize the two.
640+
if strings.Contains(arg, "=") {
641+
parts := strings.SplitN(arg, "=", 2)
642+
name = parts[0]
643+
value = parts[1]
644+
hasDefault = true
645+
} else {
646+
name = arg
647+
hasDefault = false
648+
}
649+
// add the arg to allowed list of build-time args from this step on.
650+
b.allowedBuildArgs[name] = true
651+
652+
// If there is a default value associated with this arg then add it to the
653+
// b.buildArgs if one is not already passed to the builder. The args passed
654+
// to builder override the defaut value of 'arg'.
655+
if _, ok := b.buildArgs[name]; !ok && hasDefault {
656+
b.buildArgs[name] = value
657+
}
658+
659+
return b.commit("", b.Config.Cmd, fmt.Sprintf("ARG %s", arg))
660+
}

builder/evaluator.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ var replaceEnvAllowed = map[string]struct{}{
5454
command.Volume: {},
5555
command.User: {},
5656
command.StopSignal: {},
57+
command.Arg: {},
5758
}
5859

5960
var evaluateTable map[string]func(*builder, []string, map[string]bool, string) error
@@ -75,6 +76,7 @@ func init() {
7576
command.Volume: volume,
7677
command.User: user,
7778
command.StopSignal: stopSignal,
79+
command.Arg: arg,
7880
}
7981
}
8082

@@ -111,6 +113,9 @@ type builder struct {
111113

112114
Config *runconfig.Config // runconfig for cmd, run, entrypoint etc.
113115

116+
buildArgs map[string]string // build-time args received in build context for expansion/substitution and commands in 'run'.
117+
allowedBuildArgs map[string]bool // list of build-time args that are allowed for expansion/substitution and passing to commands in 'run'.
118+
114119
// both of these are controlled by the Remove and ForceRemove options in BuildOpts
115120
TmpContainers map[string]struct{} // a map of containers used for removes
116121

@@ -194,6 +199,18 @@ func (b *builder) Run(context io.Reader) (string, error) {
194199
}
195200
}
196201

202+
// check if there are any leftover build-args that were passed but not
203+
// consumed during build. Return an error, if there are any.
204+
leftoverArgs := []string{}
205+
for arg := range b.buildArgs {
206+
if !b.isBuildArgAllowed(arg) {
207+
leftoverArgs = append(leftoverArgs, arg)
208+
}
209+
}
210+
if len(leftoverArgs) > 0 {
211+
return "", fmt.Errorf("One or more build-args %v were not consumed, failing build.", leftoverArgs)
212+
}
213+
197214
if b.image == "" {
198215
return "", fmt.Errorf("No image was generated. Is your Dockerfile empty?")
199216
}
@@ -268,6 +285,18 @@ func (b *builder) readDockerfile() error {
268285
return nil
269286
}
270287

288+
// determine if build arg is part of built-in args or user
289+
// defined args in Dockerfile at any point in time.
290+
func (b *builder) isBuildArgAllowed(arg string) bool {
291+
if _, ok := BuiltinAllowedBuildArgs[arg]; ok {
292+
return true
293+
}
294+
if _, ok := b.allowedBuildArgs[arg]; ok {
295+
return true
296+
}
297+
return false
298+
}
299+
271300
// This method is the entrypoint to all statement handling routines.
272301
//
273302
// Almost all nodes will have this structure:
@@ -330,13 +359,34 @@ func (b *builder) dispatch(stepN int, ast *parser.Node) error {
330359
msgList := make([]string, n)
331360

332361
var i int
362+
// Append the build-time args to config-environment.
363+
// This allows builder config to override the variables, making the behavior similar to
364+
// a shell script i.e. `ENV foo bar` overrides value of `foo` passed in build
365+
// context. But `ENV foo $foo` will use the value from build context if one
366+
// isn't already been defined by a previous ENV primitive.
367+
// Note, we get this behavior because we know that ProcessWord() will
368+
// stop on the first occurrence of a variable name and not notice
369+
// a subsequent one. So, putting the buildArgs list after the Config.Env
370+
// list, in 'envs', is safe.
371+
envs := b.Config.Env
372+
for key, val := range b.buildArgs {
373+
if !b.isBuildArgAllowed(key) {
374+
// skip build-args that are not in allowed list, meaning they have
375+
// not been defined by an "ARG" Dockerfile command yet.
376+
// This is an error condition but only if there is no "ARG" in the entire
377+
// Dockerfile, so we'll generate any necessary errors after we parsed
378+
// the entire file (see 'leftoverArgs' processing in evaluator.go )
379+
continue
380+
}
381+
envs = append(envs, fmt.Sprintf("%s=%s", key, val))
382+
}
333383
for ast.Next != nil {
334384
ast = ast.Next
335385
var str string
336386
str = ast.Value
337387
if _, ok := replaceEnvAllowed[cmd]; ok {
338388
var err error
339-
str, err = ProcessWord(ast.Value, b.Config.Env)
389+
str, err = ProcessWord(ast.Value, envs)
340390
if err != nil {
341391
return err
342392
}

builder/job.go

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ var validCommitCommands = map[string]bool{
4646
"workdir": true,
4747
}
4848

49+
// BuiltinAllowedBuildArgs is list of built-in allowed build args
50+
var BuiltinAllowedBuildArgs = map[string]bool{
51+
"HTTP_PROXY": true,
52+
"http_proxy": true,
53+
"HTTPS_PROXY": true,
54+
"https_proxy": true,
55+
"FTP_PROXY": true,
56+
"ftp_proxy": true,
57+
"NO_PROXY": true,
58+
"no_proxy": true,
59+
}
60+
4961
// Config contains all configs for a build job
5062
type Config struct {
5163
DockerfileName string
@@ -66,6 +78,7 @@ type Config struct {
6678
CgroupParent string
6779
Ulimits []*ulimit.Ulimit
6880
AuthConfigs map[string]cliconfig.AuthConfig
81+
BuildArgs map[string]string
6982

7083
Stdout io.Writer
7184
Context io.ReadCloser
@@ -191,26 +204,28 @@ func Build(d *daemon.Daemon, buildConfig *Config) error {
191204
Writer: buildConfig.Stdout,
192205
StreamFormatter: sf,
193206
},
194-
Verbose: !buildConfig.SuppressOutput,
195-
UtilizeCache: !buildConfig.NoCache,
196-
Remove: buildConfig.Remove,
197-
ForceRemove: buildConfig.ForceRemove,
198-
Pull: buildConfig.Pull,
199-
OutOld: buildConfig.Stdout,
200-
StreamFormatter: sf,
201-
AuthConfigs: buildConfig.AuthConfigs,
202-
dockerfileName: buildConfig.DockerfileName,
203-
cpuShares: buildConfig.CPUShares,
204-
cpuPeriod: buildConfig.CPUPeriod,
205-
cpuQuota: buildConfig.CPUQuota,
206-
cpuSetCpus: buildConfig.CPUSetCpus,
207-
cpuSetMems: buildConfig.CPUSetMems,
208-
cgroupParent: buildConfig.CgroupParent,
209-
memory: buildConfig.Memory,
210-
memorySwap: buildConfig.MemorySwap,
211-
ulimits: buildConfig.Ulimits,
212-
cancelled: buildConfig.WaitCancelled(),
213-
id: stringid.GenerateRandomID(),
207+
Verbose: !buildConfig.SuppressOutput,
208+
UtilizeCache: !buildConfig.NoCache,
209+
Remove: buildConfig.Remove,
210+
ForceRemove: buildConfig.ForceRemove,
211+
Pull: buildConfig.Pull,
212+
OutOld: buildConfig.Stdout,
213+
StreamFormatter: sf,
214+
AuthConfigs: buildConfig.AuthConfigs,
215+
dockerfileName: buildConfig.DockerfileName,
216+
cpuShares: buildConfig.CPUShares,
217+
cpuPeriod: buildConfig.CPUPeriod,
218+
cpuQuota: buildConfig.CPUQuota,
219+
cpuSetCpus: buildConfig.CPUSetCpus,
220+
cpuSetMems: buildConfig.CPUSetMems,
221+
cgroupParent: buildConfig.CgroupParent,
222+
memory: buildConfig.Memory,
223+
memorySwap: buildConfig.MemorySwap,
224+
ulimits: buildConfig.Ulimits,
225+
cancelled: buildConfig.WaitCancelled(),
226+
id: stringid.GenerateRandomID(),
227+
buildArgs: buildConfig.BuildArgs,
228+
allowedBuildArgs: make(map[string]bool),
214229
}
215230

216231
defer func() {

0 commit comments

Comments
 (0)