Check for interpolated values when reading a map

Accessing an interpolated value in a map through ConfigFieldReader can
fail, because GetRaw can't access interpolated values, so check if the
value exists at all by looking in the config. If the config has a value,
assume our map's value is interpolated and proceed as such.

We also need to lookup the correct schema to properly read a field from
a nested structure.

- Maps previously always defaulted to TypeString. Now check if Elem is a
  ValueType and use that if applicable
- Lists now return the schema for nested element types, defaulting to a
  TypeString like maps.

This only allows maps and lists to be nested one level deep, and the
inner map or list must only contain string values.
This commit is contained in:
James Bardin 2016-11-23 13:43:56 -05:00
parent 16597d4a55
commit 01be1a5ecd
3 changed files with 204 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)
}
})
}
}