typeexpr: Replace null attr values with defaults

Previously, when applying defaults to an input variable's given value
before type conversion, we would permit `null` attribute values to
override a specified default. This behaviour is inconsistent with the
intent of the type system underlying Terraform, and represented a
divergence from the treatment of `null` as equivalent to unset which
exists in resources. The same behaviour exists in top-level variable
definitions with `nullable = false`, and we consider this to be the
preferred behaviour here too.

This commit slightly changes default value application such that an
explicit `null` attribute value is treated as equivalent to the
attribute being missing. Default values for attributes will now replace
explicit nulls.
This commit is contained in:
Alisdair McDiarmid 2022-08-12 10:26:36 -04:00
parent 2aff67857f
commit c85ae29419
2 changed files with 78 additions and 3 deletions

View File

@ -91,7 +91,7 @@ func (t *defaultsTransformer) Enter(p cty.Path, v cty.Value) (cty.Value, error)
// Apply defaults where attributes are missing, constructing a new
// value with the same marks.
for attr, defaultValue := range defaults {
if _, ok := attrs[attr]; !ok {
if attrValue, ok := attrs[attr]; !ok || attrValue.IsNull() {
attrs[attr] = defaultValue
}
}

View File

@ -88,6 +88,23 @@ func TestDefaults_Apply(t *testing.T) {
"b": cty.StringVal("false"),
}),
},
// Defaults will replace explicit nulls.
"object with explicit null for attribute with default": {
defaults: &Defaults{
Type: simpleObject,
DefaultValues: map[string]cty.Value{
"b": cty.True,
},
},
value: cty.MapVal(map[string]cty.Value{
"a": cty.StringVal("foo"),
"b": cty.NullVal(cty.String),
}),
want: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("foo"),
"b": cty.True,
}),
},
// Defaults can be specified at any level of depth and will be applied
// so long as there is a parent value to populate.
"nested object with defaults applied": {
@ -371,7 +388,7 @@ func TestDefaults_Apply(t *testing.T) {
// the child type.
"c": cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("fallback"),
"b": cty.NullVal(cty.Bool),
"b": cty.False,
}),
},
Children: map[string]*Defaults{
@ -410,7 +427,65 @@ func TestDefaults_Apply(t *testing.T) {
// default value for "c" includes a non-default value
// already.
"a": cty.StringVal("fallback"),
"b": cty.NullVal(cty.Bool),
"b": cty.False,
}),
"d": cty.NumberIntVal(7),
}),
}),
},
"set of nested objects, nulls in default sub-object overridden": {
defaults: &Defaults{
Type: cty.Set(nestedObject),
Children: map[string]*Defaults{
"": {
Type: nestedObject,
DefaultValues: map[string]cty.Value{
// The default value for "c" is used to prepopulate
// the nested object's value if not specified, but
// the null default for its "b" attribute will be
// overridden by the default specified in the child
// type.
"c": cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("fallback"),
"b": cty.NullVal(cty.Bool),
}),
},
Children: map[string]*Defaults{
"c": {
Type: simpleObject,
DefaultValues: map[string]cty.Value{
"b": cty.True,
},
},
},
},
},
},
value: cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("foo"),
}),
"d": cty.NumberIntVal(5),
}),
cty.ObjectVal(map[string]cty.Value{
"d": cty.NumberIntVal(7),
}),
}),
want: cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"c": cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("foo"),
"b": cty.True,
}),
"d": cty.NumberIntVal(5),
}),
cty.ObjectVal(map[string]cty.Value{
"c": cty.ObjectVal(map[string]cty.Value{
// The default value for "b" overrides the explicit
// null in the default value for "c".
"a": cty.StringVal("fallback"),
"b": cty.True,
}),
"d": cty.NumberIntVal(7),
}),