From 73726e83b2dfcec0631e06b0232a944ea4e0bead Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Dec 2014 02:17:35 +0530 Subject: [PATCH] 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) } } }