From 394cf7f25eddcaeddf45c581ca64d71135ab887b Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Mon, 29 Apr 2019 13:11:28 -0400 Subject: [PATCH] lang/funcs: add acc tests for functions (#21112) * lang/funcs: testing of functions through the lang package API The function-specific unit tests do not cover the HCL conversion that happens when the functions are called in a terraform configuration. For e.g., HCL converts sets to lists before passing it to the function. This means that we could not test passing a set in the function _unit_ tests. This adds a higher-level acceptance test, plus a check that every (pure) function has a test. * website/docs: update function documentation --- .tfdev | 12 + lang/funcs/collection.go | 6 +- lang/functions_test.go | 846 ++++++++++++++++++ lang/testdata/functions-test/hello.tmpl | 1 + lang/testdata/functions-test/hello.txt | 1 + .../functions/coalescelist.html.md | 8 +- .../configuration/functions/concat.html.md | 27 + .../configuration/functions/csvdecode.html.md | 12 +- website/layouts/functions.erb | 4 + 9 files changed, 905 insertions(+), 12 deletions(-) create mode 100644 .tfdev create mode 100644 lang/functions_test.go create mode 100644 lang/testdata/functions-test/hello.tmpl create mode 100644 lang/testdata/functions-test/hello.txt create mode 100644 website/docs/configuration/functions/concat.html.md diff --git a/.tfdev b/.tfdev new file mode 100644 index 000000000..35596fdbf --- /dev/null +++ b/.tfdev @@ -0,0 +1,12 @@ +version_info { + commit_var = "main.GitCommit" + version_var = "github.com/hashicorp/terraform/version.Version" + prerelease_var = "github.com/hashicorp/terraform/version.Prerelease" +} + +version_exec = false + +platform { + os = "darwin" + arch = "amd64" +} diff --git a/lang/funcs/collection.go b/lang/funcs/collection.go index f85bfae2f..ee3ad2b3a 100644 --- a/lang/funcs/collection.go +++ b/lang/funcs/collection.go @@ -257,8 +257,10 @@ var CompactFunc = function.New(&function.Spec{ }, }) -// ContainsFunc contructs a function that determines whether a given list contains -// a given single value as one of its elements. +// ContainsFunc contructs a function that determines whether a given list +// contains a given single value as one of its elements. +// +// ContainsFunc also works on sets, as HCL automatically converts sets to lists. var ContainsFunc = function.New(&function.Spec{ Params: []function.Parameter{ { diff --git a/lang/functions_test.go b/lang/functions_test.go new file mode 100644 index 000000000..9bab6887d --- /dev/null +++ b/lang/functions_test.go @@ -0,0 +1,846 @@ +package lang + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/hashicorp/hcl2/hcl" + "github.com/hashicorp/hcl2/hcl/hclsyntax" + homedir "github.com/mitchellh/go-homedir" + "github.com/zclconf/go-cty/cty" +) + +// TestFunctions tests that functions are callable through the functionality +// in the langs package, via HCL. +// +// These tests are primarily here to assert that the functions are properly +// registered in the functions table, rather than to test all of the details +// of the functions. Each function should only have one or two tests here, +// since the main set of unit tests for a function should live alongside that +// function either in the "funcs" subdirectory here or over in the cty +// function/stdlib package. +// +// One exception to that is we can use this test mechanism to assert common +// patterns that are used in real-world configurations which rely on behaviors +// implemented either in this lang package or in HCL itself, such as automatic +// type conversions. The function unit tests don't cover those things because +// they call directly into the functions. +// +// With that said then, this test function should contain at least one simple +// test case per function registered in the functions table (just to prove +// it really is registered correctly) and possibly a small set of additional +// functions showing real-world use-cases that rely on type conversion +// behaviors. +func TestFunctions(t *testing.T) { + // used in `pathexpand()` test + homePath, err := homedir.Dir() + if err != nil { + t.Fatalf("Error getting home directory: %v", err) + } + + tests := map[string][]struct { + src string + want cty.Value + }{ + // Please maintain this list in alphabetical order by function, with + // a blank line between the group of tests for each function. + + "abs": { + { + `abs(-1)`, + cty.NumberIntVal(1), + }, + }, + + "base64decode": { + { + `base64decode("YWJjMTIzIT8kKiYoKSctPUB+")`, + cty.StringVal("abc123!?$*&()'-=@~"), + }, + }, + + "base64encode": { + { + `base64encode("abc123!?$*&()'-=@~")`, + cty.StringVal("YWJjMTIzIT8kKiYoKSctPUB+"), + }, + }, + + "base64gzip": { + { + `base64gzip("test")`, + cty.StringVal("H4sIAAAAAAAA/ypJLS4BAAAA//8BAAD//wx+f9gEAAAA"), + }, + }, + + "base64sha256": { + { + `base64sha256("test")`, + cty.StringVal("n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="), + }, + }, + + "base64sha512": { + { + `base64sha512("test")`, + cty.StringVal("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="), + }, + }, + + "basename": { + { + `basename("testdata/hello.txt")`, + cty.StringVal("hello.txt"), + }, + }, + + "ceil": { + { + `ceil(1.2)`, + cty.NumberIntVal(2), + }, + }, + + "chomp": { + { + `chomp("goodbye\ncruel\nworld\n")`, + cty.StringVal("goodbye\ncruel\nworld"), + }, + }, + + "chunklist": { + { + `chunklist(["a", "b", "c"], 1)`, + cty.ListVal([]cty.Value{ + cty.ListVal([]cty.Value{ + cty.StringVal("a"), + }), + cty.ListVal([]cty.Value{ + cty.StringVal("b"), + }), + cty.ListVal([]cty.Value{ + cty.StringVal("c"), + }), + }), + }, + }, + + "cidrhost": { + { + `cidrhost("192.168.1.0/24", 5)`, + cty.StringVal("192.168.1.5"), + }, + }, + + "cidrnetmask": { + { + `cidrnetmask("192.168.1.0/24")`, + cty.StringVal("255.255.255.0"), + }, + }, + + "cidrsubnet": { + { + `cidrsubnet("192.168.2.0/20", 4, 6)`, + cty.StringVal("192.168.6.0/24"), + }, + }, + + "coalesce": { + { + `coalesce("first", "second", "third")`, + cty.StringVal("first"), + }, + + { + `coalescelist(["first", "second"], ["third", "fourth"])`, + cty.ListVal([]cty.Value{ + cty.StringVal("first"), cty.StringVal("second"), + }), + }, + }, + + "coalescelist": { + { + `coalescelist(["a", "b"], ["c", "d"])`, + cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + }), + }, + }, + + "compact": { + { + `compact(["test", "", "test"])`, + cty.ListVal([]cty.Value{ + cty.StringVal("test"), cty.StringVal("test"), + }), + }, + }, + + "concat": { + { + `concat(["a", ""], ["b", "c"])`, + cty.TupleVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal(""), + cty.StringVal("b"), + cty.StringVal("c"), + }), + }, + }, + + "contains": { + { + `contains(["a", "b"], "a")`, + cty.True, + }, + { // Should also work with sets, due to automatic conversion + `contains(toset(["a", "b"]), "a")`, + cty.True, + }, + }, + + "csvdecode": { + { + `csvdecode("a,b,c\n1,2,3\n4,5,6")`, + cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("1"), + "b": cty.StringVal("2"), + "c": cty.StringVal("3"), + }), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("4"), + "b": cty.StringVal("5"), + "c": cty.StringVal("6"), + }), + }), + }, + }, + + "dirname": { + { + `dirname("testdata/hello.txt")`, + cty.StringVal("testdata"), + }, + }, + + "distinct": { + { + `distinct(["a", "b", "a", "b"])`, + cty.ListVal([]cty.Value{ + cty.StringVal("a"), cty.StringVal("b"), + }), + }, + }, + + "element": { + { + `element(["hello"], 0)`, + cty.StringVal("hello"), + }, + }, + + "file": { + { + `file("hello.txt")`, + cty.StringVal("hello!"), + }, + }, + + "fileexists": { + { + `fileexists("hello.txt")`, + cty.BoolVal(true), + }, + }, + + "filebase64": { + { + `filebase64("hello.txt")`, + cty.StringVal("aGVsbG8h"), + }, + }, + + "filebase64sha256": { + { + `filebase64sha256("hello.txt")`, + cty.StringVal("zgYJL7lI2f+sfRo3bkBLJrdXW8wR7gWkYV/vT+w6MIs="), + }, + }, + + "filebase64sha512": { + { + `filebase64sha512("hello.txt")`, + cty.StringVal("xvgdsOn4IGyXHJ5YJuO6gj/7saOpAPgEdlKov3jqmP38dFhVo4U6Y1Z1RY620arxIJ6I6tLRkjgrXEy91oUOAg=="), + }, + }, + + "filemd5": { + { + `filemd5("hello.txt")`, + cty.StringVal("5a8dd3ad0756a93ded72b823b19dd877"), + }, + }, + + "filesha1": { + { + `filesha1("hello.txt")`, + cty.StringVal("8f7d88e901a5ad3a05d8cc0de93313fd76028f8c"), + }, + }, + + "filesha256": { + { + `filesha256("hello.txt")`, + cty.StringVal("ce06092fb948d9ffac7d1a376e404b26b7575bcc11ee05a4615fef4fec3a308b"), + }, + }, + + "filesha512": { + { + `filesha512("hello.txt")`, + cty.StringVal("c6f81db0e9f8206c971c9e5826e3ba823ffbb1a3a900f8047652a8bf78ea98fdfc745855a3853a635675458eb6d1aaf1209e88ead2d192382b5c4cbdd6850e02"), + }, + }, + + "flatten": { + { + `flatten([tolist(["a", "b"]), tolist(["c", "d"])])`, + cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + cty.StringVal("c"), + cty.StringVal("d"), + }), + }, + }, + + "floor": { + { + `floor(-1.8)`, + cty.NumberFloatVal(-2), + }, + }, + + "format": { + { + `format("Hello, %s!", "Ander")`, + cty.StringVal("Hello, Ander!"), + }, + }, + + "formatlist": { + { + `formatlist("Hello, %s!", ["Valentina", "Ander", "Olivia", "Sam"])`, + cty.ListVal([]cty.Value{ + cty.StringVal("Hello, Valentina!"), + cty.StringVal("Hello, Ander!"), + cty.StringVal("Hello, Olivia!"), + cty.StringVal("Hello, Sam!"), + }), + }, + }, + + "formatdate": { + { + `formatdate("DD MMM YYYY hh:mm ZZZ", "2018-01-04T23:12:01Z")`, + cty.StringVal("04 Jan 2018 23:12 UTC"), + }, + }, + + "indent": { + { + fmt.Sprintf("indent(4, %#v)", Poem), + cty.StringVal("Fleas:\n Adam\n Had'em\n \n E.E. Cummings"), + }, + }, + + "index": { + { + `index(["a", "b", "c"], "a")`, + cty.NumberIntVal(0), + }, + }, + + "join": { + { + `join(" ", ["Hello", "World"])`, + cty.StringVal("Hello World"), + }, + }, + + "jsondecode": { + { + `jsondecode("{\"hello\": \"world\"}")`, + cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("world"), + }), + }, + }, + + "jsonencode": { + { + `jsonencode({"hello"="world"})`, + cty.StringVal("{\"hello\":\"world\"}"), + }, + }, + + "keys": { + { + `keys({"hello"=1, "goodbye"=42})`, + cty.TupleVal([]cty.Value{ + cty.StringVal("goodbye"), + cty.StringVal("hello"), + }), + }, + }, + + "length": { + { + `length(["the", "quick", "brown", "bear"])`, + cty.NumberIntVal(4), + }, + }, + + "list": { + { + `list("hello")`, + cty.ListVal([]cty.Value{ + cty.StringVal("hello"), + }), + }, + }, + + "log": { + { + `log(1, 10)`, + cty.NumberFloatVal(0), + }, + }, + + "lookup": { + { + `lookup({hello=1, goodbye=42}, "goodbye")`, + cty.NumberIntVal(42), + }, + }, + + "lower": { + { + `lower("HELLO")`, + cty.StringVal("hello"), + }, + }, + + "map": { + { + `map("hello", "world")`, + cty.MapVal(map[string]cty.Value{ + "hello": cty.StringVal("world"), + }), + }, + }, + + "matchkeys": { + { + `matchkeys(["a", "b", "c"], ["ref1", "ref2", "ref3"], ["ref1"])`, + cty.ListVal([]cty.Value{ + cty.StringVal("a"), + }), + }, + }, + + "max": { + { + `max(12, 54, 3)`, + cty.NumberIntVal(54), + }, + }, + + "md5": { + { + `md5("tada")`, + cty.StringVal("ce47d07243bb6eaf5e1322c81baf9bbf"), + }, + }, + + "merge": { + { + `merge({"a"="b"}, {"c"="d"})`, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("b"), + "c": cty.StringVal("d"), + }), + }, + }, + + "min": { + { + `min(12, 54, 3)`, + cty.NumberIntVal(3), + }, + }, + + "pathexpand": { + { + `pathexpand("~/test-file")`, + cty.StringVal(filepath.Join(homePath, "test-file")), + }, + }, + + "pow": { + { + `pow(1,0)`, + cty.NumberFloatVal(1), + }, + }, + + "replace": { + { + `replace("hello", "hel", "bel")`, + cty.StringVal("bello"), + }, + }, + + "reverse": { + { + `reverse(["a", true, 0])`, + cty.TupleVal([]cty.Value{cty.Zero, cty.True, cty.StringVal("a")}), + }, + }, + + "rsadecrypt": { + { + fmt.Sprintf("rsadecrypt(%#v, %#v)", CipherBase64, PrivateKey), + cty.StringVal("message"), + }, + }, + + "sethaselement": { + { + `sethaselement(["a", "b"], "b")`, + cty.BoolVal(true), + }, + }, + + "setintersection": { + { + `setintersection(["a", "b"], ["b", "c"], ["b", "d"])`, + cty.SetVal([]cty.Value{ + cty.StringVal("b"), + }), + }, + }, + + "setproduct": { + { + `setproduct(["development", "staging", "production"], ["app1", "app2"])`, + cty.ListVal([]cty.Value{ + cty.TupleVal([]cty.Value{cty.StringVal("development"), cty.StringVal("app1")}), + cty.TupleVal([]cty.Value{cty.StringVal("development"), cty.StringVal("app2")}), + cty.TupleVal([]cty.Value{cty.StringVal("staging"), cty.StringVal("app1")}), + cty.TupleVal([]cty.Value{cty.StringVal("staging"), cty.StringVal("app2")}), + cty.TupleVal([]cty.Value{cty.StringVal("production"), cty.StringVal("app1")}), + cty.TupleVal([]cty.Value{cty.StringVal("production"), cty.StringVal("app2")}), + }), + }, + }, + + "setunion": { + { + `setunion(["a", "b"], ["b", "c"], ["d"])`, + cty.SetVal([]cty.Value{ + cty.StringVal("d"), + cty.StringVal("b"), + cty.StringVal("a"), + cty.StringVal("c"), + }), + }, + }, + + "sha1": { + { + `sha1("test")`, + cty.StringVal("a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"), + }, + }, + + "sha256": { + { + `sha256("test")`, + cty.StringVal("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"), + }, + }, + + "sha512": { + { + `sha512("test")`, + cty.StringVal("ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff"), + }, + }, + + "signum": { + { + `signum(12)`, + cty.NumberFloatVal(1), + }, + }, + + "slice": { + { + `slice(["a", "b", "c", "d"], 1, 3)`, + cty.ListVal([]cty.Value{ + cty.StringVal("b"), cty.StringVal("c"), + }), + }, + }, + + "sort": { + { + `sort(["banana", "apple"])`, + cty.ListVal([]cty.Value{ + cty.StringVal("apple"), + cty.StringVal("banana"), + }), + }, + }, + + "split": { + { + `split(" ", "Hello World")`, + cty.ListVal([]cty.Value{ + cty.StringVal("Hello"), + cty.StringVal("World"), + }), + }, + }, + + "strrev": { + { + `strrev("hello world")`, + cty.StringVal("dlrow olleh"), + }, + }, + + "substr": { + { + `substr("hello world", 1, 4)`, + cty.StringVal("ello"), + }, + }, + + "templatefile": { + { + `templatefile("hello.tmpl", {name = "Jodie"})`, + cty.StringVal("Hello, Jodie!"), + }, + }, + + "timeadd": { + { + `timeadd("2017-11-22T00:00:00Z", "1s")`, + cty.StringVal("2017-11-22T00:00:01Z"), + }, + }, + + "title": { + { + `title("hello")`, + cty.StringVal("Hello"), + }, + }, + + "tobool": { + { + `tobool("false")`, + cty.False, + }, + }, + + "tolist": { + { + `tolist(["a", "b", "c"])`, + cty.ListVal([]cty.Value{ + cty.StringVal("a"), cty.StringVal("b"), cty.StringVal("c"), + }), + }, + }, + + "tomap": { + { + `tomap({"a" = 1, "b" = 2})`, + cty.MapVal(map[string]cty.Value{ + "a": cty.NumberIntVal(1), + "b": cty.NumberIntVal(2), + }), + }, + }, + + "tonumber": { + { + `tonumber("42")`, + cty.NumberIntVal(42), + }, + }, + + "toset": { + { + `toset(["a", "b", "c"])`, + cty.SetVal([]cty.Value{ + cty.StringVal("a"), cty.StringVal("b"), cty.StringVal("c"), + }), + }, + }, + + "tostring": { + { + `tostring("a")`, + cty.StringVal("a"), + }, + }, + + "transpose": { + { + `transpose({"a" = ["1", "2"], "b" = ["2", "3"]})`, + cty.MapVal(map[string]cty.Value{ + "1": cty.ListVal([]cty.Value{cty.StringVal("a")}), + "2": cty.ListVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")}), + "3": cty.ListVal([]cty.Value{cty.StringVal("b")}), + }), + }, + }, + + "trimspace": { + { + `trimspace(" hello ")`, + cty.StringVal("hello"), + }, + }, + + "upper": { + { + `upper("hello")`, + cty.StringVal("HELLO"), + }, + }, + + "urlencode": { + { + `urlencode("foo:bar@localhost?foo=bar&bar=baz")`, + cty.StringVal("foo%3Abar%40localhost%3Ffoo%3Dbar%26bar%3Dbaz"), + }, + }, + + "values": { + { + `values({"hello"="world", "what's"="up"})`, + cty.TupleVal([]cty.Value{ + cty.StringVal("world"), + cty.StringVal("up"), + }), + }, + }, + + "zipmap": { + { + `zipmap(["hello", "bar"], ["world", "baz"])`, + cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("world"), + "bar": cty.StringVal("baz"), + }), + }, + }, + } + + data := &dataForTests{} // no variables available; we only need literals here + scope := &Scope{ + Data: data, + BaseDir: "./testdata/functions-test", // for the functions that read from the filesystem + } + + // Check that there is at least one test case for each function, omitting + // those functions that do not return consistent values + allFunctions := scope.Functions() + + // TODO: we can test the impure functions partially by configuring the scope + // with PureOnly: true and then verify that they return unknown values of a + // suitable type. + for _, impureFunc := range impureFunctions { + delete(allFunctions, impureFunc) + } + for f, _ := range scope.Functions() { + if _, ok := tests[f]; !ok { + t.Errorf("Missing test for function %s\n", f) + } + } + + for funcName, funcTests := range tests { + t.Run(funcName, func(t *testing.T) { + for _, test := range funcTests { + t.Run(test.src, func(t *testing.T) { + expr, parseDiags := hclsyntax.ParseExpression([]byte(test.src), "test.hcl", hcl.Pos{Line: 1, Column: 1}) + if parseDiags.HasErrors() { + for _, diag := range parseDiags { + t.Error(diag.Error()) + } + return + } + + got, diags := scope.EvalExpr(expr, cty.DynamicPseudoType) + if diags.HasErrors() { + for _, diag := range diags { + t.Errorf("%s: %s", diag.Description().Summary, diag.Description().Detail) + } + return + } + + if !test.want.RawEquals(got) { + t.Errorf("wrong result\nexpr: %s\ngot: %#v\nwant: %#v", test.src, got, test.want) + } + }) + } + }) + } +} + +const ( + CipherBase64 = "eczGaDhXDbOFRZGhjx2etVzWbRqWDlmq0bvNt284JHVbwCgObiuyX9uV0LSAMY707IEgMkExJqXmsB4OWKxvB7epRB9G/3+F+pcrQpODlDuL9oDUAsa65zEpYF0Wbn7Oh7nrMQncyUPpyr9WUlALl0gRWytOA23S+y5joa4M34KFpawFgoqTu/2EEH4Xl1zo+0fy73fEto+nfkUY+meuyGZ1nUx/+DljP7ZqxHBFSlLODmtuTMdswUbHbXbWneW51D7Jm7xB8nSdiA2JQNK5+Sg5x8aNfgvFTt/m2w2+qpsyFa5Wjeu6fZmXSl840CA07aXbk9vN4I81WmJyblD/ZA==" + PrivateKey = ` +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAgUElV5mwqkloIrM8ZNZ72gSCcnSJt7+/Usa5G+D15YQUAdf9 +c1zEekTfHgDP+04nw/uFNFaE5v1RbHaPxhZYVg5ZErNCa/hzn+x10xzcepeS3KPV +Xcxae4MR0BEegvqZqJzN9loXsNL/c3H/B+2Gle3hTxjlWFb3F5qLgR+4Mf4ruhER +1v6eHQa/nchi03MBpT4UeJ7MrL92hTJYLdpSyCqmr8yjxkKJDVC2uRrr+sTSxfh7 +r6v24u/vp/QTmBIAlNPgadVAZw17iNNb7vjV7Gwl/5gHXonCUKURaV++dBNLrHIZ +pqcAM8wHRph8mD1EfL9hsz77pHewxolBATV+7QIDAQABAoIBAC1rK+kFW3vrAYm3 ++8/fQnQQw5nec4o6+crng6JVQXLeH32qXShNf8kLLG/Jj0vaYcTPPDZw9JCKkTMQ +0mKj9XR/5DLbBMsV6eNXXuvJJ3x4iKW5eD9WkLD4FKlNarBRyO7j8sfPTqXW7uat +NxWdFH7YsSRvNh/9pyQHLWA5OituidMrYbc3EUx8B1GPNyJ9W8Q8znNYLfwYOjU4 +Wv1SLE6qGQQH9Q0WzA2WUf8jklCYyMYTIywAjGb8kbAJlKhmj2t2Igjmqtwt1PYc +pGlqbtQBDUiWXt5S4YX/1maIQ/49yeNUajjpbJiH3DbhJbHwFTzP3pZ9P9GHOzlG +kYR+wSECgYEAw/Xida8kSv8n86V3qSY/I+fYQ5V+jDtXIE+JhRnS8xzbOzz3v0WS +Oo5H+o4nJx5eL3Ghb3Gcm0Jn46dHrxinHbm+3RjXv/X6tlbxIYjRSQfHOTSMCTvd +qcliF5vC6RCLXuc7R+IWR1Ky6eDEZGtrvt3DyeYABsp9fRUFR/6NluUCgYEAqNsw +1aSl7WJa27F0DoJdlU9LWerpXcazlJcIdOz/S9QDmSK3RDQTdqfTxRmrxiYI9LEs +mkOkvzlnnOBMpnZ3ZOU5qIRfprecRIi37KDAOHWGnlC0EWGgl46YLb7/jXiWf0AG +Y+DfJJNd9i6TbIDWu8254/erAS6bKMhW/3q7f2kCgYAZ7Id/BiKJAWRpqTRBXlvw +BhXoKvjI2HjYP21z/EyZ+PFPzur/lNaZhIUlMnUfibbwE9pFggQzzf8scM7c7Sf+ +mLoVSdoQ/Rujz7CqvQzi2nKSsM7t0curUIb3lJWee5/UeEaxZcmIufoNUrzohAWH +BJOIPDM4ssUTLRq7wYM9uQKBgHCBau5OP8gE6mjKuXsZXWUoahpFLKwwwmJUp2vQ +pOFPJ/6WZOlqkTVT6QPAcPUbTohKrF80hsZqZyDdSfT3peFx4ZLocBrS56m6NmHR +UYHMvJ8rQm76T1fryHVidz85g3zRmfBeWg8yqT5oFg4LYgfLsPm1gRjOhs8LfPvI +OLlRAoGBAIZ5Uv4Z3s8O7WKXXUe/lq6j7vfiVkR1NW/Z/WLKXZpnmvJ7FgxN4e56 +RXT7GwNQHIY8eDjDnsHxzrxd+raOxOZeKcMHj3XyjCX3NHfTscnsBPAGYpY/Wxzh +T8UYnFu6RzkixElTf2rseEav7rkdKkI3LAeIZy7B0HulKKsmqVQ7 +-----END RSA PRIVATE KEY----- +` + Poem = `Fleas: +Adam +Had'em + +E.E. Cummings` +) diff --git a/lang/testdata/functions-test/hello.tmpl b/lang/testdata/functions-test/hello.tmpl new file mode 100644 index 000000000..f112ef899 --- /dev/null +++ b/lang/testdata/functions-test/hello.tmpl @@ -0,0 +1 @@ +Hello, ${name}! \ No newline at end of file diff --git a/lang/testdata/functions-test/hello.txt b/lang/testdata/functions-test/hello.txt new file mode 100644 index 000000000..3462721fd --- /dev/null +++ b/lang/testdata/functions-test/hello.txt @@ -0,0 +1 @@ +hello! \ No newline at end of file diff --git a/website/docs/configuration/functions/coalescelist.html.md b/website/docs/configuration/functions/coalescelist.html.md index ff67c1d22..aa265aa7d 100644 --- a/website/docs/configuration/functions/coalescelist.html.md +++ b/website/docs/configuration/functions/coalescelist.html.md @@ -19,23 +19,23 @@ that isn't empty. ## Examples ``` -> coalesce(["a", "b"], ["c", "d"]) +> coalescelist(["a", "b"], ["c", "d"]) [ "a", "b", ] -> coalesce([], ["c", "d"]) +> coalescelist([], ["c", "d"]) [ "c", "d", ] ``` -To perform the `coalesce` operation with a list of lists, use the `...` +To perform the `coalescelist` operation with a list of lists, use the `...` symbol to expand the outer list as arguments: ``` -> coalesce([[], ["c", "d"]]...) +> coalescelist([[], ["c", "d"]]...) [ "c", "d", diff --git a/website/docs/configuration/functions/concat.html.md b/website/docs/configuration/functions/concat.html.md new file mode 100644 index 000000000..032ec8c8b --- /dev/null +++ b/website/docs/configuration/functions/concat.html.md @@ -0,0 +1,27 @@ +--- +layout: "functions" +page_title: "concat - Functions - Configuration Language" +sidebar_current: "docs-funcs-collection-concat" +description: |- + The concat function combines two or more lists into a single list. +--- + +# `concat` Function + +-> **Note:** This page is about Terraform 0.12 and later. For Terraform 0.11 and +earlier, see +[0.11 Configuration Language: Interpolation Syntax](../../configuration-0-11/interpolation.html). + +`concat` takes two or more lists and combines them into a single list. + +## Examples + +``` +> concat(["a", ""], ["b", "c"]) +[ + "a", + "", + "b", + "c", +] +``` diff --git a/website/docs/configuration/functions/csvdecode.html.md b/website/docs/configuration/functions/csvdecode.html.md index 8c90eff7a..799363d07 100644 --- a/website/docs/configuration/functions/csvdecode.html.md +++ b/website/docs/configuration/functions/csvdecode.html.md @@ -31,14 +31,14 @@ number of fields, or this function will produce an error. > csvdecode("a,b,c\n1,2,3\n4,5,6") [ { - "a" = 1 - "b" = 2 - "c" = 3 + "a" = "1" + "b" = "2" + "c" = "3" }, { - "a" = 4 - "b" = 5 - "c" = 6 + "a" = "4" + "b" = "5" + "c" = "6" } ] ``` diff --git a/website/layouts/functions.erb b/website/layouts/functions.erb index 836567372..848a76a51 100644 --- a/website/layouts/functions.erb +++ b/website/layouts/functions.erb @@ -130,6 +130,10 @@ compact +
  • + concat +
  • +
  • contains