Skip to content

Commit 9b4aaf7

Browse files
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.
1 parent c6e9351 commit 9b4aaf7

File tree

7 files changed

+802
-2
lines changed

7 files changed

+802
-2
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package terraform
5+
6+
import (
7+
"bytes"
8+
"fmt"
9+
"sort"
10+
11+
"github.com/hashicorp/hcl/v2"
12+
"github.com/hashicorp/hcl/v2/hclsyntax"
13+
"github.com/hashicorp/hcl/v2/hclwrite"
14+
"github.com/zclconf/go-cty/cty"
15+
"github.com/zclconf/go-cty/cty/function"
16+
)
17+
18+
var functions = map[string]func([]cty.Value) (cty.Value, error){
19+
"tfvarsencode": tfvarsencodeFunc,
20+
"tfvarsdecode": tfvarsdecodeFunc,
21+
"exprencode": exprencodeFunc,
22+
}
23+
24+
func tfvarsencodeFunc(args []cty.Value) (cty.Value, error) {
25+
// These error checks should not be hit in practice because the language
26+
// runtime should check them before calling, so this is just for robustness
27+
// and completeness.
28+
if len(args) > 1 {
29+
return cty.NilVal, function.NewArgErrorf(1, "too many arguments; only one expected")
30+
}
31+
if len(args) == 0 {
32+
return cty.NilVal, fmt.Errorf("exactly one argument is required")
33+
}
34+
35+
v := args[0]
36+
ty := v.Type()
37+
38+
if v.IsNull() {
39+
// Our functions schema does not say we allow null values, so we should
40+
// not get to this error message if the caller respects the schema.
41+
return cty.NilVal, function.NewArgErrorf(1, "cannot encode a null value in tfvars syntax")
42+
}
43+
if !v.IsWhollyKnown() {
44+
return cty.UnknownVal(cty.String).RefineNotNull(), nil
45+
}
46+
47+
var keys []string
48+
switch {
49+
case ty.IsObjectType():
50+
atys := ty.AttributeTypes()
51+
keys = make([]string, 0, len(atys))
52+
for key := range atys {
53+
keys = append(keys, key)
54+
}
55+
case ty.IsMapType():
56+
keys = make([]string, 0, v.LengthInt())
57+
for it := v.ElementIterator(); it.Next(); {
58+
k, _ := it.Element()
59+
keys = append(keys, k.AsString())
60+
}
61+
default:
62+
return cty.NilVal, function.NewArgErrorf(1, "invalid value to encode: must be an object whose attribute names will become the encoded variable names")
63+
}
64+
sort.Strings(keys)
65+
66+
f := hclwrite.NewEmptyFile()
67+
body := f.Body()
68+
for _, key := range keys {
69+
if !hclsyntax.ValidIdentifier(key) {
70+
// We can only encode valid identifiers as tfvars keys, since
71+
// the HCL argument grammar requires them to be identifiers.
72+
return cty.NilVal, function.NewArgErrorf(1, "invalid variable name %q: must be a valid identifier, per Terraform's rules for input variable declarations", key)
73+
}
74+
75+
// This index should not fail because we know that "key" is a valid
76+
// index from the logic above.
77+
v, _ := hcl.Index(v, cty.StringVal(key), nil)
78+
body.SetAttributeValue(key, v)
79+
}
80+
81+
result := f.Bytes()
82+
return cty.StringVal(string(result)), nil
83+
}
84+
85+
func tfvarsdecodeFunc(args []cty.Value) (cty.Value, error) {
86+
// These error checks should not be hit in practice because the language
87+
// runtime should check them before calling, so this is just for robustness
88+
// and completeness.
89+
if len(args) > 1 {
90+
return cty.NilVal, function.NewArgErrorf(1, "too many arguments; only one expected")
91+
}
92+
if len(args) == 0 {
93+
return cty.NilVal, fmt.Errorf("exactly one argument is required")
94+
}
95+
if args[0].Type() != cty.String {
96+
return cty.NilVal, fmt.Errorf("argument must be a string")
97+
}
98+
if args[0].IsNull() {
99+
return cty.NilVal, fmt.Errorf("cannot decode tfvars from a null value")
100+
}
101+
if !args[0].IsKnown() {
102+
// If our input isn't known then we can't even predict the result
103+
// type, since it will be an object type decided based on which
104+
// arguments and values we find in the string.
105+
return cty.DynamicVal, nil
106+
}
107+
108+
// If we get here then we know that:
109+
// - there's exactly one element in args
110+
// - it's a string
111+
// - it is known and non-null
112+
// So therefore the following is guaranteed to succeed.
113+
src := []byte(args[0].AsString())
114+
115+
// As usual when we wrap HCL stuff up in functions, we end up needing to
116+
// stuff HCL diagnostics into plain string error messages. This produces
117+
// a non-ideal result but is still better than hiding the HCL-provided
118+
// diagnosis altogether.
119+
f, hclDiags := hclsyntax.ParseConfig(src, "<tfvarsdecode argument>", hcl.InitialPos)
120+
if hclDiags.HasErrors() {
121+
return cty.NilVal, fmt.Errorf("invalid tfvars syntax: %s", hclDiags.Error())
122+
}
123+
attrs, hclDiags := f.Body.JustAttributes()
124+
if hclDiags.HasErrors() {
125+
return cty.NilVal, fmt.Errorf("invalid tfvars content: %s", hclDiags.Error())
126+
}
127+
retAttrs := make(map[string]cty.Value, len(attrs))
128+
for name, attr := range attrs {
129+
// Evaluating the expression with no EvalContext achieves the same
130+
// interpretation as Terraform CLI makes of .tfvars files, rejecting
131+
// any function calls or references to symbols.
132+
v, hclDiags := attr.Expr.Value(nil)
133+
if hclDiags.HasErrors() {
134+
return cty.NilVal, fmt.Errorf("invalid expression for variable %q: %s", name, hclDiags.Error())
135+
}
136+
retAttrs[name] = v
137+
}
138+
139+
return cty.ObjectVal(retAttrs), nil
140+
}
141+
142+
func exprencodeFunc(args []cty.Value) (cty.Value, error) {
143+
// These error checks should not be hit in practice because the language
144+
// runtime should check them before calling, so this is just for robustness
145+
// and completeness.
146+
if len(args) > 1 {
147+
return cty.NilVal, function.NewArgErrorf(1, "too many arguments; only one expected")
148+
}
149+
if len(args) == 0 {
150+
return cty.NilVal, fmt.Errorf("exactly one argument is required")
151+
}
152+
153+
v := args[0]
154+
if !v.IsWhollyKnown() {
155+
ret := cty.UnknownVal(cty.String).RefineNotNull()
156+
// For some types we can refine further due to the HCL grammar,
157+
// as long as w eknow the value isn't null.
158+
if !v.Range().CouldBeNull() {
159+
ty := v.Type()
160+
switch {
161+
case ty.IsObjectType() || ty.IsMapType():
162+
ret = ret.Refine().StringPrefixFull("{").NewValue()
163+
case ty.IsTupleType() || ty.IsListType() || ty.IsSetType():
164+
ret = ret.Refine().StringPrefixFull("[").NewValue()
165+
case ty == cty.String:
166+
ret = ret.Refine().StringPrefixFull(`"`).NewValue()
167+
}
168+
}
169+
return ret, nil
170+
}
171+
172+
// This bytes.TrimSpace is to ensure that future changes to HCL, that
173+
// might for some reason add extra spaces before the expression (!)
174+
// can't invalidate our unknown value prefix refinements above.
175+
src := bytes.TrimSpace(hclwrite.TokensForValue(v).Bytes())
176+
return cty.StringVal(string(src)), nil
177+
}

0 commit comments

Comments
 (0)