From 39bbbb8da6230f517f9e992cc591942d823a0da6 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 1 Aug 2016 18:10:53 -0400 Subject: [PATCH 1/3] Add merge interpolation function Add a `merge` interpolation function, which merges any number of maps. Duplicate keys are OK, with the last write winning. --- config/interpolate_funcs.go | 21 +++++++ config/interpolate_funcs_test.go | 98 +++++++++++++++++++++++++++++++- 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/config/interpolate_funcs.go b/config/interpolate_funcs.go index 099369064..d66073ec6 100644 --- a/config/interpolate_funcs.go +++ b/config/interpolate_funcs.go @@ -73,6 +73,7 @@ func Funcs() map[string]ast.Function { "lower": interpolationFuncLower(), "map": interpolationFuncMap(), "md5": interpolationFuncMd5(), + "merge": interpolationFuncMerge(), "uuid": interpolationFuncUUID(), "replace": interpolationFuncReplace(), "sha1": interpolationFuncSha1(), @@ -898,6 +899,26 @@ func interpolationFuncMd5() ast.Function { } } +func interpolationFuncMerge() ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{ast.TypeMap}, + ReturnType: ast.TypeMap, + Variadic: true, + VariadicType: ast.TypeMap, + Callback: func(args []interface{}) (interface{}, error) { + outputMap := make(map[string]ast.Variable) + + for _, arg := range args { + for k, v := range arg.(map[string]ast.Variable) { + outputMap[k] = v + } + } + + return outputMap, nil + }, + } +} + // interpolationFuncUpper implements the "upper" function that does // string upper casing. func interpolationFuncUpper() ast.Function { diff --git a/config/interpolate_funcs_test.go b/config/interpolate_funcs_test.go index 0d4f38d9c..0b77af328 100644 --- a/config/interpolate_funcs_test.go +++ b/config/interpolate_funcs_test.go @@ -498,6 +498,103 @@ func TestInterpolateFuncConcat(t *testing.T) { }) } +func TestInterpolateFuncMerge(t *testing.T) { + testFunction(t, testFunctionConfig{ + Cases: []testFunctionCase{ + // basic merge + { + `${merge(map("a", "b"), map("c", "d"))}`, + map[string]interface{}{"a": "b", "c": "d"}, + false, + }, + + // merge with conflicts is ok, last in wins. + { + `${merge(map("a", "b", "c", "X"), map("c", "d"))}`, + map[string]interface{}{"a": "b", "c": "d"}, + false, + }, + + // merge variadic + { + `${merge(map("a", "b"), map("c", "d"), map("e", "f"))}`, + map[string]interface{}{"a": "b", "c": "d", "e": "f"}, + false, + }, + + // merge with variables + { + `${merge(var.maps[0], map("c", "d"))}`, + map[string]interface{}{"key1": "a", "key2": "b", "c": "d"}, + false, + }, + + // only accept maps + { + `${merge(map("a", "b"), list("c", "d"))}`, + nil, + true, + }, + + // merge maps of maps + { + `${merge(map("a", var.maps[0]), map("b", var.maps[1]))}`, + map[string]interface{}{ + "b": map[string]interface{}{"key3": "d", "key4": "c"}, + "a": map[string]interface{}{"key1": "a", "key2": "b"}, + }, + false, + }, + // merge maps of lists + { + `${merge(map("a", list("b")), map("c", list("d", "e")))}`, + map[string]interface{}{"a": []interface{}{"b"}, "c": []interface{}{"d", "e"}}, + false, + }, + // merge map of various kinds + { + `${merge(map("a", var.maps[0]), map("b", list("c", "d")))}`, + map[string]interface{}{"a": map[string]interface{}{"key1": "a", "key2": "b"}, "b": []interface{}{"c", "d"}}, + false, + }, + }, + Vars: map[string]ast.Variable{ + "var.maps": { + Type: ast.TypeList, + Value: []ast.Variable{ + { + Type: ast.TypeMap, + Value: map[string]ast.Variable{ + "key1": { + Type: ast.TypeString, + Value: "a", + }, + "key2": { + Type: ast.TypeString, + Value: "b", + }, + }, + }, + { + Type: ast.TypeMap, + Value: map[string]ast.Variable{ + "key3": { + Type: ast.TypeString, + Value: "d", + }, + "key4": { + Type: ast.TypeString, + Value: "c", + }, + }, + }, + }, + }, + }, + }) + +} + func TestInterpolateFuncDistinct(t *testing.T) { testFunction(t, testFunctionConfig{ Cases: []testFunctionCase{ @@ -1542,7 +1639,6 @@ type testFunctionCase struct { func testFunction(t *testing.T, config testFunctionConfig) { for i, tc := range config.Cases { - fmt.Println("running", i) ast, err := hil.Parse(tc.Input) if err != nil { t.Fatalf("Case #%d: input: %#v\nerr: %v", i, tc.Input, err) From 068059ab3fe73909e1314e9618fc7f63c3552ca2 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 2 Aug 2016 09:33:08 -0400 Subject: [PATCH 2/3] Add `merge` doc --- website/source/docs/configuration/interpolation.html.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/website/source/docs/configuration/interpolation.html.md b/website/source/docs/configuration/interpolation.html.md index 2d3e42672..91de2341b 100644 --- a/website/source/docs/configuration/interpolation.html.md +++ b/website/source/docs/configuration/interpolation.html.md @@ -193,6 +193,11 @@ The supported built-in functions are: * `map("hello", "world")` * `map("us-east", list("a", "b", "c"), "us-west", list("b", "c", "d"))` + * `merge(map1, map2, ...)` - Returns the union of 2 or more maps. The maps + are consumed in the order provided, and duplciate keys overwrite previous + entries. + * `${merge(map("a", "b"), map("c", "d"))}` returns `{"a": "b", "c": "d"}` + * `md5(string)` - Returns a (conventional) hexadecimal representation of the MD5 hash of the given string. From 2a11f3a138fd6cef718ebd7026b69456ed5423ef Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 2 Aug 2016 09:34:29 -0400 Subject: [PATCH 3/3] make variadic syntax consistent in docs --- website/source/docs/configuration/interpolation.html.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/source/docs/configuration/interpolation.html.md b/website/source/docs/configuration/interpolation.html.md index 91de2341b..f7cd5108e 100644 --- a/website/source/docs/configuration/interpolation.html.md +++ b/website/source/docs/configuration/interpolation.html.md @@ -110,7 +110,7 @@ The supported built-in functions are: variables or when parsing module outputs. Example: `compact(module.my_asg.load_balancer_names)` - * `concat(list1, list2)` - Combines two or more lists into a single list. + * `concat(list1, list2, ...)` - Combines two or more lists into a single list. Example: `concat(aws_instance.db.*.tags.Name, aws_instance.web.*.tags.Name)` * `distinct(list)` - Removes duplicate items from a list. Keeps the first @@ -132,13 +132,13 @@ 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")`. - * `format(format, args...)` - Formats a string according to the given + * `format(format, args, ...)` - Formats a string according to the given format. The syntax for the format is standard `sprintf` syntax. Good documentation for the syntax can be [found here](https://golang.org/pkg/fmt/). Example to zero-prefix a count, used commonly for naming servers: `format("web-%03d", count.index + 1)`. - * `formatlist(format, args...)` - Formats each element of a list + * `formatlist(format, args, ...)` - Formats each element of a list according to the given format, similarly to `format`, and returns a list. Non-list arguments are repeated for each list element. For example, to convert a list of DNS addresses to a list of URLs, you might use: @@ -171,7 +171,7 @@ The supported built-in functions are: * `${length("a,b,c")}` = 5 * `${length(map("key", "val"))}` = 1 - * `list(items...)` - Returns a list consisting of the arguments to the function. + * `list(items, ...)` - Returns a list consisting of the arguments to the function. This function provides a way of representing list literals in interpolation. * `${list("a", "b", "c")}` returns a list of `"a", "b", "c"`. * `${list()}` returns an empty list.