terraform/internal/lang/funcs/collection.go

710 lines
21 KiB
Go
Raw Normal View History

package funcs
import (
"errors"
"fmt"
"math/big"
2018-05-31 23:46:24 +02:00
"sort"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/function/stdlib"
lang/funcs: "one" function In the Terraform language we typically use lists of zero or one values in some sense interchangably with single values that might be null, because various Terraform language constructs are designed to work with collections rather than with nullable values. In Terraform v0.12 we made the splat operator [*] have a "special power" of concisely converting from a possibly-null single value into a zero-or-one list as a way to make that common operation more concise. In a sense this "one" function is the opposite operation to that special power: it goes from a zero-or-one collection (list, set, or tuple) to a possibly-null single value. This is a concise alternative to the following clunky conditional expression, with the additional benefit that the following expression is also not viable for set values, and it also properly handles the case where there's unexpectedly more than one value: length(var.foo) != 0 ? var.foo[0] : null Instead, we can write: one(var.foo) As with the splat operator, this is a tricky tradeoff because it could be argued that it's not something that'd be immediately intuitive to someone unfamiliar with Terraform. However, I think that's justified given how often zero-or-one collections arise in typical Terraform configurations. Unlike the splat operator, it should at least be easier to search for its name and find its documentation the first time you see it in a configuration. My expectation that this will become a common pattern is also my justification for giving it a short, concise name. Arguably it could be better named something like "oneornull", but that's a pretty clunky name and I'm not convinced it really adds any clarity for someone who isn't already familiar with it.
2021-01-09 01:02:56 +01:00
"github.com/zclconf/go-cty/cty/gocty"
)
var LengthFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "value",
Type: cty.DynamicPseudoType,
AllowDynamicType: true,
AllowUnknown: true,
AllowMarked: true,
},
},
Type: func(args []cty.Value) (cty.Type, error) {
collTy := args[0].Type()
switch {
case collTy == cty.String || collTy.IsTupleType() || collTy.IsObjectType() || collTy.IsListType() || collTy.IsMapType() || collTy.IsSetType() || collTy == cty.DynamicPseudoType:
return cty.Number, nil
default:
return cty.Number, errors.New("argument must be a string, a collection type, or a structural type")
}
},
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
coll := args[0]
collTy := args[0].Type()
marks := coll.Marks()
switch {
case collTy == cty.DynamicPseudoType:
return cty.UnknownVal(cty.Number).WithMarks(marks), nil
case collTy.IsTupleType():
l := len(collTy.TupleElementTypes())
return cty.NumberIntVal(int64(l)).WithMarks(marks), nil
case collTy.IsObjectType():
l := len(collTy.AttributeTypes())
return cty.NumberIntVal(int64(l)).WithMarks(marks), nil
case collTy == cty.String:
// We'll delegate to the cty stdlib strlen function here, because
// it deals with all of the complexities of tokenizing unicode
// grapheme clusters.
return stdlib.Strlen(coll)
case collTy.IsListType() || collTy.IsSetType() || collTy.IsMapType():
return coll.Length(), nil
default:
// Should never happen, because of the checks in our Type func above
return cty.UnknownVal(cty.Number), errors.New("impossible value type for length(...)")
}
},
})
// AllTrueFunc constructs a function that returns true if all elements of the
// list are true. If the list is empty, return true.
var AllTrueFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.List(cty.Bool),
},
},
Type: function.StaticReturnType(cty.Bool),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
result := cty.True
for it := args[0].ElementIterator(); it.Next(); {
_, v := it.Element()
if !v.IsKnown() {
return cty.UnknownVal(cty.Bool), nil
}
if v.IsNull() {
return cty.False, nil
}
result = result.And(v)
if result.False() {
return cty.False, nil
}
}
return result, nil
},
})
// AnyTrueFunc constructs a function that returns true if any element of the
// list is true. If the list is empty, return false.
var AnyTrueFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.List(cty.Bool),
},
},
Type: function.StaticReturnType(cty.Bool),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
result := cty.False
var hasUnknown bool
for it := args[0].ElementIterator(); it.Next(); {
_, v := it.Element()
if !v.IsKnown() {
hasUnknown = true
continue
}
if v.IsNull() {
continue
}
result = result.Or(v)
if result.True() {
return cty.True, nil
}
}
if hasUnknown {
return cty.UnknownVal(cty.Bool), nil
}
return result, nil
},
})
// CoalesceFunc constructs a function that takes any number of arguments and
// returns the first one that isn't empty. This function was copied from go-cty
// stdlib and modified so that it returns the first *non-empty* non-null element
// from a sequence, instead of merely the first non-null.
var CoalesceFunc = function.New(&function.Spec{
Params: []function.Parameter{},
VarParam: &function.Parameter{
Name: "vals",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
Type: func(args []cty.Value) (ret cty.Type, err error) {
argTypes := make([]cty.Type, len(args))
for i, val := range args {
argTypes[i] = val.Type()
}
retType, _ := convert.UnifyUnsafe(argTypes)
if retType == cty.NilType {
return cty.NilType, errors.New("all arguments must have the same type")
}
return retType, nil
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
for _, argVal := range args {
// We already know this will succeed because of the checks in our Type func above
argVal, _ = convert.Convert(argVal, retType)
if !argVal.IsKnown() {
return cty.UnknownVal(retType), nil
}
if argVal.IsNull() {
continue
}
if retType == cty.String && argVal.RawEquals(cty.StringVal("")) {
continue
}
return argVal, nil
}
return cty.NilVal, errors.New("no non-null, non-empty-string arguments")
},
})
// IndexFunc constructs a function that finds the element index for a given value in a list.
2018-05-25 21:57:26 +02:00
var IndexFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
},
{
Name: "value",
Type: cty.DynamicPseudoType,
},
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
if !(args[0].Type().IsListType() || args[0].Type().IsTupleType()) {
return cty.NilVal, errors.New("argument must be a list or tuple")
}
if !args[0].IsKnown() {
return cty.UnknownVal(cty.Number), nil
}
2018-05-25 21:57:26 +02:00
if args[0].LengthInt() == 0 { // Easy path
return cty.NilVal, errors.New("cannot search an empty list")
2018-05-25 21:57:26 +02:00
}
for it := args[0].ElementIterator(); it.Next(); {
i, v := it.Element()
eq, err := stdlib.Equal(v, args[1])
if err != nil {
return cty.NilVal, err
}
if !eq.IsKnown() {
return cty.UnknownVal(cty.Number), nil
}
if eq.True() {
return i, nil
}
}
return cty.NilVal, errors.New("item not found")
2018-05-25 21:57:26 +02:00
},
})
// LookupFunc constructs a function that performs dynamic lookups of map types.
2018-05-31 18:33:43 +02:00
var LookupFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "inputMap",
Type: cty.DynamicPseudoType,
AllowMarked: true,
2018-05-31 18:33:43 +02:00
},
{
Name: "key",
Type: cty.String,
AllowMarked: true,
2018-05-31 18:33:43 +02:00
},
},
VarParam: &function.Parameter{
Name: "default",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
AllowMarked: true,
2018-05-31 18:33:43 +02:00
},
Type: func(args []cty.Value) (ret cty.Type, err error) {
2018-05-31 18:33:43 +02:00
if len(args) < 1 || len(args) > 3 {
return cty.NilType, fmt.Errorf("lookup() takes two or three arguments, got %d", len(args))
2018-05-31 18:33:43 +02:00
}
ty := args[0].Type()
2018-10-22 12:58:47 +02:00
switch {
case ty.IsObjectType():
2018-10-22 12:58:47 +02:00
if !args[1].IsKnown() {
2018-10-23 12:42:46 +02:00
return cty.DynamicPseudoType, nil
2018-10-22 12:58:47 +02:00
}
keyVal, _ := args[1].Unmark()
key := keyVal.AsString()
if ty.HasAttribute(key) {
return args[0].GetAttr(key).Type(), nil
} else if len(args) == 3 {
// if the key isn't found but a default is provided,
// return the default type
return args[2].Type(), nil
}
return cty.DynamicPseudoType, function.NewArgErrorf(0, "the given object has no attribute %q", key)
case ty.IsMapType():
if len(args) == 3 {
_, err = convert.Convert(args[2], ty.ElementType())
if err != nil {
return cty.NilType, function.NewArgErrorf(2, "the default value must have the same type as the map elements")
}
}
return ty.ElementType(), nil
default:
return cty.NilType, function.NewArgErrorf(0, "lookup() requires a map as the first argument")
}
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
var defaultVal cty.Value
2018-05-31 18:33:43 +02:00
defaultValueSet := false
if len(args) == 3 {
// intentionally leave default value marked
defaultVal = args[2]
2018-05-31 18:33:43 +02:00
defaultValueSet = true
}
// keep track of marks from the collection and key
var markses []cty.ValueMarks
// unmark collection, retain marks to reapply later
mapVar, mapMarks := args[0].Unmark()
markses = append(markses, mapMarks)
// include marks on the key in the result
keyVal, keyMarks := args[1].Unmark()
if len(keyMarks) > 0 {
markses = append(markses, keyMarks)
}
lookupKey := keyVal.AsString()
2018-05-31 18:33:43 +02:00
if !mapVar.IsKnown() {
return cty.UnknownVal(retType).WithMarks(markses...), nil
}
if mapVar.Type().IsObjectType() {
if mapVar.Type().HasAttribute(lookupKey) {
return mapVar.GetAttr(lookupKey).WithMarks(markses...), nil
}
} else if mapVar.HasIndex(cty.StringVal(lookupKey)) == cty.True {
return mapVar.Index(cty.StringVal(lookupKey)).WithMarks(markses...), nil
2018-05-31 18:33:43 +02:00
}
if defaultValueSet {
defaultVal, err = convert.Convert(defaultVal, retType)
if err != nil {
return cty.NilVal, err
}
return defaultVal.WithMarks(markses...), nil
2018-05-31 18:33:43 +02:00
}
return cty.UnknownVal(cty.DynamicPseudoType).WithMarks(markses...), fmt.Errorf(
2018-05-31 18:33:43 +02:00
"lookup failed to find '%s'", lookupKey)
},
})
// MatchkeysFunc constructs a function that constructs a new list by taking a
2018-05-29 22:58:32 +02:00
// subset of elements from one list whose indexes match the corresponding
// indexes of values in another list.
var MatchkeysFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "values",
Type: cty.List(cty.DynamicPseudoType),
},
{
Name: "keys",
Type: cty.List(cty.DynamicPseudoType),
},
{
Name: "searchset",
Type: cty.List(cty.DynamicPseudoType),
},
},
Type: func(args []cty.Value) (cty.Type, error) {
2019-06-04 17:54:26 +02:00
ty, _ := convert.UnifyUnsafe([]cty.Type{args[1].Type(), args[2].Type()})
if ty == cty.NilType {
return cty.NilType, errors.New("keys and searchset must be of the same type")
2018-05-29 22:58:32 +02:00
}
// the return type is based on args[0] (values)
2018-05-29 22:58:32 +02:00
return args[0].Type(), nil
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
if !args[0].IsKnown() {
return cty.UnknownVal(cty.List(retType.ElementType())), nil
}
2018-05-30 16:26:19 +02:00
if args[0].LengthInt() != args[1].LengthInt() {
return cty.ListValEmpty(retType.ElementType()), errors.New("length of keys and values should be equal")
2018-05-30 16:26:19 +02:00
}
2018-05-29 22:58:32 +02:00
output := make([]cty.Value, 0)
values := args[0]
// Keys and searchset must be the same type.
// We can skip error checking here because we've already verified that
// they can be unified in the Type function
2019-06-04 17:54:26 +02:00
ty, _ := convert.UnifyUnsafe([]cty.Type{args[1].Type(), args[2].Type()})
keys, _ := convert.Convert(args[1], ty)
searchset, _ := convert.Convert(args[2], ty)
2018-05-29 22:58:32 +02:00
// if searchset is empty, return an empty list.
if searchset.LengthInt() == 0 {
2018-05-30 16:26:19 +02:00
return cty.ListValEmpty(retType.ElementType()), nil
2018-05-29 22:58:32 +02:00
}
2018-06-07 19:51:57 +02:00
if !values.IsWhollyKnown() || !keys.IsWhollyKnown() {
return cty.UnknownVal(retType), nil
}
2018-05-29 22:58:32 +02:00
i := 0
for it := keys.ElementIterator(); it.Next(); {
_, key := it.Element()
for iter := searchset.ElementIterator(); iter.Next(); {
_, search := iter.Element()
eq, err := stdlib.Equal(key, search)
if err != nil {
return cty.NilVal, err
}
2018-05-30 16:26:19 +02:00
if !eq.IsKnown() {
return cty.ListValEmpty(retType.ElementType()), nil
}
2018-05-29 22:58:32 +02:00
if eq.True() {
v := values.Index(cty.NumberIntVal(int64(i)))
output = append(output, v)
break
}
}
i++
}
// if we haven't matched any key, then output is an empty list.
if len(output) == 0 {
2018-05-30 16:26:19 +02:00
return cty.ListValEmpty(retType.ElementType()), nil
2018-05-29 22:58:32 +02:00
}
return cty.ListVal(output), nil
},
})
lang/funcs: "one" function In the Terraform language we typically use lists of zero or one values in some sense interchangably with single values that might be null, because various Terraform language constructs are designed to work with collections rather than with nullable values. In Terraform v0.12 we made the splat operator [*] have a "special power" of concisely converting from a possibly-null single value into a zero-or-one list as a way to make that common operation more concise. In a sense this "one" function is the opposite operation to that special power: it goes from a zero-or-one collection (list, set, or tuple) to a possibly-null single value. This is a concise alternative to the following clunky conditional expression, with the additional benefit that the following expression is also not viable for set values, and it also properly handles the case where there's unexpectedly more than one value: length(var.foo) != 0 ? var.foo[0] : null Instead, we can write: one(var.foo) As with the splat operator, this is a tricky tradeoff because it could be argued that it's not something that'd be immediately intuitive to someone unfamiliar with Terraform. However, I think that's justified given how often zero-or-one collections arise in typical Terraform configurations. Unlike the splat operator, it should at least be easier to search for its name and find its documentation the first time you see it in a configuration. My expectation that this will become a common pattern is also my justification for giving it a short, concise name. Arguably it could be better named something like "oneornull", but that's a pretty clunky name and I'm not convinced it really adds any clarity for someone who isn't already familiar with it.
2021-01-09 01:02:56 +01:00
// OneFunc returns either the first element of a one-element list, or null
// if given a zero-element list.
var OneFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
},
},
Type: func(args []cty.Value) (cty.Type, error) {
ty := args[0].Type()
switch {
case ty.IsListType() || ty.IsSetType():
return ty.ElementType(), nil
case ty.IsTupleType():
etys := ty.TupleElementTypes()
switch len(etys) {
case 0:
// No specific type information, so we'll ultimately return
// a null value of unknown type.
return cty.DynamicPseudoType, nil
case 1:
return etys[0], nil
}
}
return cty.NilType, function.NewArgErrorf(0, "must be a list, set, or tuple value with either zero or one elements")
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
val := args[0]
ty := val.Type()
// Our parameter spec above doesn't set AllowUnknown or AllowNull,
// so we can assume our top-level collection is both known and non-null
// in here.
switch {
case ty.IsListType() || ty.IsSetType():
lenVal := val.Length()
if !lenVal.IsKnown() {
return cty.UnknownVal(retType), nil
}
var l int
err := gocty.FromCtyValue(lenVal, &l)
if err != nil {
// It would be very strange to get here, because that would
// suggest that the length is either not a number or isn't
// an integer, which would suggest a bug in cty.
return cty.NilVal, fmt.Errorf("invalid collection length: %s", err)
}
switch l {
case 0:
return cty.NullVal(retType), nil
case 1:
var ret cty.Value
// We'll use an iterator here because that works for both lists
// and sets, whereas indexing directly would only work for lists.
// Since we've just checked the length, we should only actually
// run this loop body once.
for it := val.ElementIterator(); it.Next(); {
_, ret = it.Element()
}
return ret, nil
}
case ty.IsTupleType():
etys := ty.TupleElementTypes()
switch len(etys) {
case 0:
return cty.NullVal(retType), nil
case 1:
ret := val.Index(cty.NumberIntVal(0))
return ret, nil
}
}
return cty.NilVal, function.NewArgErrorf(0, "must be a list, set, or tuple value with either zero or one elements")
},
})
// SumFunc constructs a function that returns the sum of all
// numbers provided in a list
var SumFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
},
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
if !args[0].CanIterateElements() {
return cty.NilVal, function.NewArgErrorf(0, "cannot sum noniterable")
}
if args[0].LengthInt() == 0 { // Easy path
return cty.NilVal, function.NewArgErrorf(0, "cannot sum an empty list")
}
arg := args[0].AsValueSlice()
ty := args[0].Type()
if !ty.IsListType() && !ty.IsSetType() && !ty.IsTupleType() {
return cty.NilVal, function.NewArgErrorf(0, fmt.Sprintf("argument must be list, set, or tuple. Received %s", ty.FriendlyName()))
}
if !args[0].IsWhollyKnown() {
return cty.UnknownVal(cty.Number), nil
}
// big.Float.Add can panic if the input values are opposing infinities,
// so we must catch that here in order to remain within
// the cty Function abstraction.
defer func() {
if r := recover(); r != nil {
if _, ok := r.(big.ErrNaN); ok {
ret = cty.NilVal
err = fmt.Errorf("can't compute sum of opposing infinities")
} else {
// not a panic we recognize
panic(r)
}
}
}()
s := arg[0]
if s.IsNull() {
return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values")
}
for _, v := range arg[1:] {
if v.IsNull() {
return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values")
}
v, err = convert.Convert(v, cty.Number)
if err != nil {
return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values")
}
s = s.Add(v)
}
return s, nil
},
})
// TransposeFunc constructs a function that takes a map of lists of strings and
2018-05-31 23:46:24 +02:00
// swaps the keys and values to produce a new map of lists of strings.
var TransposeFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "values",
Type: cty.Map(cty.List(cty.String)),
},
},
Type: function.StaticReturnType(cty.Map(cty.List(cty.String))),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
inputMap := args[0]
2018-06-01 00:49:15 +02:00
if !inputMap.IsWhollyKnown() {
return cty.UnknownVal(retType), nil
}
2018-05-31 23:46:24 +02:00
outputMap := make(map[string]cty.Value)
tmpMap := make(map[string][]string)
for it := inputMap.ElementIterator(); it.Next(); {
inKey, inVal := it.Element()
for iter := inVal.ElementIterator(); iter.Next(); {
_, val := iter.Element()
if !val.Type().Equals(cty.String) {
return cty.MapValEmpty(cty.List(cty.String)), errors.New("input must be a map of lists of strings")
2018-05-31 23:46:24 +02:00
}
outKey := val.AsString()
if _, ok := tmpMap[outKey]; !ok {
tmpMap[outKey] = make([]string, 0)
}
outVal := tmpMap[outKey]
outVal = append(outVal, inKey.AsString())
sort.Strings(outVal)
tmpMap[outKey] = outVal
}
}
for outKey, outVal := range tmpMap {
values := make([]cty.Value, 0)
for _, v := range outVal {
values = append(values, cty.StringVal(v))
}
outputMap[outKey] = cty.ListVal(values)
}
if len(outputMap) == 0 {
return cty.MapValEmpty(cty.List(cty.String)), nil
}
2018-05-31 23:46:24 +02:00
return cty.MapVal(outputMap), nil
},
})
lang/funcs: Remove the deprecated "list" and "map" functions Prior to Terraform 0.12 these two functions were the only way to construct literal lists and maps (respectively) in HIL expressions. Terraform 0.12, by switching to HCL 2, introduced first-class syntax for constructing tuple and object values, which can then be converted into list and map values using the tolist and tomap type conversion functions. We marked both of these functions as deprecated in the Terraform v0.12 release and have since then mentioned in the docs that they will be removed in a future Terraform version. The "terraform 0.12upgrade" tool from Terraform v0.12 also included a rule to automatically rewrite uses of these functions into equivalent new syntax. The main motivation for removing these now is just to get this change made prior to Terraform 1.0. as we'll be doing with various other deprecations. However, a specific reason for these two functions in particular is that their existence is what caused us to invent the idea of a "type expression" as a distinct kind of expression in Terraform v0.12, and so removing them now would allow potentially unifying type expressions with value expressions in a future release. We do not have any current specific plans to make that change, but one potential motivation for doing so would be to take another attempt at a generalized "convert" function which takes a type as one of its arguments. Our previous attempt to implement such a function was foiled by the fact that Terraform's expression validator doesn't have any way to know to treat one argument of a particular function as special, and so it was generating incorrect error messages. We won't necessarily do that, but having these "list" and "map" functions out of the way leaves the option open.
2020-11-04 22:18:44 +01:00
// ListFunc constructs a function that takes an arbitrary number of arguments
// and returns a list containing those values in the same order.
//
// This function is deprecated in Terraform v0.12
var ListFunc = function.New(&function.Spec{
Params: []function.Parameter{},
VarParam: &function.Parameter{
Name: "vals",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
Type: func(args []cty.Value) (ret cty.Type, err error) {
return cty.DynamicPseudoType, fmt.Errorf("the \"list\" function was deprecated in Terraform v0.12 and is no longer available; use tolist([ ... ]) syntax to write a literal list")
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return cty.DynamicVal, fmt.Errorf("the \"list\" function was deprecated in Terraform v0.12 and is no longer available; use tolist([ ... ]) syntax to write a literal list")
},
})
// MapFunc constructs a function that takes an even number of arguments and
// returns a map whose elements are constructed from consecutive pairs of arguments.
//
// This function is deprecated in Terraform v0.12
var MapFunc = function.New(&function.Spec{
Params: []function.Parameter{},
VarParam: &function.Parameter{
Name: "vals",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
Type: func(args []cty.Value) (ret cty.Type, err error) {
return cty.DynamicPseudoType, fmt.Errorf("the \"map\" function was deprecated in Terraform v0.12 and is no longer available; use tomap({ ... }) syntax to write a literal map")
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return cty.DynamicVal, fmt.Errorf("the \"map\" function was deprecated in Terraform v0.12 and is no longer available; use tomap({ ... }) syntax to write a literal map")
},
})
// Length returns the number of elements in the given collection or number of
// Unicode characters in the given string.
func Length(collection cty.Value) (cty.Value, error) {
return LengthFunc.Call([]cty.Value{collection})
}
// AllTrue returns true if all elements of the list are true. If the list is empty,
// return true.
func AllTrue(collection cty.Value) (cty.Value, error) {
return AllTrueFunc.Call([]cty.Value{collection})
}
// AnyTrue returns true if any element of the list is true. If the list is empty,
// return false.
func AnyTrue(collection cty.Value) (cty.Value, error) {
return AnyTrueFunc.Call([]cty.Value{collection})
}
// Coalesce takes any number of arguments and returns the first one that isn't empty.
func Coalesce(args ...cty.Value) (cty.Value, error) {
return CoalesceFunc.Call(args)
}
2018-05-25 21:57:26 +02:00
// Index finds the element index for a given value in a list.
func Index(list, value cty.Value) (cty.Value, error) {
return IndexFunc.Call([]cty.Value{list, value})
}
2018-05-26 01:15:50 +02:00
2018-05-30 17:41:31 +02:00
// List takes any number of list arguments and returns a list containing those
// values in the same order.
func List(args ...cty.Value) (cty.Value, error) {
return ListFunc.Call(args)
}
2018-05-31 18:33:43 +02:00
// Lookup performs a dynamic lookup into a map.
// There are two required arguments, map and key, plus an optional default,
// which is a value to return if no key is found in map.
func Lookup(args ...cty.Value) (cty.Value, error) {
return LookupFunc.Call(args)
}
2018-05-30 22:37:48 +02:00
// Map takes an even number of arguments and returns a map whose elements are constructed
// from consecutive pairs of arguments.
func Map(args ...cty.Value) (cty.Value, error) {
return MapFunc.Call(args)
}
2018-05-29 22:58:32 +02:00
// Matchkeys constructs a new list by taking a subset of elements from one list
// whose indexes match the corresponding indexes of values in another list.
func Matchkeys(values, keys, searchset cty.Value) (cty.Value, error) {
return MatchkeysFunc.Call([]cty.Value{values, keys, searchset})
}
2018-05-31 20:47:50 +02:00
lang/funcs: "one" function In the Terraform language we typically use lists of zero or one values in some sense interchangably with single values that might be null, because various Terraform language constructs are designed to work with collections rather than with nullable values. In Terraform v0.12 we made the splat operator [*] have a "special power" of concisely converting from a possibly-null single value into a zero-or-one list as a way to make that common operation more concise. In a sense this "one" function is the opposite operation to that special power: it goes from a zero-or-one collection (list, set, or tuple) to a possibly-null single value. This is a concise alternative to the following clunky conditional expression, with the additional benefit that the following expression is also not viable for set values, and it also properly handles the case where there's unexpectedly more than one value: length(var.foo) != 0 ? var.foo[0] : null Instead, we can write: one(var.foo) As with the splat operator, this is a tricky tradeoff because it could be argued that it's not something that'd be immediately intuitive to someone unfamiliar with Terraform. However, I think that's justified given how often zero-or-one collections arise in typical Terraform configurations. Unlike the splat operator, it should at least be easier to search for its name and find its documentation the first time you see it in a configuration. My expectation that this will become a common pattern is also my justification for giving it a short, concise name. Arguably it could be better named something like "oneornull", but that's a pretty clunky name and I'm not convinced it really adds any clarity for someone who isn't already familiar with it.
2021-01-09 01:02:56 +01:00
// One returns either the first element of a one-element list, or null
// if given a zero-element list..
func One(list cty.Value) (cty.Value, error) {
return OneFunc.Call([]cty.Value{list})
}
// Sum adds numbers in a list, set, or tuple
func Sum(list cty.Value) (cty.Value, error) {
return SumFunc.Call([]cty.Value{list})
}
2018-05-31 23:46:24 +02:00
// Transpose takes a map of lists of strings and swaps the keys and values to
// produce a new map of lists of strings.
func Transpose(values cty.Value) (cty.Value, error) {
return TransposeFunc.Call([]cty.Value{values})
}