-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
42aa821
commit 573c2db
Showing
9 changed files
with
918 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Oops, something went wrong.