From 7b6df27e4a67c9b1c1c3f10ab7caf4d77514ff69 Mon Sep 17 00:00:00 2001 From: James Nugent Date: Wed, 27 Apr 2016 16:41:17 -0500 Subject: [PATCH] helper/schema: Read native maps from configuration This adds a test and the support necessary to read from native maps passed as variables via interpolation - for example: ``` resource ...... { mapValue = "${var.map}" } ``` We also add support for interpolating maps from the flat-mapped resource config, which is necessary to support assignment of computed maps, which is now valid. Unfortunately there is no good way to distinguish between a list and a map in the flatmap. In lieu of changing that representation (which is risky), we assume that if all the keys are numeric, this is intended to be a list, and if not it is intended to be a map. This does preclude maps which have purely numeric keys, which should be noted as a backwards compatibility concern. --- helper/schema/field_reader_config.go | 22 ++++++++++- helper/schema/field_reader_config_test.go | 30 +++++++++++++++ helper/schema/schema.go | 16 +++++++- terraform/interpolate.go | 47 ++++++++++++++++------- 4 files changed, 98 insertions(+), 17 deletions(-) diff --git a/helper/schema/field_reader_config.go b/helper/schema/field_reader_config.go index 3cf4f5fc3..0d4c2a97c 100644 --- a/helper/schema/field_reader_config.go +++ b/helper/schema/field_reader_config.go @@ -100,7 +100,8 @@ func (r *ConfigFieldReader) readField( func (r *ConfigFieldReader) readMap(k string) (FieldReadResult, error) { // We want both the raw value and the interpolated. We use the interpolated // to store actual values and we use the raw one to check for - // computed keys. + // computed keys. Actual values are obtained in the switch, depending on + // the type of the raw value. mraw, ok := r.Config.GetRaw(k) if !ok { return FieldReadResult{}, nil @@ -109,6 +110,25 @@ func (r *ConfigFieldReader) readMap(k string) (FieldReadResult, error) { result := make(map[string]interface{}) computed := false switch m := mraw.(type) { + case string: + // This is a map which has come out of an interpolated variable, so we + // can just get the value directly from config. Values cannot be computed + // currently. + v, _ := r.Config.Get(k) + + // If this isn't a map[string]interface, it must be computed. + mapV, ok := v.(map[string]interface{}) + if !ok { + return FieldReadResult{ + Exists: true, + Computed: true, + }, nil + } + + // Otherwise we can proceed as usual. + for i, iv := range mapV { + result[i] = iv + } case []interface{}: for i, innerRaw := range m { for ik := range innerRaw.(map[string]interface{}) { diff --git a/helper/schema/field_reader_config_test.go b/helper/schema/field_reader_config_test.go index ba7c0f1a6..9daeab3a0 100644 --- a/helper/schema/field_reader_config_test.go +++ b/helper/schema/field_reader_config_test.go @@ -183,6 +183,36 @@ func TestConfigFieldReader_ComputedMap(t *testing.T) { }), false, }, + + "native map": { + []string{"map"}, + FieldReadResult{ + Value: map[string]interface{}{ + "bar": "baz", + "baz": "bar", + }, + Exists: true, + Computed: false, + }, + testConfigInterpolate(t, map[string]interface{}{ + "map": "${var.foo}", + }, map[string]ast.Variable{ + "var.foo": ast.Variable{ + Type: ast.TypeMap, + Value: map[string]ast.Variable{ + "bar": ast.Variable{ + Type: ast.TypeString, + Value: "baz", + }, + "baz": ast.Variable{ + Type: ast.TypeString, + Value: "bar", + }, + }, + }, + }), + false, + }, } for name, tc := range cases { diff --git a/helper/schema/schema.go b/helper/schema/schema.go index 43b5cc21a..acffd249e 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -19,8 +19,10 @@ import ( "strconv" "strings" + "github.com/davecgh/go-spew/spew" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/mapstructure" + "log" ) // Schema is used to describe the structure of a value. @@ -1120,11 +1122,21 @@ func (m schemaMap) validateMap( // case to []interface{} unless the slice is exactly that type. rawV := reflect.ValueOf(raw) switch rawV.Kind() { + case reflect.String: + // If raw and reified are equal, this is a string and should + // be rejected. + reified, reifiedOk := c.Get(k) + log.Printf("[jen20] reified: %s", spew.Sdump(reified)) + log.Printf("[jen20] raw: %s", spew.Sdump(raw)) + if reifiedOk && raw == reified && !c.IsComputed(k) { + return nil, []error{fmt.Errorf("%s: should be a map", k)} + } + // Otherwise it's likely raw is an interpolation. + return nil, nil case reflect.Map: case reflect.Slice: default: - return nil, []error{fmt.Errorf( - "%s: should be a map", k)} + return nil, []error{fmt.Errorf("%s: should be a map", k)} } // If it is not a slice, it is valid diff --git a/terraform/interpolate.go b/terraform/interpolate.go index 81c6fc21e..11eb193b6 100644 --- a/terraform/interpolate.go +++ b/terraform/interpolate.go @@ -384,9 +384,9 @@ func (i *Interpolater) computeResourceVariable( return &ast.Variable{Type: ast.TypeString, Value: attr}, nil } - // computed list attribute + // computed list or map attribute if _, ok := r.Primary.Attributes[v.Field+".#"]; ok { - variable, err := i.interpolateListAttribute(v.Field, r.Primary.Attributes) + variable, err := i.interpolateComplexTypeAttribute(v.Field, r.Primary.Attributes) return &variable, err } @@ -513,7 +513,7 @@ func (i *Interpolater) computeResourceMultiVariable( if !ok { continue } - multiAttr, err := i.interpolateListAttribute(v.Field, r.Primary.Attributes) + multiAttr, err := i.interpolateComplexTypeAttribute(v.Field, r.Primary.Attributes) if err != nil { return nil, err } @@ -559,12 +559,12 @@ func (i *Interpolater) computeResourceMultiVariable( return &variable, err } -func (i *Interpolater) interpolateListAttribute( +func (i *Interpolater) interpolateComplexTypeAttribute( resourceID string, attributes map[string]string) (ast.Variable, error) { attr := attributes[resourceID+".#"] - log.Printf("[DEBUG] Interpolating computed list attribute %s (%s)", + log.Printf("[DEBUG] Interpolating computed complex type attribute %s (%s)", resourceID, attr) // In Terraform's internal dotted representation of list-like attributes, the @@ -575,18 +575,37 @@ func (i *Interpolater) interpolateListAttribute( return unknownVariable(), nil } - // Otherwise we gather the values from the list-like attribute and return - // them. - var members []string - numberedListMember := regexp.MustCompile("^" + resourceID + "\\.[0-9]+$") - for id, value := range attributes { - if numberedListMember.MatchString(id) { - members = append(members, value) + // At this stage we don't know whether the item is a list or a map, so we + // examine the keys to see whether they are all numeric. + var numericKeys []string + var allKeys []string + numberedListKey := regexp.MustCompile("^" + resourceID + "\\.[0-9]+$") + otherListKey := regexp.MustCompile("^" + resourceID + "\\.([^#]+)$") + for id, _ := range attributes { + if numberedListKey.MatchString(id) { + numericKeys = append(numericKeys, id) + } + if submatches := otherListKey.FindAllStringSubmatch(id, -1); len(submatches) > 0 { + allKeys = append(allKeys, submatches[0][1]) } } - sort.Strings(members) - return hil.InterfaceToVariable(members) + if len(numericKeys) == len(allKeys) { + // This is a list + var members []string + for _, key := range numericKeys { + members = append(members, attributes[key]) + } + sort.Strings(members) + return hil.InterfaceToVariable(members) + } else { + // This is a map + members := make(map[string]interface{}) + for _, key := range allKeys { + members[key] = attributes[resourceID+"."+key] + } + return hil.InterfaceToVariable(members) + } } func (i *Interpolater) resourceVariableInfo(