mirror of
https://github.com/moby/moby.git
synced 2025-07-30 18:23:29 +03:00
Fix ARG scoping for Dockerfiles with multiple FROM
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
@ -75,7 +75,8 @@ type Builder struct {
|
|||||||
cmdSet bool
|
cmdSet bool
|
||||||
disableCommit bool
|
disableCommit bool
|
||||||
cacheBusted bool
|
cacheBusted bool
|
||||||
allowedBuildArgs map[string]bool // list of build-time args that are allowed for expansion/substitution and passing to commands in 'run'.
|
allowedBuildArgs map[string]*string // list of build-time args that are allowed for expansion/substitution and passing to commands in 'run'.
|
||||||
|
allBuildArgs map[string]struct{} // list of all build-time args found during parsing of the Dockerfile
|
||||||
directive parser.Directive
|
directive parser.Directive
|
||||||
|
|
||||||
// TODO: remove once docker.Commit can receive a tag
|
// TODO: remove once docker.Commit can receive a tag
|
||||||
@ -127,9 +128,6 @@ func NewBuilder(clientCtx context.Context, config *types.ImageBuildOptions, back
|
|||||||
if config == nil {
|
if config == nil {
|
||||||
config = new(types.ImageBuildOptions)
|
config = new(types.ImageBuildOptions)
|
||||||
}
|
}
|
||||||
if config.BuildArgs == nil {
|
|
||||||
config.BuildArgs = make(map[string]*string)
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithCancel(clientCtx)
|
ctx, cancel := context.WithCancel(clientCtx)
|
||||||
b = &Builder{
|
b = &Builder{
|
||||||
clientCtx: ctx,
|
clientCtx: ctx,
|
||||||
@ -142,7 +140,8 @@ func NewBuilder(clientCtx context.Context, config *types.ImageBuildOptions, back
|
|||||||
runConfig: new(container.Config),
|
runConfig: new(container.Config),
|
||||||
tmpContainers: map[string]struct{}{},
|
tmpContainers: map[string]struct{}{},
|
||||||
id: stringid.GenerateNonCryptoID(),
|
id: stringid.GenerateNonCryptoID(),
|
||||||
allowedBuildArgs: make(map[string]bool),
|
allowedBuildArgs: make(map[string]*string),
|
||||||
|
allBuildArgs: make(map[string]struct{}),
|
||||||
directive: parser.Directive{
|
directive: parser.Directive{
|
||||||
EscapeSeen: false,
|
EscapeSeen: false,
|
||||||
LookingForDirectives: true,
|
LookingForDirectives: true,
|
||||||
@ -320,7 +319,7 @@ func (b *Builder) build(stdout io.Writer, stderr io.Writer, out io.Writer) (stri
|
|||||||
func (b *Builder) warnOnUnusedBuildArgs() {
|
func (b *Builder) warnOnUnusedBuildArgs() {
|
||||||
leftoverArgs := []string{}
|
leftoverArgs := []string{}
|
||||||
for arg := range b.options.BuildArgs {
|
for arg := range b.options.BuildArgs {
|
||||||
if !b.isBuildArgAllowed(arg) {
|
if _, ok := b.allBuildArgs[arg]; !ok {
|
||||||
leftoverArgs = append(leftoverArgs, arg)
|
leftoverArgs = append(leftoverArgs, arg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -205,6 +205,8 @@ func from(b *Builder, args []string, attributes map[string]bool, original string
|
|||||||
|
|
||||||
var image builder.Image
|
var image builder.Image
|
||||||
|
|
||||||
|
b.noBaseImage = false
|
||||||
|
|
||||||
// Windows cannot support a container with no base image.
|
// Windows cannot support a container with no base image.
|
||||||
if name == api.NoBaseImageSpecifier {
|
if name == api.NoBaseImageSpecifier {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
@ -228,6 +230,8 @@ func from(b *Builder, args []string, attributes map[string]bool, original string
|
|||||||
}
|
}
|
||||||
b.from = image
|
b.from = image
|
||||||
|
|
||||||
|
b.allowedBuildArgs = make(map[string]*string)
|
||||||
|
|
||||||
return b.processImageFrom(image)
|
return b.processImageFrom(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -729,17 +733,13 @@ func arg(b *Builder, args []string, attributes map[string]bool, original string)
|
|||||||
hasDefault = false
|
hasDefault = false
|
||||||
}
|
}
|
||||||
// add the arg to allowed list of build-time args from this step on.
|
// add the arg to allowed list of build-time args from this step on.
|
||||||
b.allowedBuildArgs[name] = true
|
b.allBuildArgs[name] = struct{}{}
|
||||||
|
|
||||||
// If there is a default value associated with this arg then add it to the
|
var value *string
|
||||||
// b.buildArgs if one is not already passed to the builder. The args passed
|
if hasDefault {
|
||||||
// to builder override the default value of 'arg'. Note that a 'nil' for
|
value = &newValue
|
||||||
// a value means that the user specified "--build-arg FOO" and "FOO" wasn't
|
|
||||||
// defined as an env var - and in that case we DO want to use the default
|
|
||||||
// value specified in the ARG cmd.
|
|
||||||
if baValue, ok := b.options.BuildArgs[name]; (!ok || baValue == nil) && hasDefault {
|
|
||||||
b.options.BuildArgs[name] = &newValue
|
|
||||||
}
|
}
|
||||||
|
b.allowedBuildArgs[name] = value
|
||||||
|
|
||||||
return b.commit("", b.runConfig.Cmd, fmt.Sprintf("ARG %s", arg))
|
return b.commit("", b.runConfig.Cmd, fmt.Sprintf("ARG %s", arg))
|
||||||
}
|
}
|
||||||
|
@ -460,9 +460,11 @@ func TestStopSignal(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestArg(t *testing.T) {
|
func TestArg(t *testing.T) {
|
||||||
|
// This is a bad test that tests implementation details and not at
|
||||||
|
// any features of the builder. Replace or remove.
|
||||||
buildOptions := &types.ImageBuildOptions{BuildArgs: make(map[string]*string)}
|
buildOptions := &types.ImageBuildOptions{BuildArgs: make(map[string]*string)}
|
||||||
|
|
||||||
b := &Builder{flags: &BFlags{}, runConfig: &container.Config{}, disableCommit: true, allowedBuildArgs: make(map[string]bool), options: buildOptions}
|
b := &Builder{flags: &BFlags{}, runConfig: &container.Config{}, disableCommit: true, allowedBuildArgs: make(map[string]*string), allBuildArgs: make(map[string]struct{}), options: buildOptions}
|
||||||
|
|
||||||
argName := "foo"
|
argName := "foo"
|
||||||
argVal := "bar"
|
argVal := "bar"
|
||||||
@ -472,24 +474,14 @@ func TestArg(t *testing.T) {
|
|||||||
t.Fatalf("Error should be empty, got: %s", err.Error())
|
t.Fatalf("Error should be empty, got: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
allowed, ok := b.allowedBuildArgs[argName]
|
value, ok := b.getBuildArg(argName)
|
||||||
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("%s argument should be allowed as a build arg", argName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !allowed {
|
|
||||||
t.Fatalf("%s argument was present in map but disallowed as a build arg", argName)
|
|
||||||
}
|
|
||||||
|
|
||||||
val, ok := b.options.BuildArgs[argName]
|
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("%s argument should be a build arg", argName)
|
t.Fatalf("%s argument should be a build arg", argName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if *val != "bar" {
|
if value != "bar" {
|
||||||
t.Fatalf("%s argument should have default value 'bar', got %s", argName, *val)
|
t.Fatalf("%s argument should have default value 'bar', got %s", argName, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,17 +192,9 @@ func (b *Builder) buildArgsWithoutConfigEnv() []string {
|
|||||||
envs := []string{}
|
envs := []string{}
|
||||||
configEnv := runconfigopts.ConvertKVStringsToMap(b.runConfig.Env)
|
configEnv := runconfigopts.ConvertKVStringsToMap(b.runConfig.Env)
|
||||||
|
|
||||||
for key, val := range b.options.BuildArgs {
|
for key, val := range b.getBuildArgs() {
|
||||||
if !b.isBuildArgAllowed(key) {
|
if _, ok := configEnv[key]; !ok {
|
||||||
// skip build-args that are not in allowed list, meaning they have
|
envs = append(envs, fmt.Sprintf("%s=%s", key, val))
|
||||||
// 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 && val != nil {
|
|
||||||
envs = append(envs, fmt.Sprintf("%s=%s", key, *val))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return envs
|
return envs
|
||||||
|
@ -668,14 +668,35 @@ func (b *Builder) parseDockerfile() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// determine if build arg is part of built-in args or user
|
func (b *Builder) getBuildArg(arg string) (string, bool) {
|
||||||
// defined args in Dockerfile at any point in time.
|
defaultValue, defined := b.allowedBuildArgs[arg]
|
||||||
func (b *Builder) isBuildArgAllowed(arg string) bool {
|
_, builtin := BuiltinAllowedBuildArgs[arg]
|
||||||
if _, ok := BuiltinAllowedBuildArgs[arg]; ok {
|
if defined || builtin {
|
||||||
return true
|
if v, ok := b.options.BuildArgs[arg]; ok && v != nil {
|
||||||
|
return *v, ok
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if _, ok := b.allowedBuildArgs[arg]; ok {
|
if defaultValue == nil {
|
||||||
return true
|
return "", false
|
||||||
}
|
}
|
||||||
return false
|
return *defaultValue, defined
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) getBuildArgs() map[string]string {
|
||||||
|
m := make(map[string]string)
|
||||||
|
for arg := range b.options.BuildArgs {
|
||||||
|
v, ok := b.getBuildArg(arg)
|
||||||
|
if ok {
|
||||||
|
m[arg] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for arg := range b.allowedBuildArgs {
|
||||||
|
if _, ok := m[arg]; !ok {
|
||||||
|
v, ok := b.getBuildArg(arg)
|
||||||
|
if ok {
|
||||||
|
m[arg] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
}
|
}
|
||||||
|
@ -4740,6 +4740,61 @@ func (s *DockerSuite) TestBuildBuildTimeArgDefintionWithNoEnvInjection(c *check.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DockerSuite) TestBuildBuildTimeArgMultipleFrom(c *check.C) {
|
||||||
|
imgName := "multifrombldargtest"
|
||||||
|
dockerfile := `FROM busybox
|
||||||
|
ARG foo=abc
|
||||||
|
LABEL multifromtest=1
|
||||||
|
RUN env > /out
|
||||||
|
FROM busybox
|
||||||
|
ARG bar=def
|
||||||
|
RUN env > /out`
|
||||||
|
|
||||||
|
result := buildImage(imgName, withDockerfile(dockerfile))
|
||||||
|
result.Assert(c, icmd.Success)
|
||||||
|
|
||||||
|
result = icmd.RunCmd(icmd.Cmd{
|
||||||
|
Command: []string{dockerBinary, "images", "-q", "-f", "label=multifromtest=1"},
|
||||||
|
})
|
||||||
|
result.Assert(c, icmd.Success)
|
||||||
|
parentID := strings.TrimSpace(result.Stdout())
|
||||||
|
|
||||||
|
result = icmd.RunCmd(icmd.Cmd{
|
||||||
|
Command: []string{dockerBinary, "run", "--rm", parentID, "cat", "/out"},
|
||||||
|
})
|
||||||
|
result.Assert(c, icmd.Success)
|
||||||
|
c.Assert(result.Stdout(), checker.Contains, "foo=abc")
|
||||||
|
|
||||||
|
result = icmd.RunCmd(icmd.Cmd{
|
||||||
|
Command: []string{dockerBinary, "run", "--rm", imgName, "cat", "/out"},
|
||||||
|
})
|
||||||
|
result.Assert(c, icmd.Success)
|
||||||
|
c.Assert(result.Stdout(), checker.Not(checker.Contains), "foo")
|
||||||
|
c.Assert(result.Stdout(), checker.Contains, "bar=def")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DockerSuite) TestBuildBuildTimeUnusedArgMultipleFrom(c *check.C) {
|
||||||
|
imgName := "multifromunusedarg"
|
||||||
|
dockerfile := `FROM busybox
|
||||||
|
ARG foo
|
||||||
|
FROM busybox
|
||||||
|
ARG bar
|
||||||
|
RUN env > /out`
|
||||||
|
|
||||||
|
result := buildImage(imgName, withDockerfile(dockerfile), withBuildFlags(
|
||||||
|
"--build-arg", fmt.Sprintf("baz=abc")))
|
||||||
|
result.Assert(c, icmd.Success)
|
||||||
|
c.Assert(result.Combined(), checker.Contains, "[Warning]")
|
||||||
|
c.Assert(result.Combined(), checker.Contains, "[baz] were not consumed")
|
||||||
|
|
||||||
|
result = icmd.RunCmd(icmd.Cmd{
|
||||||
|
Command: []string{dockerBinary, "run", "--rm", imgName, "cat", "/out"},
|
||||||
|
})
|
||||||
|
result.Assert(c, icmd.Success)
|
||||||
|
c.Assert(result.Stdout(), checker.Not(checker.Contains), "bar")
|
||||||
|
c.Assert(result.Stdout(), checker.Not(checker.Contains), "baz")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *DockerSuite) TestBuildNoNamedVolume(c *check.C) {
|
func (s *DockerSuite) TestBuildNoNamedVolume(c *check.C) {
|
||||||
volName := "testname:/foo"
|
volName := "testname:/foo"
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user