From 08348eed28aa1b56e18412c1b46e4a0d9355b093 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 18 Dec 2014 11:50:01 -0800 Subject: [PATCH 01/23] config: some extra comments --- config/config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.go b/config/config.go index d356d9f42..abe60db6e 100644 --- a/config/config.go +++ b/config/config.go @@ -365,6 +365,7 @@ func (c *Config) Validate() error { } r.RawCount.init() + // Verify depends on points to resources that all exist for _, d := range r.DependsOn { if _, ok := resources[d]; !ok { errs = append(errs, fmt.Errorf( From f416e0edf02226a5c91c939048e59f2129d1136e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 19 Dec 2014 05:56:46 -0500 Subject: [PATCH 02/23] helper/schema: FieldReader and MapFieldReader --- helper/schema/field_reader.go | 240 +++++++++++++++++++++++++++++ helper/schema/field_reader_test.go | 189 +++++++++++++++++++++++ helper/schema/schema.go | 1 + 3 files changed, 430 insertions(+) create mode 100644 helper/schema/field_reader.go create mode 100644 helper/schema/field_reader_test.go diff --git a/helper/schema/field_reader.go b/helper/schema/field_reader.go new file mode 100644 index 000000000..ba94280bd --- /dev/null +++ b/helper/schema/field_reader.go @@ -0,0 +1,240 @@ +package schema + +import ( + "fmt" + "strconv" + "strings" +) + +// FieldReaders are responsible for decoding fields out of data into +// the proper typed representation. ResourceData uses this to query data +// out of multiple sources: config, state, diffs, etc. +type FieldReader interface { + ReadField([]string, *Schema) (interface{}, bool, error) +} + +// MapFieldReader reads fields out of an untyped map[string]string to +// the best of its ability. +type MapFieldReader struct { + Map map[string]string +} + +func (r *MapFieldReader) ReadField( + address []string, schema *Schema) (interface{}, bool, error) { + k := strings.Join(address, ".") + + switch schema.Type { + case TypeBool: + fallthrough + case TypeInt: + fallthrough + case TypeString: + return r.readPrimitive(k, schema) + case TypeList: + return r.readList(k, schema) + case TypeMap: + return r.readMap(k) + case TypeSet: + return r.readSet(k, schema) + case typeObject: + return r.readObject(k, schema.Elem.(map[string]*Schema)) + default: + panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) + } +} + +func (r *MapFieldReader) readObject( + k string, schema map[string]*Schema) (interface{}, bool, error) { + result := make(map[string]interface{}) + for field, schema := range schema { + v, ok, err := r.ReadField([]string{k, field}, schema) + if err != nil { + return nil, false, err + } + if !ok { + continue + } + + result[field] = v + } + + return result, true, nil +} + +func (r *MapFieldReader) readList( + k string, schema *Schema) (interface{}, bool, error) { + // Get the number of elements in the list + countRaw, countOk, err := r.readPrimitive( + k+".#", &Schema{Type: TypeInt}) + if err != nil { + return nil, false, err + } + if !countOk { + // No count, means we have no list + countRaw = 0 + } + + // If we have an empty list, then return an empty list + if countRaw.(int) == 0 { + return []interface{}{}, true, nil + } + + // Get the schema for the elements + var elemSchema *Schema + switch t := schema.Elem.(type) { + case *Resource: + elemSchema = &Schema{ + Type: typeObject, + Elem: t.Schema, + } + case *Schema: + elemSchema = t + } + + // Go through each count, and get the item value out of it + result := make([]interface{}, countRaw.(int)) + for i, _ := range result { + is := strconv.FormatInt(int64(i), 10) + raw, ok, err := r.ReadField([]string{k, is}, elemSchema) + if err != nil { + return nil, false, err + } + if !ok { + // This should never happen, because by the time the data + // gets to the FieldReaders, all the defaults should be set by + // Schema. + raw = nil + } + + result[i] = raw + } + + return result, true, nil +} + +func (r *MapFieldReader) readMap(k string) (interface{}, bool, error) { + result := make(map[string]interface{}) + resultSet := false + + prefix := k + "." + for k, v := range r.Map { + if !strings.HasPrefix(k, prefix) { + continue + } + + result[k[len(prefix):]] = v + resultSet = true + } + + var resultVal interface{} + if resultSet { + resultVal = result + } + + return resultVal, resultSet, nil +} + +func (r *MapFieldReader) readPrimitive( + k string, schema *Schema) (interface{}, bool, error) { + result, ok := r.Map[k] + if !ok { + return nil, false, nil + } + + var returnVal interface{} + switch schema.Type { + case TypeBool: + if result == "" { + returnVal = false + break + } + + v, err := strconv.ParseBool(result) + if err != nil { + return nil, false, err + } + + returnVal = v + case TypeInt: + if result == "" { + returnVal = 0 + break + } + + v, err := strconv.ParseInt(result, 0, 0) + if err != nil { + return nil, false, err + } + + returnVal = int(v) + case TypeString: + returnVal = result + default: + panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) + } + + return returnVal, true, nil +} + +func (r *MapFieldReader) readSet( + k string, schema *Schema) (interface{}, bool, error) { + // Get the number of elements in the list + countRaw, countOk, err := r.readPrimitive( + k+".#", &Schema{Type: TypeInt}) + if err != nil { + return nil, false, err + } + if !countOk { + // No count, means we have no list + countRaw = 0 + } + + // Create the set that will be our result + set := &Set{F: schema.Set} + + // If we have an empty list, then return an empty list + if countRaw.(int) == 0 { + return set, true, nil + } + + // Get the schema for the elements + var elemSchema *Schema + switch t := schema.Elem.(type) { + case *Resource: + elemSchema = &Schema{ + Type: typeObject, + Elem: t.Schema, + } + case *Schema: + elemSchema = t + } + + // Go through the map and find all the set items + prefix := k + "." + for k, _ := range r.Map { + if !strings.HasPrefix(k, prefix) { + continue + } + if strings.HasPrefix(k, prefix+"#") { + // Ignore the count field + continue + } + + // Split the key, since it might be a sub-object like "idx.field" + parts := strings.Split(k[len(prefix):], ".") + idx := parts[0] + + v, ok, err := r.ReadField([]string{prefix + idx}, elemSchema) + if err != nil { + return nil, false, err + } + if !ok { + // This shouldn't happen because we just verified it does exist + panic("missing field in set: " + k + "." + idx) + } + + set.Add(v) + } + + return set, true, nil +} diff --git a/helper/schema/field_reader_test.go b/helper/schema/field_reader_test.go new file mode 100644 index 000000000..642d7e10e --- /dev/null +++ b/helper/schema/field_reader_test.go @@ -0,0 +1,189 @@ +package schema + +import ( + "reflect" + "testing" +) + +func TestMapFieldReader_impl(t *testing.T) { + var _ FieldReader = new(MapFieldReader) +} + +func TestMapFieldReader(t *testing.T) { + r := &MapFieldReader{ + Map: map[string]string{ + "bool": "true", + "int": "42", + "string": "string", + + "list.#": "2", + "list.0": "foo", + "list.1": "bar", + + "listInt.#": "2", + "listInt.0": "21", + "listInt.1": "42", + + "map.foo": "bar", + "map.bar": "baz", + + "set.#": "2", + "set.10": "10", + "set.50": "50", + + "setDeep.#": "2", + "setDeep.10.index": "10", + "setDeep.10.value": "foo", + "setDeep.50.index": "50", + "setDeep.50.value": "bar", + }, + } + + cases := map[string]struct { + Addr []string + Schema *Schema + Out interface{} + OutOk bool + OutErr bool + }{ + "noexist": { + []string{"boolNOPE"}, + &Schema{Type: TypeBool}, + nil, + false, + false, + }, + + "bool": { + []string{"bool"}, + &Schema{Type: TypeBool}, + true, + true, + false, + }, + + "int": { + []string{"int"}, + &Schema{Type: TypeInt}, + 42, + true, + false, + }, + + "string": { + []string{"string"}, + &Schema{Type: TypeString}, + "string", + true, + false, + }, + + "list": { + []string{"list"}, + &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeString}, + }, + []interface{}{ + "foo", + "bar", + }, + true, + false, + }, + + "listInt": { + []string{"listInt"}, + &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + }, + []interface{}{ + 21, + 42, + }, + true, + false, + }, + + "map": { + []string{"map"}, + &Schema{Type: TypeMap}, + map[string]interface{}{ + "foo": "bar", + "bar": "baz", + }, + true, + false, + }, + + "mapelem": { + []string{"map", "foo"}, + &Schema{Type: TypeString}, + "bar", + true, + false, + }, + + "set": { + []string{"set"}, + &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + []interface{}{10, 50}, + true, + false, + }, + + "setDeep": { + []string{"setDeep"}, + &Schema{ + Type: TypeSet, + Elem: &Resource{ + Schema: map[string]*Schema{ + "index": &Schema{Type: TypeInt}, + "value": &Schema{Type: TypeString}, + }, + }, + Set: func(a interface{}) int { + return a.(map[string]interface{})["index"].(int) + }, + }, + []interface{}{ + map[string]interface{}{ + "index": 10, + "value": "foo", + }, + map[string]interface{}{ + "index": 50, + "value": "bar", + }, + }, + true, + false, + }, + } + + for name, tc := range cases { + out, outOk, outErr := r.ReadField(tc.Addr, tc.Schema) + if (outErr != nil) != tc.OutErr { + t.Fatalf("%s: err: %s", name, outErr) + } + + if s, ok := out.(*Set); ok { + // If it is a set, convert to a list so its more easily checked. + out = s.List() + } + + if !reflect.DeepEqual(out, tc.Out) { + t.Fatalf("%s: out: %#v", name, out) + } + if outOk != tc.OutOk { + t.Fatalf("%s: outOk: %#v", name, outOk) + } + } +} diff --git a/helper/schema/schema.go b/helper/schema/schema.go index 8c2c20674..e875028bd 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -33,6 +33,7 @@ const ( TypeList TypeMap TypeSet + typeObject ) // Schema is used to describe the structure of a value. From 5f063d321f13d1f5dd441c7b19c4c3ec1f6f3a85 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 19 Dec 2014 06:15:04 -0500 Subject: [PATCH 03/23] helper/schema: FieldReader needs to return computed status --- helper/schema/field_reader.go | 234 +----------------- helper/schema/field_reader_map.go | 233 +++++++++++++++++ ...eader_test.go => field_reader_map_test.go} | 26 +- 3 files changed, 254 insertions(+), 239 deletions(-) create mode 100644 helper/schema/field_reader_map.go rename helper/schema/{field_reader_test.go => field_reader_map_test.go} (88%) diff --git a/helper/schema/field_reader.go b/helper/schema/field_reader.go index ba94280bd..dc99eaa70 100644 --- a/helper/schema/field_reader.go +++ b/helper/schema/field_reader.go @@ -1,240 +1,8 @@ package schema -import ( - "fmt" - "strconv" - "strings" -) - // FieldReaders are responsible for decoding fields out of data into // the proper typed representation. ResourceData uses this to query data // out of multiple sources: config, state, diffs, etc. type FieldReader interface { - ReadField([]string, *Schema) (interface{}, bool, error) -} - -// MapFieldReader reads fields out of an untyped map[string]string to -// the best of its ability. -type MapFieldReader struct { - Map map[string]string -} - -func (r *MapFieldReader) ReadField( - address []string, schema *Schema) (interface{}, bool, error) { - k := strings.Join(address, ".") - - switch schema.Type { - case TypeBool: - fallthrough - case TypeInt: - fallthrough - case TypeString: - return r.readPrimitive(k, schema) - case TypeList: - return r.readList(k, schema) - case TypeMap: - return r.readMap(k) - case TypeSet: - return r.readSet(k, schema) - case typeObject: - return r.readObject(k, schema.Elem.(map[string]*Schema)) - default: - panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) - } -} - -func (r *MapFieldReader) readObject( - k string, schema map[string]*Schema) (interface{}, bool, error) { - result := make(map[string]interface{}) - for field, schema := range schema { - v, ok, err := r.ReadField([]string{k, field}, schema) - if err != nil { - return nil, false, err - } - if !ok { - continue - } - - result[field] = v - } - - return result, true, nil -} - -func (r *MapFieldReader) readList( - k string, schema *Schema) (interface{}, bool, error) { - // Get the number of elements in the list - countRaw, countOk, err := r.readPrimitive( - k+".#", &Schema{Type: TypeInt}) - if err != nil { - return nil, false, err - } - if !countOk { - // No count, means we have no list - countRaw = 0 - } - - // If we have an empty list, then return an empty list - if countRaw.(int) == 0 { - return []interface{}{}, true, nil - } - - // Get the schema for the elements - var elemSchema *Schema - switch t := schema.Elem.(type) { - case *Resource: - elemSchema = &Schema{ - Type: typeObject, - Elem: t.Schema, - } - case *Schema: - elemSchema = t - } - - // Go through each count, and get the item value out of it - result := make([]interface{}, countRaw.(int)) - for i, _ := range result { - is := strconv.FormatInt(int64(i), 10) - raw, ok, err := r.ReadField([]string{k, is}, elemSchema) - if err != nil { - return nil, false, err - } - if !ok { - // This should never happen, because by the time the data - // gets to the FieldReaders, all the defaults should be set by - // Schema. - raw = nil - } - - result[i] = raw - } - - return result, true, nil -} - -func (r *MapFieldReader) readMap(k string) (interface{}, bool, error) { - result := make(map[string]interface{}) - resultSet := false - - prefix := k + "." - for k, v := range r.Map { - if !strings.HasPrefix(k, prefix) { - continue - } - - result[k[len(prefix):]] = v - resultSet = true - } - - var resultVal interface{} - if resultSet { - resultVal = result - } - - return resultVal, resultSet, nil -} - -func (r *MapFieldReader) readPrimitive( - k string, schema *Schema) (interface{}, bool, error) { - result, ok := r.Map[k] - if !ok { - return nil, false, nil - } - - var returnVal interface{} - switch schema.Type { - case TypeBool: - if result == "" { - returnVal = false - break - } - - v, err := strconv.ParseBool(result) - if err != nil { - return nil, false, err - } - - returnVal = v - case TypeInt: - if result == "" { - returnVal = 0 - break - } - - v, err := strconv.ParseInt(result, 0, 0) - if err != nil { - return nil, false, err - } - - returnVal = int(v) - case TypeString: - returnVal = result - default: - panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) - } - - return returnVal, true, nil -} - -func (r *MapFieldReader) readSet( - k string, schema *Schema) (interface{}, bool, error) { - // Get the number of elements in the list - countRaw, countOk, err := r.readPrimitive( - k+".#", &Schema{Type: TypeInt}) - if err != nil { - return nil, false, err - } - if !countOk { - // No count, means we have no list - countRaw = 0 - } - - // Create the set that will be our result - set := &Set{F: schema.Set} - - // If we have an empty list, then return an empty list - if countRaw.(int) == 0 { - return set, true, nil - } - - // Get the schema for the elements - var elemSchema *Schema - switch t := schema.Elem.(type) { - case *Resource: - elemSchema = &Schema{ - Type: typeObject, - Elem: t.Schema, - } - case *Schema: - elemSchema = t - } - - // Go through the map and find all the set items - prefix := k + "." - for k, _ := range r.Map { - if !strings.HasPrefix(k, prefix) { - continue - } - if strings.HasPrefix(k, prefix+"#") { - // Ignore the count field - continue - } - - // Split the key, since it might be a sub-object like "idx.field" - parts := strings.Split(k[len(prefix):], ".") - idx := parts[0] - - v, ok, err := r.ReadField([]string{prefix + idx}, elemSchema) - if err != nil { - return nil, false, err - } - if !ok { - // This shouldn't happen because we just verified it does exist - panic("missing field in set: " + k + "." + idx) - } - - set.Add(v) - } - - return set, true, nil + ReadField([]string, *Schema) (interface{}, bool, bool, error) } diff --git a/helper/schema/field_reader_map.go b/helper/schema/field_reader_map.go new file mode 100644 index 000000000..df9391a16 --- /dev/null +++ b/helper/schema/field_reader_map.go @@ -0,0 +1,233 @@ +package schema + +import ( + "fmt" + "strconv" + "strings" +) + +// MapFieldReader reads fields out of an untyped map[string]string to +// the best of its ability. +type MapFieldReader struct { + Map map[string]string +} + +func (r *MapFieldReader) ReadField( + address []string, schema *Schema) (interface{}, bool, bool, error) { + k := strings.Join(address, ".") + + switch schema.Type { + case TypeBool: + fallthrough + case TypeInt: + fallthrough + case TypeString: + return r.readPrimitive(k, schema) + case TypeList: + return r.readList(k, schema) + case TypeMap: + return r.readMap(k) + case TypeSet: + return r.readSet(k, schema) + case typeObject: + return r.readObject(k, schema.Elem.(map[string]*Schema)) + default: + panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) + } +} + +func (r *MapFieldReader) readObject( + k string, schema map[string]*Schema) (interface{}, bool, bool, error) { + result := make(map[string]interface{}) + for field, schema := range schema { + v, ok, _, err := r.ReadField([]string{k, field}, schema) + if err != nil { + return nil, false, false, err + } + if !ok { + continue + } + + result[field] = v + } + + return result, true, false, nil +} + +func (r *MapFieldReader) readList( + k string, schema *Schema) (interface{}, bool, bool, error) { + // Get the number of elements in the list + countRaw, countOk, countComputed, err := r.readPrimitive( + k+".#", &Schema{Type: TypeInt}) + if err != nil { + return nil, false, false, err + } + if !countOk { + // No count, means we have no list + countRaw = 0 + } + + // If we have an empty list, then return an empty list + if countComputed || countRaw.(int) == 0 { + return []interface{}{}, true, countComputed, nil + } + + // Get the schema for the elements + var elemSchema *Schema + switch t := schema.Elem.(type) { + case *Resource: + elemSchema = &Schema{ + Type: typeObject, + Elem: t.Schema, + } + case *Schema: + elemSchema = t + } + + // Go through each count, and get the item value out of it + result := make([]interface{}, countRaw.(int)) + for i, _ := range result { + is := strconv.FormatInt(int64(i), 10) + raw, ok, _, err := r.ReadField([]string{k, is}, elemSchema) + if err != nil { + return nil, false, false, err + } + if !ok { + // This should never happen, because by the time the data + // gets to the FieldReaders, all the defaults should be set by + // Schema. + raw = nil + } + + result[i] = raw + } + + return result, true, false, nil +} + +func (r *MapFieldReader) readMap(k string) (interface{}, bool, bool, error) { + result := make(map[string]interface{}) + resultSet := false + + prefix := k + "." + for k, v := range r.Map { + if !strings.HasPrefix(k, prefix) { + continue + } + + result[k[len(prefix):]] = v + resultSet = true + } + + var resultVal interface{} + if resultSet { + resultVal = result + } + + return resultVal, resultSet, false, nil +} + +func (r *MapFieldReader) readPrimitive( + k string, schema *Schema) (interface{}, bool, bool, error) { + result, ok := r.Map[k] + if !ok { + return nil, false, false, nil + } + + var returnVal interface{} + switch schema.Type { + case TypeBool: + if result == "" { + returnVal = false + break + } + + v, err := strconv.ParseBool(result) + if err != nil { + return nil, false, false, err + } + + returnVal = v + case TypeInt: + if result == "" { + returnVal = 0 + break + } + + v, err := strconv.ParseInt(result, 0, 0) + if err != nil { + return nil, false, false, err + } + + returnVal = int(v) + case TypeString: + returnVal = result + default: + panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) + } + + return returnVal, true, false, nil +} + +func (r *MapFieldReader) readSet( + k string, schema *Schema) (interface{}, bool, bool, error) { + // Get the number of elements in the list + countRaw, countOk, countComputed, err := r.readPrimitive( + k+".#", &Schema{Type: TypeInt}) + if err != nil { + return nil, false, false, err + } + if !countOk { + // No count, means we have no list + countRaw = 0 + } + + // Create the set that will be our result + set := &Set{F: schema.Set} + + // If we have an empty list, then return an empty list + if countComputed || countRaw.(int) == 0 { + return set, true, countComputed, nil + } + + // Get the schema for the elements + var elemSchema *Schema + switch t := schema.Elem.(type) { + case *Resource: + elemSchema = &Schema{ + Type: typeObject, + Elem: t.Schema, + } + case *Schema: + elemSchema = t + } + + // Go through the map and find all the set items + prefix := k + "." + for k, _ := range r.Map { + if !strings.HasPrefix(k, prefix) { + continue + } + if strings.HasPrefix(k, prefix+"#") { + // Ignore the count field + continue + } + + // Split the key, since it might be a sub-object like "idx.field" + parts := strings.Split(k[len(prefix):], ".") + idx := parts[0] + + v, ok, _, err := r.ReadField([]string{prefix + idx}, elemSchema) + if err != nil { + return nil, false, false, err + } + if !ok { + // This shouldn't happen because we just verified it does exist + panic("missing field in set: " + k + "." + idx) + } + + set.Add(v) + } + + return set, true, false, nil +} diff --git a/helper/schema/field_reader_test.go b/helper/schema/field_reader_map_test.go similarity index 88% rename from helper/schema/field_reader_test.go rename to helper/schema/field_reader_map_test.go index 642d7e10e..fba89a158 100644 --- a/helper/schema/field_reader_test.go +++ b/helper/schema/field_reader_map_test.go @@ -40,11 +40,12 @@ func TestMapFieldReader(t *testing.T) { } cases := map[string]struct { - Addr []string - Schema *Schema - Out interface{} - OutOk bool - OutErr bool + Addr []string + Schema *Schema + Out interface{} + OutOk bool + OutComputed bool + OutErr bool }{ "noexist": { []string{"boolNOPE"}, @@ -52,6 +53,7 @@ func TestMapFieldReader(t *testing.T) { nil, false, false, + false, }, "bool": { @@ -60,6 +62,7 @@ func TestMapFieldReader(t *testing.T) { true, true, false, + false, }, "int": { @@ -68,6 +71,7 @@ func TestMapFieldReader(t *testing.T) { 42, true, false, + false, }, "string": { @@ -76,6 +80,7 @@ func TestMapFieldReader(t *testing.T) { "string", true, false, + false, }, "list": { @@ -90,6 +95,7 @@ func TestMapFieldReader(t *testing.T) { }, true, false, + false, }, "listInt": { @@ -104,6 +110,7 @@ func TestMapFieldReader(t *testing.T) { }, true, false, + false, }, "map": { @@ -115,6 +122,7 @@ func TestMapFieldReader(t *testing.T) { }, true, false, + false, }, "mapelem": { @@ -123,6 +131,7 @@ func TestMapFieldReader(t *testing.T) { "bar", true, false, + false, }, "set": { @@ -137,6 +146,7 @@ func TestMapFieldReader(t *testing.T) { []interface{}{10, 50}, true, false, + false, }, "setDeep": { @@ -165,14 +175,18 @@ func TestMapFieldReader(t *testing.T) { }, true, false, + false, }, } for name, tc := range cases { - out, outOk, outErr := r.ReadField(tc.Addr, tc.Schema) + out, outOk, outComputed, outErr := r.ReadField(tc.Addr, tc.Schema) if (outErr != nil) != tc.OutErr { t.Fatalf("%s: err: %s", name, outErr) } + if outComputed != tc.OutComputed { + t.Fatalf("%s: err: %#v", name, outComputed) + } if s, ok := out.(*Set); ok { // If it is a set, convert to a list so its more easily checked. From 944797301522b5784192fc2b05ffb4ca4695df03 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 19 Dec 2014 22:52:53 +0530 Subject: [PATCH 04/23] helper/schema: ConfigFieldReader and generic helpers --- helper/schema/field_reader.go | 123 ++++++++++++ helper/schema/field_reader_config.go | 115 ++++++++++++ helper/schema/field_reader_config_test.go | 217 ++++++++++++++++++++++ helper/schema/field_reader_map.go | 107 +---------- 4 files changed, 460 insertions(+), 102 deletions(-) create mode 100644 helper/schema/field_reader_config.go create mode 100644 helper/schema/field_reader_config_test.go diff --git a/helper/schema/field_reader.go b/helper/schema/field_reader.go index dc99eaa70..187e79bee 100644 --- a/helper/schema/field_reader.go +++ b/helper/schema/field_reader.go @@ -1,8 +1,131 @@ package schema +import ( + "fmt" + "strconv" +) + // FieldReaders are responsible for decoding fields out of data into // the proper typed representation. ResourceData uses this to query data // out of multiple sources: config, state, diffs, etc. type FieldReader interface { ReadField([]string, *Schema) (interface{}, bool, bool, error) } + +// readListField is a generic method for reading a list field out of a +// a FieldReader. It does this based on the assumption that there is a key +// "foo.#" for a list "foo" and that the indexes are "foo.0", "foo.1", etc. +// after that point. +func readListField( + r FieldReader, k string, schema *Schema) (interface{}, bool, bool, error) { + // Get the number of elements in the list + countRaw, countOk, countComputed, err := r.ReadField( + []string{k + ".#"}, &Schema{Type: TypeInt}) + if err != nil { + return nil, false, false, err + } + if !countOk { + // No count, means we have no list + countRaw = 0 + } + + // If we have an empty list, then return an empty list + if countComputed || countRaw.(int) == 0 { + return []interface{}{}, true, countComputed, nil + } + + // Get the schema for the elements + var elemSchema *Schema + switch t := schema.Elem.(type) { + case *Resource: + elemSchema = &Schema{ + Type: typeObject, + Elem: t.Schema, + } + case *Schema: + elemSchema = t + } + + // Go through each count, and get the item value out of it + result := make([]interface{}, countRaw.(int)) + for i, _ := range result { + is := strconv.FormatInt(int64(i), 10) + raw, ok, _, err := r.ReadField([]string{k, is}, elemSchema) + if err != nil { + return nil, false, false, err + } + if !ok { + // This should never happen, because by the time the data + // gets to the FieldReaders, all the defaults should be set by + // Schema. + raw = nil + } + + result[i] = raw + } + + return result, true, false, nil +} + +// readObjectField is a generic method for reading objects out of FieldReaders +// based on the assumption that building an address of []string{k, FIELD} +// will result in the proper field data. +func readObjectField( + r FieldReader, + k string, + schema map[string]*Schema) (interface{}, bool, bool, error) { + result := make(map[string]interface{}) + for field, schema := range schema { + v, ok, _, err := r.ReadField([]string{k, field}, schema) + if err != nil { + return nil, false, false, err + } + if !ok { + continue + } + + result[field] = v + } + + return result, true, false, nil +} + +func stringToPrimitive( + value string, computed bool, schema *Schema) (interface{}, error) { + var returnVal interface{} + switch schema.Type { + case TypeBool: + if value == "" { + returnVal = false + break + } + + v, err := strconv.ParseBool(value) + if err != nil { + return nil, err + } + + returnVal = v + case TypeInt: + if value == "" { + returnVal = 0 + break + } + if computed { + break + } + + v, err := strconv.ParseInt(value, 0, 0) + if err != nil { + return nil, err + } + + returnVal = int(v) + case TypeString: + returnVal = value + default: + panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) + } + + return returnVal, nil +} diff --git a/helper/schema/field_reader_config.go b/helper/schema/field_reader_config.go new file mode 100644 index 000000000..ff418287b --- /dev/null +++ b/helper/schema/field_reader_config.go @@ -0,0 +1,115 @@ +package schema + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/mapstructure" +) + +// ConfigFieldReader reads fields out of an untyped map[string]string to +// the best of its ability. +type ConfigFieldReader struct { + Config *terraform.ResourceConfig +} + +func (r *ConfigFieldReader) ReadField( + address []string, schema *Schema) (interface{}, bool, bool, error) { + k := strings.Join(address, ".") + + switch schema.Type { + case TypeBool: + fallthrough + case TypeInt: + fallthrough + case TypeString: + return r.readPrimitive(k, schema) + case TypeList: + return readListField(r, k, schema) + case TypeMap: + return r.readMap(k) + case TypeSet: + return r.readSet(k, schema) + case typeObject: + return readObjectField(r, k, schema.Elem.(map[string]*Schema)) + default: + panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) + } +} + +func (r *ConfigFieldReader) readMap(k string) (interface{}, bool, bool, error) { + mraw, ok := r.Config.Get(k) + if !ok { + return nil, false, false, nil + } + + result := make(map[string]interface{}) + switch m := mraw.(type) { + case []interface{}: + for _, innerRaw := range m { + for k, v := range innerRaw.(map[string]interface{}) { + result[k] = v + } + } + case []map[string]interface{}: + for _, innerRaw := range m { + for k, v := range innerRaw { + result[k] = v + } + } + case map[string]interface{}: + result = m + default: + panic(fmt.Sprintf("unknown type: %#v", mraw)) + } + + return result, true, false, nil +} + +func (r *ConfigFieldReader) readPrimitive( + k string, schema *Schema) (interface{}, bool, bool, error) { + raw, ok := r.Config.Get(k) + if !ok { + return nil, false, false, nil + } + + var result string + if err := mapstructure.WeakDecode(raw, &result); err != nil { + return nil, false, false, err + } + + computed := r.Config.IsComputed(k) + returnVal, err := stringToPrimitive(result, computed, schema) + if err != nil { + return nil, false, false, err + } + + return returnVal, true, computed, nil +} + +func (r *ConfigFieldReader) readSet( + k string, schema *Schema) (interface{}, bool, bool, error) { + raw, ok, computed, err := readListField(r, k, schema) + if err != nil { + return nil, false, false, err + } + if !ok { + return nil, false, false, nil + } + + // Create the set that will be our result + set := &Set{F: schema.Set} + + // If the list is computed, the set is necessarilly computed + if computed { + return set, true, computed, nil + } + + // Build up the set from the list elements + for _, v := range raw.([]interface{}) { + set.Add(v) + } + + return set, true, false, nil +} diff --git a/helper/schema/field_reader_config_test.go b/helper/schema/field_reader_config_test.go new file mode 100644 index 000000000..be266a57c --- /dev/null +++ b/helper/schema/field_reader_config_test.go @@ -0,0 +1,217 @@ +package schema + +import ( + "reflect" + "testing" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/terraform" +) + +func TestConfigFieldReader_impl(t *testing.T) { + var _ FieldReader = new(ConfigFieldReader) +} + +func TestConfigFieldReader(t *testing.T) { + r := &ConfigFieldReader{ + Config: testConfig(t, map[string]interface{}{ + "bool": true, + "int": 42, + "string": "string", + + "list": []interface{}{"foo", "bar"}, + + "listInt": []interface{}{21, 42}, + + "map": map[string]interface{}{ + "foo": "bar", + "bar": "baz", + }, + + "set": []interface{}{10, 50}, + + "setDeep": []interface{}{ + map[string]interface{}{ + "index": 10, + "value": "foo", + }, + map[string]interface{}{ + "index": 50, + "value": "bar", + }, + }, + }), + } + + cases := map[string]struct { + Addr []string + Schema *Schema + Out interface{} + OutOk bool + OutComputed bool + OutErr bool + }{ + "noexist": { + []string{"boolNOPE"}, + &Schema{Type: TypeBool}, + nil, + false, + false, + false, + }, + + "bool": { + []string{"bool"}, + &Schema{Type: TypeBool}, + true, + true, + false, + false, + }, + + "int": { + []string{"int"}, + &Schema{Type: TypeInt}, + 42, + true, + false, + false, + }, + + "string": { + []string{"string"}, + &Schema{Type: TypeString}, + "string", + true, + false, + false, + }, + + "list": { + []string{"list"}, + &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeString}, + }, + []interface{}{ + "foo", + "bar", + }, + true, + false, + false, + }, + + "listInt": { + []string{"listInt"}, + &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + }, + []interface{}{ + 21, + 42, + }, + true, + false, + false, + }, + + "map": { + []string{"map"}, + &Schema{Type: TypeMap}, + map[string]interface{}{ + "foo": "bar", + "bar": "baz", + }, + true, + false, + false, + }, + + "mapelem": { + []string{"map", "foo"}, + &Schema{Type: TypeString}, + "bar", + true, + false, + false, + }, + + "set": { + []string{"set"}, + &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + []interface{}{10, 50}, + true, + false, + false, + }, + + "setDeep": { + []string{"setDeep"}, + &Schema{ + Type: TypeSet, + Elem: &Resource{ + Schema: map[string]*Schema{ + "index": &Schema{Type: TypeInt}, + "value": &Schema{Type: TypeString}, + }, + }, + Set: func(a interface{}) int { + return a.(map[string]interface{})["index"].(int) + }, + }, + []interface{}{ + map[string]interface{}{ + "index": 10, + "value": "foo", + }, + map[string]interface{}{ + "index": 50, + "value": "bar", + }, + }, + true, + false, + false, + }, + } + + for name, tc := range cases { + out, outOk, outComputed, outErr := r.ReadField(tc.Addr, tc.Schema) + if (outErr != nil) != tc.OutErr { + t.Fatalf("%s: err: %s", name, outErr) + } + if outComputed != tc.OutComputed { + t.Fatalf("%s: err: %#v", name, outComputed) + } + + if s, ok := out.(*Set); ok { + // If it is a set, convert to a list so its more easily checked. + out = s.List() + } + + if !reflect.DeepEqual(out, tc.Out) { + t.Fatalf("%s: out: %#v", name, out) + } + if outOk != tc.OutOk { + t.Fatalf("%s: outOk: %#v", name, outOk) + } + } +} + +func testConfig( + t *testing.T, raw map[string]interface{}) *terraform.ResourceConfig { + rc, err := config.NewRawConfig(raw) + if err != nil { + t.Fatalf("err: %s", err) + } + + return terraform.NewResourceConfig(rc) +} diff --git a/helper/schema/field_reader_map.go b/helper/schema/field_reader_map.go index df9391a16..190219c6a 100644 --- a/helper/schema/field_reader_map.go +++ b/helper/schema/field_reader_map.go @@ -2,7 +2,6 @@ package schema import ( "fmt" - "strconv" "strings" ) @@ -24,87 +23,18 @@ func (r *MapFieldReader) ReadField( case TypeString: return r.readPrimitive(k, schema) case TypeList: - return r.readList(k, schema) + return readListField(r, k, schema) case TypeMap: return r.readMap(k) case TypeSet: return r.readSet(k, schema) case typeObject: - return r.readObject(k, schema.Elem.(map[string]*Schema)) + return readObjectField(r, k, schema.Elem.(map[string]*Schema)) default: panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) } } -func (r *MapFieldReader) readObject( - k string, schema map[string]*Schema) (interface{}, bool, bool, error) { - result := make(map[string]interface{}) - for field, schema := range schema { - v, ok, _, err := r.ReadField([]string{k, field}, schema) - if err != nil { - return nil, false, false, err - } - if !ok { - continue - } - - result[field] = v - } - - return result, true, false, nil -} - -func (r *MapFieldReader) readList( - k string, schema *Schema) (interface{}, bool, bool, error) { - // Get the number of elements in the list - countRaw, countOk, countComputed, err := r.readPrimitive( - k+".#", &Schema{Type: TypeInt}) - if err != nil { - return nil, false, false, err - } - if !countOk { - // No count, means we have no list - countRaw = 0 - } - - // If we have an empty list, then return an empty list - if countComputed || countRaw.(int) == 0 { - return []interface{}{}, true, countComputed, nil - } - - // Get the schema for the elements - var elemSchema *Schema - switch t := schema.Elem.(type) { - case *Resource: - elemSchema = &Schema{ - Type: typeObject, - Elem: t.Schema, - } - case *Schema: - elemSchema = t - } - - // Go through each count, and get the item value out of it - result := make([]interface{}, countRaw.(int)) - for i, _ := range result { - is := strconv.FormatInt(int64(i), 10) - raw, ok, _, err := r.ReadField([]string{k, is}, elemSchema) - if err != nil { - return nil, false, false, err - } - if !ok { - // This should never happen, because by the time the data - // gets to the FieldReaders, all the defaults should be set by - // Schema. - raw = nil - } - - result[i] = raw - } - - return result, true, false, nil -} - func (r *MapFieldReader) readMap(k string) (interface{}, bool, bool, error) { result := make(map[string]interface{}) resultSet := false @@ -134,36 +64,9 @@ func (r *MapFieldReader) readPrimitive( return nil, false, false, nil } - var returnVal interface{} - switch schema.Type { - case TypeBool: - if result == "" { - returnVal = false - break - } - - v, err := strconv.ParseBool(result) - if err != nil { - return nil, false, false, err - } - - returnVal = v - case TypeInt: - if result == "" { - returnVal = 0 - break - } - - v, err := strconv.ParseInt(result, 0, 0) - if err != nil { - return nil, false, false, err - } - - returnVal = int(v) - case TypeString: - returnVal = result - default: - panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) + returnVal, err := stringToPrimitive(result, false, schema) + if err != nil { + return nil, false, false, err } return returnVal, true, false, nil From 73726e83b2dfcec0631e06b0232a944ea4e0bead Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Dec 2014 02:17:35 +0530 Subject: [PATCH 05/23] helper/schema: DiffFieldReader for reading data from a diff --- helper/schema/field_reader.go | 72 +++-- helper/schema/field_reader_config.go | 50 ++-- helper/schema/field_reader_config_test.go | 22 +- helper/schema/field_reader_diff.go | 152 +++++++++++ helper/schema/field_reader_diff_test.go | 313 ++++++++++++++++++++++ helper/schema/field_reader_map.go | 52 ++-- helper/schema/field_reader_map_test.go | 22 +- 7 files changed, 601 insertions(+), 82 deletions(-) create mode 100644 helper/schema/field_reader_diff.go create mode 100644 helper/schema/field_reader_diff_test.go diff --git a/helper/schema/field_reader.go b/helper/schema/field_reader.go index 187e79bee..6c6e0d5a4 100644 --- a/helper/schema/field_reader.go +++ b/helper/schema/field_reader.go @@ -9,7 +9,26 @@ import ( // the proper typed representation. ResourceData uses this to query data // out of multiple sources: config, state, diffs, etc. type FieldReader interface { - ReadField([]string, *Schema) (interface{}, bool, bool, error) + ReadField([]string, *Schema) (FieldReadResult, error) +} + +// FieldReadResult encapsulates all the resulting data from reading +// a field. +type FieldReadResult struct { + // Value is the actual read value. NegValue is the _negative_ value + // or the items that should be removed (if they existed). NegValue + // doesn't make sense for primitives but is important for any + // container types such as maps, sets, lists. + Value interface{} + NegValue interface{} + + // Exists is true if the field was found in the data. False means + // it wasn't found if there was no error. + Exists bool + + // Computed is true if the field was found but the value + // is computed. + Computed bool } // readListField is a generic method for reading a list field out of a @@ -17,21 +36,24 @@ type FieldReader interface { // "foo.#" for a list "foo" and that the indexes are "foo.0", "foo.1", etc. // after that point. func readListField( - r FieldReader, k string, schema *Schema) (interface{}, bool, bool, error) { + r FieldReader, k string, schema *Schema) (FieldReadResult, error) { // Get the number of elements in the list - countRaw, countOk, countComputed, err := r.ReadField( - []string{k + ".#"}, &Schema{Type: TypeInt}) + countResult, err := r.ReadField([]string{k + ".#"}, &Schema{Type: TypeInt}) if err != nil { - return nil, false, false, err + return FieldReadResult{}, err } - if !countOk { + if !countResult.Exists { // No count, means we have no list - countRaw = 0 + countResult.Value = 0 } // If we have an empty list, then return an empty list - if countComputed || countRaw.(int) == 0 { - return []interface{}{}, true, countComputed, nil + if countResult.Computed || countResult.Value.(int) == 0 { + return FieldReadResult{ + Value: []interface{}{}, + Exists: true, + Computed: countResult.Computed, + }, nil } // Get the schema for the elements @@ -47,24 +69,27 @@ func readListField( } // Go through each count, and get the item value out of it - result := make([]interface{}, countRaw.(int)) + result := make([]interface{}, countResult.Value.(int)) for i, _ := range result { is := strconv.FormatInt(int64(i), 10) - raw, ok, _, err := r.ReadField([]string{k, is}, elemSchema) + rawResult, err := r.ReadField([]string{k, is}, elemSchema) if err != nil { - return nil, false, false, err + return FieldReadResult{}, err } - if !ok { + if !rawResult.Exists { // This should never happen, because by the time the data // gets to the FieldReaders, all the defaults should be set by // Schema. - raw = nil + rawResult.Value = nil } - result[i] = raw + result[i] = rawResult.Value } - return result, true, false, nil + return FieldReadResult{ + Value: result, + Exists: true, + }, nil } // readObjectField is a generic method for reading objects out of FieldReaders @@ -73,21 +98,24 @@ func readListField( func readObjectField( r FieldReader, k string, - schema map[string]*Schema) (interface{}, bool, bool, error) { + schema map[string]*Schema) (FieldReadResult, error) { result := make(map[string]interface{}) for field, schema := range schema { - v, ok, _, err := r.ReadField([]string{k, field}, schema) + rawResult, err := r.ReadField([]string{k, field}, schema) if err != nil { - return nil, false, false, err + return FieldReadResult{}, err } - if !ok { + if !rawResult.Exists { continue } - result[field] = v + result[field] = rawResult.Value } - return result, true, false, nil + return FieldReadResult{ + Value: result, + Exists: true, + }, nil } func stringToPrimitive( diff --git a/helper/schema/field_reader_config.go b/helper/schema/field_reader_config.go index ff418287b..146aaef6e 100644 --- a/helper/schema/field_reader_config.go +++ b/helper/schema/field_reader_config.go @@ -15,7 +15,7 @@ type ConfigFieldReader struct { } func (r *ConfigFieldReader) ReadField( - address []string, schema *Schema) (interface{}, bool, bool, error) { + address []string, schema *Schema) (FieldReadResult, error) { k := strings.Join(address, ".") switch schema.Type { @@ -38,10 +38,10 @@ func (r *ConfigFieldReader) ReadField( } } -func (r *ConfigFieldReader) readMap(k string) (interface{}, bool, bool, error) { +func (r *ConfigFieldReader) readMap(k string) (FieldReadResult, error) { mraw, ok := r.Config.Get(k) if !ok { - return nil, false, false, nil + return FieldReadResult{}, nil } result := make(map[string]interface{}) @@ -64,52 +64,66 @@ func (r *ConfigFieldReader) readMap(k string) (interface{}, bool, bool, error) { panic(fmt.Sprintf("unknown type: %#v", mraw)) } - return result, true, false, nil + return FieldReadResult{ + Value: result, + Exists: true, + }, nil } func (r *ConfigFieldReader) readPrimitive( - k string, schema *Schema) (interface{}, bool, bool, error) { + k string, schema *Schema) (FieldReadResult, error) { raw, ok := r.Config.Get(k) if !ok { - return nil, false, false, nil + return FieldReadResult{}, nil } var result string if err := mapstructure.WeakDecode(raw, &result); err != nil { - return nil, false, false, err + return FieldReadResult{}, err } computed := r.Config.IsComputed(k) returnVal, err := stringToPrimitive(result, computed, schema) if err != nil { - return nil, false, false, err + return FieldReadResult{}, err } - return returnVal, true, computed, nil + return FieldReadResult{ + Value: returnVal, + Exists: true, + Computed: computed, + }, nil } func (r *ConfigFieldReader) readSet( - k string, schema *Schema) (interface{}, bool, bool, error) { - raw, ok, computed, err := readListField(r, k, schema) + k string, schema *Schema) (FieldReadResult, error) { + raw, err := readListField(r, k, schema) if err != nil { - return nil, false, false, err + return FieldReadResult{}, err } - if !ok { - return nil, false, false, nil + if !raw.Exists { + return FieldReadResult{}, nil } // Create the set that will be our result set := &Set{F: schema.Set} // If the list is computed, the set is necessarilly computed - if computed { - return set, true, computed, nil + if raw.Computed { + return FieldReadResult{ + Value: set, + Exists: true, + Computed: raw.Computed, + }, nil } // Build up the set from the list elements - for _, v := range raw.([]interface{}) { + for _, v := range raw.Value.([]interface{}) { set.Add(v) } - return set, true, false, nil + return FieldReadResult{ + Value: set, + Exists: true, + }, nil } diff --git a/helper/schema/field_reader_config_test.go b/helper/schema/field_reader_config_test.go index be266a57c..d27f74f36 100644 --- a/helper/schema/field_reader_config_test.go +++ b/helper/schema/field_reader_config_test.go @@ -184,24 +184,24 @@ func TestConfigFieldReader(t *testing.T) { } for name, tc := range cases { - out, outOk, outComputed, outErr := r.ReadField(tc.Addr, tc.Schema) - if (outErr != nil) != tc.OutErr { - t.Fatalf("%s: err: %s", name, outErr) + out, err := r.ReadField(tc.Addr, tc.Schema) + if (err != nil) != tc.OutErr { + t.Fatalf("%s: err: %s", name, err) } - if outComputed != tc.OutComputed { - t.Fatalf("%s: err: %#v", name, outComputed) + if out.Computed != tc.OutComputed { + t.Fatalf("%s: err: %#v", name, out.Computed) } - if s, ok := out.(*Set); ok { + if s, ok := out.Value.(*Set); ok { // If it is a set, convert to a list so its more easily checked. - out = s.List() + out.Value = s.List() } - if !reflect.DeepEqual(out, tc.Out) { - t.Fatalf("%s: out: %#v", name, out) + if !reflect.DeepEqual(out.Value, tc.Out) { + t.Fatalf("%s: out: %#v", name, out.Value) } - if outOk != tc.OutOk { - t.Fatalf("%s: outOk: %#v", name, outOk) + if out.Exists != tc.OutOk { + t.Fatalf("%s: outOk: %#v", name, out.Exists) } } } diff --git a/helper/schema/field_reader_diff.go b/helper/schema/field_reader_diff.go new file mode 100644 index 000000000..c35380120 --- /dev/null +++ b/helper/schema/field_reader_diff.go @@ -0,0 +1,152 @@ +package schema + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/mapstructure" +) + +// DiffFieldReader reads fields out of a diff structures. +type DiffFieldReader struct { + Diff *terraform.InstanceDiff +} + +func (r *DiffFieldReader) ReadField( + address []string, schema *Schema) (FieldReadResult, error) { + k := strings.Join(address, ".") + + switch schema.Type { + case TypeBool: + fallthrough + case TypeInt: + fallthrough + case TypeString: + return r.readPrimitive(k, schema) + case TypeList: + return readListField(r, k, schema) + case TypeMap: + return r.readMap(k) + case TypeSet: + return r.readSet(k, schema) + case typeObject: + return readObjectField(r, k, schema.Elem.(map[string]*Schema)) + default: + panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) + } +} + +func (r *DiffFieldReader) readMap(k string) (FieldReadResult, error) { + result := make(map[string]interface{}) + negresult := make(map[string]interface{}) + resultSet := false + + prefix := k + "." + for k, v := range r.Diff.Attributes { + if !strings.HasPrefix(k, prefix) { + continue + } + resultSet = true + + k = k[len(prefix):] + if v.NewRemoved { + negresult[k] = "" + continue + } + + result[k] = v.New + } + + var resultVal interface{} + if resultSet { + resultVal = result + } + + return FieldReadResult{ + Value: resultVal, + NegValue: negresult, + Exists: resultSet, + }, nil +} + +func (r *DiffFieldReader) readPrimitive( + k string, schema *Schema) (FieldReadResult, error) { + attrD, ok := r.Diff.Attributes[k] + if !ok { + return FieldReadResult{}, nil + } + if attrD.NewComputed { + return FieldReadResult{ + Exists: true, + Computed: true, + }, nil + } + + result := attrD.New + if attrD.NewExtra != nil { + if err := mapstructure.WeakDecode(attrD.NewExtra, &result); err != nil { + return FieldReadResult{}, err + } + } + + returnVal, err := stringToPrimitive(result, false, schema) + if err != nil { + return FieldReadResult{}, err + } + + return FieldReadResult{ + Value: returnVal, + Exists: true, + }, nil +} + +func (r *DiffFieldReader) readSet( + k string, schema *Schema) (FieldReadResult, error) { + // Create the set that will be our result + set := &Set{F: schema.Set} + + // Get the schema for the elements + var elemSchema *Schema + switch t := schema.Elem.(type) { + case *Resource: + elemSchema = &Schema{ + Type: typeObject, + Elem: t.Schema, + } + case *Schema: + elemSchema = t + } + + // Go through the map and find all the set items + prefix := k + "." + for k, _ := range r.Diff.Attributes { + if !strings.HasPrefix(k, prefix) { + continue + } + if strings.HasPrefix(k, prefix+"#") { + // Ignore the count field + continue + } + + // Split the key, since it might be a sub-object like "idx.field" + parts := strings.Split(k[len(prefix):], ".") + idx := parts[0] + + raw, err := r.ReadField([]string{prefix + idx}, elemSchema) + if err != nil { + return FieldReadResult{}, err + } + if !raw.Exists { + // This shouldn't happen because we just verified it does exist + panic("missing field in set: " + k + "." + idx) + } + + set.Add(raw.Value) + } + + return FieldReadResult{ + Value: set, + Exists: true, + }, nil +} diff --git a/helper/schema/field_reader_diff_test.go b/helper/schema/field_reader_diff_test.go new file mode 100644 index 000000000..8cb384d92 --- /dev/null +++ b/helper/schema/field_reader_diff_test.go @@ -0,0 +1,313 @@ +package schema + +import ( + "reflect" + "testing" + + "github.com/hashicorp/terraform/terraform" +) + +func TestDiffFieldReader_impl(t *testing.T) { + var _ FieldReader = new(DiffFieldReader) +} + +func TestDiffFieldReader(t *testing.T) { + r := &DiffFieldReader{ + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "bool": &terraform.ResourceAttrDiff{ + Old: "", + New: "true", + }, + + "int": &terraform.ResourceAttrDiff{ + Old: "", + New: "42", + }, + + "string": &terraform.ResourceAttrDiff{ + Old: "", + New: "string", + }, + + "list.#": &terraform.ResourceAttrDiff{ + Old: "0", + New: "2", + }, + + "list.0": &terraform.ResourceAttrDiff{ + Old: "", + New: "foo", + }, + + "list.1": &terraform.ResourceAttrDiff{ + Old: "", + New: "bar", + }, + + "listInt.#": &terraform.ResourceAttrDiff{ + Old: "0", + New: "2", + }, + + "listInt.0": &terraform.ResourceAttrDiff{ + Old: "", + New: "21", + }, + + "listInt.1": &terraform.ResourceAttrDiff{ + Old: "", + New: "42", + }, + + "map.foo": &terraform.ResourceAttrDiff{ + Old: "", + New: "bar", + }, + + "map.bar": &terraform.ResourceAttrDiff{ + Old: "", + New: "baz", + }, + + "mapRemove.foo": &terraform.ResourceAttrDiff{ + Old: "", + New: "bar", + }, + + "mapRemove.bar": &terraform.ResourceAttrDiff{ + NewRemoved: true, + }, + + "set.#": &terraform.ResourceAttrDiff{ + Old: "0", + New: "2", + }, + + "set.10": &terraform.ResourceAttrDiff{ + Old: "", + New: "10", + }, + + "set.50": &terraform.ResourceAttrDiff{ + Old: "", + New: "50", + }, + + "setDeep.#": &terraform.ResourceAttrDiff{ + Old: "0", + New: "2", + }, + + "setDeep.10.index": &terraform.ResourceAttrDiff{ + Old: "", + New: "10", + }, + + "setDeep.10.value": &terraform.ResourceAttrDiff{ + Old: "", + New: "foo", + }, + + "setDeep.50.index": &terraform.ResourceAttrDiff{ + Old: "", + New: "50", + }, + + "setDeep.50.value": &terraform.ResourceAttrDiff{ + Old: "", + New: "bar", + }, + }, + }, + } + + cases := map[string]struct { + Addr []string + Schema *Schema + Result FieldReadResult + Err bool + }{ + "noexist": { + []string{"boolNOPE"}, + &Schema{Type: TypeBool}, + FieldReadResult{ + Value: nil, + Exists: false, + Computed: false, + }, + false, + }, + + "bool": { + []string{"bool"}, + &Schema{Type: TypeBool}, + FieldReadResult{ + Value: true, + Exists: true, + Computed: false, + }, + false, + }, + + "int": { + []string{"int"}, + &Schema{Type: TypeInt}, + FieldReadResult{ + Value: 42, + Exists: true, + Computed: false, + }, + false, + }, + + "string": { + []string{"string"}, + &Schema{Type: TypeString}, + FieldReadResult{ + Value: "string", + Exists: true, + Computed: false, + }, + false, + }, + + "list": { + []string{"list"}, + &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeString}, + }, + FieldReadResult{ + Value: []interface{}{ + "foo", + "bar", + }, + Exists: true, + Computed: false, + }, + false, + }, + + "listInt": { + []string{"listInt"}, + &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + }, + FieldReadResult{ + Value: []interface{}{ + 21, + 42, + }, + Exists: true, + Computed: false, + }, + false, + }, + + "map": { + []string{"map"}, + &Schema{Type: TypeMap}, + FieldReadResult{ + Value: map[string]interface{}{ + "foo": "bar", + "bar": "baz", + }, + NegValue: map[string]interface{}{}, + Exists: true, + Computed: false, + }, + false, + }, + + "mapelem": { + []string{"map", "foo"}, + &Schema{Type: TypeString}, + FieldReadResult{ + Value: "bar", + Exists: true, + Computed: false, + }, + false, + }, + + "mapRemove": { + []string{"mapRemove"}, + &Schema{Type: TypeMap}, + FieldReadResult{ + Value: map[string]interface{}{ + "foo": "bar", + }, + NegValue: map[string]interface{}{ + "bar": "", + }, + Exists: true, + Computed: false, + }, + false, + }, + + "set": { + []string{"set"}, + &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + FieldReadResult{ + Value: []interface{}{10, 50}, + Exists: true, + Computed: false, + }, + false, + }, + + "setDeep": { + []string{"setDeep"}, + &Schema{ + Type: TypeSet, + Elem: &Resource{ + Schema: map[string]*Schema{ + "index": &Schema{Type: TypeInt}, + "value": &Schema{Type: TypeString}, + }, + }, + Set: func(a interface{}) int { + return a.(map[string]interface{})["index"].(int) + }, + }, + FieldReadResult{ + Value: []interface{}{ + map[string]interface{}{ + "index": 10, + "value": "foo", + }, + map[string]interface{}{ + "index": 50, + "value": "bar", + }, + }, + Exists: true, + Computed: false, + }, + false, + }, + } + + for name, tc := range cases { + out, err := r.ReadField(tc.Addr, tc.Schema) + 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 a list so its more easily checked. + out.Value = s.List() + } + if !reflect.DeepEqual(tc.Result, out) { + t.Fatalf("%s: bad: %#v", name, out) + } + } +} diff --git a/helper/schema/field_reader_map.go b/helper/schema/field_reader_map.go index 190219c6a..8eafdd13f 100644 --- a/helper/schema/field_reader_map.go +++ b/helper/schema/field_reader_map.go @@ -12,7 +12,7 @@ type MapFieldReader struct { } func (r *MapFieldReader) ReadField( - address []string, schema *Schema) (interface{}, bool, bool, error) { + address []string, schema *Schema) (FieldReadResult, error) { k := strings.Join(address, ".") switch schema.Type { @@ -35,7 +35,7 @@ func (r *MapFieldReader) ReadField( } } -func (r *MapFieldReader) readMap(k string) (interface{}, bool, bool, error) { +func (r *MapFieldReader) readMap(k string) (FieldReadResult, error) { result := make(map[string]interface{}) resultSet := false @@ -54,43 +54,52 @@ func (r *MapFieldReader) readMap(k string) (interface{}, bool, bool, error) { resultVal = result } - return resultVal, resultSet, false, nil + return FieldReadResult{ + Value: resultVal, + Exists: resultSet, + }, nil } func (r *MapFieldReader) readPrimitive( - k string, schema *Schema) (interface{}, bool, bool, error) { + k string, schema *Schema) (FieldReadResult, error) { result, ok := r.Map[k] if !ok { - return nil, false, false, nil + return FieldReadResult{}, nil } returnVal, err := stringToPrimitive(result, false, schema) if err != nil { - return nil, false, false, err + return FieldReadResult{}, err } - return returnVal, true, false, nil + return FieldReadResult{ + Value: returnVal, + Exists: true, + }, nil } func (r *MapFieldReader) readSet( - k string, schema *Schema) (interface{}, bool, bool, error) { + k string, schema *Schema) (FieldReadResult, error) { // Get the number of elements in the list - countRaw, countOk, countComputed, err := r.readPrimitive( - k+".#", &Schema{Type: TypeInt}) + countRaw, err := r.readPrimitive(k+".#", &Schema{Type: TypeInt}) if err != nil { - return nil, false, false, err + return FieldReadResult{}, err } - if !countOk { + if !countRaw.Exists { // No count, means we have no list - countRaw = 0 + countRaw.Value = 0 } // Create the set that will be our result set := &Set{F: schema.Set} // If we have an empty list, then return an empty list - if countComputed || countRaw.(int) == 0 { - return set, true, countComputed, nil + if countRaw.Computed || countRaw.Value.(int) == 0 { + return FieldReadResult{ + Value: set, + Exists: true, + Computed: countRaw.Computed, + }, nil } // Get the schema for the elements @@ -120,17 +129,20 @@ func (r *MapFieldReader) readSet( parts := strings.Split(k[len(prefix):], ".") idx := parts[0] - v, ok, _, err := r.ReadField([]string{prefix + idx}, elemSchema) + raw, err := r.ReadField([]string{prefix + idx}, elemSchema) if err != nil { - return nil, false, false, err + return FieldReadResult{}, err } - if !ok { + if !raw.Exists { // This shouldn't happen because we just verified it does exist panic("missing field in set: " + k + "." + idx) } - set.Add(v) + set.Add(raw.Value) } - return set, true, false, nil + return FieldReadResult{ + Value: set, + Exists: true, + }, nil } diff --git a/helper/schema/field_reader_map_test.go b/helper/schema/field_reader_map_test.go index fba89a158..4334c0a5a 100644 --- a/helper/schema/field_reader_map_test.go +++ b/helper/schema/field_reader_map_test.go @@ -180,24 +180,24 @@ func TestMapFieldReader(t *testing.T) { } for name, tc := range cases { - out, outOk, outComputed, outErr := r.ReadField(tc.Addr, tc.Schema) - if (outErr != nil) != tc.OutErr { - t.Fatalf("%s: err: %s", name, outErr) + out, err := r.ReadField(tc.Addr, tc.Schema) + if (err != nil) != tc.OutErr { + t.Fatalf("%s: err: %s", name, err) } - if outComputed != tc.OutComputed { - t.Fatalf("%s: err: %#v", name, outComputed) + if out.Computed != tc.OutComputed { + t.Fatalf("%s: err: %#v", name, out.Computed) } - if s, ok := out.(*Set); ok { + if s, ok := out.Value.(*Set); ok { // If it is a set, convert to a list so its more easily checked. - out = s.List() + out.Value = s.List() } - if !reflect.DeepEqual(out, tc.Out) { - t.Fatalf("%s: out: %#v", name, out) + if !reflect.DeepEqual(out.Value, tc.Out) { + t.Fatalf("%s: out: %#v", name, out.Value) } - if outOk != tc.OutOk { - t.Fatalf("%s: outOk: %#v", name, outOk) + if out.Exists != tc.OutOk { + t.Fatalf("%s: outOk: %#v", name, out.Exists) } } } From 91a57b42e8c4a7d0183566b8eb306334dcdda633 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 3 Jan 2015 12:02:49 +0530 Subject: [PATCH 06/23] helper/schema: reading a diff with computed should return zero value --- helper/schema/field_reader_diff.go | 22 ++++++++++------------ helper/schema/field_reader_diff_test.go | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/helper/schema/field_reader_diff.go b/helper/schema/field_reader_diff.go index c35380120..e1a7ba3d2 100644 --- a/helper/schema/field_reader_diff.go +++ b/helper/schema/field_reader_diff.go @@ -76,17 +76,14 @@ func (r *DiffFieldReader) readPrimitive( if !ok { return FieldReadResult{}, nil } - if attrD.NewComputed { - return FieldReadResult{ - Exists: true, - Computed: true, - }, nil - } - result := attrD.New - if attrD.NewExtra != nil { - if err := mapstructure.WeakDecode(attrD.NewExtra, &result); err != nil { - return FieldReadResult{}, err + var result string + if !attrD.NewComputed { + result = attrD.New + if attrD.NewExtra != nil { + if err := mapstructure.WeakDecode(attrD.NewExtra, &result); err != nil { + return FieldReadResult{}, err + } } } @@ -96,8 +93,9 @@ func (r *DiffFieldReader) readPrimitive( } return FieldReadResult{ - Value: returnVal, - Exists: true, + Value: returnVal, + Exists: true, + Computed: attrD.NewComputed, }, nil } diff --git a/helper/schema/field_reader_diff_test.go b/helper/schema/field_reader_diff_test.go index 8cb384d92..8c3cbc6a0 100644 --- a/helper/schema/field_reader_diff_test.go +++ b/helper/schema/field_reader_diff_test.go @@ -30,6 +30,12 @@ func TestDiffFieldReader(t *testing.T) { New: "string", }, + "stringComputed": &terraform.ResourceAttrDiff{ + Old: "foo", + New: "bar", + NewComputed: true, + }, + "list.#": &terraform.ResourceAttrDiff{ Old: "0", New: "2", @@ -172,6 +178,17 @@ func TestDiffFieldReader(t *testing.T) { false, }, + "stringComputed": { + []string{"stringComputed"}, + &Schema{Type: TypeString}, + FieldReadResult{ + Value: "", + Exists: true, + Computed: true, + }, + false, + }, + "list": { []string{"list"}, &Schema{ From 0b1da37b20d9041a998241b7191c72d82942e3df Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 3 Jan 2015 09:55:38 -0500 Subject: [PATCH 07/23] helper/schema: diff field reader should merge result with source --- helper/schema/field_reader_diff.go | 71 +++++++++++++------ helper/schema/field_reader_diff_test.go | 91 ++++++++++++++++++++++--- 2 files changed, 133 insertions(+), 29 deletions(-) diff --git a/helper/schema/field_reader_diff.go b/helper/schema/field_reader_diff.go index e1a7ba3d2..28b5ffc88 100644 --- a/helper/schema/field_reader_diff.go +++ b/helper/schema/field_reader_diff.go @@ -9,8 +9,25 @@ import ( ) // DiffFieldReader reads fields out of a diff structures. +// +// It also requires access to a Reader that reads fields from the structure +// that the diff was derived from. This is usually the state. This is required +// because a diff on its own doesn't have complete data about full objects +// such as maps. +// +// The Source MUST be the data that the diff was derived from. If it isn't, +// the behavior of this struct is undefined. +// +// Reading fields from a DiffFieldReader is identical to reading from +// Source except the diff will be applied to the end result. +// +// The "Exists" field on the result will be set to true if the complete +// field exists whether its from the source, diff, or a combination of both. +// It cannot be determined whether a retrieved value is composed of +// diff elements. type DiffFieldReader struct { - Diff *terraform.InstanceDiff + Diff *terraform.InstanceDiff + Source FieldReader } func (r *DiffFieldReader) ReadField( @@ -27,7 +44,7 @@ func (r *DiffFieldReader) ReadField( case TypeList: return readListField(r, k, schema) case TypeMap: - return r.readMap(k) + return r.readMap(k, schema) case TypeSet: return r.readSet(k, schema) case typeObject: @@ -37,11 +54,23 @@ func (r *DiffFieldReader) ReadField( } } -func (r *DiffFieldReader) readMap(k string) (FieldReadResult, error) { +func (r *DiffFieldReader) readMap( + k string, schema *Schema) (FieldReadResult, error) { result := make(map[string]interface{}) - negresult := make(map[string]interface{}) resultSet := false + // First read the map from the underlying source + source, err := r.Source.ReadField([]string{k}, schema) + if err != nil { + return FieldReadResult{}, err + } + if source.Exists { + result = source.Value.(map[string]interface{}) + resultSet = true + } + + // Next, read all the elements we have in our diff, and apply + // the diff to our result. prefix := k + "." for k, v := range r.Diff.Attributes { if !strings.HasPrefix(k, prefix) { @@ -51,7 +80,7 @@ func (r *DiffFieldReader) readMap(k string) (FieldReadResult, error) { k = k[len(prefix):] if v.NewRemoved { - negresult[k] = "" + delete(result, k) continue } @@ -64,39 +93,41 @@ func (r *DiffFieldReader) readMap(k string) (FieldReadResult, error) { } return FieldReadResult{ - Value: resultVal, - NegValue: negresult, - Exists: resultSet, + Value: resultVal, + Exists: resultSet, }, nil } func (r *DiffFieldReader) readPrimitive( k string, schema *Schema) (FieldReadResult, error) { - attrD, ok := r.Diff.Attributes[k] - if !ok { - return FieldReadResult{}, nil + result, err := r.Source.ReadField([]string{k}, schema) + if err != nil { + return FieldReadResult{}, err } - var result string + attrD, ok := r.Diff.Attributes[k] + if !ok { + return result, nil + } + + var resultVal string if !attrD.NewComputed { - result = attrD.New + resultVal = attrD.New if attrD.NewExtra != nil { - if err := mapstructure.WeakDecode(attrD.NewExtra, &result); err != nil { + if err := mapstructure.WeakDecode(attrD.NewExtra, &resultVal); err != nil { return FieldReadResult{}, err } } } - returnVal, err := stringToPrimitive(result, false, schema) + result.Exists = true + result.Computed = attrD.NewComputed + result.Value, err = stringToPrimitive(resultVal, false, schema) if err != nil { return FieldReadResult{}, err } - return FieldReadResult{ - Value: returnVal, - Exists: true, - Computed: attrD.NewComputed, - }, nil + return result, nil } func (r *DiffFieldReader) readSet( diff --git a/helper/schema/field_reader_diff_test.go b/helper/schema/field_reader_diff_test.go index 8c3cbc6a0..0d552a256 100644 --- a/helper/schema/field_reader_diff_test.go +++ b/helper/schema/field_reader_diff_test.go @@ -76,11 +76,6 @@ func TestDiffFieldReader(t *testing.T) { New: "baz", }, - "mapRemove.foo": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - }, - "mapRemove.bar": &terraform.ResourceAttrDiff{ NewRemoved: true, }, @@ -124,6 +119,31 @@ func TestDiffFieldReader(t *testing.T) { Old: "", New: "bar", }, + + "listMap.0.bar": &terraform.ResourceAttrDiff{ + NewRemoved: true, + }, + + "setChange.10.value": &terraform.ResourceAttrDiff{ + Old: "50", + New: "80", + }, + }, + }, + + Source: &MapFieldReader{ + Map: map[string]string{ + "listMap.#": "2", + "listMap.0.foo": "bar", + "listMap.0.bar": "baz", + "listMap.1.baz": "baz", + + "mapRemove.foo": "bar", + "mapRemove.bar": "bar", + + "setChange.#": "1", + "setChange.10.index": "10", + "setChange.10.value": "50", }, }, } @@ -231,7 +251,6 @@ func TestDiffFieldReader(t *testing.T) { "foo": "bar", "bar": "baz", }, - NegValue: map[string]interface{}{}, Exists: true, Computed: false, }, @@ -256,9 +275,6 @@ func TestDiffFieldReader(t *testing.T) { Value: map[string]interface{}{ "foo": "bar", }, - NegValue: map[string]interface{}{ - "bar": "", - }, Exists: true, Computed: false, }, @@ -312,6 +328,63 @@ func TestDiffFieldReader(t *testing.T) { }, false, }, + + "listMapRemoval": { + []string{"listMap"}, + &Schema{ + Type: TypeList, + Elem: &Schema{ + Type: TypeMap, + }, + }, + FieldReadResult{ + Value: []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + map[string]interface{}{ + "baz": "baz", + }, + }, + Exists: true, + }, + false, + }, + + "setChange": { + []string{"setChange"}, + &Schema{ + Type: TypeSet, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "index": &Schema{ + Type: TypeInt, + Required: true, + }, + + "value": &Schema{ + Type: TypeString, + Required: true, + }, + }, + }, + Set: func(a interface{}) int { + m := a.(map[string]interface{}) + return m["index"].(int) + }, + }, + FieldReadResult{ + Value: []interface{}{ + map[string]interface{}{ + "index": 10, + "value": "80", + }, + }, + Exists: true, + }, + false, + }, } for name, tc := range cases { From 3ff859d734cbb8a82e787cc74fe8ad6a74bc3635 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 3 Jan 2015 10:32:24 -0500 Subject: [PATCH 08/23] helper/schema: MultiLevelFieldReader --- helper/schema/field_reader_multi.go | 67 ++++++ helper/schema/field_reader_multi_test.go | 255 +++++++++++++++++++++++ 2 files changed, 322 insertions(+) create mode 100644 helper/schema/field_reader_multi.go create mode 100644 helper/schema/field_reader_multi_test.go diff --git a/helper/schema/field_reader_multi.go b/helper/schema/field_reader_multi.go new file mode 100644 index 000000000..b8f9726cf --- /dev/null +++ b/helper/schema/field_reader_multi.go @@ -0,0 +1,67 @@ +package schema + +import ( + "fmt" +) + +// MultiLevelFieldReader reads from other field readers, +// merging their results along the way in a specific order. You can specify +// "levels" and name them in order to read only an exact level or up to +// a specific level. +// +// This is useful for saying things such as "read the field from the state +// and config and merge them" or "read the latest value of the field". +type MultiLevelFieldReader struct { + Readers map[string]FieldReader + Levels []string +} + +func (r *MultiLevelFieldReader) ReadField( + address []string, schema *Schema) (FieldReadResult, error) { + return r.ReadFieldMerge(address, schema, r.Levels[len(r.Levels)-1]) +} + +func (r *MultiLevelFieldReader) ReadFieldExact( + address []string, schema *Schema, level string) (FieldReadResult, error) { + reader, ok := r.Readers[level] + if !ok { + return FieldReadResult{}, fmt.Errorf( + "Unknown reader level: %s", level) + } + + result, err := reader.ReadField(address, schema) + if err != nil { + return FieldReadResult{}, fmt.Errorf( + "Error reading level %s: %s", level, err) + } + + return result, nil +} + +func (r *MultiLevelFieldReader) ReadFieldMerge( + address []string, schema *Schema, level string) (FieldReadResult, error) { + var result FieldReadResult + for _, l := range r.Levels { + r, ok := r.Readers[l] + if !ok { + continue + } + + out, err := r.ReadField(address, schema) + if err != nil { + return FieldReadResult{}, fmt.Errorf( + "Error reading level %s: %s", l, err) + } + + // TODO: computed + if out.Exists { + result = out + } + + if l == level { + break + } + } + + return result, nil +} diff --git a/helper/schema/field_reader_multi_test.go b/helper/schema/field_reader_multi_test.go new file mode 100644 index 000000000..0236b2849 --- /dev/null +++ b/helper/schema/field_reader_multi_test.go @@ -0,0 +1,255 @@ +package schema + +import ( + "reflect" + "strconv" + "testing" + + "github.com/hashicorp/terraform/terraform" +) + +func TestMultiLevelFieldReaderReadFieldExact(t *testing.T) { + cases := map[string]struct { + Addr []string + Schema *Schema + Readers []FieldReader + Level string + Result FieldReadResult + }{ + "specific": { + Addr: []string{"foo"}, + + Schema: &Schema{ + Type: TypeString, + }, + + Readers: []FieldReader{ + &MapFieldReader{ + Map: map[string]string{ + "foo": "bar", + }, + }, + &MapFieldReader{ + Map: map[string]string{ + "foo": "baz", + }, + }, + &MapFieldReader{ + Map: map[string]string{}, + }, + }, + + Level: "1", + Result: FieldReadResult{ + Value: "baz", + Exists: true, + }, + }, + } + + for name, tc := range cases { + readers := make(map[string]FieldReader) + levels := make([]string, len(tc.Readers)) + for i, r := range tc.Readers { + is := strconv.FormatInt(int64(i), 10) + readers[is] = r + levels[i] = is + } + + r := &MultiLevelFieldReader{ + Readers: readers, + Levels: levels, + } + + out, err := r.ReadFieldExact(tc.Addr, tc.Schema, tc.Level) + if err != nil { + t.Fatalf("%s: err: %s", name, err) + } + + if !reflect.DeepEqual(tc.Result, out) { + t.Fatalf("%s: bad: %#v", name, out) + } + } +} + +func TestMultiLevelFieldReaderReadFieldMerge(t *testing.T) { + cases := map[string]struct { + Addr []string + Schema *Schema + Readers []FieldReader + Result FieldReadResult + }{ + "stringInDiff": { + Addr: []string{"availability_zone"}, + + Schema: &Schema{ + Type: TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + Readers: []FieldReader{ + &DiffFieldReader{ + Source: &MapFieldReader{ + Map: map[string]string{ + "availability_zone": "foo", + }, + }, + + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "availability_zone": &terraform.ResourceAttrDiff{ + Old: "foo", + New: "bar", + RequiresNew: true, + }, + }, + }, + }, + }, + + Result: FieldReadResult{ + Value: "bar", + Exists: true, + }, + }, + + "lastLevelComputed": { + Addr: []string{"availability_zone"}, + + Schema: &Schema{ + Type: TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + Readers: []FieldReader{ + &MapFieldReader{ + Map: map[string]string{ + "availability_zone": "foo", + }, + }, + + &DiffFieldReader{ + Source: &MapFieldReader{ + Map: map[string]string{ + "availability_zone": "foo", + }, + }, + + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "availability_zone": &terraform.ResourceAttrDiff{ + Old: "foo", + New: "bar", + NewComputed: true, + }, + }, + }, + }, + }, + + Result: FieldReadResult{ + Value: "", + Exists: true, + Computed: true, + }, + }, + + "list of maps with removal in diff": { + Addr: []string{"config_vars"}, + + Schema: &Schema{ + Type: TypeList, + Optional: true, + Computed: true, + Elem: &Schema{ + Type: TypeMap, + }, + }, + + Readers: []FieldReader{ + &DiffFieldReader{ + Source: &MapFieldReader{ + Map: map[string]string{ + "config_vars.#": "2", + "config_vars.0.foo": "bar", + "config_vars.0.bar": "bar", + "config_vars.1.bar": "baz", + }, + }, + + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "config_vars.0.bar": &terraform.ResourceAttrDiff{ + NewRemoved: true, + }, + }, + }, + }, + }, + + Result: FieldReadResult{ + Value: []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + map[string]interface{}{ + "bar": "baz", + }, + }, + Exists: true, + }, + }, + + "first level only": { + Addr: []string{"foo"}, + + Schema: &Schema{ + Type: TypeString, + }, + + Readers: []FieldReader{ + &MapFieldReader{ + Map: map[string]string{ + "foo": "bar", + }, + }, + &MapFieldReader{ + Map: map[string]string{}, + }, + }, + + Result: FieldReadResult{ + Value: "bar", + Exists: true, + }, + }, + } + + for name, tc := range cases { + readers := make(map[string]FieldReader) + levels := make([]string, len(tc.Readers)) + for i, r := range tc.Readers { + is := strconv.FormatInt(int64(i), 10) + readers[is] = r + levels[i] = is + } + + r := &MultiLevelFieldReader{ + Readers: readers, + Levels: levels, + } + + out, err := r.ReadFieldMerge(tc.Addr, tc.Schema, levels[len(levels)-1]) + if err != nil { + t.Fatalf("%s: err: %s", name, err) + } + + if !reflect.DeepEqual(tc.Result, out) { + t.Fatalf("%s: bad: %#v", name, out) + } + } +} From 7e379cb1a182e09f1548252e3ee3f22aea8cbe82 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 3 Jan 2015 12:13:46 -0500 Subject: [PATCH 09/23] helper/schema: field readers no longer take a schema as arg --- helper/schema/field_reader.go | 108 ++++++++++++++---- helper/schema/field_reader_config.go | 18 +-- helper/schema/field_reader_config_test.go | 70 ++++++------ helper/schema/field_reader_diff.go | 49 ++++---- helper/schema/field_reader_diff_test.go | 130 +++++++++++----------- helper/schema/field_reader_map.go | 28 ++--- helper/schema/field_reader_map_test.go | 70 ++++++------ helper/schema/field_reader_multi.go | 13 +-- helper/schema/field_reader_multi_test.go | 85 ++++++++------ helper/schema/field_reader_test.go | 48 ++++++++ 10 files changed, 370 insertions(+), 249 deletions(-) create mode 100644 helper/schema/field_reader_test.go diff --git a/helper/schema/field_reader.go b/helper/schema/field_reader.go index 6c6e0d5a4..ce5f38543 100644 --- a/helper/schema/field_reader.go +++ b/helper/schema/field_reader.go @@ -9,7 +9,7 @@ import ( // the proper typed representation. ResourceData uses this to query data // out of multiple sources: config, state, diffs, etc. type FieldReader interface { - ReadField([]string, *Schema) (FieldReadResult, error) + ReadField([]string) (FieldReadResult, error) } // FieldReadResult encapsulates all the resulting data from reading @@ -31,14 +31,92 @@ type FieldReadResult struct { Computed bool } +// addrToSchema finds the final element schema for the given address +// and the given schema. +func addrToSchema(addr []string, schemaMap map[string]*Schema) *Schema { + var result *Schema + var lastType ValueType + current := &Schema{ + Type: typeObject, + Elem: schemaMap, + } + + for len(addr) > 0 { + k := addr[0] + addr = addr[1:] + + REPEAT: + if len(addr) == 0 { + result = current + } + + currentType := current.Type + switch current.Type { + case TypeBool: + fallthrough + case TypeInt: + fallthrough + case TypeString: + if len(addr) > 0 { + return nil + } + case TypeList: + fallthrough + case TypeSet: + switch v := current.Elem.(type) { + case *Resource: + current = &Schema{ + Type: typeObject, + Elem: v.Schema, + } + case *Schema: + current = v + default: + return nil + } + + if len(addr) > 0 && addr[0] == "#" { + current = &Schema{Type: TypeInt} + } + case TypeMap: + if len(addr) > 0 { + current = &Schema{Type: TypeString} + } + case typeObject: + if lastType == TypeSet || lastType == TypeList { + // We just ignore sets/lists since they don't access + // objects the same way. + break + } + + m := current.Elem.(map[string]*Schema) + val, ok := m[k] + if !ok { + return nil + } + + current = val + goto REPEAT + } + + lastType = currentType + } + + return result +} + // readListField is a generic method for reading a list field out of a // a FieldReader. It does this based on the assumption that there is a key // "foo.#" for a list "foo" and that the indexes are "foo.0", "foo.1", etc. // after that point. func readListField( - r FieldReader, k string, schema *Schema) (FieldReadResult, error) { + r FieldReader, addr []string, schema *Schema) (FieldReadResult, error) { + addrPadded := make([]string, len(addr)+1) + copy(addrPadded, addr) + addrPadded[len(addrPadded)-1] = "#" + // Get the number of elements in the list - countResult, err := r.ReadField([]string{k + ".#"}, &Schema{Type: TypeInt}) + countResult, err := r.ReadField(addrPadded) if err != nil { return FieldReadResult{}, err } @@ -56,23 +134,12 @@ func readListField( }, nil } - // Get the schema for the elements - var elemSchema *Schema - switch t := schema.Elem.(type) { - case *Resource: - elemSchema = &Schema{ - Type: typeObject, - Elem: t.Schema, - } - case *Schema: - elemSchema = t - } - // Go through each count, and get the item value out of it result := make([]interface{}, countResult.Value.(int)) for i, _ := range result { is := strconv.FormatInt(int64(i), 10) - rawResult, err := r.ReadField([]string{k, is}, elemSchema) + addrPadded[len(addrPadded)-1] = is + rawResult, err := r.ReadField(addrPadded) if err != nil { return FieldReadResult{}, err } @@ -97,11 +164,14 @@ func readListField( // will result in the proper field data. func readObjectField( r FieldReader, - k string, + addr []string, schema map[string]*Schema) (FieldReadResult, error) { result := make(map[string]interface{}) - for field, schema := range schema { - rawResult, err := r.ReadField([]string{k, field}, schema) + for field, _ := range schema { + addrRead := make([]string, len(addr), len(addr)+1) + copy(addrRead, addr) + addrRead = append(addrRead, field) + rawResult, err := r.ReadField(addrRead) if err != nil { return FieldReadResult{}, err } diff --git a/helper/schema/field_reader_config.go b/helper/schema/field_reader_config.go index 146aaef6e..167d8bf71 100644 --- a/helper/schema/field_reader_config.go +++ b/helper/schema/field_reader_config.go @@ -12,11 +12,15 @@ import ( // the best of its ability. type ConfigFieldReader struct { Config *terraform.ResourceConfig + Schema map[string]*Schema } -func (r *ConfigFieldReader) ReadField( - address []string, schema *Schema) (FieldReadResult, error) { +func (r *ConfigFieldReader) ReadField(address []string) (FieldReadResult, error) { k := strings.Join(address, ".") + schema := addrToSchema(address, r.Schema) + if schema == nil { + return FieldReadResult{}, nil + } switch schema.Type { case TypeBool: @@ -26,13 +30,13 @@ func (r *ConfigFieldReader) ReadField( case TypeString: return r.readPrimitive(k, schema) case TypeList: - return readListField(r, k, schema) + return readListField(r, address, schema) case TypeMap: return r.readMap(k) case TypeSet: - return r.readSet(k, schema) + return r.readSet(address, schema) case typeObject: - return readObjectField(r, k, schema.Elem.(map[string]*Schema)) + return readObjectField(r, address, schema.Elem.(map[string]*Schema)) default: panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) } @@ -96,8 +100,8 @@ func (r *ConfigFieldReader) readPrimitive( } func (r *ConfigFieldReader) readSet( - k string, schema *Schema) (FieldReadResult, error) { - raw, err := readListField(r, k, schema) + address []string, schema *Schema) (FieldReadResult, error) { + raw, err := readListField(r, address, schema) if err != nil { return FieldReadResult{}, err } diff --git a/helper/schema/field_reader_config_test.go b/helper/schema/field_reader_config_test.go index d27f74f36..5eda033b2 100644 --- a/helper/schema/field_reader_config_test.go +++ b/helper/schema/field_reader_config_test.go @@ -14,6 +14,40 @@ func TestConfigFieldReader_impl(t *testing.T) { func TestConfigFieldReader(t *testing.T) { r := &ConfigFieldReader{ + Schema: map[string]*Schema{ + "bool": &Schema{Type: TypeBool}, + "int": &Schema{Type: TypeInt}, + "string": &Schema{Type: TypeString}, + "list": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeString}, + }, + "listInt": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + }, + "map": &Schema{Type: TypeMap}, + "set": &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + "setDeep": &Schema{ + Type: TypeSet, + Elem: &Resource{ + Schema: map[string]*Schema{ + "index": &Schema{Type: TypeInt}, + "value": &Schema{Type: TypeString}, + }, + }, + Set: func(a interface{}) int { + return a.(map[string]interface{})["index"].(int) + }, + }, + }, + Config: testConfig(t, map[string]interface{}{ "bool": true, "int": 42, @@ -45,7 +79,6 @@ func TestConfigFieldReader(t *testing.T) { cases := map[string]struct { Addr []string - Schema *Schema Out interface{} OutOk bool OutComputed bool @@ -53,7 +86,6 @@ func TestConfigFieldReader(t *testing.T) { }{ "noexist": { []string{"boolNOPE"}, - &Schema{Type: TypeBool}, nil, false, false, @@ -62,7 +94,6 @@ func TestConfigFieldReader(t *testing.T) { "bool": { []string{"bool"}, - &Schema{Type: TypeBool}, true, true, false, @@ -71,7 +102,6 @@ func TestConfigFieldReader(t *testing.T) { "int": { []string{"int"}, - &Schema{Type: TypeInt}, 42, true, false, @@ -80,7 +110,6 @@ func TestConfigFieldReader(t *testing.T) { "string": { []string{"string"}, - &Schema{Type: TypeString}, "string", true, false, @@ -89,10 +118,6 @@ func TestConfigFieldReader(t *testing.T) { "list": { []string{"list"}, - &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeString}, - }, []interface{}{ "foo", "bar", @@ -104,10 +129,6 @@ func TestConfigFieldReader(t *testing.T) { "listInt": { []string{"listInt"}, - &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeInt}, - }, []interface{}{ 21, 42, @@ -119,7 +140,6 @@ func TestConfigFieldReader(t *testing.T) { "map": { []string{"map"}, - &Schema{Type: TypeMap}, map[string]interface{}{ "foo": "bar", "bar": "baz", @@ -131,7 +151,6 @@ func TestConfigFieldReader(t *testing.T) { "mapelem": { []string{"map", "foo"}, - &Schema{Type: TypeString}, "bar", true, false, @@ -140,13 +159,6 @@ func TestConfigFieldReader(t *testing.T) { "set": { []string{"set"}, - &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, []interface{}{10, 50}, true, false, @@ -155,18 +167,6 @@ func TestConfigFieldReader(t *testing.T) { "setDeep": { []string{"setDeep"}, - &Schema{ - Type: TypeSet, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{Type: TypeInt}, - "value": &Schema{Type: TypeString}, - }, - }, - Set: func(a interface{}) int { - return a.(map[string]interface{})["index"].(int) - }, - }, []interface{}{ map[string]interface{}{ "index": 10, @@ -184,7 +184,7 @@ func TestConfigFieldReader(t *testing.T) { } for name, tc := range cases { - out, err := r.ReadField(tc.Addr, tc.Schema) + out, err := r.ReadField(tc.Addr) if (err != nil) != tc.OutErr { t.Fatalf("%s: err: %s", name, err) } diff --git a/helper/schema/field_reader_diff.go b/helper/schema/field_reader_diff.go index 28b5ffc88..337bcc5c9 100644 --- a/helper/schema/field_reader_diff.go +++ b/helper/schema/field_reader_diff.go @@ -28,11 +28,14 @@ import ( type DiffFieldReader struct { Diff *terraform.InstanceDiff Source FieldReader + Schema map[string]*Schema } -func (r *DiffFieldReader) ReadField( - address []string, schema *Schema) (FieldReadResult, error) { - k := strings.Join(address, ".") +func (r *DiffFieldReader) ReadField(address []string) (FieldReadResult, error) { + schema := addrToSchema(address, r.Schema) + if schema == nil { + return FieldReadResult{}, nil + } switch schema.Type { case TypeBool: @@ -40,27 +43,27 @@ func (r *DiffFieldReader) ReadField( case TypeInt: fallthrough case TypeString: - return r.readPrimitive(k, schema) + return r.readPrimitive(address, schema) case TypeList: - return readListField(r, k, schema) + return readListField(r, address, schema) case TypeMap: - return r.readMap(k, schema) + return r.readMap(address, schema) case TypeSet: - return r.readSet(k, schema) + return r.readSet(address, schema) case typeObject: - return readObjectField(r, k, schema.Elem.(map[string]*Schema)) + return readObjectField(r, address, schema.Elem.(map[string]*Schema)) default: panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) } } func (r *DiffFieldReader) readMap( - k string, schema *Schema) (FieldReadResult, error) { + address []string, schema *Schema) (FieldReadResult, error) { result := make(map[string]interface{}) resultSet := false // First read the map from the underlying source - source, err := r.Source.ReadField([]string{k}, schema) + source, err := r.Source.ReadField(address) if err != nil { return FieldReadResult{}, err } @@ -71,7 +74,7 @@ func (r *DiffFieldReader) readMap( // Next, read all the elements we have in our diff, and apply // the diff to our result. - prefix := k + "." + prefix := strings.Join(address, ".") + "." for k, v := range r.Diff.Attributes { if !strings.HasPrefix(k, prefix) { continue @@ -99,13 +102,13 @@ func (r *DiffFieldReader) readMap( } func (r *DiffFieldReader) readPrimitive( - k string, schema *Schema) (FieldReadResult, error) { - result, err := r.Source.ReadField([]string{k}, schema) + address []string, schema *Schema) (FieldReadResult, error) { + result, err := r.Source.ReadField(address) if err != nil { return FieldReadResult{}, err } - attrD, ok := r.Diff.Attributes[k] + attrD, ok := r.Diff.Attributes[strings.Join(address, ".")] if !ok { return result, nil } @@ -131,24 +134,12 @@ func (r *DiffFieldReader) readPrimitive( } func (r *DiffFieldReader) readSet( - k string, schema *Schema) (FieldReadResult, error) { + address []string, schema *Schema) (FieldReadResult, error) { // Create the set that will be our result set := &Set{F: schema.Set} - // Get the schema for the elements - var elemSchema *Schema - switch t := schema.Elem.(type) { - case *Resource: - elemSchema = &Schema{ - Type: typeObject, - Elem: t.Schema, - } - case *Schema: - elemSchema = t - } - // Go through the map and find all the set items - prefix := k + "." + prefix := strings.Join(address, ".") + "." for k, _ := range r.Diff.Attributes { if !strings.HasPrefix(k, prefix) { continue @@ -162,7 +153,7 @@ func (r *DiffFieldReader) readSet( parts := strings.Split(k[len(prefix):], ".") idx := parts[0] - raw, err := r.ReadField([]string{prefix + idx}, elemSchema) + raw, err := r.ReadField(append(address, idx)) if err != nil { return FieldReadResult{}, err } diff --git a/helper/schema/field_reader_diff_test.go b/helper/schema/field_reader_diff_test.go index 0d552a256..0e64e5b5c 100644 --- a/helper/schema/field_reader_diff_test.go +++ b/helper/schema/field_reader_diff_test.go @@ -12,7 +12,71 @@ func TestDiffFieldReader_impl(t *testing.T) { } func TestDiffFieldReader(t *testing.T) { + schema := map[string]*Schema{ + "bool": &Schema{Type: TypeBool}, + "int": &Schema{Type: TypeInt}, + "string": &Schema{Type: TypeString}, + "stringComputed": &Schema{Type: TypeString}, + "list": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeString}, + }, + "listInt": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + }, + "listMap": &Schema{ + Type: TypeList, + Elem: &Schema{ + Type: TypeMap, + }, + }, + "map": &Schema{Type: TypeMap}, + "mapRemove": &Schema{Type: TypeMap}, + "set": &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + "setChange": &Schema{ + Type: TypeSet, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "index": &Schema{ + Type: TypeInt, + Required: true, + }, + + "value": &Schema{ + Type: TypeString, + Required: true, + }, + }, + }, + Set: func(a interface{}) int { + m := a.(map[string]interface{}) + return m["index"].(int) + }, + }, + "setDeep": &Schema{ + Type: TypeSet, + Elem: &Resource{ + Schema: map[string]*Schema{ + "index": &Schema{Type: TypeInt}, + "value": &Schema{Type: TypeString}, + }, + }, + Set: func(a interface{}) int { + return a.(map[string]interface{})["index"].(int) + }, + }, + } + r := &DiffFieldReader{ + Schema: schema, Diff: &terraform.InstanceDiff{ Attributes: map[string]*terraform.ResourceAttrDiff{ "bool": &terraform.ResourceAttrDiff{ @@ -132,6 +196,7 @@ func TestDiffFieldReader(t *testing.T) { }, Source: &MapFieldReader{ + Schema: schema, Map: map[string]string{ "listMap.#": "2", "listMap.0.foo": "bar", @@ -150,13 +215,11 @@ func TestDiffFieldReader(t *testing.T) { cases := map[string]struct { Addr []string - Schema *Schema Result FieldReadResult Err bool }{ "noexist": { []string{"boolNOPE"}, - &Schema{Type: TypeBool}, FieldReadResult{ Value: nil, Exists: false, @@ -167,7 +230,6 @@ func TestDiffFieldReader(t *testing.T) { "bool": { []string{"bool"}, - &Schema{Type: TypeBool}, FieldReadResult{ Value: true, Exists: true, @@ -178,7 +240,6 @@ func TestDiffFieldReader(t *testing.T) { "int": { []string{"int"}, - &Schema{Type: TypeInt}, FieldReadResult{ Value: 42, Exists: true, @@ -189,7 +250,6 @@ func TestDiffFieldReader(t *testing.T) { "string": { []string{"string"}, - &Schema{Type: TypeString}, FieldReadResult{ Value: "string", Exists: true, @@ -200,7 +260,6 @@ func TestDiffFieldReader(t *testing.T) { "stringComputed": { []string{"stringComputed"}, - &Schema{Type: TypeString}, FieldReadResult{ Value: "", Exists: true, @@ -211,10 +270,6 @@ func TestDiffFieldReader(t *testing.T) { "list": { []string{"list"}, - &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeString}, - }, FieldReadResult{ Value: []interface{}{ "foo", @@ -228,10 +283,6 @@ func TestDiffFieldReader(t *testing.T) { "listInt": { []string{"listInt"}, - &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeInt}, - }, FieldReadResult{ Value: []interface{}{ 21, @@ -245,7 +296,6 @@ func TestDiffFieldReader(t *testing.T) { "map": { []string{"map"}, - &Schema{Type: TypeMap}, FieldReadResult{ Value: map[string]interface{}{ "foo": "bar", @@ -259,7 +309,6 @@ func TestDiffFieldReader(t *testing.T) { "mapelem": { []string{"map", "foo"}, - &Schema{Type: TypeString}, FieldReadResult{ Value: "bar", Exists: true, @@ -270,7 +319,6 @@ func TestDiffFieldReader(t *testing.T) { "mapRemove": { []string{"mapRemove"}, - &Schema{Type: TypeMap}, FieldReadResult{ Value: map[string]interface{}{ "foo": "bar", @@ -283,13 +331,6 @@ func TestDiffFieldReader(t *testing.T) { "set": { []string{"set"}, - &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, FieldReadResult{ Value: []interface{}{10, 50}, Exists: true, @@ -300,18 +341,6 @@ func TestDiffFieldReader(t *testing.T) { "setDeep": { []string{"setDeep"}, - &Schema{ - Type: TypeSet, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{Type: TypeInt}, - "value": &Schema{Type: TypeString}, - }, - }, - Set: func(a interface{}) int { - return a.(map[string]interface{})["index"].(int) - }, - }, FieldReadResult{ Value: []interface{}{ map[string]interface{}{ @@ -331,12 +360,6 @@ func TestDiffFieldReader(t *testing.T) { "listMapRemoval": { []string{"listMap"}, - &Schema{ - Type: TypeList, - Elem: &Schema{ - Type: TypeMap, - }, - }, FieldReadResult{ Value: []interface{}{ map[string]interface{}{ @@ -353,27 +376,6 @@ func TestDiffFieldReader(t *testing.T) { "setChange": { []string{"setChange"}, - &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{ - Type: TypeInt, - Required: true, - }, - - "value": &Schema{ - Type: TypeString, - Required: true, - }, - }, - }, - Set: func(a interface{}) int { - m := a.(map[string]interface{}) - return m["index"].(int) - }, - }, FieldReadResult{ Value: []interface{}{ map[string]interface{}{ @@ -388,7 +390,7 @@ func TestDiffFieldReader(t *testing.T) { } for name, tc := range cases { - out, err := r.ReadField(tc.Addr, tc.Schema) + out, err := r.ReadField(tc.Addr) if (err != nil) != tc.Err { t.Fatalf("%s: err: %s", name, err) } diff --git a/helper/schema/field_reader_map.go b/helper/schema/field_reader_map.go index 8eafdd13f..f06a7170a 100644 --- a/helper/schema/field_reader_map.go +++ b/helper/schema/field_reader_map.go @@ -8,12 +8,16 @@ import ( // MapFieldReader reads fields out of an untyped map[string]string to // the best of its ability. type MapFieldReader struct { - Map map[string]string + Map map[string]string + Schema map[string]*Schema } -func (r *MapFieldReader) ReadField( - address []string, schema *Schema) (FieldReadResult, error) { +func (r *MapFieldReader) ReadField(address []string) (FieldReadResult, error) { k := strings.Join(address, ".") + schema := addrToSchema(address, r.Schema) + if schema == nil { + return FieldReadResult{}, nil + } switch schema.Type { case TypeBool: @@ -23,13 +27,13 @@ func (r *MapFieldReader) ReadField( case TypeString: return r.readPrimitive(k, schema) case TypeList: - return readListField(r, k, schema) + return readListField(r, address, schema) case TypeMap: return r.readMap(k) case TypeSet: return r.readSet(k, schema) case typeObject: - return readObjectField(r, k, schema.Elem.(map[string]*Schema)) + return readObjectField(r, address, schema.Elem.(map[string]*Schema)) default: panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) } @@ -102,18 +106,6 @@ func (r *MapFieldReader) readSet( }, nil } - // Get the schema for the elements - var elemSchema *Schema - switch t := schema.Elem.(type) { - case *Resource: - elemSchema = &Schema{ - Type: typeObject, - Elem: t.Schema, - } - case *Schema: - elemSchema = t - } - // Go through the map and find all the set items prefix := k + "." for k, _ := range r.Map { @@ -129,7 +121,7 @@ func (r *MapFieldReader) readSet( parts := strings.Split(k[len(prefix):], ".") idx := parts[0] - raw, err := r.ReadField([]string{prefix + idx}, elemSchema) + raw, err := r.ReadField([]string{prefix[:len(prefix)-1], idx}) if err != nil { return FieldReadResult{}, err } diff --git a/helper/schema/field_reader_map_test.go b/helper/schema/field_reader_map_test.go index 4334c0a5a..9734d7f7a 100644 --- a/helper/schema/field_reader_map_test.go +++ b/helper/schema/field_reader_map_test.go @@ -11,6 +11,40 @@ func TestMapFieldReader_impl(t *testing.T) { func TestMapFieldReader(t *testing.T) { r := &MapFieldReader{ + Schema: map[string]*Schema{ + "bool": &Schema{Type: TypeBool}, + "int": &Schema{Type: TypeInt}, + "string": &Schema{Type: TypeString}, + "list": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeString}, + }, + "listInt": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + }, + "map": &Schema{Type: TypeMap}, + "set": &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + "setDeep": &Schema{ + Type: TypeSet, + Elem: &Resource{ + Schema: map[string]*Schema{ + "index": &Schema{Type: TypeInt}, + "value": &Schema{Type: TypeString}, + }, + }, + Set: func(a interface{}) int { + return a.(map[string]interface{})["index"].(int) + }, + }, + }, + Map: map[string]string{ "bool": "true", "int": "42", @@ -41,7 +75,6 @@ func TestMapFieldReader(t *testing.T) { cases := map[string]struct { Addr []string - Schema *Schema Out interface{} OutOk bool OutComputed bool @@ -49,7 +82,6 @@ func TestMapFieldReader(t *testing.T) { }{ "noexist": { []string{"boolNOPE"}, - &Schema{Type: TypeBool}, nil, false, false, @@ -58,7 +90,6 @@ func TestMapFieldReader(t *testing.T) { "bool": { []string{"bool"}, - &Schema{Type: TypeBool}, true, true, false, @@ -67,7 +98,6 @@ func TestMapFieldReader(t *testing.T) { "int": { []string{"int"}, - &Schema{Type: TypeInt}, 42, true, false, @@ -76,7 +106,6 @@ func TestMapFieldReader(t *testing.T) { "string": { []string{"string"}, - &Schema{Type: TypeString}, "string", true, false, @@ -85,10 +114,6 @@ func TestMapFieldReader(t *testing.T) { "list": { []string{"list"}, - &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeString}, - }, []interface{}{ "foo", "bar", @@ -100,10 +125,6 @@ func TestMapFieldReader(t *testing.T) { "listInt": { []string{"listInt"}, - &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeInt}, - }, []interface{}{ 21, 42, @@ -115,7 +136,6 @@ func TestMapFieldReader(t *testing.T) { "map": { []string{"map"}, - &Schema{Type: TypeMap}, map[string]interface{}{ "foo": "bar", "bar": "baz", @@ -127,7 +147,6 @@ func TestMapFieldReader(t *testing.T) { "mapelem": { []string{"map", "foo"}, - &Schema{Type: TypeString}, "bar", true, false, @@ -136,13 +155,6 @@ func TestMapFieldReader(t *testing.T) { "set": { []string{"set"}, - &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, []interface{}{10, 50}, true, false, @@ -151,18 +163,6 @@ func TestMapFieldReader(t *testing.T) { "setDeep": { []string{"setDeep"}, - &Schema{ - Type: TypeSet, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{Type: TypeInt}, - "value": &Schema{Type: TypeString}, - }, - }, - Set: func(a interface{}) int { - return a.(map[string]interface{})["index"].(int) - }, - }, []interface{}{ map[string]interface{}{ "index": 10, @@ -180,7 +180,7 @@ func TestMapFieldReader(t *testing.T) { } for name, tc := range cases { - out, err := r.ReadField(tc.Addr, tc.Schema) + out, err := r.ReadField(tc.Addr) if (err != nil) != tc.OutErr { t.Fatalf("%s: err: %s", name, err) } diff --git a/helper/schema/field_reader_multi.go b/helper/schema/field_reader_multi.go index b8f9726cf..3e2134708 100644 --- a/helper/schema/field_reader_multi.go +++ b/helper/schema/field_reader_multi.go @@ -16,20 +16,19 @@ type MultiLevelFieldReader struct { Levels []string } -func (r *MultiLevelFieldReader) ReadField( - address []string, schema *Schema) (FieldReadResult, error) { - return r.ReadFieldMerge(address, schema, r.Levels[len(r.Levels)-1]) +func (r *MultiLevelFieldReader) ReadField(address []string) (FieldReadResult, error) { + return r.ReadFieldMerge(address, r.Levels[len(r.Levels)-1]) } func (r *MultiLevelFieldReader) ReadFieldExact( - address []string, schema *Schema, level string) (FieldReadResult, error) { + address []string, level string) (FieldReadResult, error) { reader, ok := r.Readers[level] if !ok { return FieldReadResult{}, fmt.Errorf( "Unknown reader level: %s", level) } - result, err := reader.ReadField(address, schema) + result, err := reader.ReadField(address) if err != nil { return FieldReadResult{}, fmt.Errorf( "Error reading level %s: %s", level, err) @@ -39,7 +38,7 @@ func (r *MultiLevelFieldReader) ReadFieldExact( } func (r *MultiLevelFieldReader) ReadFieldMerge( - address []string, schema *Schema, level string) (FieldReadResult, error) { + address []string, level string) (FieldReadResult, error) { var result FieldReadResult for _, l := range r.Levels { r, ok := r.Readers[l] @@ -47,7 +46,7 @@ func (r *MultiLevelFieldReader) ReadFieldMerge( continue } - out, err := r.ReadField(address, schema) + out, err := r.ReadField(address) if err != nil { return FieldReadResult{}, fmt.Errorf( "Error reading level %s: %s", l, err) diff --git a/helper/schema/field_reader_multi_test.go b/helper/schema/field_reader_multi_test.go index 0236b2849..915fb1447 100644 --- a/helper/schema/field_reader_multi_test.go +++ b/helper/schema/field_reader_multi_test.go @@ -11,7 +11,6 @@ import ( func TestMultiLevelFieldReaderReadFieldExact(t *testing.T) { cases := map[string]struct { Addr []string - Schema *Schema Readers []FieldReader Level string Result FieldReadResult @@ -19,22 +18,27 @@ func TestMultiLevelFieldReaderReadFieldExact(t *testing.T) { "specific": { Addr: []string{"foo"}, - Schema: &Schema{ - Type: TypeString, - }, - Readers: []FieldReader{ &MapFieldReader{ + Schema: map[string]*Schema{ + "foo": &Schema{Type: TypeString}, + }, Map: map[string]string{ "foo": "bar", }, }, &MapFieldReader{ + Schema: map[string]*Schema{ + "foo": &Schema{Type: TypeString}, + }, Map: map[string]string{ "foo": "baz", }, }, &MapFieldReader{ + Schema: map[string]*Schema{ + "foo": &Schema{Type: TypeString}, + }, Map: map[string]string{}, }, }, @@ -61,7 +65,7 @@ func TestMultiLevelFieldReaderReadFieldExact(t *testing.T) { Levels: levels, } - out, err := r.ReadFieldExact(tc.Addr, tc.Schema, tc.Level) + out, err := r.ReadFieldExact(tc.Addr, tc.Level) if err != nil { t.Fatalf("%s: err: %s", name, err) } @@ -75,23 +79,22 @@ func TestMultiLevelFieldReaderReadFieldExact(t *testing.T) { func TestMultiLevelFieldReaderReadFieldMerge(t *testing.T) { cases := map[string]struct { Addr []string - Schema *Schema Readers []FieldReader Result FieldReadResult }{ "stringInDiff": { Addr: []string{"availability_zone"}, - Schema: &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - Readers: []FieldReader{ &DiffFieldReader{ + Schema: map[string]*Schema{ + "availability_zone": &Schema{Type: TypeString}, + }, + Source: &MapFieldReader{ + Schema: map[string]*Schema{ + "availability_zone": &Schema{Type: TypeString}, + }, Map: map[string]string{ "availability_zone": "foo", }, @@ -118,22 +121,27 @@ func TestMultiLevelFieldReaderReadFieldMerge(t *testing.T) { "lastLevelComputed": { Addr: []string{"availability_zone"}, - Schema: &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - Readers: []FieldReader{ &MapFieldReader{ + Schema: map[string]*Schema{ + "availability_zone": &Schema{Type: TypeString}, + }, + Map: map[string]string{ "availability_zone": "foo", }, }, &DiffFieldReader{ + Schema: map[string]*Schema{ + "availability_zone": &Schema{Type: TypeString}, + }, + Source: &MapFieldReader{ + Schema: map[string]*Schema{ + "availability_zone": &Schema{Type: TypeString}, + }, + Map: map[string]string{ "availability_zone": "foo", }, @@ -161,18 +169,23 @@ func TestMultiLevelFieldReaderReadFieldMerge(t *testing.T) { "list of maps with removal in diff": { Addr: []string{"config_vars"}, - Schema: &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - Elem: &Schema{ - Type: TypeMap, - }, - }, - Readers: []FieldReader{ &DiffFieldReader{ + Schema: map[string]*Schema{ + "config_vars": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeMap}, + }, + }, + Source: &MapFieldReader{ + Schema: map[string]*Schema{ + "config_vars": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeMap}, + }, + }, + Map: map[string]string{ "config_vars.#": "2", "config_vars.0.foo": "bar", @@ -207,17 +220,19 @@ func TestMultiLevelFieldReaderReadFieldMerge(t *testing.T) { "first level only": { Addr: []string{"foo"}, - Schema: &Schema{ - Type: TypeString, - }, - Readers: []FieldReader{ &MapFieldReader{ + Schema: map[string]*Schema{ + "foo": &Schema{Type: TypeString}, + }, Map: map[string]string{ "foo": "bar", }, }, &MapFieldReader{ + Schema: map[string]*Schema{ + "foo": &Schema{Type: TypeString}, + }, Map: map[string]string{}, }, }, @@ -243,7 +258,7 @@ func TestMultiLevelFieldReaderReadFieldMerge(t *testing.T) { Levels: levels, } - out, err := r.ReadFieldMerge(tc.Addr, tc.Schema, levels[len(levels)-1]) + out, err := r.ReadFieldMerge(tc.Addr, levels[len(levels)-1]) if err != nil { t.Fatalf("%s: err: %s", name, err) } diff --git a/helper/schema/field_reader_test.go b/helper/schema/field_reader_test.go new file mode 100644 index 000000000..7498b05f6 --- /dev/null +++ b/helper/schema/field_reader_test.go @@ -0,0 +1,48 @@ +package schema + +import ( + "reflect" + "testing" +) + +func TestAddrToSchema(t *testing.T) { + cases := map[string]struct { + Addr []string + Schema map[string]*Schema + Result *Schema + }{ + "mapElem": { + []string{"map", "foo"}, + map[string]*Schema{ + "map": &Schema{Type: TypeMap}, + }, + &Schema{Type: TypeString}, + }, + + "setDeep": { + []string{"set", "50", "index"}, + map[string]*Schema{ + "set": &Schema{ + Type: TypeSet, + Elem: &Resource{ + Schema: map[string]*Schema{ + "index": &Schema{Type: TypeInt}, + "value": &Schema{Type: TypeString}, + }, + }, + Set: func(a interface{}) int { + return a.(map[string]interface{})["index"].(int) + }, + }, + }, + &Schema{Type: TypeInt}, + }, + } + + for name, tc := range cases { + result := addrToSchema(tc.Addr, tc.Schema) + if !reflect.DeepEqual(result, tc.Result) { + t.Fatalf("%s: %#v", name, result) + } + } +} From 864a8f24ecc59744cfc0a4ea5e7813148d8146f6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Jan 2015 11:33:15 -0800 Subject: [PATCH 10/23] helper/schema: GoString for Set --- helper/schema/set.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helper/schema/set.go b/helper/schema/set.go index 92966ea59..689be4f9f 100644 --- a/helper/schema/set.go +++ b/helper/schema/set.go @@ -1,6 +1,7 @@ package schema import ( + "fmt" "sort" "sync" ) @@ -100,6 +101,10 @@ func (s *Set) Union(other *Set) *Set { return result } +func (s *Set) GoString() string { + return fmt.Sprintf("*Set(%#v)", s.m) +} + func (s *Set) init() { s.m = make(map[int]interface{}) } From b4bf8131516c80a0b3b980924afde5d510458935 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Jan 2015 18:02:19 -0800 Subject: [PATCH 11/23] helper/schema: too big to fail --- helper/schema/field_reader.go | 51 +++-- helper/schema/field_reader_config.go | 131 +++++++++++-- helper/schema/field_reader_config_test.go | 3 + helper/schema/field_reader_diff.go | 10 +- helper/schema/field_reader_diff_test.go | 20 +- helper/schema/field_reader_map.go | 124 +++++++++--- helper/schema/field_reader_map_test.go | 19 +- helper/schema/field_reader_multi.go | 23 +-- helper/schema/field_reader_multi_test.go | 32 ++-- helper/schema/field_reader_test.go | 135 ++++++++++++- helper/schema/resource_data.go | 150 +++++++++++---- helper/schema/resource_data_test.go | 223 ++-------------------- helper/schema/schema.go | 32 +++- helper/schema/schema_test.go | 25 ++- 14 files changed, 639 insertions(+), 339 deletions(-) diff --git a/helper/schema/field_reader.go b/helper/schema/field_reader.go index ce5f38543..911ff219f 100644 --- a/helper/schema/field_reader.go +++ b/helper/schema/field_reader.go @@ -19,8 +19,8 @@ type FieldReadResult struct { // or the items that should be removed (if they existed). NegValue // doesn't make sense for primitives but is important for any // container types such as maps, sets, lists. - Value interface{} - NegValue interface{} + Value interface{} + ValueProcessed interface{} // Exists is true if the field was found in the data. False means // it wasn't found if there was no error. @@ -32,26 +32,28 @@ type FieldReadResult struct { } // addrToSchema finds the final element schema for the given address -// and the given schema. -func addrToSchema(addr []string, schemaMap map[string]*Schema) *Schema { - var result *Schema - var lastType ValueType +// and the given schema. It returns all the schemas that led to the final +// schema. These are in order of the address (out to in). +func addrToSchema(addr []string, schemaMap map[string]*Schema) []*Schema { current := &Schema{ Type: typeObject, Elem: schemaMap, } + result := make([]*Schema, 0, len(addr)) for len(addr) > 0 { k := addr[0] addr = addr[1:] REPEAT: - if len(addr) == 0 { - result = current + // We want to trim off the first "typeObject" since its not a + // real lookup that people do. i.e. []string{"foo"} in a structure + // isn't {typeObject, typeString}, its just a {typeString}. + if len(result) > 0 || current.Type != typeObject { + result = append(result, current) } - currentType := current.Type - switch current.Type { + switch t := current.Type; t { case TypeBool: fallthrough case TypeInt: @@ -75,18 +77,33 @@ func addrToSchema(addr []string, schemaMap map[string]*Schema) *Schema { return nil } + // If we only have one more thing and the the next thing + // is a #, then we're accessing the index which is always + // an int. if len(addr) > 0 && addr[0] == "#" { current = &Schema{Type: TypeInt} + break } case TypeMap: if len(addr) > 0 { current = &Schema{Type: TypeString} } case typeObject: - if lastType == TypeSet || lastType == TypeList { - // We just ignore sets/lists since they don't access - // objects the same way. - break + // If we're already in the object, then we want to handle Sets + // and Lists specially. Basically, their next key is the lookup + // key (the set value or the list element). For these scenarios, + // we just want to skip it and move to the next element if there + // is one. + if len(result) > 0 { + lastType := result[len(result)-2].Type + if lastType == TypeSet || lastType == TypeList { + if len(addr) == 0 { + break + } + + k = addr[0] + addr = addr[1:] + } } m := current.Elem.(map[string]*Schema) @@ -98,8 +115,6 @@ func addrToSchema(addr []string, schemaMap map[string]*Schema) *Schema { current = val goto REPEAT } - - lastType = currentType } return result @@ -129,7 +144,7 @@ func readListField( if countResult.Computed || countResult.Value.(int) == 0 { return FieldReadResult{ Value: []interface{}{}, - Exists: true, + Exists: countResult.Exists, Computed: countResult.Computed, }, nil } @@ -184,7 +199,7 @@ func readObjectField( return FieldReadResult{ Value: result, - Exists: true, + Exists: len(schema) > 0 && len(result) > 0, }, nil } diff --git a/helper/schema/field_reader_config.go b/helper/schema/field_reader_config.go index 167d8bf71..215c0957e 100644 --- a/helper/schema/field_reader_config.go +++ b/helper/schema/field_reader_config.go @@ -2,7 +2,9 @@ package schema import ( "fmt" + "strconv" "strings" + "sync" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/mapstructure" @@ -13,15 +15,68 @@ import ( type ConfigFieldReader struct { Config *terraform.ResourceConfig Schema map[string]*Schema + + lock sync.Mutex } func (r *ConfigFieldReader) ReadField(address []string) (FieldReadResult, error) { - k := strings.Join(address, ".") - schema := addrToSchema(address, r.Schema) - if schema == nil { + return r.readField(address, false) +} + +func (r *ConfigFieldReader) readField( + address []string, nested bool) (FieldReadResult, error) { + schemaList := addrToSchema(address, r.Schema) + if len(schemaList) == 0 { return FieldReadResult{}, nil } + if !nested { + // If we have a set anywhere in the address, then we need to + // read that set out in order and actually replace that part of + // the address with the real list index. i.e. set.50 might actually + // map to set.12 in the config, since it is in list order in the + // config, not indexed by set value. + for i, v := range schemaList { + // Sets are the only thing that cause this issue. + if v.Type != TypeSet { + continue + } + + // If we're at the end of the list, then we don't have to worry + // about this because we're just requesting the whole set. + if i == len(schemaList)-1 { + continue + } + + // If we're looking for the count, then ignore... + if address[i+1] == "#" { + continue + } + + // Get the code + code, err := strconv.ParseInt(address[i+1], 0, 0) + if err != nil { + return FieldReadResult{}, err + } + + // Get the set so we can get the index map that tells us the + // mapping of the hash code to the list index + _, indexMap, err := r.readSet(address[:i+1], v) + if err != nil { + return FieldReadResult{}, err + } + + index, ok := indexMap[int(code)] + if !ok { + return FieldReadResult{}, nil + } + + address[i+1] = strconv.FormatInt(int64(index), 10) + } + } + + k := strings.Join(address, ".") + schema := schemaList[len(schemaList)-1] switch schema.Type { case TypeBool: fallthrough @@ -30,13 +85,16 @@ func (r *ConfigFieldReader) ReadField(address []string) (FieldReadResult, error) case TypeString: return r.readPrimitive(k, schema) case TypeList: - return readListField(r, address, schema) + return readListField(&nestedConfigFieldReader{r}, address, schema) case TypeMap: return r.readMap(k) case TypeSet: - return r.readSet(address, schema) + result, _, err := r.readSet(address, schema) + return result, err case typeObject: - return readObjectField(r, address, schema.Elem.(map[string]*Schema)) + return readObjectField( + &nestedConfigFieldReader{r}, + address, schema.Elem.(map[string]*Schema)) default: panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) } @@ -100,13 +158,14 @@ func (r *ConfigFieldReader) readPrimitive( } func (r *ConfigFieldReader) readSet( - address []string, schema *Schema) (FieldReadResult, error) { - raw, err := readListField(r, address, schema) + address []string, schema *Schema) (FieldReadResult, map[int]int, error) { + indexMap := make(map[int]int) + raw, err := readListField(&nestedConfigFieldReader{r}, address, schema) if err != nil { - return FieldReadResult{}, err + return FieldReadResult{}, indexMap, err } if !raw.Exists { - return FieldReadResult{}, nil + return FieldReadResult{}, indexMap, nil } // Create the set that will be our result @@ -118,16 +177,60 @@ func (r *ConfigFieldReader) readSet( Value: set, Exists: true, Computed: raw.Computed, - }, nil + }, indexMap, nil } // Build up the set from the list elements - for _, v := range raw.Value.([]interface{}) { - set.Add(v) + for i, v := range raw.Value.([]interface{}) { + // Check if any of the keys in this item are computed + computed := r.hasComputedSubKeys( + fmt.Sprintf("%s.%d", strings.Join(address, "."), i), schema) + + code := set.add(v) + indexMap[code] = i + if computed { + set.m[-code] = set.m[code] + delete(set.m, code) + code = -code + } } return FieldReadResult{ Value: set, Exists: true, - }, nil + }, indexMap, nil +} + +// hasComputedSubKeys walks through a schema and returns whether or not the +// given key contains any subkeys that are computed. +func (r *ConfigFieldReader) hasComputedSubKeys(key string, schema *Schema) bool { + prefix := key + "." + + switch t := schema.Elem.(type) { + case *Resource: + for k, schema := range t.Schema { + if r.Config.IsComputed(prefix + k) { + return true + } + + if r.hasComputedSubKeys(prefix+k, schema) { + return true + } + } + } + + return false +} + +// nestedConfigFieldReader is a funny little thing that just wraps a +// ConfigFieldReader to call readField when ReadField is called so that +// we don't recalculate the set rewrites in the address, which leads to +// an infinite loop. +type nestedConfigFieldReader struct { + Reader *ConfigFieldReader +} + +func (r *nestedConfigFieldReader) ReadField( + address []string) (FieldReadResult, error) { + return r.Reader.readField(address, true) } diff --git a/helper/schema/field_reader_config_test.go b/helper/schema/field_reader_config_test.go index 5eda033b2..f194b3fa0 100644 --- a/helper/schema/field_reader_config_test.go +++ b/helper/schema/field_reader_config_test.go @@ -184,6 +184,9 @@ func TestConfigFieldReader(t *testing.T) { } for name, tc := range cases { + if name != "list" { + continue + } out, err := r.ReadField(tc.Addr) if (err != nil) != tc.OutErr { t.Fatalf("%s: err: %s", name, err) diff --git a/helper/schema/field_reader_diff.go b/helper/schema/field_reader_diff.go index 337bcc5c9..f627040aa 100644 --- a/helper/schema/field_reader_diff.go +++ b/helper/schema/field_reader_diff.go @@ -32,11 +32,12 @@ type DiffFieldReader struct { } func (r *DiffFieldReader) ReadField(address []string) (FieldReadResult, error) { - schema := addrToSchema(address, r.Schema) - if schema == nil { + schemaList := addrToSchema(address, r.Schema) + if len(schemaList) == 0 { return FieldReadResult{}, nil } + schema := schemaList[len(schemaList)-1] switch schema.Type { case TypeBool: fallthrough @@ -117,14 +118,15 @@ func (r *DiffFieldReader) readPrimitive( if !attrD.NewComputed { resultVal = attrD.New if attrD.NewExtra != nil { + result.ValueProcessed = resultVal if err := mapstructure.WeakDecode(attrD.NewExtra, &resultVal); err != nil { return FieldReadResult{}, err } } } - result.Exists = true result.Computed = attrD.NewComputed + result.Exists = true result.Value, err = stringToPrimitive(resultVal, false, schema) if err != nil { return FieldReadResult{}, err @@ -167,6 +169,6 @@ func (r *DiffFieldReader) readSet( return FieldReadResult{ Value: set, - Exists: true, + Exists: set.Len() > 0, }, nil } diff --git a/helper/schema/field_reader_diff_test.go b/helper/schema/field_reader_diff_test.go index 0e64e5b5c..adcdbe18e 100644 --- a/helper/schema/field_reader_diff_test.go +++ b/helper/schema/field_reader_diff_test.go @@ -73,6 +73,13 @@ func TestDiffFieldReader(t *testing.T) { return a.(map[string]interface{})["index"].(int) }, }, + "setEmpty": &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, } r := &DiffFieldReader{ @@ -197,7 +204,7 @@ func TestDiffFieldReader(t *testing.T) { Source: &MapFieldReader{ Schema: schema, - Map: map[string]string{ + Map: BasicMapReader(map[string]string{ "listMap.#": "2", "listMap.0.foo": "bar", "listMap.0.bar": "baz", @@ -209,7 +216,7 @@ func TestDiffFieldReader(t *testing.T) { "setChange.#": "1", "setChange.10.index": "10", "setChange.10.value": "50", - }, + }), }, } @@ -387,6 +394,15 @@ func TestDiffFieldReader(t *testing.T) { }, false, }, + + "setEmpty": { + []string{"setEmpty"}, + FieldReadResult{ + Value: []interface{}{}, + Exists: false, + }, + false, + }, } for name, tc := range cases { diff --git a/helper/schema/field_reader_map.go b/helper/schema/field_reader_map.go index f06a7170a..ad1a5f3dc 100644 --- a/helper/schema/field_reader_map.go +++ b/helper/schema/field_reader_map.go @@ -8,30 +8,31 @@ import ( // MapFieldReader reads fields out of an untyped map[string]string to // the best of its ability. type MapFieldReader struct { - Map map[string]string + Map MapReader Schema map[string]*Schema } func (r *MapFieldReader) ReadField(address []string) (FieldReadResult, error) { k := strings.Join(address, ".") - schema := addrToSchema(address, r.Schema) - if schema == nil { + schemaList := addrToSchema(address, r.Schema) + if len(schemaList) == 0 { return FieldReadResult{}, nil } + schema := schemaList[len(schemaList)-1] switch schema.Type { case TypeBool: fallthrough case TypeInt: fallthrough case TypeString: - return r.readPrimitive(k, schema) + return r.readPrimitive(address, schema) case TypeList: return readListField(r, address, schema) case TypeMap: return r.readMap(k) case TypeSet: - return r.readSet(k, schema) + return r.readSet(address, schema) case typeObject: return readObjectField(r, address, schema.Elem.(map[string]*Schema)) default: @@ -44,14 +45,14 @@ func (r *MapFieldReader) readMap(k string) (FieldReadResult, error) { resultSet := false prefix := k + "." - for k, v := range r.Map { - if !strings.HasPrefix(k, prefix) { - continue + r.Map.Range(func(k, v string) bool { + if strings.HasPrefix(k, prefix) { + result[k[len(prefix):]] = v + resultSet = true } - result[k[len(prefix):]] = v - resultSet = true - } + return true + }) var resultVal interface{} if resultSet { @@ -65,8 +66,9 @@ func (r *MapFieldReader) readMap(k string) (FieldReadResult, error) { } func (r *MapFieldReader) readPrimitive( - k string, schema *Schema) (FieldReadResult, error) { - result, ok := r.Map[k] + address []string, schema *Schema) (FieldReadResult, error) { + k := strings.Join(address, ".") + result, ok := r.Map.Access(k) if !ok { return FieldReadResult{}, nil } @@ -83,9 +85,10 @@ func (r *MapFieldReader) readPrimitive( } func (r *MapFieldReader) readSet( - k string, schema *Schema) (FieldReadResult, error) { + address []string, schema *Schema) (FieldReadResult, error) { // Get the number of elements in the list - countRaw, err := r.readPrimitive(k+".#", &Schema{Type: TypeInt}) + countRaw, err := r.readPrimitive( + append(address, "#"), &Schema{Type: TypeInt}) if err != nil { return FieldReadResult{}, err } @@ -101,29 +104,32 @@ func (r *MapFieldReader) readSet( if countRaw.Computed || countRaw.Value.(int) == 0 { return FieldReadResult{ Value: set, - Exists: true, + Exists: countRaw.Exists, Computed: countRaw.Computed, }, nil } // Go through the map and find all the set items - prefix := k + "." - for k, _ := range r.Map { + prefix := strings.Join(address, ".") + "." + countExpected := countRaw.Value.(int) + countActual := make(map[string]struct{}) + completed := r.Map.Range(func(k, _ string) bool { if !strings.HasPrefix(k, prefix) { - continue + return true } if strings.HasPrefix(k, prefix+"#") { // Ignore the count field - continue + return true } // Split the key, since it might be a sub-object like "idx.field" parts := strings.Split(k[len(prefix):], ".") idx := parts[0] - raw, err := r.ReadField([]string{prefix[:len(prefix)-1], idx}) + var raw FieldReadResult + raw, err = r.ReadField(append(address, idx)) if err != nil { - return FieldReadResult{}, err + return false } if !raw.Exists { // This shouldn't happen because we just verified it does exist @@ -131,6 +137,21 @@ func (r *MapFieldReader) readSet( } set.Add(raw.Value) + + // Due to the way multimap readers work, if we've seen the number + // of fields we expect, then exit so that we don't read later values. + // For example: the "set" map might have "ports.#", "ports.0", and + // "ports.1", but the "state" map might have those plus "ports.2". + // We don't want "ports.2" + countActual[idx] = struct{}{} + if len(countActual) >= countExpected { + return false + } + + return true + }) + if !completed && err != nil { + return FieldReadResult{}, err } return FieldReadResult{ @@ -138,3 +159,62 @@ func (r *MapFieldReader) readSet( Exists: true, }, nil } + +// MapReader is an interface that is given to MapFieldReader for accessing +// a "map". This can be used to have alternate implementations. For a basic +// map[string]string, use BasicMapReader. +type MapReader interface { + Access(string) (string, bool) + Range(func(string, string) bool) bool +} + +// BasicMapReader implements MapReader for a single map. +type BasicMapReader map[string]string + +func (r BasicMapReader) Access(k string) (string, bool) { + v, ok := r[k] + return v, ok +} + +func (r BasicMapReader) Range(f func(string, string) bool) bool { + for k, v := range r { + if cont := f(k, v); !cont { + return false + } + } + + return true +} + +// MultiMapReader reads over multiple maps, preferring keys that are +// founder earlier (lower number index) vs. later (higher number index) +type MultiMapReader []map[string]string + +func (r MultiMapReader) Access(k string) (string, bool) { + for _, m := range r { + if v, ok := m[k]; ok { + return v, ok + } + } + + return "", false +} + +func (r MultiMapReader) Range(f func(string, string) bool) bool { + done := make(map[string]struct{}) + for _, m := range r { + for k, v := range m { + if _, ok := done[k]; ok { + continue + } + + if cont := f(k, v); !cont { + return false + } + + done[k] = struct{}{} + } + } + + return true +} diff --git a/helper/schema/field_reader_map_test.go b/helper/schema/field_reader_map_test.go index 9734d7f7a..5c4081c36 100644 --- a/helper/schema/field_reader_map_test.go +++ b/helper/schema/field_reader_map_test.go @@ -43,9 +43,16 @@ func TestMapFieldReader(t *testing.T) { return a.(map[string]interface{})["index"].(int) }, }, + "setEmpty": &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, }, - Map: map[string]string{ + Map: BasicMapReader(map[string]string{ "bool": "true", "int": "42", "string": "string", @@ -70,7 +77,7 @@ func TestMapFieldReader(t *testing.T) { "setDeep.10.value": "foo", "setDeep.50.index": "50", "setDeep.50.value": "bar", - }, + }), } cases := map[string]struct { @@ -161,6 +168,14 @@ func TestMapFieldReader(t *testing.T) { false, }, + "setEmpty": { + []string{"setEmpty"}, + []interface{}{}, + false, + false, + false, + }, + "setDeep": { []string{"setDeep"}, []interface{}{ diff --git a/helper/schema/field_reader_multi.go b/helper/schema/field_reader_multi.go index 3e2134708..89ad3a86f 100644 --- a/helper/schema/field_reader_multi.go +++ b/helper/schema/field_reader_multi.go @@ -41,20 +41,17 @@ func (r *MultiLevelFieldReader) ReadFieldMerge( address []string, level string) (FieldReadResult, error) { var result FieldReadResult for _, l := range r.Levels { - r, ok := r.Readers[l] - if !ok { - continue - } + if r, ok := r.Readers[l]; ok { + out, err := r.ReadField(address) + if err != nil { + return FieldReadResult{}, fmt.Errorf( + "Error reading level %s: %s", l, err) + } - out, err := r.ReadField(address) - if err != nil { - return FieldReadResult{}, fmt.Errorf( - "Error reading level %s: %s", l, err) - } - - // TODO: computed - if out.Exists { - result = out + // TODO: computed + if out.Exists { + result = out + } } if l == level { diff --git a/helper/schema/field_reader_multi_test.go b/helper/schema/field_reader_multi_test.go index 915fb1447..85286a66e 100644 --- a/helper/schema/field_reader_multi_test.go +++ b/helper/schema/field_reader_multi_test.go @@ -23,23 +23,23 @@ func TestMultiLevelFieldReaderReadFieldExact(t *testing.T) { Schema: map[string]*Schema{ "foo": &Schema{Type: TypeString}, }, - Map: map[string]string{ + Map: BasicMapReader(map[string]string{ "foo": "bar", - }, + }), }, &MapFieldReader{ Schema: map[string]*Schema{ "foo": &Schema{Type: TypeString}, }, - Map: map[string]string{ + Map: BasicMapReader(map[string]string{ "foo": "baz", - }, + }), }, &MapFieldReader{ Schema: map[string]*Schema{ "foo": &Schema{Type: TypeString}, }, - Map: map[string]string{}, + Map: BasicMapReader(map[string]string{}), }, }, @@ -95,9 +95,9 @@ func TestMultiLevelFieldReaderReadFieldMerge(t *testing.T) { Schema: map[string]*Schema{ "availability_zone": &Schema{Type: TypeString}, }, - Map: map[string]string{ + Map: BasicMapReader(map[string]string{ "availability_zone": "foo", - }, + }), }, Diff: &terraform.InstanceDiff{ @@ -127,9 +127,9 @@ func TestMultiLevelFieldReaderReadFieldMerge(t *testing.T) { "availability_zone": &Schema{Type: TypeString}, }, - Map: map[string]string{ + Map: BasicMapReader(map[string]string{ "availability_zone": "foo", - }, + }), }, &DiffFieldReader{ @@ -142,9 +142,9 @@ func TestMultiLevelFieldReaderReadFieldMerge(t *testing.T) { "availability_zone": &Schema{Type: TypeString}, }, - Map: map[string]string{ + Map: BasicMapReader(map[string]string{ "availability_zone": "foo", - }, + }), }, Diff: &terraform.InstanceDiff{ @@ -186,12 +186,12 @@ func TestMultiLevelFieldReaderReadFieldMerge(t *testing.T) { }, }, - Map: map[string]string{ + Map: BasicMapReader(map[string]string{ "config_vars.#": "2", "config_vars.0.foo": "bar", "config_vars.0.bar": "bar", "config_vars.1.bar": "baz", - }, + }), }, Diff: &terraform.InstanceDiff{ @@ -225,15 +225,15 @@ func TestMultiLevelFieldReaderReadFieldMerge(t *testing.T) { Schema: map[string]*Schema{ "foo": &Schema{Type: TypeString}, }, - Map: map[string]string{ + Map: BasicMapReader(map[string]string{ "foo": "bar", - }, + }), }, &MapFieldReader{ Schema: map[string]*Schema{ "foo": &Schema{Type: TypeString}, }, - Map: map[string]string{}, + Map: BasicMapReader(map[string]string{}), }, }, diff --git a/helper/schema/field_reader_test.go b/helper/schema/field_reader_test.go index 7498b05f6..64d2c3f2c 100644 --- a/helper/schema/field_reader_test.go +++ b/helper/schema/field_reader_test.go @@ -9,14 +9,134 @@ func TestAddrToSchema(t *testing.T) { cases := map[string]struct { Addr []string Schema map[string]*Schema - Result *Schema + Result []ValueType }{ + "list": { + []string{"list"}, + map[string]*Schema{ + "list": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + }, + }, + []ValueType{TypeList}, + }, + + "list.#": { + []string{"list", "#"}, + map[string]*Schema{ + "list": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + }, + }, + []ValueType{TypeList, TypeInt}, + }, + + "list.0": { + []string{"list", "0"}, + map[string]*Schema{ + "list": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + }, + }, + []ValueType{TypeList, TypeInt}, + }, + + "list.0 with resource": { + []string{"list", "0"}, + map[string]*Schema{ + "list": &Schema{ + Type: TypeList, + Elem: &Resource{ + Schema: map[string]*Schema{ + "field": &Schema{Type: TypeString}, + }, + }, + }, + }, + []ValueType{TypeList, typeObject}, + }, + + "list.0.field": { + []string{"list", "0", "field"}, + map[string]*Schema{ + "list": &Schema{ + Type: TypeList, + Elem: &Resource{ + Schema: map[string]*Schema{ + "field": &Schema{Type: TypeString}, + }, + }, + }, + }, + []ValueType{TypeList, typeObject, TypeString}, + }, + + "set": { + []string{"set"}, + map[string]*Schema{ + "set": &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + }, + []ValueType{TypeSet}, + }, + + "set.#": { + []string{"set", "#"}, + map[string]*Schema{ + "set": &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + }, + []ValueType{TypeSet, TypeInt}, + }, + + "set.0": { + []string{"set", "0"}, + map[string]*Schema{ + "set": &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + }, + []ValueType{TypeSet, TypeInt}, + }, + + "set.0 with resource": { + []string{"set", "0"}, + map[string]*Schema{ + "set": &Schema{ + Type: TypeSet, + Elem: &Resource{ + Schema: map[string]*Schema{ + "field": &Schema{Type: TypeString}, + }, + }, + }, + }, + []ValueType{TypeSet, typeObject}, + }, + "mapElem": { []string{"map", "foo"}, map[string]*Schema{ "map": &Schema{Type: TypeMap}, }, - &Schema{Type: TypeString}, + []ValueType{TypeMap, TypeString}, }, "setDeep": { @@ -35,14 +155,19 @@ func TestAddrToSchema(t *testing.T) { }, }, }, - &Schema{Type: TypeInt}, + []ValueType{TypeSet, typeObject, TypeInt}, }, } for name, tc := range cases { result := addrToSchema(tc.Addr, tc.Schema) - if !reflect.DeepEqual(result, tc.Result) { - t.Fatalf("%s: %#v", name, result) + types := make([]ValueType, len(result)) + for i, v := range result { + types[i] = v.Type + } + + if !reflect.DeepEqual(types, tc.Result) { + t.Fatalf("%s: %#v", name, types) } } } diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go index ee6728735..2f0082be5 100644 --- a/helper/schema/resource_data.go +++ b/helper/schema/resource_data.go @@ -28,11 +28,12 @@ type ResourceData struct { diffing bool // Don't set - setMap map[string]string - newState *terraform.InstanceState - partial bool - partialMap map[string]struct{} - once sync.Once + multiReader *MultiLevelFieldReader + setMap map[string]string + newState *terraform.InstanceState + partial bool + partialMap map[string]struct{} + once sync.Once } // getSource represents the level we want to get for a value (internally). @@ -153,10 +154,7 @@ func (d *ResourceData) Partial(on bool) { // If the key is invalid or the value is not a correct type, an error // will be returned. func (d *ResourceData) Set(key string, value interface{}) error { - if d.setMap == nil { - d.setMap = make(map[string]string) - } - + d.once.Do(d.init) parts := strings.Split(key, ".") return d.setObject("", parts, d.schema, value) } @@ -236,12 +234,57 @@ func (d *ResourceData) State() *terraform.InstanceState { } func (d *ResourceData) init() { + // Initialize the field that will store our new state var copyState terraform.InstanceState if d.state != nil { copyState = *d.state } - d.newState = ©State + + // Initialize the map for storing set data + d.setMap = make(map[string]string) + + // Initialize the reader for getting data from the + // underlying sources (config, diff, etc.) + readers := make(map[string]FieldReader) + var stateAttributes map[string]string + if d.state != nil { + stateAttributes = d.state.Attributes + readers["state"] = &MapFieldReader{ + Schema: d.schema, + Map: BasicMapReader(stateAttributes), + } + } + if d.config != nil { + readers["config"] = &ConfigFieldReader{ + Schema: d.schema, + Config: d.config, + } + } + if d.diff != nil { + readers["diff"] = &DiffFieldReader{ + Schema: d.schema, + Diff: d.diff, + Source: &MultiLevelFieldReader{ + Levels: []string{"state", "config"}, + Readers: readers, + }, + } + } + readers["set"] = &MapFieldReader{ + Schema: d.schema, + Map: BasicMapReader(d.setMap), + } + d.multiReader = &MultiLevelFieldReader{ + Levels: []string{ + "state", + "config", + "diff", + "set", + }, + + Readers: readers, + } } func (d *ResourceData) diffChange( @@ -279,21 +322,57 @@ func (d *ResourceData) get( parts []string, schema *Schema, source getSource) getResult { - switch schema.Type { - case TypeList: - return d.getList(k, parts, schema, source) - case TypeMap: - return d.getMap(k, parts, schema, source) - case TypeSet: - return d.getSet(k, parts, schema, source) - case TypeBool: - fallthrough - case TypeInt: - fallthrough - case TypeString: - return d.getPrimitive(k, parts, schema, source) - default: - panic(fmt.Sprintf("%s: unknown type %#v", k, schema.Type)) + d.once.Do(d.init) + + level := "set" + flags := source & ^getSourceLevelMask + diff := flags&getSourceDiff != 0 + exact := flags&getSourceExact != 0 + source = source & getSourceLevelMask + if source >= getSourceSet { + level = "set" + } else if diff { + level = "diff" + } else if source >= getSourceConfig { + level = "config" + } else { + level = "state" + } + + // Build the address of the key we're looking for and ask the FieldReader + addr := append(strings.Split(k, "."), parts...) + for i, v := range addr { + if v[0] == '~' { + addr[i] = v[1:] + } + } + + var result FieldReadResult + var err error + if exact { + result, err = d.multiReader.ReadFieldExact(addr, level) + } else { + result, err = d.multiReader.ReadFieldMerge(addr, level) + } + if err != nil { + panic(err) + } + + // If the result doesn't exist, then we set the value to the zero value + if result.Value == nil { + if schema := addrToSchema(addr, d.schema); len(schema) > 0 { + result.Value = schema[len(schema)-1].Type.Zero() + } + } + + // Transform the FieldReadResult into a getResult. It might be worth + // merging these two structures one day. + return getResult{ + Value: result.Value, + ValueProcessed: result.ValueProcessed, + Computed: result.Computed, + Exists: result.Exists, + Schema: schema, } } @@ -831,11 +910,10 @@ func (d *ResourceData) setList( schema *Schema, value interface{}) error { if len(parts) > 0 { - // We're setting a specific element - idx := parts[0] - parts = parts[1:] + return fmt.Errorf("%s: can only set the full list, not elements", k) + } - // Special case if we're accessing the count of the list + setElement := func(k string, idx string, value interface{}) error { if idx == "#" { return fmt.Errorf("%s: can't set count of list", k) } @@ -843,10 +921,12 @@ func (d *ResourceData) setList( key := fmt.Sprintf("%s.%s", k, idx) switch t := schema.Elem.(type) { case *Resource: - return d.setObject(key, parts, t.Schema, value) + return d.setObject(key, nil, t.Schema, value) case *Schema: - return d.set(key, parts, t, value) + return d.set(key, nil, t, value) } + + return nil } var vs []interface{} @@ -858,7 +938,7 @@ func (d *ResourceData) setList( var err error for i, elem := range vs { is := strconv.FormatInt(int64(i), 10) - err = d.setList(k, []string{is}, schema, elem) + err = setElement(k, is, elem) if err != nil { break } @@ -866,7 +946,7 @@ func (d *ResourceData) setList( if err != nil { for i, _ := range vs { is := strconv.FormatInt(int64(i), 10) - d.setList(k, []string{is}, schema, nil) + setElement(k, is, nil) } return err @@ -1015,7 +1095,9 @@ func (d *ResourceData) setSet( // Build a temp *ResourceData to use for the conversion tempD := &ResourceData{ setMap: make(map[string]string), + schema: map[string]*Schema{k: schema}, } + tempD.once.Do(tempD.init) // Set the entire list, this lets us get sane values out of it if err := tempD.setList(k, nil, schema, value); err != nil { @@ -1031,7 +1113,7 @@ func (d *ResourceData) setSet( source := getSourceSet | getSourceExact for i := 0; i < v.Len(); i++ { is := strconv.FormatInt(int64(i), 10) - result := tempD.getList(k, []string{is}, schema, source) + result := tempD.get(k, []string{is}, schema, source) if !result.Exists { panic("just set item doesn't exist") } diff --git a/helper/schema/resource_data_test.go b/helper/schema/resource_data_test.go index e66a681e8..0151a45a9 100644 --- a/helper/schema/resource_data_test.go +++ b/helper/schema/resource_data_test.go @@ -606,7 +606,7 @@ func TestResourceDataGet(t *testing.T) { } if !reflect.DeepEqual(v, tc.Value) { - t.Fatalf("Bad: %d\n\n%#v", i, v) + t.Fatalf("Bad: %d\n\n%#v\n\nExpected: %#v", i, v, tc.Value) } } } @@ -816,7 +816,7 @@ func TestResourceDataGetOk(t *testing.T) { Diff: nil, Key: "ports", - Value: []interface{}{}, + Value: nil, Ok: false, }, @@ -1082,34 +1082,6 @@ func TestResourceDataSet(t *testing.T) { GetValue: "", }, - // List of primitives, set element - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Computed: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "3", - "ports.0": "1", - "ports.1": "2", - "ports.2": "5", - }, - }, - - Diff: nil, - - Key: "ports.1", - Value: 3, - - GetKey: "ports", - GetValue: []interface{}{1, 3, 5}, - }, - // List of primitives, set list { Schema: map[string]*Schema{ @@ -1153,139 +1125,6 @@ func TestResourceDataSet(t *testing.T) { GetValue: []interface{}{}, }, - // List of resource, set element - { - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Computed: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "from": &Schema{ - Type: TypeInt, - }, - }, - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ingress.#": "2", - "ingress.0.from": "80", - "ingress.1.from": "8080", - }, - }, - - Diff: nil, - - Key: "ingress.1.from", - Value: 9000, - - GetKey: "ingress", - GetValue: []interface{}{ - map[string]interface{}{ - "from": 80, - }, - map[string]interface{}{ - "from": 9000, - }, - }, - }, - - // List of resource, set full resource element - { - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Computed: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "from": &Schema{ - Type: TypeInt, - }, - }, - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ingress.#": "2", - "ingress.0.from": "80", - "ingress.1.from": "8080", - }, - }, - - Diff: nil, - - Key: "ingress.1", - Value: map[string]interface{}{ - "from": 9000, - }, - - GetKey: "ingress", - GetValue: []interface{}{ - map[string]interface{}{ - "from": 80, - }, - map[string]interface{}{ - "from": 9000, - }, - }, - }, - - // List of resource, set full resource element, with error - { - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Computed: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "from": &Schema{ - Type: TypeInt, - }, - "to": &Schema{ - Type: TypeInt, - }, - }, - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ingress.#": "2", - "ingress.0.from": "80", - "ingress.0.to": "10", - "ingress.1.from": "8080", - "ingress.1.to": "8080", - }, - }, - - Diff: nil, - - Key: "ingress.1", - Value: map[string]interface{}{ - "from": 9000, - "to": "bar", - }, - Err: true, - - GetKey: "ingress", - GetValue: []interface{}{ - map[string]interface{}{ - "from": 80, - "to": 10, - }, - map[string]interface{}{ - "from": 8080, - "to": 8080, - }, - }, - }, - // Set a list of maps { Schema: map[string]*Schema{ @@ -1325,45 +1164,6 @@ func TestResourceDataSet(t *testing.T) { }, }, - // Set a list of maps - { - Schema: map[string]*Schema{ - "config_vars": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - Elem: &Schema{ - Type: TypeMap, - }, - }, - }, - - State: nil, - - Diff: nil, - - Key: "config_vars", - Value: []interface{}{ - map[string]string{ - "foo": "bar", - }, - map[string]string{ - "bar": "baz", - }, - }, - Err: false, - - GetKey: "config_vars", - GetValue: []interface{}{ - map[string]interface{}{ - "foo": "bar", - }, - map[string]interface{}{ - "bar": "baz", - }, - }, - }, - // Set, with list { Schema: map[string]*Schema{ @@ -1727,8 +1527,13 @@ func TestResourceDataState(t *testing.T) { }, Set: map[string]interface{}{ - "config_vars.1": map[string]interface{}{ - "baz": "bang", + "config_vars": []map[string]interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + map[string]interface{}{ + "baz": "bang", + }, }, }, @@ -2141,8 +1946,13 @@ func TestResourceDataState(t *testing.T) { }, Set: map[string]interface{}{ - "config_vars.1": map[string]interface{}{ - "baz": "bang", + "config_vars": []map[string]interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + map[string]interface{}{ + "baz": "bang", + }, }, }, @@ -2150,6 +1960,7 @@ func TestResourceDataState(t *testing.T) { Result: &terraform.InstanceState{ Attributes: map[string]string{ + // TODO: broken, shouldn't bar be removed? "config_vars.#": "2", "config_vars.0.foo": "bar", "config_vars.0.bar": "bar", diff --git a/helper/schema/schema.go b/helper/schema/schema.go index e875028bd..218de7851 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -36,6 +36,28 @@ const ( typeObject ) +// Zero returns the zero value for a type. +func (t ValueType) Zero() interface{} { + switch t { + case TypeInvalid: + return nil + case TypeBool: + return false + case TypeInt: + return 0 + case TypeString: + return "" + case TypeList: + return []interface{}{} + case TypeMap: + return map[string]interface{}{} + case TypeSet: + return nil + default: + panic(fmt.Sprintf("unknown type %#v", t)) + } +} + // Schema is used to describe the structure of a value. // // Read the documentation of the struct elements for important details. @@ -221,8 +243,10 @@ func (m schemaMap) Diff( result2 := new(terraform.InstanceDiff) result2.Attributes = make(map[string]*terraform.ResourceAttrDiff) - // Reset the data to not contain state + // Reset the data to not contain state. We have to call init() + // again in order to reset the FieldReaders. d.state = nil + d.init() // Perform the diff again for k, schema := range m { @@ -458,6 +482,9 @@ func (m schemaMap) diffList( d *ResourceData, all bool) error { o, n, _, computedList := d.diffChange(k) + if computedList { + n = nil + } nSet := n != nil // If we have an old value and no new value is set or will be @@ -655,6 +682,9 @@ func (m schemaMap) diffSet( d *ResourceData, all bool) error { o, n, _, computedSet := d.diffChange(k) + if computedSet { + n = nil + } nSet := n != nil // If we have an old value and no new value is set or will be diff --git a/helper/schema/schema_test.go b/helper/schema/schema_test.go index 9f24f8b76..f741ad56c 100644 --- a/helper/schema/schema_test.go +++ b/helper/schema/schema_test.go @@ -8,6 +8,27 @@ import ( "github.com/hashicorp/terraform/terraform" ) +func TestValueType_Zero(t *testing.T) { + cases := []struct { + Type ValueType + Value interface{} + }{ + {TypeBool, false}, + {TypeInt, 0}, + {TypeString, ""}, + {TypeList, []interface{}{}}, + {TypeMap, map[string]interface{}{}}, + {TypeSet, nil}, + } + + for i, tc := range cases { + actual := tc.Type.Zero() + if !reflect.DeepEqual(actual, tc.Value) { + t.Fatalf("%d: %#v != %#v", i, actual, tc.Value) + } + } +} + func TestSchemaMap_Diff(t *testing.T) { cases := []struct { Schema map[string]*Schema @@ -844,7 +865,7 @@ func TestSchemaMap_Diff(t *testing.T) { Diff: &terraform.InstanceDiff{ Attributes: map[string]*terraform.ResourceAttrDiff{ "ports.#": &terraform.ResourceAttrDiff{ - Old: "", + Old: "0", New: "", NewComputed: true, }, @@ -1664,7 +1685,7 @@ func TestSchemaMap_Diff(t *testing.T) { New: "1", }, "route.~1.gateway.#": &terraform.ResourceAttrDiff{ - Old: "", + Old: "0", NewComputed: true, }, }, From 942a988ac24cb62763b0ab681118683c520d97d2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Jan 2015 18:47:35 -0800 Subject: [PATCH 12/23] helper/schema: zero value of a set should be a set --- helper/schema/resource_data.go | 15 +++++++++++---- helper/schema/resource_data_test.go | 25 ++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go index 2f0082be5..cac9cf280 100644 --- a/helper/schema/resource_data.go +++ b/helper/schema/resource_data.go @@ -360,8 +360,15 @@ func (d *ResourceData) get( // If the result doesn't exist, then we set the value to the zero value if result.Value == nil { - if schema := addrToSchema(addr, d.schema); len(schema) > 0 { - result.Value = schema[len(schema)-1].Type.Zero() + if schemaL := addrToSchema(addr, d.schema); len(schemaL) > 0 { + schema := schemaL[len(schemaL)-1] + result.Value = schema.Type.Zero() + + // The zero value of a set is nil, but we want it + // to actually be an empty set object... + if schema.Type == TypeSet && result.Value == nil { + result.Value = &Set{F: schema.Set} + } } } @@ -1129,8 +1136,8 @@ func (d *ResourceData) setSet( for code, elem := range value.(*Set).m { for field, _ := range t.Schema { subK := fmt.Sprintf("%s.%d", k, code) - err := d.setObject( - subK, []string{field}, t.Schema, elem.(map[string]interface{})[field]) + value := elem.(map[string]interface{})[field] + err := d.setObject(subK, []string{field}, t.Schema, value) if err != nil { return err } diff --git a/helper/schema/resource_data_test.go b/helper/schema/resource_data_test.go index 0151a45a9..effed3067 100644 --- a/helper/schema/resource_data_test.go +++ b/helper/schema/resource_data_test.go @@ -592,6 +592,29 @@ func TestResourceDataGet(t *testing.T) { }, }, }, + + // #19 Empty Set + { + Schema: map[string]*Schema{ + "ports": &Schema{ + Type: TypeSet, + Optional: true, + Computed: true, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + }, + + State: nil, + + Diff: nil, + + Key: "ports", + + Value: []interface{}{}, + }, } for i, tc := range cases { @@ -816,7 +839,7 @@ func TestResourceDataGetOk(t *testing.T) { Diff: nil, Key: "ports", - Value: nil, + Value: []interface{}{}, Ok: false, }, From f0af1c36f58822b8d5b8f6365966c331a781932a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 9 Jan 2015 11:51:29 -0800 Subject: [PATCH 13/23] helper/schema: nested resource fields should be zero-valued on get --- helper/schema/field_reader.go | 29 +++++++-- helper/schema/resource_data.go | 8 +-- helper/schema/resource_data_test.go | 98 ++++++++++++++++++++++++++--- 3 files changed, 113 insertions(+), 22 deletions(-) diff --git a/helper/schema/field_reader.go b/helper/schema/field_reader.go index 911ff219f..60d6a460a 100644 --- a/helper/schema/field_reader.go +++ b/helper/schema/field_reader.go @@ -31,6 +31,24 @@ type FieldReadResult struct { Computed bool } +// ValueOrZero returns the value of this result or the zero value of the +// schema type, ensuring a consistent non-nil return value. +func (r *FieldReadResult) ValueOrZero(s *Schema) interface{} { + if r.Value != nil { + return r.Value + } + + result := s.Type.Zero() + + // The zero value of a set is nil, but we want it + // to actually be an empty set object... + if s.Type == TypeSet && result == nil { + result = &Set{F: s.Set} + } + + return result +} + // addrToSchema finds the final element schema for the given address // and the given schema. It returns all the schemas that led to the final // schema. These are in order of the address (out to in). @@ -182,7 +200,8 @@ func readObjectField( addr []string, schema map[string]*Schema) (FieldReadResult, error) { result := make(map[string]interface{}) - for field, _ := range schema { + exists := false + for field, s := range schema { addrRead := make([]string, len(addr), len(addr)+1) copy(addrRead, addr) addrRead = append(addrRead, field) @@ -190,16 +209,16 @@ func readObjectField( if err != nil { return FieldReadResult{}, err } - if !rawResult.Exists { - continue + if rawResult.Exists { + exists = true } - result[field] = rawResult.Value + result[field] = rawResult.ValueOrZero(s) } return FieldReadResult{ Value: result, - Exists: len(schema) > 0 && len(result) > 0, + Exists: exists, }, nil } diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go index cac9cf280..5c2511bdc 100644 --- a/helper/schema/resource_data.go +++ b/helper/schema/resource_data.go @@ -362,13 +362,7 @@ func (d *ResourceData) get( if result.Value == nil { if schemaL := addrToSchema(addr, d.schema); len(schemaL) > 0 { schema := schemaL[len(schemaL)-1] - result.Value = schema.Type.Zero() - - // The zero value of a set is nil, but we want it - // to actually be an empty set object... - if schema.Type == TypeSet && result.Value == nil { - result.Value = &Set{F: schema.Set} - } + result.Value = result.ValueOrZero(schema) } } diff --git a/helper/schema/resource_data_test.go b/helper/schema/resource_data_test.go index effed3067..3025a2bed 100644 --- a/helper/schema/resource_data_test.go +++ b/helper/schema/resource_data_test.go @@ -998,8 +998,12 @@ func TestResourceDataSet(t *testing.T) { Err bool GetKey string GetValue interface{} + + // GetPreProcess can be set to munge the return value before being + // compared to GetValue + GetPreProcess func(interface{}) interface{} }{ - // Basic good + // #0: Basic good { Schema: map[string]*Schema{ "availability_zone": &Schema{ @@ -1021,7 +1025,7 @@ func TestResourceDataSet(t *testing.T) { GetValue: "foo", }, - // Basic int + // #1: Basic int { Schema: map[string]*Schema{ "port": &Schema{ @@ -1043,7 +1047,7 @@ func TestResourceDataSet(t *testing.T) { GetValue: 80, }, - // Basic bool + // #2: Basic bool { Schema: map[string]*Schema{ "vpc": &Schema{ @@ -1063,6 +1067,7 @@ func TestResourceDataSet(t *testing.T) { GetValue: true, }, + // #3 { Schema: map[string]*Schema{ "vpc": &Schema{ @@ -1082,7 +1087,7 @@ func TestResourceDataSet(t *testing.T) { GetValue: false, }, - // Invalid type + // #4: Invalid type { Schema: map[string]*Schema{ "availability_zone": &Schema{ @@ -1105,7 +1110,7 @@ func TestResourceDataSet(t *testing.T) { GetValue: "", }, - // List of primitives, set list + // #5: List of primitives, set list { Schema: map[string]*Schema{ "ports": &Schema{ @@ -1126,7 +1131,7 @@ func TestResourceDataSet(t *testing.T) { GetValue: []interface{}{1, 2, 5}, }, - // List of primitives, set list with error + // #6: List of primitives, set list with error { Schema: map[string]*Schema{ "ports": &Schema{ @@ -1148,7 +1153,7 @@ func TestResourceDataSet(t *testing.T) { GetValue: []interface{}{}, }, - // Set a list of maps + // #7: Set a list of maps { Schema: map[string]*Schema{ "config_vars": &Schema{ @@ -1187,7 +1192,7 @@ func TestResourceDataSet(t *testing.T) { }, }, - // Set, with list + // #8: Set, with list { Schema: map[string]*Schema{ "ports": &Schema{ @@ -1217,7 +1222,7 @@ func TestResourceDataSet(t *testing.T) { GetValue: []interface{}{100, 125}, }, - // Set, with Set + // #9: Set, with Set { Schema: map[string]*Schema{ "ports": &Schema{ @@ -1252,7 +1257,7 @@ func TestResourceDataSet(t *testing.T) { GetValue: []interface{}{1, 2}, }, - // Set single item + // #10: Set single item { Schema: map[string]*Schema{ "ports": &Schema{ @@ -1281,6 +1286,74 @@ func TestResourceDataSet(t *testing.T) { GetKey: "ports", GetValue: []interface{}{80, 100}, }, + + // #11: Set with nested set + { + Schema: map[string]*Schema{ + "ports": &Schema{ + Type: TypeSet, + Elem: &Resource{ + Schema: map[string]*Schema{ + "port": &Schema{ + Type: TypeInt, + }, + + "set": &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + }, + }, + Set: func(a interface{}) int { + return a.(map[string]interface{})["port"].(int) + }, + }, + }, + + State: nil, + + Key: "ports", + Value: []interface{}{ + map[string]interface{}{ + "port": 80, + }, + }, + + GetKey: "ports", + GetValue: []interface{}{ + map[string]interface{}{ + "port": 80, + "set": []interface{}{}, + }, + }, + + GetPreProcess: func(v interface{}) interface{} { + if v == nil { + return v + } + s, ok := v.([]interface{}) + if !ok { + return v + } + for _, v := range s { + m, ok := v.(map[string]interface{}) + if !ok { + continue + } + if m["set"] == nil { + continue + } + if s, ok := m["set"].(*Set); ok { + m["set"] = s.List() + } + } + + return v + }, + }, } for i, tc := range cases { @@ -1298,6 +1371,11 @@ func TestResourceDataSet(t *testing.T) { if s, ok := v.(*Set); ok { v = s.List() } + + if tc.GetPreProcess != nil { + v = tc.GetPreProcess(v) + } + if !reflect.DeepEqual(v, tc.GetValue) { t.Fatalf("Get Bad: %d\n\n%#v", i, v) } From e57f3f69b104d1a360e246674c66b4e97c8bd6bb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 9 Jan 2015 15:07:02 -0800 Subject: [PATCH 14/23] helper/schema: empty maps, support reading objects directly --- helper/schema/field_reader.go | 7 +- helper/schema/field_reader_map.go | 7 + helper/schema/field_reader_map_test.go | 13 +- helper/schema/field_reader_multi.go | 3 + helper/schema/resource_data.go | 521 +------------------------ helper/schema/resource_data_test.go | 36 +- 6 files changed, 60 insertions(+), 527 deletions(-) diff --git a/helper/schema/field_reader.go b/helper/schema/field_reader.go index 60d6a460a..136a7be0d 100644 --- a/helper/schema/field_reader.go +++ b/helper/schema/field_reader.go @@ -57,8 +57,13 @@ func addrToSchema(addr []string, schemaMap map[string]*Schema) []*Schema { Type: typeObject, Elem: schemaMap, } - result := make([]*Schema, 0, len(addr)) + // TODO: test + if len(addr) == 0 { + return []*Schema{current} + } + + result := make([]*Schema, 0, len(addr)) for len(addr) > 0 { k := addr[0] addr = addr[1:] diff --git a/helper/schema/field_reader_map.go b/helper/schema/field_reader_map.go index ad1a5f3dc..5a923c19d 100644 --- a/helper/schema/field_reader_map.go +++ b/helper/schema/field_reader_map.go @@ -44,6 +44,13 @@ func (r *MapFieldReader) readMap(k string) (FieldReadResult, error) { result := make(map[string]interface{}) resultSet := false + // If the name of the map field is directly in the map with an + // empty string, it means that the map is being deleted, so mark + // that is is set. + if v, ok := r.Map.Access(k); ok && v == "" { + resultSet = true + } + prefix := k + "." r.Map.Range(func(k, v string) bool { if strings.HasPrefix(k, prefix) { diff --git a/helper/schema/field_reader_map_test.go b/helper/schema/field_reader_map_test.go index 5c4081c36..57a02f846 100644 --- a/helper/schema/field_reader_map_test.go +++ b/helper/schema/field_reader_map_test.go @@ -23,7 +23,8 @@ func TestMapFieldReader(t *testing.T) { Type: TypeList, Elem: &Schema{Type: TypeInt}, }, - "map": &Schema{Type: TypeMap}, + "map": &Schema{Type: TypeMap}, + "mapDel": &Schema{Type: TypeMap}, "set": &Schema{ Type: TypeSet, Elem: &Schema{Type: TypeInt}, @@ -68,6 +69,8 @@ func TestMapFieldReader(t *testing.T) { "map.foo": "bar", "map.bar": "baz", + "mapDel": "", + "set.#": "2", "set.10": "10", "set.50": "50", @@ -152,6 +155,14 @@ func TestMapFieldReader(t *testing.T) { false, }, + "mapDel": { + []string{"mapDel"}, + map[string]interface{}{}, + true, + false, + false, + }, + "mapelem": { []string{"map", "foo"}, "bar", diff --git a/helper/schema/field_reader_multi.go b/helper/schema/field_reader_multi.go index 89ad3a86f..2e5fa0ae4 100644 --- a/helper/schema/field_reader_multi.go +++ b/helper/schema/field_reader_multi.go @@ -42,7 +42,10 @@ func (r *MultiLevelFieldReader) ReadFieldMerge( var result FieldReadResult for _, l := range r.Levels { if r, ok := r.Readers[l]; ok { + println(fmt.Sprintf("GET %#v %s %#v", address, l, r)) out, err := r.ReadField(address) + println(fmt.Sprintf("%#v", out)) + println("======================================") if err != nil { return FieldReadResult{}, fmt.Errorf( "Error reading level %s: %s", l, err) diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go index 5c2511bdc..9afd1daa6 100644 --- a/helper/schema/resource_data.go +++ b/helper/schema/resource_data.go @@ -105,7 +105,8 @@ func (d *ResourceData) getRaw(key string, level getSource) getResult { parts = strings.Split(key, ".") } - return d.getObject("", parts, d.schema, level) + schema := &Schema{Type: typeObject, Elem: d.schema} + return d.get("", parts, schema, level) } // HasChange returns whether or not the given key has been changed. @@ -312,8 +313,9 @@ func (d *ResourceData) getChange( parts2 = strings.Split(key, ".") } - o := d.getObject("", parts, d.schema, oldLevel) - n := d.getObject("", parts2, d.schema, newLevel) + schema := &Schema{Type: typeObject, Elem: d.schema} + o := d.get("", parts, schema, oldLevel) + n := d.get("", parts2, schema, newLevel) return o, n } @@ -340,7 +342,11 @@ func (d *ResourceData) get( } // Build the address of the key we're looking for and ask the FieldReader - addr := append(strings.Split(k, "."), parts...) + var addr []string + if k != "" { + addr = strings.Split(k, ".") + } + addr = append(addr, parts...) for i, v := range addr { if v[0] == '~' { addr[i] = v[1:] @@ -377,511 +383,6 @@ func (d *ResourceData) get( } } -func (d *ResourceData) getSet( - k string, - parts []string, - schema *Schema, - source getSource) getResult { - s := &Set{F: schema.Set} - result := getResult{Schema: schema, Value: s} - prefix := k + "." - - // Get the set. For sets, the entire source must be exact: the - // entire set must come from set, diff, state, etc. So we go backwards - // and once we get a result, we take it. Or, we never get a result. - var indexMap map[int]int - codes := make(map[string]int) - sourceLevel := source & getSourceLevelMask - sourceFlags := source & ^getSourceLevelMask - sourceDiff := sourceFlags&getSourceDiff != 0 - for setSource := sourceLevel; setSource > 0; setSource >>= 1 { - // If we're already asking for an exact source and it doesn't - // match, then leave since the original source was the match. - if sourceFlags&getSourceExact != 0 && setSource != sourceLevel { - break - } - - if d.config != nil && setSource == getSourceConfig { - raw := d.getList(k, nil, schema, setSource) - // If the entire list is computed, then the entire set is - // necessarilly computed. - if raw.Computed { - result.Computed = true - if len(parts) > 0 { - break - } - return result - } - - if raw.Exists { - result.Exists = true - - list := raw.Value.([]interface{}) - indexMap = make(map[int]int, len(list)) - - // Build the set from all the items using the given hash code - for i, v := range list { - code := s.add(v) - - // Check if any of the keys in this item are computed - computed := false - if len(d.config.ComputedKeys) > 0 { - prefix := fmt.Sprintf("%s.%d", k, i) - computed = d.hasComputedSubKeys(prefix, schema) - } - - // Check if we are computed and if so negatate the hash to - // this is a approximate hash - if computed { - s.m[-code] = s.m[code] - delete(s.m, code) - code = -code - } - indexMap[code] = i - } - - break - } - } - - if d.state != nil && setSource == getSourceState { - for k, _ := range d.state.Attributes { - if !strings.HasPrefix(k, prefix) || strings.HasPrefix(k, prefix+"#") { - continue - } - parts := strings.Split(k[len(prefix):], ".") - idx := parts[0] - if _, ok := codes[idx]; ok { - continue - } - - code, err := strconv.Atoi(strings.Replace(parts[0], "~", "-", -1)) - if err != nil { - panic(fmt.Sprintf("unable to convert %s to int: %v", idx, err)) - } - codes[idx] = code - } - } - - if d.setMap != nil && setSource == getSourceSet { - for k, _ := range d.setMap { - if !strings.HasPrefix(k, prefix) || strings.HasPrefix(k, prefix+"#") { - continue - } - parts := strings.Split(k[len(prefix):], ".") - idx := parts[0] - if _, ok := codes[idx]; ok { - continue - } - - code, err := strconv.Atoi(strings.Replace(parts[0], "~", "-", -1)) - if err != nil { - panic(fmt.Sprintf("unable to convert %s to int: %v", idx, err)) - } - codes[idx] = code - } - } - - if d.diff != nil && sourceDiff { - for k, _ := range d.diff.Attributes { - if !strings.HasPrefix(k, prefix) || strings.HasPrefix(k, prefix+"#") { - continue - } - parts := strings.Split(k[len(prefix):], ".") - idx := parts[0] - if _, ok := codes[idx]; ok { - continue - } - - code, err := strconv.Atoi(strings.Replace(parts[0], "~", "-", -1)) - if err != nil { - panic(fmt.Sprintf("unable to convert %s to int: %v", idx, err)) - } - codes[idx] = code - } - } - - if len(codes) > 0 { - break - } - } - - if indexMap == nil { - s.m = make(map[int]interface{}) - for idx, code := range codes { - switch t := schema.Elem.(type) { - case *Resource: - // Get the entire object - m := make(map[string]interface{}) - for field, _ := range t.Schema { - m[field] = d.getObject(prefix+idx, []string{field}, t.Schema, source).Value - } - s.m[code] = m - result.Exists = true - case *Schema: - // Get a single value - s.m[code] = d.get(prefix+idx, nil, t, source).Value - result.Exists = true - } - } - } - - if len(parts) > 0 { - // We still have parts left over meaning we're accessing an - // element of this set. - idx := parts[0] - parts = parts[1:] - - // Special case if we're accessing the count of the set - if idx == "#" { - schema := &Schema{Type: TypeInt} - return d.get(prefix+"#", parts, schema, source) - } - - if source&getSourceLevelMask == getSourceConfig { - i, err := strconv.Atoi(strings.Replace(idx, "~", "-", -1)) - if err != nil { - panic(fmt.Sprintf("unable to convert %s to int: %v", idx, err)) - } - if i, ok := indexMap[i]; ok { - idx = strconv.Itoa(i) - } - } - - switch t := schema.Elem.(type) { - case *Resource: - return d.getObject(prefix+idx, parts, t.Schema, source) - case *Schema: - return d.get(prefix+idx, parts, t, source) - } - } - - return result -} - -func (d *ResourceData) getMap( - k string, - parts []string, - schema *Schema, - source getSource) getResult { - elemSchema := &Schema{Type: TypeString} - - result := make(map[string]interface{}) - resultSet := false - prefix := k + "." - - flags := source & ^getSourceLevelMask - level := source & getSourceLevelMask - exact := flags&getSourceExact != 0 - diff := flags&getSourceDiff != 0 - - if !exact || level == getSourceState { - if d.state != nil && level >= getSourceState { - for k, _ := range d.state.Attributes { - if !strings.HasPrefix(k, prefix) { - continue - } - - single := k[len(prefix):] - result[single] = d.getPrimitive(k, nil, elemSchema, source).Value - resultSet = true - } - } - } - - if d.config != nil && level == getSourceConfig { - // For config, we always set the result to exactly what was requested - if mraw, ok := d.config.Get(k); ok { - result = make(map[string]interface{}) - switch m := mraw.(type) { - case []interface{}: - for _, innerRaw := range m { - for k, v := range innerRaw.(map[string]interface{}) { - result[k] = v - } - } - - resultSet = true - case []map[string]interface{}: - for _, innerRaw := range m { - for k, v := range innerRaw { - result[k] = v - } - } - - resultSet = true - case map[string]interface{}: - result = m - resultSet = true - default: - panic(fmt.Sprintf("unknown type: %#v", mraw)) - } - } else { - result = nil - } - } - - if d.diff != nil && diff { - for k, v := range d.diff.Attributes { - if !strings.HasPrefix(k, prefix) { - continue - } - resultSet = true - - single := k[len(prefix):] - - if v.NewRemoved { - delete(result, single) - } else { - result[single] = d.getPrimitive(k, nil, elemSchema, source).Value - } - } - } - - if !exact || level == getSourceSet { - if d.setMap != nil && level >= getSourceSet { - cleared := false - if v, ok := d.setMap[k]; ok && v == "" { - // We've cleared the map - result = make(map[string]interface{}) - resultSet = true - } else { - for k, _ := range d.setMap { - if !strings.HasPrefix(k, prefix) { - continue - } - resultSet = true - - if !cleared { - // We clear the results if they are in the set map - result = make(map[string]interface{}) - cleared = true - } - - single := k[len(prefix):] - result[single] = d.getPrimitive( - k, nil, elemSchema, source).Value - } - } - } - } - - // If we're requesting a specific element, return that - var resultValue interface{} = result - if len(parts) > 0 { - resultValue = result[parts[0]] - } - - return getResult{ - Value: resultValue, - Exists: resultSet, - Schema: schema, - } -} - -func (d *ResourceData) getObject( - k string, - parts []string, - schema map[string]*Schema, - source getSource) getResult { - if len(parts) > 0 { - // We're requesting a specific key in an object - key := parts[0] - parts = parts[1:] - s, ok := schema[key] - if !ok { - return getResultEmpty - } - - if k != "" { - // If we're not at the root, then we need to append - // the key to get the full key path. - key = fmt.Sprintf("%s.%s", k, key) - } - - return d.get(key, parts, s, source) - } - - // Get the entire object - result := make(map[string]interface{}) - for field, _ := range schema { - result[field] = d.getObject(k, []string{field}, schema, source).Value - } - - return getResult{ - Value: result, - Exists: true, - Schema: &Schema{ - Elem: schema, - }, - } -} - -func (d *ResourceData) getList( - k string, - parts []string, - schema *Schema, - source getSource) getResult { - if len(parts) > 0 { - // We still have parts left over meaning we're accessing an - // element of this list. - idx := parts[0] - parts = parts[1:] - - // Special case if we're accessing the count of the list - if idx == "#" { - schema := &Schema{Type: TypeInt} - return d.get(k+".#", parts, schema, source) - } - - key := fmt.Sprintf("%s.%s", k, idx) - switch t := schema.Elem.(type) { - case *Resource: - return d.getObject(key, parts, t.Schema, source) - case *Schema: - return d.get(key, parts, t, source) - } - } - - // Get the entire list. - var result []interface{} - count := d.getList(k, []string{"#"}, schema, source) - if !count.Computed { - result = make([]interface{}, count.Value.(int)) - for i, _ := range result { - is := strconv.FormatInt(int64(i), 10) - result[i] = d.getList(k, []string{is}, schema, source).Value - } - } - - return getResult{ - Value: result, - Computed: count.Computed, - Exists: count.Exists, - Schema: schema, - } -} - -func (d *ResourceData) getPrimitive( - k string, - parts []string, - schema *Schema, - source getSource) getResult { - var result string - var resultProcessed interface{} - var resultComputed, resultSet bool - flags := source & ^getSourceLevelMask - source = source & getSourceLevelMask - exact := flags&getSourceExact != 0 - diff := flags&getSourceDiff != 0 - - if !exact || source == getSourceState { - if d.state != nil && source >= getSourceState { - result, resultSet = d.state.Attributes[k] - } - } - - // No exact check is needed here because config is always exact - if d.config != nil && source == getSourceConfig { - // For config, we always return the exact value - if v, ok := d.config.Get(k); ok { - if err := mapstructure.WeakDecode(v, &result); err != nil { - panic(err) - } - - resultSet = true - } else { - result = "" - resultSet = false - } - - // If it is computed, set that. - resultComputed = d.config.IsComputed(k) - } - - if d.diff != nil && diff { - attrD, ok := d.diff.Attributes[k] - if ok { - if !attrD.NewComputed { - result = attrD.New - if attrD.NewExtra != nil { - // If NewExtra != nil, then we have processed data as the New, - // so we store that but decode the unprocessed data into result - resultProcessed = result - - err := mapstructure.WeakDecode(attrD.NewExtra, &result) - if err != nil { - panic(err) - } - } - - resultSet = true - } else { - result = "" - resultSet = false - } - } - } - - if !exact || source == getSourceSet { - if d.setMap != nil && source >= getSourceSet { - if v, ok := d.setMap[k]; ok { - result = v - resultSet = true - } - } - } - - if !resultSet { - result = "" - } - - var resultValue interface{} - switch schema.Type { - case TypeBool: - if result == "" { - resultValue = false - break - } - - v, err := strconv.ParseBool(result) - if err != nil { - panic(err) - } - - resultValue = v - case TypeString: - // Use the value as-is. We just put this case here to be explicit. - resultValue = result - case TypeInt: - if result == "" { - resultValue = 0 - break - } - - if resultComputed { - break - } - - v, err := strconv.ParseInt(result, 0, 0) - if err != nil { - panic(err) - } - - resultValue = int(v) - default: - panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) - } - - return getResult{ - Value: resultValue, - ValueProcessed: resultProcessed, - Computed: resultComputed, - Exists: resultSet, - Schema: schema, - } -} - func (d *ResourceData) set( k string, parts []string, @@ -1195,7 +696,7 @@ func (d *ResourceData) stateList( func (d *ResourceData) stateMap( prefix string, schema *Schema) map[string]string { - v := d.getMap(prefix, nil, schema, d.stateSource(prefix)) + v := d.get(prefix, nil, schema, d.stateSource(prefix)) if !v.Exists { return nil } diff --git a/helper/schema/resource_data_test.go b/helper/schema/resource_data_test.go index 3025a2bed..a76218e39 100644 --- a/helper/schema/resource_data_test.go +++ b/helper/schema/resource_data_test.go @@ -1391,7 +1391,7 @@ func TestResourceDataState(t *testing.T) { Result *terraform.InstanceState Partial []string }{ - // Basic primitive in diff + // #0 Basic primitive in diff { Schema: map[string]*Schema{ "availability_zone": &Schema{ @@ -1421,7 +1421,7 @@ func TestResourceDataState(t *testing.T) { }, }, - // Basic primitive set override + // #1 Basic primitive set override { Schema: map[string]*Schema{ "availability_zone": &Schema{ @@ -1455,6 +1455,7 @@ func TestResourceDataState(t *testing.T) { }, }, + // #2 { Schema: map[string]*Schema{ "vpc": &Schema{ @@ -1478,7 +1479,7 @@ func TestResourceDataState(t *testing.T) { }, }, - // Basic primitive with StateFunc set + // #3 Basic primitive with StateFunc set { Schema: map[string]*Schema{ "availability_zone": &Schema{ @@ -1508,7 +1509,7 @@ func TestResourceDataState(t *testing.T) { }, }, - // List + // #4 List { Schema: map[string]*Schema{ "ports": &Schema{ @@ -1547,7 +1548,7 @@ func TestResourceDataState(t *testing.T) { }, }, - // List of resources + // #5 List of resources { Schema: map[string]*Schema{ "ingress": &Schema{ @@ -1597,7 +1598,7 @@ func TestResourceDataState(t *testing.T) { }, }, - // List of maps + // #6 List of maps { Schema: map[string]*Schema{ "config_vars": &Schema{ @@ -1647,7 +1648,7 @@ func TestResourceDataState(t *testing.T) { }, }, - // List of maps with removal in diff + // #7 List of maps with removal in diff { Schema: map[string]*Schema{ "config_vars": &Schema{ @@ -1687,7 +1688,7 @@ func TestResourceDataState(t *testing.T) { }, }, - // Basic state with other keys + // #8 Basic state with other keys { Schema: map[string]*Schema{ "availability_zone": &Schema{ @@ -1724,7 +1725,7 @@ func TestResourceDataState(t *testing.T) { }, }, - // Sets + // #9 Sets { Schema: map[string]*Schema{ "ports": &Schema{ @@ -1759,6 +1760,7 @@ func TestResourceDataState(t *testing.T) { }, }, + // #10 { Schema: map[string]*Schema{ "ports": &Schema{ @@ -1789,6 +1791,7 @@ func TestResourceDataState(t *testing.T) { }, }, + // #11 { Schema: map[string]*Schema{ "ports": &Schema{ @@ -1861,7 +1864,7 @@ func TestResourceDataState(t *testing.T) { * PARTIAL STATES */ - // Basic primitive + // #12 Basic primitive { Schema: map[string]*Schema{ "availability_zone": &Schema{ @@ -1891,7 +1894,7 @@ func TestResourceDataState(t *testing.T) { }, }, - // List + // #13 List { Schema: map[string]*Schema{ "ports": &Schema{ @@ -1931,6 +1934,7 @@ func TestResourceDataState(t *testing.T) { }, }, + // #14 { Schema: map[string]*Schema{ "ports": &Schema{ @@ -1965,7 +1969,7 @@ func TestResourceDataState(t *testing.T) { }, }, - // List of resources + // #15 List of resources { Schema: map[string]*Schema{ "ingress": &Schema{ @@ -2016,7 +2020,7 @@ func TestResourceDataState(t *testing.T) { }, }, - // List of maps + // #16 List of maps { Schema: map[string]*Schema{ "config_vars": &Schema{ @@ -2070,7 +2074,7 @@ func TestResourceDataState(t *testing.T) { }, }, - // Sets + // #17 Sets { Schema: map[string]*Schema{ "ports": &Schema{ @@ -2113,6 +2117,7 @@ func TestResourceDataState(t *testing.T) { }, }, + // #18 { Schema: map[string]*Schema{ "ports": &Schema{ @@ -2146,7 +2151,7 @@ func TestResourceDataState(t *testing.T) { }, }, - // Maps + // #19 Maps { Schema: map[string]*Schema{ "tags": &Schema{ @@ -2174,6 +2179,7 @@ func TestResourceDataState(t *testing.T) { }, }, + // #20 { Schema: map[string]*Schema{ "tags": &Schema{ From e9a4aaaca767f3192c7a5855a47d8b7ce09be14d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 9 Jan 2015 17:43:44 -0800 Subject: [PATCH 15/23] helper/schema: full object test for addrToSchema --- helper/schema/field_reader.go | 3 ++- helper/schema/field_reader_multi.go | 3 --- helper/schema/field_reader_test.go | 11 +++++++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/helper/schema/field_reader.go b/helper/schema/field_reader.go index 136a7be0d..4c3b28b9a 100644 --- a/helper/schema/field_reader.go +++ b/helper/schema/field_reader.go @@ -58,7 +58,8 @@ func addrToSchema(addr []string, schemaMap map[string]*Schema) []*Schema { Elem: schemaMap, } - // TODO: test + // If we aren't given an address, then the user is requesting the + // full object, so we return the special value which is the full object. if len(addr) == 0 { return []*Schema{current} } diff --git a/helper/schema/field_reader_multi.go b/helper/schema/field_reader_multi.go index 2e5fa0ae4..89ad3a86f 100644 --- a/helper/schema/field_reader_multi.go +++ b/helper/schema/field_reader_multi.go @@ -42,10 +42,7 @@ func (r *MultiLevelFieldReader) ReadFieldMerge( var result FieldReadResult for _, l := range r.Levels { if r, ok := r.Readers[l]; ok { - println(fmt.Sprintf("GET %#v %s %#v", address, l, r)) out, err := r.ReadField(address) - println(fmt.Sprintf("%#v", out)) - println("======================================") if err != nil { return FieldReadResult{}, fmt.Errorf( "Error reading level %s: %s", l, err) diff --git a/helper/schema/field_reader_test.go b/helper/schema/field_reader_test.go index 64d2c3f2c..200c9ca62 100644 --- a/helper/schema/field_reader_test.go +++ b/helper/schema/field_reader_test.go @@ -11,6 +11,17 @@ func TestAddrToSchema(t *testing.T) { Schema map[string]*Schema Result []ValueType }{ + "full object": { + []string{}, + map[string]*Schema{ + "list": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + }, + }, + []ValueType{typeObject}, + }, + "list": { []string{"list"}, map[string]*Schema{ From 03c6453a72f5c2a0dfc0472fb121e881be2826c1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 10 Jan 2015 11:44:26 -0800 Subject: [PATCH 16/23] helper/schema: FieldWriter, replace Set --- helper/schema/field_writer.go | 8 + helper/schema/field_writer_map.go | 303 +++++++++++++++++++++++++ helper/schema/field_writer_map_test.go | 184 +++++++++++++++ helper/schema/resource_data.go | 281 +---------------------- 4 files changed, 499 insertions(+), 277 deletions(-) create mode 100644 helper/schema/field_writer.go create mode 100644 helper/schema/field_writer_map.go create mode 100644 helper/schema/field_writer_map_test.go diff --git a/helper/schema/field_writer.go b/helper/schema/field_writer.go new file mode 100644 index 000000000..9abc41b54 --- /dev/null +++ b/helper/schema/field_writer.go @@ -0,0 +1,8 @@ +package schema + +// FieldWriters are responsible for writing fields by address into +// a proper typed representation. ResourceData uses this to write new data +// into existing sources. +type FieldWriter interface { + WriteField([]string, interface{}) error +} diff --git a/helper/schema/field_writer_map.go b/helper/schema/field_writer_map.go new file mode 100644 index 000000000..fd6796377 --- /dev/null +++ b/helper/schema/field_writer_map.go @@ -0,0 +1,303 @@ +package schema + +import ( + "fmt" + "reflect" + "strconv" + "strings" + "sync" + + "github.com/mitchellh/mapstructure" +) + +// MapFieldWriter writes data into a single map[string]string structure. +type MapFieldWriter struct { + Schema map[string]*Schema + + lock sync.Mutex + result map[string]string +} + +// Map returns the underlying map that is being written to. +func (w *MapFieldWriter) Map() map[string]string { + w.lock.Lock() + defer w.lock.Unlock() + if w.result == nil { + w.result = make(map[string]string) + } + + return w.result +} + +func (w *MapFieldWriter) WriteField(addr []string, value interface{}) error { + w.lock.Lock() + defer w.lock.Unlock() + if w.result == nil { + w.result = make(map[string]string) + } + + schemaList := addrToSchema(addr, w.Schema) + if len(schemaList) == 0 { + return fmt.Errorf("Invalid address to set: %#v", addr) + } + + // If we're setting anything other than a list root or set root, + // then disallow it. + for _, schema := range schemaList[:len(schemaList)-1] { + if schema.Type == TypeList { + return fmt.Errorf( + "%s: can only set full list", + strings.Join(addr, ".")) + } + + if schema.Type == TypeMap { + return fmt.Errorf( + "%s: can only set full map", + strings.Join(addr, ".")) + } + + if schema.Type == TypeSet { + return fmt.Errorf( + "%s: can only set full set", + strings.Join(addr, ".")) + } + } + + return w.set(addr, value) +} + +func (w *MapFieldWriter) set(addr []string, value interface{}) error { + schemaList := addrToSchema(addr, w.Schema) + if len(schemaList) == 0 { + return fmt.Errorf("Invalid address to set: %#v", addr) + } + + schema := schemaList[len(schemaList)-1] + switch schema.Type { + case TypeBool: + fallthrough + case TypeInt: + fallthrough + case TypeString: + return w.setPrimitive(addr, value, schema) + case TypeList: + return w.setList(addr, value, schema) + case TypeMap: + return w.setMap(addr, value, schema) + case TypeSet: + return w.setSet(addr, value, schema) + case typeObject: + return w.setObject(addr, value, schema) + default: + panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) + } +} + +func (w *MapFieldWriter) setList( + addr []string, + v interface{}, + schema *Schema) error { + k := strings.Join(addr, ".") + setElement := func(idx string, value interface{}) error { + addrCopy := make([]string, len(addr), len(addr)+1) + copy(addrCopy, addr) + return w.set(append(addrCopy, idx), value) + } + + var vs []interface{} + if err := mapstructure.Decode(v, &vs); err != nil { + return fmt.Errorf("%s: %s", k, err) + } + + // Set the entire list. + var err error + for i, elem := range vs { + is := strconv.FormatInt(int64(i), 10) + err = setElement(is, elem) + if err != nil { + break + } + } + if err != nil { + for i, _ := range vs { + is := strconv.FormatInt(int64(i), 10) + setElement(is, nil) + } + + return err + } + + w.result[k+".#"] = strconv.FormatInt(int64(len(vs)), 10) + return nil +} + +func (w *MapFieldWriter) setMap( + addr []string, + value interface{}, + schema *Schema) error { + k := strings.Join(addr, ".") + v := reflect.ValueOf(value) + vs := make(map[string]interface{}) + + if value != nil { + if v.Kind() != reflect.Map { + return fmt.Errorf("%s: must be a map", k) + } + if v.Type().Key().Kind() != reflect.String { + return fmt.Errorf("%s: keys must strings", k) + } + for _, mk := range v.MapKeys() { + mv := v.MapIndex(mk) + vs[mk.String()] = mv.Interface() + } + } + + if len(vs) == 0 { + // The empty string here means the map is removed. + w.result[k] = "" + return nil + } + + // Remove the pure key since we're setting the full map value + delete(w.result, k) + + // Set each subkey + addrCopy := make([]string, len(addr), len(addr)+1) + copy(addrCopy, addr) + for subKey, v := range vs { + if err := w.set(append(addrCopy, subKey), v); err != nil { + return err + } + } + + return nil +} + +func (w *MapFieldWriter) setObject( + addr []string, + value interface{}, + schema *Schema) error { + // Set the entire object. First decode into a proper structure + var v map[string]interface{} + if err := mapstructure.Decode(value, &v); err != nil { + return fmt.Errorf("%s: %s", strings.Join(addr, "."), err) + } + + // Make space for additional elements in the address + addrCopy := make([]string, len(addr), len(addr)+1) + copy(addrCopy, addr) + + // Set each element in turn + var err error + for k1, v1 := range v { + if err = w.set(append(addrCopy, k1), v1); err != nil { + break + } + } + if err != nil { + for k1, _ := range v { + w.set(append(addrCopy, k1), nil) + } + } + + return err +} + +func (w *MapFieldWriter) setPrimitive( + addr []string, + v interface{}, + schema *Schema) error { + k := strings.Join(addr, ".") + + if v == nil { + delete(w.result, k) + return nil + } + + var set string + switch schema.Type { + case TypeBool: + var b bool + if err := mapstructure.Decode(v, &b); err != nil { + return fmt.Errorf("%s: %s", k, err) + } + + set = strconv.FormatBool(b) + case TypeString: + if err := mapstructure.Decode(v, &set); err != nil { + return fmt.Errorf("%s: %s", k, err) + } + case TypeInt: + var n int + if err := mapstructure.Decode(v, &n); err != nil { + return fmt.Errorf("%s: %s", k, err) + } + + set = strconv.FormatInt(int64(n), 10) + default: + return fmt.Errorf("Unknown type: %#v", schema.Type) + } + + w.result[k] = set + return nil +} + +func (w *MapFieldWriter) setSet( + addr []string, + value interface{}, + schema *Schema) error { + addrCopy := make([]string, len(addr), len(addr)+1) + copy(addrCopy, addr) + + // If it is a slice, then we have to turn it into a *Set so that + // we get the proper order back based on the hash code. + if v := reflect.ValueOf(value); v.Kind() == reflect.Slice { + // Build a temp *ResourceData to use for the conversion + tempSchema := *schema + tempSchema.Type = TypeList + tempSchemaMap := map[string]*Schema{addr[0]: &tempSchema} + tempW := &MapFieldWriter{Schema: tempSchemaMap} + + // Set the entire list, this lets us get sane values out of it + if err := tempW.WriteField(addr, value); err != nil { + return err + } + + // Build the set by going over the list items in order and + // hashing them into the set. The reason we go over the list and + // not the `value` directly is because this forces all types + // to become []interface{} (generic) instead of []string, which + // most hash functions are expecting. + s := &Set{F: schema.Set} + tempR := &MapFieldReader{ + Map: BasicMapReader(tempW.Map()), + Schema: tempSchemaMap, + } + for i := 0; i < v.Len(); i++ { + is := strconv.FormatInt(int64(i), 10) + result, err := tempR.ReadField(append(addrCopy, is)) + if err != nil { + return err + } + if !result.Exists { + panic("set item just set doesn't exist") + } + + s.Add(result.Value) + } + + value = s + } + + k := strings.Join(addr, ".") + for code, elem := range value.(*Set).m { + codeStr := strconv.FormatInt(int64(code), 10) + if err := w.set(append(addrCopy, codeStr), elem); err != nil { + return err + } + } + + w.result[k+".#"] = strconv.Itoa(value.(*Set).Len()) + return nil + +} diff --git a/helper/schema/field_writer_map_test.go b/helper/schema/field_writer_map_test.go new file mode 100644 index 000000000..38eaca94b --- /dev/null +++ b/helper/schema/field_writer_map_test.go @@ -0,0 +1,184 @@ +package schema + +import ( + "reflect" + "testing" +) + +func TestMapFieldWriter_impl(t *testing.T) { + var _ FieldWriter = new(MapFieldWriter) +} + +func TestMapFieldWriter(t *testing.T) { + schema := map[string]*Schema{ + "bool": &Schema{Type: TypeBool}, + "int": &Schema{Type: TypeInt}, + "string": &Schema{Type: TypeString}, + "list": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeString}, + }, + "listInt": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + }, + "map": &Schema{Type: TypeMap}, + "set": &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + "setDeep": &Schema{ + Type: TypeSet, + Elem: &Resource{ + Schema: map[string]*Schema{ + "index": &Schema{Type: TypeInt}, + "value": &Schema{Type: TypeString}, + }, + }, + Set: func(a interface{}) int { + return a.(map[string]interface{})["index"].(int) + }, + }, + } + + cases := map[string]struct { + Addr []string + Value interface{} + Err bool + Out map[string]string + }{ + "noexist": { + []string{"noexist"}, + 42, + true, + map[string]string{}, + }, + + "bool": { + []string{"bool"}, + false, + false, + map[string]string{ + "bool": "false", + }, + }, + + "int": { + []string{"int"}, + 42, + false, + map[string]string{ + "int": "42", + }, + }, + + "string": { + []string{"string"}, + "42", + false, + map[string]string{ + "string": "42", + }, + }, + + "list of strings": { + []string{"list"}, + []interface{}{"foo", "bar"}, + false, + map[string]string{ + "list.#": "2", + "list.0": "foo", + "list.1": "bar", + }, + }, + + "list element": { + []string{"list", "0"}, + "string", + true, + map[string]string{}, + }, + + "map": { + []string{"map"}, + map[string]interface{}{"foo": "bar"}, + false, + map[string]string{ + "map.foo": "bar", + }, + }, + + "map delete": { + []string{"map"}, + nil, + false, + map[string]string{ + "map": "", + }, + }, + + "map element": { + []string{"map", "foo"}, + "bar", + true, + map[string]string{}, + }, + + "set": { + []string{"set"}, + []interface{}{1, 2, 5}, + false, + map[string]string{ + "set.#": "3", + "set.1": "1", + "set.2": "2", + "set.5": "5", + }, + }, + + "set resource": { + []string{"setDeep"}, + []interface{}{ + map[string]interface{}{ + "index": 10, + "value": "foo", + }, + map[string]interface{}{ + "index": 50, + "value": "bar", + }, + }, + false, + map[string]string{ + "setDeep.#": "2", + "setDeep.10.index": "10", + "setDeep.10.value": "foo", + "setDeep.50.index": "50", + "setDeep.50.value": "bar", + }, + }, + + "set element": { + []string{"set", "5"}, + 5, + true, + map[string]string{}, + }, + } + + for name, tc := range cases { + w := &MapFieldWriter{Schema: schema} + err := w.WriteField(tc.Addr, tc.Value) + if (err != nil) != tc.Err { + t.Fatalf("%s: err: %s", name, err) + } + + actual := w.Map() + if !reflect.DeepEqual(actual, tc.Out) { + t.Fatalf("%s: bad: %#v", name, actual) + } + } +} diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go index 9afd1daa6..0889d294f 100644 --- a/helper/schema/resource_data.go +++ b/helper/schema/resource_data.go @@ -8,7 +8,6 @@ import ( "sync" "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/mapstructure" ) // ResourceData is used to query and set the attributes of a resource. @@ -29,7 +28,7 @@ type ResourceData struct { // Don't set multiReader *MultiLevelFieldReader - setMap map[string]string + setWriter *MapFieldWriter newState *terraform.InstanceState partial bool partialMap map[string]struct{} @@ -156,8 +155,7 @@ func (d *ResourceData) Partial(on bool) { // will be returned. func (d *ResourceData) Set(key string, value interface{}) error { d.once.Do(d.init) - parts := strings.Split(key, ".") - return d.setObject("", parts, d.schema, value) + return d.setWriter.WriteField(strings.Split(key, "."), value) } // SetPartial adds the key prefix to the final state output while @@ -243,7 +241,7 @@ func (d *ResourceData) init() { d.newState = ©State // Initialize the map for storing set data - d.setMap = make(map[string]string) + d.setWriter = &MapFieldWriter{Schema: d.schema} // Initialize the reader for getting data from the // underlying sources (config, diff, etc.) @@ -274,7 +272,7 @@ func (d *ResourceData) init() { } readers["set"] = &MapFieldReader{ Schema: d.schema, - Map: BasicMapReader(d.setMap), + Map: BasicMapReader(d.setWriter.Map()), } d.multiReader = &MultiLevelFieldReader{ Levels: []string{ @@ -383,277 +381,6 @@ func (d *ResourceData) get( } } -func (d *ResourceData) set( - k string, - parts []string, - schema *Schema, - value interface{}) error { - switch schema.Type { - case TypeList: - return d.setList(k, parts, schema, value) - case TypeMap: - return d.setMapValue(k, parts, schema, value) - case TypeSet: - return d.setSet(k, parts, schema, value) - case TypeBool: - fallthrough - case TypeInt: - fallthrough - case TypeString: - return d.setPrimitive(k, schema, value) - default: - panic(fmt.Sprintf("%s: unknown type %#v", k, schema.Type)) - } -} - -func (d *ResourceData) setList( - k string, - parts []string, - schema *Schema, - value interface{}) error { - if len(parts) > 0 { - return fmt.Errorf("%s: can only set the full list, not elements", k) - } - - setElement := func(k string, idx string, value interface{}) error { - if idx == "#" { - return fmt.Errorf("%s: can't set count of list", k) - } - - key := fmt.Sprintf("%s.%s", k, idx) - switch t := schema.Elem.(type) { - case *Resource: - return d.setObject(key, nil, t.Schema, value) - case *Schema: - return d.set(key, nil, t, value) - } - - return nil - } - - var vs []interface{} - if err := mapstructure.Decode(value, &vs); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - - // Set the entire list. - var err error - for i, elem := range vs { - is := strconv.FormatInt(int64(i), 10) - err = setElement(k, is, elem) - if err != nil { - break - } - } - if err != nil { - for i, _ := range vs { - is := strconv.FormatInt(int64(i), 10) - setElement(k, is, nil) - } - - return err - } - - d.setMap[k+".#"] = strconv.FormatInt(int64(len(vs)), 10) - return nil -} - -func (d *ResourceData) setMapValue( - k string, - parts []string, - schema *Schema, - value interface{}) error { - elemSchema := &Schema{Type: TypeString} - if len(parts) > 0 { - return fmt.Errorf("%s: full map must be set, no a single element", k) - } - - v := reflect.ValueOf(value) - if v.Kind() != reflect.Map { - return fmt.Errorf("%s: must be a map", k) - } - if v.Type().Key().Kind() != reflect.String { - return fmt.Errorf("%s: keys must strings", k) - } - vs := make(map[string]interface{}) - for _, mk := range v.MapKeys() { - mv := v.MapIndex(mk) - vs[mk.String()] = mv.Interface() - } - - if len(vs) == 0 { - // The empty string here means the map is removed. - d.setMap[k] = "" - return nil - } - - delete(d.setMap, k) - for subKey, v := range vs { - err := d.set(fmt.Sprintf("%s.%s", k, subKey), nil, elemSchema, v) - if err != nil { - return err - } - } - - return nil -} - -func (d *ResourceData) setObject( - k string, - parts []string, - schema map[string]*Schema, - value interface{}) error { - if len(parts) > 0 { - // We're setting a specific key in an object - key := parts[0] - parts = parts[1:] - - s, ok := schema[key] - if !ok { - return fmt.Errorf("%s (internal): unknown key to set: %s", k, key) - } - - if k != "" { - // If we're not at the root, then we need to append - // the key to get the full key path. - key = fmt.Sprintf("%s.%s", k, key) - } - - return d.set(key, parts, s, value) - } - - // Set the entire object. First decode into a proper structure - var v map[string]interface{} - if err := mapstructure.Decode(value, &v); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - - // Set each element in turn - var err error - for k1, v1 := range v { - err = d.setObject(k, []string{k1}, schema, v1) - if err != nil { - break - } - } - if err != nil { - for k1, _ := range v { - d.setObject(k, []string{k1}, schema, nil) - } - } - - return err -} - -func (d *ResourceData) setPrimitive( - k string, - schema *Schema, - v interface{}) error { - if v == nil { - delete(d.setMap, k) - return nil - } - - var set string - switch schema.Type { - case TypeBool: - var b bool - if err := mapstructure.Decode(v, &b); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - - set = strconv.FormatBool(b) - case TypeString: - if err := mapstructure.Decode(v, &set); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - case TypeInt: - var n int - if err := mapstructure.Decode(v, &n); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - - set = strconv.FormatInt(int64(n), 10) - default: - return fmt.Errorf("Unknown type: %#v", schema.Type) - } - - d.setMap[k] = set - return nil -} - -func (d *ResourceData) setSet( - k string, - parts []string, - schema *Schema, - value interface{}) error { - if len(parts) > 0 { - return fmt.Errorf("%s: can only set the full set, not elements", k) - } - - // If it is a slice, then we have to turn it into a *Set so that - // we get the proper order back based on the hash code. - if v := reflect.ValueOf(value); v.Kind() == reflect.Slice { - // Build a temp *ResourceData to use for the conversion - tempD := &ResourceData{ - setMap: make(map[string]string), - schema: map[string]*Schema{k: schema}, - } - tempD.once.Do(tempD.init) - - // Set the entire list, this lets us get sane values out of it - if err := tempD.setList(k, nil, schema, value); err != nil { - return err - } - - // Build the set by going over the list items in order and - // hashing them into the set. The reason we go over the list and - // not the `value` directly is because this forces all types - // to become []interface{} (generic) instead of []string, which - // most hash functions are expecting. - s := &Set{F: schema.Set} - source := getSourceSet | getSourceExact - for i := 0; i < v.Len(); i++ { - is := strconv.FormatInt(int64(i), 10) - result := tempD.get(k, []string{is}, schema, source) - if !result.Exists { - panic("just set item doesn't exist") - } - - s.Add(result.Value) - } - - value = s - } - - switch t := schema.Elem.(type) { - case *Resource: - for code, elem := range value.(*Set).m { - for field, _ := range t.Schema { - subK := fmt.Sprintf("%s.%d", k, code) - value := elem.(map[string]interface{})[field] - err := d.setObject(subK, []string{field}, t.Schema, value) - if err != nil { - return err - } - } - } - case *Schema: - for code, elem := range value.(*Set).m { - subK := fmt.Sprintf("%s.%d", k, code) - err := d.set(subK, nil, t, elem) - if err != nil { - return err - } - } - default: - return fmt.Errorf("%s: unknown element type (internal)", k) - } - - d.setMap[k+".#"] = strconv.Itoa(value.(*Set).Len()) - return nil -} - func (d *ResourceData) stateList( prefix string, schema *Schema) map[string]string { From e77b2b17c4aede27d13d5553fc56034d9444d4e4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 10 Jan 2015 11:46:09 -0800 Subject: [PATCH 17/23] helper/schema: remove unused method --- helper/schema/resource_data.go | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go index 0889d294f..10f9e8a77 100644 --- a/helper/schema/resource_data.go +++ b/helper/schema/resource_data.go @@ -114,25 +114,6 @@ func (d *ResourceData) HasChange(key string) bool { return !reflect.DeepEqual(o, n) } -// hasComputedSubKeys walks through a schema and returns whether or not the -// given key contains any subkeys that are computed. -func (d *ResourceData) hasComputedSubKeys(key string, schema *Schema) bool { - prefix := key + "." - - switch t := schema.Elem.(type) { - case *Resource: - for k, schema := range t.Schema { - if d.config.IsComputed(prefix + k) { - return true - } - if d.hasComputedSubKeys(prefix+k, schema) { - return true - } - } - } - return false -} - // Partial turns partial state mode on/off. // // When partial state mode is enabled, then only key prefixes specified From f64b09a045bc12c50447b0127a04f86d37d37ac8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 10 Jan 2015 11:49:37 -0800 Subject: [PATCH 18/23] helper/schema: more tests --- helper/schema/field_writer_map_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/helper/schema/field_writer_map_test.go b/helper/schema/field_writer_map_test.go index 38eaca94b..fa2e2fa2b 100644 --- a/helper/schema/field_writer_map_test.go +++ b/helper/schema/field_writer_map_test.go @@ -167,6 +167,21 @@ func TestMapFieldWriter(t *testing.T) { true, map[string]string{}, }, + + "full object": { + nil, + map[string]interface{}{ + "string": "foo", + "list": []interface{}{"foo", "bar"}, + }, + false, + map[string]string{ + "string": "foo", + "list.#": "2", + "list.0": "foo", + "list.1": "bar", + }, + }, } for name, tc := range cases { From 3c1b55a75fb892ea7255236387acb52478e68a59 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 10 Jan 2015 12:18:32 -0800 Subject: [PATCH 19/23] helper/schema: use the field reader/writer for state --- helper/schema/resource_data.go | 229 +++++----------------------- helper/schema/resource_data_test.go | 6 +- helper/schema/schema.go | 2 + 3 files changed, 48 insertions(+), 189 deletions(-) diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go index 10f9e8a77..bdabd684f 100644 --- a/helper/schema/resource_data.go +++ b/helper/schema/resource_data.go @@ -1,9 +1,7 @@ package schema import ( - "fmt" "reflect" - "strconv" "strings" "sync" @@ -139,8 +137,9 @@ func (d *ResourceData) Set(key string, value interface{}) error { return d.setWriter.WriteField(strings.Split(key, "."), value) } -// SetPartial adds the key prefix to the final state output while -// in partial state mode. +// SetPartial adds the key to the final state output while +// in partial state mode. The key must be a root key in the schema (i.e. +// it cannot be "list.0"). // // If partial state mode is disabled, then this has no effect. Additionally, // whenever partial state mode is toggled, the partial data is cleared. @@ -203,9 +202,46 @@ func (d *ResourceData) State() *terraform.InstanceState { return nil } - result.Attributes = d.stateObject("", d.schema) + // In order to build the final state attributes, we read the full + // attribute set as a map[string]interface{}, write it to a MapFieldWriter, + // and then use that map. + rawMap := make(map[string]interface{}) + for k, _ := range d.schema { + source := getSourceSet + if d.partial { + source = getSourceState + if _, ok := d.partialMap[k]; ok { + source = getSourceSet + } + } + + raw := d.get(k, nil, nil, source) + rawMap[k] = raw.Value + if raw.ValueProcessed != nil { + rawMap[k] = raw.ValueProcessed + } + } + mapW := &MapFieldWriter{Schema: d.schema} + if err := mapW.WriteField(nil, rawMap); err != nil { + return nil + } + + result.Attributes = mapW.Map() result.Ephemeral.ConnInfo = d.ConnInfo() + // TODO: This is hacky and we can remove this when we have a proper + // state writer. We should instead have a proper StateFieldWriter + // and use that. + for k, schema := range d.schema { + if schema.Type != TypeMap { + continue + } + + if result.Attributes[k] == "" { + delete(result.Attributes, k) + } + } + if v := d.Id(); v != "" { result.Attributes["id"] = d.Id() } @@ -361,186 +397,3 @@ func (d *ResourceData) get( Schema: schema, } } - -func (d *ResourceData) stateList( - prefix string, - schema *Schema) map[string]string { - countRaw := d.get(prefix, []string{"#"}, schema, d.stateSource(prefix)) - if !countRaw.Exists { - if schema.Computed { - // If it is computed, then it always _exists_ in the state, - // it is just empty. - countRaw.Exists = true - countRaw.Value = 0 - } else { - return nil - } - } - count := countRaw.Value.(int) - - result := make(map[string]string) - if count > 0 || schema.Computed { - result[prefix+".#"] = strconv.FormatInt(int64(count), 10) - } - for i := 0; i < count; i++ { - key := fmt.Sprintf("%s.%d", prefix, i) - - var m map[string]string - switch t := schema.Elem.(type) { - case *Resource: - m = d.stateObject(key, t.Schema) - case *Schema: - m = d.stateSingle(key, t) - } - - for k, v := range m { - result[k] = v - } - } - - return result -} - -func (d *ResourceData) stateMap( - prefix string, - schema *Schema) map[string]string { - v := d.get(prefix, nil, schema, d.stateSource(prefix)) - if !v.Exists { - return nil - } - - elemSchema := &Schema{Type: TypeString} - result := make(map[string]string) - for mk, _ := range v.Value.(map[string]interface{}) { - mp := fmt.Sprintf("%s.%s", prefix, mk) - for k, v := range d.stateSingle(mp, elemSchema) { - result[k] = v - } - } - - return result -} - -func (d *ResourceData) stateObject( - prefix string, - schema map[string]*Schema) map[string]string { - result := make(map[string]string) - for k, v := range schema { - key := k - if prefix != "" { - key = prefix + "." + key - } - - for k1, v1 := range d.stateSingle(key, v) { - result[k1] = v1 - } - } - - return result -} - -func (d *ResourceData) statePrimitive( - prefix string, - schema *Schema) map[string]string { - raw := d.getRaw(prefix, d.stateSource(prefix)) - if !raw.Exists { - return nil - } - - v := raw.Value - if raw.ValueProcessed != nil { - v = raw.ValueProcessed - } - - var vs string - switch schema.Type { - case TypeBool: - vs = strconv.FormatBool(v.(bool)) - case TypeString: - vs = v.(string) - case TypeInt: - vs = strconv.FormatInt(int64(v.(int)), 10) - default: - panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) - } - - return map[string]string{ - prefix: vs, - } -} - -func (d *ResourceData) stateSet( - prefix string, - schema *Schema) map[string]string { - raw := d.get(prefix, nil, schema, d.stateSource(prefix)) - if !raw.Exists { - if schema.Computed { - // If it is computed, then it always _exists_ in the state, - // it is just empty. - raw.Exists = true - raw.Value = new(Set) - } else { - return nil - } - } - - set := raw.Value.(*Set) - result := make(map[string]string) - result[prefix+".#"] = strconv.Itoa(set.Len()) - - for _, idx := range set.listCode() { - key := fmt.Sprintf("%s.%d", prefix, idx) - - var m map[string]string - switch t := schema.Elem.(type) { - case *Resource: - m = d.stateObject(key, t.Schema) - case *Schema: - m = d.stateSingle(key, t) - } - - for k, v := range m { - result[k] = v - } - } - - return result -} - -func (d *ResourceData) stateSingle( - prefix string, - schema *Schema) map[string]string { - switch schema.Type { - case TypeList: - return d.stateList(prefix, schema) - case TypeMap: - return d.stateMap(prefix, schema) - case TypeSet: - return d.stateSet(prefix, schema) - case TypeBool: - fallthrough - case TypeInt: - fallthrough - case TypeString: - return d.statePrimitive(prefix, schema) - default: - panic(fmt.Sprintf("%s: unknown type %#v", prefix, schema.Type)) - } -} - -func (d *ResourceData) stateSource(prefix string) getSource { - // If we're not doing a partial apply, then get the set level - if !d.partial { - return getSourceSet | getSourceDiff - } - - // Otherwise, only return getSourceSet if its in the partial map. - // Otherwise we use state level only. - for k, _ := range d.partialMap { - if strings.HasPrefix(prefix, k) { - return getSourceSet | getSourceDiff - } - } - - return getSourceState -} diff --git a/helper/schema/resource_data_test.go b/helper/schema/resource_data_test.go index a76218e39..6b9426680 100644 --- a/helper/schema/resource_data_test.go +++ b/helper/schema/resource_data_test.go @@ -1853,7 +1853,9 @@ func TestResourceDataState(t *testing.T) { "ports.10.order": "10", "ports.10.a.#": "1", "ports.10.a.0": "80", + "ports.10.b.#": "0", "ports.20.order": "20", + "ports.20.a.#": "0", "ports.20.b.#": "1", "ports.20.b.0": "100", }, @@ -1890,7 +1892,9 @@ func TestResourceDataState(t *testing.T) { Partial: []string{}, Result: &terraform.InstanceState{ - Attributes: map[string]string{}, + Attributes: map[string]string{ + "availability_zone": "", + }, }, }, diff --git a/helper/schema/schema.go b/helper/schema/schema.go index 218de7851..85bc9207e 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -53,6 +53,8 @@ func (t ValueType) Zero() interface{} { return map[string]interface{}{} case TypeSet: return nil + case typeObject: + return map[string]interface{}{} default: panic(fmt.Sprintf("unknown type %#v", t)) } From d89446391a8069895e5aedad442fc9d8736b6c01 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 10 Jan 2015 12:22:05 -0800 Subject: [PATCH 20/23] helper/schema: make the get API cleaner --- helper/schema/resource_data.go | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go index bdabd684f..0c67b823c 100644 --- a/helper/schema/resource_data.go +++ b/helper/schema/resource_data.go @@ -102,8 +102,7 @@ func (d *ResourceData) getRaw(key string, level getSource) getResult { parts = strings.Split(key, ".") } - schema := &Schema{Type: typeObject, Elem: d.schema} - return d.get("", parts, schema, level) + return d.get(parts, level) } // HasChange returns whether or not the given key has been changed. @@ -215,7 +214,7 @@ func (d *ResourceData) State() *terraform.InstanceState { } } - raw := d.get(k, nil, nil, source) + raw := d.get([]string{k}, source) rawMap[k] = raw.Value if raw.ValueProcessed != nil { rawMap[k] = raw.ValueProcessed @@ -328,17 +327,12 @@ func (d *ResourceData) getChange( parts2 = strings.Split(key, ".") } - schema := &Schema{Type: typeObject, Elem: d.schema} - o := d.get("", parts, schema, oldLevel) - n := d.get("", parts2, schema, newLevel) + o := d.get(parts, oldLevel) + n := d.get(parts2, newLevel) return o, n } -func (d *ResourceData) get( - k string, - parts []string, - schema *Schema, - source getSource) getResult { +func (d *ResourceData) get(addr []string, source getSource) getResult { d.once.Do(d.init) level := "set" @@ -357,11 +351,6 @@ func (d *ResourceData) get( } // Build the address of the key we're looking for and ask the FieldReader - var addr []string - if k != "" { - addr = strings.Split(k, ".") - } - addr = append(addr, parts...) for i, v := range addr { if v[0] == '~' { addr[i] = v[1:] @@ -394,6 +383,5 @@ func (d *ResourceData) get( ValueProcessed: result.ValueProcessed, Computed: result.Computed, Exists: result.Exists, - Schema: schema, } } From 9ab128899a8314db93591f52dc248500478ecbf8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 10 Jan 2015 12:25:34 -0800 Subject: [PATCH 21/23] helper/schema: make the getSource enum easier --- helper/schema/resource_data.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go index 0c67b823c..00d0fd3ae 100644 --- a/helper/schema/resource_data.go +++ b/helper/schema/resource_data.go @@ -41,11 +41,10 @@ type getSource byte const ( getSourceState getSource = 1 << iota getSourceConfig + getSourceDiff getSourceSet getSourceExact // Only get from the _exact_ level - getSourceDiff // Apply the diff on top our level - getSourceLevelMask getSource = getSourceState | getSourceConfig | getSourceSet - getSourceMax getSource = getSourceSet + getSourceLevelMask getSource = getSourceState | getSourceConfig | getSourceDiff | getSourceSet ) // getResult is the internal structure that is generated when a Get @@ -81,7 +80,7 @@ func (d *ResourceData) Get(key string) interface{} { // set and the new value is. This is common, for example, for boolean // fields which have a zero value of false. func (d *ResourceData) GetChange(key string) (interface{}, interface{}) { - o, n := d.getChange(key, getSourceConfig, getSourceConfig|getSourceDiff) + o, n := d.getChange(key, getSourceConfig, getSourceDiff) return o.Value, n.Value } @@ -92,7 +91,7 @@ func (d *ResourceData) GetChange(key string) (interface{}, interface{}) { // The first result will not necessarilly be nil if the value doesn't exist. // The second result should be checked to determine this information. func (d *ResourceData) GetOk(key string) (interface{}, bool) { - r := d.getRaw(key, getSourceSet|getSourceDiff) + r := d.getRaw(key, getSourceSet) return r.Value, r.Exists } @@ -337,12 +336,11 @@ func (d *ResourceData) get(addr []string, source getSource) getResult { level := "set" flags := source & ^getSourceLevelMask - diff := flags&getSourceDiff != 0 exact := flags&getSourceExact != 0 source = source & getSourceLevelMask if source >= getSourceSet { level = "set" - } else if diff { + } else if source >= getSourceDiff { level = "diff" } else if source >= getSourceConfig { level = "config" From 361d00347a7ffceda4daad080bf370fd2f409cba Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 10 Jan 2015 12:42:15 -0800 Subject: [PATCH 22/23] helper/schema: refactor tests for fieldreader to be common --- helper/schema/field_reader_config.go | 8 +- helper/schema/field_reader_config_test.go | 219 ++---------- helper/schema/field_reader_diff_test.go | 415 ++++++++-------------- helper/schema/field_reader_map_test.go | 204 ++--------- helper/schema/field_reader_test.go | 206 +++++++++++ 5 files changed, 413 insertions(+), 639 deletions(-) diff --git a/helper/schema/field_reader_config.go b/helper/schema/field_reader_config.go index 215c0957e..4c63bd07c 100644 --- a/helper/schema/field_reader_config.go +++ b/helper/schema/field_reader_config.go @@ -160,17 +160,17 @@ func (r *ConfigFieldReader) readPrimitive( func (r *ConfigFieldReader) readSet( address []string, schema *Schema) (FieldReadResult, map[int]int, error) { indexMap := make(map[int]int) + // Create the set that will be our result + set := &Set{F: schema.Set} + raw, err := readListField(&nestedConfigFieldReader{r}, address, schema) if err != nil { return FieldReadResult{}, indexMap, err } if !raw.Exists { - return FieldReadResult{}, indexMap, nil + return FieldReadResult{Value: set}, indexMap, nil } - // Create the set that will be our result - set := &Set{F: schema.Set} - // If the list is computed, the set is necessarilly computed if raw.Computed { return FieldReadResult{ diff --git a/helper/schema/field_reader_config_test.go b/helper/schema/field_reader_config_test.go index f194b3fa0..5eecbcdf2 100644 --- a/helper/schema/field_reader_config_test.go +++ b/helper/schema/field_reader_config_test.go @@ -1,7 +1,6 @@ package schema import ( - "reflect" "testing" "github.com/hashicorp/terraform/config" @@ -13,200 +12,38 @@ func TestConfigFieldReader_impl(t *testing.T) { } func TestConfigFieldReader(t *testing.T) { - r := &ConfigFieldReader{ - Schema: map[string]*Schema{ - "bool": &Schema{Type: TypeBool}, - "int": &Schema{Type: TypeInt}, - "string": &Schema{Type: TypeString}, - "list": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeString}, - }, - "listInt": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeInt}, - }, - "map": &Schema{Type: TypeMap}, - "set": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) + testFieldReader(t, func(s map[string]*Schema) FieldReader { + return &ConfigFieldReader{ + Schema: s, + + Config: testConfig(t, map[string]interface{}{ + "bool": true, + "int": 42, + "string": "string", + + "list": []interface{}{"foo", "bar"}, + + "listInt": []interface{}{21, 42}, + + "map": map[string]interface{}{ + "foo": "bar", + "bar": "baz", }, - }, - "setDeep": &Schema{ - Type: TypeSet, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{Type: TypeInt}, - "value": &Schema{Type: TypeString}, + + "set": []interface{}{10, 50}, + "setDeep": []interface{}{ + map[string]interface{}{ + "index": 10, + "value": "foo", + }, + map[string]interface{}{ + "index": 50, + "value": "bar", }, }, - Set: func(a interface{}) int { - return a.(map[string]interface{})["index"].(int) - }, - }, - }, - - Config: testConfig(t, map[string]interface{}{ - "bool": true, - "int": 42, - "string": "string", - - "list": []interface{}{"foo", "bar"}, - - "listInt": []interface{}{21, 42}, - - "map": map[string]interface{}{ - "foo": "bar", - "bar": "baz", - }, - - "set": []interface{}{10, 50}, - - "setDeep": []interface{}{ - map[string]interface{}{ - "index": 10, - "value": "foo", - }, - map[string]interface{}{ - "index": 50, - "value": "bar", - }, - }, - }), - } - - cases := map[string]struct { - Addr []string - Out interface{} - OutOk bool - OutComputed bool - OutErr bool - }{ - "noexist": { - []string{"boolNOPE"}, - nil, - false, - false, - false, - }, - - "bool": { - []string{"bool"}, - true, - true, - false, - false, - }, - - "int": { - []string{"int"}, - 42, - true, - false, - false, - }, - - "string": { - []string{"string"}, - "string", - true, - false, - false, - }, - - "list": { - []string{"list"}, - []interface{}{ - "foo", - "bar", - }, - true, - false, - false, - }, - - "listInt": { - []string{"listInt"}, - []interface{}{ - 21, - 42, - }, - true, - false, - false, - }, - - "map": { - []string{"map"}, - map[string]interface{}{ - "foo": "bar", - "bar": "baz", - }, - true, - false, - false, - }, - - "mapelem": { - []string{"map", "foo"}, - "bar", - true, - false, - false, - }, - - "set": { - []string{"set"}, - []interface{}{10, 50}, - true, - false, - false, - }, - - "setDeep": { - []string{"setDeep"}, - []interface{}{ - map[string]interface{}{ - "index": 10, - "value": "foo", - }, - map[string]interface{}{ - "index": 50, - "value": "bar", - }, - }, - true, - false, - false, - }, - } - - for name, tc := range cases { - if name != "list" { - continue + }), } - out, err := r.ReadField(tc.Addr) - if (err != nil) != tc.OutErr { - t.Fatalf("%s: err: %s", name, err) - } - if out.Computed != tc.OutComputed { - t.Fatalf("%s: err: %#v", name, out.Computed) - } - - if s, ok := out.Value.(*Set); ok { - // If it is a set, convert to a list so its more easily checked. - out.Value = s.List() - } - - if !reflect.DeepEqual(out.Value, tc.Out) { - t.Fatalf("%s: out: %#v", name, out.Value) - } - if out.Exists != tc.OutOk { - t.Fatalf("%s: outOk: %#v", name, out.Exists) - } - } + }) } func testConfig( diff --git a/helper/schema/field_reader_diff_test.go b/helper/schema/field_reader_diff_test.go index adcdbe18e..7c8a94ada 100644 --- a/helper/schema/field_reader_diff_test.go +++ b/helper/schema/field_reader_diff_test.go @@ -11,35 +11,19 @@ func TestDiffFieldReader_impl(t *testing.T) { var _ FieldReader = new(DiffFieldReader) } -func TestDiffFieldReader(t *testing.T) { +func TestDiffFieldReader_extra(t *testing.T) { schema := map[string]*Schema{ - "bool": &Schema{Type: TypeBool}, - "int": &Schema{Type: TypeInt}, - "string": &Schema{Type: TypeString}, "stringComputed": &Schema{Type: TypeString}, - "list": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeString}, - }, - "listInt": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeInt}, - }, + "listMap": &Schema{ Type: TypeList, Elem: &Schema{ Type: TypeMap, }, }, - "map": &Schema{Type: TypeMap}, + "mapRemove": &Schema{Type: TypeMap}, - "set": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, + "setChange": &Schema{ Type: TypeSet, Optional: true, @@ -61,137 +45,23 @@ func TestDiffFieldReader(t *testing.T) { return m["index"].(int) }, }, - "setDeep": &Schema{ - Type: TypeSet, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{Type: TypeInt}, - "value": &Schema{Type: TypeString}, - }, - }, - Set: func(a interface{}) int { - return a.(map[string]interface{})["index"].(int) - }, - }, - "setEmpty": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, } r := &DiffFieldReader{ Schema: schema, Diff: &terraform.InstanceDiff{ Attributes: map[string]*terraform.ResourceAttrDiff{ - "bool": &terraform.ResourceAttrDiff{ - Old: "", - New: "true", - }, - - "int": &terraform.ResourceAttrDiff{ - Old: "", - New: "42", - }, - - "string": &terraform.ResourceAttrDiff{ - Old: "", - New: "string", - }, - "stringComputed": &terraform.ResourceAttrDiff{ Old: "foo", New: "bar", NewComputed: true, }, - "list.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "2", - }, - - "list.0": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - }, - - "list.1": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - }, - - "listInt.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "2", - }, - - "listInt.0": &terraform.ResourceAttrDiff{ - Old: "", - New: "21", - }, - - "listInt.1": &terraform.ResourceAttrDiff{ - Old: "", - New: "42", - }, - - "map.foo": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - }, - - "map.bar": &terraform.ResourceAttrDiff{ - Old: "", - New: "baz", - }, - - "mapRemove.bar": &terraform.ResourceAttrDiff{ + "listMap.0.bar": &terraform.ResourceAttrDiff{ NewRemoved: true, }, - "set.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "2", - }, - - "set.10": &terraform.ResourceAttrDiff{ - Old: "", - New: "10", - }, - - "set.50": &terraform.ResourceAttrDiff{ - Old: "", - New: "50", - }, - - "setDeep.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "2", - }, - - "setDeep.10.index": &terraform.ResourceAttrDiff{ - Old: "", - New: "10", - }, - - "setDeep.10.value": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - }, - - "setDeep.50.index": &terraform.ResourceAttrDiff{ - Old: "", - New: "50", - }, - - "setDeep.50.value": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - }, - - "listMap.0.bar": &terraform.ResourceAttrDiff{ + "mapRemove.bar": &terraform.ResourceAttrDiff{ NewRemoved: true, }, @@ -225,46 +95,6 @@ func TestDiffFieldReader(t *testing.T) { Result FieldReadResult Err bool }{ - "noexist": { - []string{"boolNOPE"}, - FieldReadResult{ - Value: nil, - Exists: false, - Computed: false, - }, - false, - }, - - "bool": { - []string{"bool"}, - FieldReadResult{ - Value: true, - Exists: true, - Computed: false, - }, - false, - }, - - "int": { - []string{"int"}, - FieldReadResult{ - Value: 42, - Exists: true, - Computed: false, - }, - false, - }, - - "string": { - []string{"string"}, - FieldReadResult{ - Value: "string", - Exists: true, - Computed: false, - }, - false, - }, - "stringComputed": { []string{"stringComputed"}, FieldReadResult{ @@ -275,96 +105,6 @@ func TestDiffFieldReader(t *testing.T) { false, }, - "list": { - []string{"list"}, - FieldReadResult{ - Value: []interface{}{ - "foo", - "bar", - }, - Exists: true, - Computed: false, - }, - false, - }, - - "listInt": { - []string{"listInt"}, - FieldReadResult{ - Value: []interface{}{ - 21, - 42, - }, - Exists: true, - Computed: false, - }, - false, - }, - - "map": { - []string{"map"}, - FieldReadResult{ - Value: map[string]interface{}{ - "foo": "bar", - "bar": "baz", - }, - Exists: true, - Computed: false, - }, - false, - }, - - "mapelem": { - []string{"map", "foo"}, - FieldReadResult{ - Value: "bar", - Exists: true, - Computed: false, - }, - false, - }, - - "mapRemove": { - []string{"mapRemove"}, - FieldReadResult{ - Value: map[string]interface{}{ - "foo": "bar", - }, - Exists: true, - Computed: false, - }, - false, - }, - - "set": { - []string{"set"}, - FieldReadResult{ - Value: []interface{}{10, 50}, - Exists: true, - Computed: false, - }, - false, - }, - - "setDeep": { - []string{"setDeep"}, - FieldReadResult{ - Value: []interface{}{ - map[string]interface{}{ - "index": 10, - "value": "foo", - }, - map[string]interface{}{ - "index": 50, - "value": "bar", - }, - }, - Exists: true, - Computed: false, - }, - false, - }, - "listMapRemoval": { []string{"listMap"}, FieldReadResult{ @@ -381,6 +121,18 @@ func TestDiffFieldReader(t *testing.T) { false, }, + "mapRemove": { + []string{"mapRemove"}, + FieldReadResult{ + Value: map[string]interface{}{ + "foo": "bar", + }, + Exists: true, + Computed: false, + }, + false, + }, + "setChange": { []string{"setChange"}, FieldReadResult{ @@ -394,15 +146,6 @@ func TestDiffFieldReader(t *testing.T) { }, false, }, - - "setEmpty": { - []string{"setEmpty"}, - FieldReadResult{ - Value: []interface{}{}, - Exists: false, - }, - false, - }, } for name, tc := range cases { @@ -419,3 +162,125 @@ func TestDiffFieldReader(t *testing.T) { } } } + +func TestDiffFieldReader(t *testing.T) { + testFieldReader(t, func(s map[string]*Schema) FieldReader { + return &DiffFieldReader{ + Schema: s, + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "bool": &terraform.ResourceAttrDiff{ + Old: "", + New: "true", + }, + + "int": &terraform.ResourceAttrDiff{ + Old: "", + New: "42", + }, + + "string": &terraform.ResourceAttrDiff{ + Old: "", + New: "string", + }, + + "stringComputed": &terraform.ResourceAttrDiff{ + Old: "foo", + New: "bar", + NewComputed: true, + }, + + "list.#": &terraform.ResourceAttrDiff{ + Old: "0", + New: "2", + }, + + "list.0": &terraform.ResourceAttrDiff{ + Old: "", + New: "foo", + }, + + "list.1": &terraform.ResourceAttrDiff{ + Old: "", + New: "bar", + }, + + "listInt.#": &terraform.ResourceAttrDiff{ + Old: "0", + New: "2", + }, + + "listInt.0": &terraform.ResourceAttrDiff{ + Old: "", + New: "21", + }, + + "listInt.1": &terraform.ResourceAttrDiff{ + Old: "", + New: "42", + }, + + "map.foo": &terraform.ResourceAttrDiff{ + Old: "", + New: "bar", + }, + + "map.bar": &terraform.ResourceAttrDiff{ + Old: "", + New: "baz", + }, + + "set.#": &terraform.ResourceAttrDiff{ + Old: "0", + New: "2", + }, + + "set.10": &terraform.ResourceAttrDiff{ + Old: "", + New: "10", + }, + + "set.50": &terraform.ResourceAttrDiff{ + Old: "", + New: "50", + }, + + "setDeep.#": &terraform.ResourceAttrDiff{ + Old: "0", + New: "2", + }, + + "setDeep.10.index": &terraform.ResourceAttrDiff{ + Old: "", + New: "10", + }, + + "setDeep.10.value": &terraform.ResourceAttrDiff{ + Old: "", + New: "foo", + }, + + "setDeep.50.index": &terraform.ResourceAttrDiff{ + Old: "", + New: "50", + }, + + "setDeep.50.value": &terraform.ResourceAttrDiff{ + Old: "", + New: "bar", + }, + }, + }, + + Source: &MapFieldReader{ + Schema: s, + Map: BasicMapReader(map[string]string{ + "listMap.#": "2", + "listMap.0.foo": "bar", + "listMap.0.bar": "baz", + "listMap.1.baz": "baz", + }), + }, + } + }) +} diff --git a/helper/schema/field_reader_map_test.go b/helper/schema/field_reader_map_test.go index 57a02f846..369e2ac0e 100644 --- a/helper/schema/field_reader_map_test.go +++ b/helper/schema/field_reader_map_test.go @@ -10,76 +10,48 @@ func TestMapFieldReader_impl(t *testing.T) { } func TestMapFieldReader(t *testing.T) { + testFieldReader(t, func(s map[string]*Schema) FieldReader { + return &MapFieldReader{ + Schema: s, + + Map: BasicMapReader(map[string]string{ + "bool": "true", + "int": "42", + "string": "string", + + "list.#": "2", + "list.0": "foo", + "list.1": "bar", + + "listInt.#": "2", + "listInt.0": "21", + "listInt.1": "42", + + "map.foo": "bar", + "map.bar": "baz", + + "set.#": "2", + "set.10": "10", + "set.50": "50", + + "setDeep.#": "2", + "setDeep.10.index": "10", + "setDeep.10.value": "foo", + "setDeep.50.index": "50", + "setDeep.50.value": "bar", + }), + } + }) +} + +func TestMapFieldReader_extra(t *testing.T) { r := &MapFieldReader{ Schema: map[string]*Schema{ - "bool": &Schema{Type: TypeBool}, - "int": &Schema{Type: TypeInt}, - "string": &Schema{Type: TypeString}, - "list": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeString}, - }, - "listInt": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeInt}, - }, - "map": &Schema{Type: TypeMap}, "mapDel": &Schema{Type: TypeMap}, - "set": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - "setDeep": &Schema{ - Type: TypeSet, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{Type: TypeInt}, - "value": &Schema{Type: TypeString}, - }, - }, - Set: func(a interface{}) int { - return a.(map[string]interface{})["index"].(int) - }, - }, - "setEmpty": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, }, Map: BasicMapReader(map[string]string{ - "bool": "true", - "int": "42", - "string": "string", - - "list.#": "2", - "list.0": "foo", - "list.1": "bar", - - "listInt.#": "2", - "listInt.0": "21", - "listInt.1": "42", - - "map.foo": "bar", - "map.bar": "baz", - "mapDel": "", - - "set.#": "2", - "set.10": "10", - "set.50": "50", - - "setDeep.#": "2", - "setDeep.10.index": "10", - "setDeep.10.value": "foo", - "setDeep.50.index": "50", - "setDeep.50.value": "bar", }), } @@ -90,71 +62,6 @@ func TestMapFieldReader(t *testing.T) { OutComputed bool OutErr bool }{ - "noexist": { - []string{"boolNOPE"}, - nil, - false, - false, - false, - }, - - "bool": { - []string{"bool"}, - true, - true, - false, - false, - }, - - "int": { - []string{"int"}, - 42, - true, - false, - false, - }, - - "string": { - []string{"string"}, - "string", - true, - false, - false, - }, - - "list": { - []string{"list"}, - []interface{}{ - "foo", - "bar", - }, - true, - false, - false, - }, - - "listInt": { - []string{"listInt"}, - []interface{}{ - 21, - 42, - }, - true, - false, - false, - }, - - "map": { - []string{"map"}, - map[string]interface{}{ - "foo": "bar", - "bar": "baz", - }, - true, - false, - false, - }, - "mapDel": { []string{"mapDel"}, map[string]interface{}{}, @@ -162,47 +69,6 @@ func TestMapFieldReader(t *testing.T) { false, false, }, - - "mapelem": { - []string{"map", "foo"}, - "bar", - true, - false, - false, - }, - - "set": { - []string{"set"}, - []interface{}{10, 50}, - true, - false, - false, - }, - - "setEmpty": { - []string{"setEmpty"}, - []interface{}{}, - false, - false, - false, - }, - - "setDeep": { - []string{"setDeep"}, - []interface{}{ - map[string]interface{}{ - "index": 10, - "value": "foo", - }, - map[string]interface{}{ - "index": 50, - "value": "bar", - }, - }, - true, - false, - false, - }, } for name, tc := range cases { diff --git a/helper/schema/field_reader_test.go b/helper/schema/field_reader_test.go index 200c9ca62..ebbc3f375 100644 --- a/helper/schema/field_reader_test.go +++ b/helper/schema/field_reader_test.go @@ -182,3 +182,209 @@ func TestAddrToSchema(t *testing.T) { } } } + +// testFieldReader is a helper that should be used to verify that +// a FieldReader behaves properly in all the common cases. +func testFieldReader(t *testing.T, f func(map[string]*Schema) FieldReader) { + schema := map[string]*Schema{ + // Primitives + "bool": &Schema{Type: TypeBool}, + "int": &Schema{Type: TypeInt}, + "string": &Schema{Type: TypeString}, + + // Lists + "list": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeString}, + }, + "listInt": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + }, + "listMap": &Schema{ + Type: TypeList, + Elem: &Schema{ + Type: TypeMap, + }, + }, + + // Maps + "map": &Schema{Type: TypeMap}, + + // Sets + "set": &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + "setDeep": &Schema{ + Type: TypeSet, + Elem: &Resource{ + Schema: map[string]*Schema{ + "index": &Schema{Type: TypeInt}, + "value": &Schema{Type: TypeString}, + }, + }, + Set: func(a interface{}) int { + return a.(map[string]interface{})["index"].(int) + }, + }, + "setEmpty": &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + } + + cases := map[string]struct { + Addr []string + Result FieldReadResult + Err bool + }{ + "noexist": { + []string{"boolNOPE"}, + FieldReadResult{ + Value: nil, + Exists: false, + Computed: false, + }, + false, + }, + + "bool": { + []string{"bool"}, + FieldReadResult{ + Value: true, + Exists: true, + Computed: false, + }, + false, + }, + + "int": { + []string{"int"}, + FieldReadResult{ + Value: 42, + Exists: true, + Computed: false, + }, + false, + }, + + "string": { + []string{"string"}, + FieldReadResult{ + Value: "string", + Exists: true, + Computed: false, + }, + false, + }, + + "list": { + []string{"list"}, + FieldReadResult{ + Value: []interface{}{ + "foo", + "bar", + }, + Exists: true, + Computed: false, + }, + false, + }, + + "listInt": { + []string{"listInt"}, + FieldReadResult{ + Value: []interface{}{ + 21, + 42, + }, + Exists: true, + Computed: false, + }, + false, + }, + + "map": { + []string{"map"}, + FieldReadResult{ + Value: map[string]interface{}{ + "foo": "bar", + "bar": "baz", + }, + Exists: true, + Computed: false, + }, + false, + }, + + "mapelem": { + []string{"map", "foo"}, + FieldReadResult{ + Value: "bar", + Exists: true, + Computed: false, + }, + false, + }, + + "set": { + []string{"set"}, + FieldReadResult{ + Value: []interface{}{10, 50}, + Exists: true, + Computed: false, + }, + false, + }, + + "setDeep": { + []string{"setDeep"}, + FieldReadResult{ + Value: []interface{}{ + map[string]interface{}{ + "index": 10, + "value": "foo", + }, + map[string]interface{}{ + "index": 50, + "value": "bar", + }, + }, + Exists: true, + Computed: false, + }, + false, + }, + + "setEmpty": { + []string{"setEmpty"}, + FieldReadResult{ + Value: []interface{}{}, + Exists: false, + }, + false, + }, + } + + for name, tc := range cases { + r := f(schema) + 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 a list so its more easily checked. + out.Value = s.List() + } + if !reflect.DeepEqual(tc.Result, out) { + t.Fatalf("%s: bad: %#v", name, out) + } + } +} From 3cbcafe989e8176123cf781d466f4808b26b0f3e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 10 Jan 2015 12:50:53 -0800 Subject: [PATCH 23/23] helper/schema: remove unused field --- helper/schema/resource_data.go | 9 ++++----- helper/schema/schema.go | 7 +++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go index 00d0fd3ae..ac86b6599 100644 --- a/helper/schema/resource_data.go +++ b/helper/schema/resource_data.go @@ -18,11 +18,10 @@ import ( // The most relevant methods to take a look at are Get, Set, and Partial. type ResourceData struct { // Settable (internally) - schema map[string]*Schema - config *terraform.ResourceConfig - state *terraform.InstanceState - diff *terraform.InstanceDiff - diffing bool + schema map[string]*Schema + config *terraform.ResourceConfig + state *terraform.InstanceState + diff *terraform.InstanceDiff // Don't set multiReader *MultiLevelFieldReader diff --git a/helper/schema/schema.go b/helper/schema/schema.go index 85bc9207e..e041951ed 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -223,10 +223,9 @@ func (m schemaMap) Diff( result.Attributes = make(map[string]*terraform.ResourceAttrDiff) d := &ResourceData{ - schema: m, - state: s, - config: c, - diffing: true, + schema: m, + state: s, + config: c, } for k, schema := range m {