github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/legacy/helper/schema/core_schema.go (about)

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