1package funcs
2
3import (
4	"errors"
5	"fmt"
6	"math/big"
7	"sort"
8
9	"github.com/zclconf/go-cty/cty"
10	"github.com/zclconf/go-cty/cty/convert"
11	"github.com/zclconf/go-cty/cty/function"
12	"github.com/zclconf/go-cty/cty/function/stdlib"
13	"github.com/zclconf/go-cty/cty/gocty"
14)
15
16var LengthFunc = function.New(&function.Spec{
17	Params: []function.Parameter{
18		{
19			Name:             "value",
20			Type:             cty.DynamicPseudoType,
21			AllowDynamicType: true,
22			AllowUnknown:     true,
23			AllowMarked:      true,
24		},
25	},
26	Type: func(args []cty.Value) (cty.Type, error) {
27		collTy := args[0].Type()
28		switch {
29		case collTy == cty.String || collTy.IsTupleType() || collTy.IsObjectType() || collTy.IsListType() || collTy.IsMapType() || collTy.IsSetType() || collTy == cty.DynamicPseudoType:
30			return cty.Number, nil
31		default:
32			return cty.Number, errors.New("argument must be a string, a collection type, or a structural type")
33		}
34	},
35	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
36		coll := args[0]
37		collTy := args[0].Type()
38		marks := coll.Marks()
39		switch {
40		case collTy == cty.DynamicPseudoType:
41			return cty.UnknownVal(cty.Number).WithMarks(marks), nil
42		case collTy.IsTupleType():
43			l := len(collTy.TupleElementTypes())
44			return cty.NumberIntVal(int64(l)).WithMarks(marks), nil
45		case collTy.IsObjectType():
46			l := len(collTy.AttributeTypes())
47			return cty.NumberIntVal(int64(l)).WithMarks(marks), nil
48		case collTy == cty.String:
49			// We'll delegate to the cty stdlib strlen function here, because
50			// it deals with all of the complexities of tokenizing unicode
51			// grapheme clusters.
52			return stdlib.Strlen(coll)
53		case collTy.IsListType() || collTy.IsSetType() || collTy.IsMapType():
54			return coll.Length(), nil
55		default:
56			// Should never happen, because of the checks in our Type func above
57			return cty.UnknownVal(cty.Number), errors.New("impossible value type for length(...)")
58		}
59	},
60})
61
62// AllTrueFunc constructs a function that returns true if all elements of the
63// list are true. If the list is empty, return true.
64var AllTrueFunc = function.New(&function.Spec{
65	Params: []function.Parameter{
66		{
67			Name: "list",
68			Type: cty.List(cty.Bool),
69		},
70	},
71	Type: function.StaticReturnType(cty.Bool),
72	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
73		result := cty.True
74		for it := args[0].ElementIterator(); it.Next(); {
75			_, v := it.Element()
76			if !v.IsKnown() {
77				return cty.UnknownVal(cty.Bool), nil
78			}
79			if v.IsNull() {
80				return cty.False, nil
81			}
82			result = result.And(v)
83			if result.False() {
84				return cty.False, nil
85			}
86		}
87		return result, nil
88	},
89})
90
91// AnyTrueFunc constructs a function that returns true if any element of the
92// list is true. If the list is empty, return false.
93var AnyTrueFunc = function.New(&function.Spec{
94	Params: []function.Parameter{
95		{
96			Name: "list",
97			Type: cty.List(cty.Bool),
98		},
99	},
100	Type: function.StaticReturnType(cty.Bool),
101	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
102		result := cty.False
103		var hasUnknown bool
104		for it := args[0].ElementIterator(); it.Next(); {
105			_, v := it.Element()
106			if !v.IsKnown() {
107				hasUnknown = true
108				continue
109			}
110			if v.IsNull() {
111				continue
112			}
113			result = result.Or(v)
114			if result.True() {
115				return cty.True, nil
116			}
117		}
118		if hasUnknown {
119			return cty.UnknownVal(cty.Bool), nil
120		}
121		return result, nil
122	},
123})
124
125// CoalesceFunc constructs a function that takes any number of arguments and
126// returns the first one that isn't empty. This function was copied from go-cty
127// stdlib and modified so that it returns the first *non-empty* non-null element
128// from a sequence, instead of merely the first non-null.
129var CoalesceFunc = function.New(&function.Spec{
130	Params: []function.Parameter{},
131	VarParam: &function.Parameter{
132		Name:             "vals",
133		Type:             cty.DynamicPseudoType,
134		AllowUnknown:     true,
135		AllowDynamicType: true,
136		AllowNull:        true,
137	},
138	Type: func(args []cty.Value) (ret cty.Type, err error) {
139		argTypes := make([]cty.Type, len(args))
140		for i, val := range args {
141			argTypes[i] = val.Type()
142		}
143		retType, _ := convert.UnifyUnsafe(argTypes)
144		if retType == cty.NilType {
145			return cty.NilType, errors.New("all arguments must have the same type")
146		}
147		return retType, nil
148	},
149	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
150		for _, argVal := range args {
151			// We already know this will succeed because of the checks in our Type func above
152			argVal, _ = convert.Convert(argVal, retType)
153			if !argVal.IsKnown() {
154				return cty.UnknownVal(retType), nil
155			}
156			if argVal.IsNull() {
157				continue
158			}
159			if retType == cty.String && argVal.RawEquals(cty.StringVal("")) {
160				continue
161			}
162
163			return argVal, nil
164		}
165		return cty.NilVal, errors.New("no non-null, non-empty-string arguments")
166	},
167})
168
169// IndexFunc constructs a function that finds the element index for a given value in a list.
170var IndexFunc = function.New(&function.Spec{
171	Params: []function.Parameter{
172		{
173			Name: "list",
174			Type: cty.DynamicPseudoType,
175		},
176		{
177			Name: "value",
178			Type: cty.DynamicPseudoType,
179		},
180	},
181	Type: function.StaticReturnType(cty.Number),
182	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
183		if !(args[0].Type().IsListType() || args[0].Type().IsTupleType()) {
184			return cty.NilVal, errors.New("argument must be a list or tuple")
185		}
186
187		if !args[0].IsKnown() {
188			return cty.UnknownVal(cty.Number), nil
189		}
190
191		if args[0].LengthInt() == 0 { // Easy path
192			return cty.NilVal, errors.New("cannot search an empty list")
193		}
194
195		for it := args[0].ElementIterator(); it.Next(); {
196			i, v := it.Element()
197			eq, err := stdlib.Equal(v, args[1])
198			if err != nil {
199				return cty.NilVal, err
200			}
201			if !eq.IsKnown() {
202				return cty.UnknownVal(cty.Number), nil
203			}
204			if eq.True() {
205				return i, nil
206			}
207		}
208		return cty.NilVal, errors.New("item not found")
209
210	},
211})
212
213// LookupFunc constructs a function that performs dynamic lookups of map types.
214var LookupFunc = function.New(&function.Spec{
215	Params: []function.Parameter{
216		{
217			Name:        "inputMap",
218			Type:        cty.DynamicPseudoType,
219			AllowMarked: true,
220		},
221		{
222			Name:        "key",
223			Type:        cty.String,
224			AllowMarked: true,
225		},
226	},
227	VarParam: &function.Parameter{
228		Name:             "default",
229		Type:             cty.DynamicPseudoType,
230		AllowUnknown:     true,
231		AllowDynamicType: true,
232		AllowNull:        true,
233		AllowMarked:      true,
234	},
235	Type: func(args []cty.Value) (ret cty.Type, err error) {
236		if len(args) < 1 || len(args) > 3 {
237			return cty.NilType, fmt.Errorf("lookup() takes two or three arguments, got %d", len(args))
238		}
239
240		ty := args[0].Type()
241
242		switch {
243		case ty.IsObjectType():
244			if !args[1].IsKnown() {
245				return cty.DynamicPseudoType, nil
246			}
247
248			keyVal, _ := args[1].Unmark()
249			key := keyVal.AsString()
250			if ty.HasAttribute(key) {
251				return args[0].GetAttr(key).Type(), nil
252			} else if len(args) == 3 {
253				// if the key isn't found but a default is provided,
254				// return the default type
255				return args[2].Type(), nil
256			}
257			return cty.DynamicPseudoType, function.NewArgErrorf(0, "the given object has no attribute %q", key)
258		case ty.IsMapType():
259			if len(args) == 3 {
260				_, err = convert.Convert(args[2], ty.ElementType())
261				if err != nil {
262					return cty.NilType, function.NewArgErrorf(2, "the default value must have the same type as the map elements")
263				}
264			}
265			return ty.ElementType(), nil
266		default:
267			return cty.NilType, function.NewArgErrorf(0, "lookup() requires a map as the first argument")
268		}
269	},
270	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
271		var defaultVal cty.Value
272		defaultValueSet := false
273
274		if len(args) == 3 {
275			// intentionally leave default value marked
276			defaultVal = args[2]
277			defaultValueSet = true
278		}
279
280		// keep track of marks from the collection and key
281		var markses []cty.ValueMarks
282
283		// unmark collection, retain marks to reapply later
284		mapVar, mapMarks := args[0].Unmark()
285		markses = append(markses, mapMarks)
286
287		// include marks on the key in the result
288		keyVal, keyMarks := args[1].Unmark()
289		if len(keyMarks) > 0 {
290			markses = append(markses, keyMarks)
291		}
292		lookupKey := keyVal.AsString()
293
294		if !mapVar.IsKnown() {
295			return cty.UnknownVal(retType).WithMarks(markses...), nil
296		}
297
298		if mapVar.Type().IsObjectType() {
299			if mapVar.Type().HasAttribute(lookupKey) {
300				return mapVar.GetAttr(lookupKey).WithMarks(markses...), nil
301			}
302		} else if mapVar.HasIndex(cty.StringVal(lookupKey)) == cty.True {
303			return mapVar.Index(cty.StringVal(lookupKey)).WithMarks(markses...), nil
304		}
305
306		if defaultValueSet {
307			defaultVal, err = convert.Convert(defaultVal, retType)
308			if err != nil {
309				return cty.NilVal, err
310			}
311			return defaultVal.WithMarks(markses...), nil
312		}
313
314		return cty.UnknownVal(cty.DynamicPseudoType).WithMarks(markses...), fmt.Errorf(
315			"lookup failed to find '%s'", lookupKey)
316	},
317})
318
319// MatchkeysFunc constructs a function that constructs a new list by taking a
320// subset of elements from one list whose indexes match the corresponding
321// indexes of values in another list.
322var MatchkeysFunc = function.New(&function.Spec{
323	Params: []function.Parameter{
324		{
325			Name: "values",
326			Type: cty.List(cty.DynamicPseudoType),
327		},
328		{
329			Name: "keys",
330			Type: cty.List(cty.DynamicPseudoType),
331		},
332		{
333			Name: "searchset",
334			Type: cty.List(cty.DynamicPseudoType),
335		},
336	},
337	Type: func(args []cty.Value) (cty.Type, error) {
338		ty, _ := convert.UnifyUnsafe([]cty.Type{args[1].Type(), args[2].Type()})
339		if ty == cty.NilType {
340			return cty.NilType, errors.New("keys and searchset must be of the same type")
341		}
342
343		// the return type is based on args[0] (values)
344		return args[0].Type(), nil
345	},
346	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
347		if !args[0].IsKnown() {
348			return cty.UnknownVal(cty.List(retType.ElementType())), nil
349		}
350
351		if args[0].LengthInt() != args[1].LengthInt() {
352			return cty.ListValEmpty(retType.ElementType()), errors.New("length of keys and values should be equal")
353		}
354
355		output := make([]cty.Value, 0)
356		values := args[0]
357
358		// Keys and searchset must be the same type.
359		// We can skip error checking here because we've already verified that
360		// they can be unified in the Type function
361		ty, _ := convert.UnifyUnsafe([]cty.Type{args[1].Type(), args[2].Type()})
362		keys, _ := convert.Convert(args[1], ty)
363		searchset, _ := convert.Convert(args[2], ty)
364
365		// if searchset is empty, return an empty list.
366		if searchset.LengthInt() == 0 {
367			return cty.ListValEmpty(retType.ElementType()), nil
368		}
369
370		if !values.IsWhollyKnown() || !keys.IsWhollyKnown() {
371			return cty.UnknownVal(retType), nil
372		}
373
374		i := 0
375		for it := keys.ElementIterator(); it.Next(); {
376			_, key := it.Element()
377			for iter := searchset.ElementIterator(); iter.Next(); {
378				_, search := iter.Element()
379				eq, err := stdlib.Equal(key, search)
380				if err != nil {
381					return cty.NilVal, err
382				}
383				if !eq.IsKnown() {
384					return cty.ListValEmpty(retType.ElementType()), nil
385				}
386				if eq.True() {
387					v := values.Index(cty.NumberIntVal(int64(i)))
388					output = append(output, v)
389					break
390				}
391			}
392			i++
393		}
394
395		// if we haven't matched any key, then output is an empty list.
396		if len(output) == 0 {
397			return cty.ListValEmpty(retType.ElementType()), nil
398		}
399		return cty.ListVal(output), nil
400	},
401})
402
403// OneFunc returns either the first element of a one-element list, or null
404// if given a zero-element list.
405var OneFunc = function.New(&function.Spec{
406	Params: []function.Parameter{
407		{
408			Name: "list",
409			Type: cty.DynamicPseudoType,
410		},
411	},
412	Type: func(args []cty.Value) (cty.Type, error) {
413		ty := args[0].Type()
414		switch {
415		case ty.IsListType() || ty.IsSetType():
416			return ty.ElementType(), nil
417		case ty.IsTupleType():
418			etys := ty.TupleElementTypes()
419			switch len(etys) {
420			case 0:
421				// No specific type information, so we'll ultimately return
422				// a null value of unknown type.
423				return cty.DynamicPseudoType, nil
424			case 1:
425				return etys[0], nil
426			}
427		}
428		return cty.NilType, function.NewArgErrorf(0, "must be a list, set, or tuple value with either zero or one elements")
429	},
430	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
431		val := args[0]
432		ty := val.Type()
433
434		// Our parameter spec above doesn't set AllowUnknown or AllowNull,
435		// so we can assume our top-level collection is both known and non-null
436		// in here.
437
438		switch {
439		case ty.IsListType() || ty.IsSetType():
440			lenVal := val.Length()
441			if !lenVal.IsKnown() {
442				return cty.UnknownVal(retType), nil
443			}
444			var l int
445			err := gocty.FromCtyValue(lenVal, &l)
446			if err != nil {
447				// It would be very strange to get here, because that would
448				// suggest that the length is either not a number or isn't
449				// an integer, which would suggest a bug in cty.
450				return cty.NilVal, fmt.Errorf("invalid collection length: %s", err)
451			}
452			switch l {
453			case 0:
454				return cty.NullVal(retType), nil
455			case 1:
456				var ret cty.Value
457				// We'll use an iterator here because that works for both lists
458				// and sets, whereas indexing directly would only work for lists.
459				// Since we've just checked the length, we should only actually
460				// run this loop body once.
461				for it := val.ElementIterator(); it.Next(); {
462					_, ret = it.Element()
463				}
464				return ret, nil
465			}
466		case ty.IsTupleType():
467			etys := ty.TupleElementTypes()
468			switch len(etys) {
469			case 0:
470				return cty.NullVal(retType), nil
471			case 1:
472				ret := val.Index(cty.NumberIntVal(0))
473				return ret, nil
474			}
475		}
476		return cty.NilVal, function.NewArgErrorf(0, "must be a list, set, or tuple value with either zero or one elements")
477	},
478})
479
480// SumFunc constructs a function that returns the sum of all
481// numbers provided in a list
482var SumFunc = function.New(&function.Spec{
483	Params: []function.Parameter{
484		{
485			Name: "list",
486			Type: cty.DynamicPseudoType,
487		},
488	},
489	Type: function.StaticReturnType(cty.Number),
490	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
491
492		if !args[0].CanIterateElements() {
493			return cty.NilVal, function.NewArgErrorf(0, "cannot sum noniterable")
494		}
495
496		if args[0].LengthInt() == 0 { // Easy path
497			return cty.NilVal, function.NewArgErrorf(0, "cannot sum an empty list")
498		}
499
500		arg := args[0].AsValueSlice()
501		ty := args[0].Type()
502
503		if !ty.IsListType() && !ty.IsSetType() && !ty.IsTupleType() {
504			return cty.NilVal, function.NewArgErrorf(0, fmt.Sprintf("argument must be list, set, or tuple. Received %s", ty.FriendlyName()))
505		}
506
507		if !args[0].IsWhollyKnown() {
508			return cty.UnknownVal(cty.Number), nil
509		}
510
511		// big.Float.Add can panic if the input values are opposing infinities,
512		// so we must catch that here in order to remain within
513		// the cty Function abstraction.
514		defer func() {
515			if r := recover(); r != nil {
516				if _, ok := r.(big.ErrNaN); ok {
517					ret = cty.NilVal
518					err = fmt.Errorf("can't compute sum of opposing infinities")
519				} else {
520					// not a panic we recognize
521					panic(r)
522				}
523			}
524		}()
525
526		s := arg[0]
527		if s.IsNull() {
528			return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values")
529		}
530		for _, v := range arg[1:] {
531			if v.IsNull() {
532				return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values")
533			}
534			v, err = convert.Convert(v, cty.Number)
535			if err != nil {
536				return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values")
537			}
538			s = s.Add(v)
539		}
540
541		return s, nil
542	},
543})
544
545// TransposeFunc constructs a function that takes a map of lists of strings and
546// swaps the keys and values to produce a new map of lists of strings.
547var TransposeFunc = function.New(&function.Spec{
548	Params: []function.Parameter{
549		{
550			Name: "values",
551			Type: cty.Map(cty.List(cty.String)),
552		},
553	},
554	Type: function.StaticReturnType(cty.Map(cty.List(cty.String))),
555	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
556		inputMap := args[0]
557		if !inputMap.IsWhollyKnown() {
558			return cty.UnknownVal(retType), nil
559		}
560
561		outputMap := make(map[string]cty.Value)
562		tmpMap := make(map[string][]string)
563
564		for it := inputMap.ElementIterator(); it.Next(); {
565			inKey, inVal := it.Element()
566			for iter := inVal.ElementIterator(); iter.Next(); {
567				_, val := iter.Element()
568				if !val.Type().Equals(cty.String) {
569					return cty.MapValEmpty(cty.List(cty.String)), errors.New("input must be a map of lists of strings")
570				}
571
572				outKey := val.AsString()
573				if _, ok := tmpMap[outKey]; !ok {
574					tmpMap[outKey] = make([]string, 0)
575				}
576				outVal := tmpMap[outKey]
577				outVal = append(outVal, inKey.AsString())
578				sort.Strings(outVal)
579				tmpMap[outKey] = outVal
580			}
581		}
582
583		for outKey, outVal := range tmpMap {
584			values := make([]cty.Value, 0)
585			for _, v := range outVal {
586				values = append(values, cty.StringVal(v))
587			}
588			outputMap[outKey] = cty.ListVal(values)
589		}
590
591		if len(outputMap) == 0 {
592			return cty.MapValEmpty(cty.List(cty.String)), nil
593		}
594
595		return cty.MapVal(outputMap), nil
596	},
597})
598
599// ListFunc constructs a function that takes an arbitrary number of arguments
600// and returns a list containing those values in the same order.
601//
602// This function is deprecated in Terraform v0.12
603var ListFunc = function.New(&function.Spec{
604	Params: []function.Parameter{},
605	VarParam: &function.Parameter{
606		Name:             "vals",
607		Type:             cty.DynamicPseudoType,
608		AllowUnknown:     true,
609		AllowDynamicType: true,
610		AllowNull:        true,
611	},
612	Type: func(args []cty.Value) (ret cty.Type, err error) {
613		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")
614	},
615	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
616		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")
617	},
618})
619
620// MapFunc constructs a function that takes an even number of arguments and
621// returns a map whose elements are constructed from consecutive pairs of arguments.
622//
623// This function is deprecated in Terraform v0.12
624var MapFunc = function.New(&function.Spec{
625	Params: []function.Parameter{},
626	VarParam: &function.Parameter{
627		Name:             "vals",
628		Type:             cty.DynamicPseudoType,
629		AllowUnknown:     true,
630		AllowDynamicType: true,
631		AllowNull:        true,
632	},
633	Type: func(args []cty.Value) (ret cty.Type, err error) {
634		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")
635	},
636	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
637		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")
638	},
639})
640
641// Length returns the number of elements in the given collection or number of
642// Unicode characters in the given string.
643func Length(collection cty.Value) (cty.Value, error) {
644	return LengthFunc.Call([]cty.Value{collection})
645}
646
647// AllTrue returns true if all elements of the list are true. If the list is empty,
648// return true.
649func AllTrue(collection cty.Value) (cty.Value, error) {
650	return AllTrueFunc.Call([]cty.Value{collection})
651}
652
653// AnyTrue returns true if any element of the list is true. If the list is empty,
654// return false.
655func AnyTrue(collection cty.Value) (cty.Value, error) {
656	return AnyTrueFunc.Call([]cty.Value{collection})
657}
658
659// Coalesce takes any number of arguments and returns the first one that isn't empty.
660func Coalesce(args ...cty.Value) (cty.Value, error) {
661	return CoalesceFunc.Call(args)
662}
663
664// Index finds the element index for a given value in a list.
665func Index(list, value cty.Value) (cty.Value, error) {
666	return IndexFunc.Call([]cty.Value{list, value})
667}
668
669// List takes any number of list arguments and returns a list containing those
670//  values in the same order.
671func List(args ...cty.Value) (cty.Value, error) {
672	return ListFunc.Call(args)
673}
674
675// Lookup performs a dynamic lookup into a map.
676// There are two required arguments, map and key, plus an optional default,
677// which is a value to return if no key is found in map.
678func Lookup(args ...cty.Value) (cty.Value, error) {
679	return LookupFunc.Call(args)
680}
681
682// Map takes an even number of arguments and returns a map whose elements are constructed
683// from consecutive pairs of arguments.
684func Map(args ...cty.Value) (cty.Value, error) {
685	return MapFunc.Call(args)
686}
687
688// Matchkeys constructs a new list by taking a subset of elements from one list
689// whose indexes match the corresponding indexes of values in another list.
690func Matchkeys(values, keys, searchset cty.Value) (cty.Value, error) {
691	return MatchkeysFunc.Call([]cty.Value{values, keys, searchset})
692}
693
694// One returns either the first element of a one-element list, or null
695// if given a zero-element list..
696func One(list cty.Value) (cty.Value, error) {
697	return OneFunc.Call([]cty.Value{list})
698}
699
700// Sum adds numbers in a list, set, or tuple
701func Sum(list cty.Value) (cty.Value, error) {
702	return SumFunc.Call([]cty.Value{list})
703}
704
705// Transpose takes a map of lists of strings and swaps the keys and values to
706// produce a new map of lists of strings.
707func Transpose(values cty.Value) (cty.Value, error) {
708	return TransposeFunc.Call([]cty.Value{values})
709}
710