浏览代码

Merge pull request #15182 from mapuri/build-arg

Support for passing build-time variables in build context
Tibor Vass 9 年之前
父节点
当前提交
1ffff4c8e2

+ 11 - 0
api/client/build.go

@@ -35,6 +35,7 @@ import (
 	"github.com/docker/docker/pkg/units"
 	"github.com/docker/docker/pkg/units"
 	"github.com/docker/docker/pkg/urlutil"
 	"github.com/docker/docker/pkg/urlutil"
 	"github.com/docker/docker/registry"
 	"github.com/docker/docker/registry"
+	"github.com/docker/docker/runconfig"
 	"github.com/docker/docker/utils"
 	"github.com/docker/docker/utils"
 )
 )
 
 
@@ -64,6 +65,8 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
 	flCPUSetCpus := cmd.String([]string{"-cpuset-cpus"}, "", "CPUs in which to allow execution (0-3, 0,1)")
 	flCPUSetCpus := cmd.String([]string{"-cpuset-cpus"}, "", "CPUs in which to allow execution (0-3, 0,1)")
 	flCPUSetMems := cmd.String([]string{"-cpuset-mems"}, "", "MEMs in which to allow execution (0-3, 0,1)")
 	flCPUSetMems := cmd.String([]string{"-cpuset-mems"}, "", "MEMs in which to allow execution (0-3, 0,1)")
 	flCgroupParent := cmd.String([]string{"-cgroup-parent"}, "", "Optional parent cgroup for the container")
 	flCgroupParent := cmd.String([]string{"-cgroup-parent"}, "", "Optional parent cgroup for the container")
+	flBuildArg := opts.NewListOpts(opts.ValidateEnv)
+	cmd.Var(&flBuildArg, []string{"-build-arg"}, "Set build-time variables")
 
 
 	ulimits := make(map[string]*ulimit.Ulimit)
 	ulimits := make(map[string]*ulimit.Ulimit)
 	flUlimits := opts.NewUlimitOpt(&ulimits)
 	flUlimits := opts.NewUlimitOpt(&ulimits)
@@ -257,6 +260,14 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
 	}
 	}
 	v.Set("ulimits", string(ulimitsJSON))
 	v.Set("ulimits", string(ulimitsJSON))
 
 
+	// collect all the build-time environment variables for the container
+	buildArgs := runconfig.ConvertKVStringsToMap(flBuildArg.GetAll())
+	buildArgsJSON, err := json.Marshal(buildArgs)
+	if err != nil {
+		return err
+	}
+	v.Set("buildargs", string(buildArgsJSON))
+
 	headers := http.Header(make(map[string][]string))
 	headers := http.Header(make(map[string][]string))
 	buf, err := json.Marshal(cli.configFile.AuthConfigs)
 	buf, err := json.Marshal(cli.configFile.AuthConfigs)
 	if err != nil {
 	if err != nil {

+ 9 - 0
api/server/image.go

@@ -323,6 +323,15 @@ func (s *Server) postBuild(ctx context.Context, w http.ResponseWriter, r *http.R
 		buildConfig.Ulimits = buildUlimits
 		buildConfig.Ulimits = buildUlimits
 	}
 	}
 
 
+	var buildArgs = map[string]string{}
+	buildArgsJSON := r.FormValue("buildargs")
+	if buildArgsJSON != "" {
+		if err := json.NewDecoder(strings.NewReader(buildArgsJSON)).Decode(&buildArgs); err != nil {
+			return err
+		}
+	}
+	buildConfig.BuildArgs = buildArgs
+
 	// Job cancellation. Note: not all job types support this.
 	// Job cancellation. Note: not all job types support this.
 	if closeNotifier, ok := w.(http.CloseNotifier); ok {
 	if closeNotifier, ok := w.(http.CloseNotifier); ok {
 		finished := make(chan struct{})
 		finished := make(chan struct{})

+ 2 - 0
builder/command/command.go

@@ -18,6 +18,7 @@ const (
 	Volume     = "volume"
 	Volume     = "volume"
 	User       = "user"
 	User       = "user"
 	StopSignal = "stopsignal"
 	StopSignal = "stopsignal"
+	Arg        = "arg"
 )
 )
 
 
 // Commands is list of all Dockerfile commands
 // Commands is list of all Dockerfile commands
@@ -37,4 +38,5 @@ var Commands = map[string]struct{}{
 	Volume:     {},
 	Volume:     {},
 	User:       {},
 	User:       {},
 	StopSignal: {},
 	StopSignal: {},
+	Arg:        {},
 }
 }

+ 104 - 3
builder/dispatchers.go

@@ -327,15 +327,59 @@ func run(b *builder, args []string, attributes map[string]bool, original string)
 		return err
 		return err
 	}
 	}
 
 
+	// stash the cmd
 	cmd := b.Config.Cmd
 	cmd := b.Config.Cmd
-	// set Cmd manually, this is special case only for Dockerfiles
-	b.Config.Cmd = config.Cmd
 	runconfig.Merge(b.Config, config)
 	runconfig.Merge(b.Config, config)
+	// stash the config environment
+	env := b.Config.Env
 
 
 	defer func(cmd *stringutils.StrSlice) { b.Config.Cmd = cmd }(cmd)
 	defer func(cmd *stringutils.StrSlice) { b.Config.Cmd = cmd }(cmd)
+	defer func(env []string) { b.Config.Env = env }(env)
+
+	// derive the net build-time environment for this run. We let config
+	// environment override the build time environment.
+	// This means that we take the b.buildArgs list of env vars and remove
+	// any of those variables that are defined as part of the container. In other
+	// words, anything in b.Config.Env. What's left is the list of build-time env
+	// vars that we need to add to each RUN command - note the list could be empty.
+	//
+	// We don't persist the build time environment with container's config
+	// environment, but just sort and prepend it to the command string at time
+	// of commit.
+	// This helps with tracing back the image's actual environment at the time
+	// of RUN, without leaking it to the final image. It also aids cache
+	// lookup for same image built with same build time environment.
+	cmdBuildEnv := []string{}
+	configEnv := runconfig.ConvertKVStringsToMap(b.Config.Env)
+	for key, val := range b.buildArgs {
+		if !b.isBuildArgAllowed(key) {
+			// skip build-args that are not in allowed list, meaning they have
+			// not been defined by an "ARG" Dockerfile command yet.
+			// This is an error condition but only if there is no "ARG" in the entire
+			// Dockerfile, so we'll generate any necessary errors after we parsed
+			// the entire file (see 'leftoverArgs' processing in evaluator.go )
+			continue
+		}
+		if _, ok := configEnv[key]; !ok {
+			cmdBuildEnv = append(cmdBuildEnv, fmt.Sprintf("%s=%s", key, val))
+		}
+	}
 
 
-	logrus.Debugf("[BUILDER] Command to be executed: %v", b.Config.Cmd)
+	// derive the command to use for probeCache() and to commit in this container.
+	// Note that we only do this if there are any build-time env vars.  Also, we
+	// use the special argument "|#" at the start of the args array. This will
+	// avoid conflicts with any RUN command since commands can not
+	// start with | (vertical bar). The "#" (number of build envs) is there to
+	// help ensure proper cache matches. We don't want a RUN command
+	// that starts with "foo=abc" to be considered part of a build-time env var.
+	saveCmd := config.Cmd
+	if len(cmdBuildEnv) > 0 {
+		sort.Strings(cmdBuildEnv)
+		tmpEnv := append([]string{fmt.Sprintf("|%d", len(cmdBuildEnv))}, cmdBuildEnv...)
+		saveCmd = stringutils.NewStrSlice(append(tmpEnv, saveCmd.Slice()...)...)
+	}
 
 
+	b.Config.Cmd = saveCmd
 	hit, err := b.probeCache()
 	hit, err := b.probeCache()
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -344,6 +388,13 @@ func run(b *builder, args []string, attributes map[string]bool, original string)
 		return nil
 		return nil
 	}
 	}
 
 
+	// set Cmd manually, this is special case only for Dockerfiles
+	b.Config.Cmd = config.Cmd
+	// set build-time environment for 'run'.
+	b.Config.Env = append(b.Config.Env, cmdBuildEnv...)
+
+	logrus.Debugf("[BUILDER] Command to be executed: %v", b.Config.Cmd)
+
 	c, err := b.create()
 	c, err := b.create()
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -358,6 +409,12 @@ func run(b *builder, args []string, attributes map[string]bool, original string)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
+
+	// revert to original config environment and set the command string to
+	// have the build-time env vars in it (if any) so that future cache look-ups
+	// properly match it.
+	b.Config.Env = env
+	b.Config.Cmd = saveCmd
 	if err := b.commit(c.ID, cmd, "run"); err != nil {
 	if err := b.commit(c.ID, cmd, "run"); err != nil {
 		return err
 		return err
 	}
 	}
