Merge pull request #10325 from hashicorp/jbardin/GH-10187

Fix some cases for nested maps and lists
This commit is contained in:
James Bardin 2016-11-29 12:24:53 -05:00 committed by GitHub
commit 7677bd94ed
4 changed files with 326 additions and 25 deletions

View File

@ -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,21 +85,53 @@ 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 {
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} 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
// and Lists specially. Basically, their next key is the lookup // and Lists specially. Basically, their next key is the lookup

View File

@ -104,9 +104,20 @@ 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 {
// 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 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{})
computed := false computed := false
switch m := mraw.(type) { switch m := mraw.(type) {

View File

@ -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,16 +306,135 @@ 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 {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
r := &ConfigFieldReader{ r := &ConfigFieldReader{
Schema: schema, Schema: schema,
Config: tc.Config, Config: tc.Config,
} }
out, err := r.ReadField(tc.Addr) out, err := r.ReadField(tc.Addr)
if err != nil != tc.Err { if err != nil != tc.Err {
t.Fatalf("%s: err: %s", name, err) t.Fatal(err)
} }
if s, ok := out.Value.(*Set); ok { if s, ok := out.Value.(*Set); ok {
// If it is a set, convert to the raw map // If it is a set, convert to the raw map
@ -311,8 +444,9 @@ func TestConfigFieldReader_ComputedMap(t *testing.T) {
} }
} }
if !reflect.DeepEqual(tc.Result, out) { if !reflect.DeepEqual(tc.Result, out) {
t.Fatalf("%s: bad: %#v", name, out) t.Fatalf("\nexpected: %#v\ngot: %#v", tc.Result, out)
} }
})
} }
} }

View File

@ -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