Merge pull request #31634 from hashicorp/alisdair/optional-object-attribute-explicit-null

typeexpr: Replace null attr values with defaults
This commit is contained in:
Alisdair McDiarmid 2022-08-17 08:54:21 -04:00 committed by GitHub
commit 4960e6aeba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 111 additions and 4 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),
}),

View File

@ -260,7 +260,7 @@ value and thus perform no type conversion whatsoever.
## Optional Object Type Attributes
-> **Note:** Optional type attributes are supported in Terraform v1.3 and later.
-> **Note:** Optional object type attributes are supported only in Terraform v1.3 and later.
Terraform typically returns an error when it does not receive a value for specified object attributes. When you mark an attribute as optional, Terraform instead inserts a default value for the missing attribute. This allows the receiving module to describe an appropriate fallback behavior.
@ -281,6 +281,8 @@ The `optional` modifier takes one or two arguments.
specifies the type of the attribute.
- **Default:** (Optional) The second argument defines the default value that Terraform should use if the attribute is not present. This must be compatible with the attribute type. If not specified, Terraform uses a `null` value of the appropriate type as the default.
An optional attribute with a non-`null` default value is guaranteed to never have the value `null` within the receiving module. Terraform will substitute the default value both when a caller omits the attribute altogether and when a caller explicitly sets it to `null`, thereby avoiding the need for additional checks to handle a possible null value.
Terraform applies object attribute defaults top-down in nested variable types. This means that Terraform applies the default value you specify in the `optional` modifier first and then later applies any nested default values to that attribute.
### Example: Nested Structures with Optional Attributes and Defaults
@ -383,3 +385,33 @@ tolist([
},
])
```
### Example: Conditionally setting an optional attribute
Sometimes the decision about whether or not to set a value for an optional argument needs to be made dynamically based on some other data. In that case, the calling `module` block can use a conditional expression with `null` as one of its result arms to represent dynamically leaving the argument unset.
With the `variable "buckets"` declaration shown in the previous section, the following example conditionally overrides the `index_document` and `error_document` settings in the `website` object based on a new variable `var.legacy_filenames`:
```hcl
variable "legacy_filenames" {
type = bool
default = false
nullable = false
}
module "buckets" {
source = "./modules/buckets"
buckets = [
{
name = "maybe_legacy"
website = {
error_document = var.legacy_filenames ? "ERROR.HTM" : null
index_document = var.legacy_filenames ? "INDEX.HTM" : null
}
},
]
}
```
When `var.legacy_filenames` is set to `true`, the call will override the document filenames. When it is `false`, the call will leave the two filenames unspecified, thereby allowing the module to use its specified default values.