diff --git a/helper/schema/field_reader.go b/helper/schema/field_reader.go index 5606e6d8b7..1660a67027 100644 --- a/helper/schema/field_reader.go +++ b/helper/schema/field_reader.go @@ -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 diff --git a/helper/schema/field_reader_config.go b/helper/schema/field_reader_config.go index 042fe9d0e1..53ff5208f9 100644 --- a/helper/schema/field_reader_config.go +++ b/helper/schema/field_reader_config.go @@ -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{}) diff --git a/helper/schema/field_reader_config_test.go b/helper/schema/field_reader_config_test.go index 0b68cce22d..5db643d75e 100644 --- a/helper/schema/field_reader_config_test.go +++ b/helper/schema/field_reader_config_test.go @@ -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) + } + }) } } diff --git a/terraform/resource_test.go b/terraform/resource_test.go index dde22b1ff4..750e7060d1 100644 --- a/terraform/resource_test.go +++ b/terraform/resource_test.go @@ -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