config: "matchkeys" interpolation function

This new function allows using a search within one list to filter another list. For example, it can be used to find the ids of EC2 instances in a particular AZ.

The interface is made slightly awkward by the constraints of HIL's featureset.

#13847
This commit is contained in:
Pavel Khusainov 2017-04-21 21:04:49 +04:00 committed by Martin Atkins
parent 7ff92746a2
commit f9fb6010ee
3 changed files with 129 additions and 0 deletions

View File

@ -69,6 +69,7 @@ func Funcs() map[string]ast.Function {
"distinct": interpolationFuncDistinct(), "distinct": interpolationFuncDistinct(),
"element": interpolationFuncElement(), "element": interpolationFuncElement(),
"file": interpolationFuncFile(), "file": interpolationFuncFile(),
"matchkeys": interpolationFuncMatchKeys(),
"floor": interpolationFuncFloor(), "floor": interpolationFuncFloor(),
"format": interpolationFuncFormat(), "format": interpolationFuncFormat(),
"formatlist": interpolationFuncFormatList(), "formatlist": interpolationFuncFormatList(),
@ -668,6 +669,57 @@ func appendIfMissing(slice []string, element string) []string {
return append(slice, element) 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 // interpolationFuncJoin implements the "join" function that allows
// multi-variable values to be joined by some character. // multi-variable values to be joined by some character.
func interpolationFuncJoin() ast.Function { func interpolationFuncJoin() ast.Function {

View File

@ -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) { func TestInterpolateFuncFile(t *testing.T) {
tf, err := ioutil.TempFile("", "tf") tf, err := ioutil.TempFile("", "tf")
if err != nil { if err != nil {

View File

@ -208,6 +208,15 @@ The supported built-in functions are:
module, you generally want to make the path relative to the module base, module, you generally want to make the path relative to the module base,
like this: `file("${path.module}/file")`. 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 * `floor(float)` - Returns the greatest integer value less than or equal to
the argument. the argument.