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