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
|
||||
}
|
||||
case TypeList, TypeSet:
|
||||
isIndex := len(addr) > 0 && addr[0] == "#"
|
||||
|
||||
switch v := current.Elem.(type) {
|
||||
case *Resource:
|
||||
current = &Schema{
|
||||
@ -83,20 +85,52 @@ func addrToSchema(addr []string, schemaMap map[string]*Schema) []*Schema {
|
||||
}
|
||||
case *Schema:
|
||||
current = v
|
||||
case ValueType:
|
||||
current = &Schema{Type: v}
|
||||
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
|
||||
}
|
||||
|
||||
// If we only have one more thing and the next thing
|
||||
// is a #, then we're accessing the index which is always
|
||||
// an int.
|
||||
if len(addr) > 0 && addr[0] == "#" {
|
||||
if isIndex {
|
||||
current = &Schema{Type: TypeInt}
|
||||
break
|
||||
}
|
||||
|
||||
case TypeMap:
|
||||
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:
|
||||
// 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.
|
||||
mraw, ok := r.Config.GetRaw(k)
|
||||
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{})
|
||||
|
@ -219,15 +219,27 @@ func TestConfigFieldReader_ComputedMap(t *testing.T) {
|
||||
Type: TypeMap,
|
||||
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
|
||||
Result FieldReadResult
|
||||
Config *terraform.ResourceConfig
|
||||
Err bool
|
||||
}{
|
||||
"set, normal": {
|
||||
{
|
||||
"set, normal",
|
||||
[]string{"map"},
|
||||
FieldReadResult{
|
||||
Value: map[string]interface{}{
|
||||
@ -244,7 +256,8 @@ func TestConfigFieldReader_ComputedMap(t *testing.T) {
|
||||
false,
|
||||
},
|
||||
|
||||
"computed element": {
|
||||
{
|
||||
"computed element",
|
||||
[]string{"map"},
|
||||
FieldReadResult{
|
||||
Exists: true,
|
||||
@ -263,7 +276,8 @@ func TestConfigFieldReader_ComputedMap(t *testing.T) {
|
||||
false,
|
||||
},
|
||||
|
||||
"native map": {
|
||||
{
|
||||
"native map",
|
||||
[]string{"map"},
|
||||
FieldReadResult{
|
||||
Value: map[string]interface{}{
|
||||
@ -292,27 +306,147 @@ func TestConfigFieldReader_ComputedMap(t *testing.T) {
|
||||
}),
|
||||
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 {
|
||||
r := &ConfigFieldReader{
|
||||
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
|
||||
for i, tc := range cases {
|
||||
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
|
||||
r := &ConfigFieldReader{
|
||||
Schema: schema,
|
||||
Config: tc.Config,
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(tc.Result, out) {
|
||||
t.Fatalf("%s: bad: %#v", name, out)
|
||||
}
|
||||
out, err := r.ReadField(tc.Addr)
|
||||
if err != nil != tc.Err {
|
||||
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,
|
||||
},
|
||||
|
||||
// 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
|
||||
// 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) {
|
||||
cases := []struct {
|
||||
Name string
|
||||
|
Loading…
Reference in New Issue
Block a user