mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Merge pull request #10325 from hashicorp/jbardin/GH-10187
Fix some cases for nested maps and lists
This commit is contained in:
commit
7677bd94ed
@ -75,6 +75,8 @@ func addrToSchema(addr []string, schemaMap map[string]*Schema) []*Schema {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
case TypeList, TypeSet:
|
case TypeList, TypeSet:
|
||||||
|
isIndex := len(addr) > 0 && addr[0] == "#"
|
||||||
|
|
||||||
switch v := current.Elem.(type) {
|
switch v := current.Elem.(type) {
|
||||||
case *Resource:
|
case *Resource:
|
||||||
current = &Schema{
|
current = &Schema{
|
||||||
@ -83,20 +85,52 @@ func addrToSchema(addr []string, schemaMap map[string]*Schema) []*Schema {
|
|||||||
}
|
}
|
||||||
case *Schema:
|
case *Schema:
|
||||||
current = v
|
current = v
|
||||||
|
case ValueType:
|
||||||
|
current = &Schema{Type: v}
|
||||||
default:
|
default:
|
||||||
|
// we may not know the Elem type and are just looking for the
|
||||||
|
// index
|
||||||
|
if isIndex {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(addr) == 0 {
|
||||||
|
// we've processed the address, so return what we've
|
||||||
|
// collected
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(addr) == 1 {
|
||||||
|
if _, err := strconv.Atoi(addr[0]); err == nil {
|
||||||
|
// we're indexing a value without a schema. This can
|
||||||
|
// happen if the list is nested in another schema type.
|
||||||
|
// Default to a TypeString like we do with a map
|
||||||
|
current = &Schema{Type: TypeString}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we only have one more thing and the next thing
|
// If we only have one more thing and the next thing
|
||||||
// is a #, then we're accessing the index which is always
|
// is a #, then we're accessing the index which is always
|
||||||
// an int.
|
// an int.
|
||||||
if len(addr) > 0 && addr[0] == "#" {
|
if isIndex {
|
||||||
current = &Schema{Type: TypeInt}
|
current = &Schema{Type: TypeInt}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case TypeMap:
|
case TypeMap:
|
||||||
if len(addr) > 0 {
|
if len(addr) > 0 {
|
||||||
current = &Schema{Type: TypeString}
|
switch v := current.Elem.(type) {
|
||||||
|
case ValueType:
|
||||||
|
current = &Schema{Type: v}
|
||||||
|
default:
|
||||||
|
// maps default to string values. This is all we can have
|
||||||
|
// if this is nested in another list or map.
|
||||||
|
current = &Schema{Type: TypeString}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case typeObject:
|
case typeObject:
|
||||||
// If we're already in the object, then we want to handle Sets
|
// If we're already in the object, then we want to handle Sets
|
||||||
|
@ -104,7 +104,18 @@ func (r *ConfigFieldReader) readMap(k string, schema *Schema) (FieldReadResult,
|
|||||||
// the type of the raw value.
|
// the type of the raw value.
|
||||||
mraw, ok := r.Config.GetRaw(k)
|
mraw, ok := r.Config.GetRaw(k)
|
||||||
if !ok {
|
if !ok {
|
||||||
return FieldReadResult{}, nil
|
// check if this is from an interpolated field by seeing if it exists
|
||||||
|
// in the config
|
||||||
|
_, ok := r.Config.Get(k)
|
||||||
|
if !ok {
|
||||||
|
// this really doesn't exist
|
||||||
|
return FieldReadResult{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We couldn't fetch the value from a nested data structure, so treat the
|
||||||
|
// raw value as an interpolation string. The mraw value is only used
|
||||||
|
// for the type switch below.
|
||||||
|
mraw = "${INTERPOLATED}"
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make(map[string]interface{})
|
result := make(map[string]interface{})
|
||||||
|
@ -219,15 +219,27 @@ func TestConfigFieldReader_ComputedMap(t *testing.T) {
|
|||||||
Type: TypeMap,
|
Type: TypeMap,
|
||||||
Computed: true,
|
Computed: true,
|
||||||
},
|
},
|
||||||
|
"listmap": &Schema{
|
||||||
|
Type: TypeMap,
|
||||||
|
Computed: true,
|
||||||
|
Elem: TypeList,
|
||||||
|
},
|
||||||
|
"maplist": &Schema{
|
||||||
|
Type: TypeList,
|
||||||
|
Computed: true,
|
||||||
|
Elem: TypeMap,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cases := map[string]struct {
|
cases := []struct {
|
||||||
|
Name string
|
||||||
Addr []string
|
Addr []string
|
||||||
Result FieldReadResult
|
Result FieldReadResult
|
||||||
Config *terraform.ResourceConfig
|
Config *terraform.ResourceConfig
|
||||||
Err bool
|
Err bool
|
||||||
}{
|
}{
|
||||||
"set, normal": {
|
{
|
||||||
|
"set, normal",
|
||||||
[]string{"map"},
|
[]string{"map"},
|
||||||
FieldReadResult{
|
FieldReadResult{
|
||||||
Value: map[string]interface{}{
|
Value: map[string]interface{}{
|
||||||
@ -244,7 +256,8 @@ func TestConfigFieldReader_ComputedMap(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
|
|
||||||
"computed element": {
|
{
|
||||||
|
"computed element",
|
||||||
[]string{"map"},
|
[]string{"map"},
|
||||||
FieldReadResult{
|
FieldReadResult{
|
||||||
Exists: true,
|
Exists: true,
|
||||||
@ -263,7 +276,8 @@ func TestConfigFieldReader_ComputedMap(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
|
|
||||||
"native map": {
|
{
|
||||||
|
"native map",
|
||||||
[]string{"map"},
|
[]string{"map"},
|
||||||
FieldReadResult{
|
FieldReadResult{
|
||||||
Value: map[string]interface{}{
|
Value: map[string]interface{}{
|
||||||
@ -292,27 +306,147 @@ func TestConfigFieldReader_ComputedMap(t *testing.T) {
|
|||||||
}),
|
}),
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"map-from-list-of-maps",
|
||||||
|
[]string{"maplist", "0"},
|
||||||
|
FieldReadResult{
|
||||||
|
Value: map[string]interface{}{
|
||||||
|
"key": "bar",
|
||||||
|
},
|
||||||
|
Exists: true,
|
||||||
|
Computed: false,
|
||||||
|
},
|
||||||
|
testConfigInterpolate(t, map[string]interface{}{
|
||||||
|
"maplist": "${var.foo}",
|
||||||
|
}, map[string]ast.Variable{
|
||||||
|
"var.foo": ast.Variable{
|
||||||
|
Type: ast.TypeList,
|
||||||
|
Value: []ast.Variable{
|
||||||
|
{
|
||||||
|
Type: ast.TypeMap,
|
||||||
|
Value: map[string]ast.Variable{
|
||||||
|
"key": ast.Variable{
|
||||||
|
Type: ast.TypeString,
|
||||||
|
Value: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"value-from-list-of-maps",
|
||||||
|
[]string{"maplist", "0", "key"},
|
||||||
|
FieldReadResult{
|
||||||
|
Value: "bar",
|
||||||
|
Exists: true,
|
||||||
|
Computed: false,
|
||||||
|
},
|
||||||
|
testConfigInterpolate(t, map[string]interface{}{
|
||||||
|
"maplist": "${var.foo}",
|
||||||
|
}, map[string]ast.Variable{
|
||||||
|
"var.foo": ast.Variable{
|
||||||
|
Type: ast.TypeList,
|
||||||
|
Value: []ast.Variable{
|
||||||
|
{
|
||||||
|
Type: ast.TypeMap,
|
||||||
|
Value: map[string]ast.Variable{
|
||||||
|
"key": ast.Variable{
|
||||||
|
Type: ast.TypeString,
|
||||||
|
Value: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"list-from-map-of-lists",
|
||||||
|
[]string{"listmap", "key"},
|
||||||
|
FieldReadResult{
|
||||||
|
Value: []interface{}{"bar"},
|
||||||
|
Exists: true,
|
||||||
|
Computed: false,
|
||||||
|
},
|
||||||
|
testConfigInterpolate(t, map[string]interface{}{
|
||||||
|
"listmap": "${var.foo}",
|
||||||
|
}, map[string]ast.Variable{
|
||||||
|
"var.foo": ast.Variable{
|
||||||
|
Type: ast.TypeMap,
|
||||||
|
Value: map[string]ast.Variable{
|
||||||
|
"key": ast.Variable{
|
||||||
|
Type: ast.TypeList,
|
||||||
|
Value: []ast.Variable{
|
||||||
|
ast.Variable{
|
||||||
|
Type: ast.TypeString,
|
||||||
|
Value: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"value-from-map-of-lists",
|
||||||
|
[]string{"listmap", "key", "0"},
|
||||||
|
FieldReadResult{
|
||||||
|
Value: "bar",
|
||||||
|
Exists: true,
|
||||||
|
Computed: false,
|
||||||
|
},
|
||||||
|
testConfigInterpolate(t, map[string]interface{}{
|
||||||
|
"listmap": "${var.foo}",
|
||||||
|
}, map[string]ast.Variable{
|
||||||
|
"var.foo": ast.Variable{
|
||||||
|
Type: ast.TypeMap,
|
||||||
|
Value: map[string]ast.Variable{
|
||||||
|
"key": ast.Variable{
|
||||||
|
Type: ast.TypeList,
|
||||||
|
Value: []ast.Variable{
|
||||||
|
ast.Variable{
|
||||||
|
Type: ast.TypeString,
|
||||||
|
Value: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, tc := range cases {
|
for i, tc := range cases {
|
||||||
r := &ConfigFieldReader{
|
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
|
||||||
Schema: schema,
|
r := &ConfigFieldReader{
|
||||||
Config: tc.Config,
|
Schema: schema,
|
||||||
}
|
Config: tc.Config,
|
||||||
out, err := r.ReadField(tc.Addr)
|
|
||||||
if err != nil != tc.Err {
|
|
||||||
t.Fatalf("%s: err: %s", name, err)
|
|
||||||
}
|
|
||||||
if s, ok := out.Value.(*Set); ok {
|
|
||||||
// If it is a set, convert to the raw map
|
|
||||||
out.Value = s.m
|
|
||||||
if len(s.m) == 0 {
|
|
||||||
out.Value = nil
|
|
||||||
}
|
}
|
||||||
}
|
out, err := r.ReadField(tc.Addr)
|
||||||
if !reflect.DeepEqual(tc.Result, out) {
|
if err != nil != tc.Err {
|
||||||
t.Fatalf("%s: bad: %#v", name, out)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if s, ok := out.Value.(*Set); ok {
|
||||||
|
// If it is a set, convert to the raw map
|
||||||
|
out.Value = s.m
|
||||||
|
if len(s.m) == 0 {
|
||||||
|
out.Value = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(tc.Result, out) {
|
||||||
|
t.Fatalf("\nexpected: %#v\ngot: %#v", tc.Result, out)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,6 +172,42 @@ func TestResourceConfigGet(t *testing.T) {
|
|||||||
Value: nil,
|
Value: nil,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Reference list of maps variable.
|
||||||
|
// This does not work from GetRaw.
|
||||||
|
{
|
||||||
|
Vars: map[string]interface{}{
|
||||||
|
"maplist": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"key": "a",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"key": "b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"maplist": "${var.maplist}",
|
||||||
|
},
|
||||||
|
Key: "maplist.0",
|
||||||
|
Value: map[string]interface{}{"key": "a"},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Reference a map-of-lists variable.
|
||||||
|
// This does not work from GetRaw.
|
||||||
|
{
|
||||||
|
Vars: map[string]interface{}{
|
||||||
|
"listmap": map[string]interface{}{
|
||||||
|
"key1": []interface{}{"a", "b"},
|
||||||
|
"key2": []interface{}{"c", "d"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"listmap": "${var.listmap}",
|
||||||
|
},
|
||||||
|
Key: "listmap.key1",
|
||||||
|
Value: []interface{}{"a", "b"},
|
||||||
|
},
|
||||||
|
|
||||||
// FIXME: this is ambiguous, and matches the nested map
|
// FIXME: this is ambiguous, and matches the nested map
|
||||||
// leaving here to catch this behaviour if it changes.
|
// leaving here to catch this behaviour if it changes.
|
||||||
{
|
{
|
||||||
@ -270,6 +306,92 @@ func TestResourceConfigGet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResourceConfigGetRaw(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
Config map[string]interface{}
|
||||||
|
Vars map[string]interface{}
|
||||||
|
Key string
|
||||||
|
Value interface{}
|
||||||
|
}{
|
||||||
|
// Referencing a list-of-maps variable doesn't work from GetRaw.
|
||||||
|
// The ConfigFieldReader currently catches this case and looks up the
|
||||||
|
// variable in the config.
|
||||||
|
{
|
||||||
|
Vars: map[string]interface{}{
|
||||||
|
"maplist": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"key": "a",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"key": "b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"maplist": "${var.maplist}",
|
||||||
|
},
|
||||||
|
Key: "maplist.0",
|
||||||
|
Value: nil,
|
||||||
|
},
|
||||||
|
// Reference a map-of-lists variable.
|
||||||
|
// The ConfigFieldReader currently catches this case and looks up the
|
||||||
|
// variable in the config.
|
||||||
|
{
|
||||||
|
Vars: map[string]interface{}{
|
||||||
|
"listmap": map[string]interface{}{
|
||||||
|
"key1": []interface{}{"a", "b"},
|
||||||
|
"key2": []interface{}{"c", "d"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"listmap": "${var.listmap}",
|
||||||
|
},
|
||||||
|
Key: "listmap.key1",
|
||||||
|
Value: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
var rawC *config.RawConfig
|
||||||
|
if tc.Config != nil {
|
||||||
|
var err error
|
||||||
|
rawC, err = config.NewRawConfig(tc.Config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.Vars != nil {
|
||||||
|
vs := make(map[string]ast.Variable)
|
||||||
|
for k, v := range tc.Vars {
|
||||||
|
hilVar, err := hil.InterfaceToVariable(v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%#v to var: %s", v, err)
|
||||||
|
}
|
||||||
|
vs["var."+k] = hilVar
|
||||||
|
}
|
||||||
|
if err := rawC.Interpolate(vs); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rc := NewResourceConfig(rawC)
|
||||||
|
rc.interpolateForce()
|
||||||
|
|
||||||
|
// Test getting a key
|
||||||
|
t.Run(fmt.Sprintf("get-%d", i), func(t *testing.T) {
|
||||||
|
v, ok := rc.GetRaw(tc.Key)
|
||||||
|
if ok && v == nil {
|
||||||
|
t.Fatal("(nil, true) returned from GetRaw")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(v, tc.Value) {
|
||||||
|
t.Fatalf("%d bad: %#v", i, v)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResourceConfigIsComputed(t *testing.T) {
|
func TestResourceConfigIsComputed(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
Name string
|
Name string
|
||||||
|
Loading…
Reference in New Issue
Block a user