diff --git a/bake/hcl_test.go b/bake/hcl_test.go index db1ca9c6cf73..450cd5de7d3c 100644 --- a/bake/hcl_test.go +++ b/bake/hcl_test.go @@ -480,6 +480,79 @@ func TestHCLTypedValuelessVariables(t *testing.T) { } } +func TestHCLTypedValuelessVariablesUsedAsNull(t *testing.T) { + // Omitting complex types since they can't (shouldn't) be used as direct build arg values. + // Complex types can be used directly for other bake attributes, but that'll conflate HCL + // evaluation with whether the attribute accepts null in lieu of empty list/object, etc. + // This usage probably hits the 80/20 rule for usage of null variables. + types := []string{ + "any", + "string", "number", "bool", + } + + t.Run("assignment", func(t *testing.T) { + for _, varType := range types { + tName := fmt.Sprintf("value-less var typed %q", varType) + t.Run(tName, func(t *testing.T) { + dt := fmt.Sprintf(` + variable "FOO" { + type = %s + } + + target "default" { + args = { + foo = FOO + } + }`, varType) + c, err := ParseFile([]byte(dt), "docker-bake.hcl") + require.NoError(t, err) + require.Equal(t, 1, len(c.Targets)) + require.Nil(t, c.Targets[0].Args["foo"]) + }) + } + }) + t.Run("ternary", func(t *testing.T) { + for _, varType := range types { + tName := fmt.Sprintf("value-less var of %q on 'false' branch", varType) + t.Run(tName, func(t *testing.T) { + dt := fmt.Sprintf(` + variable "FOO" { + type = %s + } + + target "default" { + args = { + foo = FOO == null ? "hi" : FOO + } + }`, varType) + c, err := ParseFile([]byte(dt), "docker-bake.hcl") + require.NoError(t, err) + require.Equal(t, 1, len(c.Targets)) + require.Equal(t, "hi", *c.Targets[0].Args["foo"]) + }) + } + for _, varType := range types { + tName := fmt.Sprintf("value-less var of %q on 'true' branch", varType) + t.Run(tName, func(t *testing.T) { + dt := fmt.Sprintf(` + variable "FOO" { + type = %s + } + + target "default" { + args = { + foo = FOO == null ? FOO : "hi" + } + }`, varType) + c, err := ParseFile([]byte(dt), "docker-bake.hcl") + require.NoError(t, err) + require.Equal(t, 1, len(c.Targets)) + require.Nil(t, c.Targets[0].Args["foo"]) + }) + } + }) +} + func TestJSONNullVariables(t *testing.T) { dt := []byte(`{ "variable": { diff --git a/bake/hclparser/hclparser.go b/bake/hclparser/hclparser.go index 0cf38e142aa8..6b62efad0521 100644 --- a/bake/hclparser/hclparser.go +++ b/bake/hclparser/hclparser.go @@ -304,17 +304,21 @@ func (p *parser) resolveValue(ectx *hcl.EvalContext, name string) (err error) { } } + var vv cty.Value if def == nil { // Lack of specified value, when untyped is considered to have an empty string value. // A typed variable with no value will result in (typed) nil. - if _, ok, _ := p.valueHasOverride(name, false); !ok && !typeSpecified { - vv := cty.StringVal("") + if _, ok, _ := p.valueHasOverride(name, false); !ok { + if typeSpecified { + vv = cty.NullVal(varType) + } else { + vv = cty.StringVal("") + } v = &vv return } } - var vv cty.Value if def != nil { if diags := p.loadDeps(ectx, def.Expr, nil, true); diags.HasErrors() { return diags