1package schema
2
3import (
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.
24func (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.
88func (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.
131func (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.
176func (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.
231func (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
301func (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.
307func (r *Backend) CoreConfigSchema() *configschema.Block {
308	return schemaMap(r.Schema).CoreConfigSchema()
309}
310