diff --git a/config/hcl2_shim_util.go b/config/hcl2_shim_util.go index 2dc937def..207d10598 100644 --- a/config/hcl2_shim_util.go +++ b/config/hcl2_shim_util.go @@ -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() -} diff --git a/config/hcl2_shim_util_test.go b/config/hcl2_shim_util_test.go index 15382edde..cb93dce4a 100644 --- a/config/hcl2_shim_util_test.go +++ b/config/hcl2_shim_util_test.go @@ -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 diff --git a/config/hcl2shim/single_attr_body.go b/config/hcl2shim/single_attr_body.go new file mode 100644 index 000000000..19651c81c --- /dev/null +++ b/config/hcl2shim/single_attr_body.go @@ -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() +} diff --git a/config/hcl2shim/values.go b/config/hcl2shim/values.go new file mode 100644 index 000000000..0b697a5f5 --- /dev/null +++ b/config/hcl2shim/values.go @@ -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 + } +} diff --git a/config/hcl2shim/values_test.go b/config/hcl2shim/values_test.go new file mode 100644 index 000000000..7f335a3fd --- /dev/null +++ b/config/hcl2shim/values_test.go @@ -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) + } + }) + } +} diff --git a/config/loader_hcl2.go b/config/loader_hcl2.go index ced193bb5..9243bfb76 100644 --- a/config/loader_hcl2.go +++ b/config/loader_hcl2.go @@ -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, }),