config/hcl2shim: make some of the HCL2 shim functions public

The value-conversion machinery is also needed in the main "terraform"
package to help us populate our HCL2 evaluation scope, so a subset of the
shim functions move here into a new package where they can be public.

Some of them remain private within the config package since they depend
on some other symbols in the config package, and they are not needed
by outside callers anyway.
This commit is contained in:
Martin Atkins 2017-10-13 18:50:10 -07:00
parent 094cdca688
commit 71e989ba3e
6 changed files with 524 additions and 494 deletions

View File

@ -2,11 +2,11 @@ package config
import (
"fmt"
"math/big"
"github.com/zclconf/go-cty/cty/function/stdlib"
"github.com/hashicorp/hil/ast"
"github.com/hashicorp/terraform/config/hcl2shim"
hcl2 "github.com/hashicorp/hcl2/hcl"
"github.com/zclconf/go-cty/cty"
@ -20,237 +20,6 @@ import (
// 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{}
@ -284,25 +53,25 @@ func hcl2InterpolationFuncShim(hilFunc ast.Function) function.Function {
for i, hilArgType := range hilFunc.ArgTypes {
spec.Params = append(spec.Params, function.Parameter{
Type: hcl2TypeForHILType(hilArgType),
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: hcl2TypeForHILType(hilFunc.VariadicType),
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 hcl2TypeForHILType(hilFunc.ReturnType), nil
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 := hilVariableFromHCL2Value(arg)
hilV := hcl2shim.HILVariableFromHCL2Value(arg)
// Although the cty function system does automatic type conversions
// to match the argument types, cty doesn't distinguish int and
@ -337,7 +106,7 @@ func hcl2InterpolationFuncShim(hilFunc ast.Function) function.Function {
// 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{
rv := hcl2shim.HCL2ValueFromHILVariable(ast.Variable{
Type: hilFunc.ReturnType,
Value: hilResult,
})
@ -363,81 +132,3 @@ func hcl2EvalWithUnknownVars(expr hcl2.Expression) (cty.Value, hcl2.Diagnostics)
}
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()
}

View File

@ -1,8 +1,6 @@
package config
import (
"fmt"
"reflect"
"testing"
hcl2 "github.com/hashicorp/hcl2/hcl"
@ -10,176 +8,6 @@ import (
"github.com/zclconf/go-cty/cty"
)
func TestConfigValueFromHCL2(t *testing.T) {
tests := []struct {
Input cty.Value
Want interface{}
}{
{
cty.True,
true,
},
{
cty.False,
false,
},
{
cty.NumberIntVal(12),
int(12),
},
{
cty.NumberFloatVal(12.5),
float64(12.5),
},
{
cty.StringVal("hello world"),
"hello world",
},
{
cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("Ermintrude"),
"age": cty.NumberIntVal(19),
"address": cty.ObjectVal(map[string]cty.Value{
"street": cty.ListVal([]cty.Value{cty.StringVal("421 Shoreham Loop")}),
"city": cty.StringVal("Fridgewater"),
"state": cty.StringVal("MA"),
"zip": cty.StringVal("91037"),
}),
}),
map[string]interface{}{
"name": "Ermintrude",
"age": int(19),
"address": map[string]interface{}{
"street": []interface{}{"421 Shoreham Loop"},
"city": "Fridgewater",
"state": "MA",
"zip": "91037",
},
},
},
{
cty.MapVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
"bar": cty.StringVal("baz"),
}),
map[string]interface{}{
"foo": "bar",
"bar": "baz",
},
},
{
cty.TupleVal([]cty.Value{
cty.StringVal("foo"),
cty.True,
}),
[]interface{}{
"foo",
true,
},
},
{
cty.NullVal(cty.String),
nil,
},
{
cty.UnknownVal(cty.String),
UnknownVariableValue,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%#v", test.Input), func(t *testing.T) {
got := configValueFromHCL2(test.Input)
if !reflect.DeepEqual(got, test.Want) {
t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, got, test.Want)
}
})
}
}
func TestHCL2ValueFromConfigValue(t *testing.T) {
tests := []struct {
Input interface{}
Want cty.Value
}{
{
nil,
cty.NullVal(cty.DynamicPseudoType),
},
{
UnknownVariableValue,
cty.DynamicVal,
},
{
true,
cty.True,
},
{
false,
cty.False,
},
{
int(12),
cty.NumberIntVal(12),
},
{
int(0),
cty.Zero,
},
{
float64(12.5),
cty.NumberFloatVal(12.5),
},
{
"hello world",
cty.StringVal("hello world"),
},
{
"O\u0308", // decomposed letter + diacritic
cty.StringVal("\u00D6"), // NFC-normalized on entry into cty
},
{
[]interface{}{},
cty.EmptyTupleVal,
},
{
[]interface{}(nil),
cty.EmptyTupleVal,
},
{
[]interface{}{"hello", "world"},
cty.TupleVal([]cty.Value{cty.StringVal("hello"), cty.StringVal("world")}),
},
{
map[string]interface{}{},
cty.EmptyObjectVal,
},
{
map[string]interface{}(nil),
cty.EmptyObjectVal,
},
{
map[string]interface{}{
"foo": "bar",
"bar": "baz",
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
"bar": cty.StringVal("baz"),
}),
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%#v", test.Input), func(t *testing.T) {
got := hcl2ValueFromConfigValue(test.Input)
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, got, test.Want)
}
})
}
}
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

