444 lines
13 KiB
Go
444 lines
13 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"math/big"
|
|
|
|
"github.com/zclconf/go-cty/cty/function/stdlib"
|
|
|
|
"github.com/hashicorp/hil/ast"
|
|
|
|
hcl2 "github.com/hashicorp/hcl2/hcl"
|
|
"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.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// configValueFromHCL2 converts a value from HCL2 (really, from the cty dynamic
|
|
// types library that HCL2 uses) to a value type that matches what would've
|
|
// been produced from the HCL-based interpolator for an equivalent structure.
|
|
//
|
|
// This function will transform a cty null value into a Go nil value, which
|
|
// isn't a possible outcome of the HCL/HIL-based decoder and so callers may
|
|
// need to detect and reject any null values.
|
|
func configValueFromHCL2(v cty.Value) interface{} {
|
|
if !v.IsKnown() {
|
|
return UnknownVariableValue
|
|
}
|
|
if v.IsNull() {
|
|
return nil
|
|
}
|
|
|
|
switch v.Type() {
|
|
case cty.Bool:
|
|
return v.True() // like HCL.BOOL
|
|
case cty.String:
|
|
return v.AsString() // like HCL token.STRING or token.HEREDOC
|
|
case cty.Number:
|
|
// We can't match HCL _exactly_ here because it distinguishes between
|
|
// int and float values, but we'll get as close as we can by using
|
|
// an int if the number is exactly representable, and a float if not.
|
|
// The conversion to float will force precision to that of a float64,
|
|
// which is potentially losing information from the specific number
|
|
// given, but no worse than what HCL would've done in its own conversion
|
|
// to float.
|
|
|
|
f := v.AsBigFloat()
|
|
if i, acc := f.Int64(); acc == big.Exact {
|
|
// if we're on a 32-bit system and the number is too big for 32-bit
|
|
// int then we'll fall through here and use a float64.
|
|
const MaxInt = int(^uint(0) >> 1)
|
|
const MinInt = -MaxInt - 1
|
|
if i <= int64(MaxInt) && i >= int64(MinInt) {
|
|
return int(i) // Like HCL token.NUMBER
|
|
}
|
|
}
|
|
|
|
f64, _ := f.Float64()
|
|
return f64 // like HCL token.FLOAT
|
|
}
|
|
|
|
if v.Type().IsListType() || v.Type().IsSetType() || v.Type().IsTupleType() {
|
|
l := make([]interface{}, 0, v.LengthInt())
|
|
it := v.ElementIterator()
|
|
for it.Next() {
|
|
_, ev := it.Element()
|
|
l = append(l, configValueFromHCL2(ev))
|
|
}
|
|
return l
|
|
}
|
|
|
|
if v.Type().IsMapType() || v.Type().IsObjectType() {
|
|
l := make(map[string]interface{})
|
|
it := v.ElementIterator()
|
|
for it.Next() {
|
|
ek, ev := it.Element()
|
|
l[ek.AsString()] = configValueFromHCL2(ev)
|
|
}
|
|
return l
|
|
}
|
|
|
|
// If we fall out here then we have some weird type that we haven't
|
|
// accounted for. This should never happen unless the caller is using
|
|
// capsule types, and we don't currently have any such types defined.
|
|
panic(fmt.Errorf("can't convert %#v to config value", v))
|
|
}
|
|
|
|
// hcl2ValueFromConfigValue is the opposite of configValueFromHCL2: it takes
|
|
// a value as would be returned from the old interpolator and turns it into
|
|
// a cty.Value so it can be used within, for example, an HCL2 EvalContext.
|
|
func hcl2ValueFromConfigValue(v interface{}) cty.Value {
|
|
if v == nil {
|
|
return cty.NullVal(cty.DynamicPseudoType)
|
|
}
|
|
if v == UnknownVariableValue {
|
|
return cty.DynamicVal
|
|
}
|
|
|
|
switch tv := v.(type) {
|
|
case bool:
|
|
return cty.BoolVal(tv)
|
|
case string:
|
|
return cty.StringVal(tv)
|
|
case int:
|
|
return cty.NumberIntVal(int64(tv))
|
|
case float64:
|
|
return cty.NumberFloatVal(tv)
|
|
case []interface{}:
|
|
vals := make([]cty.Value, len(tv))
|
|
for i, ev := range tv {
|
|
vals[i] = hcl2ValueFromConfigValue(ev)
|
|
}
|
|
return cty.TupleVal(vals)
|
|
case map[string]interface{}:
|
|
vals := map[string]cty.Value{}
|
|
for k, ev := range tv {
|
|
vals[k] = hcl2ValueFromConfigValue(ev)
|
|
}
|
|
return cty.ObjectVal(vals)
|
|
default:
|
|
// HCL/HIL should never generate anything that isn't caught by
|
|
// the above, so if we get here something has gone very wrong.
|
|
panic(fmt.Errorf("can't convert %#v to cty.Value", v))
|
|
}
|
|
}
|
|
|
|
func hilVariableFromHCL2Value(v cty.Value) ast.Variable {
|
|
if v.IsNull() {
|
|
// Caller should guarantee/check this before calling
|
|
panic("Null values cannot be represented in HIL")
|
|
}
|
|
if !v.IsKnown() {
|
|
return ast.Variable{
|
|
Type: ast.TypeUnknown,
|
|
Value: UnknownVariableValue,
|
|
}
|
|
}
|
|
|
|
switch v.Type() {
|
|
case cty.Bool:
|
|
return ast.Variable{
|
|
Type: ast.TypeBool,
|
|
Value: v.True(),
|
|
}
|
|
case cty.Number:
|
|
v := configValueFromHCL2(v)
|
|
switch tv := v.(type) {
|
|
case int:
|
|
return ast.Variable{
|
|
Type: ast.TypeInt,
|
|
Value: tv,
|
|
}
|
|
case float64:
|
|
return ast.Variable{
|
|
Type: ast.TypeFloat,
|
|
Value: tv,
|
|
}
|
|
default:
|
|
// should never happen
|
|
panic("invalid return value for configValueFromHCL2")
|
|
}
|
|
case cty.String:
|
|
return ast.Variable{
|
|
Type: ast.TypeString,
|
|
Value: v.AsString(),
|
|
}
|
|
}
|
|
|
|
if v.Type().IsListType() || v.Type().IsSetType() || v.Type().IsTupleType() {
|
|
l := make([]ast.Variable, 0, v.LengthInt())
|
|
it := v.ElementIterator()
|
|
for it.Next() {
|
|
_, ev := it.Element()
|
|
l = append(l, hilVariableFromHCL2Value(ev))
|
|
}
|
|
// If we were given a tuple then this could actually produce an invalid
|
|
// list with non-homogenous types, which we expect to be caught inside
|
|
// HIL just like a user-supplied non-homogenous list would be.
|
|
return ast.Variable{
|
|
Type: ast.TypeList,
|
|
Value: l,
|
|
}
|
|
}
|
|
|
|
if v.Type().IsMapType() || v.Type().IsObjectType() {
|
|
l := make(map[string]ast.Variable)
|
|
it := v.ElementIterator()
|
|
for it.Next() {
|
|
ek, ev := it.Element()
|
|
l[ek.AsString()] = hilVariableFromHCL2Value(ev)
|
|
}
|
|
// If we were given an object then this could actually produce an invalid
|
|
// map with non-homogenous types, which we expect to be caught inside
|
|
// HIL just like a user-supplied non-homogenous map would be.
|
|
return ast.Variable{
|
|
Type: ast.TypeMap,
|
|
Value: l,
|
|
}
|
|
}
|
|
|
|
// If we fall out here then we have some weird type that we haven't
|
|
// accounted for. This should never happen unless the caller is using
|
|
// capsule types, and we don't currently have any such types defined.
|
|
panic(fmt.Errorf("can't convert %#v to HIL variable", v))
|
|
}
|
|
|
|
func hcl2ValueFromHILVariable(v ast.Variable) cty.Value {
|
|
switch v.Type {
|
|
case ast.TypeList:
|
|
vals := make([]cty.Value, len(v.Value.([]ast.Variable)))
|
|
for i, ev := range v.Value.([]ast.Variable) {
|
|
vals[i] = hcl2ValueFromHILVariable(ev)
|
|
}
|
|
return cty.TupleVal(vals)
|
|
case ast.TypeMap:
|
|
vals := make(map[string]cty.Value, len(v.Value.(map[string]ast.Variable)))
|
|
for k, ev := range v.Value.(map[string]ast.Variable) {
|
|
vals[k] = hcl2ValueFromHILVariable(ev)
|
|
}
|
|
return cty.ObjectVal(vals)
|
|
default:
|
|
return hcl2ValueFromConfigValue(v.Value)
|
|
}
|
|
}
|
|
|
|
func hcl2TypeForHILType(hilType ast.Type) cty.Type {
|
|
switch hilType {
|
|
case ast.TypeAny:
|
|
return cty.DynamicPseudoType
|
|
case ast.TypeUnknown:
|
|
return cty.DynamicPseudoType
|
|
case ast.TypeBool:
|
|
return cty.Bool
|
|
case ast.TypeInt:
|
|
return cty.Number
|
|
case ast.TypeFloat:
|
|
return cty.Number
|
|
case ast.TypeString:
|
|
return cty.String
|
|
case ast.TypeList:
|
|
return cty.List(cty.DynamicPseudoType)
|
|
case ast.TypeMap:
|
|
return cty.Map(cty.DynamicPseudoType)
|
|
default:
|
|
return cty.NilType // equilvalent to ast.TypeInvalid
|
|
}
|
|
}
|
|
|
|
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: 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: 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 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 := 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 := 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)
|
|
}
|
|
|
|
// hcl2SingleAttrBody is a weird implementation of hcl2.Body that acts as if
|
|
// it has a single attribute whose value is the given expression.
|
|
//
|
|
// This is used to shim Resource.RawCount and Output.RawConfig to behave
|
|
// more like they do in the old HCL loader.
|
|
type hcl2SingleAttrBody struct {
|
|
Name string
|
|
Expr hcl2.Expression
|
|
}
|
|
|
|
var _ hcl2.Body = hcl2SingleAttrBody{}
|
|
|
|
func (b hcl2SingleAttrBody) Content(schema *hcl2.BodySchema) (*hcl2.BodyContent, hcl2.Diagnostics) {
|
|
content, all, diags := b.content(schema)
|
|
if !all {
|
|
// This should never happen because this body implementation should only
|
|
// be used by code that is aware that it's using a single-attr body.
|
|
diags = append(diags, &hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: "Invalid attribute",
|
|
Detail: fmt.Sprintf("The correct attribute name is %q.", b.Name),
|
|
Subject: b.Expr.Range().Ptr(),
|
|
})
|
|
}
|
|
return content, diags
|
|
}
|
|
|
|
func (b hcl2SingleAttrBody) PartialContent(schema *hcl2.BodySchema) (*hcl2.BodyContent, hcl2.Body, hcl2.Diagnostics) {
|
|
content, all, diags := b.content(schema)
|
|
var remain hcl2.Body
|
|
if all {
|
|
// If the request matched the one attribute we represent, then the
|
|
// remaining body is empty.
|
|
remain = hcl2.EmptyBody()
|
|
} else {
|
|
remain = b
|
|
}
|
|
return content, remain, diags
|
|
}
|
|
|
|
func (b hcl2SingleAttrBody) content(schema *hcl2.BodySchema) (*hcl2.BodyContent, bool, hcl2.Diagnostics) {
|
|
ret := &hcl2.BodyContent{}
|
|
all := false
|
|
var diags hcl2.Diagnostics
|
|
|
|
for _, attrS := range schema.Attributes {
|
|
if attrS.Name == b.Name {
|
|
attrs, _ := b.JustAttributes()
|
|
ret.Attributes = attrs
|
|
all = true
|
|
} else if attrS.Required {
|
|
diags = append(diags, &hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: "Missing attribute",
|
|
Detail: fmt.Sprintf("The attribute %q is required.", attrS.Name),
|
|
Subject: b.Expr.Range().Ptr(),
|
|
})
|
|
}
|
|
}
|
|
|
|
return ret, all, diags
|
|
}
|
|
|
|
func (b hcl2SingleAttrBody) JustAttributes() (hcl2.Attributes, hcl2.Diagnostics) {
|
|
return hcl2.Attributes{
|
|
b.Name: {
|
|
Expr: b.Expr,
|
|
Name: b.Name,
|
|
NameRange: b.Expr.Range(),
|
|
Range: b.Expr.Range(),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (b hcl2SingleAttrBody) MissingItemRange() hcl2.Range {
|
|
return b.Expr.Range()
|
|
}
|