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

View File

@ -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{})

View File

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

View File

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