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..fcb816e5c244 100644
--- a/internal/lang/functions.go
+++ b/internal/lang/functions.go
@@ -1,15 +1,12 @@
package lang
import (
- "fmt"
-
"github.com/hashicorp/hcl/v2/ext/tryfunc"
ctyyaml "github.com/zclconf/go-cty-yaml"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/function/stdlib"
- "github.com/hashicorp/terraform/internal/experiments"
"github.com/hashicorp/terraform/internal/lang/funcs"
)
@@ -56,7 +53,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,
@@ -169,32 +165,3 @@ func (s *Scope) Functions() map[string]function.Function {
return s.funcs
}
-
-// experimentalFunction checks whether the given experiment is enabled for
-// 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.
-func (s *Scope) experimentalFunction(experiment experiments.Experiment, fn function.Function) function.Function {
- if s.activeExperiments.Has(experiment) {
- return fn
- }
-
- err := fmt.Errorf(
- "this function is experimental and available only when the experiment keyword %s is enabled for the current module",
- experiment.Keyword(),
- )
-
- return function.New(&function.Spec{
- Params: fn.Params(),
- VarParam: fn.VarParam(),
- Type: func(args []cty.Value) (cty.Type, error) {
- return cty.DynamicPseudoType, err
- },
- Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
- // It would be weird to get here because the Type function always
- // fails, but we'll return an error here too anyway just to be
- // robust.
- return cty.DynamicVal, err
- },
- })
-}
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/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/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
-