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:
Martin Atkins 2019-10-28 16:26:40 -07:00
parent f8a32f0b83
commit ce68b4d27c
6 changed files with 11 additions and 5104 deletions

View File

@ -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) { func TestConfigValidate_countBadContext(t *testing.T) {
c := testConfig(t, "validate-count-bad-context") 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) { func TestConfigValidate_countUserVar(t *testing.T) {
c := testConfig(t, "validate-count-user-var") c := testConfig(t, "validate-count-user-var")
if err := c.Validate(); err != nil { 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) { func TestConfigValidate_countLocalValue(t *testing.T) {
c := testConfig(t, "validate-local-value-count") c := testConfig(t, "validate-local-value-count")
if err := c.Validate(); err != nil { if err := c.Validate(); err != nil {
@ -740,23 +707,6 @@ func testConfig(t *testing.T, name string) *Config {
return c 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) { func TestConfigDataCount(t *testing.T) {
c := testConfig(t, "data-count") c := testConfig(t, "data-count")
actual, err := c.Resources[0].Count() actual, err := c.Resources[0].Count()

View File

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

View File

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

View File

@ -7,9 +7,6 @@ import (
"strconv" "strconv"
"sync" "sync"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
hcl2 "github.com/hashicorp/hcl/v2" hcl2 "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hil" "github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast" "github.com/hashicorp/hil/ast"
@ -349,37 +346,9 @@ func (r *RawConfig) couldBeInteger() bool {
_, err := strconv.ParseInt(r.Value().(string), 0, 0) _, err := strconv.ParseInt(r.Value().(string), 0, 0)
return err == nil return err == nil
} else { } else {
// HCL2 experiment path: using the HCL2 API via shims // We briefly tried to gradually implement HCL2 support by adding a
// // branch here, but that experiment was not successful.
// This path catches fewer situations because we have to assume all panic("HCL2 experimental path no longer supported")
// 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
} }
} }
@ -432,14 +401,14 @@ type gobRawConfig struct {
} }
// langEvalConfig returns the evaluation configuration we use to execute. // 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 { func langEvalConfig(vs map[string]ast.Variable) *hil.EvalConfig {
funcMap := make(map[string]ast.Function) funcMap := make(map[string]ast.Function)
for k, v := range Funcs() { for k, v := range Funcs() {
funcMap[k] = v funcMap[k] = v
} }
funcMap["lookup"] = interpolationFuncLookup(vs)
funcMap["keys"] = interpolationFuncKeys(vs)
funcMap["values"] = interpolationFuncValues(vs)
return &hil.EvalConfig{ return &hil.EvalConfig{
GlobalScope: &ast.BasicScope{ GlobalScope: &ast.BasicScope{