config: Audit all interpolation functions for list/map behavior

- `distinct()` - error on non-flat lists
 - `element()` - error on non-flat lists
 - `join()` - error on non-flat lists
 - `length()` - support maps
 - `lookup()` - error on non-flat maps
 - `values()` - error on non-flat maps
This commit is contained in:
Paul Hinze 2016-07-27 12:47:44 -05:00
parent 1425b34562
commit 88030764ff
No known key found for this signature in database
GPG Key ID: B69DEDF2D55501C0
3 changed files with 136 additions and 41 deletions

View File

@ -180,13 +180,17 @@ func interpolationFuncCompact() ast.Function {
var outputList []string
for _, val := range inputList {
if strVal, ok := val.Value.(string); ok {
if strVal == "" {
continue
}
outputList = append(outputList, strVal)
strVal, ok := val.Value.(string)
if !ok {
return nil, fmt.Errorf(
"compact() may only be used with flat lists, this list contains elements of %s",
val.Type.Printable())
}
if strVal == "" {
continue
}
outputList = append(outputList, strVal)
}
return stringSliceToVariableValue(outputList), nil
},
@ -487,11 +491,16 @@ func interpolationFuncDistinct() ast.Function {
var list []string
if len(args) != 1 {
return nil, fmt.Errorf("distinct() excepts only one argument.")
return nil, fmt.Errorf("accepts only one argument.")
}
if argument, ok := args[0].([]ast.Variable); ok {
for _, element := range argument {
if element.Type != ast.TypeString {
return nil, fmt.Errorf(
"only works for flat lists, this list contains elements of %s",
element.Type.Printable())
}
list = appendIfMissing(list, element.Value.(string))
}
}
@ -527,15 +536,13 @@ func interpolationFuncJoin() ast.Function {
}
for _, arg := range args[1:] {
if parts, ok := arg.(ast.Variable); ok {
for _, part := range parts.Value.([]ast.Variable) {
list = append(list, part.Value.(string))
}
}
if parts, ok := arg.([]ast.Variable); ok {
for _, part := range parts {
list = append(list, part.Value.(string))
for _, part := range arg.([]ast.Variable) {
if part.Type != ast.TypeString {
return nil, fmt.Errorf(
"only works on flat lists, this list contains elements of %s",
part.Type.Printable())
}
list = append(list, part.Value.(string))
}
}
@ -639,9 +646,11 @@ func interpolationFuncLength() ast.Function {
return len(typedSubject), nil
case []ast.Variable:
return len(typedSubject), nil
case map[string]ast.Variable:
return len(typedSubject), nil
}
return 0, fmt.Errorf("arguments to length() must be a string or list")
return 0, fmt.Errorf("arguments to length() must be a string, list, or map")
},
}
}
@ -740,9 +749,9 @@ func interpolationFuncLookup(vs map[string]ast.Variable) ast.Function {
}
}
if v.Type != ast.TypeString {
return "", fmt.Errorf(
"lookup for '%s' has bad type %s",
args[1].(string), v.Type)
return nil, fmt.Errorf(
"lookup() may only be used with flat maps, this map contains elements of %s",
v.Type.Printable())
}
return v.Value.(string), nil
@ -771,8 +780,13 @@ func interpolationFuncElement() ast.Function {
resolvedIndex := index % len(list)
v := list[resolvedIndex].Value
return v, nil
v := list[resolvedIndex]
if v.Type != ast.TypeString {
return nil, fmt.Errorf(
"element() may only be used with flat lists, this list contains elements of %s",
v.Type.Printable())
}
return v.Value, nil
},
}
}
@ -793,7 +807,7 @@ func interpolationFuncKeys(vs map[string]ast.Variable) ast.Function {
sort.Strings(keys)
//Keys are guaranteed to be strings
// Keys are guaranteed to be strings
return stringSliceToVariableValue(keys), nil
},
}

View File

