github.com/opentofu/opentofu@v1.7.1/internal/legacy/helper/schema/core_schema.go (about)

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