Skip to content

Commit

Permalink
config: "matchkeys" interpolation function
Browse files Browse the repository at this point in the history
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
  • Loading branch information
alvelcom authored and apparentlymart committed Apr 21, 2017
1 parent 7ff9274 commit f9fb601
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 0 deletions.
52 changes: 52 additions & 0 deletions config/interpolate_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ func Funcs() map[string]ast.Function {
"distinct": interpolationFuncDistinct(),
"element": interpolationFuncElement(),
"file": interpolationFuncFile(),
"matchkeys": interpolationFuncMatchKeys(),
"floor": interpolationFuncFloor(),
"format": interpolationFuncFormat(),
"formatlist": interpolationFuncFormatList(),
Expand Down Expand Up @@ -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 {
Expand Down
68 changes: 68 additions & 0 deletions config/interpolate_funcs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions website/source/docs/configuration/interpolation.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down

0 comments on commit f9fb601

Please sign in to comment.