github.com/hashicorp/terraform-plugin-sdk@v1.17.2/helper/schema/core_schema.go (about)

     1  package schema
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema"
     7  	"github.com/zclconf/go-cty/cty"
     8  )
     9  
    10  var (
    11  	// DescriptionKind is the default StringKind of descriptions in this provider.
    12  	// It defaults to StringPlain but can be globally switched to StringMarkdown.
    13  	DescriptionKind = configschema.StringPlain
    14  
    15  	// SchemaDescriptionBuilder converts helper/schema.Schema Descriptions to configschema.Attribute
    16  	// and Block Descriptions. This method can be used to modify the description text prior to it
    17  	// being returned in the schema.
    18  	SchemaDescriptionBuilder = func(s *Schema) string {
    19  		return s.Description
    20  	}
    21  
    22  	// ResourceDescriptionBuilder converts helper/schema.Resource Descriptions to configschema.Block
    23  	// Descriptions at the resource top level. This method can be used to modify the description prior
    24  	// to it being returned in the schema.
    25  	ResourceDescriptionBuilder = func(r *Resource) string {
    26  		return r.Description
    27  	}
    28  )
    29  
    30  // The functions and methods in this file are concerned with the conversion
    31  // of this package's schema model into the slightly-lower-level schema model
    32  // used by Terraform core for configuration parsing.
    33  
    34  // CoreConfigSchema lowers the receiver to the schema model expected by
    35  // Terraform core.
    36  //
    37  // This lower-level model has fewer features than the schema in this package,
    38  // describing only the basic structure of configuration and state values we
    39  // expect. The full schemaMap from this package is still required for full
    40  // validation, handling of default values, etc.
    41  //
    42  // This method presumes a schema that passes InternalValidate, and so may
    43  // panic or produce an invalid result if given an invalid schemaMap.
    44  func (m schemaMap) CoreConfigSchema() *configschema.Block {
    45  	if len(m) == 0 {
    46  		// We return an actual (empty) object here, rather than a nil,
    47  		// because a nil result would mean that we don't have a schema at
    48  		// all, rather than that we have an empty one.
    49  		return &configschema.Block{}
    50  	}
    51  
    52  	ret := &configschema.Block{
    53  		Attributes: map[string]*configschema.Attribute{},
    54  		BlockTypes: map[string]*configschema.NestedBlock{},
    55  	}
    56  
    57  	for name, schema := range m {
    58  		if schema.Elem == nil {
    59  			ret.Attributes[name] = schema.coreConfigSchemaAttribute()
    60  			continue
    61  		}
    62  		if schema.Type == TypeMap {
    63  			// For TypeMap in particular, it isn't valid for Elem to be a
    64  			// *Resource (since that would be ambiguous in flatmap) and
    65  			// so Elem is treated as a TypeString schema if so. This matches
    66  			// how the field readers treat this situation, for compatibility
    67  			// with configurations targeting Terraform 0.11 and earlier.
    68  			if _, isResource := schema.Elem.(*Resource); isResource {
    69  				sch := *schema // shallow copy
    70  				sch.Elem = &Schema{
    71  					Type: TypeString,
    72  				}
    73  				ret.Attributes[name] = sch.coreConfigSchemaAttribute()
    74  				continue
    75  			}
    76  		}
    77  		switch schema.ConfigMode {
    78  		case SchemaConfigModeAttr:
    79  			ret.Attributes[name] = schema.coreConfigSchemaAttribute()
    80  		case SchemaConfigModeBlock:
    81  			ret.BlockTypes[name] = schema.coreConfigSchemaBlock()
    82  		default: // SchemaConfigModeAuto, or any other invalid value
    83  			if schema.Computed && !schema.Optional {
    84  				// Computed-only schemas are always handled as attributes,
    85  				// because they never appear in configuration.
    86  				ret.Attributes[name] = schema.coreConfigSchemaAttribute()
    87  				continue
    88  			}
    89  			switch schema.Elem.(type) {
    90  			case *Schema, ValueType:
    91  				ret.Attributes[name] = schema.coreConfigSchemaAttribute()
    92  			case *Resource:
    93  				ret.BlockTypes[name] = schema.coreConfigSchemaBlock()
    94  			default:
    95  				// Should never happen for a valid schema
    96  				panic(fmt.Errorf("invalid Schema.Elem %#v; need *Schema or *Resource", schema.Elem))
    97  			}
    98  		}
    99  	}
   100  
   101  	return ret
   102  }
   103  
   104  // coreConfigSchemaAttribute prepares a configschema.Attribute representation
   105  // of a schema. This is appropriate only for primitives or collections whose
   106  // Elem is an instance of Schema. Use coreConfigSchemaBlock for collections
   107  // whose elem is a whole resource.
   108  func (s *Schema) coreConfigSchemaAttribute() *configschema.Attribute {
   109  	// The Schema.DefaultFunc capability adds some extra weirdness here since
   110  	// it can be combined with "Required: true" to create a sitution where
   111  	// required-ness is conditional. Terraform Core doesn't share this concept,
   112  	// so we must sniff for this possibility here and conditionally turn
   113  	// off the "Required" flag if it looks like the DefaultFunc is going
   114  	// to provide a value.
   115  	// This is not 100% true to the original interface of DefaultFunc but
   116  	// works well enough for the EnvDefaultFunc and MultiEnvDefaultFunc
   117  	// situations, which are the main cases we care about.
   118  	//
   119  	// Note that this also has a consequence for commands that return schema
   120  	// information for documentation purposes: running those for certain
   121  	// providers will produce different results depending on which environment
   122  	// variables are set. We accept that weirdness in order to keep this
   123  	// interface to core otherwise simple.
   124  	reqd := s.Required
   125  	opt := s.Optional
   126  	if reqd && s.DefaultFunc != nil {
   127  		v, err := s.DefaultFunc()
   128  		// We can't report errors from here, so we'll instead just force
   129  		// "Required" to false and let the provider try calling its
   130  		// DefaultFunc again during the validate step, where it can then
   131  		// return the error.
   132  		if err != nil || (err == nil && v != nil) {
   133  			reqd = false
   134  			opt = true
   135  		}
   136  	}
   137  
   138  	desc := SchemaDescriptionBuilder(s)
   139  	descKind := DescriptionKind
   140  	if desc == "" {
   141  		// fallback to plain text if empty
   142  		descKind = configschema.StringPlain
   143  	}
   144  
   145  	return &configschema.Attribute{
   146  		Type:            s.coreConfigSchemaType(),
   147  		Optional:        opt,
   148  		Required:        reqd,
   149  		Computed:        s.Computed,
   150  		Sensitive:       s.Sensitive,
   151  		Description:     desc,
   152  		DescriptionKind: descKind,
   153  		Deprecated:      s.Deprecated != "",
   154  	}
   155  }
   156  
   157  // coreConfigSchemaBlock prepares a configschema.NestedBlock representation of
   158  // a schema. This is appropriate only for collections whose Elem is an instance
   159  // of Resource, and will panic otherwise.
   160  func (s *Schema) coreConfigSchemaBlock() *configschema.NestedBlock {
   161  	ret := &configschema.NestedBlock{}
   162  	if nested := s.Elem.(*Resource).coreConfigSchema(); nested != nil {
   163  		ret.Block = *nested
   164  
   165  		desc := SchemaDescriptionBuilder(s)
   166  		descKind := DescriptionKind
   167  		if desc == "" {
   168  			// fallback to plain text if empty
   169  			descKind = configschema.StringPlain
   170  		}
   171  		// set these on the block from the attribute Schema
   172  		ret.Block.Description = desc
   173  		ret.Block.DescriptionKind = descKind
   174  		ret.Block.Deprecated = s.Deprecated != ""
   175  	}
   176  	switch s.Type {
   177  	case TypeList:
   178  		ret.Nesting = configschema.NestingList
   179  	case TypeSet:
   180  		ret.Nesting = configschema.NestingSet
   181  	case TypeMap:
   182  		ret.Nesting = configschema.NestingMap
   183  	default:
   184  		// Should never happen for a valid schema
   185  		panic(fmt.Errorf("invalid s.Type %s for s.Elem being resource", s.Type))
   186  	}
   187  
   188  	ret.MinItems = s.MinItems
   189  	ret.MaxItems = s.MaxItems
   190  
   191  	if s.Required && s.MinItems == 0 {
   192  		// configschema doesn't have a "required" representation for nested
   193  		// blocks, but we can fake it by requiring at least one item.
   194  		ret.MinItems = 1
   195  	}
   196  	if s.Optional && s.MinItems > 0 {
   197  		// Historically helper/schema would ignore MinItems if Optional were
   198  		// set, so we must mimic this behavior here to ensure that providers
   199  		// relying on that undocumented behavior can continue to operate as
   200  		// they did before.
   201  		ret.MinItems = 0
   202  	}
   203  	if s.Computed && !s.Optional {
   204  		// MinItems/MaxItems are meaningless for computed nested blocks, since
   205  		// they are never set by the user anyway. This ensures that we'll never
   206  		// generate weird errors about them.
   207  		ret.MinItems = 0
   208  		ret.MaxItems = 0
   209  	}
   210  
   211  	return ret
   212  }
   213  
   214  // coreConfigSchemaType determines the core config schema type that corresponds
   215  // to a particular schema's type.
   216  func (s *Schema) coreConfigSchemaType() cty.Type {
   217  	switch s.Type {
   218  	case TypeString:
   219  		return cty.String
   220  	case TypeBool:
   221  		return cty.Bool
   222  	case TypeInt, TypeFloat:
   223  		// configschema doesn't distinguish int and float, so helper/schema
   224  		// will deal with this as an additional validation step after
   225  		// configuration has been parsed and decoded.
   226  		return cty.Number
   227  	case TypeList, TypeSet, TypeMap:
   228  		var elemType cty.Type
   229  		switch set := s.Elem.(type) {
   230  		case *Schema:
   231  			elemType = set.coreConfigSchemaType()
   232  		case ValueType:
   233  			// This represents a mistake in the provider code, but it's a
   234  			// common one so we'll just shim it.
   235  			elemType = (&Schema{Type: set}).coreConfigSchemaType()
   236  		case *Resource:
   237  			// By default we construct a NestedBlock in this case, but this
   238  			// behavior is selected either for computed-only schemas or
   239  			// when ConfigMode is explicitly SchemaConfigModeBlock.
   240  			// See schemaMap.CoreConfigSchema for the exact rules.
   241  			elemType = set.coreConfigSchema().ImpliedType()
   242  		default:
   243  			if set != nil {
   244  				// Should never happen for a valid schema
   245  				panic(fmt.Errorf("invalid Schema.Elem %#v; need *Schema or *Resource", s.Elem))
   246  			}
   247  			// Some pre-existing schemas assume string as default, so we need
   248  			// to be compatible with them.
   249  			elemType = cty.String
   250  		}
   251  		switch s.Type {
   252  		case TypeList:
   253  			return cty.List(elemType)
   254  		case TypeSet:
   255  			return cty.Set(elemType)
   256  		case TypeMap:
   257  			return cty.Map(elemType)
   258  		default:
   259  			// can never get here in practice, due to the case we're inside
   260  			panic("invalid collection type")
   261  		}
   262  	default:
   263  		// should never happen for a valid schema
   264  		panic(fmt.Errorf("invalid Schema.Type %s", s.Type))
   265  	}
   266  }
   267  
   268  // CoreConfigSchema is a convenient shortcut for calling CoreConfigSchema on
   269  // the resource's schema. CoreConfigSchema adds the implicitly required "id"
   270  // attribute for top level resources if it doesn't exist.
   271  func (r *Resource) CoreConfigSchema() *configschema.Block {
   272  	block := r.coreConfigSchema()
   273  
   274  	desc := ResourceDescriptionBuilder(r)
   275  	descKind := DescriptionKind
   276  	if desc == "" {
   277  		// fallback to plain text if empty
   278  		descKind = configschema.StringPlain
   279  	}
   280  
   281  	// Only apply Resource Description, Kind, Deprecation at top level
   282  	block.Description = desc
   283  	block.DescriptionKind = descKind
   284  	block.Deprecated = r.DeprecationMessage != ""
   285  
   286  	if block.Attributes == nil {
   287  		block.Attributes = map[string]*configschema.Attribute{}
   288  	}
   289  
   290  	// Add the implicitly required "id" field if it doesn't exist
   291  	if block.Attributes["id"] == nil {
   292  		block.Attributes["id"] = &configschema.Attribute{
   293  			Type:     cty.String,
   294  			Optional: true,
   295  			Computed: true,
   296  		}
   297  	}
   298  
   299  	_, timeoutsAttr := block.Attributes[TimeoutsConfigKey]
   300  	_, timeoutsBlock := block.BlockTypes[TimeoutsConfigKey]
   301  
   302  	// Insert configured timeout values into the schema, as long as the schema
   303  	// didn't define anything else by that name.
   304  	if r.Timeouts != nil && !timeoutsAttr && !timeoutsBlock {
   305  		timeouts := configschema.Block{
   306  			Attributes: map[string]*configschema.Attribute{},
   307  		}
   308  
   309  		if r.Timeouts.Create != nil {
   310  			timeouts.Attributes[TimeoutCreate] = &configschema.Attribute{
   311  				Type:     cty.String,
   312  				Optional: true,
   313  			}
   314  		}
   315  
   316  		if r.Timeouts.Read != nil {
   317  			timeouts.Attributes[TimeoutRead] = &configschema.Attribute{
   318  				Type:     cty.String,
   319  				Optional: true,
   320  			}
   321  		}
   322  
   323  		if r.Timeouts.Update != nil {
   324  			timeouts.Attributes[TimeoutUpdate] = &configschema.Attribute{
   325  				Type:     cty.String,
   326  				Optional: true,
   327  			}
   328  		}
   329  
   330  		if r.Timeouts.Delete != nil {
   331  			timeouts.Attributes[TimeoutDelete] = &configschema.Attribute{
   332  				Type:     cty.String,
   333  				Optional: true,
   334  			}
   335  		}
   336  
   337  		if r.Timeouts.Default != nil {
   338  			timeouts.Attributes[TimeoutDefault] = &configschema.Attribute{
   339  				Type:     cty.String,
   340  				Optional: true,
   341  			}
   342  		}
   343  
   344  		block.BlockTypes[TimeoutsConfigKey] = &configschema.NestedBlock{
   345  			Nesting: configschema.NestingSingle,
   346  			Block:   timeouts,
   347  		}
   348  	}
   349  
   350  	return block
   351  }
   352  
   353  func (r *Resource) coreConfigSchema() *configschema.Block {
   354  	return schemaMap(r.Schema).CoreConfigSchema()
   355  }
   356  
   357  // CoreConfigSchema is a convenient shortcut for calling CoreConfigSchema
   358  // on the backends's schema.
   359  func (r *Backend) CoreConfigSchema() *configschema.Block {
   360  	return schemaMap(r.Schema).CoreConfigSchema()
   361  }