590 lines
17 KiB
Go
590 lines
17 KiB
Go
package funcs
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"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.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.
|
|
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
|
|
}
|
|
|
|
if args[0].LengthInt() == 0 { // Easy path
|
|
return cty.NilVal, errors.New("cannot search an empty list")
|
|
}
|
|
|
|
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")
|
|
|
|
},
|
|
})
|
|
|
|
// LookupFunc constructs a function that performs dynamic lookups of map types.
|
|
var LookupFunc = function.New(&function.Spec{
|
|
Params: []function.Parameter{
|
|
{
|
|
Name: "inputMap",
|
|
Type: cty.DynamicPseudoType,
|
|
},
|
|
{
|
|
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) {
|
|
if len(args) < 1 || len(args) > 3 {
|
|
return cty.NilType, fmt.Errorf("lookup() takes two or three arguments, got %d", len(args))
|
|
}
|
|
|
|
ty := args[0].Type()
|
|
|
|
switch {
|
|
case ty.IsObjectType():
|
|
if !args[1].IsKnown() {
|
|
return cty.DynamicPseudoType, nil
|
|
}
|
|
|
|
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
|
|
defaultValueSet := false
|
|
|
|
if len(args) == 3 {
|
|
defaultVal = args[2]
|
|
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
|
|
}
|
|
|
|
if defaultValueSet {
|
|
defaultVal, err = convert.Convert(defaultVal, retType)
|
|
if err != nil {
|
|
return cty.NilVal, err
|
|
}
|
|
return defaultVal, nil
|
|
}
|
|
|
|
return cty.UnknownVal(cty.DynamicPseudoType), fmt.Errorf(
|
|
"lookup failed to find '%s'", lookupKey)
|
|
},
|
|
})
|
|
|
|
// MatchkeysFunc constructs a function that constructs a new list by taking a
|
|
// 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) {
|
|
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")
|
|
}
|
|
|
|
// the return type is based on args[0] (values)
|
|
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
|
|
}
|
|
|
|
if args[0].LengthInt() != args[1].LengthInt() {
|
|
return cty.ListValEmpty(retType.ElementType()), errors.New("length of keys and values should be equal")
|
|
}
|
|
|
|
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
|
|
ty, _ := convert.UnifyUnsafe([]cty.Type{args[1].Type(), args[2].Type()})
|
|
keys, _ := convert.Convert(args[1], ty)
|
|
searchset, _ := convert.Convert(args[2], ty)
|
|
|
|
// if searchset is empty, return an empty list.
|
|
if searchset.LengthInt() == 0 {
|
|
return cty.ListValEmpty(retType.ElementType()), nil
|
|
}
|
|
|
|
if !values.IsWhollyKnown() || !keys.IsWhollyKnown() {
|
|
return cty.UnknownVal(retType), nil
|
|
}
|
|
|
|
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
|
|
}
|
|
if !eq.IsKnown() {
|
|
return cty.ListValEmpty(retType.ElementType()), nil
|
|
}
|
|
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 {
|
|
return cty.ListValEmpty(retType.ElementType()), nil
|
|
}
|
|
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
|
|
// 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]
|
|
if !inputMap.IsWhollyKnown() {
|
|
return cty.UnknownVal(retType), nil
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
return cty.MapVal(outputMap), nil
|
|
},
|
|
})
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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})
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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})
|
|
}
|
|
|
|
// Sum adds numbers in a list, set, or tuple
|
|
func Sum(list cty.Value) (cty.Value, error) {
|
|
return SumFunc.Call([]cty.Value{list})
|
|
}
|
|
|
|
// 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})
|
|
}
|