diff --git a/internal/builtin/providers/terraform/functions.go b/internal/builtin/providers/terraform/functions.go new file mode 100644 index 000000000000..3a6171e39e15 --- /dev/null +++ b/internal/builtin/providers/terraform/functions.go @@ -0,0 +1,177 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "bytes" + "fmt" + "sort" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" +) + +var functions = map[string]func([]cty.Value) (cty.Value, error){ + "tfvarsencode": tfvarsencodeFunc, + "tfvarsdecode": tfvarsdecodeFunc, + "exprencode": exprencodeFunc, +} + +func tfvarsencodeFunc(args []cty.Value) (cty.Value, error) { + // These error checks should not be hit in practice because the language + // runtime should check them before calling, so this is just for robustness + // and completeness. + if len(args) > 1 { + return cty.NilVal, function.NewArgErrorf(1, "too many arguments; only one expected") + } + if len(args) == 0 { + return cty.NilVal, fmt.Errorf("exactly one argument is required") + } + + v := args[0] + ty := v.Type() + + if v.IsNull() { + // Our functions schema does not say we allow null values, so we should + // not get to this error message if the caller respects the schema. + return cty.NilVal, function.NewArgErrorf(1, "cannot encode a null value in tfvars syntax") + } + if !v.IsWhollyKnown() { + return cty.UnknownVal(cty.String).RefineNotNull(), nil + } + + var keys []string + switch { + case ty.IsObjectType(): + atys := ty.AttributeTypes() + keys = make([]string, 0, len(atys)) + for key := range atys { + keys = append(keys, key) + } + case ty.IsMapType(): + keys = make([]string, 0, v.LengthInt()) + for it := v.ElementIterator(); it.Next(); { + k, _ := it.Element() + keys = append(keys, k.AsString()) + } + default: + return cty.NilVal, function.NewArgErrorf(1, "invalid value to encode: must be an object whose attribute names will become the encoded variable names") + } + sort.Strings(keys) + + f := hclwrite.NewEmptyFile() + body := f.Body() + for _, key := range keys { + if !hclsyntax.ValidIdentifier(key) { + // We can only encode valid identifiers as tfvars keys, since + // the HCL argument grammar requires them to be identifiers. + return cty.NilVal, function.NewArgErrorf(1, "invalid variable name %q: must be a valid identifier, per Terraform's rules for input variable declarations", key) + } + + // This index should not fail because we know that "key" is a valid + // index from the logic above. + v, _ := hcl.Index(v, cty.StringVal(key), nil) + body.SetAttributeValue(key, v) + } + + result := f.Bytes() + return cty.StringVal(string(result)), nil +} + +func tfvarsdecodeFunc(args []cty.Value) (cty.Value, error) { + // These error checks should not be hit in practice because the language + // runtime should check them before calling, so this is just for robustness + // and completeness. + if len(args) > 1 { + return cty.NilVal, function.NewArgErrorf(1, "too many arguments; only one expected") + } + if len(args) == 0 { + return cty.NilVal, fmt.Errorf("exactly one argument is required") + } + if args[0].Type() != cty.String { + return cty.NilVal, fmt.Errorf("argument must be a string") + } + if args[0].IsNull() { + return cty.NilVal, fmt.Errorf("cannot decode tfvars from a null value") + } + if !args[0].IsKnown() { + // If our input isn't known then we can't even predict the result + // type, since it will be an object type decided based on which + // arguments and values we find in the string. + return cty.DynamicVal, nil + } + + // If we get here then we know that: + // - there's exactly one element in args + // - it's a string + // - it is known and non-null + // So therefore the following is guaranteed to succeed. + src := []byte(args[0].AsString()) + + // As usual when we wrap HCL stuff up in functions, we end up needing to + // stuff HCL diagnostics into plain string error messages. This produces + // a non-ideal result but is still better than hiding the HCL-provided + // diagnosis altogether. + f, hclDiags := hclsyntax.ParseConfig(src, "", hcl.InitialPos) + if hclDiags.HasErrors() { + return cty.NilVal, fmt.Errorf("invalid tfvars syntax: %s", hclDiags.Error()) + } + attrs, hclDiags := f.Body.JustAttributes() + if hclDiags.HasErrors() { + return cty.NilVal, fmt.Errorf("invalid tfvars content: %s", hclDiags.Error()) + } + retAttrs := make(map[string]cty.Value, len(attrs)) + for name, attr := range attrs { + // Evaluating the expression with no EvalContext achieves the same + // interpretation as Terraform CLI makes of .tfvars files, rejecting + // any function calls or references to symbols. + v, hclDiags := attr.Expr.Value(nil) + if hclDiags.HasErrors() { + return cty.NilVal, fmt.Errorf("invalid expression for variable %q: %s", name, hclDiags.Error()) + } + retAttrs[name] = v + } + + return cty.ObjectVal(retAttrs), nil +} + +func exprencodeFunc(args []cty.Value) (cty.Value, error) { + // These error checks should not be hit in practice because the language + // runtime should check them before calling, so this is just for robustness + // and completeness. + if len(args) > 1 { + return cty.NilVal, function.NewArgErrorf(1, "too many arguments; only one expected") + } + if len(args) == 0 { + return cty.NilVal, fmt.Errorf("exactly one argument is required") + } + + v := args[0] + if !v.IsWhollyKnown() { + ret := cty.UnknownVal(cty.String).RefineNotNull() + // For some types we can refine further due to the HCL grammar, + // as long as w eknow the value isn't null. + if !v.Range().CouldBeNull() { + ty := v.Type() + switch { + case ty.IsObjectType() || ty.IsMapType(): + ret = ret.Refine().StringPrefixFull("{").NewValue() + case ty.IsTupleType() || ty.IsListType() || ty.IsSetType(): + ret = ret.Refine().StringPrefixFull("[").NewValue() + case ty == cty.String: + ret = ret.Refine().StringPrefixFull(`"`).NewValue() + } + } + return ret, nil + } + + // This bytes.TrimSpace is to ensure that future changes to HCL, that + // might for some reason add extra spaces before the expression (!) + // can't invalidate our unknown value prefix refinements above. + src := bytes.TrimSpace(hclwrite.TokensForValue(v).Bytes()) + return cty.StringVal(string(src)), nil +} diff --git a/internal/builtin/providers/terraform/functions_test.go b/internal/builtin/providers/terraform/functions_test.go new file mode 100644 index 000000000000..9f8def9d57ac --- /dev/null +++ b/internal/builtin/providers/terraform/functions_test.go @@ -0,0 +1,345 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/providers" +) + +func TestTfvarsencode(t *testing.T) { + tableTestFunction(t, "tfvarsencode", []functionTest{ + { + Input: cty.ObjectVal(map[string]cty.Value{ + "string": cty.StringVal("hello"), + "number": cty.NumberIntVal(5), + "bool": cty.True, + "set": cty.SetVal([]cty.Value{cty.StringVal("beep"), cty.StringVal("boop")}), + "list": cty.SetVal([]cty.Value{cty.StringVal("bleep"), cty.StringVal("bloop")}), + "tuple": cty.SetVal([]cty.Value{cty.StringVal("bibble"), cty.StringVal("wibble")}), + "map": cty.MapVal(map[string]cty.Value{"one": cty.NumberIntVal(1)}), + "object": cty.ObjectVal(map[string]cty.Value{"one": cty.NumberIntVal(1), "true": cty.True}), + "null": cty.NullVal(cty.String), + }), + Want: cty.StringVal( + `bool = true +list = ["bleep", "bloop"] +map = { + one = 1 +} +null = null +number = 5 +object = { + one = 1 + true = true +} +set = ["beep", "boop"] +string = "hello" +tuple = ["bibble", "wibble"] +`), + }, + { + Input: cty.EmptyObjectVal, + Want: cty.StringVal(``), + }, + { + Input: cty.MapVal(map[string]cty.Value{ + "one": cty.NumberIntVal(1), + "two": cty.NumberIntVal(2), + "three": cty.NumberIntVal(3), + }), + Want: cty.StringVal( + `one = 1 +three = 3 +two = 2 +`), + }, + { + Input: cty.MapValEmpty(cty.String), + Want: cty.StringVal(``), + }, + { + Input: cty.UnknownVal(cty.EmptyObject), + Want: cty.UnknownVal(cty.String).RefineNotNull(), + }, + { + Input: cty.UnknownVal(cty.Map(cty.String)), + Want: cty.UnknownVal(cty.String).RefineNotNull(), + }, + { + Input: cty.ObjectVal(map[string]cty.Value{ + "string": cty.UnknownVal(cty.String), + }), + Want: cty.UnknownVal(cty.String).RefineNotNull(), + }, + { + Input: cty.MapVal(map[string]cty.Value{ + "string": cty.UnknownVal(cty.String), + }), + Want: cty.UnknownVal(cty.String).RefineNotNull(), + }, + { + Input: cty.NullVal(cty.EmptyObject), + WantErr: `cannot encode a null value in tfvars syntax`, + }, + { + Input: cty.NullVal(cty.Map(cty.String)), + WantErr: `cannot encode a null value in tfvars syntax`, + }, + { + Input: cty.StringVal("nope"), + WantErr: `invalid value to encode: must be an object whose attribute names will become the encoded variable names`, + }, + { + Input: cty.Zero, + WantErr: `invalid value to encode: must be an object whose attribute names will become the encoded variable names`, + }, + { + Input: cty.False, + WantErr: `invalid value to encode: must be an object whose attribute names will become the encoded variable names`, + }, + { + Input: cty.ListValEmpty(cty.String), + WantErr: `invalid value to encode: must be an object whose attribute names will become the encoded variable names`, + }, + { + Input: cty.SetValEmpty(cty.String), + WantErr: `invalid value to encode: must be an object whose attribute names will become the encoded variable names`, + }, + { + Input: cty.EmptyTupleVal, + WantErr: `invalid value to encode: must be an object whose attribute names will become the encoded variable names`, + }, + { + Input: cty.ObjectVal(map[string]cty.Value{ + "not valid identifier": cty.StringVal("!"), + }), + WantErr: `invalid variable name "not valid identifier": must be a valid identifier, per Terraform's rules for input variable declarations`, + }, + }) +} + +func TestTfvarsdecode(t *testing.T) { + tableTestFunction(t, "tfvarsdecode", []functionTest{ + { + Input: cty.StringVal(`string = "hello" +number = 2`), + Want: cty.ObjectVal(map[string]cty.Value{ + "string": cty.StringVal("hello"), + "number": cty.NumberIntVal(2), + }), + }, + { + Input: cty.StringVal(``), + Want: cty.EmptyObjectVal, + }, + { + Input: cty.UnknownVal(cty.String), + Want: cty.UnknownVal(cty.DynamicPseudoType), + }, + { + Input: cty.NullVal(cty.String), + WantErr: `cannot decode tfvars from a null value`, + }, + { + Input: cty.StringVal(`not valid syntax`), + // This is actually not a very good diagnosis for this error, + // since we're expecting HCL arguments rather than HCL blocks, + // but that's something we'd need to address in HCL. + WantErr: `invalid tfvars syntax: :1,17-17: Invalid block definition; Either a quoted string block label or an opening brace ("{") is expected here.`, + }, + { + Input: cty.StringVal(`foo = not valid syntax`), + WantErr: `invalid tfvars syntax: :1,11-16: Missing newline after argument; An argument definition must end with a newline.`, + }, + { + Input: cty.StringVal(`foo = var.whatever`), + WantErr: `invalid expression for variable "foo": :1,7-10: Variables not allowed; Variables may not be used here.`, + }, + { + Input: cty.StringVal(`foo = whatever()`), + WantErr: `invalid expression for variable "foo": :1,7-17: Function calls not allowed; Functions may not be called here.`, + }, + }) +} + +func TestExprencode(t *testing.T) { + tableTestFunction(t, "exprencode", []functionTest{ + { + Input: cty.StringVal("hello"), + Want: cty.StringVal(`"hello"`), + }, + { + Input: cty.StringVal("hello\nworld\n"), + Want: cty.StringVal(`"hello\nworld\n"`), + // NOTE: If HCL changes the above to be a heredoc in future (which + // would make this test fail) then our function's refinement + // that unknown strings encode with the prefix " will become + // invalid, and should be removed. + }, + { + Input: cty.StringVal("hel${lo"), + Want: cty.StringVal(`"hel$${lo"`), // Escape template interpolation sequence + }, + { + Input: cty.StringVal("hel%{lo"), + Want: cty.StringVal(`"hel%%{lo"`), // Escape template control sequence + }, + { + Input: cty.StringVal(`boop\boop`), + Want: cty.StringVal(`"boop\\boop"`), // Escape literal backslash + }, + { + Input: cty.StringVal(""), + Want: cty.StringVal(`""`), + }, + { + Input: cty.NumberIntVal(2), + Want: cty.StringVal(`2`), + }, + { + Input: cty.True, + Want: cty.StringVal(`true`), + }, + { + Input: cty.False, + Want: cty.StringVal(`false`), + }, + { + Input: cty.EmptyObjectVal, + Want: cty.StringVal(`{}`), + }, + { + Input: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(5), + "string": cty.StringVal("..."), + }), + Want: cty.StringVal(`{ + number = 5 + string = "..." +}`), + }, + { + Input: cty.MapVal(map[string]cty.Value{ + "one": cty.NumberIntVal(1), + "two": cty.NumberIntVal(2), + }), + Want: cty.StringVal(`{ + one = 1 + two = 2 +}`), + }, + { + Input: cty.EmptyTupleVal, + Want: cty.StringVal(`[]`), + }, + { + Input: cty.TupleVal([]cty.Value{ + cty.NumberIntVal(5), + cty.StringVal("..."), + }), + Want: cty.StringVal(`[5, "..."]`), + }, + { + Input: cty.SetVal([]cty.Value{ + cty.NumberIntVal(1), + cty.NumberIntVal(5), + cty.NumberIntVal(20), + cty.NumberIntVal(55), + }), + Want: cty.StringVal(`[1, 5, 20, 55]`), + }, + { + Input: cty.DynamicVal, + Want: cty.UnknownVal(cty.String).RefineNotNull(), + }, + { + Input: cty.UnknownVal(cty.Number).RefineNotNull(), + Want: cty.UnknownVal(cty.String).RefineNotNull(), + }, + { + Input: cty.UnknownVal(cty.String).RefineNotNull(), + Want: cty.UnknownVal(cty.String).Refine(). + NotNull(). + StringPrefixFull(`"`). + NewValue(), + }, + { + Input: cty.UnknownVal(cty.EmptyObject).RefineNotNull(), + Want: cty.UnknownVal(cty.String).Refine(). + NotNull(). + StringPrefixFull(`{`). + NewValue(), + }, + { + Input: cty.UnknownVal(cty.Map(cty.String)).RefineNotNull(), + Want: cty.UnknownVal(cty.String).Refine(). + NotNull(). + StringPrefixFull(`{`). + NewValue(), + }, + { + Input: cty.UnknownVal(cty.EmptyTuple).RefineNotNull(), + Want: cty.UnknownVal(cty.String).Refine(). + NotNull(). + StringPrefixFull(`[`). + NewValue(), + }, + { + Input: cty.UnknownVal(cty.List(cty.String)).RefineNotNull(), + Want: cty.UnknownVal(cty.String).Refine(). + NotNull(). + StringPrefixFull(`[`). + NewValue(), + }, + { + Input: cty.UnknownVal(cty.Set(cty.String)).RefineNotNull(), + Want: cty.UnknownVal(cty.String).Refine(). + NotNull(). + StringPrefixFull(`[`). + NewValue(), + }, + }) +} + +type functionTest struct { + Input cty.Value + Want cty.Value + WantErr string +} + +func tableTestFunction(t *testing.T, functionName string, tests []functionTest) { + t.Helper() + + provider := NewProvider() + for _, test := range tests { + t.Run(test.Input.GoString(), func(t *testing.T) { + resp := provider.CallFunction(providers.CallFunctionRequest{ + FunctionName: functionName, + Arguments: []cty.Value{test.Input}, + }) + if test.WantErr != "" { + err := resp.Err + if err == nil { + t.Fatalf("unexpected success for %#v; want error\ngot: %#v", test.Input, resp.Result) + } + if err.Error() != test.WantErr { + t.Errorf("wrong error\ngot: %s\nwant: %s", err.Error(), test.WantErr) + } + return + } + if resp.Err != nil { + t.Fatalf("unexpected error: %s", resp.Err) + } + if diff := cmp.Diff(test.Want, resp.Result, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result for %#v\n%s", test.Input, diff) + } + }) + } +} diff --git a/internal/builtin/providers/terraform/provider.go b/internal/builtin/providers/terraform/provider.go index 2720b259d9f9..55b5ec52bc61 100644 --- a/internal/builtin/providers/terraform/provider.go +++ b/internal/builtin/providers/terraform/provider.go @@ -7,6 +7,8 @@ import ( "fmt" "log" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/providers" ) @@ -27,6 +29,37 @@ func (p *Provider) GetProviderSchema() providers.GetProviderSchemaResponse { ResourceTypes: map[string]providers.Schema{ "terraform_data": dataStoreResourceSchema(), }, + Functions: map[string]providers.FunctionDecl{ + "tfvarsencode": { + Parameters: []providers.FunctionParam{ + { + Name: "value", + Type: cty.DynamicPseudoType, + AllowUnknownValues: true, // to perform refinements + }, + }, + ReturnType: cty.String, + }, + "tfvarsdecode": { + Parameters: []providers.FunctionParam{ + { + Name: "src", + Type: cty.String, + }, + }, + ReturnType: cty.DynamicPseudoType, + }, + "exprencode": { + Parameters: []providers.FunctionParam{ + { + Name: "value", + Type: cty.DynamicPseudoType, + AllowUnknownValues: true, // to perform refinements + }, + }, + ReturnType: cty.String, + }, + }, } } @@ -143,8 +176,31 @@ func (p *Provider) ValidateResourceConfig(req providers.ValidateResourceConfigRe // CallFunction would call a function contributed by this provider, but this // provider has no functions and so this function just panics. -func (p *Provider) CallFunction(providers.CallFunctionRequest) providers.CallFunctionResponse { - panic("unimplemented - terraform.io/builtin/terraform provider has no functions") +func (p *Provider) CallFunction(req providers.CallFunctionRequest) providers.CallFunctionResponse { + fn, ok := functions[req.FunctionName] + if !ok { + // Should not get here if the caller is behaving correctly, because + // we don't declare any functions in our schema that we don't have + // implementations for. + return providers.CallFunctionResponse{ + Err: fmt.Errorf("provider has no function named %q", req.FunctionName), + } + } + + // NOTE: We assume that none of the arguments can be marked, because we're + // expecting to be called from logic in Terraform Core that strips marks + // before calling a provider-contributed function, and then reapplies them + // afterwards. + + result, err := fn(req.Arguments) + if err != nil { + return providers.CallFunctionResponse{ + Err: err, + } + } + return providers.CallFunctionResponse{ + Result: result, + } } // Close is a noop for this provider, since it's run in-process. diff --git a/internal/command/e2etest/terraform_provider_funcs_test.go b/internal/command/e2etest/terraform_provider_funcs_test.go new file mode 100644 index 000000000000..7b6d4ac65e59 --- /dev/null +++ b/internal/command/e2etest/terraform_provider_funcs_test.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package e2etest + +import ( + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/e2e" +) + +func TestTerraformProviderFunctions(t *testing.T) { + // This test ensures that the terraform.io/builtin/terraform provider + // remains available and that its three functions are available to be + // called. This test is here because builtin providers are a bit of a + // special case in the CLI layer which could in principle get accidentally + // broken there even with deeper tests in the provider package itself + // still passing. + // + // The tests in the provider's own package are authoritative for the + // expected behavior of the functions. This test is focused on whether + // the functions can be called at all, though it does some very light + // testing of results for one specific input each. If the functions + // are intentionally changed to produce different results for those + // inputs in future then it may be appropriate to just update these + // tests to match. + + t.Parallel() + fixturePath := filepath.Join("testdata", "terraform-provider-funcs") + tf := e2e.NewBinary(t, terraformBin, fixturePath) + + //// INIT + _, stderr, err := tf.Run("init") + if err != nil { + t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr) + } + + //// PLAN + _, stderr, err = tf.Run("plan", "-out=tfplan") + if err != nil { + t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr) + } + + // The saved plan should include three planned output values containing + // results from our function calls. + plan, err := tf.Plan("tfplan") + if err != nil { + t.Fatalf("can't reload saved plan: %s", err) + } + + gotOutputs := make(map[string]cty.Value, 3) + for _, outputSrc := range plan.Changes.Outputs { + output, err := outputSrc.Decode() + if err != nil { + t.Fatalf("can't decode planned change for %s: %s", outputSrc.Addr, err) + } + gotOutputs[output.Addr.String()] = output.After + } + wantOutputs := map[string]cty.Value{ + "output.exprencode": cty.StringVal(`[1, 2, 3]`), + "output.tfvarsdecode": cty.ObjectVal(map[string]cty.Value{ + "baaa": cty.StringVal("🐑"), + "boop": cty.StringVal("👃"), + }), + "output.tfvarsencode": cty.StringVal(`a = "👋" +b = "🐝" +c = "👓" +`), + } + if diff := cmp.Diff(wantOutputs, gotOutputs, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong output values\n%s", diff) + } +} diff --git a/internal/command/e2etest/testdata/terraform-provider-funcs/terraform-provider-funcs.tf b/internal/command/e2etest/testdata/terraform-provider-funcs/terraform-provider-funcs.tf new file mode 100644 index 000000000000..7d07f8a0219d --- /dev/null +++ b/internal/command/e2etest/testdata/terraform-provider-funcs/terraform-provider-funcs.tf @@ -0,0 +1,35 @@ +# This test fixture is here primarily just to make sure that the +# terraform.io/builtin/terraform functions remain available for use. The +# actual behavior of these functions is the responsibility of +# ./internal/builtin/providers/terraform, and so it has more detailed tests +# whereas this one is focused largely just on whether these functions are +# callable at all. + +terraform { + required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } + } +} + +output "tfvarsencode" { + value = provider::terraform::tfvarsencode({ + a = "👋" + b = "🐝" + c = "👓" + }) +} + +output "tfvarsdecode" { + value = provider::terraform::tfvarsdecode( + <<-EOT + boop = "👃" + baaa = "🐑" + EOT + ) +} + +output "exprencode" { + value = provider::terraform::exprencode([1, 2, 3]) +} diff --git a/website/data/language-nav-data.json b/website/data/language-nav-data.json index abd7d366eba1..416e28452740 100644 --- a/website/data/language-nav-data.json +++ b/website/data/language-nav-data.json @@ -730,6 +730,14 @@ { "title": "type", "href": "/language/functions/type" } ] }, + { + "title": "Terraform-specific Functions", + "routes": [ + { "title": "provider::terraform::tfvarsencode", "href": "/language/functions/terraform-tfvarsencode" }, + { "title": "provider::terraform::tfvarsdecode", "href": "/language/functions/terraform-tfvarsdecode" }, + { "title": "provider::terraform::exprencode", "href": "/language/functions/terraform-exprencode" } + ] + }, { "title": "abs", "path": "functions/abs", "hidden": true }, { "title": "abspath", "path": "functions/abspath", "hidden": true }, { "title": "alltrue", "path": "functions/alltrue", "hidden": true }, @@ -878,6 +886,9 @@ "path": "functions/templatefile", "hidden": true }, + { "title": "terraform-tfvarsencode", "path": "functions/terraform-tfvarsencode", "hidden": true }, + { "title": "terraform-tfvarsdecode", "path": "functions/terraform-tfvarsdecode", "hidden": true }, + { "title": "terraform-exprencode", "path": "functions/terraform-exprencode", "hidden": true }, { "title": "textdecodebase64", "path": "functions/textdecodebase64", diff --git a/website/docs/language/functions/terraform-exprencode.mdx b/website/docs/language/functions/terraform-exprencode.mdx new file mode 100644 index 000000000000..f7491b68d8b8 --- /dev/null +++ b/website/docs/language/functions/terraform-exprencode.mdx @@ -0,0 +1,72 @@ +--- +page_title: provider::terraform::exprencode - Functions - Configuration Language +description: >- + The exprencode function produces a string representation of an arbitrary value + using Terraform expression syntax. +--- + +# `provider::terraform::exprencode` Function + +-> **Note:** This function is supported only in Terraform v1.8 and later. + +`provider::terraform::exprencode` is a rarely-needed function which takes +any value and produces a string containing Terraform language expression syntax +approximating that value. + +To use this function, your module must declare a dependency on the built-in +`terraform` provider, which contains this function: + +```hcl +terraform { + required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } + } +} +``` + +The primary use for this function is in conjunction with the `hashicorp/tfe` +provider's resource type +[`tfe_variable`](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/resources/variable), +which expects variable values to be provided in Terraform expression syntax. + +For example, the following concisely declares multiple input variables for +a particular Terraform Cloud workspace: + +```hcl +locals { + workspace_vars = { + example1 = "Hello" + example2 = ["A", "B"] + } +} + +resource "tfe_variable" "test" { + for_each = local.workspace_vars + + category = "terraform" + workspace_id = tfe_workspace.example.id + + key = each.key + value = provider::terraform::exprencode(each.value) + hcl = true +} +``` + +When using this pattern, always set `hcl = true` in the resource declaration +to ensure that Terraform Cloud will expect `value` to be given as Terraform +expression syntax. + +We do not recommend using this function in any other situation. + +~> **Warning:** The exact syntax used to encode certain values may change +in future versions of Terraform to follow idiomatic style. Avoid using the +results of this function in any context where such changes might be disruptive +when upgrading Terraform in future. + +## Related Functions + +* [`tfvarsencode`](/terraform/language/functions/terraform-tfvarsencode) + produces expression strings for many different values at once, in `.tfvars` + syntax. diff --git a/website/docs/language/functions/terraform-tfvarsdecode.mdx b/website/docs/language/functions/terraform-tfvarsdecode.mdx new file mode 100644 index 000000000000..fb3a93a541d9 --- /dev/null +++ b/website/docs/language/functions/terraform-tfvarsdecode.mdx @@ -0,0 +1,70 @@ +--- +page_title: provider::terraform::tfvarsdecode - Functions - Configuration Language +description: >- + The tfvarsencode function parses a string containing syntax like that used + in a ".tfvars" file. +--- + +# `provider::terraform::tfvarsdecode` Function + +-> **Note:** This function is supported only in Terraform v1.8 and later. + +`provider::terraform::tfvarsdecode` is a rarely-needed function which takes +a string containing the content of a +[`.tfvars` file](/terraform/language/values/variables#variable-definitions-tfvars-files) +and returns an object describing the raw variable values it defines. + +To use this function, your module must declare a dependency on the built-in +`terraform` provider, which contains this function: + +```hcl +terraform { + required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } + } +} +``` + +Elsewhere in your module you can then call this function: + +```hcl +provider::terraform::tfvarsdecode( + <- + The tfvarsencode function produces a string representation of an object + using the same syntax as for ".tfvars" files used in Terraform CLI. +--- + +# `provider::terraform::tfvarsencode` Function + +-> **Note:** This function is supported only in Terraform v1.8 and later. + +`provider::terraform::tfvarsencode` is a rarely-needed function which takes +an object value and produces a string containing a description of that object +using the same syntax as Terraform CLI would expect in a +[`.tfvars` file](/terraform/language/values/variables#variable-definitions-tfvars-files). + +In most cases it's better to pass data between Terraform configurations using +[Data Sources](/terraform/language/data-sources), +instead of writing generated `.tfvars` files to disk. Use this function only as +a last resort. + +To use this function, your module must declare a dependency on the built-in +`terraform` provider, which contains this function: + +```hcl +terraform { + required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } + } +} +``` + +Elsewhere in your module you can then call this function: + +```hcl +provider::terraform::tfvarsencode({ + example = "Hello!" +}) +``` + +The call above would produce the following result: + +```hcl +example = "Hello!" +``` + +Due to Terraform's requirements for the `.tfvars` format, all of the attributes +of the given object must be valid Terraform variable names, as would be +accepted in an +[input variable declaration](/terraform/language/values/variables#declaring-an-input-variable). + +The `.tfvars` format is specific to Terraform and so we do not recommend using +it as a general serialization format. +Use [`jsonencode`](/terraform/language/functions/jsonencode) or +[`yamlencode`](/terraform/language/functions/yamlencode) instead to produce +formats that are supported by other software. + +~> **Warning:** The exact syntax used to encode certain values may change +in future versions of Terraform to follow idiomatic style. Avoid using the +results of this function in any context where such changes might be disruptive +when upgrading Terraform in future. + +## Related Functions + +* [`tfvarsdecode`](/terraform/language/functions/terraform-tfvarsdecode) + performs the opposite operation: parsing `.tfvars` content to obtain + the variable values declared inside. +* [`exprencode`](/terraform/language/functions/terraform-exprdecode) + encodes a single value as a plain expression, without the `.tfvars` + container around it.