Merge pull request #26766 from hashicorp/f-experimental-funcs
lang/funcs: Experimental "defaults" function
This commit is contained in:
commit
ca4b860902
|
@ -0,0 +1,257 @@
|
||||||
|
package funcs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "defaults",
|
||||||
|
Type: cty.DynamicPseudoType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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 {
|
||||||
|
const fallbackArgIdx = 1
|
||||||
|
|
||||||
|
wantTy := input.Type()
|
||||||
|
if !(input.IsKnown() && fallback.IsKnown()) {
|
||||||
|
return cty.UnknownVal(wantTy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 !input.IsNull() {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
v, err := convert.Convert(fallback, wantTy)
|
||||||
|
if err != nil {
|
||||||
|
// Should not happen because we checked in defaultsAssertSuitableFallback
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
|
||||||
|
case wantTy.IsObjectType():
|
||||||
|
atys := wantTy.AttributeTypes()
|
||||||
|
ret := map[string]cty.Value{}
|
||||||
|
for attr, aty := range atys {
|
||||||
|
inputSub := input.GetAttr(attr)
|
||||||
|
fallbackSub := cty.NullVal(aty)
|
||||||
|
if fallback.Type().HasAttribute(attr) {
|
||||||
|
fallbackSub = fallback.GetAttr(attr)
|
||||||
|
}
|
||||||
|
ret[attr] = defaultsApply(inputSub, fallbackSub)
|
||||||
|
}
|
||||||
|
return cty.ObjectVal(ret)
|
||||||
|
|
||||||
|
case wantTy.IsTupleType():
|
||||||
|
l := wantTy.Length()
|
||||||
|
ret := make([]cty.Value, l)
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
inputSub := input.Index(cty.NumberIntVal(int64(i)))
|
||||||
|
fallbackSub := fallback.Index(cty.NumberIntVal(int64(i)))
|
||||||
|
ret[i] = defaultsApply(inputSub, fallbackSub)
|
||||||
|
}
|
||||||
|
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{}
|
||||||
|
|
||||||
|
for it := input.ElementIterator(); it.Next(); {
|
||||||
|
k, v := it.Element()
|
||||||
|
newVals[k.AsString()] = defaultsApply(v, fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newVals) == 0 {
|
||||||
|
return cty.MapValEmpty(ety)
|
||||||
|
}
|
||||||
|
return cty.MapVal(newVals)
|
||||||
|
case wantTy.IsListType(), wantTy.IsSetType():
|
||||||
|
var newVals []cty.Value
|
||||||
|
|
||||||
|
for it := input.ElementIterator(); it.Next(); {
|
||||||
|
_, v := it.Element()
|
||||||
|
newV := defaultsApply(v, fallback)
|
||||||
|
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.GetConversionUnsafe(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})
|
||||||
|
}
|
|
@ -0,0 +1,396 @@
|
||||||
|
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
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
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`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/zclconf/go-cty/cty/function"
|
"github.com/zclconf/go-cty/cty/function"
|
||||||
"github.com/zclconf/go-cty/cty/function/stdlib"
|
"github.com/zclconf/go-cty/cty/function/stdlib"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/experiments"
|
||||||
"github.com/hashicorp/terraform/lang/funcs"
|
"github.com/hashicorp/terraform/lang/funcs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -55,6 +56,7 @@ func (s *Scope) Functions() map[string]function.Function {
|
||||||
"concat": stdlib.ConcatFunc,
|
"concat": stdlib.ConcatFunc,
|
||||||
"contains": stdlib.ContainsFunc,
|
"contains": stdlib.ContainsFunc,
|
||||||
"csvdecode": stdlib.CSVDecodeFunc,
|
"csvdecode": stdlib.CSVDecodeFunc,
|
||||||
|
"defaults": s.experimentalFunction(experiments.ModuleVariableOptionalAttrs, funcs.DefaultsFunc),
|
||||||
"dirname": funcs.DirnameFunc,
|
"dirname": funcs.DirnameFunc,
|
||||||
"distinct": stdlib.DistinctFunc,
|
"distinct": stdlib.DistinctFunc,
|
||||||
"element": stdlib.ElementFunc,
|
"element": stdlib.ElementFunc,
|
||||||
|
@ -168,3 +170,32 @@ var unimplFunc = function.New(&function.Spec{
|
||||||
return cty.DynamicVal, fmt.Errorf("function not yet implemented")
|
return cty.DynamicVal, fmt.Errorf("function not yet implemented")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 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
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||||
|
"github.com/hashicorp/terraform/experiments"
|
||||||
homedir "github.com/mitchellh/go-homedir"
|
homedir "github.com/mitchellh/go-homedir"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
)
|
)
|
||||||
|
@ -289,6 +290,18 @@ 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": {
|
||||||
{
|
{
|
||||||
`dirname("testdata/hello.txt")`,
|
`dirname("testdata/hello.txt")`,
|
||||||
|
@ -1039,32 +1052,89 @@ func TestFunctions(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
data := &dataForTests{} // no variables available; we only need literals here
|
experimentalFuncs := map[string]experiments.Experiment{}
|
||||||
scope := &Scope{
|
experimentalFuncs["defaults"] = experiments.ModuleVariableOptionalAttrs
|
||||||
Data: data,
|
|
||||||
BaseDir: "./testdata/functions-test", // for the functions that read from the filesystem
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that there is at least one test case for each function, omitting
|
t.Run("all functions are tested", func(t *testing.T) {
|
||||||
// those functions that do not return consistent values
|
data := &dataForTests{} // no variables available; we only need literals here
|
||||||
allFunctions := scope.Functions()
|
scope := &Scope{
|
||||||
|
Data: data,
|
||||||
// TODO: we can test the impure functions partially by configuring the scope
|
BaseDir: "./testdata/functions-test", // for the functions that read from the filesystem
|
||||||
// with PureOnly: true and then verify that they return unknown values of a
|
|
||||||
// suitable type.
|
|
||||||
for _, impureFunc := range impureFunctions {
|
|
||||||
delete(allFunctions, impureFunc)
|
|
||||||
}
|
|
||||||
for f, _ := range scope.Functions() {
|
|
||||||
if _, ok := tests[f]; !ok {
|
|
||||||
t.Errorf("Missing test for function %s\n", f)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Check that there is at least one test case for each function, omitting
|
||||||
|
// those functions that do not return consistent values
|
||||||
|
allFunctions := scope.Functions()
|
||||||
|
|
||||||
|
// TODO: we can test the impure functions partially by configuring the scope
|
||||||
|
// with PureOnly: true and then verify that they return unknown values of a
|
||||||
|
// suitable type.
|
||||||
|
for _, impureFunc := range impureFunctions {
|
||||||
|
delete(allFunctions, impureFunc)
|
||||||
|
}
|
||||||
|
for f := range scope.Functions() {
|
||||||
|
if _, ok := tests[f]; !ok {
|
||||||
|
t.Errorf("Missing test for function %s\n", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
for funcName, funcTests := range tests {
|
for funcName, funcTests := range tests {
|
||||||
t.Run(funcName, func(t *testing.T) {
|
t.Run(funcName, func(t *testing.T) {
|
||||||
|
|
||||||
|
// prepareScope starts as a no-op, but if a function is marked as
|
||||||
|
// experimental in our experimentalFuncs table above then we'll
|
||||||
|
// reassign this to be a function that activates the appropriate
|
||||||
|
// experiment.
|
||||||
|
prepareScope := func(t *testing.T, scope *Scope) {}
|
||||||
|
|
||||||
|
if experiment, isExperimental := experimentalFuncs[funcName]; isExperimental {
|
||||||
|
// First, we'll run all of the tests without the experiment
|
||||||
|
// enabled to see that they do actually fail in that case.
|
||||||
|
for _, test := range funcTests {
|
||||||
|
testName := fmt.Sprintf("experimental(%s)", test.src)
|
||||||
|
t.Run(testName, func(t *testing.T) {
|
||||||
|
data := &dataForTests{} // no variables available; we only need literals here
|
||||||
|
scope := &Scope{
|
||||||
|
Data: data,
|
||||||
|
BaseDir: "./testdata/functions-test", // for the functions that read from the filesystem
|
||||||
|
}
|
||||||
|
|
||||||
|
expr, parseDiags := hclsyntax.ParseExpression([]byte(test.src), "test.hcl", hcl.Pos{Line: 1, Column: 1})
|
||||||
|
if parseDiags.HasErrors() {
|
||||||
|
for _, diag := range parseDiags {
|
||||||
|
t.Error(diag.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, diags := scope.EvalExpr(expr, cty.DynamicPseudoType)
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Errorf("experimental function %q succeeded without its experiment %s enabled\nexpr: %s", funcName, experiment.Keyword(), test.src)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now make the experiment active in the scope so that the
|
||||||
|
// function will actually work when we test it below.
|
||||||
|
prepareScope = func(t *testing.T, scope *Scope) {
|
||||||
|
t.Helper()
|
||||||
|
t.Logf("activating experiment %s to test %q", experiment.Keyword(), funcName)
|
||||||
|
experimentsSet := experiments.NewSet()
|
||||||
|
experimentsSet.Add(experiment)
|
||||||
|
scope.SetActiveExperiments(experimentsSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, test := range funcTests {
|
for _, test := range funcTests {
|
||||||
t.Run(test.src, func(t *testing.T) {
|
t.Run(test.src, func(t *testing.T) {
|
||||||
|
data := &dataForTests{} // no variables available; we only need literals here
|
||||||
|
scope := &Scope{
|
||||||
|
Data: data,
|
||||||
|
BaseDir: "./testdata/functions-test", // for the functions that read from the filesystem
|
||||||
|
}
|
||||||
|
prepareScope(t, scope)
|
||||||
|
|
||||||
expr, parseDiags := hclsyntax.ParseExpression([]byte(test.src), "test.hcl", hcl.Pos{Line: 1, Column: 1})
|
expr, parseDiags := hclsyntax.ParseExpression([]byte(test.src), "test.hcl", hcl.Pos{Line: 1, Column: 1})
|
||||||
if parseDiags.HasErrors() {
|
if parseDiags.HasErrors() {
|
||||||
for _, diag := range parseDiags {
|
for _, diag := range parseDiags {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/zclconf/go-cty/cty/function"
|
"github.com/zclconf/go-cty/cty/function"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/addrs"
|
"github.com/hashicorp/terraform/addrs"
|
||||||
|
"github.com/hashicorp/terraform/experiments"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Scope is the main type in this package, allowing dynamic evaluation of
|
// Scope is the main type in this package, allowing dynamic evaluation of
|
||||||
|
@ -31,4 +32,16 @@ type Scope struct {
|
||||||
|
|
||||||
funcs map[string]function.Function
|
funcs map[string]function.Function
|
||||||
funcsLock sync.Mutex
|
funcsLock sync.Mutex
|
||||||
|
|
||||||
|
// activeExperiments is an optional set of experiments that should be
|
||||||
|
// considered as active in the module that this scope will be used for.
|
||||||
|
// Callers can populate it by calling the SetActiveExperiments method.
|
||||||
|
activeExperiments experiments.Set
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetActiveExperiments allows a caller to declare that a set of experiments
|
||||||
|
// is active for the module that the receiving Scope belongs to, which might
|
||||||
|
// then cause the scope to activate some additional experimental behaviors.
|
||||||
|
func (s *Scope) SetActiveExperiments(active experiments.Set) {
|
||||||
|
s.activeExperiments = active
|
||||||
}
|
}
|
||||||
|
|
|
@ -297,7 +297,19 @@ func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, keyData
|
||||||
InstanceKeyData: keyData,
|
InstanceKeyData: keyData,
|
||||||
Operation: ctx.Evaluator.Operation,
|
Operation: ctx.Evaluator.Operation,
|
||||||
}
|
}
|
||||||
return ctx.Evaluator.Scope(data, self)
|
scope := ctx.Evaluator.Scope(data, self)
|
||||||
|
|
||||||
|
// ctx.PathValue is the path of the module that contains whatever
|
||||||
|
// expression the caller will be trying to evaluate, so this will
|
||||||
|
// activate only the experiments from that particular module, to
|
||||||
|
// be consistent with how experiment checking in the "configs"
|
||||||
|
// package itself works. The nil check here is for robustness in
|
||||||
|
// incompletely-mocked testing situations; mc should never be nil in
|
||||||
|
// real situations.
|
||||||
|
if mc := ctx.Evaluator.Config.DescendentForInstance(ctx.PathValue); mc != nil {
|
||||||
|
scope.SetActiveExperiments(mc.Module.ActiveExperiments)
|
||||||
|
}
|
||||||
|
return scope
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *BuiltinEvalContext) Path() addrs.ModuleInstance {
|
func (ctx *BuiltinEvalContext) Path() addrs.ModuleInstance {
|
||||||
|
|
|
@ -0,0 +1,201 @@
|
||||||
|
---
|
||||||
|
layout: "functions"
|
||||||
|
page_title: "defaults - Functions - Configuration Language"
|
||||||
|
sidebar_current: "docs-funcs-conversion-defaults"
|
||||||
|
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](../types.html#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](../types.html)
|
||||||
|
(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.
|
|
@ -739,6 +739,10 @@
|
||||||
<a href="/docs/configuration/functions/can.html">can</a>
|
<a href="/docs/configuration/functions/can.html">can</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="/docs/configuration/functions/defaults.html">defaults</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="/docs/configuration/functions/tobool.html">tobool</a>
|
<a href="/docs/configuration/functions/tobool.html">tobool</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
Loading…
Reference in New Issue