2018-05-22 02:39:26 +02:00
package funcs
import (
2019-04-30 19:12:00 +02:00
"errors"
2018-05-22 02:39:26 +02:00
"fmt"
2020-12-10 22:56:06 +01:00
"math/big"
2018-05-31 23:46:24 +02:00
"sort"
2018-05-22 02:39:26 +02:00
"github.com/zclconf/go-cty/cty"
2018-05-24 20:48:44 +02:00
"github.com/zclconf/go-cty/cty/convert"
2018-05-22 02:39:26 +02:00
"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"
2018-05-22 02:39:26 +02:00
)
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 {
2018-11-06 02:13:05 +01:00
case collTy == cty . String || collTy . IsTupleType ( ) || collTy . IsObjectType ( ) || collTy . IsListType ( ) || collTy . IsMapType ( ) || collTy . IsSetType ( ) || collTy == cty . DynamicPseudoType :
2018-05-22 02:39:26 +02:00
return cty . Number , nil
default :
2019-05-01 23:55:44 +02:00
return cty . Number , errors . New ( "argument must be a string, a collection type, or a structural type" )
2018-05-22 02:39:26 +02:00
}
} ,
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
2019-05-01 23:55:44 +02:00
return cty . UnknownVal ( cty . Number ) , errors . New ( "impossible value type for length(...)" )
2018-05-22 02:39:26 +02:00
}
} ,
} )
2020-09-22 15:06:42 +02:00
// AllTrueFunc constructs a function that returns true if all elements of the
2020-10-23 22:52:48 +02:00
// list are true. If the list is empty, return true.
2020-09-22 15:06:42 +02:00
var AllTrueFunc = function . New ( & function . Spec {
Params : [ ] function . Parameter {
{
2020-10-23 22:52:48 +02:00
Name : "list" ,
Type : cty . List ( cty . Bool ) ,
2020-09-22 15:06:42 +02:00
} ,
} ,
Type : function . StaticReturnType ( cty . Bool ) ,
Impl : func ( args [ ] cty . Value , retType cty . Type ) ( ret cty . Value , err error ) {
2020-10-23 22:52:48 +02:00
result := cty . True
2020-09-22 15:06:42 +02:00
for it := args [ 0 ] . ElementIterator ( ) ; it . Next ( ) ; {
_ , v := it . Element ( )
2020-12-10 17:20:57 +01:00
if ! v . IsKnown ( ) {
return cty . UnknownVal ( cty . Bool ) , nil
}
2020-10-23 22:52:48 +02:00
if v . IsNull ( ) {
return cty . False , nil
2020-09-22 15:06:42 +02:00
}
2020-10-23 22:52:48 +02:00
result = result . And ( v )
if result . False ( ) {
2020-09-22 15:06:42 +02:00
return cty . False , nil
}
2020-10-23 22:52:48 +02:00
}
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
2020-12-10 17:20:57 +01:00
var hasUnknown bool
2020-10-23 22:52:48 +02:00
for it := args [ 0 ] . ElementIterator ( ) ; it . Next ( ) ; {
_ , v := it . Element ( )
2020-12-10 17:20:57 +01:00
if ! v . IsKnown ( ) {
hasUnknown = true
continue
}
2020-10-23 22:52:48 +02:00
if v . IsNull ( ) {
continue
2020-09-22 15:06:42 +02:00
}
2020-10-23 22:52:48 +02:00
result = result . Or ( v )
if result . True ( ) {
return cty . True , nil
2020-09-22 15:06:42 +02:00
}
}
2020-12-10 17:20:57 +01:00
if hasUnknown {
return cty . UnknownVal ( cty . Bool ) , nil
}
2020-10-23 22:52:48 +02:00
return result , nil
2020-09-22 15:06:42 +02:00
} ,
} )
2019-05-02 16:47:19 +02:00
// CoalesceFunc constructs a function that takes any number of arguments and
2019-04-12 19:57:52 +02:00
// 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 {
2019-05-01 23:55:44 +02:00
return cty . NilType , errors . New ( "all arguments must have the same type" )
2019-04-12 19:57:52 +02:00
}
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
}
2019-05-01 23:55:44 +02:00
return cty . NilVal , errors . New ( "no non-null, non-empty-string arguments" )
2019-04-12 19:57:52 +02:00
} ,
} )
2019-05-02 16:47:19 +02:00
// 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 ) {
2018-05-26 01:30:50 +02:00
if ! ( args [ 0 ] . Type ( ) . IsListType ( ) || args [ 0 ] . Type ( ) . IsTupleType ( ) ) {
2019-05-01 23:55:44 +02:00
return cty . NilVal , errors . New ( "argument must be a list or tuple" )
2018-05-26 01:30:50 +02:00
}
2018-10-19 01:19:59 +02:00
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
2019-05-01 23:55:44 +02:00
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
}
}
2019-05-01 23:55:44 +02:00
return cty . NilVal , errors . New ( "item not found" )
2018-05-25 21:57:26 +02:00
} ,
} )
2019-05-02 16:47: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" ,
2018-10-16 20:04:19 +02:00
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 ,
} ,
2018-06-06 19:43:58 +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 {
2018-06-06 19:43:58 +02:00
return cty . NilType , fmt . Errorf ( "lookup() takes two or three arguments, got %d" , len ( args ) )
2018-05-31 18:33:43 +02:00
}
2018-10-16 21:09:04 +02:00
2018-10-16 20:04:19 +02:00
ty := args [ 0 ] . Type ( )
2018-10-22 12:58:47 +02:00
2018-10-16 20:04:19 +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 ( )
2018-10-16 21:09:04 +02:00
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
}
2018-10-16 23:55:57 +02:00
return cty . DynamicPseudoType , function . NewArgErrorf ( 0 , "the given object has no attribute %q" , key )
2018-10-16 21:57:05 +02:00
case ty . IsMapType ( ) :
2019-08-01 21:52:52 +02:00
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" )
}
}
2018-10-16 21:09:04 +02:00
return ty . ElementType ( ) , nil
2018-10-16 21:57:05 +02:00
default :
2018-10-16 23:55:57 +02:00
return cty . NilType , function . NewArgErrorf ( 0 , "lookup() requires a map as the first argument" )
2018-10-16 20:04:19 +02:00
}
2018-06-06 19:43:58 +02:00
} ,
Impl : func ( args [ ] cty . Value , retType cty . Type ) ( ret cty . Value , err error ) {
2018-06-06 17:09:33 +02:00
var defaultVal cty . Value
2018-05-31 18:33:43 +02:00
defaultValueSet := false
if len ( args ) == 3 {
2018-06-06 17:09:33 +02:00
defaultVal = args [ 2 ]
2018-05-31 18:33:43 +02:00
defaultValueSet = true
}
mapVar := args [ 0 ]
lookupKey := args [ 1 ] . AsString ( )
2020-10-05 14:48:49 +02:00
if ! mapVar . IsKnown ( ) {
2018-06-06 19:43:58 +02:00
return cty . UnknownVal ( retType ) , nil
}
2018-10-16 21:09:04 +02:00
if mapVar . Type ( ) . IsObjectType ( ) {
if mapVar . Type ( ) . HasAttribute ( lookupKey ) {
return mapVar . GetAttr ( lookupKey ) , nil
2018-10-16 20:04:19 +02:00
}
2018-10-16 21:09:04 +02:00
} else if mapVar . HasIndex ( cty . StringVal ( lookupKey ) ) == cty . True {
2019-08-01 21:52:52 +02:00
return mapVar . Index ( cty . StringVal ( lookupKey ) ) , nil
2018-05-31 18:33:43 +02:00
}
if defaultValueSet {
2018-06-06 19:43:58 +02:00
defaultVal , err = convert . Convert ( defaultVal , retType )
if err != nil {
return cty . NilVal , err
2018-06-06 17:09:33 +02:00
}
2018-06-06 19:43:58 +02:00
return defaultVal , nil
2018-05-31 18:33:43 +02:00
}
2018-06-06 17:09:33 +02:00
return cty . UnknownVal ( cty . DynamicPseudoType ) , fmt . Errorf (
2018-05-31 18:33:43 +02:00
"lookup failed to find '%s'" , lookupKey )
} ,
} )
2019-05-02 16:47:19 +02:00
// 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 ( ) } )
2019-06-04 00:00:53 +02:00
if ty == cty . NilType {
2019-06-04 14:36:30 +02:00
return cty . NilType , errors . New ( "keys and searchset must be of the same type" )
2018-05-29 22:58:32 +02:00
}
2019-06-04 14:36:30 +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 ) {
2018-10-19 01:19:59 +02:00
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 ( ) {
2019-05-01 23:55:44 +02:00
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 ]
2019-06-04 14:36:30 +02:00
// 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 ( ) } )
2019-06-04 14:36:30 +02:00
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 ( ) {
2018-06-06 19:43:58 +02:00
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" )
} ,
} )
2020-04-15 20:27:06 +02:00
// 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 ( ) ) )
}
2020-12-10 22:56:06 +01:00
if ! args [ 0 ] . IsWhollyKnown ( ) {
2020-04-15 20:27:06 +02:00
return cty . UnknownVal ( cty . Number ) , nil
}
2020-12-10 22:56:06 +01:00
// 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 )
}
}
} ( )
2020-04-15 20:27:06 +02:00
2020-12-10 22:56:06 +01:00
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" )
2020-04-15 20:27:06 +02:00
}
2020-12-10 22:56:06 +01:00
s = s . Add ( v )
2020-04-15 20:27:06 +02:00
}
2020-12-10 22:56:06 +01:00
return s , nil
2020-04-15 20:27:06 +02:00
} ,
} )
2019-05-02 16:47:19 +02:00
// 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 ) {
2019-05-01 23:55:44 +02:00
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 )
}
2019-11-08 18:40:39 +01:00
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
} ,
} )
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-22 02:39:26 +02:00
// 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 } )
}
2018-05-24 20:48:44 +02:00
2020-10-23 22:52:48 +02:00
// AllTrue returns true if all elements of the list are true. If the list is empty,
// return true.
2020-09-22 15:06:42 +02:00
func AllTrue ( collection cty . Value ) ( cty . Value , error ) {
return AllTrueFunc . Call ( [ ] cty . Value { collection } )
}
2020-10-23 22:52:48 +02:00
// 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 } )
}
2019-04-12 19:57:52 +02:00
// 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 } )
}
2020-04-15 20:27:06 +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 } )
}