config: Remove legacy interpolation function implementations
These are often confusing for new contributors, since this looks suspiciously like the right place to add new functions or change the behavior of existing ones. To reduce that confusion, here we remove them entirely from this package (which is now dead code in Terraform 0.12 anyway) and include in the documentation comments a pointer to the current function implementations.
This commit is contained in:
parent
f8a32f0b83
commit
ce68b4d27c
|
@ -282,13 +282,6 @@ func TestConfigValidate_countInt(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestConfigValidate_countInt_HCL2(t *testing.T) {
|
||||
c := testConfigHCL2(t, "validate-count-int")
|
||||
if err := c.Validate(); err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidate_countBadContext(t *testing.T) {
|
||||
c := testConfig(t, "validate-count-bad-context")
|
||||
|
||||
|
@ -320,25 +313,6 @@ func TestConfigValidate_countNotInt(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestConfigValidate_countNotInt_HCL2(t *testing.T) {
|
||||
c := testConfigHCL2(t, "validate-count-not-int-const")
|
||||
if err := c.Validate(); err == nil {
|
||||
t.Fatal("should not be valid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidate_countNotIntUnknown_HCL2(t *testing.T) {
|
||||
c := testConfigHCL2(t, "validate-count-not-int")
|
||||
// In HCL2 this is not an error because the unknown variable interpolates
|
||||
// to produce an unknown string, which we assume (incorrectly, it turns out)
|
||||
// will become a string containing only digits. This is okay because
|
||||
// the config validation is only a "best effort" and we'll get a definitive
|
||||
// result during the validation graph walk.
|
||||
if err := c.Validate(); err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidate_countUserVar(t *testing.T) {
|
||||
c := testConfig(t, "validate-count-user-var")
|
||||
if err := c.Validate(); err != nil {
|
||||
|
@ -346,13 +320,6 @@ func TestConfigValidate_countUserVar(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestConfigValidate_countUserVar_HCL2(t *testing.T) {
|
||||
c := testConfigHCL2(t, "validate-count-user-var")
|
||||
if err := c.Validate(); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidate_countLocalValue(t *testing.T) {
|
||||
c := testConfig(t, "validate-local-value-count")
|
||||
if err := c.Validate(); err != nil {
|
||||
|
@ -740,23 +707,6 @@ func testConfig(t *testing.T, name string) *Config {
|
|||
return c
|
||||
}
|
||||
|
||||
// testConfigHCL loads a config, forcing it to be processed with the HCL2
|
||||
// loader even if it doesn't explicitly opt in to the HCL2 experiment.
|
||||
func testConfigHCL2(t *testing.T, name string) *Config {
|
||||
t.Helper()
|
||||
cer, _, err := globalHCL2Loader.loadFile(filepath.Join(fixtureDir, name, "main.tf"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load %s: %s", name, err)
|
||||
}
|
||||
|
||||
cfg, err := cer.Config()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode %s: %s", name, err)
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func TestConfigDataCount(t *testing.T) {
|
||||
c := testConfig(t, "data-count")
|
||||
actual, err := c.Resources[0].Count()
|
||||
|
|
|
@ -1,134 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zclconf/go-cty/cty/function/stdlib"
|
||||
|
||||
"github.com/hashicorp/hil/ast"
|
||||
"github.com/hashicorp/terraform/configs/hcl2shim"
|
||||
|
||||
hcl2 "github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// This file contains some helper functions that are used to shim between
|
||||
// HCL2 concepts and HCL/HIL concepts, to help us mostly preserve the existing
|
||||
// public API that was built around HCL/HIL-oriented approaches.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func hcl2InterpolationFuncs() map[string]function.Function {
|
||||
hcl2Funcs := map[string]function.Function{}
|
||||
|
||||
for name, hilFunc := range Funcs() {
|
||||
hcl2Funcs[name] = hcl2InterpolationFuncShim(hilFunc)
|
||||
}
|
||||
|
||||
// Some functions in the old world are dealt with inside langEvalConfig
|
||||
// due to their legacy reliance on direct access to the symbol table.
|
||||
// Since 0.7 they don't actually need it anymore and just ignore it,
|
||||
// so we're cheating a bit here and exploiting that detail by passing nil.
|
||||
hcl2Funcs["lookup"] = hcl2InterpolationFuncShim(interpolationFuncLookup(nil))
|
||||
hcl2Funcs["keys"] = hcl2InterpolationFuncShim(interpolationFuncKeys(nil))
|
||||
hcl2Funcs["values"] = hcl2InterpolationFuncShim(interpolationFuncValues(nil))
|
||||
|
||||
// As a bonus, we'll provide the JSON-handling functions from the cty
|
||||
// function library since its "jsonencode" is more complete (doesn't force
|
||||
// weird type conversions) and HIL's type system can't represent
|
||||
// "jsondecode" at all. The result of jsondecode will eventually be forced
|
||||
// to conform to the HIL type system on exit into the rest of Terraform due
|
||||
// to our shimming right now, but it should be usable for decoding _within_
|
||||
// an expression.
|
||||
hcl2Funcs["jsonencode"] = stdlib.JSONEncodeFunc
|
||||
hcl2Funcs["jsondecode"] = stdlib.JSONDecodeFunc
|
||||
|
||||
return hcl2Funcs
|
||||
}
|
||||
|
||||
func hcl2InterpolationFuncShim(hilFunc ast.Function) function.Function {
|
||||
spec := &function.Spec{}
|
||||
|
||||
for i, hilArgType := range hilFunc.ArgTypes {
|
||||
spec.Params = append(spec.Params, function.Parameter{
|
||||
Type: hcl2shim.HCL2TypeForHILType(hilArgType),
|
||||
Name: fmt.Sprintf("arg%d", i+1), // HIL args don't have names, so we'll fudge it
|
||||
})
|
||||
}
|
||||
|
||||
if hilFunc.Variadic {
|
||||
spec.VarParam = &function.Parameter{
|
||||
Type: hcl2shim.HCL2TypeForHILType(hilFunc.VariadicType),
|
||||
Name: "varargs", // HIL args don't have names, so we'll fudge it
|
||||
}
|
||||
}
|
||||
|
||||
spec.Type = func(args []cty.Value) (cty.Type, error) {
|
||||
return hcl2shim.HCL2TypeForHILType(hilFunc.ReturnType), nil
|
||||
}
|
||||
spec.Impl = func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||||
hilArgs := make([]interface{}, len(args))
|
||||
for i, arg := range args {
|
||||
hilV := hcl2shim.HILVariableFromHCL2Value(arg)
|
||||
|
||||
// Although the cty function system does automatic type conversions
|
||||
// to match the argument types, cty doesn't distinguish int and
|
||||
// float and so we may need to adjust here to ensure that the
|
||||
// wrapped function gets exactly the Go type it was expecting.
|
||||
var wantType ast.Type
|
||||
if i < len(hilFunc.ArgTypes) {
|
||||
wantType = hilFunc.ArgTypes[i]
|
||||
} else {
|
||||
wantType = hilFunc.VariadicType
|
||||
}
|
||||
switch {
|
||||
case hilV.Type == ast.TypeInt && wantType == ast.TypeFloat:
|
||||
hilV.Type = wantType
|
||||
hilV.Value = float64(hilV.Value.(int))
|
||||
case hilV.Type == ast.TypeFloat && wantType == ast.TypeInt:
|
||||
hilV.Type = wantType
|
||||
hilV.Value = int(hilV.Value.(float64))
|
||||
}
|
||||
|
||||
// HIL functions actually expect to have the outermost variable
|
||||
// "peeled" but any nested values (in lists or maps) will
|
||||
// still have their ast.Variable wrapping.
|
||||
hilArgs[i] = hilV.Value
|
||||
}
|
||||
|
||||
hilResult, err := hilFunc.Callback(hilArgs)
|
||||
if err != nil {
|
||||
return cty.DynamicVal, err
|
||||
}
|
||||
|
||||
// Just as on the way in, we get back a partially-peeled ast.Variable
|
||||
// which we need to re-wrap in order to convert it back into what
|
||||
// we're calling a "config value".
|
||||
rv := hcl2shim.HCL2ValueFromHILVariable(ast.Variable{
|
||||
Type: hilFunc.ReturnType,
|
||||
Value: hilResult,
|
||||
})
|
||||
|
||||
return convert.Convert(rv, retType) // if result is unknown we'll force the correct type here
|
||||
}
|
||||
return function.New(spec)
|
||||
}
|
||||
|
||||
func hcl2EvalWithUnknownVars(expr hcl2.Expression) (cty.Value, hcl2.Diagnostics) {
|
||||
trs := expr.Variables()
|
||||
vars := map[string]cty.Value{}
|
||||
val := cty.DynamicVal
|
||||
|
||||
for _, tr := range trs {
|
||||
name := tr.RootName()
|
||||
vars[name] = val
|
||||
}
|
||||
|
||||
ctx := &hcl2.EvalContext{
|
||||
Variables: vars,
|
||||
Functions: hcl2InterpolationFuncs(),
|
||||
}
|
||||
return expr.Value(ctx)
|
||||
}
|
|
@ -1,176 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
hcl2 "github.com/hashicorp/hcl/v2"
|
||||
hcl2syntax "github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestHCL2InterpolationFuncs(t *testing.T) {
|
||||
// This is not a comprehensive test of all the functions (they are tested
|
||||
// in interpolation_funcs_test.go already) but rather just calling a
|
||||
// representative set via the HCL2 API to verify that the HCL2-to-HIL
|
||||
// function shim is working as expected.
|
||||
tests := []struct {
|
||||
Expr string
|
||||
Want cty.Value
|
||||
Err bool
|
||||
}{
|
||||
{
|
||||
`upper("hello")`,
|
||||
cty.StringVal("HELLO"),
|
||||
false,
|
||||
},
|
||||
{
|
||||
`abs(-2)`,
|
||||
cty.NumberIntVal(2),
|
||||
false,
|
||||
},
|
||||
{
|
||||
`abs(-2.5)`,
|
||||
cty.NumberFloatVal(2.5),
|
||||
false,
|
||||
},
|
||||
{
|
||||
`cidrsubnet("")`,
|
||||
cty.DynamicVal,
|
||||
true, // not enough arguments
|
||||
},
|
||||
{
|
||||
`cidrsubnet("10.1.0.0/16", 8, 2)`,
|
||||
cty.StringVal("10.1.2.0/24"),
|
||||
false,
|
||||
},
|
||||
{
|
||||
`concat([])`,
|
||||
// Since HIL doesn't maintain element type information for list
|
||||
// types, HCL2 can't either without elements to sniff.
|
||||
cty.ListValEmpty(cty.DynamicPseudoType),
|
||||
false,
|
||||
},
|
||||
{
|
||||
`concat([], [])`,
|
||||
cty.ListValEmpty(cty.DynamicPseudoType),
|
||||
false,
|
||||
},
|
||||
{
|
||||
`concat(["a"], ["b", "c"])`,
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.StringVal("a"),
|
||||
cty.StringVal("b"),
|
||||
cty.StringVal("c"),
|
||||
}),
|
||||
false,
|
||||
},
|
||||
{
|
||||
`list()`,
|
||||
cty.ListValEmpty(cty.DynamicPseudoType),
|
||||
false,
|
||||
},
|
||||
{
|
||||
`list("a", "b", "c")`,
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.StringVal("a"),
|
||||
cty.StringVal("b"),
|
||||
cty.StringVal("c"),
|
||||
}),
|
||||
false,
|
||||
},
|
||||
{
|
||||
`list(list("a"), list("b"), list("c"))`,
|
||||
// The types emerge here in a bit of a strange tangle because of
|
||||
// the guesswork we do when trying to recover lost information from
|
||||
// HIL, but the rest of the language doesn't really care whether
|
||||
// we use lists or tuples here as long as we are consistent with
|
||||
// the type system invariants.
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.TupleVal([]cty.Value{cty.StringVal("a")}),
|
||||
cty.TupleVal([]cty.Value{cty.StringVal("b")}),
|
||||
cty.TupleVal([]cty.Value{cty.StringVal("c")}),
|
||||
}),
|
||||
false,
|
||||
},
|
||||
{
|
||||
`list(list("a"), "b")`,
|
||||
cty.DynamicVal,
|
||||
true, // inconsistent types
|
||||
},
|
||||
{
|
||||
`length([])`,
|
||||
cty.NumberIntVal(0),
|
||||
false,
|
||||
},
|
||||
{
|
||||
`length([2])`,
|
||||
cty.NumberIntVal(1),
|
||||
false,
|
||||
},
|
||||
{
|
||||
`jsonencode(2)`,
|
||||
cty.StringVal(`2`),
|
||||
false,
|
||||
},
|
||||
{
|
||||
`jsonencode(true)`,
|
||||
cty.StringVal(`true`),
|
||||
false,
|
||||
},
|
||||
{
|
||||
`jsonencode("foo")`,
|
||||
cty.StringVal(`"foo"`),
|
||||
false,
|
||||
},
|
||||
{
|
||||
`jsonencode({})`,
|
||||
cty.StringVal(`{}`),
|
||||
false,
|
||||
},
|
||||
{
|
||||
`jsonencode([1])`,
|
||||
cty.StringVal(`[1]`),
|
||||
false,
|
||||
},
|
||||
{
|
||||
`jsondecode("{}")`,
|
||||
cty.EmptyObjectVal,
|
||||
false,
|
||||
},
|
||||
{
|
||||
`jsondecode("[5, true]")[0]`,
|
||||
cty.NumberIntVal(5),
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.Expr, func(t *testing.T) {
|
||||
expr, diags := hcl2syntax.ParseExpression([]byte(test.Expr), "", hcl2.Pos{Line: 1, Column: 1})
|
||||
if len(diags) != 0 {
|
||||
for _, diag := range diags {
|
||||
t.Logf("- %s", diag)
|
||||
}
|
||||
t.Fatalf("unexpected diagnostics while parsing expression")
|
||||
}
|
||||
|
||||
got, diags := expr.Value(&hcl2.EvalContext{
|
||||
Functions: hcl2InterpolationFuncs(),
|
||||
})
|
||||
gotErr := diags.HasErrors()
|
||||
if gotErr != test.Err {
|
||||
if test.Err {
|
||||
t.Errorf("expected errors but got none")
|
||||
} else {
|
||||
t.Errorf("unexpected errors")
|
||||
for _, diag := range diags {
|
||||
t.Logf("- %s", diag)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !got.RawEquals(test.Want) {
|
||||
t.Errorf("wrong result\nexpr: %s\ngot: %#v\nwant: %#v", test.Expr, got, test.Want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -7,9 +7,6 @@ import (
|
|||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
|
||||
hcl2 "github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hil"
|
||||
"github.com/hashicorp/hil/ast"
|
||||
|
@ -349,37 +346,9 @@ func (r *RawConfig) couldBeInteger() bool {
|
|||
_, err := strconv.ParseInt(r.Value().(string), 0, 0)
|
||||
return err == nil
|
||||
} else {
|
||||
// HCL2 experiment path: using the HCL2 API via shims
|
||||
//
|
||||
// This path catches fewer situations because we have to assume all
|
||||
// variables are entirely unknown in HCL2, rather than the assumption
|
||||
// above that all variables can be numbers because names like "var.foo"
|
||||
// are considered a single variable rather than an attribute access.
|
||||
// This is fine in practice, because we get a definitive answer
|
||||
// during the graph walk when we have real values to work with.
|
||||
attrs, diags := r.Body.JustAttributes()
|
||||
if diags.HasErrors() {
|
||||
// This body is not just a single attribute with a value, so
|
||||
// this can't be a number.
|
||||
return false
|
||||
}
|
||||
attr, hasAttr := attrs[r.Key]
|
||||
if !hasAttr {
|
||||
return false
|
||||
}
|
||||
result, diags := hcl2EvalWithUnknownVars(attr.Expr)
|
||||
if diags.HasErrors() {
|
||||
// We'll conservatively assume that this error is a result of
|
||||
// us not being ready to fully-populate the scope, and catch
|
||||
// any further problems during the main graph walk.
|
||||
return true
|
||||
}
|
||||
|
||||
// If the result is convertable to number then we'll allow it.
|
||||
// We do this because an unknown string is optimistically convertable
|
||||
// to number (might be "5") but a _known_ string "hello" is not.
|
||||
_, err := convert.Convert(result, cty.Number)
|
||||
return err == nil
|
||||
// We briefly tried to gradually implement HCL2 support by adding a
|
||||
// branch here, but that experiment was not successful.
|
||||
panic("HCL2 experimental path no longer supported")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -432,14 +401,14 @@ type gobRawConfig struct {
|
|||
}
|
||||
|
||||
// langEvalConfig returns the evaluation configuration we use to execute.
|
||||
//
|
||||
// The interpolation functions are no longer available here, because this
|
||||
// codepath is no longer used. Instead, see ../lang/functions.go .
|
||||
func langEvalConfig(vs map[string]ast.Variable) *hil.EvalConfig {
|
||||
funcMap := make(map[string]ast.Function)
|
||||
for k, v := range Funcs() {
|
||||
funcMap[k] = v
|
||||
}
|
||||
funcMap["lookup"] = interpolationFuncLookup(vs)
|
||||
funcMap["keys"] = interpolationFuncKeys(vs)
|
||||
funcMap["values"] = interpolationFuncValues(vs)
|
||||
|
||||
return &hil.EvalConfig{
|
||||
GlobalScope: &ast.BasicScope{
|
||||
|
|
Loading…
Reference in New Issue