diff --git a/hcl2helper/doc.go b/hcl2helper/doc.go new file mode 100644 index 00000000..d213fe8d --- /dev/null +++ b/hcl2helper/doc.go @@ -0,0 +1,4 @@ +/* +Package hcl2helper provides helper functions for parsing or getting hcl2 types to and from a Packer plugin config. +*/ +package hcl2helper diff --git a/hcl2helper/mock.go b/hcl2helper/mock.go new file mode 100644 index 00000000..25ba154d --- /dev/null +++ b/hcl2helper/mock.go @@ -0,0 +1,40 @@ +//go:generate mapstructure-to-hcl2 -type MockConfig,NestedMockConfig,MockTag + +package hcl2helper + +import ( + "time" + + "github.com/hashicorp/packer-plugin-sdk/template/config" +) + +type NestedMockConfig struct { + String string `mapstructure:"string"` + Int int `mapstructure:"int"` + Int64 int64 `mapstructure:"int64"` + Bool bool `mapstructure:"bool"` + Trilean config.Trilean `mapstructure:"trilean"` + Duration time.Duration `mapstructure:"duration"` + MapStringString map[string]string `mapstructure:"map_string_string"` + SliceString []string `mapstructure:"slice_string"` + SliceSliceString [][]string `mapstructure:"slice_slice_string"` + NamedMapStringString NamedMapStringString `mapstructure:"named_map_string_string"` + NamedString NamedString `mapstructure:"named_string"` + Tags []MockTag `mapstructure:"tag"` + Datasource string `mapstructure:"data_source"` +} + +type MockTag struct { + Key string `mapstructure:"key"` + Value string `mapstructure:"value"` +} + +type MockConfig struct { + NotSquashed string `mapstructure:"not_squashed"` + NestedMockConfig `mapstructure:",squash"` + Nested NestedMockConfig `mapstructure:"nested"` + NestedSlice []NestedMockConfig `mapstructure:"nested_slice"` +} + +type NamedMapStringString map[string]string +type NamedString string diff --git a/hcl2helper/mock.hcl2spec.go b/hcl2helper/mock.hcl2spec.go new file mode 100644 index 00000000..b1cac77f --- /dev/null +++ b/hcl2helper/mock.hcl2spec.go @@ -0,0 +1,133 @@ +// Code generated by "mapstructure-to-hcl2 -type MockConfig,NestedMockConfig,MockTag"; DO NOT EDIT. + +package hcl2helper + +import ( + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/zclconf/go-cty/cty" +) + +// FlatMockConfig is an auto-generated flat version of MockConfig. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatMockConfig struct { + NotSquashed *string `mapstructure:"not_squashed" cty:"not_squashed" hcl:"not_squashed"` + String *string `mapstructure:"string" cty:"string" hcl:"string"` + Int *int `mapstructure:"int" cty:"int" hcl:"int"` + Int64 *int64 `mapstructure:"int64" cty:"int64" hcl:"int64"` + Bool *bool `mapstructure:"bool" cty:"bool" hcl:"bool"` + Trilean *bool `mapstructure:"trilean" cty:"trilean" hcl:"trilean"` + Duration *string `mapstructure:"duration" cty:"duration" hcl:"duration"` + MapStringString map[string]string `mapstructure:"map_string_string" cty:"map_string_string" hcl:"map_string_string"` + SliceString []string `mapstructure:"slice_string" cty:"slice_string" hcl:"slice_string"` + SliceSliceString [][]string `mapstructure:"slice_slice_string" cty:"slice_slice_string" hcl:"slice_slice_string"` + NamedMapStringString NamedMapStringString `mapstructure:"named_map_string_string" cty:"named_map_string_string" hcl:"named_map_string_string"` + NamedString *NamedString `mapstructure:"named_string" cty:"named_string" hcl:"named_string"` + Tags []FlatMockTag `mapstructure:"tag" cty:"tag" hcl:"tag"` + Datasource *string `mapstructure:"data_source" cty:"data_source" hcl:"data_source"` + Nested *FlatNestedMockConfig `mapstructure:"nested" cty:"nested" hcl:"nested"` + NestedSlice []FlatNestedMockConfig `mapstructure:"nested_slice" cty:"nested_slice" hcl:"nested_slice"` +} + +// FlatMapstructure returns a new FlatMockConfig. +// FlatMockConfig is an auto-generated flat version of MockConfig. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*MockConfig) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatMockConfig) +} + +// HCL2Spec returns the hcl spec of a MockConfig. +// This spec is used by HCL to read the fields of MockConfig. +// The decoded values from this spec will then be applied to a FlatMockConfig. +func (*FlatMockConfig) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "not_squashed": &hcldec.AttrSpec{Name: "not_squashed", Type: cty.String, Required: false}, + "string": &hcldec.AttrSpec{Name: "string", Type: cty.String, Required: false}, + "int": &hcldec.AttrSpec{Name: "int", Type: cty.Number, Required: false}, + "int64": &hcldec.AttrSpec{Name: "int64", Type: cty.Number, Required: false}, + "bool": &hcldec.AttrSpec{Name: "bool", Type: cty.Bool, Required: false}, + "trilean": &hcldec.AttrSpec{Name: "trilean", Type: cty.Bool, Required: false}, + "duration": &hcldec.AttrSpec{Name: "duration", Type: cty.String, Required: false}, + "map_string_string": &hcldec.AttrSpec{Name: "map_string_string", Type: cty.Map(cty.String), Required: false}, + "slice_string": &hcldec.AttrSpec{Name: "slice_string", Type: cty.List(cty.String), Required: false}, + "slice_slice_string": &hcldec.AttrSpec{Name: "slice_slice_string", Type: cty.List(cty.List(cty.String)), Required: false}, + "named_map_string_string": &hcldec.AttrSpec{Name: "named_map_string_string", Type: cty.Map(cty.String), Required: false}, + "named_string": &hcldec.AttrSpec{Name: "named_string", Type: cty.String, Required: false}, + "tag": &hcldec.BlockListSpec{TypeName: "tag", Nested: hcldec.ObjectSpec((*FlatMockTag)(nil).HCL2Spec())}, + "data_source": &hcldec.AttrSpec{Name: "data_source", Type: cty.String, Required: false}, + "nested": &hcldec.BlockSpec{TypeName: "nested", Nested: hcldec.ObjectSpec((*FlatNestedMockConfig)(nil).HCL2Spec())}, + "nested_slice": &hcldec.BlockListSpec{TypeName: "nested_slice", Nested: hcldec.ObjectSpec((*FlatNestedMockConfig)(nil).HCL2Spec())}, + } + return s +} + +// FlatMockTag is an auto-generated flat version of MockTag. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatMockTag struct { + Key *string `mapstructure:"key" cty:"key" hcl:"key"` + Value *string `mapstructure:"value" cty:"value" hcl:"value"` +} + +// FlatMapstructure returns a new FlatMockTag. +// FlatMockTag is an auto-generated flat version of MockTag. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*MockTag) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatMockTag) +} + +// HCL2Spec returns the hcl spec of a MockTag. +// This spec is used by HCL to read the fields of MockTag. +// The decoded values from this spec will then be applied to a FlatMockTag. +func (*FlatMockTag) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "key": &hcldec.AttrSpec{Name: "key", Type: cty.String, Required: false}, + "value": &hcldec.AttrSpec{Name: "value", Type: cty.String, Required: false}, + } + return s +} + +// FlatNestedMockConfig is an auto-generated flat version of NestedMockConfig. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatNestedMockConfig struct { + String *string `mapstructure:"string" cty:"string" hcl:"string"` + Int *int `mapstructure:"int" cty:"int" hcl:"int"` + Int64 *int64 `mapstructure:"int64" cty:"int64" hcl:"int64"` + Bool *bool `mapstructure:"bool" cty:"bool" hcl:"bool"` + Trilean *bool `mapstructure:"trilean" cty:"trilean" hcl:"trilean"` + Duration *string `mapstructure:"duration" cty:"duration" hcl:"duration"` + MapStringString map[string]string `mapstructure:"map_string_string" cty:"map_string_string" hcl:"map_string_string"` + SliceString []string `mapstructure:"slice_string" cty:"slice_string" hcl:"slice_string"` + SliceSliceString [][]string `mapstructure:"slice_slice_string" cty:"slice_slice_string" hcl:"slice_slice_string"` + NamedMapStringString NamedMapStringString `mapstructure:"named_map_string_string" cty:"named_map_string_string" hcl:"named_map_string_string"` + NamedString *NamedString `mapstructure:"named_string" cty:"named_string" hcl:"named_string"` + Tags []FlatMockTag `mapstructure:"tag" cty:"tag" hcl:"tag"` + Datasource *string `mapstructure:"data_source" cty:"data_source" hcl:"data_source"` +} + +// FlatMapstructure returns a new FlatNestedMockConfig. +// FlatNestedMockConfig is an auto-generated flat version of NestedMockConfig. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*NestedMockConfig) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatNestedMockConfig) +} + +// HCL2Spec returns the hcl spec of a NestedMockConfig. +// This spec is used by HCL to read the fields of NestedMockConfig. +// The decoded values from this spec will then be applied to a FlatNestedMockConfig. +func (*FlatNestedMockConfig) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "string": &hcldec.AttrSpec{Name: "string", Type: cty.String, Required: false}, + "int": &hcldec.AttrSpec{Name: "int", Type: cty.Number, Required: false}, + "int64": &hcldec.AttrSpec{Name: "int64", Type: cty.Number, Required: false}, + "bool": &hcldec.AttrSpec{Name: "bool", Type: cty.Bool, Required: false}, + "trilean": &hcldec.AttrSpec{Name: "trilean", Type: cty.Bool, Required: false}, + "duration": &hcldec.AttrSpec{Name: "duration", Type: cty.String, Required: false}, + "map_string_string": &hcldec.AttrSpec{Name: "map_string_string", Type: cty.Map(cty.String), Required: false}, + "slice_string": &hcldec.AttrSpec{Name: "slice_string", Type: cty.List(cty.String), Required: false}, + "slice_slice_string": &hcldec.AttrSpec{Name: "slice_slice_string", Type: cty.List(cty.List(cty.String)), Required: false}, + "named_map_string_string": &hcldec.AttrSpec{Name: "named_map_string_string", Type: cty.Map(cty.String), Required: false}, + "named_string": &hcldec.AttrSpec{Name: "named_string", Type: cty.String, Required: false}, + "tag": &hcldec.BlockListSpec{TypeName: "tag", Nested: hcldec.ObjectSpec((*FlatMockTag)(nil).HCL2Spec())}, + "data_source": &hcldec.AttrSpec{Name: "data_source", Type: cty.String, Required: false}, + } + return s +} diff --git a/hcl2helper/values.go b/hcl2helper/values.go new file mode 100644 index 00000000..882fc398 --- /dev/null +++ b/hcl2helper/values.go @@ -0,0 +1,131 @@ +package hcl2helper + +import ( + "fmt" + "time" + + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/hashicorp/packer-plugin-sdk/template/config" + "github.com/mitchellh/mapstructure" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/gocty" +) + +// 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" + +// HCL2ValueFromConfigValue takes a value 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 []string: + vals := make([]cty.Value, len(tv)) + for i, ev := range tv { + vals[i] = cty.StringVal(ev) + } + return cty.ListVal(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)) + } +} + +// HCL2ValueFromConfig takes a struct with it's map of hcldec.Spec, and turns it into +// a cty.Value so it can be used as, for example, a Datasource value. +func HCL2ValueFromConfig(conf interface{}, configSpec map[string]hcldec.Spec) cty.Value { + c := map[string]interface{}{} + if err := mapstructure.Decode(conf, &c); err != nil { + panic(fmt.Errorf("can't convert %#v to cty.Value", conf)) + } + + // Use the HCL2Spec to know the expected cty.Type for an attribute + resp := map[string]cty.Value{} + for k, v := range c { + spec := configSpec[k] + + switch st := spec.(type) { + case *hcldec.BlockListSpec: + // This should be a slice of objects, so we need to take a special care + if hcldec.ImpliedType(st.Nested).IsObjectType() { + res := []cty.Value{} + c := []interface{}{} + if err := mapstructure.Decode(v, &c); err != nil { + panic(fmt.Errorf("can't convert %#v to cty.Value", conf)) + } + types := hcldec.ChildBlockTypes(spec) + for _, e := range c { + res = append(res, HCL2ValueFromConfig(e, types[k].(hcldec.ObjectSpec))) + } + if len(res) != 0 { + resp[k] = cty.ListVal(res) + continue + } + // At this point this is an empty list so we want it to go to gocty.ToCtyValue(v, impT) + // and make it a NullVal + } + } + + impT := hcldec.ImpliedType(spec) + if value, err := gocty.ToCtyValue(v, impT); err == nil { + resp[k] = value + continue + } + + // Uncommon types not caught until now + switch tv := v.(type) { + case config.Trilean: + resp[k] = cty.BoolVal(tv.True()) + continue + case time.Duration: + if tv.Microseconds() == int64(0) { + resp[k] = cty.NumberIntVal(int64(0)) + continue + } + resp[k] = cty.NumberIntVal(v.(time.Duration).Milliseconds()) + continue + } + + // This is a nested object and we should recursively go through the same process + if impT.IsObjectType() { + types := hcldec.ChildBlockTypes(spec) + resp[k] = HCL2ValueFromConfig(v, types[k].(hcldec.ObjectSpec)) + continue + } + + panic("not supported type - contact the Packer team with further information") + } + + // This is decoding structs so it will always be an cty.ObjectVal at the end + return cty.ObjectVal(resp) +} diff --git a/hcl2helper/values_test.go b/hcl2helper/values_test.go new file mode 100644 index 00000000..35b61319 --- /dev/null +++ b/hcl2helper/values_test.go @@ -0,0 +1,320 @@ +package hcl2helper + +import ( + "reflect" + "testing" + "time" + + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/hashicorp/packer-plugin-sdk/template/config" + "github.com/zclconf/go-cty/cty" +) + +func TestHCL2ValueFromConfigValue(t *testing.T) { + tests := []struct { + Name string + Input interface{} + Want cty.Value + }{ + { + Name: "bool true", + Input: true, + Want: cty.True, + }, + { + Name: "bool false", + Input: false, + Want: cty.False, + }, + { + Name: "int", + Input: int(12), + Want: cty.NumberIntVal(12), + }, + { + Name: "float64", + Input: float64(12.5), + Want: cty.NumberFloatVal(12.5), + }, + { + Name: "string", + Input: "hello world", + Want: cty.StringVal("hello world"), + }, + { + Name: "nested map[string]interface{}", + Input: map[string]interface{}{ + "name": "Ermintrude", + "age": int(19), + "address": map[string]interface{}{ + "street": []interface{}{"421 Shoreham Loop"}, + "city": "Fridgewater", + "state": "MA", + "zip": "91037", + }, + }, + Want: cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("Ermintrude"), + "age": cty.NumberIntVal(19), + "address": cty.ObjectVal(map[string]cty.Value{ + "street": cty.TupleVal([]cty.Value{cty.StringVal("421 Shoreham Loop")}), + "city": cty.StringVal("Fridgewater"), + "state": cty.StringVal("MA"), + "zip": cty.StringVal("91037"), + }), + }), + }, + { + Name: "simple map[string]interface{}", + Input: map[string]interface{}{ + "foo": "bar", + "bar": "baz", + }, + Want: cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + "bar": cty.StringVal("baz"), + }), + }, + { + Name: "[]interface{} as tuple", + Input: []interface{}{ + "foo", + true, + }, + Want: cty.TupleVal([]cty.Value{ + cty.StringVal("foo"), + cty.True, + }), + }, + { + Name: "nil", + Input: nil, + Want: cty.NullVal(cty.DynamicPseudoType), + }, + { + Name: "UnknownVariableValue", + Input: UnknownVariableValue, + Want: cty.DynamicVal, + }, + { + Name: "SliceString", + Input: []string{ + "a", + "b", + "c", + }, + Want: cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + cty.StringVal("c"), + }), + }, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + got := HCL2ValueFromConfigValue(test.Input) + if !reflect.DeepEqual(got, test.Want) { + t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, got, test.Want) + } + }) + } +} + +func TestHCL2ValueFromConfig(t *testing.T) { + tests := []struct { + Name string + Input interface{} + Spec map[string]hcldec.Spec + Want cty.Value + }{ + { + Name: "Empty config", + Input: MockConfig{}, + Spec: new(MockConfig).FlatMapstructure().HCL2Spec(), + Want: cty.ObjectVal(map[string]cty.Value{ + "not_squashed": cty.StringVal(""), + "string": cty.StringVal(""), + "int": cty.NumberIntVal(int64(0)), + "int64": cty.NumberIntVal(int64(0)), + "bool": cty.False, + "trilean": cty.False, + "duration": cty.NumberIntVal(int64(0)), + "map_string_string": cty.NullVal(cty.Map(cty.String)), + "slice_string": cty.NullVal(cty.List(cty.String)), + "slice_slice_string": cty.NullVal(cty.List(cty.List(cty.String))), + "named_map_string_string": cty.NullVal(cty.Map(cty.String)), + "named_string": cty.StringVal(""), + "tag": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "key": cty.String, "value": cty.String, + }))), + "data_source": cty.StringVal(""), + "nested": cty.ObjectVal(map[string]cty.Value{ + "string": cty.StringVal(""), + "int": cty.NumberIntVal(int64(0)), + "int64": cty.NumberIntVal(int64(0)), + "bool": cty.False, + "trilean": cty.False, + "duration": cty.NumberIntVal(int64(0)), + "map_string_string": cty.NullVal(cty.Map(cty.String)), + "slice_string": cty.NullVal(cty.List(cty.String)), + "slice_slice_string": cty.NullVal(cty.List(cty.List(cty.String))), + "named_map_string_string": cty.NullVal(cty.Map(cty.String)), + "named_string": cty.StringVal(""), + "tag": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "key": cty.String, "value": cty.String, + }))), + "data_source": cty.StringVal(""), + }), + "nested_slice": cty.NullVal(hcldec.ImpliedType(new(MockConfig).FlatMapstructure().HCL2Spec()["nested_slice"])), + }), + }, + { + Name: "Full filled config", + Input: MockConfig{ + NotSquashed: "not squashed", + NestedMockConfig: NestedMockConfig{ + String: "string", + Int: 1, + Int64: int64(2), + Bool: true, + Trilean: config.TriTrue, + Duration: 10 * time.Second, + MapStringString: map[string]string{"a": "b"}, + SliceString: []string{"a", "b"}, + SliceSliceString: [][]string{{"a", "b"}}, + NamedMapStringString: NamedMapStringString{"a": "b"}, + NamedString: "named string", + Tags: []MockTag{{ + Key: "a", + Value: "b", + }}, + Datasource: "datasource", + }, + Nested: NestedMockConfig{ + String: "string", + Int: 1, + Int64: int64(2), + Bool: true, + Trilean: config.TriTrue, + Duration: 10 * time.Second, + MapStringString: map[string]string{"a": "b"}, + SliceString: []string{"a", "b"}, + SliceSliceString: [][]string{{"a", "b"}}, + NamedMapStringString: NamedMapStringString{"a": "b"}, + NamedString: "named string", + Tags: []MockTag{{ + Key: "a", + Value: "b", + }}, + Datasource: "datasource", + }, + NestedSlice: []NestedMockConfig{ + { + String: "string", + Int: 1, + Int64: int64(2), + Bool: true, + Trilean: config.TriTrue, + Duration: 10 * time.Second, + MapStringString: map[string]string{"a": "b"}, + SliceString: []string{"a", "b"}, + SliceSliceString: [][]string{{"a", "b"}}, + NamedMapStringString: NamedMapStringString{"a": "b"}, + NamedString: "named string", + Tags: []MockTag{{ + Key: "a", + Value: "b", + }}, + Datasource: "datasource", + }, + }, + }, + Spec: new(MockConfig).FlatMapstructure().HCL2Spec(), + Want: cty.ObjectVal(map[string]cty.Value{ + "not_squashed": cty.StringVal("not squashed"), + "string": cty.StringVal("string"), + "int": cty.NumberIntVal(int64(1)), + "int64": cty.NumberIntVal(int64(2)), + "bool": cty.True, + "trilean": cty.True, + "duration": cty.NumberIntVal((10 * time.Second).Milliseconds()), + "map_string_string": cty.MapVal(map[string]cty.Value{"a": cty.StringVal("b")}), + "slice_string": cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + }), + "slice_slice_string": cty.ListVal([]cty.Value{cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + })}), + "named_map_string_string": cty.MapVal(map[string]cty.Value{"a": cty.StringVal("b")}), + "named_string": cty.StringVal("named string"), + "tag": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "key": cty.StringVal("a"), + "value": cty.StringVal("b"), + })}), + "data_source": cty.StringVal("datasource"), + "nested": cty.ObjectVal(map[string]cty.Value{ + "string": cty.StringVal("string"), + "int": cty.NumberIntVal(int64(1)), + "int64": cty.NumberIntVal(int64(2)), + "bool": cty.True, + "trilean": cty.True, + "duration": cty.NumberIntVal((10 * time.Second).Milliseconds()), + "map_string_string": cty.MapVal(map[string]cty.Value{"a": cty.StringVal("b")}), + "slice_string": cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + }), + "slice_slice_string": cty.ListVal([]cty.Value{cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + })}), + "named_map_string_string": cty.MapVal(map[string]cty.Value{"a": cty.StringVal("b")}), + "named_string": cty.StringVal("named string"), + "tag": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "key": cty.StringVal("a"), + "value": cty.StringVal("b"), + })}), + "data_source": cty.StringVal("datasource"), + }), + "nested_slice": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "string": cty.StringVal("string"), + "int": cty.NumberIntVal(int64(1)), + "int64": cty.NumberIntVal(int64(2)), + "bool": cty.True, + "trilean": cty.True, + "duration": cty.NumberIntVal((10 * time.Second).Milliseconds()), + "map_string_string": cty.MapVal(map[string]cty.Value{"a": cty.StringVal("b")}), + "slice_string": cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + }), + "slice_slice_string": cty.ListVal([]cty.Value{cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + })}), + "named_map_string_string": cty.MapVal(map[string]cty.Value{"a": cty.StringVal("b")}), + "named_string": cty.StringVal("named string"), + "tag": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "key": cty.StringVal("a"), + "value": cty.StringVal("b"), + })}), + "data_source": cty.StringVal("datasource"), + }), + }), + }), + }, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + got := HCL2ValueFromConfig(test.Input, test.Spec) + if !reflect.DeepEqual(got, test.Want) { + t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, got, test.Want) + } + }) + } +}