helper/schema: TypeMap of Resource is actually of TypeString

Historically helper/schema did not support non-primitive map attributes
because they cannot be represented unambiguously in flatmap. When we
initially implemented CoreConfigSchema here we mapped that situation to
a nested block of mode NestingMap, even though that'd never worked until
now, assuming that it'd be harmless because providers wouldn't be using
it.

It turns out that some providers are, in fact, incorrectly populating
a TypeMap schema with Elem: &schema.Resource, apparently under the false
assumption that it would constrain the keys allowed in the map. In
practice, helper/schema has just been ignoring this and treating such
attributes as map of string. (#20076)

In order to preserve the behavior of these existing incorrectly-specified
attribute definitions, here we mimic the helper/schema behavior by
presenting as an attribute of type map(string).

These attributes have also been shown in some documentation as nested
blocks (with no equals sign), so that'll need to be fixed in user
configurations as they upgrade to Terraform 0.12. However, the existing
upgrade tool rules will take care of that as a natural consequence of the
name being indicated as an attribute in the schema, rather than as a block
type.

This fixes #20076.
This commit is contained in:
Martin Atkins 2019-01-24 17:41:30 -08:00
parent 954d38e870
commit ae0be75ae0
2 changed files with 27 additions and 5 deletions

View File

@ -39,6 +39,21 @@ func (m schemaMap) CoreConfigSchema() *configschema.Block {
ret.Attributes[name] = schema.coreConfigSchemaAttribute() ret.Attributes[name] = schema.coreConfigSchemaAttribute()
continue continue
} }
if schema.Type == TypeMap {
// For TypeMap in particular, it isn't valid for Elem to be a
// *Resource (since that would be ambiguous in flatmap) and
// so Elem is treated as a TypeString schema if so. This matches
// how the field readers treat this situation, for compatibility
// with configurations targeting Terraform 0.11 and earlier.
if _, isResource := schema.Elem.(*Resource); isResource {
sch := *schema // shallow copy
sch.Elem = &Schema{
Type: TypeString,
}
ret.Attributes[name] = sch.coreConfigSchemaAttribute()
continue
}
}
switch schema.Elem.(type) { switch schema.Elem.(type) {
case *Schema, ValueType: case *Schema, ValueType:
ret.Attributes[name] = schema.coreConfigSchemaAttribute() ret.Attributes[name] = schema.coreConfigSchemaAttribute()

View File

@ -204,7 +204,18 @@ func TestSchemaMapCoreConfigSchema(t *testing.T) {
}, },
}, },
testResource(&configschema.Block{ testResource(&configschema.Block{
Attributes: map[string]*configschema.Attribute{}, Attributes: map[string]*configschema.Attribute{
// This one becomes a string attribute because helper/schema
// doesn't actually support maps of resource. The given
// "Elem" is just ignored entirely here, which is important
// because that is also true of the helper/schema logic and
// existing providers rely on this being ignored for
// correct operation.
"map": {
Type: cty.Map(cty.String),
Optional: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{ BlockTypes: map[string]*configschema.NestedBlock{
"list": { "list": {
Nesting: configschema.NestingList, Nesting: configschema.NestingList,
@ -217,10 +228,6 @@ func TestSchemaMapCoreConfigSchema(t *testing.T) {
Block: configschema.Block{}, Block: configschema.Block{},
MinItems: 1, // because schema is Required MinItems: 1, // because schema is Required
}, },
"map": {
Nesting: configschema.NestingMap,
Block: configschema.Block{},
},
}, },
}), }),
}, },