diff --git a/config/interpolate_funcs.go b/config/interpolate_funcs.go index bc2c49f45..32fc2ba4b 100644 --- a/config/interpolate_funcs.go +++ b/config/interpolate_funcs.go @@ -69,6 +69,7 @@ func Funcs() map[string]ast.Function { "distinct": interpolationFuncDistinct(), "element": interpolationFuncElement(), "file": interpolationFuncFile(), + "matchkeys": interpolationFuncMatchKeys(), "floor": interpolationFuncFloor(), "format": interpolationFuncFormat(), "formatlist": interpolationFuncFormatList(), @@ -668,6 +669,57 @@ func appendIfMissing(slice []string, element string) []string { return append(slice, element) } +// for two lists `keys` and `values` of equal length, returns all elements +// from `values` where the corresponding element from `keys` is in `searchset`. +func interpolationFuncMatchKeys() ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{ast.TypeList, ast.TypeList, ast.TypeList}, + ReturnType: ast.TypeList, + Callback: func(args []interface{}) (interface{}, error) { + output := make([]ast.Variable, 0) + + values, _ := args[0].([]ast.Variable) + keys, _ := args[1].([]ast.Variable) + searchset, _ := args[2].([]ast.Variable) + + if len(keys) != len(values) { + return nil, fmt.Errorf("length of keys and values should be equal") + } + + for i, key := range keys { + for _, search := range searchset { + if res, err := compareSimpleVariables(key, search); err != nil { + return nil, err + } else if res == true { + output = append(output, values[i]) + break + } + } + } + // if searchset is empty, then output is an empty list as well. + // if we haven't matched any key, then output is an empty list. + return output, nil + }, + } +} + +// compare two variables of the same type, i.e. non complex one, such as TypeList or TypeMap +func compareSimpleVariables(a, b ast.Variable) (bool, error) { + if a.Type != b.Type { + return false, fmt.Errorf( + "won't compare items of different types %s and %s", + a.Type.Printable(), b.Type.Printable()) + } + switch a.Type { + case ast.TypeString: + return a.Value.(string) == b.Value.(string), nil + default: + return false, fmt.Errorf( + "can't compare items of type %s", + a.Type.Printable()) + } +} + // interpolationFuncJoin implements the "join" function that allows // multi-variable values to be joined by some character. func interpolationFuncJoin() ast.Function { diff --git a/config/interpolate_funcs_test.go b/config/interpolate_funcs_test.go index 801be6dbb..29c09aa9e 100644 --- a/config/interpolate_funcs_test.go +++ b/config/interpolate_funcs_test.go @@ -964,6 +964,74 @@ func TestInterpolateFuncDistinct(t *testing.T) { }) } +func TestInterpolateFuncMatchKeys(t *testing.T) { + testFunction(t, testFunctionConfig{ + Cases: []testFunctionCase{ + // normal usage + { + `${matchkeys(list("a", "b", "c"), list("ref1", "ref2", "ref3"), list("ref2"))}`, + []interface{}{"b"}, + false, + }, + // normal usage 2, check the order + { + `${matchkeys(list("a", "b", "c"), list("ref1", "ref2", "ref3"), list("ref2", "ref1"))}`, + []interface{}{"a", "b"}, + false, + }, + // duplicate item in searchset + { + `${matchkeys(list("a", "b", "c"), list("ref1", "ref2", "ref3"), list("ref2", "ref2"))}`, + []interface{}{"b"}, + false, + }, + // no matches + { + `${matchkeys(list("a", "b", "c"), list("ref1", "ref2", "ref3"), list("ref4"))}`, + []interface{}{}, + false, + }, + // no matches 2 + { + `${matchkeys(list("a", "b", "c"), list("ref1", "ref2", "ref3"), list())}`, + []interface{}{}, + false, + }, + // zero case + { + `${matchkeys(list(), list(), list("nope"))}`, + []interface{}{}, + false, + }, + // complex values + { + `${matchkeys(list(list("a", "a")), list("a"), list("a"))}`, + []interface{}{[]interface{}{"a", "a"}}, + false, + }, + // errors + // different types + { + `${matchkeys(list("a"), list(1), list("a"))}`, + nil, + true, + }, + // different types + { + `${matchkeys(list("a"), list(list("a"), list("a")), list("a"))}`, + nil, + true, + }, + // lists of different length is an error + { + `${matchkeys(list("a"), list("a", "b"), list("a"))}`, + nil, + true, + }, + }, + }) +} + func TestInterpolateFuncFile(t *testing.T) { tf, err := ioutil.TempFile("", "tf") if err != nil { diff --git a/website/source/docs/configuration/interpolation.html.md b/website/source/docs/configuration/interpolation.html.md index 1b4b69c5c..bd003679e 100644 --- a/website/source/docs/configuration/interpolation.html.md +++ b/website/source/docs/configuration/interpolation.html.md @@ -208,6 +208,15 @@ The supported built-in functions are: module, you generally want to make the path relative to the module base, like this: `file("${path.module}/file")`. + * `matchkeys(values, keys, searchset)` - For two lists `values` and `keys` of + equal length, returns all elements from `values` where the corresponding + element from `keys` exists in the `searchset` list. E.g. + `matchkeys(aws_instance.example.*.id, + aws_instance.example.*.availability_zone, list("us-west-2a"))` will return a + list of the instance IDs of the `aws_instance.example` instances in + `"us-west-2a"`. No match will result in empty list. Items of `keys` are + processed sequentially, so the order of returned `values` is preserved. + * `floor(float)` - Returns the greatest integer value less than or equal to the argument.