provider/terraform: Terraform-specific encoding functions

Using the new possibility of provider-contributed functions, this
introduces three new functions which live in the
terraform.io/builtin/terraform provider, rather than being language
builtins, due to their Terraform-domain-specific nature.

The three new functions are:
 - tfvarsencode: takes a mapping value and tries to transform it into
   Terraform CLI's "tfvars" syntax, which is a small subset of HCL that
   only supports key/value pairs with constant values.
 - tfvarsdecode: takes a string containing content that could potentially
   appear in a "tfvars" file and returns an object representing the
   raw variable values defined inside.
 - exprencode: takes an arbitrary Terraform value and produces a string
   that would yield a similar value if parsed as a Terraform expression.

All three of these are very specialized, of use only in unusual situations
where someone is "gluing together" different Terraform configurations etc
when the usual strategies such as data sources are not suitable. There's
more information on the motivations for (and limitations of) each function
in the included documentation.
pull/34744/head
Martin Atkins 2 years ago
parent 42aa821194
commit 573c2db658

@ -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, "<tfvarsdecode argument>", 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
}

@ -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: <tfvarsdecode argument>: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: <tfvarsdecode argument>: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": <tfvarsdecode argument>:1,7-10: Variables not allowed; Variables may not be used here.`,
},
{
Input: cty.StringVal(`foo = whatever()`),
WantErr: `invalid expression for variable "foo": <tfvarsdecode argument>: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)
}
})
}
}

@ -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.

@ -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)
}
}

@ -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])
}

@ -730,6 +730,14 @@
{ "title": "<code>type</code>", "href": "/language/functions/type" }
]
},
{
"title": "Terraform-specific Functions",
"routes": [
{ "title": "<code>provider::terraform::tfvarsencode</code>", "href": "/language/functions/terraform-tfvarsencode" },
{ "title": "<code>provider::terraform::tfvarsdecode</code>", "href": "/language/functions/terraform-tfvarsdecode" },
{ "title": "<code>provider::terraform::exprencode</code>", "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",

@ -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.

@ -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(
<<EOT
example = "Hello!"
EOT
)
```
The call above would produce an object value like the following:
```hcl
{
example = "Hello!"
}
```
## Result Types
When interpreting a `.tfvars` file, Terraform CLI normally uses the variable
declarations from the related module to find a target type to convert the
definitions for use in the module.
`tfvarsdecode` does not have access to that type information, and so the result
always uses the most general type that a particular syntax could represent.
The supported value types for attributes of the result are:
* `string`, `number`, and `bool`
* `object` types
* `tuple` types
If you need to interpret object or tuple values as collection types, use
the type conversion functions to convert the returned values. There is no way
to represent list, set, or map values directly in the `.tfvars` format.
## Related Functions
* [`tfvarsencode`](/terraform/language/functions/terraform-tfvarsencode)
performs the opposite operation: producing `.tfvars` content from an
object value.

@ -0,0 +1,72 @@
---
page_title: provider::terraform::tfvarsencode - Functions - Configuration Language
description: >-
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.
Loading…
Cancel
Save