View File

@ -0,0 +1,85 @@
package hcl2shim
import (
"fmt"
hcl2 "github.com/hashicorp/hcl2/hcl"
)
// SingleAttrBody 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 SingleAttrBody struct {
Name string
Expr hcl2.Expression
}
var _ hcl2.Body = SingleAttrBody{}
func (b SingleAttrBody) 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 SingleAttrBody) 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 SingleAttrBody) 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 SingleAttrBody) 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 SingleAttrBody) MissingItemRange() hcl2.Range {
return b.Expr.Range()
}

246
config/hcl2shim/values.go Normal file
View File

@ -0,0 +1,246 @@
package hcl2shim
import (
"fmt"
"math/big"
"github.com/hashicorp/hil/ast"
"github.com/zclconf/go-cty/cty"
)
// UnknownVariableValue is a sentinel value that can be used
// to denote that the value of a variable is unknown at this time.
// RawConfig uses this information to build up data about
// unknown keys.
const UnknownVariableValue = "74D93920-ED26-11E3-AC10-0800200C9A66"
// 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
}
}

View File

@ -0,0 +1,179 @@
package hcl2shim
import (
"fmt"
"reflect"
"testing"
"github.com/zclconf/go-cty/cty"
)
func TestConfigValueFromHCL2(t *testing.T) {
tests := []struct {
Input cty.Value
Want interface{}
}{
{
cty.True,
true,
},
{
cty.False,
false,
},
{
cty.NumberIntVal(12),
int(12),
},
{
cty.NumberFloatVal(12.5),
float64(12.5),
},
{
cty.StringVal("hello world"),
"hello world",
},
{
cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("Ermintrude"),
"age": cty.NumberIntVal(19),
"address": cty.ObjectVal(map[string]cty.Value{
"street": cty.ListVal([]cty.Value{cty.StringVal("421 Shoreham Loop")}),
"city": cty.StringVal("Fridgewater"),
"state": cty.StringVal("MA"),
"zip": cty.StringVal("91037"),
}),
}),
map[string]interface{}{
"name": "Ermintrude",
"age": int(19),
"address": map[string]interface{}{
"street": []interface{}{"421 Shoreham Loop"},
"city": "Fridgewater",
"state": "MA",
"zip": "91037",
},
},
},
{
cty.MapVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
"bar": cty.StringVal("baz"),
}),
map[string]interface{}{
"foo": "bar",
"bar": "baz",
},
},
{
cty.TupleVal([]cty.Value{
cty.StringVal("foo"),
cty.True,
}),
[]interface{}{
"foo",
true,
},
},
{
cty.NullVal(cty.String),
nil,
},
{
cty.UnknownVal(cty.String),
UnknownVariableValue,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%#v", test.Input), func(t *testing.T) {
got := ConfigValueFromHCL2(test.Input)
if !reflect.DeepEqual(got, test.Want) {
t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, got, test.Want)
}
})
}
}
func TestHCL2ValueFromConfigValue(t *testing.T) {
tests := []struct {
Input interface{}
Want cty.Value
}{
{
nil,
cty.NullVal(cty.DynamicPseudoType),
},
{
UnknownVariableValue,
cty.DynamicVal,
},
{
true,
cty.True,
},
{
false,
cty.False,
},
{
int(12),
cty.NumberIntVal(12),
},
{
int(0),
cty.Zero,
},
{
float64(12.5),
cty.NumberFloatVal(12.5),
},
{
"hello world",
cty.StringVal("hello world"),
},
{
"O\u0308", // decomposed letter + diacritic
cty.StringVal("\u00D6"), // NFC-normalized on entry into cty
},
{
[]interface{}{},
cty.EmptyTupleVal,
},
{
[]interface{}(nil),
cty.EmptyTupleVal,
},
{
[]interface{}{"hello", "world"},
cty.TupleVal([]cty.Value{cty.StringVal("hello"), cty.StringVal("world")}),
},
{
map[string]interface{}{},
cty.EmptyObjectVal,
},
{
map[string]interface{}(nil),
cty.EmptyObjectVal,
},
{
map[string]interface{}{
"foo": "bar",
"bar": "baz",
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
"bar": cty.StringVal("baz"),
}),
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%#v", test.Input), func(t *testing.T) {
got := HCL2ValueFromConfigValue(test.Input)
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, got, test.Want)
}
})
}
}

