diff --git a/helper/schema/field_reader.go b/helper/schema/field_reader.go index c3a6c76fa08f..1ca721d1657c 100644 --- a/helper/schema/field_reader.go +++ b/helper/schema/field_reader.go @@ -130,60 +130,6 @@ func addrToSchema(addr []string, schemaMap map[string]*Schema) []*Schema { 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, 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(addrPadded) - if err != nil { - return FieldReadResult{}, err - } - if !countResult.Exists { - // No count, means we have no list - countResult.Value = 0 - } - - // If we have an empty list, then return an empty list - if countResult.Computed || countResult.Value.(int) == 0 { - return FieldReadResult{ - Value: []interface{}{}, - Exists: countResult.Exists, - Computed: countResult.Computed, - }, nil - } - - // 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) - addrPadded[len(addrPadded)-1] = is - rawResult, err := r.ReadField(addrPadded) - if err != nil { - return FieldReadResult{}, err - } - 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. - rawResult.Value = nil - } - - result[i] = rawResult.Value - } - - return FieldReadResult{ - Value: result, - Exists: true, - }, 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. diff --git a/helper/schema/field_reader_config.go b/helper/schema/field_reader_config.go index 0d4c2a97c9bb..c11dff795724 100644 --- a/helper/schema/field_reader_config.go +++ b/helper/schema/field_reader_config.go @@ -83,7 +83,7 @@ func (r *ConfigFieldReader) readField( case TypeBool, TypeFloat, TypeInt, TypeString: return r.readPrimitive(k, schema) case TypeList: - return readListField(&nestedConfigFieldReader{r}, address, schema) + return r.readList(address, schema) case TypeMap: return r.readMap(k) case TypeSet: @@ -216,13 +216,65 @@ func (r *ConfigFieldReader) readPrimitive( }, nil } +func (r *ConfigFieldReader) readList( + address []string, schema *Schema) (FieldReadResult, error) { + + addrPadded := make([]string, len(address)+1) + copy(addrPadded, address) + + // Get the number of elements in the list + addrPadded[len(addrPadded)-1] = "#" + countResult, err := r.readPrimitive( + strings.Join(addrPadded, "."), &Schema{Type: TypeInt}) + if err != nil { + return FieldReadResult{}, err + } + if !countResult.Exists { + // No count, means we have no list + countResult.Value = 0 + } + + // If we have an empty list, then return an empty list + if countResult.Computed || countResult.Value.(int) == 0 { + return FieldReadResult{ + Value: []interface{}{}, + Exists: countResult.Exists, + Computed: countResult.Computed, + }, nil + } + + // Go through each count, and get the item value out of it + result := make([]interface{}, countResult.Value.(int)) + for i, _ := range result { + idx := strconv.Itoa(i) + addrPadded[len(addrPadded)-1] = idx + rawResult, err := r.readField(addrPadded, true) + if err != nil { + return FieldReadResult{}, err + } + 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. + panic("missing field in list: " + strings.Join(addrPadded, ".")) + } + + result[i] = rawResult.Value + } + + return FieldReadResult{ + Value: result, + Exists: true, + }, nil +} + func (r *ConfigFieldReader) readSet( address []string, schema *Schema) (FieldReadResult, error) { indexMap := make(map[string]int) // Create the set that will be our result set := schema.ZeroValue().(*Set) - raw, err := readListField(&nestedConfigFieldReader{r}, address, schema) + raw, err := r.readList(address, schema) if err != nil { return FieldReadResult{}, err } diff --git a/helper/schema/field_reader_diff.go b/helper/schema/field_reader_diff.go index 661c5687c28b..6edb9cf72b5f 100644 --- a/helper/schema/field_reader_diff.go +++ b/helper/schema/field_reader_diff.go @@ -2,6 +2,8 @@ package schema import ( "fmt" + "sort" + "strconv" "strings" "github.com/hashicorp/terraform/terraform" @@ -12,8 +14,8 @@ import ( // // 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. +// because a diff on its own doesn't have complete data about non-primitive +// objects such as maps, lists and sets. // // The Source MUST be the data that the diff was derived from. If it isn't, // the behavior of this struct is undefined. @@ -42,7 +44,7 @@ func (r *DiffFieldReader) ReadField(address []string) (FieldReadResult, error) { case TypeBool, TypeInt, TypeFloat, TypeString: return r.readPrimitive(address, schema) case TypeList: - return readListField(r, address, schema) + return r.readList(address, schema) case TypeMap: return r.readMap(address, schema) case TypeSet: @@ -57,7 +59,7 @@ func (r *DiffFieldReader) ReadField(address []string) (FieldReadResult, error) { func (r *DiffFieldReader) readMap( address []string, schema *Schema) (FieldReadResult, error) { result := make(map[string]interface{}) - resultSet := false + exists := false // First read the map from the underlying source source, err := r.Source.ReadField(address) @@ -66,7 +68,7 @@ func (r *DiffFieldReader) readMap( } if source.Exists { result = source.Value.(map[string]interface{}) - resultSet = true + exists = true } // Next, read all the elements we have in our diff, and apply @@ -81,7 +83,7 @@ func (r *DiffFieldReader) readMap( continue } - resultSet = true + exists = true k = k[len(prefix):] if v.NewRemoved { @@ -93,13 +95,13 @@ func (r *DiffFieldReader) readMap( } var resultVal interface{} - if resultSet { + if exists { resultVal = result } return FieldReadResult{ Value: resultVal, - Exists: resultSet, + Exists: exists, }, nil } @@ -136,6 +138,99 @@ func (r *DiffFieldReader) readPrimitive( return result, nil } +func (r *DiffFieldReader) readList( + address []string, schema *Schema) (FieldReadResult, error) { + prefix := strings.Join(address, ".") + "." + + addrPadded := make([]string, len(address)+1) + copy(addrPadded, address) + + // Get the number of elements in the list + addrPadded[len(addrPadded)-1] = "#" + countResult, err := r.readPrimitive(addrPadded, &Schema{Type: TypeInt}) + if err != nil { + return FieldReadResult{}, err + } + if !countResult.Exists { + // No count, means we have no list + countResult.Value = 0 + } + // If we have an empty list, then return an empty list + if countResult.Computed || countResult.Value.(int) == 0 { + return FieldReadResult{ + Value: []interface{}{}, + Exists: countResult.Exists, + Computed: countResult.Computed, + }, nil + } + + // Bail out if diff doesn't contain the given field at all + // This has to be a separate loop because we're only + // iterating over raw list items (list.idx). + // Other fields (list.idx.*) are left for other read* methods + // which can deal with these fields appropriately. + diffContainsField := false + for k, _ := range r.Diff.Attributes { + if strings.HasPrefix(k, address[0]+".") { + diffContainsField = true + } + } + if !diffContainsField { + return FieldReadResult{ + Value: []interface{}{}, + Exists: false, + }, nil + } + + // Create the list that will be our result + list := []interface{}{} + + // Go through the diff and find all the list items + // We are not iterating over the diff directly as some indexes + // may be missing and we expect the whole list to be returned. + for i := 0; i < countResult.Value.(int); i++ { + idx := strconv.Itoa(i) + addrString := prefix + idx + + d, ok := r.Diff.Attributes[addrString] + if ok && d.NewRemoved { + // If the field is being removed, we ignore it + continue + } + + addrPadded[len(addrPadded)-1] = idx + raw, err := r.ReadField(addrPadded) + if err != nil { + return FieldReadResult{}, err + } + if !raw.Exists { + // This should never happen, because by the time the data + // gets to the FieldReaders, all the defaults should be set by + // Schema. + panic("missing field in set: " + addrString + "." + idx) + } + list = append(list, raw.Value) + } + + // Determine if the list "exists". It exists if there are items or if + // the diff explicitly wanted it empty. + exists := len(list) > 0 + if !exists { + // We could check if the diff value is "0" here but I think the + // existence of "#" on its own is enough to show it existed. This + // protects us in the future from the zero value changing from + // "0" to "" breaking us (if that were to happen). + if _, ok := r.Diff.Attributes[prefix+"#"]; ok { + exists = true + } + } + + return FieldReadResult{ + Value: list, + Exists: exists, + }, nil +} + func (r *DiffFieldReader) readSet( address []string, schema *Schema) (FieldReadResult, error) { prefix := strings.Join(address, ".") + "." @@ -143,10 +238,92 @@ func (r *DiffFieldReader) readSet( // Create the set that will be our result set := schema.ZeroValue().(*Set) + // Check if we're supposed to remove it + v, ok := r.Diff.Attributes[prefix+"#"] + if ok && v.New == "0" { + // I'm not entirely sure what's the point of + // returning empty set w/ Exists: true + return FieldReadResult{ + Value: set, + Exists: true, + }, nil + } + + // Compose list of all keys (diff + source) + var keys []string + + // Add keys from diff + diffContainsField := false + for k, _ := range r.Diff.Attributes { + if strings.HasPrefix(k, address[0]+".") { + diffContainsField = true + } + keys = append(keys, k) + } + // Bail out if diff doesn't contain the given field at all + if !diffContainsField { + return FieldReadResult{ + Value: set, + Exists: false, + }, nil + } + // Add keys from source + sourceResult, err := r.Source.ReadField(address) + if err == nil && sourceResult.Exists { + sourceSet := sourceResult.Value.(*Set) + sourceMap := sourceSet.Map() + + // Doesn't this belong to readObjectField? + res, rOk := schema.Elem.(*Resource) + + for k, v := range sourceMap { + key := prefix + k + + // If ResourceAttrDiff of a set item which only contains + // NewRemoved: true fields also had NewRemoved: true itself + // then we wouldn't have to do this ugly thing + sourceFields, fieldsOk := v.(map[string]interface{}) + if fieldsOk { + for fieldKey, _ := range sourceFields { + fa, fieldOk := r.Diff.Attributes[key+"."+fieldKey] + if fieldOk && fa.NewRemoved { + delete(sourceFields, fieldKey) + } + + // Delete computed fields + if !fieldOk && rOk && res.Schema[fieldKey].Computed { + delete(sourceFields, fieldKey) + } + + countField, cFieldOk := r.Diff.Attributes[key+"."+fieldKey+".#"] + if cFieldOk && countField.New == "0" { + delete(sourceFields, fieldKey) + delete(sourceFields, fieldKey+".#") + } + } + } + + _, parentOk := r.Diff.Attributes[key] + + // Only bother processing set items which either + // have at least 1 field left (not NewRemoved) or + // aren't in the diff at all + if (!parentOk && len(sourceFields) > 0) || !fieldsOk { + keys = append(keys, key) + } + } + } + + // Keep the order consistent for hashing functions + sort.Strings(keys) + // Go through the map and find all the set items - for k, d := range r.Diff.Attributes { - if d.NewRemoved { - // If the field is removed, we always ignore it + // We are not iterating over the diff directly as some indexes + // may be missing and we expect the whole set to be returned. + for _, k := range keys { + d, ok := r.Diff.Attributes[k] + if ok && d.NewRemoved { + // If the field is being removed, we ignore it continue } if !strings.HasPrefix(k, prefix) { @@ -161,6 +338,12 @@ func (r *DiffFieldReader) readSet( parts := strings.Split(k[len(prefix):], ".") idx := parts[0] + kD, ok := r.Diff.Attributes[prefix+parts[0]] + if ok && kD.NewRemoved { + // Skip any sub-objects that are being removed + continue + } + 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 cfd329492b9f..5ca795e1d950 100644 --- a/helper/schema/field_reader_diff_test.go +++ b/helper/schema/field_reader_diff_test.go @@ -1,9 +1,14 @@ package schema import ( + "bytes" + "fmt" "reflect" + "sort" + "strings" "testing" + "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/terraform" ) @@ -245,7 +250,7 @@ func TestDiffFieldReader_extra(t *testing.T) { out.Value = s.List() } if !reflect.DeepEqual(tc.Result, out) { - t.Fatalf("%s: bad: %#v", name, out) + t.Fatalf("Case %q:\ngiven: %#v\nexpected: %#v", name, out, tc.Result) } } } @@ -376,3 +381,1192 @@ func TestDiffFieldReader(t *testing.T) { } }) } + +func TestDiffFieldReader_SetInSet(t *testing.T) { + schema := map[string]*Schema{ + "main_set": &Schema{ + Type: TypeSet, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "inner_string_set": &Schema{ + Type: TypeSet, + Required: true, + Set: HashString, + Elem: &Schema{Type: TypeString}, + }, + "inner_int": &Schema{ + Type: TypeInt, + Required: true, + }, + }, + }, + }, + "main_int": &Schema{ + Type: TypeInt, + Optional: true, + }, + } + + var readers = make(map[string]FieldReader) + readers["state"] = &MapFieldReader{ + Schema: schema, + Map: BasicMapReader(map[string]string{ + "id": "8395051352714003426", + "main_int": "9", + "main_set.#": "1", + "main_set.2476980464.inner_string_set.#": "2", + "main_set.2476980464.inner_string_set.2654390964": "blue", + "main_set.2476980464.inner_string_set.3499814433": "green", + "main_set.2476980464.inner_int": "4", + }), + } + + // If we're only changing main_int + dfr := &DiffFieldReader{ + Schema: schema, + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "main_int": &terraform.ResourceAttrDiff{ + Old: "9", + New: "2", + }, + }, + }, + Source: &MultiLevelFieldReader{ + Levels: []string{"state", "config"}, + Readers: readers, + }, + } + + // main_list should NOT be in the diff at all + result, err := dfr.ReadField([]string{"main_set"}) + if err != nil { + t.Fatalf("ReadField failed: %s", err) + } + expectedResult := NewSet(HashString, []interface{}{}) + if !expectedResult.Equal(result.Value) { + t.Fatalf("ReadField returned unexpected result.\nGiven: %#v\nexpected: %#v", + result, expectedResult) + } +} + +func TestDiffFieldReader_SetInList(t *testing.T) { + schema := map[string]*Schema{ + "main_list": &Schema{ + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "inner_string_set": &Schema{ + Type: TypeSet, + Required: true, + Set: HashString, + Elem: &Schema{Type: TypeString}, + }, + "inner_int": &Schema{ + Type: TypeInt, + Required: true, + }, + }, + }, + }, + "main_int": &Schema{ + Type: TypeInt, + Optional: true, + }, + } + + var readers = make(map[string]FieldReader) + readers["state"] = &MapFieldReader{ + Schema: schema, + Map: BasicMapReader(map[string]string{ + "id": "8395051352714003426", + "main_int": "9", + "main_list.#": "1", + "main_list.0.inner_string_set.#": "2", + "main_list.0.inner_string_set.2654390964": "blue", + "main_list.0.inner_string_set.3499814433": "green", + "main_list.0.inner_int": "4", + }), + } + + // If we're only changing main_int + dfr := &DiffFieldReader{ + Schema: schema, + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "main_int": &terraform.ResourceAttrDiff{ + Old: "9", + New: "2", + }, + }, + }, + Source: &MultiLevelFieldReader{ + Levels: []string{"state", "config"}, + Readers: readers, + }, + } + + // main_list should NOT be in the diff at all + result, err := dfr.ReadField([]string{"main_list"}) + if err != nil { + t.Fatalf("ReadField failed: %s", err) + } + expectedResult := FieldReadResult{ + Value: []interface{}{}, + ValueProcessed: nil, + Exists: false, + Computed: false, + } + if !reflect.DeepEqual(result, expectedResult) { + t.Fatalf("ReadField returned unexpected result.\nGiven: %#v\nexpected: %#v", + result, expectedResult) + } +} + +func TestDiffFieldReader_SetInList_singleInstance(t *testing.T) { + schema := map[string]*Schema{ + "main_list": &Schema{ + Type: TypeList, + Optional: true, + MaxItems: 1, + Elem: &Resource{ + Schema: map[string]*Schema{ + "inner_string_set": &Schema{ + Type: TypeSet, + Required: true, + Set: HashString, + Elem: &Schema{Type: TypeString}, + }, + "inner_int": &Schema{ + Type: TypeInt, + Required: true, + }, + }, + }, + }, + "main_int": &Schema{ + Type: TypeInt, + Optional: true, + }, + } + + var readers = make(map[string]FieldReader) + readers["state"] = &MapFieldReader{ + Schema: schema, + Map: BasicMapReader(map[string]string{ + "id": "8395051352714003426", + "main_int": "9", + "main_list.#": "1", + "main_list.0.inner_string_set.#": "2", + "main_list.0.inner_string_set.2654390964": "blue", + "main_list.0.inner_string_set.3499814433": "green", + "main_list.0.inner_int": "4", + }), + } + + // 1. NEGATIVE (diff doesn't contain list) + // If we're only changing main_int + dfrNegative := &DiffFieldReader{ + Schema: schema, + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "main_int": &terraform.ResourceAttrDiff{ + Old: "9", + New: "2", + }, + }, + }, + Source: &MultiLevelFieldReader{ + Levels: []string{"state", "config"}, + Readers: readers, + }, + } + // main_list should NOT be in the diff at all + resultNegative, err := dfrNegative.ReadField([]string{"main_list"}) + if err != nil { + t.Fatalf("ReadField failed: %s", err) + } + expectedNegativeResult := FieldReadResult{ + Value: []interface{}{}, + ValueProcessed: nil, + Exists: false, + Computed: false, + } + if !reflect.DeepEqual(resultNegative, expectedNegativeResult) { + t.Fatalf("ReadField returned unexpected resultNegative.\nGiven: %#v\nexpected: %#v", + resultNegative, expectedNegativeResult) + } + + // 1. POSITIVE (diff contains list) + dfrPositive := &DiffFieldReader{ + Schema: schema, + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "main_list.0.inner_int": &terraform.ResourceAttrDiff{ + Old: "4", + New: "2", + }, + }, + }, + Source: &MultiLevelFieldReader{ + Levels: []string{"state", "config"}, + Readers: readers, + }, + } + resultPositive, err := dfrPositive.ReadField([]string{"main_list"}) + if err != nil { + t.Fatalf("ReadField failed: %s", err) + } + if !resultPositive.Exists { + t.Fatal("Expected resultPositive to exist") + } + list := resultPositive.Value.([]interface{}) + if len(list) != 1 { + t.Fatalf("Expected exactly 1 list instance, %d given", len(list)) + } + + m := list[0].(map[string]interface{}) + + m_expectedInnerInt := 2 + m_innerInt, ok := m["inner_int"] + if !ok { + t.Fatal("Expected inner_int key to exist in map") + } + if m_innerInt != m_expectedInnerInt { + t.Fatalf("Expected inner_int (%d) doesn't match w/ given: %d", m_expectedInnerInt, m_innerInt) + } + + m_expectedStringSet := NewSet(HashString, []interface{}{"blue", "green"}) + m_StringSet, ok := m["inner_string_set"] + if !ok { + t.Fatal("Expected inner_string_set key to exist in map") + } + if !m_expectedStringSet.Equal(m_StringSet) { + t.Fatalf("Expected inner_string_set (%q) doesn't match w/ given: %q", + m_expectedStringSet.List(), m_StringSet.(*Set).List()) + } +} + +func TestDiffFieldReader_SetInList_multipleInstances(t *testing.T) { + schema := map[string]*Schema{ + "main_list": &Schema{ + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "inner_string_set": &Schema{ + Type: TypeSet, + Required: true, + Set: HashString, + Elem: &Schema{Type: TypeString}, + }, + "inner_int": &Schema{ + Type: TypeInt, + Required: true, + }, + }, + }, + }, + "main_int": &Schema{ + Type: TypeInt, + Optional: true, + }, + } + + var readers = make(map[string]FieldReader) + readers["state"] = &MapFieldReader{ + Schema: schema, + Map: BasicMapReader(map[string]string{ + "id": "8395051352714003426", + "main_int": "9", + "main_list.#": "3", + "main_list.0.inner_string_set.#": "2", + "main_list.0.inner_string_set.2654390964": "blue", + "main_list.0.inner_string_set.3499814433": "green", + "main_list.0.inner_int": "4", + "main_list.1.inner_string_set.#": "2", + "main_list.1.inner_string_set.1830392916": "brown", + "main_list.1.inner_string_set.4200685455": "red", + "main_list.1.inner_int": "4", + "main_list.2.inner_string_set.#": "3", + "main_list.2.inner_string_set.2053932785": "one", + "main_list.2.inner_string_set.298486374": "two", + "main_list.2.inner_string_set.1187371253": "three", + "main_list.2.inner_int": "914", + }), + } + + dfr := &DiffFieldReader{ + Schema: schema, + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "main_list.0.inner_int": &terraform.ResourceAttrDiff{ + Old: "4", + New: "5", + }, + "main_list.1.inner_int": &terraform.ResourceAttrDiff{ + Old: "4", + New: "34", + }, + }, + }, + Source: &MultiLevelFieldReader{ + Levels: []string{"state", "config"}, + Readers: readers, + }, + } + + result, err := dfr.ReadField([]string{"main_list"}) + if err != nil { + t.Fatalf("ReadField 2 failed: %s", err) + } + if !result.Exists { + t.Fatal("Expected result to exist") + } + list := result.Value.([]interface{}) + if len(list) != 3 { + t.Fatalf("Expected exactly 3 list instances, %d given", len(list)) + } + + // First + m1 := list[0].(map[string]interface{}) + + m1_expectedInnerInt := 5 + m1_innerInt, ok := m1["inner_int"] + if !ok { + t.Fatal("Expected 1st inner_int key to exist in map") + } + if m1_innerInt != m1_expectedInnerInt { + t.Fatalf("Expected 1st inner_int (%d) doesn't match w/ given: %d", m1_expectedInnerInt, m1_innerInt) + } + + m1_expectedStringSet := NewSet(HashString, []interface{}{"blue", "green"}) + m1_StringSet, ok := m1["inner_string_set"] + if !ok { + t.Fatal("Expected 1st inner_string_set key to exist in map") + } + if !m1_expectedStringSet.Equal(m1_StringSet) { + t.Fatalf("Expected 1st inner_string_set (%q) doesn't match w/ given: %q", + m1_expectedStringSet.List(), m1_StringSet.(*Set).List()) + } + + // Second + m2 := list[1].(map[string]interface{}) + + m2_expectedInnerInt := 34 + m2_innerInt, ok := m2["inner_int"] + if !ok { + t.Fatal("Expected 2nd inner_int key to exist in map") + } + if m2_innerInt != m2_expectedInnerInt { + t.Fatalf("Expected 2nd inner_int (%d) doesn't match w/ given: %d", m2_expectedInnerInt, m2_innerInt) + } + + m2_expectedStringSet := NewSet(HashString, []interface{}{"brown", "red"}) + m2_StringSet, ok := m2["inner_string_set"].(*Set) + if !ok { + t.Fatal("Expected 2nd inner_string_set key to exist in map") + } + if !m2_expectedStringSet.Equal(m2_StringSet) { + t.Fatalf("Expected 2nd inner_string_set (%q) doesn't match w/ given: %q", + m2_expectedStringSet.List(), m2_StringSet.List()) + } + + // Third + m3 := list[2].(map[string]interface{}) + + m3_expectedInnerInt := 914 + m3_innerInt, ok := m3["inner_int"] + if !ok { + t.Fatal("Expected 3rd inner_int key to exist in map") + } + if m3_innerInt != m3_expectedInnerInt { + t.Fatalf("Expected 3rd inner_int (%d) doesn't match w/ given: %d", m3_expectedInnerInt, m3_innerInt) + } + + m3_expectedStringSet := NewSet(HashString, []interface{}{"one", "two", "three"}) + m3_StringSet, ok := m3["inner_string_set"].(*Set) + if !ok { + t.Fatal("Expected 3rd inner_string_set key to exist in map") + } + if !m3_expectedStringSet.Equal(m3_StringSet) { + t.Fatalf("Expected 3rd inner_string_set (%q) doesn't match w/ given: %q", + m3_expectedStringSet.List(), m3_StringSet.List()) + } +} + +func TestDiffFieldReader_SetInList_deeplyNested_singleInstance(t *testing.T) { + inInnerSetResource := &Resource{ + Schema: map[string]*Schema{ + "in_in_inner_string": &Schema{ + Type: TypeString, + Required: true, + }, + "in_in_inner_list": &Schema{ + Type: TypeList, + Optional: true, + Elem: &Schema{Type: TypeString}, + }, + }, + } + innerSetResource := &Resource{ + Schema: map[string]*Schema{ + "in_inner_set": &Schema{ + Type: TypeSet, + Required: true, + MaxItems: 1, + Elem: inInnerSetResource, + }, + "in_inner_string_list": &Schema{ + Type: TypeList, + Optional: true, + Elem: &Schema{Type: TypeString}, + }, + "in_inner_bool": &Schema{ + Type: TypeBool, + Required: true, + }, + }, + } + schema := map[string]*Schema{ + "main_list": &Schema{ + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "inner_string_set": &Schema{ + Type: TypeSet, + Required: true, + Set: HashString, + Elem: &Schema{Type: TypeString}, + }, + "inner_set": &Schema{ + Type: TypeSet, + Required: true, + MaxItems: 1, + Elem: innerSetResource, + }, + "inner_bool": &Schema{ + Type: TypeBool, + Optional: true, + Default: false, + }, + "inner_int": &Schema{ + Type: TypeInt, + Required: true, + }, + }, + }, + }, + "main_int": &Schema{ + Type: TypeInt, + Optional: true, + }, + } + + var readers = make(map[string]FieldReader) + readers["state"] = &MapFieldReader{ + Schema: schema, + Map: BasicMapReader(map[string]string{ + "id": "8395051352714003426", + "main_int": "5", + "main_list.#": "1", + "main_list.0.inner_bool": "true", + "main_list.0.inner_int": "2", + "main_list.0.inner_set.#": "1", + "main_list.0.inner_set.2496801729.in_inner_bool": "false", + "main_list.0.inner_set.2496801729.in_inner_set.#": "1", + "main_list.0.inner_set.2496801729.in_inner_set.1989773763.in_in_inner_list.#": "1", + "main_list.0.inner_set.2496801729.in_inner_set.1989773763.in_in_inner_list.0": "alpha", + "main_list.0.inner_set.2496801729.in_inner_set.1989773763.in_in_inner_string": "delta", + "main_list.0.inner_set.2496801729.in_inner_string_list.#": "3", + "main_list.0.inner_set.2496801729.in_inner_string_list.0": "one", + "main_list.0.inner_set.2496801729.in_inner_string_list.1": "two", + "main_list.0.inner_set.2496801729.in_inner_string_list.2": "three", + "main_list.0.inner_string_set.#": "2", + "main_list.0.inner_string_set.1830392916": "brown", + "main_list.0.inner_string_set.4200685455": "red", + }), + } + + // If we're only changing main_int + dfr := &DiffFieldReader{ + Schema: schema, + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "main_list.0.inner_int": &terraform.ResourceAttrDiff{ + Old: "2", + New: "78", + }, + }, + }, + Source: &MultiLevelFieldReader{ + Levels: []string{"state", "config"}, + Readers: readers, + }, + } + + // main_list should NOT be in the diff at all + result, err := dfr.ReadField([]string{"main_list"}) + if err != nil { + t.Fatalf("ReadField failed: %s", err) + } + if !result.Exists { + t.Fatal("Expected result to exist") + } + list := result.Value.([]interface{}) + if len(list) != 1 { + t.Fatalf("Expected exactly 1 list instance, %d given", len(list)) + } + + // One day we may have a custom comparison function for nested sets + // Until that day comes it will look as ridiculous as below + m1 := list[0].(map[string]interface{}) + + m1_expectedInnerInt := 78 + m1_innerInt, ok := m1["inner_int"] + if !ok { + t.Fatal("Expected inner_int key to exist in map") + } + if m1_innerInt != m1_expectedInnerInt { + t.Fatalf("Expected inner_int (%d) doesn't match w/ given: %d", m1_expectedInnerInt, m1_innerInt) + } + + m1_expectedInnerBool := true + m1_innerBool, ok := m1["inner_bool"] + if !ok { + t.Fatal("Expected inner_bool key to exist in map") + } + if m1_innerBool != m1_expectedInnerBool { + t.Fatalf("Expected inner_bool (%t) doesn't match w/ given: %t", m1_expectedInnerBool, m1_innerBool) + } + + m1_expectedStringSet := NewSet(HashString, []interface{}{"brown", "red"}) + m1_StringSet, ok := m1["inner_string_set"] + if !ok { + t.Fatal("Expected inner_string_set key to exist in map") + } + if !m1_expectedStringSet.Equal(m1_StringSet) { + t.Fatalf("Expected inner_string_set (%q) doesn't match w/ given: %q", + m1_expectedStringSet.List(), m1_StringSet.(*Set).List()) + } + + m1_InnerSet, ok := m1["inner_set"] + if !ok { + t.Fatal("Expected inner_set key to exist in map") + } + m1_InnerSet_list := m1_InnerSet.(*Set).List() + m := m1_InnerSet_list[0].(map[string]interface{}) + + expectedInInnerBool := false + inInnerBool, ok := m["in_inner_bool"] + if !ok { + t.Fatal("Expected in_inner_bool key to exist in map") + } + if inInnerBool != expectedInInnerBool { + t.Fatalf("Expected inner_set[0].in_inner_bool (%#v) doesn't match w/ given: %#v", + expectedInInnerBool, inInnerBool) + } + expectedInInnerStringList := []interface{}{"one", "two", "three"} + inInnerStringList, ok := m["in_inner_string_list"] + if !ok { + t.Fatal("Expected in_inner_string_list key to exist in map") + } + if !reflect.DeepEqual(inInnerStringList, expectedInInnerStringList) { + t.Fatalf("Expected inner_set[0].in_inner_string_list (%#v) doesn't match w/ given: %#v", + expectedInInnerStringList, inInnerStringList) + } + + expectedInInnerSet := map[string]interface{}{ + "in_in_inner_string": "delta", + "in_in_inner_list": []interface{}{"alpha"}, + } + inInnerSet, ok := m["in_inner_set"] + if !ok { + t.Fatal("Expected in_inner_set key to exist in map") + } + inInnerSet_list := inInnerSet.(*Set).List() + m2 := inInnerSet_list[0].(map[string]interface{}) + if !reflect.DeepEqual(expectedInInnerSet, m2) { + t.Fatalf("Expected in_inner_set to match:\nGiven: %#v\nExpected: %#v\n", + m2, expectedInInnerSet) + } +} + +func TestDiffFieldReader_setItemRemoved(t *testing.T) { + resourceAwsElbListenerHash := func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%d-", m["instance_port"].(int))) + buf.WriteString(fmt.Sprintf("%s-", + strings.ToLower(m["instance_protocol"].(string)))) + buf.WriteString(fmt.Sprintf("%d-", m["lb_port"].(int))) + buf.WriteString(fmt.Sprintf("%s-", + strings.ToLower(m["lb_protocol"].(string)))) + + if v, ok := m["ssl_certificate_id"]; ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + return hashcode.String(buf.String()) + } + + schema := map[string]*Schema{ + "listener": &Schema{ + Type: TypeSet, + Required: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "instance_port": &Schema{ + Type: TypeInt, + Required: true, + }, + + "instance_protocol": &Schema{ + Type: TypeString, + Required: true, + }, + + "lb_port": &Schema{ + Type: TypeInt, + Required: true, + }, + + "lb_protocol": &Schema{ + Type: TypeString, + Required: true, + }, + + "ssl_certificate_id": &Schema{ + Type: TypeString, + Optional: true, + }, + }, + }, + Set: resourceAwsElbListenerHash, + }, + + "tags": &Schema{ + Type: TypeMap, + Optional: true, + }, + } + + var readers = make(map[string]FieldReader) + readers["state"] = &MapFieldReader{ + Schema: schema, + Map: BasicMapReader(map[string]string{ + "id": "tf-lb-t77qtnbbm5dczcnker4c7boj7m", + "listener.#": "1", + "listener.206423021.instance_port": "8000", + "listener.206423021.instance_protocol": "http", + "listener.206423021.lb_port": "80", + "listener.206423021.lb_protocol": "http", + "listener.206423021.ssl_certificate_id": "", + "tags.%": "1", + "tags.bar": "baz", + }), + } + + dfr := &DiffFieldReader{ + Schema: schema, + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "listener.3931999347.ssl_certificate_id": &terraform.ResourceAttrDiff{ + Old: "", New: "", NewRemoved: false, + }, + "tags.bar": &terraform.ResourceAttrDiff{ + Old: "baz", New: "", NewRemoved: true, + }, + "listener.206423021.instance_port": &terraform.ResourceAttrDiff{ + Old: "8000", New: "0", NewRemoved: true, + }, + "listener.206423021.lb_port": &terraform.ResourceAttrDiff{ + Old: "80", New: "0", NewRemoved: true, + }, + "listener.206423021.lb_protocol": &terraform.ResourceAttrDiff{ + Old: "http", New: "", NewRemoved: true, + }, + "listener.206423021.ssl_certificate_id": &terraform.ResourceAttrDiff{ + Old: "", New: "", NewRemoved: true, + }, + "listener.3931999347.instance_protocol": &terraform.ResourceAttrDiff{ + Old: "", New: "http", NewRemoved: false, + }, + "listener.206423021.instance_protocol": &terraform.ResourceAttrDiff{ + Old: "http", New: "", NewRemoved: true, + }, + "listener.3931999347.instance_port": &terraform.ResourceAttrDiff{ + Old: "", New: "8080", NewRemoved: false, + }, + "listener.3931999347.lb_port": &terraform.ResourceAttrDiff{ + Old: "", New: "80", NewRemoved: false, + }, + "listener.3931999347.lb_protocol": &terraform.ResourceAttrDiff{ + Old: "", New: "http", NewRemoved: false, + }, + "tags.%": &terraform.ResourceAttrDiff{ + Old: "1", New: "0", NewRemoved: false, + }, + }, + }, + Source: &MultiLevelFieldReader{ + Levels: []string{"state", "config"}, + Readers: readers, + }, + } + + result, err := dfr.ReadField([]string{"listener"}) + if err != nil { + t.Fatalf("ReadField failed: %s", err) + } + expectedResult := NewSet(resourceAwsElbListenerHash, []interface{}{ + map[string]interface{}{ + "lb_port": 80, + "lb_protocol": "http", + "ssl_certificate_id": "", + "instance_port": 8080, + "instance_protocol": "http", + }, + }) + if !expectedResult.Equal(result.Value) { + t.Fatalf("ReadField returned unexpected result.\nGiven: %#v\nexpected: %#v", + result, expectedResult) + } +} + +func TestDiffFieldReader_CodeDeploy_special(t *testing.T) { + resourceAwsCodeDeployTriggerConfigHash := func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + if v, ok := m["trigger_name"]; ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + if v, ok := m["trigger_target_arn"]; ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + if triggerEvents, ok := m["trigger_events"]; ok { + names := triggerEvents.(*Set).List() + strings := make([]string, len(names)) + for i, raw := range names { + strings[i] = raw.(string) + } + sort.Strings(strings) + + for _, s := range strings { + buf.WriteString(fmt.Sprintf("%s-", s)) + } + } + return hashcode.String(buf.String()) + } + schema := map[string]*Schema{ + "trigger_configuration": &Schema{ + Type: TypeSet, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "trigger_events": &Schema{ + Type: TypeSet, + Required: true, + Set: HashString, + Elem: &Schema{ + Type: TypeString, + }, + }, + + "trigger_name": &Schema{ + Type: TypeString, + Required: true, + }, + + "trigger_target_arn": &Schema{ + Type: TypeString, + Required: true, + }, + }, + }, + Set: resourceAwsCodeDeployTriggerConfigHash, + }, + } + + var readers = make(map[string]FieldReader) + readers["state"] = &MapFieldReader{ + Schema: schema, + Map: BasicMapReader(map[string]string{ + "id": "b62bb84c-2d61-42b2-a50c-b629bf76027d", + "trigger_configuration.#": "1", + "trigger_configuration.2509701091.trigger_events.#": "1", + "trigger_configuration.2509701091.trigger_events.4157777861": "DeploymentFailure", + "trigger_configuration.2509701091.trigger_name": "foo-trigger", + "trigger_configuration.2509701091.trigger_target_arn": "arn:aws:sns:us-west-2:123456789012:foo-topic-rsimko-test", + }), + } + + dfr := &DiffFieldReader{ + Schema: schema, + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "trigger_configuration.2509701091.trigger_target_arn": &terraform.ResourceAttrDiff{ + Old: "arn:aws:sns:us-west-2:123456789012:foo-topic-rsimko-test", New: "", + NewComputed: false, NewRemoved: true, + }, + "trigger_configuration.2509701091.trigger_events.#": &terraform.ResourceAttrDiff{ + Old: "1", New: "0", + NewComputed: false, NewRemoved: false, + }, + "trigger_configuration.2509701091.trigger_events.4157777861": &terraform.ResourceAttrDiff{ + Old: "DeploymentFailure", New: "", + NewComputed: false, NewRemoved: true, + }, + "trigger_configuration.2509701091.trigger_name": &terraform.ResourceAttrDiff{ + Old: "foo-trigger", New: "", + NewComputed: false, NewRemoved: true, + }, + + "trigger_configuration.3738386998.trigger_events.#": &terraform.ResourceAttrDiff{ + Old: "0", New: "2", + NewComputed: false, NewRemoved: false, + }, + "trigger_configuration.3738386998.trigger_name": &terraform.ResourceAttrDiff{ + Old: "", New: "foo-trigger", + NewComputed: false, NewRemoved: false, + }, + "trigger_configuration.3738386998.trigger_events.4157777861": &terraform.ResourceAttrDiff{ + Old: "", New: "DeploymentFailure", + NewComputed: false, NewRemoved: false, + }, + "trigger_configuration.3738386998.trigger_events.3108600758": &terraform.ResourceAttrDiff{ + Old: "", New: "DeploymentSuccess", + NewComputed: false, NewRemoved: false, + }, + "trigger_configuration.3738386998.trigger_target_arn": &terraform.ResourceAttrDiff{ + Old: "", New: "arn:aws:sns:us-west-2:123456789012:foo-topic-rsimko-test", + NewComputed: false, NewRemoved: false, + }, + }, + Destroy: false, + DestroyTainted: false, + }, + Source: &MultiLevelFieldReader{ + Levels: []string{"state", "config"}, + Readers: readers, + }, + } + + result, err := dfr.ReadField([]string{"trigger_configuration"}) + if err != nil { + t.Fatalf("ReadField failed: %s", err) + } + + list := result.Value.(*Set).List() + m := list[0].(map[string]interface{}) + + expectedEvents := NewSet(HashString, []interface{}{"DeploymentSuccess", "DeploymentFailure"}) + if !m["trigger_events"].(*Set).Equal(expectedEvents) { + t.Fatalf("ReadField returned unexpected trigger_events.\nGiven: %#v\nexpected: %#v", + m["trigger_events"].(*Set).List(), expectedEvents.List()) + } + delete(m, "trigger_events") + + expectedResult := map[string]interface{}{ + "trigger_name": "foo-trigger", + "trigger_target_arn": "arn:aws:sns:us-west-2:123456789012:foo-topic-rsimko-test", + } + if !reflect.DeepEqual(m, expectedResult) { + t.Fatalf("ReadField returned unexpected result.\nGiven: %#v\nexpected: %#v", + m, expectedResult) + } +} + +func TestDiffFieldReader_Fastly_special(t *testing.T) { + schema := map[string]*Schema{ + "name": &Schema{ + Type: TypeString, + Required: true, + }, + + "domain": &Schema{ + Type: TypeSet, + Required: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "name": &Schema{ + Type: TypeString, + Required: true, + }, + + "comment": &Schema{ + Type: TypeString, + Optional: true, + }, + }, + }, + }, + + "default_ttl": &Schema{ + Type: TypeInt, + Optional: true, + Default: 3600, + }, + + "backend": &Schema{ + Type: TypeSet, + Required: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "name": &Schema{ + Type: TypeString, + Required: true, + }, + "address": &Schema{ + Type: TypeString, + Required: true, + }, + "auto_loadbalance": &Schema{ + Type: TypeBool, + Optional: true, + Default: true, + }, + "between_bytes_timeout": &Schema{ + Type: TypeInt, + Optional: true, + Default: 10000, + }, + "connect_timeout": &Schema{ + Type: TypeInt, + Optional: true, + Default: 1000, + }, + "error_threshold": &Schema{ + Type: TypeInt, + Optional: true, + Default: 0, + }, + "first_byte_timeout": &Schema{ + Type: TypeInt, + Optional: true, + Default: 15000, + }, + "max_conn": &Schema{ + Type: TypeInt, + Optional: true, + Default: 200, + }, + "port": &Schema{ + Type: TypeInt, + Optional: true, + Default: 80, + }, + "ssl_check_cert": &Schema{ + Type: TypeBool, + Optional: true, + Default: true, + }, + "weight": &Schema{ + Type: TypeInt, + Optional: true, + Default: 100, + }, + }, + }, + }, + + "force_destroy": &Schema{ + Type: TypeBool, + Optional: true, + }, + + "gzip": &Schema{ + Type: TypeSet, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "name": &Schema{ + Type: TypeString, + Required: true, + }, + "content_types": &Schema{ + Type: TypeSet, + Optional: true, + Elem: &Schema{Type: TypeString}, + }, + "extensions": &Schema{ + Type: TypeSet, + Optional: true, + Elem: &Schema{Type: TypeString}, + }, + "cache_condition": &Schema{ + Type: TypeString, + Computed: true, + }, + }, + }, + }, + } + + var readers = make(map[string]FieldReader) + readers["state"] = &MapFieldReader{ + Schema: schema, + Map: BasicMapReader(map[string]string{ + "backend.#": "1", + "backend.2044061536.address": "aws.amazon.com", + "backend.2044061536.auto_loadbalance": "true", + "backend.2044061536.between_bytes_timeout": "10000", + "backend.2044061536.connect_timeout": "1000", + "backend.2044061536.first_byte_timeout": "15000", + "backend.2044061536.max_conn": "200", + "backend.2044061536.name": "amazon docs", + "backend.2044061536.port": "80", + "backend.2044061536.ssl_check_cert": "true", + "backend.2044061536.weight": "100", + "default_ttl": "3600", + "domain.#": "1", + "domain.142573846.comment": "tf-testing-domain", + "domain.142573846.name": "tf-test-yada", + "force_destroy": "true", + "gzip.#": "2", "backend.2044061536.error_threshold": "0", + "gzip.3704620722.cache_condition": "", + "gzip.3704620722.content_types.#": "0", + "gzip.3704620722.extensions.#": "2", + "gzip.3704620722.extensions.253252853": "js", + "gzip.3704620722.extensions.3950613225": "css", + "gzip.3704620722.name": "gzip file types", + "gzip.3820313126.cache_condition": "", + "gzip.3820313126.content_types.#": "2", + "gzip.3820313126.content_types.366283795": "text/css", + "gzip.3820313126.content_types.4008173114": "text/html", + "gzip.3820313126.extensions.#": "0", + "gzip.3820313126.name": "gzip extensions", + "id": "1609878447410863288", + "name": "tf-test-yada", + }), + } + + dfr := &DiffFieldReader{ + Schema: schema, + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "gzip.#": &terraform.ResourceAttrDiff{ + Old: "2", New: "1", NewComputed: false, NewRemoved: false, + }, + + "gzip.3704620722.extensions.3950613225": &terraform.ResourceAttrDiff{ + Old: "css", New: "", NewComputed: false, NewRemoved: true, + }, + "gzip.3704620722.name": &terraform.ResourceAttrDiff{ + Old: "gzip file types", New: "", NewComputed: false, NewRemoved: true, + }, + "gzip.3704620722.content_types.#": &terraform.ResourceAttrDiff{ + Old: "0", New: "0", NewComputed: false, NewRemoved: false, + }, + "gzip.3704620722.extensions.#": &terraform.ResourceAttrDiff{ + Old: "2", New: "0", NewComputed: false, NewRemoved: false, + }, + "gzip.3704620722.extensions.253252853": &terraform.ResourceAttrDiff{ + Old: "js", New: "", NewComputed: false, NewRemoved: true, + }, + + "gzip.3820313126.name": &terraform.ResourceAttrDiff{ + Old: "gzip extensions", New: "", NewComputed: false, NewRemoved: true, + }, + "gzip.3820313126.extensions.#": &terraform.ResourceAttrDiff{ + Old: "0", New: "0", NewComputed: false, NewRemoved: false, + }, + "gzip.3820313126.content_types.#": &terraform.ResourceAttrDiff{ + Old: "2", New: "0", NewComputed: false, NewRemoved: false, + }, + "gzip.3820313126.content_types.4008173114": &terraform.ResourceAttrDiff{ + Old: "text/html", New: "", NewComputed: false, NewRemoved: true, + }, + "gzip.3820313126.content_types.366283795": &terraform.ResourceAttrDiff{ + Old: "text/css", New: "", NewComputed: false, NewRemoved: true, + }, + + "gzip.3694165387.extensions.#": &terraform.ResourceAttrDiff{ + Old: "0", New: "3", NewComputed: false, NewRemoved: false, + }, + "gzip.3694165387.extensions.3950613225": &terraform.ResourceAttrDiff{ + Old: "", New: "css", NewComputed: false, NewRemoved: false, + }, + "gzip.3694165387.extensions.253252853": &terraform.ResourceAttrDiff{ + Old: "", New: "js", NewComputed: false, NewRemoved: false, + }, + "gzip.3694165387.extensions.3010554278": &terraform.ResourceAttrDiff{ + Old: "", New: "html", NewComputed: false, NewRemoved: false, + }, + "gzip.3694165387.content_types.#": &terraform.ResourceAttrDiff{ + Old: "0", New: "5", NewComputed: false, NewRemoved: false, + }, + "gzip.3694165387.content_types.3132442313": &terraform.ResourceAttrDiff{ + Old: "", New: "application/x-javascript", NewComputed: false, NewRemoved: false, + }, + "gzip.3694165387.content_types.3453298448": &terraform.ResourceAttrDiff{ + Old: "", New: "application/javascript", NewComputed: false, NewRemoved: false, + }, + "gzip.3694165387.content_types.4008173114": &terraform.ResourceAttrDiff{ + Old: "", New: "text/html", NewComputed: false, NewRemoved: false, + }, + "gzip.3694165387.content_types.366283795": &terraform.ResourceAttrDiff{ + Old: "", New: "text/css", NewComputed: false, NewRemoved: false, + }, + "gzip.3694165387.content_types.1951959585": &terraform.ResourceAttrDiff{ + Old: "", New: "text/javascript", NewComputed: false, NewRemoved: false, + }, + "gzip.3694165387.cache_condition": &terraform.ResourceAttrDiff{ + Old: "", New: "", NewComputed: true, NewRemoved: false, + }, + "gzip.3694165387.name": &terraform.ResourceAttrDiff{ + Old: "", New: "all", NewComputed: false, NewRemoved: false, + }, + }, + Destroy: false, + DestroyTainted: false, + }, + Source: &MultiLevelFieldReader{ + Levels: []string{"state", "config"}, + Readers: readers, + }, + } + + result, err := dfr.ReadField([]string{"gzip"}) + if err != nil { + t.Fatalf("ReadField failed: %s", err) + } + + list := result.Value.(*Set).List() + m := list[0].(map[string]interface{}) + + expectedContentTypes := NewSet(HashSchema(&Schema{Type: TypeString}), []interface{}{ + "text/javascript", + "application/x-javascript", + "application/javascript", + "text/css", + "text/html", + }) + if !m["content_types"].(*Set).Equal(expectedContentTypes) { + t.Fatalf("ReadField returned unexpected content_types.\nGiven: %#v\nexpected: %#v", + m["content_types"].(*Set).List(), expectedContentTypes.List()) + } + delete(m, "content_types") + + expectedExtensions := NewSet(HashSchema(&Schema{Type: TypeString}), []interface{}{ + "js", "html", "css", + }) + if !m["extensions"].(*Set).Equal(expectedExtensions) { + t.Fatalf("ReadField returned unexpected extensions.\nGiven: %#v\nexpected: %#v", + m["extensions"].(*Set).List(), expectedExtensions.List()) + } + delete(m, "extensions") + + expectedGzip := map[string]interface{}{ + "name": "all", + "cache_condition": "", + } + if !reflect.DeepEqual(m, expectedGzip) { + t.Fatalf("ReadField returned unexpected result.\nGiven: %#v\nexpected: %#v", + m, expectedGzip) + } +} diff --git a/helper/schema/field_reader_map.go b/helper/schema/field_reader_map.go index fc3b5a02f593..837646d0c0c5 100644 --- a/helper/schema/field_reader_map.go +++ b/helper/schema/field_reader_map.go @@ -2,6 +2,7 @@ package schema import ( "fmt" + "strconv" "strings" ) @@ -24,7 +25,7 @@ func (r *MapFieldReader) ReadField(address []string) (FieldReadResult, error) { case TypeBool, TypeInt, TypeFloat, TypeString: return r.readPrimitive(address, schema) case TypeList: - return readListField(r, address, schema) + return r.readList(address, schema) case TypeMap: return r.readMap(k) case TypeSet: @@ -91,6 +92,57 @@ func (r *MapFieldReader) readPrimitive( }, nil } +func (r *MapFieldReader) readList( + address []string, schema *Schema) (FieldReadResult, error) { + + addrPadded := make([]string, len(address)+1) + copy(addrPadded, address) + + // Get the number of elements in the list + addrPadded[len(addrPadded)-1] = "#" + countResult, err := r.readPrimitive(addrPadded, &Schema{Type: TypeInt}) + if err != nil { + return FieldReadResult{}, err + } + if !countResult.Exists { + // No count, means we have no list + countResult.Value = 0 + } + + // If we have an empty list, then return an empty list + if countResult.Computed || countResult.Value.(int) == 0 { + return FieldReadResult{ + Value: []interface{}{}, + Exists: countResult.Exists, + Computed: countResult.Computed, + }, nil + } + + // Go through each count, and get the item value out of it + result := make([]interface{}, countResult.Value.(int)) + for i, _ := range result { + idx := strconv.Itoa(i) + addrPadded[len(addrPadded)-1] = idx + rawResult, err := r.ReadField(addrPadded) + if err != nil { + return FieldReadResult{}, err + } + 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. + panic("missing field in list: " + strings.Join(addrPadded, ".")) + } + + result[i] = rawResult.Value + } + + return FieldReadResult{ + Value: result, + Exists: true, + }, nil +} + func (r *MapFieldReader) readSet( address []string, schema *Schema) (FieldReadResult, error) { // Get the number of elements in the list diff --git a/helper/schema/field_reader_map_test.go b/helper/schema/field_reader_map_test.go index 279b9145bdea..aab8588a2076 100644 --- a/helper/schema/field_reader_map_test.go +++ b/helper/schema/field_reader_map_test.go @@ -106,3 +106,252 @@ func TestMapFieldReader_extra(t *testing.T) { } } } + +func TestMapFieldReader_SetInSet(t *testing.T) { + schema := map[string]*Schema{ + "main_set": &Schema{ + Type: TypeSet, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "inner_string_set": &Schema{ + Type: TypeSet, + Required: true, + Set: HashString, + Elem: &Schema{Type: TypeString}, + }, + }, + }, + }, + "main_int": &Schema{ + Type: TypeInt, + Optional: true, + }, + } + + r := &MapFieldReader{ + Schema: schema, + Map: BasicMapReader(map[string]string{ + "id": "8395051352714003426", + "main_int": "9", + "main_set.#": "1", + "main_set.2813616083.inner_string_set.#": "2", + "main_set.2813616083.inner_string_set.2654390964": "blue", + "main_set.2813616083.inner_string_set.3499814433": "green", + }), + } + + result, err := r.ReadField([]string{"main_set"}) + if err != nil { + t.Fatalf("ReadField failed: %#v", err) + } + + v := result.Value + if v == nil { + t.Fatal("Expected Value to be not nil") + } + list := v.(*Set).List() + if len(list) != 1 { + t.Fatalf("Expected exactly 1 instance, got %d", len(list)) + } + if list[0] == nil { + t.Fatalf("Expected value to be not nil: %#v", list) + } + + m := list[0].(map[string]interface{}) + set := m["inner_string_set"].(*Set).List() + + expectedSet := NewSet(HashString, []interface{}{"blue", "green"}).List() + + if !reflect.DeepEqual(set, expectedSet) { + t.Fatalf("Given: %#v\n\nExpected: %#v", set, expectedSet) + } +} + +func TestMapFieldReader_SetInList(t *testing.T) { + schema := map[string]*Schema{ + "main_list": &Schema{ + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "inner_string_set": &Schema{ + Type: TypeSet, + Required: true, + Set: HashString, + Elem: &Schema{Type: TypeString}, + }, + }, + }, + }, + "main_int": &Schema{ + Type: TypeInt, + Optional: true, + }, + } + + r := &MapFieldReader{ + Schema: schema, + Map: BasicMapReader(map[string]string{ + "id": "8395051352714003426", + "main_int": "9", + "main_list.#": "1", + "main_list.0.inner_string_set.#": "2", + "main_list.0.inner_string_set.2654390964": "blue", + "main_list.0.inner_string_set.3499814433": "green", + }), + } + + result, err := r.ReadField([]string{"main_list"}) + if err != nil { + t.Fatalf("ReadField failed: %#v", err) + } + + v := result.Value + if v == nil { + t.Fatal("Expected Value to be not nil") + } + list := v.([]interface{}) + if len(list) != 1 { + t.Fatalf("Expected exactly 1 instance, got %d", len(list)) + } + if list[0] == nil { + t.Fatalf("Expected value to be not nil: %#v", list) + } + + m := list[0].(map[string]interface{}) + set := m["inner_string_set"].(*Set).List() + + expectedSet := NewSet(HashString, []interface{}{"blue", "green"}).List() + + if !reflect.DeepEqual(set, expectedSet) { + t.Fatalf("Given: %#v\n\nExpected: %#v", set, expectedSet) + } +} + +func TestMapFieldReader_SetInList_complex(t *testing.T) { + schema := map[string]*Schema{ + "main_list": &Schema{ + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "inner_string_set": &Schema{ + Type: TypeSet, + Required: true, + Set: HashString, + Elem: &Schema{Type: TypeString}, + }, + "inner_int": &Schema{ + Type: TypeInt, + Required: true, + }, + }, + }, + }, + "main_int": &Schema{ + Type: TypeInt, + Optional: true, + }, + } + + r := &MapFieldReader{ + Schema: schema, + Map: BasicMapReader(map[string]string{ + "id": "8395051352714003426", + "main_int": "9", + "main_list.#": "1", + "main_list.0.inner_string_set.#": "2", + "main_list.0.inner_string_set.2654390964": "blue", + "main_list.0.inner_string_set.3499814433": "green", + "main_list.0.inner_int": "4", + }), + } + + result, err := r.ReadField([]string{"main_list"}) + if err != nil { + t.Fatalf("ReadField failed: %#v", err) + } + + v := result.Value + if v == nil { + t.Fatal("Expected Value to be not nil") + } + list := v.([]interface{}) + if len(list) != 1 { + t.Fatalf("Expected exactly 1 instance, got %d", len(list)) + } + if list[0] == nil { + t.Fatalf("Expected value to be not nil: %#v", list) + } + + m := list[0].(map[string]interface{}) + set := m["inner_string_set"].(*Set).List() + + expectedSet := NewSet(HashString, []interface{}{"blue", "green"}).List() + + if !reflect.DeepEqual(set, expectedSet) { + t.Fatalf("Given: %#v\n\nExpected: %#v", set, expectedSet) + } +} + +func TestMapFieldReader_readSet_SetInSet(t *testing.T) { + schema := map[string]*Schema{ + "main_set": &Schema{ + Type: TypeSet, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "inner_string_set": &Schema{ + Type: TypeSet, + Required: true, + Set: HashString, + Elem: &Schema{Type: TypeString}, + }, + }, + }, + }, + "main_int": &Schema{ + Type: TypeInt, + Optional: true, + }, + } + + r := &MapFieldReader{ + Schema: schema, + Map: BasicMapReader(map[string]string{ + "id": "8395051352714003426", + "main_int": "9", + "main_set.#": "1", + "main_set.2813616083.inner_string_set.#": "2", + "main_set.2813616083.inner_string_set.2654390964": "blue", + "main_set.2813616083.inner_string_set.3499814433": "green", + }), + } + + result, err := r.readSet([]string{"main_set"}, schema["main_set"]) + if err != nil { + t.Fatalf("readSet failed: %#v", err) + } + + v := result.Value + if v == nil { + t.Fatal("Expected Value to be not nil") + } + list := v.(*Set).List() + if len(list) != 1 { + t.Fatalf("Expected exactly 1 instance, got %d", len(list)) + } + if list[0] == nil { + t.Fatalf("Expected value to be not nil: %#v", list) + } + + m := list[0].(map[string]interface{}) + set := m["inner_string_set"].(*Set).List() + + expectedSet := NewSet(HashString, []interface{}{"blue", "green"}).List() + + if !reflect.DeepEqual(set, expectedSet) { + t.Fatalf("Given: %#v\n\nExpected: %#v", set, expectedSet) + } +} diff --git a/helper/schema/field_reader_multi_test.go b/helper/schema/field_reader_multi_test.go index 85286a66ed3d..1b482083d276 100644 --- a/helper/schema/field_reader_multi_test.go +++ b/helper/schema/field_reader_multi_test.go @@ -264,7 +264,361 @@ func TestMultiLevelFieldReaderReadFieldMerge(t *testing.T) { } if !reflect.DeepEqual(tc.Result, out) { - t.Fatalf("%s: bad: %#v", name, out) + t.Fatalf("Case %s:\ngiven: %#v\nexpected: %#v", name, out, tc.Result) } } } + +func TestMultiLevelFieldReader_ReadField_SetInSet(t *testing.T) { + schema := map[string]*Schema{ + "main_set": &Schema{ + Type: TypeSet, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "inner_string_set": &Schema{ + Type: TypeSet, + Required: true, + Set: HashString, + Elem: &Schema{Type: TypeString}, + }, + }, + }, + }, + "main_int": &Schema{ + Type: TypeInt, + Optional: true, + }, + } + + var readers = make(map[string]FieldReader) + readers["state"] = &MapFieldReader{ + Schema: schema, + Map: BasicMapReader(map[string]string{ + "id": "8395051352714003426", + "main_int": "9", + "main_set.#": "1", + "main_set.2813616083.inner_string_set.#": "2", + "main_set.2813616083.inner_string_set.2654390964": "blue", + "main_set.2813616083.inner_string_set.3499814433": "green", + }), + } + readers["diff"] = &DiffFieldReader{ + Schema: schema, + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "main_int": &terraform.ResourceAttrDiff{ + Old: "9", + New: "2", + }, + }, + }, + Source: &MultiLevelFieldReader{ + Levels: []string{"state", "config"}, + Readers: readers, + }, + } + + mr := &MultiLevelFieldReader{ + Levels: []string{ + "state", + "diff", + }, + + Readers: readers, + } + + result, err := mr.ReadField([]string{"main_set"}) + if err != nil { + t.Fatalf("ReadField failed: %#v", err) + } + + v := result.Value + if v == nil { + t.Fatal("Expected Value to be not nil") + } + list := v.(*Set).List() + if len(list) != 1 { + t.Fatalf("Expected exactly 1 instance, got %d", len(list)) + } + if list[0] == nil { + t.Fatalf("Expected value to be not nil: %#v", list) + } + + m := list[0].(map[string]interface{}) + set := m["inner_string_set"].(*Set).List() + + expectedSet := NewSet(HashString, []interface{}{"blue", "green"}).List() + + if !reflect.DeepEqual(set, expectedSet) { + t.Fatalf("Given: %#v\n\nExpected: %#v", set, expectedSet) + } +} + +func TestMultiLevelFieldReader_ReadField_SetInSet_complex(t *testing.T) { + schema := map[string]*Schema{ + "main_set": &Schema{ + Type: TypeSet, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "inner_string_set": &Schema{ + Type: TypeSet, + Required: true, + Set: HashString, + Elem: &Schema{Type: TypeString}, + }, + "inner_int": &Schema{ + Type: TypeInt, + Required: true, + }, + }, + }, + }, + "main_int": &Schema{ + Type: TypeInt, + Optional: true, + }, + } + + var readers = make(map[string]FieldReader) + readers["state"] = &MapFieldReader{ + Schema: schema, + Map: BasicMapReader(map[string]string{ + "id": "8395051352714003426", + "main_int": "9", + "main_set.#": "1", + "main_set.2476980464.inner_string_set.#": "2", + "main_set.2476980464.inner_string_set.2654390964": "blue", + "main_set.2476980464.inner_string_set.3499814433": "green", + "main_set.2476980464.inner_int": "4", + }), + } + readers["diff"] = &DiffFieldReader{ + Schema: schema, + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "main_int": &terraform.ResourceAttrDiff{ + Old: "9", + New: "2", + }, + }, + }, + Source: &MultiLevelFieldReader{ + Levels: []string{"state", "config"}, + Readers: readers, + }, + } + + mr := &MultiLevelFieldReader{ + Levels: []string{ + "state", + "diff", + }, + + Readers: readers, + } + + result, err := mr.ReadFieldMerge([]string{"main_set"}, "diff") + if err != nil { + t.Fatalf("ReadFieldMerge failed: %#v", err) + } + + v := result.Value + if v == nil { + t.Fatal("Expected Value to be not nil") + } + list := v.(*Set).List() + if len(list) != 1 { + t.Fatalf("Expected exactly 1 instance, got %d", len(list)) + } + if list[0] == nil { + t.Fatalf("Expected value to be not nil: %#v", list) + } + + m := list[0].(map[string]interface{}) + set := m["inner_string_set"].(*Set).List() + + expectedSet := NewSet(HashString, []interface{}{"blue", "green"}).List() + + if !reflect.DeepEqual(set, expectedSet) { + t.Fatalf("Given: %#v\n\nExpected: %#v", set, expectedSet) + } +} + +func TestMultiLevelFieldReader_ReadField_SetInList_simple(t *testing.T) { + schema := map[string]*Schema{ + "main_list": &Schema{ + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "inner_string_set": &Schema{ + Type: TypeSet, + Required: true, + Set: HashString, + Elem: &Schema{Type: TypeString}, + }, + }, + }, + }, + "main_int": &Schema{ + Type: TypeInt, + Optional: true, + }, + } + + var readers = make(map[string]FieldReader) + readers["state"] = &MapFieldReader{ + Schema: schema, + Map: BasicMapReader(map[string]string{ + "id": "8395051352714003426", + "main_int": "9", + "main_list.#": "1", + "main_list.0.inner_string_set.#": "2", + "main_list.0.inner_string_set.2654390964": "blue", + "main_list.0.inner_string_set.3499814433": "green", + }), + } + readers["diff"] = &DiffFieldReader{ + Schema: schema, + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "main_int": &terraform.ResourceAttrDiff{ + Old: "9", + New: "2", + }, + }, + }, + Source: &MultiLevelFieldReader{ + Levels: []string{"state", "config"}, + Readers: readers, + }, + } + + mr := &MultiLevelFieldReader{ + Levels: []string{ + "state", + "diff", + }, + + Readers: readers, + } + + result, err := mr.ReadField([]string{"main_list"}) + if err != nil { + t.Fatalf("ReadField failed: %#v", err) + } + + v := result.Value + if v == nil { + t.Fatal("Expected Value to be not nil") + } + list := v.([]interface{}) + if len(list) != 1 { + t.Fatalf("Expected exactly 1 instance, got %d", len(list)) + } + if list[0] == nil { + t.Fatalf("Expected value to be not nil: %#v", list) + } + + m := list[0].(map[string]interface{}) + set := m["inner_string_set"].(*Set).List() + + expectedSet := NewSet(HashString, []interface{}{"blue", "green"}).List() + + if !reflect.DeepEqual(set, expectedSet) { + t.Fatalf("Given: %#v\n\nExpected: %#v", set, expectedSet) + } +} + +func TestMultiLevelFieldReader_ReadField_SetInList_complex(t *testing.T) { + schema := map[string]*Schema{ + "main_list": &Schema{ + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "inner_string_set": &Schema{ + Type: TypeSet, + Required: true, + Set: HashString, + Elem: &Schema{Type: TypeString}, + }, + "inner_int": &Schema{ + Type: TypeInt, + Required: true, + }, + }, + }, + }, + "main_int": &Schema{ + Type: TypeInt, + Optional: true, + }, + } + + var readers = make(map[string]FieldReader) + readers["state"] = &MapFieldReader{ + Schema: schema, + Map: BasicMapReader(map[string]string{ + "id": "8395051352714003426", + "main_int": "9", + "main_list.#": "1", + "main_list.0.inner_string_set.#": "2", + "main_list.0.inner_string_set.2654390964": "blue", + "main_list.0.inner_string_set.3499814433": "green", + "main_list.0.inner_int": "4", + }), + } + readers["diff"] = &DiffFieldReader{ + Schema: schema, + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "main_int": &terraform.ResourceAttrDiff{ + Old: "9", + New: "2", + }, + }, + }, + Source: &MultiLevelFieldReader{ + Levels: []string{"state", "config"}, + Readers: readers, + }, + } + + mr := &MultiLevelFieldReader{ + Levels: []string{ + "state", + "diff", + }, + + Readers: readers, + } + + result, err := mr.ReadFieldMerge([]string{"main_list"}, "diff") + if err != nil { + t.Fatalf("ReadFieldMerge failed: %#v", err) + } + + v := result.Value + if v == nil { + t.Fatal("Expected Value to be not nil") + } + list := v.([]interface{}) + if len(list) != 1 { + t.Fatalf("Expected exactly 1 instance, got %d", len(list)) + } + if list[0] == nil { + t.Fatalf("Expected value to be not nil: %#v", list) + } + + m := list[0].(map[string]interface{}) + set := m["inner_string_set"].(*Set).List() + + expectedSet := NewSet(HashString, []interface{}{"blue", "green"}).List() + + if !reflect.DeepEqual(set, expectedSet) { + t.Fatalf("Given: %#v\n\nExpected: %#v", set, expectedSet) + } +} diff --git a/helper/schema/field_reader_test.go b/helper/schema/field_reader_test.go index c61fb8eb7323..003c98c37995 100644 --- a/helper/schema/field_reader_test.go +++ b/helper/schema/field_reader_test.go @@ -395,7 +395,68 @@ func testFieldReader(t *testing.T, f func(map[string]*Schema) FieldReader) { out.Value = s.List() } if !reflect.DeepEqual(tc.Result, out) { - t.Fatalf("%s: bad: %#v", name, out) + t.Fatalf("%s: Unexpected field result:\nGiven: %#v\nExpected: %#v", name, out, tc.Result) } } } + +func TestReadList_SetInList(t *testing.T) { + schema := map[string]*Schema{ + "main_list": &Schema{ + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "inner_string_set": &Schema{ + Type: TypeSet, + Required: true, + Set: HashString, + Elem: &Schema{Type: TypeString}, + }, + }, + }, + }, + "main_int": &Schema{ + Type: TypeInt, + Optional: true, + }, + } + + r := &MapFieldReader{ + Schema: schema, + Map: BasicMapReader(map[string]string{ + "id": "8395051352714003426", + "main_int": "9", + "main_list.#": "1", + "main_list.0.inner_string_set.#": "2", + "main_list.0.inner_string_set.2654390964": "blue", + "main_list.0.inner_string_set.3499814433": "green", + }), + } + + result, err := r.readList([]string{"main_list"}, schema["main_list"]) + if err != nil { + t.Fatalf("readListField failed: %#v", err) + } + + v := result.Value + if v == nil { + t.Fatal("Expected Value to be not nil") + } + list := v.([]interface{}) + if len(list) != 1 { + t.Fatalf("Expected exactly 1 instance, got %d", len(list)) + } + if list[0] == nil { + t.Fatalf("Expected value to be not nil: %#v", list) + } + + m := list[0].(map[string]interface{}) + set := m["inner_string_set"].(*Set).List() + + expectedSet := NewSet(HashString, []interface{}{"blue", "green"}).List() + + if !reflect.DeepEqual(set, expectedSet) { + t.Fatalf("Given: %#v\n\nExpected: %#v", set, expectedSet) + } +} diff --git a/helper/schema/resource_data_test.go b/helper/schema/resource_data_test.go index 0e6d1b2dc396..ea885f22b9eb 100644 --- a/helper/schema/resource_data_test.go +++ b/helper/schema/resource_data_test.go @@ -1,10 +1,14 @@ package schema import ( + "bytes" + "fmt" "math" "reflect" + "strings" "testing" + "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/terraform" ) @@ -751,6 +755,212 @@ func TestResourceDataGet(t *testing.T) { } } +func TestSetInsideSet(t *testing.T) { + _schema := map[string]*Schema{ + "main_set": &Schema{ + Type: TypeSet, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "inner_string_set": &Schema{ + Type: TypeSet, + Required: true, + Set: HashString, + Elem: &Schema{Type: TypeString}, + }, + }, + }, + }, + "main_int": &Schema{ + Type: TypeInt, + Optional: true, + }, + } + + existingState := &terraform.InstanceState{ + ID: "8395051352714003426", + Attributes: map[string]string{ + "id": "8395051352714003426", + "main_int": "9", + "main_set.#": "1", + "main_set.2813616083.inner_string_set.#": "2", + "main_set.2813616083.inner_string_set.2654390964": "blue", + "main_set.2813616083.inner_string_set.3499814433": "green", + }, + Meta: map[string]string{}, + Tainted: false, + } + + suggestedDiff := &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "main_int": &terraform.ResourceAttrDiff{ + Old: "9", + New: "2", + }, + }, + } + + d := &ResourceData{ + schema: _schema, + state: existingState, + diff: suggestedDiff, + } + + v := d.Get("main_set").(*Set).List() + if len(v) != 1 { + t.Fatalf("Expected exactly 1 instance of main_set, got %d", len(v)) + } + if v[0] == nil { + t.Fatalf("Expected main_set to be not nil: %#v", v) + } + + m := v[0].(map[string]interface{}) + set := m["inner_string_set"].(*Set).List() + expectedSet := NewSet(HashString, []interface{}{"blue", "green"}).List() + if !reflect.DeepEqual(set, expectedSet) { + t.Fatalf("Given: %#v\n\nExpected: %#v", set, expectedSet) + } +} + +func TestSetInsideList_simple(t *testing.T) { + _schema := map[string]*Schema{ + "main_list": &Schema{ + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "inner_string_set": &Schema{ + Type: TypeSet, + Required: true, + Set: HashString, + Elem: &Schema{Type: TypeString}, + }, + }, + }, + }, + "main_int": &Schema{ + Type: TypeInt, + Optional: true, + }, + } + + existingState := &terraform.InstanceState{ + ID: "8395051352714003426", + Attributes: map[string]string{ + "id": "8395051352714003426", + "main_int": "9", + "main_list.#": "1", + "main_list.0.inner_string_set.#": "2", + "main_list.0.inner_string_set.2654390964": "blue", + "main_list.0.inner_string_set.3499814433": "green", + }, + Meta: map[string]string{}, + Tainted: false, + } + + suggestedDiff := &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "main_int": &terraform.ResourceAttrDiff{ + Old: "9", + New: "2", + }, + }, + } + + d := &ResourceData{ + schema: _schema, + state: existingState, + diff: suggestedDiff, + } + + v := d.Get("main_list").([]interface{}) + if len(v) != 1 { + t.Fatalf("Expected exactly 1 instance of main_list, got %d", len(v)) + } + if v[0] == nil { + t.Fatalf("Expected main_list to be not nil: %#v", v) + } + + m := v[0].(map[string]interface{}) + set := m["inner_string_set"].(*Set).List() + expectedSet := NewSet(HashString, []interface{}{"blue", "green"}).List() + if !reflect.DeepEqual(set, expectedSet) { + t.Fatalf("Given: %#v\n\nExpected: %#v", set, expectedSet) + } +} + +func TestSetInsideList_complex(t *testing.T) { + _schema := map[string]*Schema{ + "main_list": &Schema{ + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "inner_string_set": &Schema{ + Type: TypeSet, + Required: true, + Set: HashString, + Elem: &Schema{Type: TypeString}, + }, + "inner_int": &Schema{ + Type: TypeInt, + Required: true, + }, + }, + }, + }, + "main_int": &Schema{ + Type: TypeInt, + Optional: true, + }, + } + + existingState := &terraform.InstanceState{ + ID: "8395051352714003426", + Attributes: map[string]string{ + "id": "8395051352714003426", + "main_int": "9", + "main_list.#": "1", + "main_list.0.inner_string_set.#": "2", + "main_list.0.inner_string_set.2654390964": "blue", + "main_list.0.inner_string_set.3499814433": "green", + "main_list.0.inner_int": "4", + }, + Meta: map[string]string{}, + Tainted: false, + } + + suggestedDiff := &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "main_int": &terraform.ResourceAttrDiff{ + Old: "9", + New: "2", + }, + }, + } + + d := &ResourceData{ + schema: _schema, + state: existingState, + diff: suggestedDiff, + } + + v := d.Get("main_list").([]interface{}) + if len(v) != 1 { + t.Fatalf("Expected exactly 1 instance of main_list, got %d", len(v)) + } + if v[0] == nil { + t.Fatalf("Expected main_list to be not nil: %#v", v) + } + + m := v[0].(map[string]interface{}) + set := m["inner_string_set"].(*Set).List() + expectedSet := NewSet(HashString, []interface{}{"blue", "green"}).List() + if !reflect.DeepEqual(set, expectedSet) { + t.Fatalf("Given: %#v\n\nExpected: %#v", set, expectedSet) + } +} + func TestResourceDataGetChange(t *testing.T) { cases := []struct { Schema map[string]*Schema @@ -3021,6 +3231,150 @@ func TestResourceDataSetType(t *testing.T) { } } +func TestResourceDataGet_setItemRemoved(t *testing.T) { + resourceAwsElbListenerHash := func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%d-", m["instance_port"].(int))) + buf.WriteString(fmt.Sprintf("%s-", + strings.ToLower(m["instance_protocol"].(string)))) + buf.WriteString(fmt.Sprintf("%d-", m["lb_port"].(int))) + buf.WriteString(fmt.Sprintf("%s-", + strings.ToLower(m["lb_protocol"].(string)))) + + if v, ok := m["ssl_certificate_id"]; ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + return hashcode.String(buf.String()) + } + + schema := map[string]*Schema{ + "listener": &Schema{ + Type: TypeSet, + Required: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "instance_port": &Schema{ + Type: TypeInt, + Required: true, + }, + + "instance_protocol": &Schema{ + Type: TypeString, + Required: true, + }, + + "lb_port": &Schema{ + Type: TypeInt, + Required: true, + }, + + "lb_protocol": &Schema{ + Type: TypeString, + Required: true, + }, + + "ssl_certificate_id": &Schema{ + Type: TypeString, + Optional: true, + }, + }, + }, + Set: resourceAwsElbListenerHash, + }, + + "tags": &Schema{ + Type: TypeMap, + Optional: true, + }, + } + + existingState := &terraform.InstanceState{ + ID: "tf-lb-viox4pkvmfd5dbjf2qwiqmxtaq", + Attributes: map[string]string{ + "id": "tf-lb-t77qtnbbm5dczcnker4c7boj7m", + "listener.#": "1", + "listener.206423021.instance_port": "8000", + "listener.206423021.instance_protocol": "http", + "listener.206423021.lb_port": "80", + "listener.206423021.lb_protocol": "http", + "listener.206423021.ssl_certificate_id": "", + "tags.%": "1", + "tags.bar": "baz", + }, + } + + suggestedDiff := &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "listener.3931999347.ssl_certificate_id": &terraform.ResourceAttrDiff{ + Old: "", New: "", NewRemoved: false, + }, + "tags.bar": &terraform.ResourceAttrDiff{ + Old: "baz", New: "", NewRemoved: true, + }, + "listener.206423021.instance_port": &terraform.ResourceAttrDiff{ + Old: "8000", New: "0", NewRemoved: true, + }, + "listener.206423021.lb_port": &terraform.ResourceAttrDiff{ + Old: "80", New: "0", NewRemoved: true, + }, + "listener.206423021.lb_protocol": &terraform.ResourceAttrDiff{ + Old: "http", New: "", NewRemoved: true, + }, + "listener.206423021.ssl_certificate_id": &terraform.ResourceAttrDiff{ + Old: "", New: "", NewRemoved: true, + }, + "listener.3931999347.instance_protocol": &terraform.ResourceAttrDiff{ + Old: "", New: "http", NewRemoved: false, + }, + "listener.206423021.instance_protocol": &terraform.ResourceAttrDiff{ + Old: "http", New: "", NewRemoved: true, + }, + "listener.3931999347.instance_port": &terraform.ResourceAttrDiff{ + Old: "", New: "8080", NewRemoved: false, + }, + "listener.3931999347.lb_port": &terraform.ResourceAttrDiff{ + Old: "", New: "80", NewRemoved: false, + }, + "listener.3931999347.lb_protocol": &terraform.ResourceAttrDiff{ + Old: "", New: "http", NewRemoved: false, + }, + "tags.%": &terraform.ResourceAttrDiff{ + Old: "1", New: "0", NewRemoved: false, + }, + }, + } + + d := &ResourceData{ + schema: schema, + state: existingState, + diff: suggestedDiff, + } + + set := d.Get("listener").(*Set) + if len(set.List()) != 1 { + t.Fatalf("Expected exactly 1 instance of listener, got %d", len(set.List())) + } + if set.List()[0] == nil { + t.Fatalf("Expected listener to be not nil: %#v", set.List()) + } + + expectedSet := NewSet(resourceAwsElbListenerHash, []interface{}{ + map[string]interface{}{ + "lb_port": 80, + "lb_protocol": "http", + "ssl_certificate_id": "", + "instance_port": 8080, + "instance_protocol": "http", + }, + }) + + if !expectedSet.Equal(set) { + t.Fatalf("Given: %#v\n\nExpected: %#v", set, expectedSet.List()) + } +} + func testPtrTo(raw interface{}) interface{} { return &raw } diff --git a/helper/schema/set.go b/helper/schema/set.go index de05f40eed91..0d5bebd7c01a 100644 --- a/helper/schema/set.go +++ b/helper/schema/set.go @@ -98,6 +98,10 @@ func (s *Set) List() []interface{} { return result } +func (s *Set) Map() map[string]interface{} { + return s.m +} + // Difference performs a set difference of the two sets, returning // a new third set that has only the elements unique to this set. func (s *Set) Difference(other *Set) *Set {