@@ -557,3 +614,47 @@ func stopSignal(b *builder, args []string, attributes map[string]bool, original
 	b.Config.StopSignal = sig
 	b.Config.StopSignal = sig
 	return b.commit("", b.Config.Cmd, fmt.Sprintf("STOPSIGNAL %v", args))
 	return b.commit("", b.Config.Cmd, fmt.Sprintf("STOPSIGNAL %v", args))
 }
 }
+
+// ARG name[=value]
+//
+// Adds the variable foo to the trusted list of variables that can be passed
+// to builder using the --build-arg flag for expansion/subsitution or passing to 'run'.
+// Dockerfile author may optionally set a default value of this variable.
+func arg(b *builder, args []string, attributes map[string]bool, original string) error {
+	if len(args) != 1 {
+		return fmt.Errorf("ARG requires exactly one argument definition")
+	}
+
+	var (
+		name       string
+		value      string
+		hasDefault bool
+	)
+
+	arg := args[0]
+	// 'arg' can just be a name or name-value pair. Note that this is different
+	// from 'env' that handles the split of name and value at the parser level.
+	// The reason for doing it differently for 'arg' is that we support just
+	// defining an arg and not assign it a value (while 'env' always expects a
+	// name-value pair). If possible, it will be good to harmonize the two.
+	if strings.Contains(arg, "=") {
+		parts := strings.SplitN(arg, "=", 2)
+		name = parts[0]
+		value = parts[1]
+		hasDefault = true
+	} else {
+		name = arg
+		hasDefault = false
+	}
+	// add the arg to allowed list of build-time args from this step on.
+	b.allowedBuildArgs[name] = true
+
+	// If there is a default value associated with this arg then add it to the
+	// b.buildArgs if one is not already passed to the builder. The args passed
+	// to builder override the defaut value of 'arg'.
+	if _, ok := b.buildArgs[name]; !ok && hasDefault {
+		b.buildArgs[name] = value
+	}
+
+	return b.commit("", b.Config.Cmd, fmt.Sprintf("ARG %s", arg))
+}

+ 51 - 1
builder/evaluator.go

@@ -54,6 +54,7 @@ var replaceEnvAllowed = map[string]struct{}{
 	command.Volume:     {},
 	command.Volume:     {},
 	command.User:       {},
 	command.User:       {},
 	command.StopSignal: {},
 	command.StopSignal: {},
+	command.Arg:        {},
 }
 }
 
 
 var evaluateTable map[string]func(*builder, []string, map[string]bool, string) error
 var evaluateTable map[string]func(*builder, []string, map[string]bool, string) error
@@ -75,6 +76,7 @@ func init() {
 		command.Volume:     volume,
 		command.Volume:     volume,
 		command.User:       user,
 		command.User:       user,
 		command.StopSignal: stopSignal,
 		command.StopSignal: stopSignal,
+		command.Arg:        arg,
 	}
 	}
 }
 }
 
 
@@ -111,6 +113,9 @@ type builder struct {
 
 
 	Config *runconfig.Config // runconfig for cmd, run, entrypoint etc.
 	Config *runconfig.Config // runconfig for cmd, run, entrypoint etc.
 
 
+	buildArgs        map[string]string // build-time args received in build context for expansion/substitution and commands in 'run'.
+	allowedBuildArgs map[string]bool   // list of build-time args that are allowed for expansion/substitution and passing to commands in 'run'.
+
 	// both of these are controlled by the Remove and ForceRemove options in BuildOpts
 	// both of these are controlled by the Remove and ForceRemove options in BuildOpts
 	TmpContainers map[string]struct{} // a map of containers used for removes
 	TmpContainers map[string]struct{} // a map of containers used for removes
 
 
@@ -194,6 +199,18 @@ func (b *builder) Run(context io.Reader) (string, error) {
 		}
 		}
 	}
 	}
 
 
+	// check if there are any leftover build-args that were passed but not
+	// consumed during build. Return an error, if there are any.
+	leftoverArgs := []string{}
+	for arg := range b.buildArgs {
+		if !b.isBuildArgAllowed(arg) {
+			leftoverArgs = append(leftoverArgs, arg)
+		}
+	}
+	if len(leftoverArgs) > 0 {
+		return "", fmt.Errorf("One or more build-args %v were not consumed, failing build.", leftoverArgs)
+	}
+
 	if b.image == "" {
 	if b.image == "" {
 		return "", fmt.Errorf("No image was generated. Is your Dockerfile empty?")
 		return "", fmt.Errorf("No image was generated. Is your Dockerfile empty?")
 	}
 	}
@@ -268,6 +285,18 @@ func (b *builder) readDockerfile() error {
 	return nil
 	return nil
 }
 }
 
 
+// determine if build arg is part of built-in args or user
+// defined args in Dockerfile at any point in time.
+func (b *builder) isBuildArgAllowed(arg string) bool {
+	if _, ok := BuiltinAllowedBuildArgs[arg]; ok {
+		return true
+	}
+	if _, ok := b.allowedBuildArgs[arg]; ok {
+		return true
+	}
+	return false
+}
+
 // This method is the entrypoint to all statement handling routines.
 // This method is the entrypoint to all statement handling routines.
 //
 //
 // Almost all nodes will have this structure:
 // Almost all nodes will have this structure:
