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 -
  • - defaults -
  • -
  • nonsensitive