helper/schema: Schema.SkipCoreTypeCheck flag

When running in v0.12-and-higher mode, this will cause the SDK to report
the type of the attribute as "any", effectively skipping type checking
on the Core side altogether and checking only in the SDK and provider
code.

The practical impact of this is to restore the v0.11-style checking
behavior of allowing object values to be missing certain attributes as
long as they are marked as optional in the schema. The SDK can do this
because it uses a unified schema model for both object values and nested
blocks, while Terraform Core only supports the idea of "optional" when
talking about attributes in nested blocks.

This is a continuation of the pile of workarounds that also includes
the ConfigMode and AsSingle fields, allowing providers to selectively opt
out of new v0.12 behaviors in situations where they conflict with
decisions made in the design of the providers in our old world where
Terraform Core delegated _all_ validation to providers.

This is designed as an opt-in so that we can limit its impact only to
specific cases where it's needed and minimize the risk of regressions
elsewhere. Providers should use this sparingly only in situations where
prevailing usage disagrees with the new expectations of Terraform Core in
v0.12.

This commit only adds the flag, and does not implement any behavior for it
yet. That means this commit can exist in both the v0.11 and v0.12
codebases, allowing for API compatibility. A subsequent commit for v0.12
(not included in v0.11) will then implement this behavior.
This commit is contained in:
Martin Atkins 2019-03-21 09:33:34 -07:00
parent 1e32ae243c
commit 7f860dc83e
1 changed files with 40 additions and 2 deletions

View File

@ -95,6 +95,34 @@ type Schema struct {
// behavior, and SchemaConfigModeBlock is not permitted. // behavior, and SchemaConfigModeBlock is not permitted.
ConfigMode SchemaConfigMode ConfigMode SchemaConfigMode
// SkipCoreTypeCheck, if set, will advertise this attribute to Terraform Core
// has being dynamically-typed rather than deriving a type from the schema.
// This has the effect of making Terraform Core skip all type-checking of
// the value, and thus leaves all type checking up to a combination of this
// SDK and the provider's own code.
//
// This flag does nothing for Terraform versions prior to v0.12, because
// in prior versions there was no Core-side typecheck anyway.
//
// The most practical effect of this flag is to allow object-typed schemas
// (specified with Elem: schema.Resource) to pass through Terraform Core
// even without all of the object type attributes specified, which may be
// useful when using ConfigMode: SchemaConfigModeAttr to achieve
// nested-block-like behaviors while using attribute syntax.
//
// However, by doing so we require type information to be sent and stored
// per-object rather than just once statically in the schema, and so this
// will change the wire serialization of a resource type in state. Changing
// the value of SkipCoreTypeCheck will therefore require a state migration
// if there has previously been any release of the provider compatible with
// Terraform v0.12.
//
// SkipCoreTypeCheck can only be set when ConfigMode is SchemaConfigModeAttr,
// because nested blocks cannot be decoded by Terraform Core without at
// least shallow information about the next level of nested attributes and
// blocks.
SkipCoreTypeCheck bool
// If one of these is set, then this item can come from the configuration. // 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 // Both cannot be set. If Optional is set, the value is optional. If
// Required is set, the value is required. // Required is set, the value is required.
@ -719,6 +747,8 @@ func (m schemaMap) internalValidate(topSchemaMap schemaMap, attrsOnly bool) erro
computedOnly := v.Computed && !v.Optional computedOnly := v.Computed && !v.Optional
isBlock := false
switch v.ConfigMode { switch v.ConfigMode {
case SchemaConfigModeBlock: case SchemaConfigModeBlock:
if _, ok := v.Elem.(*Resource); !ok { if _, ok := v.Elem.(*Resource); !ok {
@ -730,19 +760,27 @@ func (m schemaMap) internalValidate(topSchemaMap schemaMap, attrsOnly bool) erro
if computedOnly { if computedOnly {
return fmt.Errorf("%s: ConfigMode of block cannot be used for computed schema", k) return fmt.Errorf("%s: ConfigMode of block cannot be used for computed schema", k)
} }
isBlock = true
case SchemaConfigModeAttr: case SchemaConfigModeAttr:
// anything goes // anything goes
case SchemaConfigModeAuto: case SchemaConfigModeAuto:
// Since "Auto" for Elem: *Resource would create a nested block, // Since "Auto" for Elem: *Resource would create a nested block,
// and that's impossible inside an attribute, we require it to be // and that's impossible inside an attribute, we require it to be
// explicitly overridden as mode "Attr" for clarity. // explicitly overridden as mode "Attr" for clarity.
if _, ok := v.Elem.(*Resource); ok && attrsOnly { if _, ok := v.Elem.(*Resource); ok {
isBlock = true
if attrsOnly {
return fmt.Errorf("%s: in *schema.Resource with ConfigMode of attribute, so must also have ConfigMode of attribute", k) return fmt.Errorf("%s: in *schema.Resource with ConfigMode of attribute, so must also have ConfigMode of attribute", k)
} }
}
default: default:
return fmt.Errorf("%s: invalid ConfigMode value", k) return fmt.Errorf("%s: invalid ConfigMode value", k)
} }
if isBlock && v.SkipCoreTypeCheck {
return fmt.Errorf("%s: SkipCoreTypeCheck must be false unless ConfigMode is attribute", k)
}
if v.Computed && v.Default != nil { if v.Computed && v.Default != nil {
return fmt.Errorf("%s: Default must be nil if computed", k) return fmt.Errorf("%s: Default must be nil if computed", k)
} }