diff --git a/internal/configs/named_values.go b/internal/configs/named_values.go
index 10882ac088f4..9491224e1ef6 100644
--- a/internal/configs/named_values.go
+++ b/internal/configs/named_values.go
@@ -27,6 +27,7 @@ type Variable struct {
// ConstraintType is used for decoding and type conversions, and may
// contain nested ObjectWithOptionalAttr types.
ConstraintType cty.Type
+ TypeDefaults *typeexpr.Defaults
ParsingMode VariableParsingMode
Validations []*CheckRule
@@ -102,9 +103,10 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno
}
if attr, exists := content.Attributes["type"]; exists {
- ty, parseMode, tyDiags := decodeVariableType(attr.Expr)
+ ty, tyDefaults, parseMode, tyDiags := decodeVariableType(attr.Expr)
diags = append(diags, tyDiags...)
v.ConstraintType = ty
+ v.TypeDefaults = tyDefaults
v.Type = ty.WithoutOptionalAttributesDeep()
v.ParsingMode = parseMode
}
@@ -137,6 +139,11 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno
// the type might not be set; we'll catch that during merge.
if v.ConstraintType != cty.NilType {
var err error
+ // If the type constraint has defaults, we must apply those
+ // defaults to the variable default value before type conversion.
+ if v.TypeDefaults != nil {
+ val = v.TypeDefaults.Apply(val)
+ }
val, err = convert.Convert(val, v.ConstraintType)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
@@ -179,7 +186,7 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno
return v, diags
}
-func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl.Diagnostics) {
+func decodeVariableType(expr hcl.Expression) (cty.Type, *typeexpr.Defaults, VariableParsingMode, hcl.Diagnostics) {
if exprIsNativeQuotedString(expr) {
// If a user provides the pre-0.12 form of variable type argument where
// the string values "string", "list" and "map" are accepted, we
@@ -190,7 +197,7 @@ func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl
// in the normal codepath below.
val, diags := expr.Value(nil)
if diags.HasErrors() {
- return cty.DynamicPseudoType, VariableParseHCL, diags
+ return cty.DynamicPseudoType, nil, VariableParseHCL, diags
}
str := val.AsString()
switch str {
@@ -201,7 +208,7 @@ func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl
Detail: "Terraform 0.11 and earlier required type constraints to be given in quotes, but that form is now deprecated and will be removed in a future version of Terraform. Remove the quotes around \"string\".",
Subject: expr.Range().Ptr(),
})
- return cty.DynamicPseudoType, VariableParseLiteral, diags
+ return cty.DynamicPseudoType, nil, VariableParseLiteral, diags
case "list":
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
@@ -209,7 +216,7 @@ func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl
Detail: "Terraform 0.11 and earlier required type constraints to be given in quotes, but that form is now deprecated and will be removed in a future version of Terraform. Remove the quotes around \"list\" and write list(string) instead to explicitly indicate that the list elements are strings.",
Subject: expr.Range().Ptr(),
})
- return cty.DynamicPseudoType, VariableParseHCL, diags
+ return cty.DynamicPseudoType, nil, VariableParseHCL, diags
case "map":
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
@@ -217,9 +224,9 @@ func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl
Detail: "Terraform 0.11 and earlier required type constraints to be given in quotes, but that form is now deprecated and will be removed in a future version of Terraform. Remove the quotes around \"map\" and write map(string) instead to explicitly indicate that the map elements are strings.",
Subject: expr.Range().Ptr(),
})
- return cty.DynamicPseudoType, VariableParseHCL, diags
+ return cty.DynamicPseudoType, nil, VariableParseHCL, diags
default:
- return cty.DynamicPseudoType, VariableParseHCL, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, VariableParseHCL, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: "Invalid legacy variable type hint",
Detail: `To provide a full type expression, remove the surrounding quotes and give the type expression directly.`,
@@ -234,23 +241,23 @@ func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl
// elements are consistent. This is the same as list(any) or map(any).
switch hcl.ExprAsKeyword(expr) {
case "list":
- return cty.List(cty.DynamicPseudoType), VariableParseHCL, nil
+ return cty.List(cty.DynamicPseudoType), nil, VariableParseHCL, nil
case "map":
- return cty.Map(cty.DynamicPseudoType), VariableParseHCL, nil
+ return cty.Map(cty.DynamicPseudoType), nil, VariableParseHCL, nil
}
- ty, diags := typeexpr.TypeConstraint(expr)
+ ty, typeDefaults, diags := typeexpr.TypeConstraintWithDefaults(expr)
if diags.HasErrors() {
- return cty.DynamicPseudoType, VariableParseHCL, diags
+ return cty.DynamicPseudoType, nil, VariableParseHCL, diags
}
switch {
case ty.IsPrimitiveType():
// Primitive types use literal parsing.
- return ty, VariableParseLiteral, diags
+ return ty, typeDefaults, VariableParseLiteral, diags
default:
// Everything else uses HCL parsing
- return ty, VariableParseHCL, diags
+ return ty, typeDefaults, VariableParseHCL, diags
}
}
diff --git a/internal/configs/testdata/warning-files/object-optional-attrs-experiment.tf b/internal/configs/testdata/warning-files/object-optional-attrs-experiment.tf
index 1645fb0eca70..90bd8a6329c9 100644
--- a/internal/configs/testdata/warning-files/object-optional-attrs-experiment.tf
+++ b/internal/configs/testdata/warning-files/object-optional-attrs-experiment.tf
@@ -7,6 +7,7 @@ terraform {
variable "a" {
type = object({
foo = optional(string)
+ bar = optional(bool, true)
})
}
diff --git a/internal/lang/funcs/defaults.go b/internal/lang/funcs/defaults.go
deleted file mode 100644
index b91ae9395f77..000000000000
--- a/internal/lang/funcs/defaults.go
+++ /dev/null
@@ -1,288 +0,0 @@
-package funcs
-
-import (
- "fmt"
-
- "github.com/hashicorp/terraform/internal/tfdiags"
- "github.com/zclconf/go-cty/cty"
- "github.com/zclconf/go-cty/cty/convert"
- "github.com/zclconf/go-cty/cty/function"
-)
-
-// DefaultsFunc is a helper function for substituting default values in
-// place of null values in a given data structure.
-//
-// See the documentation for function Defaults for more information.
-var DefaultsFunc = function.New(&function.Spec{
- Params: []function.Parameter{
- {
- Name: "input",
- Type: cty.DynamicPseudoType,
- AllowNull: true,
- AllowMarked: true,
- },
- {
- Name: "defaults",
- Type: cty.DynamicPseudoType,
- AllowMarked: true,
- },
- },
- Type: func(args []cty.Value) (cty.Type, error) {
- // The result type is guaranteed to be the same as the input type,
- // since all we're doing is replacing null values with non-null
- // values of the same type.
- retType := args[0].Type()
- defaultsType := args[1].Type()
-
- // This function is aimed at filling in object types or collections
- // of object types where some of the attributes might be null, so
- // it doesn't make sense to use a primitive type directly with it.
- // (The "coalesce" function may be appropriate for such cases.)
- if retType.IsPrimitiveType() {
- // This error message is a bit of a fib because we can actually
- // apply defaults to tuples too, but we expect that to be so
- // unusual as to not be worth mentioning here, because mentioning
- // it would require using some less-well-known Terraform language
- // terminology in the message (tuple types, structural types).
- return cty.DynamicPseudoType, function.NewArgErrorf(1, "only object types and collections of object types can have defaults applied")
- }
-
- defaultsPath := make(cty.Path, 0, 4) // some capacity so that most structures won't reallocate
- if err := defaultsAssertSuitableFallback(retType, defaultsType, defaultsPath); err != nil {
- errMsg := tfdiags.FormatError(err) // add attribute path prefix
- return cty.DynamicPseudoType, function.NewArgErrorf(1, "%s", errMsg)
- }
-
- return retType, nil
- },
- Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
- if args[0].Type().HasDynamicTypes() {
- // If the types our input object aren't known yet for some reason
- // then we'll defer all of our work here, because our
- // interpretation of the defaults depends on the types in
- // the input.
- return cty.UnknownVal(retType), nil
- }
-
- v := defaultsApply(args[0], args[1])
- return v, nil
- },
-})
-
-func defaultsApply(input, fallback cty.Value) cty.Value {
- wantTy := input.Type()
-
- umInput, inputMarks := input.Unmark()
- umFb, fallbackMarks := fallback.Unmark()
-
- // If neither are known, we very conservatively return an unknown value
- // with the union of marks on both input and default.
- if !(umInput.IsKnown() && umFb.IsKnown()) {
- return cty.UnknownVal(wantTy).WithMarks(inputMarks).WithMarks(fallbackMarks)
- }
-
- // For the rest of this function we're assuming that the given defaults
- // will always be valid, because we expect to have caught any problems
- // during the type checking phase. Any inconsistencies that reach here are
- // therefore considered to be implementation bugs, and so will panic.
-
- // Our strategy depends on the kind of type we're working with.
- switch {
- case wantTy.IsPrimitiveType():
- // For leaf primitive values the rule is relatively simple: use the
- // input if it's non-null, or fallback if input is null.
- if !umInput.IsNull() {
- return input
- }
- v, err := convert.Convert(umFb, wantTy)
- if err != nil {
- // Should not happen because we checked in defaultsAssertSuitableFallback
- panic(err.Error())
- }
- return v.WithMarks(fallbackMarks)
-
- case wantTy.IsObjectType():
- // For structural types, a null input value must be passed through. We
- // do not apply default values for missing optional structural values,
- // only their contents.
- //
- // We also pass through the input if the fallback value is null. This
- // can happen if the given defaults do not include a value for this
- // attribute.
- if umInput.IsNull() || umFb.IsNull() {
- return input
- }
- atys := wantTy.AttributeTypes()
- ret := map[string]cty.Value{}
- for attr, aty := range atys {
- inputSub := umInput.GetAttr(attr)
- fallbackSub := cty.NullVal(aty)
- if umFb.Type().HasAttribute(attr) {
- fallbackSub = umFb.GetAttr(attr)
- }
- ret[attr] = defaultsApply(inputSub.WithMarks(inputMarks), fallbackSub.WithMarks(fallbackMarks))
- }
- return cty.ObjectVal(ret)
-
- case wantTy.IsTupleType():
- // For structural types, a null input value must be passed through. We
- // do not apply default values for missing optional structural values,
- // only their contents.
- //
- // We also pass through the input if the fallback value is null. This
- // can happen if the given defaults do not include a value for this
- // attribute.
- if umInput.IsNull() || umFb.IsNull() {
- return input
- }
-
- l := wantTy.Length()
- ret := make([]cty.Value, l)
- for i := 0; i < l; i++ {
- inputSub := umInput.Index(cty.NumberIntVal(int64(i)))
- fallbackSub := umFb.Index(cty.NumberIntVal(int64(i)))
- ret[i] = defaultsApply(inputSub.WithMarks(inputMarks), fallbackSub.WithMarks(fallbackMarks))
- }
- return cty.TupleVal(ret)
-
- case wantTy.IsCollectionType():
- // For collection types we apply a single fallback value to each
- // element of the input collection, because in the situations this
- // function is intended for we assume that the number of elements
- // is the caller's decision, and so we'll just apply the same defaults
- // to all of the elements.
- ety := wantTy.ElementType()
- switch {
- case wantTy.IsMapType():
- newVals := map[string]cty.Value{}
-
- if !umInput.IsNull() {
- for it := umInput.ElementIterator(); it.Next(); {
- k, v := it.Element()
- newVals[k.AsString()] = defaultsApply(v.WithMarks(inputMarks), fallback.WithMarks(fallbackMarks))
- }
- }
-
- if len(newVals) == 0 {
- return cty.MapValEmpty(ety)
- }
- return cty.MapVal(newVals)
- case wantTy.IsListType(), wantTy.IsSetType():
- var newVals []cty.Value
-
- if !umInput.IsNull() {
- for it := umInput.ElementIterator(); it.Next(); {
- _, v := it.Element()
- newV := defaultsApply(v.WithMarks(inputMarks), fallback.WithMarks(fallbackMarks))
- newVals = append(newVals, newV)
- }
- }
-
- if len(newVals) == 0 {
- if wantTy.IsSetType() {
- return cty.SetValEmpty(ety)
- }
- return cty.ListValEmpty(ety)
- }
- if wantTy.IsSetType() {
- return cty.SetVal(newVals)
- }
- return cty.ListVal(newVals)
- default:
- // There are no other collection types, so this should not happen
- panic(fmt.Sprintf("invalid collection type %#v", wantTy))
- }
- default:
- // We should've caught anything else in defaultsAssertSuitableFallback,
- // so this should not happen.
- panic(fmt.Sprintf("invalid target type %#v", wantTy))
- }
-}
-
-func defaultsAssertSuitableFallback(wantTy, fallbackTy cty.Type, fallbackPath cty.Path) error {
- // If the type we want is a collection type then we need to keep peeling
- // away collection type wrappers until we find the non-collection-type
- // that's underneath, which is what the fallback will actually be applied
- // to.
- inCollection := false
- for wantTy.IsCollectionType() {
- wantTy = wantTy.ElementType()
- inCollection = true
- }
-
- switch {
- case wantTy.IsPrimitiveType():
- // The fallback is valid if it's equal to or convertible to what we want.
- if fallbackTy.Equals(wantTy) {
- return nil
- }
- conversion := convert.GetConversion(fallbackTy, wantTy)
- if conversion == nil {
- msg := convert.MismatchMessage(fallbackTy, wantTy)
- return fallbackPath.NewErrorf("invalid default value for %s: %s", wantTy.FriendlyName(), msg)
- }
- return nil
- case wantTy.IsObjectType():
- if !fallbackTy.IsObjectType() {
- if inCollection {
- return fallbackPath.NewErrorf("the default value for a collection of an object type must itself be an object type, not %s", fallbackTy.FriendlyName())
- }
- return fallbackPath.NewErrorf("the default value for an object type must itself be an object type, not %s", fallbackTy.FriendlyName())
- }
- for attr, wantAty := range wantTy.AttributeTypes() {
- if !fallbackTy.HasAttribute(attr) {
- continue // it's always okay to not have a default value
- }
- fallbackSubpath := fallbackPath.GetAttr(attr)
- fallbackSubTy := fallbackTy.AttributeType(attr)
- err := defaultsAssertSuitableFallback(wantAty, fallbackSubTy, fallbackSubpath)
- if err != nil {
- return err
- }
- }
- for attr := range fallbackTy.AttributeTypes() {
- if !wantTy.HasAttribute(attr) {
- fallbackSubpath := fallbackPath.GetAttr(attr)
- return fallbackSubpath.NewErrorf("target type does not expect an attribute named %q", attr)
- }
- }
- return nil
- case wantTy.IsTupleType():
- if !fallbackTy.IsTupleType() {
- if inCollection {
- return fallbackPath.NewErrorf("the default value for a collection of a tuple type must itself be a tuple type, not %s", fallbackTy.FriendlyName())
- }
- return fallbackPath.NewErrorf("the default value for a tuple type must itself be a tuple type, not %s", fallbackTy.FriendlyName())
- }
- wantEtys := wantTy.TupleElementTypes()
- fallbackEtys := fallbackTy.TupleElementTypes()
- if got, want := len(wantEtys), len(fallbackEtys); got != want {
- return fallbackPath.NewErrorf("the default value for a tuple type of length %d must also have length %d, not %d", want, want, got)
- }
- for i := 0; i < len(wantEtys); i++ {
- fallbackSubpath := fallbackPath.IndexInt(i)
- wantSubTy := wantEtys[i]
- fallbackSubTy := fallbackEtys[i]
- err := defaultsAssertSuitableFallback(wantSubTy, fallbackSubTy, fallbackSubpath)
- if err != nil {
- return err
- }
- }
- return nil
- default:
- // No other types are supported right now.
- return fallbackPath.NewErrorf("cannot apply defaults to %s", wantTy.FriendlyName())
- }
-}
-
-// Defaults is a helper function for substituting default values in
-// place of null values in a given data structure.
-//
-// This is primarily intended for use with a module input variable that
-// has an object type constraint (or a collection thereof) that has optional
-// attributes, so that the receiver of a value that omits those attributes
-// can insert non-null default values in place of the null values caused by
-// omitting the attributes.
-func Defaults(input, defaults cty.Value) (cty.Value, error) {
- return DefaultsFunc.Call([]cty.Value{input, defaults})
-}
diff --git a/internal/lang/funcs/defaults_test.go b/internal/lang/funcs/defaults_test.go
deleted file mode 100644
index e40163265a19..000000000000
--- a/internal/lang/funcs/defaults_test.go
+++ /dev/null
@@ -1,648 +0,0 @@
-package funcs
-
-import (
- "fmt"
- "testing"
-
- "github.com/zclconf/go-cty/cty"
-)
-
-func TestDefaults(t *testing.T) {
- tests := []struct {
- Input, Defaults cty.Value
- Want cty.Value
- WantErr string
- }{
- { // When *either* input or default are unknown, an unknown is returned.
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.UnknownVal(cty.String),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello"),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.UnknownVal(cty.String),
- }),
- },
- {
- // When *either* input or default are unknown, an unknown is
- // returned with marks from both input and defaults.
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.UnknownVal(cty.String),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello").Mark("marked"),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.UnknownVal(cty.String).Mark("marked"),
- }),
- },
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.NullVal(cty.String),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello"),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello"),
- }),
- },
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hey"),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello"),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hey"),
- }),
- },
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.NullVal(cty.String),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.NullVal(cty.String),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.NullVal(cty.String),
- }),
- },
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.NullVal(cty.String),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{}),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.NullVal(cty.String),
- }),
- },
- {
- Input: cty.ObjectVal(map[string]cty.Value{}),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.NullVal(cty.String),
- }),
- WantErr: `.a: target type does not expect an attribute named "a"`,
- },
-
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ListVal([]cty.Value{
- cty.NullVal(cty.String),
- }),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello"),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ListVal([]cty.Value{
- cty.StringVal("hello"),
- }),
- }),
- },
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ListVal([]cty.Value{
- cty.NullVal(cty.String),
- cty.StringVal("hey"),
- cty.NullVal(cty.String),
- }),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello"),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ListVal([]cty.Value{
- cty.StringVal("hello"),
- cty.StringVal("hey"),
- cty.StringVal("hello"),
- }),
- }),
- },
- {
- // Using defaults with single set elements is a pretty
- // odd thing to do, but this behavior is just here because
- // it generalizes from how we handle collections. It's
- // tested only to ensure it doesn't change accidentally
- // in future.
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.SetVal([]cty.Value{
- cty.NullVal(cty.String),
- cty.StringVal("hey"),
- }),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello"),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.SetVal([]cty.Value{
- cty.StringVal("hey"),
- cty.StringVal("hello"),
- }),
- }),
- },
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.MapVal(map[string]cty.Value{
- "x": cty.NullVal(cty.String),
- "y": cty.StringVal("hey"),
- "z": cty.NullVal(cty.String),
- }),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello"),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.MapVal(map[string]cty.Value{
- "x": cty.StringVal("hello"),
- "y": cty.StringVal("hey"),
- "z": cty.StringVal("hello"),
- }),
- }),
- },
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ListVal([]cty.Value{
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hey"),
- }),
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.NullVal(cty.String),
- }),
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hey"),
- }),
- }),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hello"),
- }),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ListVal([]cty.Value{
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hey"),
- }),
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hello"),
- }),
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hey"),
- }),
- }),
- }),
- },
- {
- Input: cty.ListVal([]cty.Value{
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hey"),
- }),
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.NullVal(cty.String),
- }),
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hey"),
- }),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hello"),
- }),
- Want: cty.ListVal([]cty.Value{
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hey"),
- }),
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hello"),
- }),
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hey"),
- }),
- }),
- },
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.SetVal([]cty.Value{
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("boop"),
- }),
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.NullVal(cty.String),
- }),
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hey"),
- }),
- }),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hello"),
- }),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.SetVal([]cty.Value{
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("boop"),
- }),
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hello"),
- }),
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hey"),
- }),
- }),
- }),
- },
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.SetVal([]cty.Value{
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hello"),
- }),
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.NullVal(cty.String),
- }),
- }),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hello"),
- }),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.SetVal([]cty.Value{
- // After applying defaults, the one with a null value
- // coalesced with the one with a non-null value,
- // and so there's only one left.
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hello"),
- }),
- }),
- }),
- },
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.MapVal(map[string]cty.Value{
- "boop": cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hey"),
- }),
- "beep": cty.ObjectVal(map[string]cty.Value{
- "b": cty.NullVal(cty.String),
- }),
- }),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hello"),
- }),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.MapVal(map[string]cty.Value{
- "boop": cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hey"),
- }),
- "beep": cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hello"),
- }),
- }),
- }),
- },
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ListVal([]cty.Value{
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hey"),
- }),
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.NullVal(cty.String),
- }),
- cty.ObjectVal(map[string]cty.Value{
- "b": cty.StringVal("hey"),
- }),
- }),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello"),
- }),
- WantErr: `.a: the default value for a collection of an object type must itself be an object type, not string`,
- },
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ListVal([]cty.Value{
- cty.NullVal(cty.String),
- cty.StringVal("hey"),
- cty.NullVal(cty.String),
- }),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- // The default value for a list must be a single value
- // of the list's element type which provides defaults
- // for each element separately, so the default for a
- // list of string should be just a single string, not
- // a list of string.
- "a": cty.ListVal([]cty.Value{
- cty.StringVal("hello"),
- }),
- }),
- WantErr: `.a: invalid default value for string: string required`,
- },
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.TupleVal([]cty.Value{
- cty.NullVal(cty.String),
- cty.StringVal("hey"),
- cty.NullVal(cty.String),
- }),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello"),
- }),
- WantErr: `.a: the default value for a tuple type must itself be a tuple type, not string`,
- },
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.TupleVal([]cty.Value{
- cty.NullVal(cty.String),
- cty.StringVal("hey"),
- cty.NullVal(cty.String),
- }),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.TupleVal([]cty.Value{
- cty.StringVal("hello 0"),
- cty.StringVal("hello 1"),
- cty.StringVal("hello 2"),
- }),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.TupleVal([]cty.Value{
- cty.StringVal("hello 0"),
- cty.StringVal("hey"),
- cty.StringVal("hello 2"),
- }),
- }),
- },
- {
- // There's no reason to use this function for plain primitive
- // types, because the "default" argument in a variable definition
- // already has the equivalent behavior. This function is only
- // to deal with the situation of a complex-typed variable where
- // only parts of the data structure are optional.
- Input: cty.NullVal(cty.String),
- Defaults: cty.StringVal("hello"),
- WantErr: `only object types and collections of object types can have defaults applied`,
- },
- // When applying default values to structural types, null objects or
- // tuples in the input should be passed through.
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.NullVal(cty.Object(map[string]cty.Type{
- "x": cty.String,
- "y": cty.String,
- })),
- "b": cty.NullVal(cty.Tuple([]cty.Type{cty.String, cty.String})),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ObjectVal(map[string]cty.Value{
- "x": cty.StringVal("hello"),
- "y": cty.StringVal("there"),
- }),
- "b": cty.TupleVal([]cty.Value{
- cty.StringVal("how are"),
- cty.StringVal("you?"),
- }),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.NullVal(cty.Object(map[string]cty.Type{
- "x": cty.String,
- "y": cty.String,
- })),
- "b": cty.NullVal(cty.Tuple([]cty.Type{cty.String, cty.String})),
- }),
- },
- // When applying default values to structural types, we permit null
- // values in the defaults, and just pass through the input value.
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ListVal([]cty.Value{
- cty.ObjectVal(map[string]cty.Value{
- "p": cty.StringVal("xyz"),
- "q": cty.StringVal("xyz"),
- }),
- }),
- "b": cty.SetVal([]cty.Value{
- cty.TupleVal([]cty.Value{
- cty.NumberIntVal(0),
- cty.NumberIntVal(2),
- }),
- cty.TupleVal([]cty.Value{
- cty.NumberIntVal(1),
- cty.NumberIntVal(3),
- }),
- }),
- "c": cty.NullVal(cty.String),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "c": cty.StringVal("tada"),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ListVal([]cty.Value{
- cty.ObjectVal(map[string]cty.Value{
- "p": cty.StringVal("xyz"),
- "q": cty.StringVal("xyz"),
- }),
- }),
- "b": cty.SetVal([]cty.Value{
- cty.TupleVal([]cty.Value{
- cty.NumberIntVal(0),
- cty.NumberIntVal(2),
- }),
- cty.TupleVal([]cty.Value{
- cty.NumberIntVal(1),
- cty.NumberIntVal(3),
- }),
- }),
- "c": cty.StringVal("tada"),
- }),
- },
- // When applying default values to collection types, null collections in the
- // input should result in empty collections in the output.
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.NullVal(cty.List(cty.String)),
- "b": cty.NullVal(cty.Map(cty.String)),
- "c": cty.NullVal(cty.Set(cty.String)),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello"),
- "b": cty.StringVal("hi"),
- "c": cty.StringVal("greetings"),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ListValEmpty(cty.String),
- "b": cty.MapValEmpty(cty.String),
- "c": cty.SetValEmpty(cty.String),
- }),
- },
- // When specifying fallbacks, we allow mismatched primitive attribute
- // types so long as a safe conversion is possible. This means that we
- // can accept number or boolean values for string attributes.
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.NullVal(cty.String),
- "b": cty.NullVal(cty.String),
- "c": cty.NullVal(cty.String),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.NumberIntVal(5),
- "b": cty.True,
- "c": cty.StringVal("greetings"),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("5"),
- "b": cty.StringVal("true"),
- "c": cty.StringVal("greetings"),
- }),
- },
- // Fallbacks with mismatched primitive attribute types which do not
- // have safe conversions must not pass the suitable fallback check,
- // even if unsafe conversion would be possible.
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.NullVal(cty.Bool),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("5"),
- }),
- WantErr: ".a: invalid default value for bool: bool required",
- },
- // marks: we should preserve marks from both input value and defaults as leafily as possible
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.NullVal(cty.String),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello").Mark("world"),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello").Mark("world"),
- }),
- },
- { // "unused" marks don't carry over
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.NullVal(cty.String).Mark("a"),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello"),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello"),
- }),
- },
- { // Marks on tuples remain attached to individual elements
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.TupleVal([]cty.Value{
- cty.NullVal(cty.String),
- cty.StringVal("hey").Mark("input"),
- cty.NullVal(cty.String),
- }),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.TupleVal([]cty.Value{
- cty.StringVal("hello 0").Mark("fallback"),
- cty.StringVal("hello 1"),
- cty.StringVal("hello 2"),
- }),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.TupleVal([]cty.Value{
- cty.StringVal("hello 0").Mark("fallback"),
- cty.StringVal("hey").Mark("input"),
- cty.StringVal("hello 2"),
- }),
- }),
- },
- { // Marks from list elements
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ListVal([]cty.Value{
- cty.NullVal(cty.String),
- cty.StringVal("hey").Mark("input"),
- cty.NullVal(cty.String),
- }),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello 0").Mark("fallback"),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ListVal([]cty.Value{
- cty.StringVal("hello 0").Mark("fallback"),
- cty.StringVal("hey").Mark("input"),
- cty.StringVal("hello 0").Mark("fallback"),
- }),
- }),
- },
- {
- // Sets don't allow individually-marked elements, so the marks
- // end up aggregating on the set itself anyway in this case.
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.SetVal([]cty.Value{
- cty.NullVal(cty.String),
- cty.NullVal(cty.String),
- cty.StringVal("hey").Mark("input"),
- }),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello 0").Mark("fallback"),
- }),
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.SetVal([]cty.Value{
- cty.StringVal("hello 0"),
- cty.StringVal("hey"),
- cty.StringVal("hello 0"),
- }).WithMarks(cty.NewValueMarks("fallback", "input")),
- }),
- },
- {
- Input: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ListVal([]cty.Value{
- cty.NullVal(cty.String),
- }),
- }),
- Defaults: cty.ObjectVal(map[string]cty.Value{
- "a": cty.StringVal("hello").Mark("beep"),
- }).Mark("boop"),
- // This is the least-intuitive case. The mark "boop" is attached to
- // the default object, not it's elements, but both marks end up
- // aggregated on the list element.
- Want: cty.ObjectVal(map[string]cty.Value{
- "a": cty.ListVal([]cty.Value{
- cty.StringVal("hello").WithMarks(cty.NewValueMarks("beep", "boop")),
- }),
- }),
- },
- }
-
- for _, test := range tests {
- t.Run(fmt.Sprintf("defaults(%#v, %#v)", test.Input, test.Defaults), func(t *testing.T) {
- got, gotErr := Defaults(test.Input, test.Defaults)
-
- if test.WantErr != "" {
- if gotErr == nil {
- t.Fatalf("unexpected success\nwant error: %s", test.WantErr)
- }
- if got, want := gotErr.Error(), test.WantErr; got != want {
- t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want)
- }
- return
- } else if gotErr != nil {
- t.Fatalf("unexpected error\ngot: %s", gotErr.Error())
- }
-
- if !test.Want.RawEquals(got) {
- t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
- }
- })
- }
-}
diff --git a/internal/lang/functions.go b/internal/lang/functions.go
index f367f6cf7a25..1b3f88ce0476 100644
--- a/internal/lang/functions.go
+++ b/internal/lang/functions.go
@@ -56,7 +56,6 @@ func (s *Scope) Functions() map[string]function.Function {
"concat": stdlib.ConcatFunc,
"contains": stdlib.ContainsFunc,
"csvdecode": stdlib.CSVDecodeFunc,
- "defaults": s.experimentalFunction(experiments.ModuleVariableOptionalAttrs, funcs.DefaultsFunc),
"dirname": funcs.DirnameFunc,
"distinct": stdlib.DistinctFunc,
"element": stdlib.ElementFunc,
@@ -174,6 +173,8 @@ func (s *Scope) Functions() map[string]function.Function {
// the recieving scope. If so, it will return the given function verbatim.
// If not, it will return a placeholder function that just returns an
// error explaining that the function requires the experiment to be enabled.
+//
+//lint:ignore U1000 Ignore unused function error for now
func (s *Scope) experimentalFunction(experiment experiments.Experiment, fn function.Function) function.Function {
if s.activeExperiments.Has(experiment) {
return fn
diff --git a/internal/lang/functions_test.go b/internal/lang/functions_test.go
index 9a69432bd1b3..ea2091eb97fa 100644
--- a/internal/lang/functions_test.go
+++ b/internal/lang/functions_test.go
@@ -291,18 +291,6 @@ func TestFunctions(t *testing.T) {
},
},
- "defaults": {
- // This function is pretty specialized and so this is mainly
- // just a test that it is defined at all. See the function's
- // own unit tests for more interesting test cases.
- {
- `defaults({a: 4}, {a: 5})`,
- cty.ObjectVal(map[string]cty.Value{
- "a": cty.NumberIntVal(4),
- }),
- },
- },
-
"dirname": {
{
`dirname("testdata/hello.txt")`,
diff --git a/internal/terraform/context_apply_test.go b/internal/terraform/context_apply_test.go
index 841ec7719d13..97ee07769b58 100644
--- a/internal/terraform/context_apply_test.go
+++ b/internal/terraform/context_apply_test.go
@@ -11984,8 +11984,17 @@ terraform {
variable "in" {
type = object({
- required = string
- optional = optional(string)
+ required = string
+ optional = optional(string)
+ default = optional(bool, true)
+ nested = optional(
+ map(object({
+ a = optional(string, "foo")
+ b = optional(number, 5)
+ })), {
+ "boop": {}
+ }
+ )
})
}
@@ -12023,6 +12032,73 @@ output "out" {
// Because "optional" was marked as optional, it got silently filled
// in as a null value of string type rather than returning an error.
"optional": cty.NullVal(cty.String),
+
+ // Similarly, "default" was marked as optional with a default value,
+ // and since it was omitted should be filled in with that default.
+ "default": cty.True,
+
+ // Nested is a complex structure which has fully described defaults,
+ // so again it should be filled with the default structure.
+ "nested": cty.MapVal(map[string]cty.Value{
+ "boop": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.NumberIntVal(5),
+ }),
+ }),
+ })
+ if !want.RawEquals(got) {
+ t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want)
+ }
+}
+
+func TestContext2Apply_moduleVariableOptionalAttributesDefault(t *testing.T) {
+ m := testModuleInline(t, map[string]string{
+ "main.tf": `
+terraform {
+ experiments = [module_variable_optional_attrs]
+}
+
+variable "in" {
+ type = object({
+ required = string
+ optional = optional(string)
+ default = optional(bool, true)
+ })
+ default = {
+ required = "boop"
+ }
+}
+
+output "out" {
+ value = var.in
+}
+`})
+
+ ctx := testContext2(t, &ContextOpts{})
+
+ // We don't specify a value for the variable here, relying on its defined
+ // default.
+ plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
+ if diags.HasErrors() {
+ t.Fatal(diags.ErrWithWarnings())
+ }
+
+ state, diags := ctx.Apply(plan, m)
+ if diags.HasErrors() {
+ t.Fatal(diags.ErrWithWarnings())
+ }
+
+ got := state.RootModule().OutputValues["out"].Value
+ want := cty.ObjectVal(map[string]cty.Value{
+ "required": cty.StringVal("boop"),
+
+ // "optional" is not present in the variable default, so it is filled
+ // with null.
+ "optional": cty.NullVal(cty.String),
+
+ // Similarly, "default" is not present in the variable default, so its
+ // value is replaced with the type's specified default.
+ "default": cty.True,
})
if !want.RawEquals(got) {
t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want)
diff --git a/internal/terraform/eval_variable.go b/internal/terraform/eval_variable.go
index 1d50b911b206..67a777430fb2 100644
--- a/internal/terraform/eval_variable.go
+++ b/internal/terraform/eval_variable.go
@@ -90,6 +90,11 @@ func prepareFinalInputVariableValue(addr addrs.AbsInputVariableInstance, raw *In
given = defaultVal // must be set, because we checked above that the variable isn't required
}
+ // Apply defaults from the variable's type constraint to the given value
+ if cfg.TypeDefaults != nil {
+ given = cfg.TypeDefaults.Apply(given)
+ }
+
val, err := convert.Convert(given, convertTy)
if err != nil {
log.Printf("[ERROR] prepareFinalInputVariableValue: %s has unsuitable type\n got: %s\n want: %s", addr, given.Type(), convertTy)
diff --git a/internal/typeexpr/defaults.go b/internal/typeexpr/defaults.go
new file mode 100644
index 000000000000..fe1d5776b2b8
--- /dev/null
+++ b/internal/typeexpr/defaults.go
@@ -0,0 +1,157 @@
+package typeexpr
+
+import (
+ "github.com/zclconf/go-cty/cty"
+)
+
+// Defaults represents a type tree which may contain default values for
+// optional object attributes at any level. This is used to apply nested
+// defaults to an input value before converting it to the concrete type.
+type Defaults struct {
+ // Type of the node for which these defaults apply. This is necessary in
+ // order to determine how to inspect the Defaults and Children collections.
+ Type cty.Type
+
+ // DefaultValues contains the default values for each object attribute,
+ // indexed by attribute name.
+ DefaultValues map[string]cty.Value
+
+ // Children is a map of Defaults for elements contained in this type. This
+ // only applies to structural and collection types.
+ //
+ // The map is indexed by string instead of cty.Value because cty.Number
+ // instances are non-comparable, due to embedding a *big.Float.
+ //
+ // Collections have a single element type, which is stored at key "".
+ Children map[string]*Defaults
+}
+
+// Apply walks the given value, applying specified defaults wherever optional
+// attributes are missing. The input and output values may have different
+// types, and the result may still require type conversion to the final desired
+// type.
+//
+// This function is permissive and does not report errors, assuming that the
+// caller will have better context to report useful type conversion failure
+// diagnostics.
+func (d *Defaults) Apply(val cty.Value) cty.Value {
+ val, err := cty.TransformWithTransformer(val, &defaultsTransformer{defaults: d})
+
+ // The transformer should never return an error.
+ if err != nil {
+ panic(err)
+ }
+
+ return val
+}
+
+// defaultsTransformer implements cty.Transformer, as a pre-order traversal,
+// applying defaults as it goes. The pre-order traversal allows us to specify
+// defaults more loosely for structural types, as the defaults for the types
+// will be applied to the default value later in the walk.
+type defaultsTransformer struct {
+ defaults *Defaults
+}
+
+var _ cty.Transformer = (*defaultsTransformer)(nil)
+
+func (t *defaultsTransformer) Enter(p cty.Path, v cty.Value) (cty.Value, error) {
+ // Cannot apply defaults to an unknown value
+ if !v.IsKnown() {
+ return v, nil
+ }
+
+ // Look up the defaults for this path.
+ defaults := t.defaults.traverse(p)
+
+ // If we have no defaults, nothing to do.
+ if len(defaults) == 0 {
+ return v, nil
+ }
+
+ // Ensure we are working with an object or map.
+ vt := v.Type()
+ if !vt.IsObjectType() && !vt.IsMapType() {
+ // Cannot apply defaults because the value type is incompatible.
+ // We'll ignore this and let the later conversion stage display a
+ // more useful diagnostic.
+ return v, nil
+ }
+
+ // Unmark the value and reapply the marks later.
+ v, valMarks := v.Unmark()
+
+ // Convert the given value into an attribute map (if it's non-null and
+ // non-empty).
+ attrs := make(map[string]cty.Value)
+ if !v.IsNull() && v.LengthInt() > 0 {
+ attrs = v.AsValueMap()
+ }
+
+ // Apply defaults where attributes are missing, constructing a new
+ // value with the same marks.
+ for attr, defaultValue := range defaults {
+ if _, ok := attrs[attr]; !ok {
+ attrs[attr] = defaultValue
+ }
+ }
+
+ // We construct an object even if the input value was a map, as the
+ // type of an attribute's default value may be incompatible with the
+ // map element type.
+ return cty.ObjectVal(attrs).WithMarks(valMarks), nil
+}
+
+func (t *defaultsTransformer) Exit(p cty.Path, v cty.Value) (cty.Value, error) {
+ return v, nil
+}
+
+// traverse walks the abstract defaults structure for a given path, returning
+// a set of default values (if any are present) or nil (if not). This operation
+// differs from applying a path to a value because we need to customize the
+// traversal steps for collection types, where a single set of defaults can be
+// applied to an arbitrary number of elements.
+func (d *Defaults) traverse(path cty.Path) map[string]cty.Value {
+ if len(path) == 0 {
+ return d.DefaultValues
+ }
+
+ switch s := path[0].(type) {
+ case cty.GetAttrStep:
+ if d.Type.IsObjectType() {
+ // Attribute path steps are normally applied to objects, where each
+ // attribute may have different defaults.
+ return d.traverseChild(s.Name, path)
+ } else if d.Type.IsMapType() {
+ // Literal values for maps can result in attribute path steps, in which
+ // case we need to disregard the attribute name, as maps can have only
+ // one child.
+ return d.traverseChild("", path)
+ }
+
+ return nil
+ case cty.IndexStep:
+ if d.Type.IsTupleType() {
+ // Tuples can have different types for each element, so we look
+ // up the defaults based on the index key.
+ return d.traverseChild(s.Key.AsBigFloat().String(), path)
+ } else if d.Type.IsCollectionType() {
+ // Defaults for collection element types are stored with a blank
+ // key, so we disregard the index key.
+ return d.traverseChild("", path)
+ }
+ return nil
+ default:
+ // At time of writing there are no other path step types.
+ return nil
+ }
+}
+
+// traverseChild continues the traversal for a given child key, and mutually
+// recurses with traverse.
+func (d *Defaults) traverseChild(name string, path cty.Path) map[string]cty.Value {
+ if child, ok := d.Children[name]; ok {
+ return child.traverse(path[1:])
+ }
+ return nil
+}
diff --git a/internal/typeexpr/defaults_test.go b/internal/typeexpr/defaults_test.go
new file mode 100644
index 000000000000..b37e92c515fc
--- /dev/null
+++ b/internal/typeexpr/defaults_test.go
@@ -0,0 +1,429 @@
+package typeexpr
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/zclconf/go-cty/cty"
+)
+
+var (
+ valueComparer = cmp.Comparer(cty.Value.RawEquals)
+)
+
+func TestDefaults_Apply(t *testing.T) {
+ simpleObject := cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Bool,
+ }, []string{"b"})
+ nestedObject := cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "c": simpleObject,
+ "d": cty.Number,
+ }, []string{"c"})
+
+ testCases := map[string]struct {
+ defaults *Defaults
+ value cty.Value
+ want cty.Value
+ }{
+ // Nothing happens when there are no default values and no children.
+ "no defaults": {
+ defaults: &Defaults{
+ Type: cty.Map(cty.String),
+ },
+ value: cty.MapVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.StringVal("bar"),
+ }),
+ want: cty.MapVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.StringVal("bar"),
+ }),
+ },
+ // Passing a map which does not include one of the attributes with a
+ // default results in the default being applied to the output. Output
+ // is always an object.
+ "simple object with defaults applied": {
+ defaults: &Defaults{
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.True,
+ },
+ },
+ value: cty.MapVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ }),
+ want: cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.True,
+ }),
+ },
+ // Unknown values may be assigned to root modules during validation,
+ // and we cannot apply defaults at that time.
+ "simple object with defaults but unknown value": {
+ defaults: &Defaults{
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.True,
+ },
+ },
+ value: cty.UnknownVal(cty.Map(cty.String)),
+ want: cty.UnknownVal(cty.Map(cty.String)),
+ },
+ // Defaults do not override attributes which are present in the given
+ // value.
+ "simple object with optional attributes specified": {
+ 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.StringVal("false"),
+ }),
+ want: cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.StringVal("false"),
+ }),
+ },
+ // 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": {
+ defaults: &Defaults{
+ Type: nestedObject,
+ Children: map[string]*Defaults{
+ "c": {
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.False,
+ },
+ },
+ },
+ },
+ value: cty.ObjectVal(map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ }),
+ "d": cty.NumberIntVal(5),
+ }),
+ want: cty.ObjectVal(map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.False,
+ }),
+ "d": cty.NumberIntVal(5),
+ }),
+ },
+ // Testing traversal of collections.
+ "map of objects with defaults applied": {
+ defaults: &Defaults{
+ Type: cty.Map(simpleObject),
+ Children: map[string]*Defaults{
+ "": {
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.True,
+ },
+ },
+ },
+ },
+ value: cty.MapVal(map[string]cty.Value{
+ "f": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ }),
+ "b": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("bar"),
+ }),
+ }),
+ want: cty.MapVal(map[string]cty.Value{
+ "f": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.True,
+ }),
+ "b": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("bar"),
+ "b": cty.True,
+ }),
+ }),
+ },
+ // A map variable value specified in a tfvars file will be an object,
+ // in which case we must still traverse the defaults structure
+ // correctly.
+ "map of objects with defaults applied, given object instead of map": {
+ defaults: &Defaults{
+ Type: cty.Map(simpleObject),
+ Children: map[string]*Defaults{
+ "": {
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.True,
+ },
+ },
+ },
+ },
+ value: cty.ObjectVal(map[string]cty.Value{
+ "f": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ }),
+ "b": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("bar"),
+ }),
+ }),
+ want: cty.ObjectVal(map[string]cty.Value{
+ "f": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.True,
+ }),
+ "b": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("bar"),
+ "b": cty.True,
+ }),
+ }),
+ },
+ // Another example of a collection type, this time exercising the code
+ // processing a tuple input.
+ "list of objects with defaults applied": {
+ defaults: &Defaults{
+ Type: cty.List(simpleObject),
+ Children: map[string]*Defaults{
+ "": {
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.True,
+ },
+ },
+ },
+ },
+ value: cty.TupleVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("bar"),
+ }),
+ }),
+ want: cty.TupleVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.True,
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("bar"),
+ "b": cty.True,
+ }),
+ }),
+ },
+ // Unlike collections, tuple variable types can have defaults for
+ // multiple element types.
+ "tuple of objects with defaults applied": {
+ defaults: &Defaults{
+ Type: cty.Tuple([]cty.Type{simpleObject, nestedObject}),
+ Children: map[string]*Defaults{
+ "0": {
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.False,
+ },
+ },
+ "1": {
+ Type: nestedObject,
+ DefaultValues: map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("default"),
+ "b": cty.True,
+ }),
+ },
+ },
+ },
+ },
+ value: cty.TupleVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "d": cty.NumberIntVal(5),
+ }),
+ }),
+ want: cty.TupleVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.False,
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("default"),
+ "b": cty.True,
+ }),
+ "d": cty.NumberIntVal(5),
+ }),
+ }),
+ },
+ // More complex cases with deeply nested defaults, testing the "default
+ // within a default" edges.
+ "set of nested objects, no default sub-object": {
+ defaults: &Defaults{
+ Type: cty.Set(nestedObject),
+ Children: map[string]*Defaults{
+ "": {
+ Type: nestedObject,
+ 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{
+ // No default value for "c" specified, so none applied. The
+ // convert stage will fill in a null.
+ "d": cty.NumberIntVal(7),
+ }),
+ }),
+ },
+ "set of nested objects, empty default sub-object": {
+ defaults: &Defaults{
+ Type: cty.Set(nestedObject),
+ Children: map[string]*Defaults{
+ "": {
+ Type: nestedObject,
+ DefaultValues: map[string]cty.Value{
+ // This is a convenient shorthand which causes a
+ // missing sub-object to be filled with an object
+ // with all of the default values specified in the
+ // sub-object's type.
+ "c": cty.EmptyObjectVal,
+ },
+ 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{
+ // Default value for "b" is applied to the empty object
+ // specified as the default for "c"
+ "b": cty.True,
+ }),
+ "d": cty.NumberIntVal(7),
+ }),
+ }),
+ },
+ "set of nested objects, overriding default sub-object": {
+ defaults: &Defaults{
+ Type: cty.Set(nestedObject),
+ Children: map[string]*Defaults{
+ "": {
+ Type: nestedObject,
+ DefaultValues: map[string]cty.Value{
+ // If no value is given for "c", we use this object
+ // of non-default values instead. These take
+ // precedence over the default values 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" is not applied, as the
+ // default value for "c" includes a non-default value
+ // already.
+ "a": cty.StringVal("fallback"),
+ "b": cty.NullVal(cty.Bool),
+ }),
+ "d": cty.NumberIntVal(7),
+ }),
+ }),
+ },
+ }
+
+ for name, tc := range testCases {
+ t.Run(name, func(t *testing.T) {
+ got := tc.defaults.Apply(tc.value)
+ if !cmp.Equal(tc.want, got, valueComparer) {
+ t.Errorf("wrong result\n%s", cmp.Diff(tc.want, got, valueComparer))
+ }
+ })
+ }
+}
diff --git a/internal/typeexpr/get_type.go b/internal/typeexpr/get_type.go
index 726326adccda..10ed611cb2c6 100644
--- a/internal/typeexpr/get_type.go
+++ b/internal/typeexpr/get_type.go
@@ -5,49 +5,52 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
+ "github.com/zclconf/go-cty/cty/convert"
)
const invalidTypeSummary = "Invalid type specification"
-// getType is the internal implementation of both Type and TypeConstraint,
-// using the passed flag to distinguish. When constraint is false, the "any"
-// keyword will produce an error.
-func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
+// getType is the internal implementation of Type, TypeConstraint, and
+// TypeConstraintWithDefaults, using the passed flags to distinguish. When
+// `constraint` is true, the "any" keyword can be used in place of a concrete
+// type. When `withDefaults` is true, the "optional" call expression supports
+// an additional argument describing a default value.
+func getType(expr hcl.Expression, constraint, withDefaults bool) (cty.Type, *Defaults, hcl.Diagnostics) {
// First we'll try for one of our keywords
kw := hcl.ExprAsKeyword(expr)
switch kw {
case "bool":
- return cty.Bool, nil
+ return cty.Bool, nil, nil
case "string":
- return cty.String, nil
+ return cty.String, nil, nil
case "number":
- return cty.Number, nil
+ return cty.Number, nil, nil
case "any":
if constraint {
- return cty.DynamicPseudoType, nil
+ return cty.DynamicPseudoType, nil, nil
}
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: fmt.Sprintf("The keyword %q cannot be used in this type specification: an exact type is required.", kw),
Subject: expr.Range().Ptr(),
}}
case "list", "map", "set":
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", kw),
Subject: expr.Range().Ptr(),
}}
case "object":
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: "The object type constructor requires one argument specifying the attribute types and values as a map.",
Subject: expr.Range().Ptr(),
}}
case "tuple":
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: "The tuple type constructor requires one argument specifying the element types as a list.",
@@ -56,7 +59,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
case "":
// okay! we'll fall through and try processing as a call, then.
default:
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: fmt.Sprintf("The keyword %q is not a valid type specification.", kw),
@@ -68,7 +71,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
// try to process it as a call instead.
call, diags := hcl.ExprCall(expr)
if diags.HasErrors() {
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: "A type specification is either a primitive type keyword (bool, number, string) or a complex type constructor call, like list(string).",
@@ -78,14 +81,14 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
switch call.Name {
case "bool", "string", "number":
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: fmt.Sprintf("Primitive type keyword %q does not expect arguments.", call.Name),
Subject: &call.ArgsRange,
}}
case "any":
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: fmt.Sprintf("Type constraint keyword %q does not expect arguments.", call.Name),
@@ -105,7 +108,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
switch call.Name {
case "list", "set", "map":
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", call.Name),
@@ -113,7 +116,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
Context: &contextRange,
}}
case "object":
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: "The object type constructor requires one argument specifying the attribute types and values as a map.",
@@ -121,7 +124,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
Context: &contextRange,
}}
case "tuple":
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: "The tuple type constructor requires one argument specifying the element types as a list.",
@@ -134,18 +137,21 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
switch call.Name {
case "list":
- ety, diags := getType(call.Arguments[0], constraint)
- return cty.List(ety), diags
+ ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults)
+ ty := cty.List(ety)
+ return ty, collectionDefaults(ty, defaults), diags
case "set":
- ety, diags := getType(call.Arguments[0], constraint)
- return cty.Set(ety), diags
+ ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults)
+ ty := cty.Set(ety)
+ return ty, collectionDefaults(ty, defaults), diags
case "map":
- ety, diags := getType(call.Arguments[0], constraint)
- return cty.Map(ety), diags
+ ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults)
+ ty := cty.Map(ety)
+ return ty, collectionDefaults(ty, defaults), diags
case "object":
attrDefs, diags := hcl.ExprMap(call.Arguments[0])
if diags.HasErrors() {
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: "Object type constructor requires a map whose keys are attribute names and whose values are the corresponding attribute types.",
@@ -155,6 +161,8 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
}
atys := make(map[string]cty.Type)
+ defaultValues := make(map[string]cty.Value)
+ children := make(map[string]*Defaults)
var optAttrs []string
for _, attrDef := range attrDefs {
attrName := hcl.ExprAsKeyword(attrDef.Key)
@@ -174,6 +182,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
// modifier optional(...) to indicate an optional attribute. If
// so, we'll unwrap that first and make a note about it being
// optional for when we construct the type below.
+ var defaultExpr hcl.Expression
if call, callDiags := hcl.ExprCall(atyExpr); !callDiags.HasErrors() {
if call.Name == "optional" {
if len(call.Arguments) < 1 {
@@ -187,16 +196,40 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
continue
}
if constraint {
- if len(call.Arguments) > 1 {
- diags = append(diags, &hcl.Diagnostic{
- Severity: hcl.DiagError,
- Summary: invalidTypeSummary,
- Detail: "Optional attribute modifier expects only one argument: the attribute type.",
- Subject: call.ArgsRange.Ptr(),
- Context: atyExpr.Range().Ptr(),
- })
+ if withDefaults {
+ switch len(call.Arguments) {
+ case 2:
+ defaultExpr = call.Arguments[1]
+ defaultVal, defaultDiags := defaultExpr.Value(nil)
+ diags = append(diags, defaultDiags...)
+ if !defaultDiags.HasErrors() {
+ optAttrs = append(optAttrs, attrName)
+ defaultValues[attrName] = defaultVal
+ }
+ case 1:
+ optAttrs = append(optAttrs, attrName)
+ default:
+ diags = append(diags, &hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: invalidTypeSummary,
+ Detail: "Optional attribute modifier expects at most two arguments: the attribute type, and a default value.",
+ Subject: call.ArgsRange.Ptr(),
+ Context: atyExpr.Range().Ptr(),
+ })
+ }
+ } else {
+ if len(call.Arguments) == 1 {
+ optAttrs = append(optAttrs, attrName)
+ } else {
+ diags = append(diags, &hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: invalidTypeSummary,
+ Detail: "Optional attribute modifier expects only one argument: the attribute type.",
+ Subject: call.ArgsRange.Ptr(),
+ Context: atyExpr.Range().Ptr(),
+ })
+ }
}
- optAttrs = append(optAttrs, attrName)
} else {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
@@ -210,19 +243,39 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
}
}
- aty, attrDiags := getType(atyExpr, constraint)
+ aty, aDefaults, attrDiags := getType(atyExpr, constraint, withDefaults)
diags = append(diags, attrDiags...)
+
+ // If a default is set for an optional attribute, verify that it is
+ // convertible to the attribute type.
+ if defaultVal, ok := defaultValues[attrName]; ok {
+ _, err := convert.Convert(defaultVal, aty)
+ if err != nil {
+ diags = append(diags, &hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: "Invalid default value for optional attribute",
+ Detail: fmt.Sprintf("This default value is not compatible with the attribute's type constraint: %s.", err),
+ Subject: defaultExpr.Range().Ptr(),
+ })
+ delete(defaultValues, attrName)
+ }
+ }
+
atys[attrName] = aty
+ if aDefaults != nil {
+ children[attrName] = aDefaults
+ }
}
// NOTE: ObjectWithOptionalAttrs is experimental in cty at the
// time of writing, so this interface might change even in future
// minor versions of cty. We're accepting that because Terraform
// itself is considering optional attributes as experimental right now.
- return cty.ObjectWithOptionalAttrs(atys, optAttrs), diags
+ ty := cty.ObjectWithOptionalAttrs(atys, optAttrs)
+ return ty, structuredDefaults(ty, defaultValues, children), diags
case "tuple":
elemDefs, diags := hcl.ExprList(call.Arguments[0])
if diags.HasErrors() {
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: "Tuple type constructor requires a list of element types.",
@@ -231,14 +284,19 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
}}
}
etys := make([]cty.Type, len(elemDefs))
+ children := make(map[string]*Defaults, len(elemDefs))
for i, defExpr := range elemDefs {
- ety, elemDiags := getType(defExpr, constraint)
+ ety, elemDefaults, elemDiags := getType(defExpr, constraint, withDefaults)
diags = append(diags, elemDiags...)
etys[i] = ety
+ if elemDefaults != nil {
+ children[fmt.Sprintf("%d", i)] = elemDefaults
+ }
}
- return cty.Tuple(etys), diags
+ ty := cty.Tuple(etys)
+ return ty, structuredDefaults(ty, nil, children), diags
case "optional":
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: fmt.Sprintf("Keyword %q is valid only as a modifier for object type attributes.", call.Name),
@@ -247,7 +305,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
default:
// Can't access call.Arguments in this path because we've not validated
// that it contains exactly one expression here.
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: fmt.Sprintf("Keyword %q is not a valid type constructor.", call.Name),
@@ -255,3 +313,33 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
}}
}
}
+
+func collectionDefaults(ty cty.Type, defaults *Defaults) *Defaults {
+ if defaults == nil {
+ return nil
+ }
+ return &Defaults{
+ Type: ty,
+ Children: map[string]*Defaults{
+ "": defaults,
+ },
+ }
+}
+
+func structuredDefaults(ty cty.Type, defaultValues map[string]cty.Value, children map[string]*Defaults) *Defaults {
+ if len(defaultValues) == 0 && len(children) == 0 {
+ return nil
+ }
+
+ defaults := &Defaults{
+ Type: ty,
+ }
+ if len(defaultValues) > 0 {
+ defaults.DefaultValues = defaultValues
+ }
+ if len(children) > 0 {
+ defaults.Children = children
+ }
+
+ return defaults
+}
diff --git a/internal/typeexpr/get_type_test.go b/internal/typeexpr/get_type_test.go
index e46dca3ff988..2dca23d27e41 100644
--- a/internal/typeexpr/get_type_test.go
+++ b/internal/typeexpr/get_type_test.go
@@ -6,12 +6,17 @@ import (
"github.com/hashicorp/hcl/v2/gohcl"
+ "github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/json"
"github.com/zclconf/go-cty/cty"
)
+var (
+ typeComparer = cmp.Comparer(cty.Type.Equals)
+)
+
func TestGetType(t *testing.T) {
tests := []struct {
Source string
@@ -284,6 +289,23 @@ func TestGetType(t *testing.T) {
}),
`Optional attribute modifier is only for type constraints, not for exact types.`,
},
+ {
+ `object({name=string,meta=optional()})`,
+ true,
+ cty.Object(map[string]cty.Type{
+ "name": cty.String,
+ }),
+ `Optional attribute modifier requires the attribute type as its argument.`,
+ },
+ {
+ `object({name=string,meta=optional(string, "hello")})`,
+ true,
+ cty.Object(map[string]cty.Type{
+ "name": cty.String,
+ "meta": cty.String,
+ }),
+ `Optional attribute modifier expects only one argument: the attribute type.`,
+ },
{
`optional(string)`,
false,
@@ -305,7 +327,7 @@ func TestGetType(t *testing.T) {
t.Fatalf("failed to parse: %s", diags)
}
- got, diags := getType(expr, test.Constraint)
+ got, _, diags := getType(expr, test.Constraint, false)
if test.WantError == "" {
for _, diag := range diags {
t.Error(diag)
@@ -377,7 +399,7 @@ func TestGetTypeJSON(t *testing.T) {
t.Fatalf("failed to decode: %s", diags)
}
- got, diags := getType(content.Expr, test.Constraint)
+ got, _, diags := getType(content.Expr, test.Constraint, false)
if test.WantError == "" {
for _, diag := range diags {
t.Error(diag)
@@ -401,3 +423,247 @@ func TestGetTypeJSON(t *testing.T) {
})
}
}
+
+func TestGetTypeDefaults(t *testing.T) {
+ tests := []struct {
+ Source string
+ Want *Defaults
+ WantError string
+ }{
+ // primitive types have nil defaults
+ {
+ `bool`,
+ nil,
+ "",
+ },
+ {
+ `number`,
+ nil,
+ "",
+ },
+ {
+ `string`,
+ nil,
+ "",
+ },
+ {
+ `any`,
+ nil,
+ "",
+ },
+
+ // complex structures with no defaults have nil defaults
+ {
+ `map(string)`,
+ nil,
+ "",
+ },
+ {
+ `set(number)`,
+ nil,
+ "",
+ },
+ {
+ `tuple([number, string])`,
+ nil,
+ "",
+ },
+ {
+ `object({ a = string, b = number })`,
+ nil,
+ "",
+ },
+ {
+ `map(list(object({ a = string, b = optional(number) })))`,
+ nil,
+ "",
+ },
+
+ // object optional attribute with defaults
+ {
+ `object({ a = string, b = optional(number, 5) })`,
+ &Defaults{
+ Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"b"}),
+ DefaultValues: map[string]cty.Value{
+ "b": cty.NumberIntVal(5),
+ },
+ },
+ "",
+ },
+
+ // nested defaults
+ {
+ `object({ a = optional(object({ b = optional(number, 5) }), {}) })`,
+ &Defaults{
+ Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "b": cty.Number,
+ }, []string{"b"}),
+ }, []string{"a"}),
+ DefaultValues: map[string]cty.Value{
+ "a": cty.EmptyObjectVal,
+ },
+ Children: map[string]*Defaults{
+ "a": {
+ Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "b": cty.Number,
+ }, []string{"b"}),
+ DefaultValues: map[string]cty.Value{
+ "b": cty.NumberIntVal(5),
+ },
+ },
+ },
+ },
+ "",
+ },
+
+ // collections of objects with defaults
+ {
+ `map(object({ a = string, b = optional(number, 5) }))`,
+ &Defaults{
+ Type: cty.Map(cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"b"})),
+ Children: map[string]*Defaults{
+ "": {
+ Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"b"}),
+ DefaultValues: map[string]cty.Value{
+ "b": cty.NumberIntVal(5),
+ },
+ },
+ },
+ },
+ "",
+ },
+ {
+ `list(object({ a = string, b = optional(number, 5) }))`,
+ &Defaults{
+ Type: cty.List(cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"b"})),
+ Children: map[string]*Defaults{
+ "": {
+ Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"b"}),
+ DefaultValues: map[string]cty.Value{
+ "b": cty.NumberIntVal(5),
+ },
+ },
+ },
+ },
+ "",
+ },
+ {
+ `set(object({ a = string, b = optional(number, 5) }))`,
+ &Defaults{
+ Type: cty.Set(cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"b"})),
+ Children: map[string]*Defaults{
+ "": {
+ Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"b"}),
+ DefaultValues: map[string]cty.Value{
+ "b": cty.NumberIntVal(5),
+ },
+ },
+ },
+ },
+ "",
+ },
+
+ // tuples containing objects with defaults work differently from
+ // collections
+ {
+ `tuple([string, bool, object({ a = string, b = optional(number, 5) })])`,
+ &Defaults{
+ Type: cty.Tuple([]cty.Type{
+ cty.String,
+ cty.Bool,
+ cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"b"}),
+ }),
+ Children: map[string]*Defaults{
+ "2": {
+ Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"b"}),
+ DefaultValues: map[string]cty.Value{
+ "b": cty.NumberIntVal(5),
+ },
+ },
+ },
+ },
+ "",
+ },
+
+ // incompatible default value causes an error
+ {
+ `object({ a = optional(string, "hello"), b = optional(number, true) })`,
+ &Defaults{
+ Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"a", "b"}),
+ DefaultValues: map[string]cty.Value{
+ "a": cty.StringVal("hello"),
+ },
+ },
+ "This default value is not compatible with the attribute's type constraint: number required.",
+ },
+
+ // Too many arguments
+ {
+ `object({name=string,meta=optional(string, "hello", "world")})`,
+ nil,
+ `Optional attribute modifier expects at most two arguments: the attribute type, and a default value.`,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.Source, func(t *testing.T) {
+ expr, diags := hclsyntax.ParseExpression([]byte(test.Source), "", hcl.Pos{Line: 1, Column: 1})
+ if diags.HasErrors() {
+ t.Fatalf("failed to parse: %s", diags)
+ }
+
+ _, got, diags := getType(expr, true, true)
+ if test.WantError == "" {
+ for _, diag := range diags {
+ t.Error(diag)
+ }
+ } else {
+ found := false
+ for _, diag := range diags {
+ t.Log(diag)
+ if diag.Severity == hcl.DiagError && diag.Detail == test.WantError {
+ found = true
+ }
+ }
+ if !found {
+ t.Errorf("missing expected error detail message: %s", test.WantError)
+ }
+ }
+
+ if !cmp.Equal(test.Want, got, valueComparer, typeComparer) {
+ t.Errorf("wrong result\n%s", cmp.Diff(test.Want, got, valueComparer, typeComparer))
+ }
+ })
+ }
+}
diff --git a/internal/typeexpr/public.go b/internal/typeexpr/public.go
index 3b8f618fbcd1..82f215c0978f 100644
--- a/internal/typeexpr/public.go
+++ b/internal/typeexpr/public.go
@@ -15,7 +15,8 @@ import (
// successful, returns the resulting type. If unsuccessful, error diagnostics
// are returned.
func Type(expr hcl.Expression) (cty.Type, hcl.Diagnostics) {
- return getType(expr, false)
+ ty, _, diags := getType(expr, false, false)
+ return ty, diags
}
// TypeConstraint attempts to parse the given expression as a type constraint
@@ -26,7 +27,20 @@ func Type(expr hcl.Expression) (cty.Type, hcl.Diagnostics) {
// allows the keyword "any" to represent cty.DynamicPseudoType, which is often
// used as a wildcard in type checking and type conversion operations.
func TypeConstraint(expr hcl.Expression) (cty.Type, hcl.Diagnostics) {
- return getType(expr, true)
+ ty, _, diags := getType(expr, true, false)
+ return ty, diags
+}
+
+// TypeConstraintWithDefaults attempts to parse the given expression as a type
+// constraint which may include default values for object attributes. If
+// successful both the resulting type and corresponding defaults are returned.
+// If unsuccessful, error diagnostics are returned.
+//
+// When using this function, defaults should be applied to the input value
+// before type conversion, to ensure that objects with missing attributes have
+// default values populated.
+func TypeConstraintWithDefaults(expr hcl.Expression) (cty.Type, *Defaults, hcl.Diagnostics) {
+ return getType(expr, true, true)
}
// TypeString returns a string rendering of the given type as it would be
diff --git a/website/data/language-nav-data.json b/website/data/language-nav-data.json
index acde5fa6a916..8771f9e298be 100644
--- a/website/data/language-nav-data.json
+++ b/website/data/language-nav-data.json
@@ -684,10 +684,6 @@
"title": "Type Conversion Functions",
"routes": [
{ "title": "can
", "href": "/language/functions/can" },
- {
- "title": "defaults
",
- "href": "/language/functions/defaults"
- },
{
"title": "nonsensitive
",
"href": "/language/functions/nonsensitive"
@@ -777,7 +773,6 @@
{ "title": "concat", "path": "functions/concat", "hidden": true },
{ "title": "contains", "path": "functions/contains", "hidden": true },
{ "title": "csvdecode", "path": "functions/csvdecode", "hidden": true },
- { "title": "defaults", "path": "functions/defaults", "hidden": true },
{ "title": "dirname", "path": "functions/dirname", "hidden": true },
{ "title": "distinct", "path": "functions/distinct", "hidden": true },
{ "title": "element", "path": "functions/element", "hidden": true },
diff --git a/website/docs/language/expressions/type-constraints.mdx b/website/docs/language/expressions/type-constraints.mdx
index 9ff803b81bb3..5925b88472de 100644
--- a/website/docs/language/expressions/type-constraints.mdx
+++ b/website/docs/language/expressions/type-constraints.mdx
@@ -258,10 +258,10 @@ variable "no_type_constraint" {
In this case, Terraform will replace `any` with the exact type of the given
value and thus perform no type conversion whatsoever.
-## Experimental: Optional Object Type Attributes
+## Optional Object Type Attributes
-From Terraform v0.14 there is _experimental_ support for marking particular
-attributes as optional in an object type constraint.
+Terraform v1.3 adds support for marking particular attributes as optional in an
+object type constraint.
To mark an attribute as optional, use the additional `optional(...)` modifier
around its type declaration:
@@ -269,17 +269,139 @@ around its type declaration:
```hcl
variable "with_optional_attribute" {
type = object({
- a = string # a required attribute
- b = optional(string) # an optional attribute
+ a = string # a required attribute
+ b = optional(string) # an optional attribute
+ c = optional(number, 127) # an optional attribute with default value
})
}
```
-By default, for required attributes, Terraform will return an error if the
-source value has no matching attribute. Marking an attribute as optional
-changes the behavior in that situation: Terraform will instead just silently
-insert `null` as the value of the attribute, allowing the receiving module
-to describe an appropriate fallback behavior.
+When evaluating variable values, Terraform will return an error if an object
+attribute specified in the variable type is not present in the given value.
+Marking an attribute as optional changes the behavior in that situation:
+Terraform will instead insert a default value for the missing attribute,
+allowing the receiving module to describe an appropriate fallback behavior.
+
+The `optional` modifier takes one or two arguments. The first argument
+specifies the type of the attribute, and (if given) the second attribute
+defines the default value to use if the attribute is not present. The default
+must be compatible with the attribute type. If no default is specified, a
+`null` value of the appropriate type will be used as the default.
+
+During evaluation, object attribute defaults are applied top-down in nested
+variable types. This means that a given attribute's default value will also
+have any nested default values applied to it later.
+
+### Example: Nested Structures with Optional Attributes and Defaults
+
+The following configuration defines a variable which describes a number of storage buckets, each of which is used to host a website. This variable type uses several optional attributes, one of which is itself an `object` type with optional attributes and defaults.
+
+```hcl
+terraform {
+ # Optional attributes are currently experimental.
+ experiments = [module_variable_optional_attrs]
+}
+
+variable "buckets" {
+ type = list(object({
+ name = string
+ enabled = optional(bool, true)
+ website = optional(object({
+ index_document = optional(string, "index.html")
+ error_document = optional(string, "error.html")
+ routing_rules = optional(string)
+ }), {})
+ }))
+}
+```
+
+To test this out, we can create a file `terraform.tfvars` to provide an example
+value for `var.buckets`:
+
+```hcl
+buckets = [
+ {
+ name = "production"
+ website = {
+ routing_rules = <<-EOT
+ [
+ {
+ "Condition" = { "KeyPrefixEquals": "img/" },
+ "Redirect" = { "ReplaceKeyPrefixWith": "images/" }
+ }
+ ]
+ EOT
+ }
+ },
+ {
+ name = "archived"
+ enabled = false
+ },
+ {
+ name = "docs"
+ website = {
+ index_document = "index.txt"
+ error_document = "error.txt"
+ }
+ },
+]
+```
+
+The intent here is to specify three bucket configurations:
+
+- `production` sets the routing rules to add a redirect;
+- `archived` uses default configuration but is disabled;
+- `docs` overrides the index and error documents to use text files.
+
+Note that `production` does not specify the index and error documents, and `archived` omits the website configuration altogether. Because our type specifies a default value for the `website` attribute as an empty object `{}`, Terraform fills in the defaults specified in the nested type.
+
+The resulting variable value is:
+
+```hcl
+tolist([
+ {
+ "enabled" = true
+ "name" = "production"
+ "website" = {
+ "error_document" = "error.html"
+ "index_document" = "index.html"
+ "routing_rules" = <<-EOT
+ [
+ {
+ "Condition" = { "KeyPrefixEquals": "img/" },
+ "Redirect" = { "ReplaceKeyPrefixWith": "images/" }
+ }
+ ]
+
+ EOT
+ }
+ },
+ {
+ "enabled" = false
+ "name" = "archived"
+ "website" = {
+ "error_document" = "error.html"
+ "index_document" = "index.html"
+ "routing_rules" = tostring(null)
+ }
+ },
+ {
+ "enabled" = true
+ "name" = "docs"
+ "website" = {
+ "error_document" = "error.txt"
+ "index_document" = "index.txt"
+ "routing_rules" = tostring(null)
+ }
+ },
+])
+```
+
+Here we can see that for `production` and `docs`, the `enabled` attribute has been filled in as `true`. The default values for the `website` attribute have also been filled in, with the values specified by `docs` overriding the defaults. For `archived`, the entire default `website` value is populated.
+
+One important point is that the `website` attribute for the `archived` and `docs` buckets contains a `null` value for `routing_rules`. When declaring a type constraint with an optional object attributes without a default, a value which omits that attribute will be populated with a `null` value, rather than continuing to omit the attribute in the final result.
+
+### Experimental Status
Because this feature is currently experimental, it requires an explicit
opt-in on a per-module basis. To use it, write a `terraform` block with the
diff --git a/website/docs/language/functions/defaults.mdx b/website/docs/language/functions/defaults.mdx
deleted file mode 100644
index 74c535280b01..000000000000
--- a/website/docs/language/functions/defaults.mdx
+++ /dev/null
@@ -1,198 +0,0 @@
----
-page_title: defaults - Functions - Configuration Language
-description: The defaults function can fill in default values in place of null values.
----
-
-# `defaults` Function
-
--> **Note:** This function is available only in Terraform 0.15 and later.
-
-~> **Experimental:** This function is part of
-[the optional attributes experiment](/language/expressions/type-constraints#experimental-optional-object-type-attributes)
-and is only available in modules where the `module_variable_optional_attrs`
-experiment is explicitly enabled.
-
-The `defaults` function is a specialized function intended for use with
-input variables whose type constraints are object types or collections of
-object types that include optional attributes.
-
-When you define an attribute as optional and the caller doesn't provide an
-explicit value for it, Terraform will set the attribute to `null` to represent
-that it was omitted. If you want to use a placeholder value other than `null`
-when an attribute isn't set, you can use the `defaults` function to concisely
-assign default values only where an attribute value was set to `null`.
-
-```
-defaults(input_value, defaults)
-```
-
-The `defaults` function expects that the `input_value` argument will be the
-value of an input variable with an exact [type constraint](/language/expressions/types)
-(not containing `any`). The function will then visit every attribute in
-the data structure, including attributes of nested objects, and apply the
-default values given in the defaults object.
-
-The interpretation of attributes in the `defaults` argument depends on what
-type an attribute has in the `input_value`:
-
-* **Primitive types** (`string`, `number`, `bool`): if a default value is given
- then it will be used only if the `input_value`'s attribute of the same
- name has the value `null`. The default value's type must match the input
- value's type.
-* **Structural types** (`object` and `tuple` types): Terraform will recursively
- visit all of the attributes or elements of the nested value and repeat the
- same defaults-merging logic one level deeper. The default value's type must
- be of the same kind as the input value's type, and a default value for an
- object type must only contain attribute names that appear in the input
- value's type.
-* **Collection types** (`list`, `map`, and `set` types): Terraform will visit
- each of the collection elements in turn and apply defaults to them. In this
- case the default value is only a single value to be applied to _all_ elements
- of the collection, so it must have a type compatible with the collection's
- element type rather than with the collection type itself.
-
-The above rules may be easier to follow with an example. Consider the following
-Terraform configuration:
-
-```hcl
-terraform {
- # Optional attributes and the defaults function are
- # both experimental, so we must opt in to the experiment.
- experiments = [module_variable_optional_attrs]
-}
-
-variable "storage" {
- type = object({
- name = string
- enabled = optional(bool)
- website = object({
- index_document = optional(string)
- error_document = optional(string)
- })
- documents = map(
- object({
- source_file = string
- content_type = optional(string)
- })
- )
- })
-}
-
-locals {
- storage = defaults(var.storage, {
- # If "enabled" isn't set then it will default
- # to true.
- enabled = true
-
- # The "website" attribute is required, but
- # it's here to provide defaults for the
- # optional attributes inside.
- website = {
- index_document = "index.html"
- error_document = "error.html"
- }
-
- # The "documents" attribute has a map type,
- # so the default value represents defaults
- # to be applied to all of the elements in
- # the map, not for the map itself. Therefore
- # it's a single object matching the map
- # element type, not a map itself.
- documents = {
- # If _any_ of the map elements omit
- # content_type then this default will be
- # used instead.
- content_type = "application/octet-stream"
- }
- })
-}
-
-output "storage" {
- value = local.storage
-}
-```
-
-To test this out, we can create a file `terraform.tfvars` to provide an example
-value for `var.storage`:
-
-```hcl
-storage = {
- name = "example"
-
- website = {
- error_document = "error.txt"
- }
- documents = {
- "index.html" = {
- source_file = "index.html.tmpl"
- content_type = "text/html"
- }
- "error.txt" = {
- source_file = "error.txt.tmpl"
- content_type = "text/plain"
- }
- "terraform.exe" = {
- source_file = "terraform.exe"
- }
- }
-}
-```
-
-The above value conforms to the variable's type constraint because it only
-omits attributes that are declared as optional. Terraform will automatically
-populate those attributes with the value `null` before evaluating anything
-else, and then the `defaults` function in `local.storage` will substitute
-default values for each of them.
-
-The result of this `defaults` call would therefore be the following object:
-
-```
-storage = {
- "documents" = tomap({
- "error.txt" = {
- "content_type" = "text/plain"
- "source_file" = "error.txt.tmpl"
- }
- "index.html" = {
- "content_type" = "text/html"
- "source_file" = "index.html.tmpl"
- }
- "terraform.exe" = {
- "content_type" = "application/octet-stream"
- "source_file" = "terraform.exe"
- }
- })
- "enabled" = true
- "name" = "example"
- "website" = {
- "error_document" = "error.txt"
- "index_document" = "index.html"
- }
-}
-```
-
-Notice that `enabled` and `website.index_document` were both populated directly
-from the defaults. Notice also that the `"terraform.exe"` element of
-`documents` had its `content_type` attribute populated from the `documents`
-default, but the default value didn't need to predict that there would be an
-element key `"terraform.exe"` because the default values apply equally to
-all elements of the map where the optional attributes are `null`.
-
-## Using `defaults` elsewhere
-
-The design of the `defaults` function depends on input values having
-well-specified type constraints, so it can reliably recognize the difference
-between similar types: maps vs. objects, lists vs. tuples. The type constraint
-causes Terraform to convert the caller's value to conform to the constraint
-and thus `defaults` can rely on the input to conform.
-
-Elsewhere in the Terraform language it's typical to be less precise about
-types, for example using the object construction syntax `{ ... }` to construct
-values that will be used as if they are maps. Because `defaults` uses the
-type information of `input_value`, an `input_value` that _doesn't_ originate
-in an input variable will tend not to have an appropriate value type and will
-thus not be interpreted as expected by `defaults`.
-
-We recommend using `defaults` only with fully-constrained input variable values
-in the first argument, so you can use the variable's type constraint to
-explicitly distinguish between collection and structural types.
diff --git a/website/layouts/language.erb b/website/layouts/language.erb
index b2233530f218..7f7e55e7f3df 100644
--- a/website/layouts/language.erb
+++ b/website/layouts/language.erb
@@ -792,10 +792,6 @@
can
-