2017-10-04 20:27:51 +02:00
|
|
|
package schema
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
|
2018-07-05 19:33:29 +02:00
|
|
|
"github.com/hashicorp/terraform/configs/configschema"
|
2017-10-04 20:27:51 +02:00
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
)
|
|
|
|
|
|
|
|
// The functions and methods in this file are concerned with the conversion
|
|
|
|
// of this package's schema model into the slightly-lower-level schema model
|
|
|
|
// used by Terraform core for configuration parsing.
|
|
|
|
|
|
|
|
// CoreConfigSchema lowers the receiver to the schema model expected by
|
|
|
|
// Terraform core.
|
|
|
|
//
|
|
|
|
// This lower-level model has fewer features than the schema in this package,
|
|
|
|
// describing only the basic structure of configuration and state values we
|
|
|
|
// expect. The full schemaMap from this package is still required for full
|
|
|
|
// validation, handling of default values, etc.
|
|
|
|
//
|
|
|
|
// This method presumes a schema that passes InternalValidate, and so may
|
|
|
|
// panic or produce an invalid result if given an invalid schemaMap.
|
|
|
|
func (m schemaMap) CoreConfigSchema() *configschema.Block {
|
2019-03-14 22:39:38 +01:00
|
|
|
return m.coreConfigSchema(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
// CoreConfigSchemaWhenShimmed is a variant of CoreConfigSchema that returns
|
|
|
|
// the schema as it would appear when working with data structures that have
|
|
|
|
// already been shimmed to the legacy form.
|
|
|
|
//
|
|
|
|
// In particular, it ignores the AsSingle flag on any legacy schemas and behaves
|
|
|
|
// as if they were really lists/sets instead, thus giving a description of
|
|
|
|
// the shape of the data structure after the AsSingle fixup has been applied.
|
|
|
|
//
|
|
|
|
// This should be used with care only in unusual situations where we need to
|
|
|
|
// work with an already-shimmed value using a new-style schema.
|
|
|
|
func (m schemaMap) CoreConfigSchemaWhenShimmed() *configschema.Block {
|
|
|
|
return m.coreConfigSchema(false)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m schemaMap) coreConfigSchema(enableAsSingle bool) *configschema.Block {
|
2017-10-04 20:27:51 +02:00
|
|
|
if len(m) == 0 {
|
2017-10-14 03:43:08 +02:00
|
|
|
// We return an actual (empty) object here, rather than a nil,
|
|
|
|
// because a nil result would mean that we don't have a schema at
|
|
|
|
// all, rather than that we have an empty one.
|
|
|
|
return &configschema.Block{}
|
2017-10-04 20:27:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
ret := &configschema.Block{
|
|
|
|
Attributes: map[string]*configschema.Attribute{},
|
|
|
|
BlockTypes: map[string]*configschema.NestedBlock{},
|
|
|
|
}
|
|
|
|
|
|
|
|
for name, schema := range m {
|
|
|
|
if schema.Elem == nil {
|
2019-03-14 22:39:38 +01:00
|
|
|
ret.Attributes[name] = schema.coreConfigSchemaAttribute(enableAsSingle)
|
2017-10-04 20:27:51 +02:00
|
|
|
continue
|
|
|
|
}
|
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.
2019-01-25 02:41:30 +01:00
|
|
|
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,
|
|
|
|
}
|
2019-03-14 22:39:38 +01:00
|
|
|
ret.Attributes[name] = sch.coreConfigSchemaAttribute(enableAsSingle)
|
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.
2019-01-25 02:41:30 +01:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
2019-03-09 01:31:22 +01:00
|
|
|
switch schema.ConfigMode {
|
|
|
|
case SchemaConfigModeAttr:
|
2019-03-14 22:39:38 +01:00
|
|
|
ret.Attributes[name] = schema.coreConfigSchemaAttribute(enableAsSingle)
|
2019-03-09 01:31:22 +01:00
|
|
|
case SchemaConfigModeBlock:
|
2019-03-14 22:39:38 +01:00
|
|
|
ret.BlockTypes[name] = schema.coreConfigSchemaBlock(enableAsSingle)
|
2019-03-09 01:31:22 +01:00
|
|
|
default: // SchemaConfigModeAuto, or any other invalid value
|
|
|
|
if schema.Computed && !schema.Optional {
|
|
|
|
// Computed-only schemas are always handled as attributes,
|
|
|
|
// because they never appear in configuration.
|
2019-03-14 22:39:38 +01:00
|
|
|
ret.Attributes[name] = schema.coreConfigSchemaAttribute(enableAsSingle)
|
2019-03-09 01:31:22 +01:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
switch schema.Elem.(type) {
|
|
|
|
case *Schema, ValueType:
|
2019-03-14 22:39:38 +01:00
|
|
|
ret.Attributes[name] = schema.coreConfigSchemaAttribute(enableAsSingle)
|
2019-03-09 01:31:22 +01:00
|
|
|
case *Resource:
|
2019-03-14 22:39:38 +01:00
|
|
|
ret.BlockTypes[name] = schema.coreConfigSchemaBlock(enableAsSingle)
|
2019-03-09 01:31:22 +01:00
|
|
|
default:
|
|
|
|
// Should never happen for a valid schema
|
|
|
|
panic(fmt.Errorf("invalid Schema.Elem %#v; need *Schema or *Resource", schema.Elem))
|
|
|
|
}
|
2017-10-04 20:27:51 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
|
|
|
// coreConfigSchemaAttribute prepares a configschema.Attribute representation
|
|
|
|
// of a schema. This is appropriate only for primitives or collections whose
|
|
|
|
// Elem is an instance of Schema. Use coreConfigSchemaBlock for collections
|
|
|
|
// whose elem is a whole resource.
|
2019-03-14 22:39:38 +01:00
|
|
|
func (s *Schema) coreConfigSchemaAttribute(enableAsSingle bool) *configschema.Attribute {
|
2018-11-27 00:29:59 +01:00
|
|
|
// The Schema.DefaultFunc capability adds some extra weirdness here since
|
|
|
|
// it can be combined with "Required: true" to create a sitution where
|
|
|
|
// required-ness is conditional. Terraform Core doesn't share this concept,
|
|
|
|
// so we must sniff for this possibility here and conditionally turn
|
|
|
|
// off the "Required" flag if it looks like the DefaultFunc is going
|
|
|
|
// to provide a value.
|
|
|
|
// This is not 100% true to the original interface of DefaultFunc but
|
|
|
|
// works well enough for the EnvDefaultFunc and MultiEnvDefaultFunc
|
|
|
|
// situations, which are the main cases we care about.
|
|
|
|
//
|
|
|
|
// Note that this also has a consequence for commands that return schema
|
|
|
|
// information for documentation purposes: running those for certain
|
|
|
|
// providers will produce different results depending on which environment
|
|
|
|
// variables are set. We accept that weirdness in order to keep this
|
|
|
|
// interface to core otherwise simple.
|
|
|
|
reqd := s.Required
|
|
|
|
opt := s.Optional
|
|
|
|
if reqd && s.DefaultFunc != nil {
|
|
|
|
v, err := s.DefaultFunc()
|
|
|
|
// We can't report errors from here, so we'll instead just force
|
|
|
|
// "Required" to false and let the provider try calling its
|
|
|
|
// DefaultFunc again during the validate step, where it can then
|
|
|
|
// return the error.
|
|
|
|
if err != nil || (err == nil && v != nil) {
|
|
|
|
reqd = false
|
|
|
|
opt = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-04 20:27:51 +02:00
|
|
|
return &configschema.Attribute{
|
2019-03-14 22:39:38 +01:00
|
|
|
Type: s.coreConfigSchemaType(enableAsSingle),
|
2018-11-27 00:29:59 +01:00
|
|
|
Optional: opt,
|
|
|
|
Required: reqd,
|
2018-03-16 18:43:35 +01:00
|
|
|
Computed: s.Computed,
|
|
|
|
Sensitive: s.Sensitive,
|
|
|
|
Description: s.Description,
|
2017-10-04 20:27:51 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// coreConfigSchemaBlock prepares a configschema.NestedBlock representation of
|
|
|
|
// a schema. This is appropriate only for collections whose Elem is an instance
|
|
|
|
// of Resource, and will panic otherwise.
|
2019-03-14 22:39:38 +01:00
|
|
|
func (s *Schema) coreConfigSchemaBlock(enableAsSingle bool) *configschema.NestedBlock {
|
2017-10-04 20:27:51 +02:00
|
|
|
ret := &configschema.NestedBlock{}
|
2019-03-14 22:39:38 +01:00
|
|
|
if nested := schemaMap(s.Elem.(*Resource).Schema).coreConfigSchema(enableAsSingle); nested != nil {
|
2017-10-04 20:27:51 +02:00
|
|
|
ret.Block = *nested
|
|
|
|
}
|
|
|
|
switch s.Type {
|
|
|
|
case TypeList:
|
|
|
|
ret.Nesting = configschema.NestingList
|
|
|
|
case TypeSet:
|
|
|
|
ret.Nesting = configschema.NestingSet
|
|
|
|
case TypeMap:
|
|
|
|
ret.Nesting = configschema.NestingMap
|
|
|
|
default:
|
|
|
|
// Should never happen for a valid schema
|
|
|
|
panic(fmt.Errorf("invalid s.Type %s for s.Elem being resource", s.Type))
|
|
|
|
}
|
|
|
|
|
|
|
|
ret.MinItems = s.MinItems
|
|
|
|
ret.MaxItems = s.MaxItems
|
|
|
|
|
2019-03-14 22:39:38 +01:00
|
|
|
if s.AsSingle && enableAsSingle {
|
|
|
|
// In AsSingle mode, we artifically force a TypeList or TypeSet
|
|
|
|
// attribute in the SDK to be treated as a single block by Terraform Core.
|
|
|
|
// This must then be fixed up in the shim code (in helper/plugin) so
|
|
|
|
// that the SDK still sees the lists or sets it's expecting.
|
|
|
|
ret.Nesting = configschema.NestingSingle
|
|
|
|
}
|
|
|
|
|
2017-10-04 20:27:51 +02:00
|
|
|
if s.Required && s.MinItems == 0 {
|
|
|
|
// configschema doesn't have a "required" representation for nested
|
|
|
|
// blocks, but we can fake it by requiring at least one item.
|
|
|
|
ret.MinItems = 1
|
|
|
|
}
|
2018-11-27 00:44:27 +01:00
|
|
|
if s.Optional && s.MinItems > 0 {
|
|
|
|
// Historically helper/schema would ignore MinItems if Optional were
|
|
|
|
// set, so we must mimic this behavior here to ensure that providers
|
|
|
|
// relying on that undocumented behavior can continue to operate as
|
|
|
|
// they did before.
|
|
|
|
ret.MinItems = 0
|
|
|
|
}
|
|
|
|
if s.Computed && !s.Optional {
|
|
|
|
// MinItems/MaxItems are meaningless for computed nested blocks, since
|
|
|
|
// they are never set by the user anyway. This ensures that we'll never
|
|
|
|
// generate weird errors about them.
|
|
|
|
ret.MinItems = 0
|
|
|
|
ret.MaxItems = 0
|
|
|
|
}
|
2017-10-04 20:27:51 +02:00
|
|
|
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
|
|
|
// coreConfigSchemaType determines the core config schema type that corresponds
|
|
|
|
// to a particular schema's type.
|
2019-03-14 22:39:38 +01:00
|
|
|
func (s *Schema) coreConfigSchemaType(enableAsSingle bool) cty.Type {
|
2017-10-04 20:27:51 +02:00
|
|
|
switch s.Type {
|
|
|
|
case TypeString:
|
|
|
|
return cty.String
|
|
|
|
case TypeBool:
|
|
|
|
return cty.Bool
|
|
|
|
case TypeInt, TypeFloat:
|
|
|
|
// configschema doesn't distinguish int and float, so helper/schema
|
|
|
|
// will deal with this as an additional validation step after
|
|
|
|
// configuration has been parsed and decoded.
|
|
|
|
return cty.Number
|
|
|
|
case TypeList, TypeSet, TypeMap:
|
|
|
|
var elemType cty.Type
|
|
|
|
switch set := s.Elem.(type) {
|
|
|
|
case *Schema:
|
2019-03-14 22:39:38 +01:00
|
|
|
elemType = set.coreConfigSchemaType(enableAsSingle)
|
2018-06-02 02:32:01 +02:00
|
|
|
case ValueType:
|
|
|
|
// This represents a mistake in the provider code, but it's a
|
|
|
|
// common one so we'll just shim it.
|
2019-03-14 22:39:38 +01:00
|
|
|
elemType = (&Schema{Type: set}).coreConfigSchemaType(enableAsSingle)
|
2017-10-04 20:27:51 +02:00
|
|
|
case *Resource:
|
2019-03-09 01:31:22 +01:00
|
|
|
// By default we construct a NestedBlock in this case, but this
|
|
|
|
// behavior is selected either for computed-only schemas or
|
|
|
|
// when ConfigMode is explicitly SchemaConfigModeBlock.
|
|
|
|
// See schemaMap.CoreConfigSchema for the exact rules.
|
2019-03-14 22:39:38 +01:00
|
|
|
elemType = schemaMap(set.Schema).coreConfigSchema(enableAsSingle).ImpliedType()
|
2017-10-04 20:27:51 +02:00
|
|
|
default:
|
|
|
|
if set != nil {
|
|
|
|
// Should never happen for a valid schema
|
|
|
|
panic(fmt.Errorf("invalid Schema.Elem %#v; need *Schema or *Resource", s.Elem))
|
|
|
|
}
|
|
|
|
// Some pre-existing schemas assume string as default, so we need
|
|
|
|
// to be compatible with them.
|
|
|
|
elemType = cty.String
|
|
|
|
}
|
2019-03-14 22:39:38 +01:00
|
|
|
if s.AsSingle && enableAsSingle {
|
|
|
|
// In AsSingle mode, we artifically force a TypeList or TypeSet
|
|
|
|
// attribute in the SDK to be treated as a single value by Terraform Core.
|
|
|
|
// This must then be fixed up in the shim code (in helper/plugin) so
|
|
|
|
// that the SDK still sees the lists or sets it's expecting.
|
|
|
|
return elemType
|
|
|
|
}
|
2017-10-04 20:27:51 +02:00
|
|
|
switch s.Type {
|
|
|
|
case TypeList:
|
|
|
|
return cty.List(elemType)
|
|
|
|
case TypeSet:
|
|
|
|
return cty.Set(elemType)
|
|
|
|
case TypeMap:
|
|
|
|
return cty.Map(elemType)
|
|
|
|
default:
|
|
|
|
// can never get here in practice, due to the case we're inside
|
|
|
|
panic("invalid collection type")
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
// should never happen for a valid schema
|
|
|
|
panic(fmt.Errorf("invalid Schema.Type %s", s.Type))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-12 02:59:32 +02:00
|
|
|
// CoreConfigSchema is a convenient shortcut for calling CoreConfigSchema on
|
|
|
|
// the resource's schema. CoreConfigSchema adds the implicitly required "id"
|
|
|
|
// attribute for top level resources if it doesn't exist.
|
2017-10-04 20:27:51 +02:00
|
|
|
func (r *Resource) CoreConfigSchema() *configschema.Block {
|
2019-03-14 22:39:38 +01:00
|
|
|
return r.coreConfigSchema(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
// CoreConfigSchemaWhenShimmed is a variant of CoreConfigSchema that returns
|
|
|
|
// the schema as it would appear when working with data structures that have
|
|
|
|
// already been shimmed to the legacy form.
|
|
|
|
//
|
|
|
|
// In particular, it ignores the AsSingle flag on any legacy schemas and behaves
|
|
|
|
// as if they were really lists/sets instead, thus giving a description of
|
|
|
|
// the shape of the data structure after the AsSingle fixup has been applied.
|
|
|
|
//
|
|
|
|
// This should be used with care only in unusual situations where we need to
|
|
|
|
// work with an already-shimmed value using a new-style schema.
|
|
|
|
func (r *Resource) CoreConfigSchemaWhenShimmed() *configschema.Block {
|
|
|
|
return r.coreConfigSchema(false)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Resource) coreConfigSchema(enableAsSingle bool) *configschema.Block {
|
|
|
|
block := schemaMap(r.Schema).coreConfigSchema(enableAsSingle)
|
2018-07-12 02:59:32 +02:00
|
|
|
|
|
|
|
if block.Attributes == nil {
|
|
|
|
block.Attributes = map[string]*configschema.Attribute{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add the implicitly required "id" field if it doesn't exist
|
|
|
|
if block.Attributes["id"] == nil {
|
|
|
|
block.Attributes["id"] = &configschema.Attribute{
|
|
|
|
Type: cty.String,
|
|
|
|
Optional: true,
|
|
|
|
Computed: true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-30 19:14:23 +01:00
|
|
|
_, timeoutsAttr := block.Attributes[TimeoutsConfigKey]
|
|
|
|
_, timeoutsBlock := block.BlockTypes[TimeoutsConfigKey]
|
|
|
|
|
|
|
|
// Insert configured timeout values into the schema, as long as the schema
|
|
|
|
// didn't define anything else by that name.
|
|
|
|
if r.Timeouts != nil && !timeoutsAttr && !timeoutsBlock {
|
2018-10-29 21:11:19 +01:00
|
|
|
timeouts := configschema.Block{
|
|
|
|
Attributes: map[string]*configschema.Attribute{},
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.Timeouts.Create != nil {
|
2018-10-30 19:14:23 +01:00
|
|
|
timeouts.Attributes[TimeoutCreate] = &configschema.Attribute{
|
2018-10-29 21:11:19 +01:00
|
|
|
Type: cty.String,
|
|
|
|
Optional: true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.Timeouts.Read != nil {
|
2018-10-30 19:14:23 +01:00
|
|
|
timeouts.Attributes[TimeoutRead] = &configschema.Attribute{
|
2018-10-29 21:11:19 +01:00
|
|
|
Type: cty.String,
|
|
|
|
Optional: true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.Timeouts.Update != nil {
|
2018-10-30 19:14:23 +01:00
|
|
|
timeouts.Attributes[TimeoutUpdate] = &configschema.Attribute{
|
2018-10-29 21:11:19 +01:00
|
|
|
Type: cty.String,
|
|
|
|
Optional: true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.Timeouts.Delete != nil {
|
2018-10-30 19:14:23 +01:00
|
|
|
timeouts.Attributes[TimeoutDelete] = &configschema.Attribute{
|
2018-10-29 21:11:19 +01:00
|
|
|
Type: cty.String,
|
|
|
|
Optional: true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.Timeouts.Default != nil {
|
2018-10-30 19:14:23 +01:00
|
|
|
timeouts.Attributes[TimeoutDefault] = &configschema.Attribute{
|
2018-10-29 21:11:19 +01:00
|
|
|
Type: cty.String,
|
|
|
|
Optional: true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-30 19:14:23 +01:00
|
|
|
block.BlockTypes[TimeoutsConfigKey] = &configschema.NestedBlock{
|
2018-10-29 21:11:19 +01:00
|
|
|
Nesting: configschema.NestingSingle,
|
|
|
|
Block: timeouts,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-12 02:59:32 +02:00
|
|
|
return block
|
|
|
|
}
|
|
|
|
|
2018-03-16 23:51:33 +01:00
|
|
|
// CoreConfigSchema is a convenient shortcut for calling CoreConfigSchema
|
|
|
|
// on the backends's schema.
|
|
|
|
func (r *Backend) CoreConfigSchema() *configschema.Block {
|
|
|
|
return schemaMap(r.Schema).CoreConfigSchema()
|
|
|
|
}
|