Skip to content

Commit 4d612b6

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 4d612b6

File tree

7 files changed

+796
-2
lines changed

7 files changed

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

0 commit comments

Comments
 (0)