terraform/helper/schema/core_schema.go

357 lines
12 KiB
Go
Raw Normal View History

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()
}