@ -211,6 +211,13 @@ func TestInterpolateFuncCompact(t *testing.T) {
[]interface{}{},
false,
},
// errrors on list of lists
{
`${compact(list(list("a"), list("b")))}`,
nil,
true,
},
},
})
}
@ -502,6 +509,12 @@ func TestInterpolateFuncDistinct(t *testing.T) {
nil,
true,
},
// non-flat list is an error
{
`${distinct(list(list("a"), list("a")))}`,
nil,
true,
},
},
})
}
@ -665,6 +678,7 @@ func TestInterpolateFuncJoin(t *testing.T) {
Vars: map[string]ast.Variable{
"var.a_list": interfaceToVariableSwallowError([]string{"foo"}),
"var.a_longer_list": interfaceToVariableSwallowError([]string{"foo", "bar", "baz"}),
"var.list_of_lists": interfaceToVariableSwallowError([]interface{}{[]string{"foo"}, []string{"bar"}, []string{"baz"}}),
},
Cases: []testFunctionCase{
{
@ -684,6 +698,17 @@ func TestInterpolateFuncJoin(t *testing.T) {
"foo.bar.baz",
false,
},
{
`${join(".", var.list_of_lists)}`,
nil,
true,
},
{
`${join(".", list(list("nested")))}`,
nil,
true,
},
},
})
}
@ -878,6 +903,17 @@ func TestInterpolateFuncLength(t *testing.T) {
"0",
false,
},
// Works for maps
{
`${length(map("k", "v"))}`,
"1",
false,
},
{
`${length(map("k1", "v1", "k2", "v2"))}`,
"2",
false,
},
},
})
}
@ -1003,15 +1039,29 @@ func TestInterpolateFuncSplit(t *testing.T) {
func TestInterpolateFuncLookup(t *testing.T) {
testFunction(t, testFunctionConfig{
Vars: map[string]ast.Variable{
"var.foo": ast.Variable{
"var.foo": {
Type: ast.TypeMap,
Value: map[string]ast.Variable{
"bar": ast.Variable{
"bar": {
Type: ast.TypeString,
Value: "baz",
},
},
},
"var.map_of_lists": ast.Variable{
Type: ast.TypeMap,
Value: map[string]ast.Variable{
"bar": {
Type: ast.TypeList,
Value: []ast.Variable{
{
Type: ast.TypeString,
Value: "baz",
},
},
},
},
},
},
Cases: []testFunctionCase{
{
@ -1048,6 +1098,13 @@ func TestInterpolateFuncLookup(t *testing.T) {
true,
},
// Cannot lookup into map of lists
{
`${lookup(var.map_of_lists, "bar")}`,
nil,
true,
},
// Non-empty default
{
`${lookup(var.foo, "zap", "xyz")}`,
@ -1209,6 +1266,13 @@ func TestInterpolateFuncValues(t *testing.T) {
nil,
true,
},
// Map of lists
{
`${values(map("one", list()))}`,
nil,
true,
},
},
})
}
@ -1221,9 +1285,10 @@ func interfaceToVariableSwallowError(input interface{}) ast.Variable {
func TestInterpolateFuncElement(t *testing.T) {
testFunction(t, testFunctionConfig{
Vars: map[string]ast.Variable{
"var.a_list": interfaceToVariableSwallowError([]string{"foo", "baz"}),
"var.a_short_list": interfaceToVariableSwallowError([]string{"foo"}),
"var.empty_list": interfaceToVariableSwallowError([]interface{}{}),
"var.a_list": interfaceToVariableSwallowError([]string{"foo", "baz"}),
"var.a_short_list": interfaceToVariableSwallowError([]string{"foo"}),
"var.empty_list": interfaceToVariableSwallowError([]interface{}{}),
"var.a_nested_list": interfaceToVariableSwallowError([]interface{}{[]string{"foo"}, []string{"baz"}}),
},
Cases: []testFunctionCase{
{
@ -1265,6 +1330,13 @@ func TestInterpolateFuncElement(t *testing.T) {
nil,
true,
},
// Only works on single-level lists
{
`${element(var.a_nested_list, "0")}`,
nil,
true,
},
},
})
}
@ -1466,6 +1538,7 @@ func testFunction(t *testing.T, config testFunctionConfig) {
}
result, err := hil.Eval(ast, langEvalConfig(config.Vars))
t.Logf("err: %s", err)
if err != nil != tc.Error {
t.Fatalf("Case #%d:\ninput: %#v\nerr: %s", i, tc.Input, err)
}

View File

@ -115,15 +115,15 @@ The supported built-in functions are:
Example: `concat(aws_instance.db.*.tags.Name, aws_instance.web.*.tags.Name)`
* `distinct(list)` - Removes duplicate items from a list. Keeps the first
occurrence of each element, and removes subsequent occurences.
Example: `distinct(var.usernames)`
occurrence of each element, and removes subsequent occurences. This
function is only valid for flat lists. Example: `distinct(var.usernames)`
* `element(list, index)` - Returns a single element from a list
at the given index. If the index is greater than the number of
elements, this function will wrap using a standard mod algorithm.
A list is only possible with splat variables from resources with
a count greater than one.
Example: `element(aws_subnet.foo.*.id, count.index)`
This function only works on flat lists. Examples:
* `element(aws_subnet.foo.*.id, count.index)`
* `element(var.list_of_strings, 2)`
* `file(path)` - Reads the contents of a file into the string. Variables
in this file are _not_ interpolated. The contents of the file are
@ -149,24 +149,28 @@ The supported built-in functions are:
`formatlist("instance %v has private ip %v", aws_instance.foo.*.id, aws_instance.foo.*.private_ip)`.
Passing lists with different lengths to formatlist results in an error.
* `index(list, elem)` - Finds the index of a given element in a list. Example:
`index(aws_instance.foo.*.tags.Name, "foo-test")`
* `index(list, elem)` - Finds the index of a given element in a list.
This function only works on flat lists.
Example: `index(aws_instance.foo.*.tags.Name, "foo-test")`
* `join(delim, list)` - Joins the list with the delimiter for a resultant string. A list is
only possible with splat variables from resources with a count
greater than one. Example: `join(",", aws_instance.foo.*.id)`
* `join(delim, list)` - Joins the list with the delimiter for a resultant string.
This function works only on flat lists.
Examples:
* `join(",", aws_instance.foo.*.id)`
* `join(",", var.ami_list)`
* `jsonencode(item)` - Returns a JSON-encoded representation of the given
item, which may be a string, list of strings, or map from string to string.
Note that if the item is a string, the return value includes the double
quotes.
* `keys(map)` - Returns a lexically sorted, JSON-encoded list of the map keys.
* `keys(map)` - Returns a lexically sorted list of the map keys.
* `length(list)` - Returns a number of members in a given list
* `length(list)` - Returns a number of members in a given list, map, or string.
or a number of characters in a given string.
* `${length(split(",", "a,b,c"))}` = 3
* `${length("a,b,c")}` = 5
* `${length(map("key", "val"))}` = 1
* `list(items...)` - Returns a list consisting of the arguments to the function.
This function provides a way of representing list literals in interpolation.
@ -177,7 +181,9 @@ The supported built-in functions are:
variable. The `map` parameter should be another variable, such
as `var.amis`. If `key` does not exist in `map`, the interpolation will
fail unless you specify a third argument, `default`, which should be a
string value to return if no `key` is found in `map.
string value to return if no `key` is found in `map`. This function
only works on flat maps and will return an error for maps that
include nested lists or maps.
* `lower(string)` - Returns a copy of the string with all Unicode letters mapped to their lower case.
@ -232,7 +238,9 @@ The supported built-in functions are:
* `uuid()` - Returns a UUID string in RFC 4122 v4 format. This string will change with every invocation of the function, so in order to prevent diffs on every plan & apply, it must be used with the [`ignore_changes`](/docs/configuration/resources.html#ignore-changes) lifecycle attribute.
* `values(map)` - Returns a JSON-encoded list of the map values, in the order of the keys returned by the `keys` function.
* `values(map)` - Returns a list of the map values, in the order of the keys
returned by the `keys` function. This function only works on flat maps and
will return an error for maps that include nested lists or maps.
## Templates