terraform/lang/funcs/collection.go

617 lines
18 KiB
Go
Raw Normal View History

package funcs
import (
"errors"
"fmt"
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"
"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,
},
},
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()
switch {
case collTy == cty.DynamicPseudoType:
return cty.UnknownVal(cty.Number), nil
case collTy.IsTupleType():
l := len(collTy.TupleElementTypes())
return cty.NumberIntVal(int64(l)), nil
case collTy.IsObjectType():
l := len(collTy.AttributeTypes())
return cty.NumberIntVal(int64(l)), 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.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
for it := args[0].ElementIterator(); it.Next(); {
_, v := it.Element()
if v.IsNull() {
continue
}
result = result.Or(v)
if result.True() {
return cty.True, 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
},
})
// Flatten until it's not a cty.List, and return whether the value is known.
// We can flatten lists with unknown values, as long as they are not
// lists themselves.
func flattener(flattenList cty.Value) ([]cty.Value, bool) {
out := make([]cty.Value, 0)
2018-05-30 16:26:19 +02:00
for it := flattenList.ElementIterator(); it.Next(); {
_, val := it.Element()
if val.Type().IsListType() || val.Type().IsSetType() || val.Type().IsTupleType() {
if !val.IsKnown() {
return out, false
}
res, known := flattener(val)
if !known {
return res, known
}
out = append(out, res...)
2018-05-30 16:26:19 +02:00
} else {
out = append(out, val)
2018-05-30 16:26:19 +02:00
}
}
return out, true
2018-05-30 16:26:19 +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,
2018-05-31 18:33:43 +02:00
},
{
Name: "key",
Type: cty.String,
},
},
VarParam: &function.Parameter{
Name: "default",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
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
}
key := args[1].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 {
defaultVal = args[2]
2018-05-31 18:33:43 +02:00
defaultValueSet = true
}
mapVar := args[0]
lookupKey := args[1].AsString()
if !mapVar.IsKnown() {
return cty.UnknownVal(retType), nil
}
if mapVar.Type().IsObjectType() {
if mapVar.Type().HasAttribute(lookupKey) {
return mapVar.GetAttr(lookupKey), nil
}
} else if mapVar.HasIndex(cty.StringVal(lookupKey)) == cty.True {
return mapVar.Index(cty.StringVal(lookupKey)), 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, nil
2018-05-31 18:33:43 +02:00
}
return cty.UnknownVal(cty.DynamicPseudoType), 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
},
})
// 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()
var i float64
var s float64
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].IsKnown() {
return cty.UnknownVal(cty.Number), nil
}
for _, v := range arg {
if err := gocty.FromCtyValue(v, &i); err != nil {
return cty.UnknownVal(cty.Number), function.NewArgErrorf(0, "argument must be list, set, or tuple of number values")
} else {
s += i
}
}
return cty.NumberFloatVal(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")
},
})
2018-05-31 00:38:55 +02:00
// helper function to add an element to a list, if it does not already exist
2018-05-26 01:15:50 +02:00
func appendIfMissing(slice []cty.Value, element cty.Value) ([]cty.Value, error) {
for _, ele := range slice {
eq, err := stdlib.Equal(ele, element)
if err != nil {
return slice, err
}
if eq.True() {
return slice, nil
}
}
return append(slice, element), nil
}
// 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
// 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})
}