357 lines
12 KiB
Go
357 lines
12 KiB
Go
package schema
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/terraform/configs/configschema"
|
|
"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 {
|
|
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 {
|
|
if len(m) == 0 {
|
|
// 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{}
|
|
}
|
|
|
|
ret := &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{},
|
|
BlockTypes: map[string]*configschema.NestedBlock{},
|
|
}
|
|
|
|
for name, schema := range m {
|
|
if schema.Elem == nil {
|
|
ret.Attributes[name] = schema.coreConfigSchemaAttribute(enableAsSingle)
|
|
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(enableAsSingle)
|
|
continue
|
|
}
|
|
}
|
|
switch schema.ConfigMode {
|
|
case SchemaConfigModeAttr:
|
|
ret.Attributes[name] = schema.coreConfigSchemaAttribute(enableAsSingle)
|
|
case SchemaConfigModeBlock:
|
|
ret.BlockTypes[name] = schema.coreConfigSchemaBlock(enableAsSingle)
|
|
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.
|
|
ret.Attributes[name] = schema.coreConfigSchemaAttribute(enableAsSingle)
|
|
continue
|
|
}
|
|
switch schema.Elem.(type) {
|
|
case *Schema, ValueType:
|
|
ret.Attributes[name] = schema.coreConfigSchemaAttribute(enableAsSingle)
|
|
case *Resource:
|
|
ret.BlockTypes[name] = schema.coreConfigSchemaBlock(enableAsSingle)
|
|
default:
|
|
// Should never happen for a valid schema
|
|
panic(fmt.Errorf("invalid Schema.Elem %#v; need *Schema or *Resource", schema.Elem))
|
|
}
|
|
}
|
|
}
|
|
|
|
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.
|
|
func (s *Schema) coreConfigSchemaAttribute(enableAsSingle bool) *configschema.Attribute {
|
|
// 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
|
|
}
|
|
}
|
|
|
|
return &configschema.Attribute{
|
|
Type: s.coreConfigSchemaType(enableAsSingle),
|
|
Optional: opt,
|
|
Required: reqd,
|
|
Computed: s.Computed,
|
|
Sensitive: s.Sensitive,
|
|
Description: s.Description,
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
func (s *Schema) coreConfigSchemaBlock(enableAsSingle bool) *configschema.NestedBlock {
|
|
ret := &configschema.NestedBlock{}
|
|
if nested := schemaMap(s.Elem.(*Resource).Schema).coreConfigSchema(enableAsSingle); nested != nil {
|
|
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
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
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
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
// coreConfigSchemaType determines the core config schema type that corresponds
|
|
// to a particular schema's type.
|
|
func (s *Schema) coreConfigSchemaType(enableAsSingle bool) cty.Type {
|
|
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:
|
|
elemType = set.coreConfigSchemaType(enableAsSingle)
|
|
case ValueType:
|
|
// This represents a mistake in the provider code, but it's a
|
|
// common one so we'll just shim it.
|
|
elemType = (&Schema{Type: set}).coreConfigSchemaType(enableAsSingle)
|
|
case *Resource:
|
|
// 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.
|
|
elemType = schemaMap(set.Schema).coreConfigSchema(enableAsSingle).ImpliedType()
|
|
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
|
|
}
|
|
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
|
|
}
|
|
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))
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
func (r *Resource) CoreConfigSchema() *configschema.Block {
|
|
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)
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
_, 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 {
|
|
timeouts := configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{},
|
|
}
|
|
|
|
if r.Timeouts.Create != nil {
|
|
timeouts.Attributes[TimeoutCreate] = &configschema.Attribute{
|
|
Type: cty.String,
|
|
Optional: true,
|
|
}
|
|
}
|
|
|
|
if r.Timeouts.Read != nil {
|
|
timeouts.Attributes[TimeoutRead] = &configschema.Attribute{
|
|
Type: cty.String,
|
|
Optional: true,
|
|
}
|
|
}
|
|
|
|
if r.Timeouts.Update != nil {
|
|
timeouts.Attributes[TimeoutUpdate] = &configschema.Attribute{
|
|
Type: cty.String,
|
|
Optional: true,
|
|
}
|
|
}
|
|
|
|
if r.Timeouts.Delete != nil {
|
|
timeouts.Attributes[TimeoutDelete] = &configschema.Attribute{
|
|
Type: cty.String,
|
|
Optional: true,
|
|
}
|
|
}
|
|
|
|
if r.Timeouts.Default != nil {
|
|
timeouts.Attributes[TimeoutDefault] = &configschema.Attribute{
|
|
Type: cty.String,
|
|
Optional: true,
|
|
}
|
|
}
|
|
|
|
block.BlockTypes[TimeoutsConfigKey] = &configschema.NestedBlock{
|
|
Nesting: configschema.NestingSingle,
|
|
Block: timeouts,
|
|
}
|
|
}
|
|
|
|
return block
|
|
}
|
|
|
|
// CoreConfigSchema is a convenient shortcut for calling CoreConfigSchema
|
|
// on the backends's schema.
|
|
func (r *Backend) CoreConfigSchema() *configschema.Block {
|
|
return schemaMap(r.Schema).CoreConfigSchema()
|
|
}
|