helper/schema: ConfigMode field in *Schema

This allows a provider developer slightly more control over how an SDK
schema is mapped into the Terraform configuration language, overriding
some default assumptions.

ConfigMode overrides the default assumption that a schema with
an Elem of type *Resource is to be mapped to configuration as a nested
block, allowing mapping as an attribute containing an object type instead.

These behaviors only apply when a provider is being used with Terraform
v0.12 or later. They are ignored altogether in Terraform v0.11 mode, to
preserve compatibility. We are adding these primarily to allow the v0.12
version of a resource type schema to be specified to match the prevailing
usage of it in existing configurations, in situations where the default
mapping to v0.12 concepts is not appropriate.

This commit adds only the fields themselves and the InternalValidate rules
for them. A subsequent commit for Terraform v0.12 will add the behavior
as part of the protocol version 5 shim layer.
This commit is contained in:
Martin Atkins 2019-03-06 16:54:18 -08:00
parent bf04503f04
commit a6d322edec
2 changed files with 152 additions and 1 deletions

View File

@ -75,6 +75,26 @@ type Schema struct {
//
Type ValueType
// ConfigMode allows for overriding the default behaviors for mapping
// schema entries onto configuration constructs.
//
// By default, the Elem field is used to choose whether a particular
// schema is represented in configuration as an attribute or as a nested
// block; if Elem is a *schema.Resource then it's a block and it's an
// attribute otherwise.
//
// If Elem is *schema.Resource then setting ConfigMode to
// SchemaConfigModeAttr will force it to be represented in configuration
// as an attribute, which means that the Computed flag can be used to
// provide default elements when the argument isn't set at all, while still
// allowing the user to force zero elements by explicitly assigning an
// empty list.
//
// When Computed is set without Optional, the attribute is not settable
// in configuration at all and so SchemaConfigModeAttr is the automatic
// behavior, and SchemaConfigModeBlock is not permitted.
ConfigMode SchemaConfigMode
// If one of these is set, then this item can come from the configuration.
// Both cannot be set. If Optional is set, the value is optional. If
// Required is set, the value is required.
@ -227,6 +247,17 @@ type Schema struct {
Sensitive bool
}
// SchemaConfigMode is used to influence how a schema item is mapped into a
// corresponding configuration construct, using the ConfigMode field of
// Schema.
type SchemaConfigMode int
const (
SchemaConfigModeAuto SchemaConfigMode = iota
SchemaConfigModeAttr
SchemaConfigModeBlock
)
// SchemaDiffSuppressFunc is a function which can be used to determine
// whether a detected diff on a schema element is "valid" or not, and
// suppress it from the plan if necessary.
@ -648,6 +679,10 @@ func (m schemaMap) Validate(c *terraform.ResourceConfig) ([]string, []error) {
// from a unit test (and not in user-path code) to verify that a schema
// is properly built.
func (m schemaMap) InternalValidate(topSchemaMap schemaMap) error {
return m.internalValidate(topSchemaMap, false)
}
func (m schemaMap) internalValidate(topSchemaMap schemaMap, attrsOnly bool) error {
if topSchemaMap == nil {
topSchemaMap = m
}
@ -668,6 +703,32 @@ func (m schemaMap) InternalValidate(topSchemaMap schemaMap) error {
return fmt.Errorf("%s: One of optional, required, or computed must be set", k)
}
computedOnly := v.Computed && !v.Optional
switch v.ConfigMode {
case SchemaConfigModeBlock:
if _, ok := v.Elem.(*Resource); !ok {
return fmt.Errorf("%s: ConfigMode of block is allowed only when Elem is *schema.Resource", k)
}
if attrsOnly {
return fmt.Errorf("%s: ConfigMode of block cannot be used in child of schema with ConfigMode of attribute", k)
}
if computedOnly {
return fmt.Errorf("%s: ConfigMode of block cannot be used for computed schema", k)
}
case SchemaConfigModeAttr:
// anything goes
case SchemaConfigModeAuto:
// Since "Auto" for Elem: *Resource would create a nested block,
// and that's impossible inside an attribute, we require it to be
// explicitly overridden as mode "Attr" for clarity.
if _, ok := v.Elem.(*Resource); ok && attrsOnly {
return fmt.Errorf("%s: in *schema.Resource with ConfigMode of attribute, so must also have ConfigMode of attribute", k)
}
default:
return fmt.Errorf("%s: invalid ConfigMode value", k)
}
if v.Computed && v.Default != nil {
return fmt.Errorf("%s: Default must be nil if computed", k)
}
@ -732,7 +793,9 @@ func (m schemaMap) InternalValidate(topSchemaMap schemaMap) error {
switch t := v.Elem.(type) {
case *Resource:
if err := t.InternalValidate(topSchemaMap, true); err != nil {
attrsOnly := attrsOnly || v.ConfigMode == SchemaConfigModeAttr
if err := schemaMap(t.Schema).internalValidate(topSchemaMap, attrsOnly); err != nil {
return err
}
case *Schema:

View File

@ -3836,6 +3836,94 @@ func TestSchemaMap_InternalValidate(t *testing.T) {
},
false,
},
"ConfigModeBlock with Elem *Resource": {
map[string]*Schema{
"block": &Schema{
Type: TypeList,
ConfigMode: SchemaConfigModeBlock,
Optional: true,
Elem: &Resource{},
},
},
false,
},
"ConfigModeBlock Computed with Elem *Resource": {
map[string]*Schema{
"block": &Schema{
Type: TypeList,
ConfigMode: SchemaConfigModeBlock,
Computed: true,
Elem: &Resource{},
},
},
true, // ConfigMode of block cannot be used for computed schema
},
"ConfigModeBlock with Elem *Schema": {
map[string]*Schema{
"block": &Schema{
Type: TypeList,
ConfigMode: SchemaConfigModeBlock,
Optional: true,
Elem: &Schema{
Type: TypeString,
},
},
},
true,
},
"ConfigModeBlock with no Elem": {
map[string]*Schema{
"block": &Schema{
Type: TypeString,
ConfigMode: SchemaConfigModeBlock,
Optional: true,
},
},
true,
},
"ConfigModeBlock inside ConfigModeAttr": {
map[string]*Schema{
"block": &Schema{
Type: TypeList,
ConfigMode: SchemaConfigModeAttr,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"sub": &Schema{
Type: TypeList,
ConfigMode: SchemaConfigModeBlock,
Elem: &Resource{},
},
},
},
},
},
true, // ConfigMode of block cannot be used in child of schema with ConfigMode of attribute
},
"ConfigModeAuto with *Resource inside ConfigModeAttr": {
map[string]*Schema{
"block": &Schema{
Type: TypeList,
ConfigMode: SchemaConfigModeAttr,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"sub": &Schema{
Type: TypeList,
Elem: &Resource{},
},
},
},
},
},
true, // in *schema.Resource with ConfigMode of attribute, so must also have ConfigMode of attribute
},
}
for tn, tc := range cases {