github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/helper/schema/core_schema.go (about)

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