diff --git a/helper/schema/field_writer.go b/helper/schema/field_writer.go new file mode 100644 index 000000000..9abc41b54 --- /dev/null +++ b/helper/schema/field_writer.go @@ -0,0 +1,8 @@ +package schema + +// FieldWriters are responsible for writing fields by address into +// a proper typed representation. ResourceData uses this to write new data +// into existing sources. +type FieldWriter interface { + WriteField([]string, interface{}) error +} diff --git a/helper/schema/field_writer_map.go b/helper/schema/field_writer_map.go new file mode 100644 index 000000000..fd6796377 --- /dev/null +++ b/helper/schema/field_writer_map.go @@ -0,0 +1,303 @@ +package schema + +import ( + "fmt" + "reflect" + "strconv" + "strings" + "sync" + + "github.com/mitchellh/mapstructure" +) + +// MapFieldWriter writes data into a single map[string]string structure. +type MapFieldWriter struct { + Schema map[string]*Schema + + lock sync.Mutex + result map[string]string +} + +// Map returns the underlying map that is being written to. +func (w *MapFieldWriter) Map() map[string]string { + w.lock.Lock() + defer w.lock.Unlock() + if w.result == nil { + w.result = make(map[string]string) + } + + return w.result +} + +func (w *MapFieldWriter) WriteField(addr []string, value interface{}) error { + w.lock.Lock() + defer w.lock.Unlock() + if w.result == nil { + w.result = make(map[string]string) + } + + schemaList := addrToSchema(addr, w.Schema) + if len(schemaList) == 0 { + return fmt.Errorf("Invalid address to set: %#v", addr) + } + + // If we're setting anything other than a list root or set root, + // then disallow it. + for _, schema := range schemaList[:len(schemaList)-1] { + if schema.Type == TypeList { + return fmt.Errorf( + "%s: can only set full list", + strings.Join(addr, ".")) + } + + if schema.Type == TypeMap { + return fmt.Errorf( + "%s: can only set full map", + strings.Join(addr, ".")) + } + + if schema.Type == TypeSet { + return fmt.Errorf( + "%s: can only set full set", + strings.Join(addr, ".")) + } + } + + return w.set(addr, value) +} + +func (w *MapFieldWriter) set(addr []string, value interface{}) error { + schemaList := addrToSchema(addr, w.Schema) + if len(schemaList) == 0 { + return fmt.Errorf("Invalid address to set: %#v", addr) + } + + schema := schemaList[len(schemaList)-1] + switch schema.Type { + case TypeBool: + fallthrough + case TypeInt: + fallthrough + case TypeString: + return w.setPrimitive(addr, value, schema) + case TypeList: + return w.setList(addr, value, schema) + case TypeMap: + return w.setMap(addr, value, schema) + case TypeSet: + return w.setSet(addr, value, schema) + case typeObject: + return w.setObject(addr, value, schema) + default: + panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) + } +} + +func (w *MapFieldWriter) setList( + addr []string, + v interface{}, + schema *Schema) error { + k := strings.Join(addr, ".") + setElement := func(idx string, value interface{}) error { + addrCopy := make([]string, len(addr), len(addr)+1) + copy(addrCopy, addr) + return w.set(append(addrCopy, idx), value) + } + + var vs []interface{} + if err := mapstructure.Decode(v, &vs); err != nil { + return fmt.Errorf("%s: %s", k, err) + } + + // Set the entire list. + var err error + for i, elem := range vs { + is := strconv.FormatInt(int64(i), 10) + err = setElement(is, elem) + if err != nil { + break + } + } + if err != nil { + for i, _ := range vs { + is := strconv.FormatInt(int64(i), 10) + setElement(is, nil) + } + + return err + } + + w.result[k+".#"] = strconv.FormatInt(int64(len(vs)), 10) + return nil +} + +func (w *MapFieldWriter) setMap( + addr []string, + value interface{}, + schema *Schema) error { + k := strings.Join(addr, ".") + v := reflect.ValueOf(value) + vs := make(map[string]interface{}) + + if value != nil { + if v.Kind() != reflect.Map { + return fmt.Errorf("%s: must be a map", k) + } + if v.Type().Key().Kind() != reflect.String { + return fmt.Errorf("%s: keys must strings", k) + } + for _, mk := range v.MapKeys() { + mv := v.MapIndex(mk) + vs[mk.String()] = mv.Interface() + } + } + + if len(vs) == 0 { + // The empty string here means the map is removed. + w.result[k] = "" + return nil + } + + // Remove the pure key since we're setting the full map value + delete(w.result, k) + + // Set each subkey + addrCopy := make([]string, len(addr), len(addr)+1) + copy(addrCopy, addr) + for subKey, v := range vs { + if err := w.set(append(addrCopy, subKey), v); err != nil { + return err + } + } + + return nil +} + +func (w *MapFieldWriter) setObject( + addr []string, + value interface{}, + schema *Schema) error { + // Set the entire object. First decode into a proper structure + var v map[string]interface{} + if err := mapstructure.Decode(value, &v); err != nil { + return fmt.Errorf("%s: %s", strings.Join(addr, "."), err) + } + + // Make space for additional elements in the address + addrCopy := make([]string, len(addr), len(addr)+1) + copy(addrCopy, addr) + + // Set each element in turn + var err error + for k1, v1 := range v { + if err = w.set(append(addrCopy, k1), v1); err != nil { + break + } + } + if err != nil { + for k1, _ := range v { + w.set(append(addrCopy, k1), nil) + } + } + + return err +} + +func (w *MapFieldWriter) setPrimitive( + addr []string, + v interface{}, + schema *Schema) error { + k := strings.Join(addr, ".") + + if v == nil { + delete(w.result, k) + return nil + } + + var set string + switch schema.Type { + case TypeBool: + var b bool + if err := mapstructure.Decode(v, &b); err != nil { + return fmt.Errorf("%s: %s", k, err) + } + + set = strconv.FormatBool(b) + case TypeString: + if err := mapstructure.Decode(v, &set); err != nil { + return fmt.Errorf("%s: %s", k, err) + } + case TypeInt: + var n int + if err := mapstructure.Decode(v, &n); err != nil { + return fmt.Errorf("%s: %s", k, err) + } + + set = strconv.FormatInt(int64(n), 10) + default: + return fmt.Errorf("Unknown type: %#v", schema.Type) + } + + w.result[k] = set + return nil +} + +func (w *MapFieldWriter) setSet( + addr []string, + value interface{}, + schema *Schema) error { + addrCopy := make([]string, len(addr), len(addr)+1) + copy(addrCopy, addr) + + // If it is a slice, then we have to turn it into a *Set so that + // we get the proper order back based on the hash code. + if v := reflect.ValueOf(value); v.Kind() == reflect.Slice { + // Build a temp *ResourceData to use for the conversion + tempSchema := *schema + tempSchema.Type = TypeList + tempSchemaMap := map[string]*Schema{addr[0]: &tempSchema} + tempW := &MapFieldWriter{Schema: tempSchemaMap} + + // Set the entire list, this lets us get sane values out of it + if err := tempW.WriteField(addr, value); err != nil { + return err + } + + // Build the set by going over the list items in order and + // hashing them into the set. The reason we go over the list and + // not the `value` directly is because this forces all types + // to become []interface{} (generic) instead of []string, which + // most hash functions are expecting. + s := &Set{F: schema.Set} + tempR := &MapFieldReader{ + Map: BasicMapReader(tempW.Map()), + Schema: tempSchemaMap, + } + for i := 0; i < v.Len(); i++ { + is := strconv.FormatInt(int64(i), 10) + result, err := tempR.ReadField(append(addrCopy, is)) + if err != nil { + return err + } + if !result.Exists { + panic("set item just set doesn't exist") + } + + s.Add(result.Value) + } + + value = s + } + + k := strings.Join(addr, ".") + for code, elem := range value.(*Set).m { + codeStr := strconv.FormatInt(int64(code), 10) + if err := w.set(append(addrCopy, codeStr), elem); err != nil { + return err + } + } + + w.result[k+".#"] = strconv.Itoa(value.(*Set).Len()) + return nil + +} diff --git a/helper/schema/field_writer_map_test.go b/helper/schema/field_writer_map_test.go new file mode 100644 index 000000000..38eaca94b --- /dev/null +++ b/helper/schema/field_writer_map_test.go @@ -0,0 +1,184 @@ +package schema + +import ( + "reflect" + "testing" +) + +func TestMapFieldWriter_impl(t *testing.T) { + var _ FieldWriter = new(MapFieldWriter) +} + +func TestMapFieldWriter(t *testing.T) { + schema := map[string]*Schema{ + "bool": &Schema{Type: TypeBool}, + "int": &Schema{Type: TypeInt}, + "string": &Schema{Type: TypeString}, + "list": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeString}, + }, + "listInt": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + }, + "map": &Schema{Type: TypeMap}, + "set": &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + "setDeep": &Schema{ + Type: TypeSet, + Elem: &Resource{ + Schema: map[string]*Schema{ + "index": &Schema{Type: TypeInt}, + "value": &Schema{Type: TypeString}, + }, + }, + Set: func(a interface{}) int { + return a.(map[string]interface{})["index"].(int) + }, + }, + } + + cases := map[string]struct { + Addr []string + Value interface{} + Err bool + Out map[string]string + }{ + "noexist": { + []string{"noexist"}, + 42, + true, + map[string]string{}, + }, + + "bool": { + []string{"bool"}, + false, + false, + map[string]string{ + "bool": "false", + }, + }, + + "int": { + []string{"int"}, + 42, + false, + map[string]string{ + "int": "42", + }, + }, + + "string": { + []string{"string"}, + "42", + false, + map[string]string{ + "string": "42", + }, + }, + + "list of strings": { + []string{"list"}, + []interface{}{"foo", "bar"}, + false, + map[string]string{ + "list.#": "2", + "list.0": "foo", + "list.1": "bar", + }, + }, + + "list element": { + []string{"list", "0"}, + "string", + true, + map[string]string{}, + }, + + "map": { + []string{"map"}, + map[string]interface{}{"foo": "bar"}, + false, + map[string]string{ + "map.foo": "bar", + }, + }, + + "map delete": { + []string{"map"}, + nil, + false, + map[string]string{ + "map": "", + }, + }, + + "map element": { + []string{"map", "foo"}, + "bar", + true, + map[string]string{}, + }, + + "set": { + []string{"set"}, + []interface{}{1, 2, 5}, + false, + map[string]string{ + "set.#": "3", + "set.1": "1", + "set.2": "2", + "set.5": "5", + }, + }, + + "set resource": { + []string{"setDeep"}, + []interface{}{ + map[string]interface{}{ + "index": 10, + "value": "foo", + }, + map[string]interface{}{ + "index": 50, + "value": "bar", + }, + }, + false, + map[string]string{ + "setDeep.#": "2", + "setDeep.10.index": "10", + "setDeep.10.value": "foo", + "setDeep.50.index": "50", + "setDeep.50.value": "bar", + }, + }, + + "set element": { + []string{"set", "5"}, + 5, + true, + map[string]string{}, + }, + } + + for name, tc := range cases { + w := &MapFieldWriter{Schema: schema} + err := w.WriteField(tc.Addr, tc.Value) + if (err != nil) != tc.Err { + t.Fatalf("%s: err: %s", name, err) + } + + actual := w.Map() + if !reflect.DeepEqual(actual, tc.Out) { + t.Fatalf("%s: bad: %#v", name, actual) + } + } +} diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go index 9afd1daa6..0889d294f 100644 --- a/helper/schema/resource_data.go +++ b/helper/schema/resource_data.go @@ -8,7 +8,6 @@ import ( "sync" "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/mapstructure" ) // ResourceData is used to query and set the attributes of a resource. @@ -29,7 +28,7 @@ type ResourceData struct { // Don't set multiReader *MultiLevelFieldReader - setMap map[string]string + setWriter *MapFieldWriter newState *terraform.InstanceState partial bool partialMap map[string]struct{} @@ -156,8 +155,7 @@ func (d *ResourceData) Partial(on bool) { // will be returned. func (d *ResourceData) Set(key string, value interface{}) error { d.once.Do(d.init) - parts := strings.Split(key, ".") - return d.setObject("", parts, d.schema, value) + return d.setWriter.WriteField(strings.Split(key, "."), value) } // SetPartial adds the key prefix to the final state output while @@ -243,7 +241,7 @@ func (d *ResourceData) init() { d.newState = ©State // Initialize the map for storing set data - d.setMap = make(map[string]string) + d.setWriter = &MapFieldWriter{Schema: d.schema} // Initialize the reader for getting data from the // underlying sources (config, diff, etc.) @@ -274,7 +272,7 @@ func (d *ResourceData) init() { } readers["set"] = &MapFieldReader{ Schema: d.schema, - Map: BasicMapReader(d.setMap), + Map: BasicMapReader(d.setWriter.Map()), } d.multiReader = &MultiLevelFieldReader{ Levels: []string{ @@ -383,277 +381,6 @@ func (d *ResourceData) get( } } -func (d *ResourceData) set( - k string, - parts []string, - schema *Schema, - value interface{}) error { - switch schema.Type { - case TypeList: - return d.setList(k, parts, schema, value) - case TypeMap: - return d.setMapValue(k, parts, schema, value) - case TypeSet: - return d.setSet(k, parts, schema, value) - case TypeBool: - fallthrough - case TypeInt: - fallthrough - case TypeString: - return d.setPrimitive(k, schema, value) - default: - panic(fmt.Sprintf("%s: unknown type %#v", k, schema.Type)) - } -} - -func (d *ResourceData) setList( - k string, - parts []string, - schema *Schema, - value interface{}) error { - if len(parts) > 0 { - return fmt.Errorf("%s: can only set the full list, not elements", k) - } - - setElement := func(k string, idx string, value interface{}) error { - if idx == "#" { - return fmt.Errorf("%s: can't set count of list", k) - } - - key := fmt.Sprintf("%s.%s", k, idx) - switch t := schema.Elem.(type) { - case *Resource: - return d.setObject(key, nil, t.Schema, value) - case *Schema: - return d.set(key, nil, t, value) - } - - return nil - } - - var vs []interface{} - if err := mapstructure.Decode(value, &vs); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - - // Set the entire list. - var err error - for i, elem := range vs { - is := strconv.FormatInt(int64(i), 10) - err = setElement(k, is, elem) - if err != nil { - break - } - } - if err != nil { - for i, _ := range vs { - is := strconv.FormatInt(int64(i), 10) - setElement(k, is, nil) - } - - return err - } - - d.setMap[k+".#"] = strconv.FormatInt(int64(len(vs)), 10) - return nil -} - -func (d *ResourceData) setMapValue( - k string, - parts []string, - schema *Schema, - value interface{}) error { - elemSchema := &Schema{Type: TypeString} - if len(parts) > 0 { - return fmt.Errorf("%s: full map must be set, no a single element", k) - } - - v := reflect.ValueOf(value) - if v.Kind() != reflect.Map { - return fmt.Errorf("%s: must be a map", k) - } - if v.Type().Key().Kind() != reflect.String { - return fmt.Errorf("%s: keys must strings", k) - } - vs := make(map[string]interface{}) - for _, mk := range v.MapKeys() { - mv := v.MapIndex(mk) - vs[mk.String()] = mv.Interface() - } - - if len(vs) == 0 { - // The empty string here means the map is removed. - d.setMap[k] = "" - return nil - } - - delete(d.setMap, k) - for subKey, v := range vs { - err := d.set(fmt.Sprintf("%s.%s", k, subKey), nil, elemSchema, v) - if err != nil { - return err - } - } - - return nil -} - -func (d *ResourceData) setObject( - k string, - parts []string, - schema map[string]*Schema, - value interface{}) error { - if len(parts) > 0 { - // We're setting a specific key in an object - key := parts[0] - parts = parts[1:] - - s, ok := schema[key] - if !ok { - return fmt.Errorf("%s (internal): unknown key to set: %s", k, key) - } - - if k != "" { - // If we're not at the root, then we need to append - // the key to get the full key path. - key = fmt.Sprintf("%s.%s", k, key) - } - - return d.set(key, parts, s, value) - } - - // Set the entire object. First decode into a proper structure - var v map[string]interface{} - if err := mapstructure.Decode(value, &v); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - - // Set each element in turn - var err error - for k1, v1 := range v { - err = d.setObject(k, []string{k1}, schema, v1) - if err != nil { - break - } - } - if err != nil { - for k1, _ := range v { - d.setObject(k, []string{k1}, schema, nil) - } - } - - return err -} - -func (d *ResourceData) setPrimitive( - k string, - schema *Schema, - v interface{}) error { - if v == nil { - delete(d.setMap, k) - return nil - } - - var set string - switch schema.Type { - case TypeBool: - var b bool - if err := mapstructure.Decode(v, &b); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - - set = strconv.FormatBool(b) - case TypeString: - if err := mapstructure.Decode(v, &set); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - case TypeInt: - var n int - if err := mapstructure.Decode(v, &n); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - - set = strconv.FormatInt(int64(n), 10) - default: - return fmt.Errorf("Unknown type: %#v", schema.Type) - } - - d.setMap[k] = set - return nil -} - -func (d *ResourceData) setSet( - k string, - parts []string, - schema *Schema, - value interface{}) error { - if len(parts) > 0 { - return fmt.Errorf("%s: can only set the full set, not elements", k) - } - - // If it is a slice, then we have to turn it into a *Set so that - // we get the proper order back based on the hash code. - if v := reflect.ValueOf(value); v.Kind() == reflect.Slice { - // Build a temp *ResourceData to use for the conversion - tempD := &ResourceData{ - setMap: make(map[string]string), - schema: map[string]*Schema{k: schema}, - } - tempD.once.Do(tempD.init) - - // Set the entire list, this lets us get sane values out of it - if err := tempD.setList(k, nil, schema, value); err != nil { - return err - } - - // Build the set by going over the list items in order and - // hashing them into the set. The reason we go over the list and - // not the `value` directly is because this forces all types - // to become []interface{} (generic) instead of []string, which - // most hash functions are expecting. - s := &Set{F: schema.Set} - source := getSourceSet | getSourceExact - for i := 0; i < v.Len(); i++ { - is := strconv.FormatInt(int64(i), 10) - result := tempD.get(k, []string{is}, schema, source) - if !result.Exists { - panic("just set item doesn't exist") - } - - s.Add(result.Value) - } - - value = s - } - - switch t := schema.Elem.(type) { - case *Resource: - for code, elem := range value.(*Set).m { - for field, _ := range t.Schema { - subK := fmt.Sprintf("%s.%d", k, code) - value := elem.(map[string]interface{})[field] - err := d.setObject(subK, []string{field}, t.Schema, value) - if err != nil { - return err - } - } - } - case *Schema: - for code, elem := range value.(*Set).m { - subK := fmt.Sprintf("%s.%d", k, code) - err := d.set(subK, nil, t, elem) - if err != nil { - return err - } - } - default: - return fmt.Errorf("%s: unknown element type (internal)", k) - } - - d.setMap[k+".#"] = strconv.Itoa(value.(*Set).Len()) - return nil -} - func (d *ResourceData) stateList( prefix string, schema *Schema) map[string]string {