]>
Commit | Line | Data |
---|---|---|
15c0b25d AP |
1 | package schema |
2 | ||
3 | import ( | |
4 | "fmt" | |
5 | ||
107c1cdb | 6 | "github.com/hashicorp/terraform/configs/configschema" |
15c0b25d AP |
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 | } | |
107c1cdb ND |
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: | |
15c0b25d | 59 | ret.Attributes[name] = schema.coreConfigSchemaAttribute() |
107c1cdb | 60 | case SchemaConfigModeBlock: |
15c0b25d | 61 | ret.BlockTypes[name] = schema.coreConfigSchemaBlock() |
107c1cdb ND |
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 | } | |
15c0b25d AP |
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 { | |
107c1cdb ND |
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 | ||
15c0b25d | 118 | return &configschema.Attribute{ |
107c1cdb ND |
119 | Type: s.coreConfigSchemaType(), |
120 | Optional: opt, | |
121 | Required: reqd, | |
122 | Computed: s.Computed, | |
123 | Sensitive: s.Sensitive, | |
124 | Description: s.Description, | |
15c0b25d AP |
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{} | |
107c1cdb | 133 | if nested := s.Elem.(*Resource).coreConfigSchema(); nested != nil { |
15c0b25d AP |
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 | } | |
107c1cdb ND |
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 | } | |
15c0b25d AP |
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() | |
107c1cdb ND |
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() | |
15c0b25d | 196 | case *Resource: |
107c1cdb ND |
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() | |
15c0b25d AP |
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 | ||
107c1cdb ND |
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. | |
15c0b25d | 231 | func (r *Resource) CoreConfigSchema() *configschema.Block { |
107c1cdb ND |
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 { | |
15c0b25d AP |
308 | return schemaMap(r.Schema).CoreConfigSchema() |
309 | } |