diff --git a/bake/bake.go b/bake/bake.go index 423d074c..db03e05e 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -559,24 +559,24 @@ type Target struct { Attest []string `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"` Inherits []string `json:"inherits,omitempty" hcl:"inherits,optional" cty:"inherits"` - Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"` - Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"` - Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"` - DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional" cty:"dockerfile-inline"` - Args map[string]string `json:"args,omitempty" hcl:"args,optional" cty:"args"` - Labels map[string]string `json:"labels,omitempty" hcl:"labels,optional" cty:"labels"` - Tags []string `json:"tags,omitempty" hcl:"tags,optional" cty:"tags"` - CacheFrom []string `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"` - CacheTo []string `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"` - Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"` - Secrets []string `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"` - SSH []string `json:"ssh,omitempty" hcl:"ssh,optional" cty:"ssh"` - Platforms []string `json:"platforms,omitempty" hcl:"platforms,optional" cty:"platforms"` - Outputs []string `json:"output,omitempty" hcl:"output,optional" cty:"output"` - Pull *bool `json:"pull,omitempty" hcl:"pull,optional" cty:"pull"` - NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional" cty:"no-cache"` - NetworkMode *string `json:"-" hcl:"-" cty:"-"` - NoCacheFilter []string `json:"no-cache-filter,omitempty" hcl:"no-cache-filter,optional" cty:"no-cache-filter"` + Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"` + Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"` + Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"` + DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional" cty:"dockerfile-inline"` + Args map[string]*string `json:"args,omitempty" hcl:"args,optional" cty:"args"` + Labels map[string]string `json:"labels,omitempty" hcl:"labels,optional" cty:"labels"` + Tags []string `json:"tags,omitempty" hcl:"tags,optional" cty:"tags"` + CacheFrom []string `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"` + CacheTo []string `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"` + Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"` + Secrets []string `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"` + SSH []string `json:"ssh,omitempty" hcl:"ssh,optional" cty:"ssh"` + Platforms []string `json:"platforms,omitempty" hcl:"platforms,optional" cty:"platforms"` + Outputs []string `json:"output,omitempty" hcl:"output,optional" cty:"output"` + Pull *bool `json:"pull,omitempty" hcl:"pull,optional" cty:"pull"` + NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional" cty:"no-cache"` + NetworkMode *string `json:"-" hcl:"-" cty:"-"` + NoCacheFilter []string `json:"no-cache-filter,omitempty" hcl:"no-cache-filter,optional" cty:"no-cache-filter"` // IMPORTANT: if you add more fields here, do not forget to update newOverrides and docs/manuals/bake/file-definition.md. // linked is a private field to mark a target used as a linked one @@ -615,8 +615,11 @@ func (t *Target) Merge(t2 *Target) { t.DockerfileInline = t2.DockerfileInline } for k, v := range t2.Args { + if v == nil { + continue + } if t.Args == nil { - t.Args = map[string]string{} + t.Args = map[string]*string{} } t.Args[k] = v } @@ -688,9 +691,9 @@ func (t *Target) AddOverrides(overrides map[string]Override) error { return errors.Errorf("args require name") } if t.Args == nil { - t.Args = map[string]string{} + t.Args = map[string]*string{} } - t.Args[keys[1]] = value + t.Args[keys[1]] = &value case "contexts": if len(keys) != 2 { return errors.Errorf("contexts require name") @@ -882,6 +885,14 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { dockerfilePath = path.Join(contextPath, dockerfilePath) } + args := map[string]string{} + for k, v := range t.Args { + if v == nil { + continue + } + args[k] = *v + } + noCache := false if t.NoCache != nil { noCache = *t.NoCache @@ -922,7 +933,7 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { bo := &build.Options{ Inputs: bi, Tags: t.Tags, - BuildArgs: t.Args, + BuildArgs: args, Labels: t.Labels, NoCache: noCache, NoCacheFilter: t.NoCacheFilter, diff --git a/bake/bake_test.go b/bake/bake_test.go index be9052a6..50950814 100644 --- a/bake/bake_test.go +++ b/bake/bake_test.go @@ -2,7 +2,6 @@ package bake import ( "context" - "os" "sort" "strings" "testing" @@ -42,7 +41,7 @@ target "webapp" { require.Equal(t, "Dockerfile.webapp", *m["webapp"].Dockerfile) require.Equal(t, ".", *m["webapp"].Context) - require.Equal(t, "webDEP", m["webapp"].Args["VAR_INHERITED"]) + require.Equal(t, ptrstr("webDEP"), m["webapp"].Args["VAR_INHERITED"]) require.Equal(t, true, *m["webapp"].NoCache) require.Nil(t, m["webapp"].Pull) @@ -58,8 +57,7 @@ target "webapp" { t.Run("ArgsOverrides", func(t *testing.T) { t.Run("leaf", func(t *testing.T) { - os.Setenv("VAR_FROMENV"+t.Name(), "fromEnv") - defer os.Unsetenv("VAR_FROM_ENV" + t.Name()) + t.Setenv("VAR_FROMENV"+t.Name(), "fromEnv") m, g, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{ "webapp.args.VAR_UNSET", @@ -80,12 +78,12 @@ target "webapp" { _, isSet = m["webapp"].Args["VAR_EMPTY"] require.True(t, isSet, m["webapp"].Args["VAR_EMPTY"]) - require.Equal(t, m["webapp"].Args["VAR_SET"], "bananas") + require.Equal(t, ptrstr("bananas"), m["webapp"].Args["VAR_SET"]) - require.Equal(t, m["webapp"].Args["VAR_FROMENV"+t.Name()], "fromEnv") + require.Equal(t, ptrstr("fromEnv"), m["webapp"].Args["VAR_FROMENV"+t.Name()]) - require.Equal(t, m["webapp"].Args["VAR_BOTH"], "webapp") - require.Equal(t, m["webapp"].Args["VAR_INHERITED"], "override") + require.Equal(t, ptrstr("webapp"), m["webapp"].Args["VAR_BOTH"]) + require.Equal(t, ptrstr("override"), m["webapp"].Args["VAR_INHERITED"]) require.Equal(t, 1, len(g)) require.Equal(t, []string{"webapp"}, g["default"].Targets) @@ -99,8 +97,8 @@ target "webapp" { }, nil) require.NoError(t, err) - require.Equal(t, m["webapp"].Args["VAR_INHERITED"], "override") - require.Equal(t, m["webapp"].Args["VAR_BOTH"], "webapp") + require.Equal(t, ptrstr("override"), m["webapp"].Args["VAR_INHERITED"]) + require.Equal(t, ptrstr("webapp"), m["webapp"].Args["VAR_BOTH"]) require.Equal(t, 1, len(g)) require.Equal(t, []string{"webapp"}, g["default"].Targets) }) @@ -139,9 +137,9 @@ target "webapp" { require.NoError(t, err) require.Equal(t, 2, len(m)) require.Equal(t, "foo", *m["webapp"].Dockerfile) - require.Equal(t, "webDEP", m["webapp"].Args["VAR_INHERITED"]) + require.Equal(t, ptrstr("webDEP"), m["webapp"].Args["VAR_INHERITED"]) require.Equal(t, "foo", *m["webDEP"].Dockerfile) - require.Equal(t, "webDEP", m["webDEP"].Args["VAR_INHERITED"]) + require.Equal(t, ptrstr("webDEP"), m["webDEP"].Args["VAR_INHERITED"]) require.Equal(t, 1, len(g)) sort.Strings(g["default"].Targets) require.Equal(t, []string{"webDEP", "webapp"}, g["default"].Targets) @@ -173,7 +171,7 @@ target "webapp" { require.NoError(t, err) require.Equal(t, 1, len(m)) require.Equal(t, "foo", *m["webapp"].Dockerfile) - require.Equal(t, "webDEP", m["webapp"].Args["VAR_INHERITED"]) + require.Equal(t, ptrstr("webDEP"), m["webapp"].Args["VAR_INHERITED"]) require.Equal(t, 1, len(g)) require.Equal(t, []string{"webapp"}, g["default"].Targets) }, @@ -300,8 +298,8 @@ services: require.True(t, ok) require.Equal(t, "Dockerfile.webapp", *m["webapp"].Dockerfile) require.Equal(t, ".", *m["webapp"].Context) - require.Equal(t, "1", m["webapp"].Args["buildno"]) - require.Equal(t, "12", m["webapp"].Args["buildno2"]) + require.Equal(t, ptrstr("1"), m["webapp"].Args["buildno"]) + require.Equal(t, ptrstr("12"), m["webapp"].Args["buildno2"]) require.Equal(t, 1, len(g)) sort.Strings(g["default"].Targets) @@ -344,7 +342,7 @@ services: _, ok := m["web_app"] require.True(t, ok) require.Equal(t, "Dockerfile.webapp", *m["web_app"].Dockerfile) - require.Equal(t, "1", m["web_app"].Args["buildno"]) + require.Equal(t, ptrstr("1"), m["web_app"].Args["buildno"]) m, _, err = ReadTargets(ctx, []File{fp2}, []string{"web_app"}, nil, nil) require.NoError(t, err) @@ -352,7 +350,7 @@ services: _, ok = m["web_app"] require.True(t, ok) require.Equal(t, "Dockerfile", *m["web_app"].Dockerfile) - require.Equal(t, "12", m["web_app"].Args["buildno2"]) + require.Equal(t, ptrstr("12"), m["web_app"].Args["buildno2"]) m, g, err := ReadTargets(ctx, []File{fp, fp2}, []string{"default"}, nil, nil) require.NoError(t, err) @@ -361,8 +359,8 @@ services: require.True(t, ok) require.Equal(t, "Dockerfile.webapp", *m["web_app"].Dockerfile) require.Equal(t, ".", *m["web_app"].Context) - require.Equal(t, "1", m["web_app"].Args["buildno"]) - require.Equal(t, "12", m["web_app"].Args["buildno2"]) + require.Equal(t, ptrstr("1"), m["web_app"].Args["buildno"]) + require.Equal(t, ptrstr("12"), m["web_app"].Args["buildno2"]) require.Equal(t, 1, len(g)) sort.Strings(g["default"].Targets) @@ -999,22 +997,22 @@ target "d" { cases := []struct { name string overrides []string - want map[string]string + want map[string]*string }{ { name: "nested simple", overrides: nil, - want: map[string]string{"bar": "234", "baz": "890", "foo": "123"}, + want: map[string]*string{"bar": ptrstr("234"), "baz": ptrstr("890"), "foo": ptrstr("123")}, }, { name: "nested with overrides first", overrides: []string{"a.args.foo=321", "b.args.bar=432"}, - want: map[string]string{"bar": "234", "baz": "890", "foo": "321"}, + want: map[string]*string{"bar": ptrstr("234"), "baz": ptrstr("890"), "foo": ptrstr("321")}, }, { name: "nested with overrides last", overrides: []string{"a.args.foo=321", "c.args.bar=432"}, - want: map[string]string{"bar": "432", "baz": "890", "foo": "321"}, + want: map[string]*string{"bar": ptrstr("432"), "baz": ptrstr("890"), "foo": ptrstr("321")}, }, } for _, tt := range cases { @@ -1067,26 +1065,26 @@ group "default" { cases := []struct { name string overrides []string - wantch1 map[string]string - wantch2 map[string]string + wantch1 map[string]*string + wantch2 map[string]*string }{ { name: "nested simple", overrides: nil, - wantch1: map[string]string{"BAR": "fuu", "FOO": "bar"}, - wantch2: map[string]string{"BAR": "fuu", "FOO": "bar", "FOO2": "bar2"}, + wantch1: map[string]*string{"BAR": ptrstr("fuu"), "FOO": ptrstr("bar")}, + wantch2: map[string]*string{"BAR": ptrstr("fuu"), "FOO": ptrstr("bar"), "FOO2": ptrstr("bar2")}, }, { name: "nested with overrides first", overrides: []string{"grandparent.args.BAR=fii", "child1.args.FOO=baaar"}, - wantch1: map[string]string{"BAR": "fii", "FOO": "baaar"}, - wantch2: map[string]string{"BAR": "fii", "FOO": "bar", "FOO2": "bar2"}, + wantch1: map[string]*string{"BAR": ptrstr("fii"), "FOO": ptrstr("baaar")}, + wantch2: map[string]*string{"BAR": ptrstr("fii"), "FOO": ptrstr("bar"), "FOO2": ptrstr("bar2")}, }, { name: "nested with overrides last", overrides: []string{"grandparent.args.BAR=fii", "child2.args.FOO=baaar"}, - wantch1: map[string]string{"BAR": "fii", "FOO": "bar"}, - wantch2: map[string]string{"BAR": "fii", "FOO": "baaar", "FOO2": "bar2"}, + wantch1: map[string]*string{"BAR": ptrstr("fii"), "FOO": ptrstr("bar")}, + wantch2: map[string]*string{"BAR": ptrstr("fii"), "FOO": ptrstr("baaar"), "FOO2": ptrstr("bar2")}, }, } for _, tt := range cases { @@ -1285,8 +1283,70 @@ services: require.Equal(t, 1, len(c.Targets)) require.Equal(t, "app", c.Targets[0].Name) - require.Equal(t, "foo", c.Targets[0].Args["v1"]) - require.Equal(t, "bar", c.Targets[0].Args["v2"]) + require.Equal(t, ptrstr("foo"), c.Targets[0].Args["v1"]) + require.Equal(t, ptrstr("bar"), c.Targets[0].Args["v2"]) require.Equal(t, "dir", *c.Targets[0].Context) require.Equal(t, "Dockerfile-alternate", *c.Targets[0].Dockerfile) } + +func TestHCLNullVars(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + Data: []byte( + `variable "FOO" { + default = null + } + target "default" { + args = { + foo = FOO + bar = "baz" + } + }`), + } + + ctx := context.TODO() + m, _, err := ReadTargets(ctx, []File{fp}, []string{"default"}, nil, nil) + require.NoError(t, err) + + require.Equal(t, 1, len(m)) + _, ok := m["default"] + require.True(t, ok) + + _, err = TargetsToBuildOpt(m, &Input{}) + require.NoError(t, err) + require.Equal(t, map[string]*string{"bar": ptrstr("baz")}, m["default"].Args) +} + +func TestJSONNullVars(t *testing.T) { + fp := File{ + Name: "docker-bake.json", + Data: []byte( + `{ + "variable": { + "FOO": { + "default": null + } + }, + "target": { + "default": { + "args": { + "foo": "${FOO}", + "bar": "baz" + } + } + } + }`), + } + + ctx := context.TODO() + m, _, err := ReadTargets(ctx, []File{fp}, []string{"default"}, nil, nil) + require.NoError(t, err) + + require.Equal(t, 1, len(m)) + _, ok := m["default"] + require.True(t, ok) + + _, err = TargetsToBuildOpt(m, &Input{}) + require.NoError(t, err) + require.Equal(t, map[string]*string{"bar": ptrstr("baz")}, m["default"].Args) +} diff --git a/bake/compose.go b/bake/compose.go index 341f4e97..ea8eab65 100644 --- a/bake/compose.go +++ b/bake/compose.go @@ -193,16 +193,16 @@ func loadDotEnv(curenv map[string]string, workingDir string) (map[string]string, return curenv, nil } -func flatten(in compose.MappingWithEquals) compose.Mapping { +func flatten(in compose.MappingWithEquals) map[string]*string { if len(in) == 0 { return nil } - out := compose.Mapping{} + out := map[string]*string{} for k, v := range in { if v == nil { continue } - out[k] = *v + out[k] = v } return out } diff --git a/bake/compose_test.go b/bake/compose_test.go index d62f14a7..537065a5 100644 --- a/bake/compose_test.go +++ b/bake/compose_test.go @@ -60,7 +60,7 @@ secrets: require.Equal(t, "./dir", *c.Targets[1].Context) require.Equal(t, "Dockerfile-alternate", *c.Targets[1].Dockerfile) require.Equal(t, 1, len(c.Targets[1].Args)) - require.Equal(t, "123", c.Targets[1].Args["buildno"]) + require.Equal(t, ptrstr("123"), c.Targets[1].Args["buildno"]) require.Equal(t, []string{"type=local,src=path/to/cache"}, c.Targets[1].CacheFrom) require.Equal(t, []string{"type=local,dest=path/to/cache"}, c.Targets[1].CacheTo) require.Equal(t, "none", *c.Targets[1].NetworkMode) @@ -149,18 +149,15 @@ services: BRB: FOO `) - os.Setenv("FOO", "bar") - defer os.Unsetenv("FOO") - os.Setenv("BAR", "foo") - defer os.Unsetenv("BAR") - os.Setenv("ZZZ_BAR", "zzz_foo") - defer os.Unsetenv("ZZZ_BAR") + t.Setenv("FOO", "bar") + t.Setenv("BAR", "foo") + t.Setenv("ZZZ_BAR", "zzz_foo") c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, sliceToMap(os.Environ())) require.NoError(t, err) - require.Equal(t, "bar", c.Targets[0].Args["FOO"]) - require.Equal(t, "zzz_foo", c.Targets[0].Args["BAR"]) - require.Equal(t, "FOO", c.Targets[0].Args["BRB"]) + require.Equal(t, ptrstr("bar"), c.Targets[0].Args["FOO"]) + require.Equal(t, ptrstr("zzz_foo"), c.Targets[0].Args["BAR"]) + require.Equal(t, ptrstr("FOO"), c.Targets[0].Args["BRB"]) } func TestInconsistentComposeFile(t *testing.T) { @@ -308,7 +305,7 @@ services: sort.Slice(c.Targets, func(i, j int) bool { return c.Targets[i].Name < c.Targets[j].Name }) - require.Equal(t, map[string]string{"CT_ECR": "foo", "CT_TAG": "bar"}, c.Targets[0].Args) + require.Equal(t, map[string]*string{"CT_ECR": ptrstr("foo"), "CT_TAG": ptrstr("bar")}, c.Targets[0].Args) require.Equal(t, []string{"ct-addon:baz", "ct-addon:foo", "ct-addon:alp"}, c.Targets[0].Tags) require.Equal(t, []string{"linux/amd64", "linux/arm64"}, c.Targets[0].Platforms) require.Equal(t, []string{"user/app:cache", "type=local,src=path/to/cache"}, c.Targets[0].CacheFrom) @@ -381,7 +378,7 @@ services: c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) - require.Equal(t, map[string]string{"CT_ECR": "foo", "FOO": "bsdf -csdf", "NODE_ENV": "test"}, c.Targets[0].Args) + require.Equal(t, map[string]*string{"CT_ECR": ptrstr("foo"), "FOO": ptrstr("bsdf -csdf"), "NODE_ENV": ptrstr("test")}, c.Targets[0].Args) } func TestDotEnv(t *testing.T) { @@ -405,7 +402,7 @@ services: Data: dt, }}) require.NoError(t, err) - require.Equal(t, map[string]string{"FOO": "bar"}, c.Targets[0].Args) + require.Equal(t, map[string]*string{"FOO": ptrstr("bar")}, c.Targets[0].Args) } func TestPorts(t *testing.T) { @@ -629,6 +626,22 @@ target "default" { } } +func TestComposeNullArgs(t *testing.T) { + var dt = []byte(` +services: + scratch: + build: + context: . + args: + FOO: null + bar: "baz" +`) + + c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) + require.NoError(t, err) + require.Equal(t, map[string]*string{"bar": ptrstr("baz")}, c.Targets[0].Args) +} + // chdir changes the current working directory to the named directory, // and then restore the original working directory at the end of the test. func chdir(t *testing.T, dir string) { diff --git a/bake/hcl_test.go b/bake/hcl_test.go index 74c212d8..6d6fda0e 100644 --- a/bake/hcl_test.go +++ b/bake/hcl_test.go @@ -1,7 +1,7 @@ package bake import ( - "os" + "reflect" "testing" "github.com/stretchr/testify/require" @@ -54,7 +54,7 @@ func TestHCLBasic(t *testing.T) { require.Equal(t, c.Targets[1].Name, "webapp") require.Equal(t, 1, len(c.Targets[1].Args)) - require.Equal(t, "123", c.Targets[1].Args["buildno"]) + require.Equal(t, ptrstr("123"), c.Targets[1].Args["buildno"]) require.Equal(t, c.Targets[2].Name, "cross") require.Equal(t, 2, len(c.Targets[2].Platforms)) @@ -62,7 +62,7 @@ func TestHCLBasic(t *testing.T) { require.Equal(t, c.Targets[3].Name, "webapp-plus") require.Equal(t, 1, len(c.Targets[3].Args)) - require.Equal(t, map[string]string{"IAMCROSS": "true"}, c.Targets[3].Args) + require.Equal(t, map[string]*string{"IAMCROSS": ptrstr("true")}, c.Targets[3].Args) } func TestHCLBasicInJSON(t *testing.T) { @@ -114,7 +114,7 @@ func TestHCLBasicInJSON(t *testing.T) { require.Equal(t, c.Targets[1].Name, "webapp") require.Equal(t, 1, len(c.Targets[1].Args)) - require.Equal(t, "123", c.Targets[1].Args["buildno"]) + require.Equal(t, ptrstr("123"), c.Targets[1].Args["buildno"]) require.Equal(t, c.Targets[2].Name, "cross") require.Equal(t, 2, len(c.Targets[2].Platforms)) @@ -122,7 +122,7 @@ func TestHCLBasicInJSON(t *testing.T) { require.Equal(t, c.Targets[3].Name, "webapp-plus") require.Equal(t, 1, len(c.Targets[3].Args)) - require.Equal(t, map[string]string{"IAMCROSS": "true"}, c.Targets[3].Args) + require.Equal(t, map[string]*string{"IAMCROSS": ptrstr("true")}, c.Targets[3].Args) } func TestHCLWithFunctions(t *testing.T) { @@ -147,7 +147,7 @@ func TestHCLWithFunctions(t *testing.T) { require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "webapp") - require.Equal(t, "124", c.Targets[0].Args["buildno"]) + require.Equal(t, ptrstr("124"), c.Targets[0].Args["buildno"]) } func TestHCLWithUserDefinedFunctions(t *testing.T) { @@ -177,7 +177,7 @@ func TestHCLWithUserDefinedFunctions(t *testing.T) { require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "webapp") - require.Equal(t, "124", c.Targets[0].Args["buildno"]) + require.Equal(t, ptrstr("124"), c.Targets[0].Args["buildno"]) } func TestHCLWithVariables(t *testing.T) { @@ -206,9 +206,9 @@ func TestHCLWithVariables(t *testing.T) { require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "webapp") - require.Equal(t, "123", c.Targets[0].Args["buildno"]) + require.Equal(t, ptrstr("123"), c.Targets[0].Args["buildno"]) - os.Setenv("BUILD_NUMBER", "456") + t.Setenv("BUILD_NUMBER", "456") c, err = ParseFile(dt, "docker-bake.hcl") require.NoError(t, err) @@ -219,7 +219,7 @@ func TestHCLWithVariables(t *testing.T) { require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "webapp") - require.Equal(t, "456", c.Targets[0].Args["buildno"]) + require.Equal(t, ptrstr("456"), c.Targets[0].Args["buildno"]) } func TestHCLWithVariablesInFunctions(t *testing.T) { @@ -244,7 +244,7 @@ func TestHCLWithVariablesInFunctions(t *testing.T) { require.Equal(t, c.Targets[0].Name, "webapp") require.Equal(t, []string{"user/repo:v1"}, c.Targets[0].Tags) - os.Setenv("REPO", "docker/buildx") + t.Setenv("REPO", "docker/buildx") c, err = ParseFile(dt, "docker-bake.hcl") require.NoError(t, err) @@ -280,10 +280,10 @@ func TestHCLMultiFileSharedVariables(t *testing.T) { require.NoError(t, err) require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "app") - require.Equal(t, "pre-abc", c.Targets[0].Args["v1"]) - require.Equal(t, "abc-post", c.Targets[0].Args["v2"]) + require.Equal(t, ptrstr("pre-abc"), c.Targets[0].Args["v1"]) + require.Equal(t, ptrstr("abc-post"), c.Targets[0].Args["v2"]) - os.Setenv("FOO", "def") + t.Setenv("FOO", "def") c, err = ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, @@ -293,12 +293,11 @@ func TestHCLMultiFileSharedVariables(t *testing.T) { require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "app") - require.Equal(t, "pre-def", c.Targets[0].Args["v1"]) - require.Equal(t, "def-post", c.Targets[0].Args["v2"]) + require.Equal(t, ptrstr("pre-def"), c.Targets[0].Args["v1"]) + require.Equal(t, ptrstr("def-post"), c.Targets[0].Args["v2"]) } func TestHCLVarsWithVars(t *testing.T) { - os.Unsetenv("FOO") dt := []byte(` variable "FOO" { default = upper("${BASE}def") @@ -330,10 +329,10 @@ func TestHCLVarsWithVars(t *testing.T) { require.NoError(t, err) require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "app") - require.Equal(t, "pre--ABCDEF-", c.Targets[0].Args["v1"]) - require.Equal(t, "ABCDEF-post", c.Targets[0].Args["v2"]) + require.Equal(t, ptrstr("pre--ABCDEF-"), c.Targets[0].Args["v1"]) + require.Equal(t, ptrstr("ABCDEF-post"), c.Targets[0].Args["v2"]) - os.Setenv("BASE", "new") + t.Setenv("BASE", "new") c, err = ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, @@ -343,12 +342,11 @@ func TestHCLVarsWithVars(t *testing.T) { require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "app") - require.Equal(t, "pre--NEWDEF-", c.Targets[0].Args["v1"]) - require.Equal(t, "NEWDEF-post", c.Targets[0].Args["v2"]) + require.Equal(t, ptrstr("pre--NEWDEF-"), c.Targets[0].Args["v1"]) + require.Equal(t, ptrstr("NEWDEF-post"), c.Targets[0].Args["v2"]) } func TestHCLTypedVariables(t *testing.T) { - os.Unsetenv("FOO") dt := []byte(` variable "FOO" { default = 3 @@ -369,33 +367,80 @@ func TestHCLTypedVariables(t *testing.T) { require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "app") - require.Equal(t, "lower", c.Targets[0].Args["v1"]) - require.Equal(t, "yes", c.Targets[0].Args["v2"]) + require.Equal(t, ptrstr("lower"), c.Targets[0].Args["v1"]) + require.Equal(t, ptrstr("yes"), c.Targets[0].Args["v2"]) - os.Setenv("FOO", "5.1") - os.Setenv("IS_FOO", "0") + t.Setenv("FOO", "5.1") + t.Setenv("IS_FOO", "0") c, err = ParseFile(dt, "docker-bake.hcl") require.NoError(t, err) require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "app") - require.Equal(t, "higher", c.Targets[0].Args["v1"]) - require.Equal(t, "no", c.Targets[0].Args["v2"]) + require.Equal(t, ptrstr("higher"), c.Targets[0].Args["v1"]) + require.Equal(t, ptrstr("no"), c.Targets[0].Args["v2"]) - os.Setenv("FOO", "NaN") + t.Setenv("FOO", "NaN") _, err = ParseFile(dt, "docker-bake.hcl") require.Error(t, err) require.Contains(t, err.Error(), "failed to parse FOO as number") - os.Setenv("FOO", "0") - os.Setenv("IS_FOO", "maybe") + t.Setenv("FOO", "0") + t.Setenv("IS_FOO", "maybe") _, err = ParseFile(dt, "docker-bake.hcl") require.Error(t, err) require.Contains(t, err.Error(), "failed to parse IS_FOO as bool") } +func TestHCLNullVariables(t *testing.T) { + dt := []byte(` + variable "FOO" { + default = null + } + target "default" { + args = { + foo = FOO + } + }`) + + c, err := ParseFile(dt, "docker-bake.hcl") + require.NoError(t, err) + require.Equal(t, ptrstr(nil), c.Targets[0].Args["foo"]) + + t.Setenv("FOO", "bar") + c, err = ParseFile(dt, "docker-bake.hcl") + require.NoError(t, err) + require.Equal(t, ptrstr("bar"), c.Targets[0].Args["foo"]) +} + +func TestJSONNullVariables(t *testing.T) { + dt := []byte(`{ + "variable": { + "FOO": { + "default": null + } + }, + "target": { + "default": { + "args": { + "foo": "${FOO}" + } + } + } + }`) + + c, err := ParseFile(dt, "docker-bake.json") + require.NoError(t, err) + require.Equal(t, ptrstr(nil), c.Targets[0].Args["foo"]) + + t.Setenv("FOO", "bar") + c, err = ParseFile(dt, "docker-bake.json") + require.NoError(t, err) + require.Equal(t, ptrstr("bar"), c.Targets[0].Args["foo"]) +} + func TestHCLVariableCycle(t *testing.T) { dt := []byte(` variable "FOO" { @@ -431,16 +476,16 @@ func TestHCLAttrs(t *testing.T) { require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "app") - require.Equal(t, "attr-abcdef", c.Targets[0].Args["v1"]) + require.Equal(t, ptrstr("attr-abcdef"), c.Targets[0].Args["v1"]) // env does not apply if no variable - os.Setenv("FOO", "bar") + t.Setenv("FOO", "bar") c, err = ParseFile(dt, "docker-bake.hcl") require.NoError(t, err) require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "app") - require.Equal(t, "attr-abcdef", c.Targets[0].Args["v1"]) + require.Equal(t, ptrstr("attr-abcdef"), c.Targets[0].Args["v1"]) // attr-multifile } @@ -549,11 +594,10 @@ func TestHCLAttrsCustomType(t *testing.T) { require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "app") require.Equal(t, []string{"linux/arm64", "linux/amd64"}, c.Targets[0].Platforms) - require.Equal(t, "linux/arm64", c.Targets[0].Args["v1"]) + require.Equal(t, ptrstr("linux/arm64"), c.Targets[0].Args["v1"]) } func TestHCLMultiFileAttrs(t *testing.T) { - os.Unsetenv("FOO") dt := []byte(` variable "FOO" { default = "abc" @@ -575,9 +619,9 @@ func TestHCLMultiFileAttrs(t *testing.T) { require.NoError(t, err) require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "app") - require.Equal(t, "pre-def", c.Targets[0].Args["v1"]) + require.Equal(t, ptrstr("pre-def"), c.Targets[0].Args["v1"]) - os.Setenv("FOO", "ghi") + t.Setenv("FOO", "ghi") c, err = ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, @@ -587,7 +631,7 @@ func TestHCLMultiFileAttrs(t *testing.T) { require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "app") - require.Equal(t, "pre-ghi", c.Targets[0].Args["v1"]) + require.Equal(t, ptrstr("pre-ghi"), c.Targets[0].Args["v1"]) } func TestJSONAttributes(t *testing.T) { @@ -598,7 +642,7 @@ func TestJSONAttributes(t *testing.T) { require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "app") - require.Equal(t, "pre-abc-def", c.Targets[0].Args["v1"]) + require.Equal(t, ptrstr("pre-abc-def"), c.Targets[0].Args["v1"]) } func TestJSONFunctions(t *testing.T) { @@ -623,7 +667,7 @@ func TestJSONFunctions(t *testing.T) { require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "app") - require.Equal(t, "pre-", c.Targets[0].Args["v1"]) + require.Equal(t, ptrstr("pre-"), c.Targets[0].Args["v1"]) } func TestHCLFunctionInAttr(t *testing.T) { @@ -651,7 +695,7 @@ func TestHCLFunctionInAttr(t *testing.T) { require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "app") - require.Equal(t, "FOO <> [baz]", c.Targets[0].Args["v1"]) + require.Equal(t, ptrstr("FOO <> [baz]"), c.Targets[0].Args["v1"]) } func TestHCLCombineCompose(t *testing.T) { @@ -682,8 +726,8 @@ services: require.Equal(t, 1, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "app") - require.Equal(t, "foo", c.Targets[0].Args["v1"]) - require.Equal(t, "bar", c.Targets[0].Args["v2"]) + require.Equal(t, ptrstr("foo"), c.Targets[0].Args["v1"]) + require.Equal(t, ptrstr("bar"), c.Targets[0].Args["v2"]) require.Equal(t, "dir", *c.Targets[0].Context) require.Equal(t, "Dockerfile-alternate", *c.Targets[0].Dockerfile) } @@ -828,10 +872,10 @@ target "two" { require.Equal(t, 2, len(c.Targets)) require.Equal(t, c.Targets[0].Name, "one") - require.Equal(t, map[string]string{"a": "pre-ghi-jkl"}, c.Targets[0].Args) + require.Equal(t, map[string]*string{"a": ptrstr("pre-ghi-jkl")}, c.Targets[0].Args) require.Equal(t, c.Targets[1].Name, "two") - require.Equal(t, map[string]string{"b": "pre-jkl"}, c.Targets[1].Args) + require.Equal(t, map[string]*string{"b": ptrstr("pre-jkl")}, c.Targets[1].Args) } func TestEmptyVariableJSON(t *testing.T) { @@ -882,3 +926,12 @@ func TestVarUnsupportedType(t *testing.T) { _, err := ParseFile(dt, "docker-bake.hcl") require.Error(t, err) } + +func ptrstr(s interface{}) *string { + var n *string = nil + if reflect.ValueOf(s).Kind() == reflect.String { + ss := s.(string) + n = &ss + } + return n +} diff --git a/bake/hclparser/expr.go b/bake/hclparser/expr.go index f8a5e765..23a99f54 100644 --- a/bake/hclparser/expr.go +++ b/bake/hclparser/expr.go @@ -83,7 +83,7 @@ func appendJSONFuncCalls(exp hcl.Expression, m map[string]struct{}) error { // hcl/v2/json/ast#stringVal val := src.FieldByName("Value") - if val.IsZero() { + if !val.IsValid() || val.IsZero() { return nil } rng := src.FieldByName("SrcRange") diff --git a/bake/hclparser/hclparser.go b/bake/hclparser/hclparser.go index ac1f248e..6a8a7926 100644 --- a/bake/hclparser/hclparser.go +++ b/bake/hclparser/hclparser.go @@ -281,19 +281,16 @@ func (p *parser) resolveValue(name string) (err error) { _, isVar := p.vars[name] if envv, ok := p.opt.LookupVar(name); ok && isVar { - if vv.Type().Equals(cty.Bool) { + switch { + case vv.Type().Equals(cty.Bool): b, err := strconv.ParseBool(envv) if err != nil { return errors.Wrapf(err, "failed to parse %s as bool", name) } - vv := cty.BoolVal(b) - v = &vv - return nil - } else if vv.Type().Equals(cty.String) { - vv := cty.StringVal(envv) - v = &vv - return nil - } else if vv.Type().Equals(cty.Number) { + vv = cty.BoolVal(b) + case vv.Type().Equals(cty.String), vv.Type().Equals(cty.DynamicPseudoType): + vv = cty.StringVal(envv) + case vv.Type().Equals(cty.Number): n, err := strconv.ParseFloat(envv, 64) if err == nil && (math.IsNaN(n) || math.IsInf(n, 0)) { err = errors.Errorf("invalid number value") @@ -301,10 +298,8 @@ func (p *parser) resolveValue(name string) (err error) { if err != nil { return errors.Wrapf(err, "failed to parse %s as number", name) } - vv := cty.NumberVal(big.NewFloat(n)) - v = &vv - return nil - } else { + vv = cty.NumberVal(big.NewFloat(n)) + default: // TODO: support lists with csv values return errors.Errorf("unsupported type %s for variable %s", vv.Type().FriendlyName(), name) }