diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go index e34689b06..5a2f0b710 100644 --- a/helper/schema/resource_data.go +++ b/helper/schema/resource_data.go @@ -173,11 +173,83 @@ func (d *ResourceData) get( return d.getList(k, parts, schema, source) case TypeMap: return d.getMap(k, parts, schema, source) - default: + case TypeSet: + return d.getSet(k, parts, schema, source) + case TypeBool: + fallthrough + case TypeInt: + fallthrough + case TypeString: return d.getPrimitive(k, parts, schema, source) + default: + panic(fmt.Sprintf("%s: unknown type %s", k, schema.Type)) } } +func (d *ResourceData) getSet( + k string, + parts []string, + schema *Schema, + source getSource) interface{} { + raw := d.getList(k, nil, schema, source) + if raw == nil { + return nil + } + + list := raw.([]interface{}) + if len(list) == 0 { + return nil + } + + // This is a reverse map of hash code => index in config used to + // resolve direct set item lookup for turning into state. Confused? + // Read on... + // + // To create the state (the state* functions), a Get call is done + // with a full key such as "ports.0". The index of a set ("0") doesn't + // make a lot of sense, but we need to deterministically list out + // elements of the set like this. Luckily, same sets have a deterministic + // List() output, so we can use that to look things up. + // + // This mapping makes it so that we can look up the hash code of an + // object back to its index in the REAL config. + var indexMap map[int]int + if len(parts) > 0 { + indexMap = make(map[int]int) + } + + // Build the set from all the items using the given hash code + s := &Set{F: schema.Set} + for i, v := range list { + code := s.add(v) + if indexMap != nil { + indexMap[code] = i + } + } + + // If we're trying to get a specific element, then rewrite the + // index to be just that, then jump direct to getList. + if len(parts) > 0 { + index := parts[0] + indexInt, err := strconv.ParseInt(index, 0, 0) + if err != nil { + return nil + } + + codes := s.listCode() + if int(indexInt) >= len(codes) { + return nil + } + code := codes[indexInt] + realIndex := indexMap[code] + + parts[0] = strconv.FormatInt(int64(realIndex), 10) + return d.getList(k, parts, schema, source) + } + + return s +} + func (d *ResourceData) getMap( k string, parts []string, @@ -241,6 +313,11 @@ func (d *ResourceData) getMap( } } + // If we're requesting a specific element, return that + if len(parts) > 0 { + return result[parts[0]] + } + return result } @@ -657,7 +734,7 @@ func (d *ResourceData) stateObject( func (d *ResourceData) statePrimitive( prefix string, schema *Schema) map[string]string { - v := d.getPrimitive(prefix, nil, schema, getSourceSet) + v := d.Get(prefix) if v == nil { return nil } @@ -679,6 +756,37 @@ func (d *ResourceData) statePrimitive( } } +func (d *ResourceData) stateSet( + prefix string, + schema *Schema) map[string]string { + raw := d.get(prefix, nil, schema, getSourceSet) + if raw == nil { + return nil + } + + set := raw.(*Set) + list := set.List() + result := make(map[string]string) + result[prefix+".#"] = strconv.FormatInt(int64(len(list)), 10) + for i := 0; i < len(list); i++ { + key := fmt.Sprintf("%s.%d", prefix, i) + + var m map[string]string + switch t := schema.Elem.(type) { + case *Resource: + m = d.stateObject(key, t.Schema) + case *Schema: + m = d.stateSingle(key, t) + } + + for k, v := range m { + result[k] = v + } + } + + return result +} + func (d *ResourceData) stateSingle( prefix string, schema *Schema) map[string]string { @@ -687,7 +795,15 @@ func (d *ResourceData) stateSingle( return d.stateList(prefix, schema) case TypeMap: return d.stateMap(prefix, schema) - default: + case TypeSet: + return d.stateSet(prefix, schema) + case TypeBool: + fallthrough + case TypeInt: + fallthrough + case TypeString: return d.statePrimitive(prefix, schema) + default: + panic(fmt.Sprintf("%s: unknown type %s", prefix, schema.Type)) } } diff --git a/helper/schema/resource_data_test.go b/helper/schema/resource_data_test.go index 36f2e92eb..ec00c0d10 100644 --- a/helper/schema/resource_data_test.go +++ b/helper/schema/resource_data_test.go @@ -444,6 +444,34 @@ func TestResourceDataGet(t *testing.T) { Value: []interface{}{}, }, + + // Sets + { + Schema: map[string]*Schema{ + "ports": &Schema{ + Type: TypeSet, + Optional: true, + Computed: true, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + }, + + State: &terraform.ResourceState{ + Attributes: map[string]string{ + "ports.#": "1", + "ports.0": "80", + }, + }, + + Diff: nil, + + Key: "ports", + + Value: []interface{}{80}, + }, } for i, tc := range cases { @@ -453,6 +481,10 @@ func TestResourceDataGet(t *testing.T) { } v := d.Get(tc.Key) + if s, ok := v.(*Set); ok { + v = s.List() + } + if !reflect.DeepEqual(v, tc.Value) { t.Fatalf("Bad: %d\n\n%#v", i, v) } @@ -1346,6 +1378,39 @@ func TestResourceDataState(t *testing.T) { }, }, }, + + // Sets + { + Schema: map[string]*Schema{ + "ports": &Schema{ + Type: TypeSet, + Optional: true, + Computed: true, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + }, + + State: &terraform.ResourceState{ + Attributes: map[string]string{ + "ports.#": "2", + "ports.0": "100", + "ports.1": "80", + }, + }, + + Diff: nil, + + Result: &terraform.ResourceState{ + Attributes: map[string]string{ + "ports.#": "2", + "ports.0": "80", + "ports.1": "100", + }, + }, + }, } for i, tc := range cases { diff --git a/helper/schema/schema.go b/helper/schema/schema.go index 296366d11..ae57e10df 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -20,6 +20,7 @@ const ( TypeString TypeList TypeMap + TypeSet ) // Schema is used to describe the structure of a value. @@ -43,18 +44,19 @@ type Schema struct { Computed bool ForceNew bool - // The following fields are only set for a TypeList Type. + // The following fields are only set for a TypeList or TypeSet Type. // // Elem must be either a *Schema or a *Resource only if the Type is // TypeList, and represents what the element type is. If it is *Schema, // the element type is just a simple value. If it is *Resource, the // element type is a complex structure, potentially with its own lifecycle. + Elem interface{} + + // The follow fields are only valid for a TypeSet type. // - // Order defines a function to be called to order the elements in the - // list. See SchemaOrderFunc for more info. If Order is set, then any - // access of this list will result in the ordered list. - Elem interface{} - Order SchemaOrderFunc + // Set defines a function to determine the unique ID of an item so that + // a proper set can be built. + Set SchemaSetFunc // ComputedWhen is a set of queries on the configuration. Whenever any // of these things is changed, it will require a recompute (this requires @@ -62,9 +64,9 @@ type Schema struct { ComputedWhen []string } -// SchemaOrderFunc is the function used to compare two elements in a list -// for ordering. It should return a boolean true if a is less than b. -type SchemaOrderFunc func(a, b interface{}) bool +// SchemaSetFunc is a function that must return a unique ID for the given +// element. This unique ID is used to store the element in a hash. +type SchemaSetFunc func(a interface{}) int func (s *Schema) finalizeDiff( d *terraform.ResourceAttrDiff) *terraform.ResourceAttrDiff { @@ -181,6 +183,11 @@ func (m schemaMap) InternalValidate() error { return fmt.Errorf("%s: Elem must be set for lists", k) } + // TODO: test + if v.Set != nil { + return fmt.Errorf("%s: Set can only be set for TypeSet", k) + } + switch t := v.Elem.(type) { case *Resource: if err := t.InternalValidate(); err != nil { diff --git a/helper/schema/schema_sort.go b/helper/schema/schema_sort.go deleted file mode 100644 index d803250c8..000000000 --- a/helper/schema/schema_sort.go +++ /dev/null @@ -1,20 +0,0 @@ -package schema - -// listSort implements sort.Interface to sort a list of []interface according -// to a schema. -type listSort struct { - List []interface{} - Schema *Schema -} - -func (s *listSort) Len() int { - return len(s.List) -} - -func (s *listSort) Less(i, j int) bool { - return s.Schema.Order(s.List[i], s.List[j]) -} - -func (s *listSort) Swap(i, j int) { - s.List[i], s.List[j] = s.List[j], s.List[i] -} diff --git a/helper/schema/schema_sort_test.go b/helper/schema/schema_sort_test.go deleted file mode 100644 index 6a86f338d..000000000 --- a/helper/schema/schema_sort_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package schema - -import ( - "reflect" - "sort" - "testing" -) - -func TestListSort_impl(t *testing.T) { - var _ sort.Interface = new(listSort) -} - -func TestListSort(t *testing.T) { - s := &listSort{ - List: []interface{}{5, 2, 1, 3, 4}, - Schema: &Schema{ - Order: func(a, b interface{}) bool { - return a.(int) < b.(int) - }, - }, - } - - sort.Sort(s) - - expected := []interface{}{1, 2, 3, 4, 5} - if !reflect.DeepEqual(s.List, expected) { - t.Fatalf("bad: %#v", s.List) - } -} diff --git a/helper/schema/schema_test.go b/helper/schema/schema_test.go index 0b60f4305..3215081ae 100644 --- a/helper/schema/schema_test.go +++ b/helper/schema/schema_test.go @@ -310,7 +310,7 @@ func TestSchemaMap_Diff(t *testing.T) { Diff: &terraform.ResourceDiff{ Attributes: map[string]*terraform.ResourceAttrDiff{ "ports.#": &terraform.ResourceAttrDiff{ - Old: "", + Old: "", NewComputed: true, }, }, @@ -323,11 +323,11 @@ func TestSchemaMap_Diff(t *testing.T) { { Schema: map[string]*Schema{ "ports": &Schema{ - Type: TypeList, + Type: TypeSet, Required: true, Elem: &Schema{Type: TypeInt}, - Order: func(a, b interface{}) bool { - return a.(int) < b.(int) + Set: func(a interface{}) int { + return a.(int) }, }, }, diff --git a/helper/schema/set.go b/helper/schema/set.go new file mode 100644 index 000000000..479ff3db9 --- /dev/null +++ b/helper/schema/set.go @@ -0,0 +1,58 @@ +package schema + +import ( + "sort" + "sync" +) + +// Set is a set data structure that is returned for elements of type +// TypeSet. +type Set struct { + F SchemaSetFunc + + m map[int]interface{} + once sync.Once +} + +// Add adds an item to the set if it isn't already in the set. +func (s *Set) Add(item interface{}) { + s.add(item) +} + +// List returns the elements of this set in slice format. +// +// The order of the returned elements is deterministic. Given the same +// set, the order of this will always be the same. +func (s *Set) List() []interface{}{ + result := make([]interface{}, len(s.m)) + for i, k := range s.listCode() { + result[i] = s.m[k] + } + + return result +} + +func (s *Set) init() { + s.m = make(map[int]interface{}) +} + +func (s *Set) add(item interface{}) int { + s.once.Do(s.init) + + code := s.F(item) + if _, ok := s.m[code]; !ok { + s.m[code] = item + } + + return code +} + +func (s *Set) listCode() []int{ + // Sort the hash codes so the order of the list is deterministic + keys := make([]int, 0, len(s.m)) + for k, _ := range s.m { + keys = append(keys, k) + } + sort.Sort(sort.IntSlice(keys)) + return keys +}