@@ -330,13 +359,34 @@ func (b *builder) dispatch(stepN int, ast *parser.Node) error {
 	msgList := make([]string, n)
 	msgList := make([]string, n)
 
 
 	var i int
 	var i int
+	// Append the build-time args to config-environment.
+	// This allows builder config to override the variables, making the behavior similar to
+	// a shell script i.e. `ENV foo bar` overrides value of `foo` passed in build
+	// context. But `ENV foo $foo` will use the value from build context if one
+	// isn't already been defined by a previous ENV primitive.
+	// Note, we get this behavior because we know that ProcessWord() will
+	// stop on the first occurrence of a variable name and not notice
+	// a subsequent one. So, putting the buildArgs list after the Config.Env
+	// list, in 'envs', is safe.
+	envs := b.Config.Env
+	for key, val := range b.buildArgs {
+		if !b.isBuildArgAllowed(key) {
+			// skip build-args that are not in allowed list, meaning they have
+			// not been defined by an "ARG" Dockerfile command yet.
+			// This is an error condition but only if there is no "ARG" in the entire
+			// Dockerfile, so we'll generate any necessary errors after we parsed
+			// the entire file (see 'leftoverArgs' processing in evaluator.go )
+			continue
+		}
+		envs = append(envs, fmt.Sprintf("%s=%s", key, val))
+	}
 	for ast.Next != nil {
 	for ast.Next != nil {
 		ast = ast.Next
 		ast = ast.Next
 		var str string
 		var str string
 		str = ast.Value
 		str = ast.Value
 		if _, ok := replaceEnvAllowed[cmd]; ok {
 		if _, ok := replaceEnvAllowed[cmd]; ok {
 			var err error
 			var err error
-			str, err = ProcessWord(ast.Value, b.Config.Env)
+			str, err = ProcessWord(ast.Value, envs)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}

+ 35 - 20
builder/job.go

@@ -46,6 +46,18 @@ var validCommitCommands = map[string]bool{
 	"workdir":    true,
 	"workdir":    true,
 }
 }
 
 
+// BuiltinAllowedBuildArgs is list of built-in allowed build args
+var BuiltinAllowedBuildArgs = map[string]bool{
+	"HTTP_PROXY":  true,
+	"http_proxy":  true,
+	"HTTPS_PROXY": true,
+	"https_proxy": true,
+	"FTP_PROXY":   true,
+	"ftp_proxy":   true,
+	"NO_PROXY":    true,
+	"no_proxy":    true,
+}
+
 // Config contains all configs for a build job
 // Config contains all configs for a build job
 type Config struct {
 type Config struct {
 	DockerfileName string
 	DockerfileName string
@@ -66,6 +78,7 @@ type Config struct {
 	CgroupParent   string
 	CgroupParent   string
 	Ulimits        []*ulimit.Ulimit
 	Ulimits        []*ulimit.Ulimit
 	AuthConfigs    map[string]cliconfig.AuthConfig
 	AuthConfigs    map[string]cliconfig.AuthConfig
+	BuildArgs      map[string]string
 
 
 	Stdout  io.Writer
 	Stdout  io.Writer
 	Context io.ReadCloser
 	Context io.ReadCloser
@@ -191,26 +204,28 @@ func Build(d *daemon.Daemon, buildConfig *Config) error {
 			Writer:          buildConfig.Stdout,
 			Writer:          buildConfig.Stdout,
 			StreamFormatter: sf,
 			StreamFormatter: sf,
 		},
 		},
-		Verbose:         !buildConfig.SuppressOutput,
-		UtilizeCache:    !buildConfig.NoCache,
-		Remove:          buildConfig.Remove,
-		ForceRemove:     buildConfig.ForceRemove,
-		Pull:            buildConfig.Pull,
-		OutOld:          buildConfig.Stdout,
-		StreamFormatter: sf,
-		AuthConfigs:     buildConfig.AuthConfigs,
-		dockerfileName:  buildConfig.DockerfileName,
-		cpuShares:       buildConfig.CPUShares,
-		cpuPeriod:       buildConfig.CPUPeriod,
-		cpuQuota:        buildConfig.CPUQuota,
-		cpuSetCpus:      buildConfig.CPUSetCpus,
-		cpuSetMems:      buildConfig.CPUSetMems,
-		cgroupParent:    buildConfig.CgroupParent,
-		memory:          buildConfig.Memory,
-		memorySwap:      buildConfig.MemorySwap,
-		ulimits:         buildConfig.Ulimits,
-		cancelled:       buildConfig.WaitCancelled(),
-		id:              stringid.GenerateRandomID(),
+		Verbose:          !buildConfig.SuppressOutput,
+		UtilizeCache:     !buildConfig.NoCache,
+		Remove:           buildConfig.Remove,
+		ForceRemove:      buildConfig.ForceRemove,
+		Pull:             buildConfig.Pull,
+		OutOld:           buildConfig.Stdout,
+		StreamFormatter:  sf,
+		AuthConfigs:      buildConfig.AuthConfigs,
+		dockerfileName:   buildConfig.DockerfileName,
+		cpuShares:        buildConfig.CPUShares,
+		cpuPeriod:        buildConfig.CPUPeriod,
+		cpuQuota:         buildConfig.CPUQuota,
+		cpuSetCpus:       buildConfig.CPUSetCpus,
+		cpuSetMems:       buildConfig.CPUSetMems,
+		cgroupParent:     buildConfig.CgroupParent,
+		memory:           buildConfig.Memory,
+		memorySwap:       buildConfig.MemorySwap,
+		ulimits:          buildConfig.Ulimits,
+		cancelled:        buildConfig.WaitCancelled(),
+		id:               stringid.GenerateRandomID(),
+		buildArgs:        buildConfig.BuildArgs,
+		allowedBuildArgs: make(map[string]bool),
 	}
 	}
 
 
 	defer func() {
 	defer func() {

+ 50 - 19
builder/parser/line_parsers.go

@@ -42,15 +42,10 @@ func parseSubCommand(rest string) (*Node, map[string]bool, error) {
 	return &Node{Children: []*Node{child}}, nil, nil
 	return &Node{Children: []*Node{child}}, nil, nil
 }
 }
 
 
-// parse environment like statements. Note that this does *not* handle
-// variable interpolation, which will be handled in the evaluator.
-func parseNameVal(rest string, key string) (*Node, map[string]bool, error) {
-	// This is kind of tricky because we need to support the old
-	// variant:   KEY name value
-	// as well as the new one:    KEY name=value ...
-	// The trigger to know which one is being used will be whether we hit
-	// a space or = first.  space ==> old, "=" ==> new
-
+// helper to parse words (i.e space delimited or quoted strings) in a statement.
+// The quotes are preserved as part of this function and they are stripped later
+// as part of processWords().
+func parseWords(rest string) []string {
 	const (
 	const (
 		inSpaces = iota // looking for start of a word
 		inSpaces = iota // looking for start of a word
 		inWord
 		inWord
@@ -89,15 +84,6 @@ func parseNameVal(rest string, key string) (*Node, map[string]bool, error) {
 				phase = inSpaces
 				phase = inSpaces
 				if blankOK || len(word) > 0 {
 				if blankOK || len(word) > 0 {
 					words = append(words, word)
 					words = append(words, word)
-
-					// Look for = and if not there assume
-					// we're doing the old stuff and
-					// just read the rest of the line
-					if !strings.Contains(word, "=") {
-						word = strings.TrimSpace(rest[pos:])
-						words = append(words, word)
-						break
-					}
 				}
 				}
 				word = ""
 				word = ""
 				blankOK = false
 				blankOK = false
@@ -141,13 +127,26 @@ func parseNameVal(rest string, key string) (*Node, map[string]bool, error) {
 		}
 		}
 	}
 	}
 
 
+	return words
+}
+
+// parse environment like statements. Note that this does *not* handle
+// variable interpolation, which will be handled in the evaluator.
+func parseNameVal(rest string, key string) (*Node, map[string]bool, error) {
+	// This is kind of tricky because we need to support the old
+	// variant:   KEY name value
+	// as well as the new one:    KEY name=value ...
+	// The trigger to know which one is being used will be whether we hit
+	// a space or = first.  space ==> old, "=" ==> new
+
+	words := parseWords(rest)
 	if len(words) == 0 {
 	if len(words) == 0 {
 		return nil, nil, nil
 		return nil, nil, nil
 	}
 	}
 
 
-	// Old format (KEY name value)
 	var rootnode *Node
 	var rootnode *Node
 
 
+	// Old format (KEY name value)
 	if !strings.Contains(words[0], "=") {
 	if !strings.Contains(words[0], "=") {
 		node := &Node{}
 		node := &Node{}
 		rootnode = node
 		rootnode = node
@@ -195,6 +194,38 @@ func parseLabel(rest string) (*Node, map[string]bool, error) {
 	return parseNameVal(rest, "LABEL")
 	return parseNameVal(rest, "LABEL")
 }
 }
 
 
+// parses a statement containing one or more keyword definition(s) and/or
+// value assignments, like `name1 name2= name3="" name4=value`.
+// Note that this is a stricter format than the old format of assignment,
+// allowed by parseNameVal(), in a way that this only allows assignment of the
+// form `keyword=[<value>]` like  `name2=`, `name3=""`, and `name4=value` above.
+// In addition, a keyword definition alone is of the form `keyword` like `name1`
+// above. And the assignments `name2=` and `name3=""` are equivalent and
+// assign an empty value to the respective keywords.
+func parseNameOrNameVal(rest string) (*Node, map[string]bool, error) {
+	words := parseWords(rest)
+	if len(words) == 0 {
+		return nil, nil, nil
+	}
+
+	var (
+		rootnode *Node
+		prevNode *Node
+	)
+	for i, word := range words {
+		node := &Node{}
+		node.Value = word
+		if i == 0 {
+			rootnode = node
+		} else {
+			prevNode.Next = node
+		}
+		prevNode = node
+	}
+
+	return rootnode, nil, nil
+}
+
 // parses a whitespace-delimited set of arguments. The result is effectively a
 // parses a whitespace-delimited set of arguments. The result is effectively a
 // linked list of string arguments.
 // linked list of string arguments.
 func parseStringsWhitespaceDelimited(rest string) (*Node, map[string]bool, error) {
 func parseStringsWhitespaceDelimited(rest string) (*Node, map[string]bool, error) {

+ 1 - 0
builder/parser/parser.go

@@ -62,6 +62,7 @@ func init() {
 		command.Expose:     parseStringsWhitespaceDelimited,
 		command.Expose:     parseStringsWhitespaceDelimited,
 		command.Volume:     parseMaybeJSONToList,
 		command.Volume:     parseMaybeJSONToList,
 		command.StopSignal: parseString,
 		command.StopSignal: parseString,
+		command.Arg:        parseNameOrNameVal,
 	}
 	}
 }
 }
 
 

+ 37 - 0
builder/parser/parser_test.go

@@ -73,3 +73,40 @@ func TestTestData(t *testing.T) {
 		}
 		}
 	}
 	}
 }
 }
+
+func TestParseWords(t *testing.T) {
+	tests := []map[string][]string{
+		{
+			"input":  {"foo"},
+			"expect": {"foo"},
+		},
+		{
+			"input":  {"foo bar"},
+			"expect": {"foo", "bar"},
+		},
+		{
+			"input":  {"foo=bar"},
+			"expect": {"foo=bar"},
+		},
+		{
+			"input":  {"foo bar 'abc xyz'"},
+			"expect": {"foo", "bar", "'abc xyz'"},
+		},
+		{
+			"input":  {`foo bar "abc xyz"`},
+			"expect": {"foo", "bar", `"abc xyz"`},
+		},
+	}
+
+	for _, test := range tests {
+		words := parseWords(test["input"][0])
+		if len(words) != len(test["expect"]) {
+			t.Fatalf("length check failed. input: %v, expect: %v, output: %v", test["input"][0], test["expect"], words)
+		}
+		for i, word := range words {
+			if word != test["expect"][i] {
+				t.Fatalf("word check failed for word: %q. input: %v, expect: %v, output: %v", word, test["input"][0], test["expect"], words)
+			}
+		}
+	}
+}

+ 1 - 1
docs/reference/api/docker_remote_api.md

@@ -86,7 +86,7 @@ This section lists each version from latest to oldest.  Each listing includes a
 * `GET /containers/(id)/stats` will return networking information respectively for each interface.
 * `GET /containers/(id)/stats` will return networking information respectively for each interface.
 * The `hostConfig` option now accepts the field `DnsOptions`, which specifies a
 * The `hostConfig` option now accepts the field `DnsOptions`, which specifies a
 list of DNS options to be used in the container.
 list of DNS options to be used in the container.
-
+* `POST /build` now optionally takes a serialized map of build-time variables.
 
 
 ### v1.20 API changes
 ### v1.20 API changes
 
 

+ 5 - 0
docs/reference/api/docker_remote_api_v1.21.md

@@ -1367,6 +1367,11 @@ Query Parameters:
 -   **memswap** - Total memory (memory + swap), `-1` to disable swap.
 -   **memswap** - Total memory (memory + swap), `-1` to disable swap.
 -   **cpushares** - CPU shares (relative weight).
 -   **cpushares** - CPU shares (relative weight).
 -   **cpusetcpus** - CPUs in which to allow execution (e.g., `0-3`, `0,1`).
 -   **cpusetcpus** - CPUs in which to allow execution (e.g., `0-3`, `0,1`).
+-   **buildargs** – JSON map of string pairs for build-time variables. Users pass
+        these values at build-time. Docker uses the `buildargs` as the environment
+        context for command(s) run via the Dockerfile's `RUN` instruction or for
+        variable expansion in other Dockerfile instructions. This is not meant for
+        passing secret values. [Read more about the buildargs instruction](/reference/builder/#arg)
 
 
     Request Headers:
     Request Headers:
 
 

+ 122 - 0
docs/reference/builder.md

@@ -966,6 +966,128 @@ For example:
 The output of the final `pwd` command in this `Dockerfile` would be
 The output of the final `pwd` command in this `Dockerfile` would be
 `/path/$DIRNAME`
 `/path/$DIRNAME`
 
 
+## ARG
+
+    ARG <name>[=<default value>]
+
+The `ARG` instruction defines a variable that users can pass at build-time to
+the builder with the `docker build` command using the `--build-arg
+<varname>=<value>` flag. If a user specifies a build argument that was not
+defined in the Dockerfile, the build outputs an error.
+
+```
+One or more build-args were not consumed, failing build.
+```
+
+The Dockerfile author can define a single variable by specifying `ARG` once or many
+variables by specifying `ARG` more than once. For example, a valid Dockerfile:
+
+```
+FROM busybox
+ARG user1
+ARG buildno
+...
+```
+
+A Dockerfile author may optionally specify a default value for an `ARG` instruction:
+
+```
+FROM busybox
+ARG user1=someuser
+ARG buildno=1
+...
+```
+
+If an `ARG` value has a default and if there is no value passed at build-time, the
+builder uses the default.
+
+An `ARG` variable definition comes into effect from the line on which it is
+defined in the `Dockerfile` not from the argument's use on the command-line or
+elsewhere.  For example, consider this Dockerfile:
+
+```
+1 FROM busybox
+2 USER ${user:-some_user}
+3 ARG user
+4 USER $user
+...
+```
+A user builds this file by calling:
+
+```
+$ docker build --build-arg user=what_user Dockerfile
+```
+
+The `USER` at line 2 evaluates to `some_user` as the `user` variable is defined on the
+subsequent line 3. The `USER` at line 4 evaluates to `what_user` as `user` is
+defined and the `what_user` value was passed on the command line. Prior to its definition by an
+`ARG` instruction, any use of a variable results in an empty string.
+
+> **Note:** It is not recommended to use build-time variables for
+>  passing secrets like github keys, user credentials etc.
+
+You can use an `ARG` or an `ENV` instruction to specify variables that are
+available to the `RUN` instruction. Environment variables defined using the
+`ENV` instruction always override an `ARG` instruction of the same name. Consider
+this Dockerfile with an `ENV` and `ARG` instruction.
+
+```
+1 FROM ubuntu
+2 ARG CONT_IMG_VER
+3 ENV CONT_IMG_VER v1.0.0
+4 RUN echo $CONT_IMG_VER
+```
+Then, assume this image is built with this command:
+
+```
+$ docker build --build-arg CONT_IMG_VER=v2.0.1 Dockerfile
+```
+
+In this case, the `RUN` instruction uses `v1.0.0` instead of the `ARG` setting
+passed by the user:`v2.0.1` This behavior is similar to a shell
+script where a locally scoped variable overrides the variables passed as
+arguments or inherited from environment, from its point of definition.
+
+Using the example above but a different `ENV` specification you can create more
+useful interactions between `ARG` and `ENV` instructions:
+
+```
+1 FROM ubuntu
+2 ARG CONT_IMG_VER
+3 ENV CONT_IMG_VER ${CONT_IMG_VER:-v1.0.0}
+4 RUN echo $CONT_IMG_VER
+```
+
+Unlike an `ARG` instruction, `ENV` values are always persisted in the built
+image. Consider a docker build without the --build-arg flag:
+
+```
+$ docker build Dockerfile
+```
+
+Using this Dockerfile example, `CONT_IMG_VER` is still persisted in the image but
+its value would be `v1.0.0` as it is the default set in line 3 by the `ENV` instruction.
+
+The variable expansion technique in this example allows you to pass arguments
+from the command line and persist them in the final image by leveraging the
+`ENV` instruction. Variable expansion is only supported for [a limited set of
+Dockerfile instructions.](#environment-replacement)
+
+Docker has a set of predefined `ARG` variables that you can use without a
+corresponding `ARG` instruction in the Dockerfile.
+
+* `HTTP_PROXY`
+* `http_proxy`
+* `HTTPS_PROXY`
+* `https_proxy`
+* `FTP_PROXY`
+* `ftp_proxy`
+* `NO_PROXY`
+* `no_proxy`
+
+To use these, simply pass them on the command line using the `--build-arg
+<varname>=<value>` flag.
+
 ## ONBUILD
 ## ONBUILD
 
 
     ONBUILD [INSTRUCTION]
     ONBUILD [INSTRUCTION]

+ 20 - 0
docs/reference/commandline/build.md

@@ -17,6 +17,7 @@ weight=1
 
 
       -f, --file=""            Name of the Dockerfile (Default is 'PATH/Dockerfile')
       -f, --file=""            Name of the Dockerfile (Default is 'PATH/Dockerfile')
       --force-rm=false         Always remove intermediate containers
       --force-rm=false         Always remove intermediate containers
+      --build-arg=[]           Set build-time variables
       --no-cache=false         Do not use cache when building the image
       --no-cache=false         Do not use cache when building the image
       --pull=false             Always attempt to pull a newer version of the image
       --pull=false             Always attempt to pull a newer version of the image
       -q, --quiet=false        Suppress the verbose output generated by the containers
       -q, --quiet=false        Suppress the verbose output generated by the containers
@@ -251,3 +252,22 @@ flag](/reference/run/#specifying-custom-cgroups).
 Using the `--ulimit` option with `docker build` will cause each build step's 
 Using the `--ulimit` option with `docker build` will cause each build step's 
 container to be started using those [`--ulimit`
 container to be started using those [`--ulimit`
 flag values](/reference/run/#setting-ulimits-in-a-container).
 flag values](/reference/run/#setting-ulimits-in-a-container).
+
+You can use `ENV` instructions in a Dockerfile to define variable
+values. These values persist in the built image. However, often
+persistence is not what you want. Users want to specify variables differently
+depending on which host they build an image on.
+
+A good example is `http_proxy` or source versions for pulling intermediate
+files. The `ARG` instruction lets Dockerfile authors define values that users
+can set at build-time using the  `---build-arg` flag:
+
+    $ docker build --build-arg HTTP_PROXY=http://10.20.30.2:1234 .
+
+This flag allows you to pass the build-time variables that are
+accessed like regular environment variables in the `RUN` instruction of the
+Dockerfile. Also, these values don't persist in the intermediate or final images
+like `ENV` values do.
+
+For detailed information on using `ARG` and `ENV` instructions, see the
+[Dockerfile reference](/reference/builder).

+ 504 - 0
integration-cli/docker_cli_build_test.go

@@ -5676,3 +5676,507 @@ func (s *DockerSuite) TestBuildStopSignal(c *check.C) {
 		c.Fatalf("Signal %s, expected SIGKILL", res)
 		c.Fatalf("Signal %s, expected SIGKILL", res)
 	}
 	}
 }
 }
+
+func (s *DockerSuite) TestBuildBuildTimeArg(c *check.C) {
+	imgName := "bldargtest"
+	envKey := "foo"
+	envVal := "bar"
+	args := []string{
+		"--build-arg", fmt.Sprintf("%s=%s", envKey, envVal),
+	}
+	dockerfile := fmt.Sprintf(`FROM busybox
+		ARG %s
+		RUN echo $%s
+		CMD echo $%s`, envKey, envKey, envKey)
+
+	if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil || !strings.Contains(out, envVal) {
+		if err != nil {
+			c.Fatalf("build failed to complete: %q %q", out, err)
+		}
+		c.Fatalf("failed to access environment variable in output: %q expected: %q", out, envVal)
+	}
+
+	containerName := "bldargCont"
+	if out, _ := dockerCmd(c, "run", "--name", containerName, imgName); out != "\n" {
+		c.Fatalf("run produced invalid output: %q, expected empty string", out)
+	}
+}
+
+func (s *DockerSuite) TestBuildBuildTimeArgHistory(c *check.C) {
+	imgName := "bldargtest"
+	envKey := "foo"
+	envVal := "bar"
+	envDef := "bar1"
+	args := []string{
+		"--build-arg", fmt.Sprintf("%s=%s", envKey, envVal),
+	}
+	dockerfile := fmt.Sprintf(`FROM busybox
+		ARG %s=%s`, envKey, envDef)
+
+	if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil || !strings.Contains(out, envVal) {
+		if err != nil {
+			c.Fatalf("build failed to complete: %q %q", out, err)
+		}
+		c.Fatalf("failed to access environment variable in output: %q expected: %q", out, envVal)
+	}
+
+	out, _ := dockerCmd(c, "history", "--no-trunc", imgName)
+	outputTabs := strings.Split(out, "\n")[1]
+	if !strings.Contains(outputTabs, envDef) {
+		c.Fatalf("failed to find arg default in image history output: %q expected: %q", outputTabs, envDef)
+	}
+}
+
+func (s *DockerSuite) TestBuildBuildTimeArgCacheHit(c *check.C) {
+	imgName := "bldargtest"
+	envKey := "foo"
+	envVal := "bar"
+	args := []string{
+		"--build-arg", fmt.Sprintf("%s=%s", envKey, envVal),
+	}
+	dockerfile := fmt.Sprintf(`FROM busybox
+		ARG %s
+		RUN echo $%s`, envKey, envKey)
+
+	origImgID := ""
+	var err error
+	if origImgID, err = buildImage(imgName, dockerfile, true, args...); err != nil {
+		c.Fatal(err)
+	}
+
+	imgNameCache := "bldargtestcachehit"
+	if newImgID, err := buildImage(imgNameCache, dockerfile, true, args...); err != nil || newImgID != origImgID {
+		if err != nil {
+			c.Fatal(err)
+		}
+		c.Fatalf("build didn't use cache! expected image id: %q built image id: %q", origImgID, newImgID)
+	}
+}
+
+func (s *DockerSuite) TestBuildBuildTimeArgCacheMissExtraArg(c *check.C) {
+	imgName := "bldargtest"
+	envKey := "foo"
+	envVal := "bar"
+	extraEnvKey := "foo1"
+	extraEnvVal := "bar1"
+	args := []string{
+		"--build-arg", fmt.Sprintf("%s=%s", envKey, envVal),
+	}
+
+	dockerfile := fmt.Sprintf(`FROM busybox
+		ARG %s
+		ARG %s
+		RUN echo $%s`, envKey, extraEnvKey, envKey)
+
+	origImgID := ""
+	var err error
+	if origImgID, err = buildImage(imgName, dockerfile, true, args...); err != nil {
+		c.Fatal(err)
+	}
+
+	imgNameCache := "bldargtestcachemiss"
+	args = append(args, "--build-arg", fmt.Sprintf("%s=%s", extraEnvKey, extraEnvVal))
+	if newImgID, err := buildImage(imgNameCache, dockerfile, true, args...); err != nil || newImgID == origImgID {
+		if err != nil {
+			c.Fatal(err)
+		}
+		c.Fatalf("build used cache, expected a miss!")
+	}
+}
+
+func (s *DockerSuite) TestBuildBuildTimeArgCacheMissSameArgDiffVal(c *check.C) {
+	imgName := "bldargtest"
+	envKey := "foo"
+	envVal := "bar"
+	newEnvVal := "bar1"
+	args := []string{
+		"--build-arg", fmt.Sprintf("%s=%s", envKey, envVal),
+	}
+
+	dockerfile := fmt.Sprintf(`FROM busybox
+		ARG %s
+		RUN echo $%s`, envKey, envKey)
+
+	origImgID := ""
+	var err error
+	if origImgID, err = buildImage(imgName, dockerfile, true, args...); err != nil {
+		c.Fatal(err)
+	}
+
+	imgNameCache := "bldargtestcachemiss"
+	args = []string{
+		"--build-arg", fmt.Sprintf("%s=%s", envKey, newEnvVal),
+	}
+	if newImgID, err := buildImage(imgNameCache, dockerfile, true, args...); err != nil || newImgID == origImgID {
+		if err != nil {
+			c.Fatal(err)
+		}
+		c.Fatalf("build used cache, expected a miss!")
+	}
+}
+
+func (s *DockerSuite) TestBuildBuildTimeArgOverrideArgDefinedBeforeEnv(c *check.C) {
+	imgName := "bldargtest"
+	envKey := "foo"
+	envVal := "bar"
+	envValOveride := "barOverride"
+	args := []string{
+		"--build-arg", fmt.Sprintf("%s=%s", envKey, envVal),
+	}
+	dockerfile := fmt.Sprintf(`FROM busybox
+		ARG %s
+		ENV %s %s
+		RUN echo $%s
+		CMD echo $%s
+        `, envKey, envKey, envValOveride, envKey, envKey)
+
+	if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil || strings.Count(out, envValOveride) != 2 {
+		if err != nil {
+			c.Fatalf("build failed to complete: %q %q", out, err)
+		}
+		c.Fatalf("failed to access environment variable in output: %q expected: %q", out, envValOveride)
+	}
+
+	containerName := "bldargCont"
+	if out, _ := dockerCmd(c, "run", "--name", containerName, imgName); !strings.Contains(out, envValOveride) {
+		c.Fatalf("run produced invalid output: %q, expected %q", out, envValOveride)
+	}
+}
+
+func (s *DockerSuite) TestBuildBuildTimeArgOverrideEnvDefinedBeforeArg(c *check.C) {
+	imgName := "bldargtest"
+	envKey := "foo"
+	envVal := "bar"
+	envValOveride := "barOverride"
+	args := []string{
+		"--build-arg", fmt.Sprintf("%s=%s", envKey, envVal),
+	}
+	dockerfile := fmt.Sprintf(`FROM busybox
+		ENV %s %s
+		ARG %s
+		RUN echo $%s
+		CMD echo $%s
+        `, envKey, envValOveride, envKey, envKey, envKey)
+
+	if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil || strings.Count(out, envValOveride) != 2 {
+		if err != nil {
+			c.Fatalf("build failed to complete: %q %q", out, err)
+		}
+		c.Fatalf("failed to access environment variable in output: %q expected: %q", out, envValOveride)
+	}
+
+	containerName := "bldargCont"
+	if out, _ := dockerCmd(c, "run", "--name", containerName, imgName); !strings.Contains(out, envValOveride) {
+		c.Fatalf("run produced invalid output: %q, expected %q", out, envValOveride)
+	}
+}
+
+func (s *DockerSuite) TestBuildBuildTimeArgExpansion(c *check.C) {
+	imgName := "bldvarstest"
+
+	wdVar := "WDIR"
+	wdVal := "/tmp/"
+	addVar := "AFILE"
+	addVal := "addFile"
+	copyVar := "CFILE"
+	copyVal := "copyFile"
+	envVar := "foo"
+	envVal := "bar"
+	exposeVar := "EPORT"
+	exposeVal := "9999"
+	userVar := "USER"
+	userVal := "testUser"
+	volVar := "VOL"
+	volVal := "/testVol/"
+	args := []string{
+		"--build-arg", fmt.Sprintf("%s=%s", wdVar, wdVal),
+		"--build-arg", fmt.Sprintf("%s=%s", addVar, addVal),
+		"--build-arg", fmt.Sprintf("%s=%s", copyVar, copyVal),
+		"--build-arg", fmt.Sprintf("%s=%s", envVar, envVal),
+		"--build-arg", fmt.Sprintf("%s=%s", exposeVar, exposeVal),
+		"--build-arg", fmt.Sprintf("%s=%s", userVar, userVal),
+		"--build-arg", fmt.Sprintf("%s=%s", volVar, volVal),
+	}
+	ctx, err := fakeContext(fmt.Sprintf(`FROM busybox
+		ARG %s
+		WORKDIR ${%s}
+		ARG %s
+		ADD ${%s} testDir/
+		ARG %s
+		COPY $%s testDir/
+		ARG %s
+		ENV %s=${%s}
+		ARG %s
+		EXPOSE $%s
+		ARG %s
+		USER $%s
+		ARG %s
+		VOLUME ${%s}`,
+		wdVar, wdVar, addVar, addVar, copyVar, copyVar, envVar, envVar,
+		envVar, exposeVar, exposeVar, userVar, userVar, volVar, volVar),
+		map[string]string{
+			addVal:  "some stuff",
+			copyVal: "some stuff",
+		})
+	if err != nil {
+		c.Fatal(err)
+	}
+	defer ctx.Close()
+
+	if _, err := buildImageFromContext(imgName, ctx, true, args...); err != nil {
+		c.Fatal(err)
+	}
+
+	var resMap map[string]interface{}
+	var resArr []string
+	res := ""
+	res, err = inspectField(imgName, "Config.WorkingDir")
+	if err != nil {
+		c.Fatal(err)
+	}
+	if res != wdVal {
+		c.Fatalf("Config.WorkingDir value mismatch. Expected: %s, got: %s", wdVal, res)
+	}
+
+	err = inspectFieldAndMarshall(imgName, "Config.Env", &resArr)
+	if err != nil {
+		c.Fatal(err)
+	}
+
+	found := false
+	for _, v := range resArr {
+		if fmt.Sprintf("%s=%s", envVar, envVal) == v {
+			found = true
+			break
+		}
+	}
+	if !found {
+		c.Fatalf("Config.Env value mismatch. Expected <key=value> to exist: %s=%s, got: %v",
+			envVar, envVal, resArr)
+	}
+
+	err = inspectFieldAndMarshall(imgName, "Config.ExposedPorts", &resMap)
+	if err != nil {
+		c.Fatal(err)
+	}
+	if _, ok := resMap[fmt.Sprintf("%s/tcp", exposeVal)]; !ok {
+		c.Fatalf("Config.ExposedPorts value mismatch. Expected exposed port: %s/tcp, got: %v", exposeVal, resMap)
+	}
+
+	res, err = inspectField(imgName, "Config.User")
+	if err != nil {
+		c.Fatal(err)
+	}
+	if res != userVal {
+		c.Fatalf("Config.User value mismatch. Expected: %s, got: %s", userVal, res)
+	}
+
+	err = inspectFieldAndMarshall(imgName, "Config.Volumes", &resMap)
+	if err != nil {
+		c.Fatal(err)
+	}
+	if _, ok := resMap[volVal]; !ok {
+		c.Fatalf("Config.Volumes value mismatch. Expected volume: %s, got: %v", volVal, resMap)
+	}
+}
+
+func (s *DockerSuite) TestBuildBuildTimeArgExpansionOverride(c *check.C) {
+	imgName := "bldvarstest"
+	envKey := "foo"
+	envVal := "bar"
+	envKey1 := "foo1"
+	envValOveride := "barOverride"
+	args := []string{
+		"--build-arg", fmt.Sprintf("%s=%s", envKey, envVal),
+	}
+	dockerfile := fmt.Sprintf(`FROM busybox
+		ARG %s
+		ENV %s %s
+		ENV %s ${%s}
+		RUN echo $%s
+		CMD echo $%s`, envKey, envKey, envValOveride, envKey1, envKey, envKey1, envKey1)
+
+	if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil || strings.Count(out, envValOveride) != 2 {
+		if err != nil {
+			c.Fatalf("build failed to complete: %q %q", out, err)
+		}
+		c.Fatalf("failed to access environment variable in output: %q expected: %q", out, envValOveride)
+	}
+
+	containerName := "bldargCont"
+	if out, _ := dockerCmd(c, "run", "--name", containerName, imgName); !strings.Contains(out, envValOveride) {
+		c.Fatalf("run produced invalid output: %q, expected %q", out, envValOveride)
+	}
+}
+
+func (s *DockerSuite) TestBuildBuildTimeArgUntrustedDefinedAfterUse(c *check.C) {
+	imgName := "bldargtest"
+	envKey := "foo"
+	envVal := "bar"
+	args := []string{
+		"--build-arg", fmt.Sprintf("%s=%s", envKey, envVal),
+	}
+	dockerfile := fmt.Sprintf(`FROM busybox
+		RUN echo $%s
+		ARG %s
+		CMD echo $%s`, envKey, envKey, envKey)
+
+	if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil || strings.Contains(out, envVal) {
+		if err != nil {
+			c.Fatalf("build failed to complete: %q %q", out, err)
+		}
+		c.Fatalf("able to access environment variable in output: %q expected to be missing", out)
+	}
+
+	containerName := "bldargCont"
+	if out, _ := dockerCmd(c, "run", "--name", containerName, imgName); out != "\n" {
+		c.Fatalf("run produced invalid output: %q, expected empty string", out)
+	}
+}
+
+func (s *DockerSuite) TestBuildBuildTimeArgBuiltinArg(c *check.C) {
+	imgName := "bldargtest"
+	envKey := "HTTP_PROXY"
+	envVal := "bar"
+	args := []string{
+		"--build-arg", fmt.Sprintf("%s=%s", envKey, envVal),
+	}
+	dockerfile := fmt.Sprintf(`FROM busybox
+		RUN echo $%s
+		CMD echo $%s`, envKey, envKey)
+
+	if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil || !strings.Contains(out, envVal) {
+		if err != nil {
+			c.Fatalf("build failed to complete: %q %q", out, err)
+		}
+		c.Fatalf("failed to access environment variable in output: %q expected: %q", out, envVal)
+	}
+
+	containerName := "bldargCont"
+	if out, _ := dockerCmd(c, "run", "--name", containerName, imgName); out != "\n" {
+		c.Fatalf("run produced invalid output: %q, expected empty string", out)
+	}
+}
+
+func (s *DockerSuite) TestBuildBuildTimeArgDefaultOverride(c *check.C) {
+	imgName := "bldargtest"
+	envKey := "foo"
+	envVal := "bar"
+	envValOveride := "barOverride"
+	args := []string{
+		"--build-arg", fmt.Sprintf("%s=%s", envKey, envValOveride),
+	}
+	dockerfile := fmt.Sprintf(`FROM busybox
+		ARG %s=%s
+		ENV %s $%s
+		RUN echo $%s
+		CMD echo $%s`, envKey, envVal, envKey, envKey, envKey, envKey)
+
+	if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil || strings.Count(out, envValOveride) != 1 {
+		if err != nil {
+			c.Fatalf("build failed to complete: %q %q", out, err)
+		}
+		c.Fatalf("failed to access environment variable in output: %q expected: %q", out, envValOveride)
+	}
+
+	containerName := "bldargCont"
+	if out, _ := dockerCmd(c, "run", "--name", containerName, imgName); !strings.Contains(out, envValOveride) {
+		c.Fatalf("run produced invalid output: %q, expected %q", out, envValOveride)
+	}
+}
+
+func (s *DockerSuite) TestBuildBuildTimeArgMultiArgsSameLine(c *check.C) {
+	imgName := "bldargtest"
+	envKey := "foo"
+	envKey1 := "foo1"
+	args := []string{}
+	dockerfile := fmt.Sprintf(`FROM busybox
+		ARG %s %s`, envKey, envKey1)
+
+	errStr := "ARG requires exactly one argument definition"
+	if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err == nil {
+		c.Fatalf("build succeeded, expected to fail. Output: %v", out)
+	} else if !strings.Contains(out, errStr) {
+		c.Fatalf("Unexpected error. output: %q, expected error: %q", out, errStr)
+	}
+}
+
+func (s *DockerSuite) TestBuildBuildTimeArgUnconsumedArg(c *check.C) {
+	imgName := "bldargtest"
+	envKey := "foo"
+	envVal := "bar"
+	args := []string{
+		"--build-arg", fmt.Sprintf("%s=%s", envKey, envVal),
+	}
+	dockerfile := fmt.Sprintf(`FROM busybox
+		RUN echo $%s
+		CMD echo $%s`, envKey, envKey)
+
+	errStr := "One or more build-args"
+	if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err == nil {
+		c.Fatalf("build succeeded, expected to fail. Output: %v", out)
+	} else if !strings.Contains(out, errStr) {
+		c.Fatalf("Unexpected error. output: %q, expected error: %q", out, errStr)
+	}
+
+}
+
+func (s *DockerSuite) TestBuildBuildTimeArgQuotedValVariants(c *check.C) {
+	imgName := "bldargtest"
+	envKey := "foo"
+	envKey1 := "foo1"
+	envKey2 := "foo2"
+	envKey3 := "foo3"
+	args := []string{}
+	dockerfile := fmt.Sprintf(`FROM busybox
+		ARG %s=""
+		ARG %s=''
+		ARG %s="''"
+		ARG %s='""'
+		RUN [ "$%s" != "$%s" ]
+		RUN [ "$%s" != "$%s" ]
+		RUN [ "$%s" != "$%s" ]
+		RUN [ "$%s" != "$%s" ]
+		RUN [ "$%s" != "$%s" ]`, envKey, envKey1, envKey2, envKey3,
+		envKey, envKey2, envKey, envKey3, envKey1, envKey2, envKey1, envKey3,
+		envKey2, envKey3)
+
+	if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil {
+		c.Fatalf("build failed to complete: %q %q", out, err)
+	}
+}
+
+func (s *DockerSuite) TestBuildBuildTimeArgEmptyValVariants(c *check.C) {
+	imgName := "bldargtest"
+	envKey := "foo"
+	envKey1 := "foo1"
+	envKey2 := "foo2"
+	args := []string{}
+	dockerfile := fmt.Sprintf(`FROM busybox
+		ARG %s=
+		ARG %s=""
+		ARG %s=''
+		RUN [ "$%s" == "$%s" ]
+		RUN [ "$%s" == "$%s" ]
+		RUN [ "$%s" == "$%s" ]`, envKey, envKey1, envKey2, envKey, envKey1, envKey1, envKey2, envKey, envKey2)
+
+	if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil {
+		c.Fatalf("build failed to complete: %q %q", out, err)
+	}
+}
+
+func (s *DockerSuite) TestBuildBuildTimeArgDefintionWithNoEnvInjection(c *check.C) {
+	imgName := "bldargtest"
+	envKey := "foo"
+	args := []string{}
+	dockerfile := fmt.Sprintf(`FROM busybox
+		ARG %s
+		RUN env`, envKey)
+
+	if _, out, err := buildImageWithOut(imgName, dockerfile, true, args...); err != nil || strings.Count(out, envKey) != 1 {
+		if err != nil {
+			c.Fatalf("build failed to complete: %q %q", out, err)
+		}
+		c.Fatalf("unexpected number of occurrences of the arg in output: %q expected: 1", out)
+	}
+}

+ 12 - 9
integration-cli/docker_utils.go

@@ -1025,11 +1025,12 @@ func getContainerState(c *check.C, id string) (int, bool, error) {
 	return exitStatus, running, nil
 	return exitStatus, running, nil
 }
 }
 
 
-func buildImageCmd(name, dockerfile string, useCache bool) *exec.Cmd {
+func buildImageCmd(name, dockerfile string, useCache bool, buildFlags ...string) *exec.Cmd {
 	args := []string{"-D", "build", "-t", name}
 	args := []string{"-D", "build", "-t", name}
 	if !useCache {
 	if !useCache {
 		args = append(args, "--no-cache")
 		args = append(args, "--no-cache")
 	}
 	}
+	args = append(args, buildFlags...)
 	args = append(args, "-")
 	args = append(args, "-")
 	buildCmd := exec.Command(dockerBinary, args...)
 	buildCmd := exec.Command(dockerBinary, args...)
 	buildCmd.Stdin = strings.NewReader(dockerfile)
 	buildCmd.Stdin = strings.NewReader(dockerfile)
@@ -1037,8 +1038,8 @@ func buildImageCmd(name, dockerfile string, useCache bool) *exec.Cmd {
 
 
 }
 }
 
 
-func buildImageWithOut(name, dockerfile string, useCache bool) (string, string, error) {
-	buildCmd := buildImageCmd(name, dockerfile, useCache)
+func buildImageWithOut(name, dockerfile string, useCache bool, buildFlags ...string) (string, string, error) {
+	buildCmd := buildImageCmd(name, dockerfile, useCache, buildFlags...)
 	out, exitCode, err := runCommandWithOutput(buildCmd)
 	out, exitCode, err := runCommandWithOutput(buildCmd)
 	if err != nil || exitCode != 0 {
 	if err != nil || exitCode != 0 {
 		return "", out, fmt.Errorf("failed to build the image: %s", out)
 		return "", out, fmt.Errorf("failed to build the image: %s", out)
@@ -1050,8 +1051,8 @@ func buildImageWithOut(name, dockerfile string, useCache bool) (string, string,
 	return id, out, nil
 	return id, out, nil
 }
 }
 
 
-func buildImageWithStdoutStderr(name, dockerfile string, useCache bool) (string, string, string, error) {
-	buildCmd := buildImageCmd(name, dockerfile, useCache)
+func buildImageWithStdoutStderr(name, dockerfile string, useCache bool, buildFlags ...string) (string, string, string, error) {
+	buildCmd := buildImageCmd(name, dockerfile, useCache, buildFlags...)
 	stdout, stderr, exitCode, err := runCommandWithStdoutStderr(buildCmd)
 	stdout, stderr, exitCode, err := runCommandWithStdoutStderr(buildCmd)
 	if err != nil || exitCode != 0 {
 	if err != nil || exitCode != 0 {
 		return "", stdout, stderr, fmt.Errorf("failed to build the image: %s", stdout)
 		return "", stdout, stderr, fmt.Errorf("failed to build the image: %s", stdout)
@@ -1063,16 +1064,17 @@ func buildImageWithStdoutStderr(name, dockerfile string, useCache bool) (string,
 	return id, stdout, stderr, nil
 	return id, stdout, stderr, nil
 }
 }
 
 
-func buildImage(name, dockerfile string, useCache bool) (string, error) {
-	id, _, err := buildImageWithOut(name, dockerfile, useCache)
+func buildImage(name, dockerfile string, useCache bool, buildFlags ...string) (string, error) {
+	id, _, err := buildImageWithOut(name, dockerfile, useCache, buildFlags...)
 	return id, err
 	return id, err
 }
 }
 
 
-func buildImageFromContext(name string, ctx *FakeContext, useCache bool) (string, error) {
+func buildImageFromContext(name string, ctx *FakeContext, useCache bool, buildFlags ...string) (string, error) {
 	args := []string{"build", "-t", name}
 	args := []string{"build", "-t", name}
 	if !useCache {
 	if !useCache {
 		args = append(args, "--no-cache")
 		args = append(args, "--no-cache")
 	}
 	}
+	args = append(args, buildFlags...)
 	args = append(args, ".")
 	args = append(args, ".")
 	buildCmd := exec.Command(dockerBinary, args...)
 	buildCmd := exec.Command(dockerBinary, args...)
 	buildCmd.Dir = ctx.Dir
 	buildCmd.Dir = ctx.Dir
@@ -1083,11 +1085,12 @@ func buildImageFromContext(name string, ctx *FakeContext, useCache bool) (string
 	return getIDByName(name)
 	return getIDByName(name)
 }
 }
 
 
-func buildImageFromPath(name, path string, useCache bool) (string, error) {
+func buildImageFromPath(name, path string, useCache bool, buildFlags ...string) (string, error) {
 	args := []string{"build", "-t", name}
 	args := []string{"build", "-t", name}
 	if !useCache {
 	if !useCache {
 		args = append(args, "--no-cache")
 		args = append(args, "--no-cache")
 	}
 	}
+	args = append(args, buildFlags...)
 	args = append(args, path)
 	args = append(args, path)
 	buildCmd := exec.Command(dockerBinary, args...)
 	buildCmd := exec.Command(dockerBinary, args...)
 	out, exitCode, err := runCommandWithOutput(buildCmd)
 	out, exitCode, err := runCommandWithOutput(buildCmd)

+ 121 - 0
man/Dockerfile.5.md

@@ -317,6 +317,127 @@ A Dockerfile is similar to a Makefile.
 
 
   In the above example, the output of the **pwd** command is **a/b/c**.
   In the above example, the output of the **pwd** command is **a/b/c**.
 
 
+**ARG**
+   -- ARG <name>[=<default value>]
+
+  The `ARG` instruction defines a variable that users can pass at build-time to
+  the builder with the `docker build` command using the `--build-arg
+  <varname>=<value>` flag. If a user specifies a build argument that was not
+  defined in the Dockerfile, the build outputs an error.
+
+  ```
+  One or more build-args were not consumed, failing build.
+  ```
+
+  The Dockerfile author can define a single variable by specifying `ARG` once or many
+  variables by specifying `ARG` more than once. For example, a valid Dockerfile:
+
+  ```
+  FROM busybox
+  ARG user1
+  ARG buildno
+  ...
+  ```
+
+  A Dockerfile author may optionally specify a default value for an `ARG` instruction:
+
+  ```
+  FROM busybox
+  ARG user1=someuser
+  ARG buildno=1
+  ...
+  ```
+
+  If an `ARG` value has a default and if there is no value passed at build-time, the
+  builder uses the default.
+
+  An `ARG` variable definition comes into effect from the line on which it is
+  defined in the `Dockerfile` not from the argument's use on the command-line or
+  elsewhere.  For example, consider this Dockerfile:
+
+  ```
+  1 FROM busybox
+  2 USER ${user:-some_user}
+  3 ARG user
+  4 USER $user
+  ...
+  ```
+  A user builds this file by calling:
+
+  ```
+  $ docker build --build-arg user=what_user Dockerfile
+  ```
+
+  The `USER` at line 2 evaluates to `some_user` as the `user` variable is defined on the
+  subsequent line 3. The `USER` at line 4 evaluates to `what_user` as `user` is
+  defined and the `what_user` value was passed on the command line. Prior to its definition by an
+  `ARG` instruction, any use of a variable results in an empty string.
+
+  > **Note:** It is not recommended to use build-time variables for
+  >  passing secrets like github keys, user credentials etc.
+
+  You can use an `ARG` or an `ENV` instruction to specify variables that are
+  available to the `RUN` instruction. Environment variables defined using the
+  `ENV` instruction always override an `ARG` instruction of the same name. Consider
+  this Dockerfile with an `ENV` and `ARG` instruction.
+
+  ```
+  1 FROM ubuntu
+  2 ARG CONT_IMG_VER
+  3 ENV CONT_IMG_VER v1.0.0
+  4 RUN echo $CONT_IMG_VER
+  ```
+  Then, assume this image is built with this command:
+
+  ```
+  $ docker build --build-arg CONT_IMG_VER=v2.0.1 Dockerfile
+  ```
+
+  In this case, the `RUN` instruction uses `v1.0.0` instead of the `ARG` setting
+  passed by the user:`v2.0.1` This behavior is similar to a shell
+  script where a locally scoped variable overrides the variables passed as
+  arguments or inherited from environment, from its point of definition.
+
+  Using the example above but a different `ENV` specification you can create more
+  useful interactions between `ARG` and `ENV` instructions:
+
+  ```
+  1 FROM ubuntu
+  2 ARG CONT_IMG_VER
+  3 ENV CONT_IMG_VER ${CONT_IMG_VER:-v1.0.0}
+  4 RUN echo $CONT_IMG_VER
+  ```
+
+  Unlike an `ARG` instruction, `ENV` values are always persisted in the built
+  image. Consider a docker build without the --build-arg flag:
+
+  ```
+  $ docker build Dockerfile
+  ```
+
+  Using this Dockerfile example, `CONT_IMG_VER` is still persisted in the image but
+  its value would be `v1.0.0` as it is the default set in line 3 by the `ENV` instruction.
+
+  The variable expansion technique in this example allows you to pass arguments
+  from the command line and persist them in the final image by leveraging the
+  `ENV` instruction. Variable expansion is only supported for [a limited set of
+  Dockerfile instructions.](#environment-replacement)
+
+  Docker has a set of predefined `ARG` variables that you can use without a
+  corresponding `ARG` instruction in the Dockerfile.
+
+  * `HTTP_PROXY`
+  * `http_proxy`
+  * `HTTPS_PROXY`
+  * `https_proxy`
+  * `FTP_PROXY`
+  * `ftp_proxy`
+  * `NO_PROXY`
+  * `no_proxy`
+
+  To use these, simply pass them on the command line using the `--build-arg
+  <varname>=<value>` flag.
+
 **ONBUILD**
 **ONBUILD**
   -- `ONBUILD [INSTRUCTION]`
   -- `ONBUILD [INSTRUCTION]`
   The **ONBUILD** instruction adds a trigger instruction to an image. The
   The **ONBUILD** instruction adds a trigger instruction to an image. The

+ 12 - 0
man/docker-build.1.md

@@ -8,6 +8,7 @@ docker-build - Build a new image from the source code at PATH
 **docker build**
 **docker build**
 [**--help**]
 [**--help**]
 [**-f**|**--file**[=*PATH/Dockerfile*]]
 [**-f**|**--file**[=*PATH/Dockerfile*]]
+[**--build-arg**[=*[]*]]
 [**--force-rm**[=*false*]]
 [**--force-rm**[=*false*]]
 [**--no-cache**[=*false*]]
 [**--no-cache**[=*false*]]
 [**--pull**[=*false*]]
 [**--pull**[=*false*]]
@@ -51,6 +52,17 @@ cloned locally and then sent as the context.
    the remote context. In all cases, the file must be within the build context.
    the remote context. In all cases, the file must be within the build context.
    The default is *Dockerfile*.
    The default is *Dockerfile*.
 
 
+**--build-arg**=*variable*
+   name and value of a **buildarg**.
+
+   For example, if you want to pass a value for `http_proxy`, use
+   `--bulid-arg=http_proxy="http://some.proxy.url"`
+
+   Users pass these values at build-time. Docker uses the `buildargs` as the
+   environment context for command(s) run via the Dockerfile's `RUN` instruction
+   or for variable expansion in other Dockerfile instructions. This is not meant
+   for passing secret values. [Read more about the buildargs instruction](/reference/builder/#arg)
+
 **--force-rm**=*true*|*false*
 **--force-rm**=*true*|*false*
    Always remove intermediate containers, even after unsuccessful builds. The default is *false*.
    Always remove intermediate containers, even after unsuccessful builds. The default is *false*.
 
 

+ 4 - 4
runconfig/parse.go

@@ -324,7 +324,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe
 		MacAddress:      *flMacAddress,
 		MacAddress:      *flMacAddress,
 		Entrypoint:      entrypoint,
 		Entrypoint:      entrypoint,
 		WorkingDir:      *flWorkingDir,
 		WorkingDir:      *flWorkingDir,
-		Labels:          convertKVStringsToMap(labels),
+		Labels:          ConvertKVStringsToMap(labels),
 		StopSignal:      *flStopSignal,
 		StopSignal:      *flStopSignal,
 	}
 	}
 
 
@@ -394,8 +394,8 @@ func readKVStrings(files []string, override []string) ([]string, error) {
 	return envVariables, nil
 	return envVariables, nil
 }
 }
 
 
-// converts ["key=value"] to {"key":"value"}
-func convertKVStringsToMap(values []string) map[string]string {
+// ConvertKVStringsToMap converts ["key=value"] to {"key":"value"}
+func ConvertKVStringsToMap(values []string) map[string]string {
 	result := make(map[string]string, len(values))
 	result := make(map[string]string, len(values))
 	for _, value := range values {
 	for _, value := range values {
 		kv := strings.SplitN(value, "=", 2)
 		kv := strings.SplitN(value, "=", 2)
@@ -410,7 +410,7 @@ func convertKVStringsToMap(values []string) map[string]string {
 }
 }
 
 
 func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) {
 func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) {
-	loggingOptsMap := convertKVStringsToMap(loggingOpts)
+	loggingOptsMap := ConvertKVStringsToMap(loggingOpts)
 	if loggingDriver == "none" && len(loggingOpts) > 0 {
 	if loggingDriver == "none" && len(loggingOpts) > 0 {
 		return map[string]string{}, fmt.Errorf("Invalid logging opts for driver %s", loggingDriver)
 		return map[string]string{}, fmt.Errorf("Invalid logging opts for driver %s", loggingDriver)
 	}
 	}