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(...)") } }, }) // 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") }, }) // 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) 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...) } else { out = append(out, val) } } return out, true } // 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) { if len(args) == 0 { return cty.NilType, errors.New("at least one argument is required") } argTypes := make([]cty.Type, len(args)) for i, arg := range args { argTypes[i] = arg.Type() } retType, _ := convert.UnifyUnsafe(argTypes) if retType == cty.NilType { return cty.NilType, errors.New("all arguments must have the same type") } return cty.List(retType), nil }, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { newList := make([]cty.Value, 0, len(args)) for _, arg := range args { // We already know this will succeed because of the checks in our Type func above arg, _ = convert.Convert(arg, retType.ElementType()) newList = append(newList, arg) } return cty.ListVal(newList), nil }, }) // 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.IsWhollyKnown() { 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) }, }) // 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) { if len(args) < 2 || len(args)%2 != 0 { return cty.NilType, fmt.Errorf("map requires an even number of two or more arguments, got %d", len(args)) } argTypes := make([]cty.Type, len(args)/2) index := 0 for i := 0; i < len(args); i += 2 { argTypes[index] = args[i+1].Type() index++ } valType, _ := convert.UnifyUnsafe(argTypes) if valType == cty.NilType { return cty.NilType, errors.New("all arguments must have the same type") } return cty.Map(valType), nil }, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { for _, arg := range args { if !arg.IsWhollyKnown() { return cty.UnknownVal(retType), nil } } outputMap := make(map[string]cty.Value) for i := 0; i < len(args); i += 2 { keyVal, err := convert.Convert(args[i], cty.String) if err != nil { return cty.NilVal, err } if keyVal.IsNull() { return cty.NilVal, fmt.Errorf("argument %d is a null key", i+1) } key := keyVal.AsString() val := args[i+1] var variable cty.Value err = gocty.FromCtyValue(val, &variable) if err != nil { return cty.NilVal, err } // We already know this will succeed because of the checks in our Type func above variable, _ = convert.Convert(variable, retType.ElementType()) // Check for duplicate keys if _, ok := outputMap[key]; ok { return cty.NilVal, fmt.Errorf("argument %d is a duplicate key: %q", i+1, key) } outputMap[key] = variable } return cty.MapVal(outputMap), nil }, }) // 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 }, }) // helper function to add an element to a list, if it does not already exist 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}) } // 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}) }