View File

@ -8,6 +8,7 @@ import (
gohcl2 "github.com/hashicorp/hcl2/gohcl"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl2parse "github.com/hashicorp/hcl2/hclparse"
"github.com/hashicorp/terraform/config/hcl2shim"
"github.com/zclconf/go-cty/cty"
)
@ -258,7 +259,7 @@ func (t *hcl2Configurable) Config() (*Config, error) {
v.DeclaredType = *rawV.DeclaredType
}
if rawV.Default != nil {
v.Default = configValueFromHCL2(*rawV.Default)
v.Default = hcl2shim.ConfigValueFromHCL2(*rawV.Default)
}
if rawV.Description != nil {
v.Description = *rawV.Description
@ -283,8 +284,8 @@ func (t *hcl2Configurable) Config() (*Config, error) {
}
// The result is expected to be a map like map[string]interface{}{"value": something},
// so we'll fake that with our hcl2SingleAttrBody shim.
o.RawConfig = NewRawConfigHCL2(hcl2SingleAttrBody{
// so we'll fake that with our hcl2shim.SingleAttrBody shim.
o.RawConfig = NewRawConfigHCL2(hcl2shim.SingleAttrBody{
Name: "value",
Expr: rawO.ValueExpr,
})
@ -365,7 +366,7 @@ func (t *hcl2Configurable) Config() (*Config, error) {
// a single-element map inside. Since the rest of the world is assuming
// that, we'll mimic it here.
{
countBody := hcl2SingleAttrBody{
countBody := hcl2shim.SingleAttrBody{
Name: "count",
Expr: rawR.CountExpr,
}
@ -398,7 +399,7 @@ func (t *hcl2Configurable) Config() (*Config, error) {
// a single-element map inside. Since the rest of the world is assuming
// that, we'll mimic it here.
{
countBody := hcl2SingleAttrBody{
countBody := hcl2shim.SingleAttrBody{
Name: "count",
Expr: rawR.CountExpr,
}
@ -425,7 +426,7 @@ func (t *hcl2Configurable) Config() (*Config, error) {
}
// The result is expected to be a map like map[string]interface{}{"value": something},
// so we'll fake that with our hcl2SingleAttrBody shim.
// so we'll fake that with our hcl2shim.SingleAttrBody shim.
p.RawConfig = NewRawConfigHCL2(rawP.Config)
config.ProviderConfigs = append(config.ProviderConfigs, p)
@ -441,7 +442,7 @@ func (t *hcl2Configurable) Config() (*Config, error) {
attr := rawL.Definitions[n]
l := &Local{
Name: n,
RawConfig: NewRawConfigHCL2(hcl2SingleAttrBody{
RawConfig: NewRawConfigHCL2(hcl2shim.SingleAttrBody{
Name: "value",
Expr: attr.Expr,
}),