From 382e1ca8213fdda13ef77468ffba2e24a44db5ff Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sun, 26 May 2019 10:25:18 -0700 Subject: [PATCH] lang: yamldecode and yamlencode functions These follow the same principle as jsondecode and jsonencode, but use YAML instead of JSON. YAML has a much more complex information model than JSON, so we can only support a subset of it during decoding, but hopefully the subset supported here is a useful one. Because there are many different ways to _generate_ YAML, the yamlencode function is forced to make some decisions, and those decisions are likely to affect compatibility with other real-world YAML parsers. Although the format here is intended to be generic and compatible, we may find that there are problems with it that'll we'll want to adjust for in a future release, so yamlencode is therefore marked as experimental for now until the underlying library is ready to commit to ongoing byte-for-byte compatibility in serialization. The main use-case here is met by yamldecode, which will allow reading in files written in YAML format by humans for use in Terraform modules, in situations where a higher-level input format than direct Terraform language declarations is helpful. --- lang/functions.go | 3 + lang/functions_test.go | 23 ++++ .../functions/yamldecode.html.md | 105 ++++++++++++++++++ .../functions/yamlencode.html.md | 92 +++++++++++++++ website/layouts/functions.erb | 8 ++ 5 files changed, 231 insertions(+) create mode 100644 website/docs/configuration/functions/yamldecode.html.md create mode 100644 website/docs/configuration/functions/yamlencode.html.md diff --git a/lang/functions.go b/lang/functions.go index 5fe0558d847e..ba75bcf77975 100644 --- a/lang/functions.go +++ b/lang/functions.go @@ -3,6 +3,7 @@ package lang import ( "fmt" + ctyyaml "github.com/zclconf/go-cty-yaml" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" "github.com/zclconf/go-cty/cty/function/stdlib" @@ -116,6 +117,8 @@ func (s *Scope) Functions() map[string]function.Function { "urlencode": funcs.URLEncodeFunc, "uuid": funcs.UUIDFunc, "values": funcs.ValuesFunc, + "yamldecode": ctyyaml.YAMLDecodeFunc, + "yamlencode": ctyyaml.YAMLEncodeFunc, "zipmap": funcs.ZipmapFunc, } diff --git a/lang/functions_test.go b/lang/functions_test.go index 8ae3de12d1b2..780a0496c5e5 100644 --- a/lang/functions_test.go +++ b/lang/functions_test.go @@ -787,6 +787,29 @@ func TestFunctions(t *testing.T) { }, }, + "yamldecode": { + { + `yamldecode("true")`, + cty.True, + }, + }, + + "yamlencode": { + { + `yamlencode(["foo", "bar", true])`, + cty.StringVal("- \"foo\"\n- \"bar\"\n- true\n"), + }, + { + `yamlencode({a = "b", c = "d"})`, + cty.StringVal("\"a\": \"b\"\n\"c\": \"d\"\n"), + }, + { + `yamlencode(true)`, + // the ... here is an "end of document" marker, produced for implied primitive types only + cty.StringVal("true\n...\n"), + }, + }, + "zipmap": { { `zipmap(["hello", "bar"], ["world", "baz"])`, diff --git a/website/docs/configuration/functions/yamldecode.html.md b/website/docs/configuration/functions/yamldecode.html.md new file mode 100644 index 000000000000..e276d8585520 --- /dev/null +++ b/website/docs/configuration/functions/yamldecode.html.md @@ -0,0 +1,105 @@ +--- +layout: "functions" +page_title: "yamldecode - Functions - Configuration Language" +sidebar_current: "docs-funcs-encoding-yamldecode" +description: |- + The yamldecode function decodes a YAML string into a representation of its + value. +--- + +# `yamldecode` 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). + +`yamldecode` parses a string as a subset of YAML, and produces a representation +of its value. + +This function supports a subset of [YAML 1.2](https://yaml.org/spec/1.2/spec.html), +as described below. + +This function maps YAML values to +[Terraform language values](../expressions.html#types-and-values) +in the following way: + +| YAML type | Terraform type | +| ------------- | ------------------------------------------------------------------ | +| `!!str` | `string` | +| `!!float` | `number` | +| `!!int` | `number` | +| `!!bool` | `bool` | +| `!!map` | `object(...)` with attribute types determined per this table | +| `!!seq` | `tuple(...)` with element types determined per this table | +| `!!null` | The Terraform language `null` value | +| `!!timestamp` | `string` in [RFC 3339](https://tools.ietf.org/html/rfc3339) format | +| `!!binary` | `string` containing base64-encoded representation | + +The Terraform language automatic type conversion rules mean that you don't +usually need to worry about exactly what type is produced for a given value, +and can just use the result in an intuitive way. + +Note though that the mapping above is ambiguous -- several different source +types map to the same target type -- and so round-tripping through `yamldecode` +and then `yamlencode` cannot produce an identical result. + +YAML is a complex language and it supports a number of possibilities that the +Terraform language's type system cannot represent. Therefore this YAML decoder +supports only a subset of YAML 1.2, with restrictions including the following: + +- Although aliases to earlier anchors are supported, cyclic data structures + (where a reference to a collection appears inside that collection) are not. + If `yamldecode` detects such a structure then it will return an error. + +- Only the type tags shown in the above table (or equivalent alternative + representations of those same tags) are supported. Any other tags will + result in an error. + +- Only one YAML document is permitted. If multiple documents are present in + the given string then this function will return an error. + +## Examples + +``` +> yamldecode("{\"hello\": \"world\"}") +{ + "hello" = "world" +} + +> yamldecode("true") +true + +> yamldecode("{a: &foo [1, 2, 3], b: *foo}") +{ + "a" = [ + 1, + 2, + 3, + ] + "b" = [ + 1, + 2, + 3, + ] +} + +> yamldecode("{a: &foo [1, *foo, 3]}") + +Error: Error in function call + +Call to function "yamldecode" failed: cannot refer to anchor "foo" from inside +its own definition. + +> yamldecode("{a: !not-supported foo}") + +Error: Error in function call + +Call to function "yamldecode" failed: unsupported tag "!not-supported". +``` + +## Related Functions + +- [`jsondecode`](./jsondecode.html) is a similar operation using JSON instead + of YAML. +- [`yamlencode`](./yamlencode.html) performs the opposite operation, _encoding_ + a value as YAML. diff --git a/website/docs/configuration/functions/yamlencode.html.md b/website/docs/configuration/functions/yamlencode.html.md new file mode 100644 index 000000000000..c562f854b1e6 --- /dev/null +++ b/website/docs/configuration/functions/yamlencode.html.md @@ -0,0 +1,92 @@ +--- +layout: "functions" +page_title: "yamlencode - Functions - Configuration Language" +sidebar_current: "docs-funcs-encoding-yamlencode" +description: |- + The yamlencode function encodes a given value as a YAML string. +--- + +# `yamlencode` 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). + +`yamlencode` encodes a given value to a string using +[YAML 1.2](https://yaml.org/spec/1.2/spec.html) block syntax. + +~> **Warning:** This function is currently **experimental** and its exact +result format may change in future versions of Terraform, based on feedback. +Do not use `yamldecode` to construct a value for any resource argument where +changes to the result would be disruptive. To get a consistent string +representation of a value use [`jsonencode`](./jsonencode.html) instead; its +results are also valid YAML because YAML is a JSON superset. + + + +This function maps +[Terraform language values](../expressions.html#types-and-values) +to YAML tags in the following way: + +| Terraform type | YAML type | +| -------------- | -------------------- | +| `string` | `!!str` | +| `number` | `!!float` or `!!int` | +| `bool` | `!!bool` | +| `list(...)` | `!!seq` | +| `set(...)` | `!!seq` | +| `tuple(...)` | `!!seq` | +| `map(...)` | `!!map` | +| `object(...)` | `!!map` | +| Null value | `!!null` | + +`yamlencode` uses the implied syntaxes for all of the above types, so it does +not generate explicit YAML tags. + +Because the YAML format cannot fully represent all of the Terraform language +types, passing the `yamlencode` result to `yamldecode` will not produce an +identical value, but the Terraform language automatic type conversion rules +mean that this is rarely a problem in practice. + +## Examples + +``` +> yamlencode({"a":"b", "c":"d"}) +"a": "b" +"c": "d" + +> yamlencode({"foo":[1, 2, 3], "bar": "baz"}) +"bar": "baz" +"foo": +- 1 +- 2 +- 3 + +> yamlencode({"foo":[1, {"a":"b","c":"d"}, 3], "bar": "baz"}) +"bar": "baz" +"foo": +- 1 +- "a": "b" + "c": "d" +- 3 +``` + +`yamlencode` always uses YAML's "block style" for mappings and sequences, unless +the mapping or sequence is empty. To generate flow-style YAML, use +[`jsonencode`](./jsonencode.html) instead: YAML flow-style is a superset +of JSON syntax. + +## Related Functions + +- [`jsonencode`](./jsonencode.html) is a similar operation using JSON instead + of YAML. +- [`yamldecode`](./yamldecode.html) performs the opposite operation, _decoding_ + a YAML string to obtain its represented value. diff --git a/website/layouts/functions.erb b/website/layouts/functions.erb index 2e333d046978..6a8e3f438b97 100644 --- a/website/layouts/functions.erb +++ b/website/layouts/functions.erb @@ -257,6 +257,14 @@ urlencode +
  • + yamldecode +
  • + +
  • + yamlencode +
  • +