]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | package schema |
2 | ||
3 | import ( | |
4 | "errors" | |
5 | "fmt" | |
6 | "log" | |
7 | "strconv" | |
8 | ||
c680a8e1 | 9 | "github.com/hashicorp/terraform/config" |
bae9f6d2 | 10 | "github.com/hashicorp/terraform/terraform" |
107c1cdb | 11 | "github.com/zclconf/go-cty/cty" |
bae9f6d2 JC |
12 | ) |
13 | ||
14 | // Resource represents a thing in Terraform that has a set of configurable | |
15 | // attributes and a lifecycle (create, read, update, delete). | |
16 | // | |
17 | // The Resource schema is an abstraction that allows provider writers to | |
18 | // worry only about CRUD operations while off-loading validation, diff | |
19 | // generation, etc. to this higher level library. | |
20 | // | |
21 | // In spite of the name, this struct is not used only for terraform resources, | |
22 | // but also for data sources. In the case of data sources, the Create, | |
23 | // Update and Delete functions must not be provided. | |
24 | type Resource struct { | |
25 | // Schema is the schema for the configuration of this resource. | |
26 | // | |
27 | // The keys of this map are the configuration keys, and the values | |
28 | // describe the schema of the configuration value. | |
29 | // | |
30 | // The schema is used to represent both configurable data as well | |
31 | // as data that might be computed in the process of creating this | |
32 | // resource. | |
33 | Schema map[string]*Schema | |
34 | ||
35 | // SchemaVersion is the version number for this resource's Schema | |
36 | // definition. The current SchemaVersion stored in the state for each | |
37 | // resource. Provider authors can increment this version number | |
38 | // when Schema semantics change. If the State's SchemaVersion is less than | |
39 | // the current SchemaVersion, the InstanceState is yielded to the | |
40 | // MigrateState callback, where the provider can make whatever changes it | |
41 | // needs to update the state to be compatible to the latest version of the | |
42 | // Schema. | |
43 | // | |
44 | // When unset, SchemaVersion defaults to 0, so provider authors can start | |
45 | // their Versioning at any integer >= 1 | |
46 | SchemaVersion int | |
47 | ||
107c1cdb ND |
48 | // MigrateState is deprecated and any new changes to a resource's schema |
49 | // should be handled by StateUpgraders. Existing MigrateState implementations | |
50 | // should remain for compatibility with existing state. MigrateState will | |
51 | // still be called if the stored SchemaVersion is less than the | |
52 | // first version of the StateUpgraders. | |
53 | // | |
bae9f6d2 JC |
54 | // MigrateState is responsible for updating an InstanceState with an old |
55 | // version to the format expected by the current version of the Schema. | |
56 | // | |
57 | // It is called during Refresh if the State's stored SchemaVersion is less | |
58 | // than the current SchemaVersion of the Resource. | |
59 | // | |
60 | // The function is yielded the state's stored SchemaVersion and a pointer to | |
61 | // the InstanceState that needs updating, as well as the configured | |
62 | // provider's configured meta interface{}, in case the migration process | |
63 | // needs to make any remote API calls. | |
64 | MigrateState StateMigrateFunc | |
65 | ||
107c1cdb ND |
66 | // StateUpgraders contains the functions responsible for upgrading an |
67 | // existing state with an old schema version to a newer schema. It is | |
68 | // called specifically by Terraform when the stored schema version is less | |
69 | // than the current SchemaVersion of the Resource. | |
70 | // | |
71 | // StateUpgraders map specific schema versions to a StateUpgrader | |
72 | // function. The registered versions are expected to be ordered, | |
73 | // consecutive values. The initial value may be greater than 0 to account | |
74 | // for legacy schemas that weren't recorded and can be handled by | |
75 | // MigrateState. | |
76 | StateUpgraders []StateUpgrader | |
77 | ||
bae9f6d2 JC |
78 | // The functions below are the CRUD operations for this resource. |
79 | // | |
80 | // The only optional operation is Update. If Update is not implemented, | |
81 | // then updates will not be supported for this resource. | |
82 | // | |
83 | // The ResourceData parameter in the functions below are used to | |
84 | // query configuration and changes for the resource as well as to set | |
85 | // the ID, computed data, etc. | |
86 | // | |
87 | // The interface{} parameter is the result of the ConfigureFunc in | |
88 | // the provider for this resource. If the provider does not define | |
89 | // a ConfigureFunc, this will be nil. This parameter should be used | |
90 | // to store API clients, configuration structures, etc. | |
91 | // | |
92 | // If any errors occur during each of the operation, an error should be | |
93 | // returned. If a resource was partially updated, be careful to enable | |
94 | // partial state mode for ResourceData and use it accordingly. | |
95 | // | |
96 | // Exists is a function that is called to check if a resource still | |
97 | // exists. If this returns false, then this will affect the diff | |
98 | // accordingly. If this function isn't set, it will not be called. It | |
99 | // is highly recommended to set it. The *ResourceData passed to Exists | |
100 | // should _not_ be modified. | |
101 | Create CreateFunc | |
102 | Read ReadFunc | |
103 | Update UpdateFunc | |
104 | Delete DeleteFunc | |
105 | Exists ExistsFunc | |
106 | ||
15c0b25d AP |
107 | // CustomizeDiff is a custom function for working with the diff that |
108 | // Terraform has created for this resource - it can be used to customize the | |
109 | // diff that has been created, diff values not controlled by configuration, | |
110 | // or even veto the diff altogether and abort the plan. It is passed a | |
111 | // *ResourceDiff, a structure similar to ResourceData but lacking most write | |
112 | // functions like Set, while introducing new functions that work with the | |
113 | // diff such as SetNew, SetNewComputed, and ForceNew. | |
114 | // | |
115 | // The phases Terraform runs this in, and the state available via functions | |
116 | // like Get and GetChange, are as follows: | |
117 | // | |
118 | // * New resource: One run with no state | |
119 | // * Existing resource: One run with state | |
120 | // * Existing resource, forced new: One run with state (before ForceNew), | |
121 | // then one run without state (as if new resource) | |
122 | // * Tainted resource: No runs (custom diff logic is skipped) | |
123 | // * Destroy: No runs (standard diff logic is skipped on destroy diffs) | |
124 | // | |
125 | // This function needs to be resilient to support all scenarios. | |
126 | // | |
127 | // If this function needs to access external API resources, remember to flag | |
128 | // the RequiresRefresh attribute mentioned below to ensure that | |
129 | // -refresh=false is blocked when running plan or apply, as this means that | |
130 | // this resource requires refresh-like behaviour to work effectively. | |
131 | // | |
132 | // For the most part, only computed fields can be customized by this | |
133 | // function. | |
134 | // | |
135 | // This function is only allowed on regular resources (not data sources). | |
136 | CustomizeDiff CustomizeDiffFunc | |
137 | ||
bae9f6d2 JC |
138 | // Importer is the ResourceImporter implementation for this resource. |
139 | // If this is nil, then this resource does not support importing. If | |
140 | // this is non-nil, then it supports importing and ResourceImporter | |
141 | // must be validated. The validity of ResourceImporter is verified | |
142 | // by InternalValidate on Resource. | |
143 | Importer *ResourceImporter | |
144 | ||
145 | // If non-empty, this string is emitted as a warning during Validate. | |
15c0b25d | 146 | DeprecationMessage string |
bae9f6d2 JC |
147 | |
148 | // Timeouts allow users to specify specific time durations in which an | |
149 | // operation should time out, to allow them to extend an action to suit their | |
150 | // usage. For example, a user may specify a large Creation timeout for their | |
151 | // AWS RDS Instance due to it's size, or restoring from a snapshot. | |
152 | // Resource implementors must enable Timeout support by adding the allowed | |
153 | // actions (Create, Read, Update, Delete, Default) to the Resource struct, and | |
154 | // accessing them in the matching methods. | |
155 | Timeouts *ResourceTimeout | |
156 | } | |
157 | ||
107c1cdb ND |
158 | // ShimInstanceStateFromValue converts a cty.Value to a |
159 | // terraform.InstanceState. | |
160 | func (r *Resource) ShimInstanceStateFromValue(state cty.Value) (*terraform.InstanceState, error) { | |
161 | // Get the raw shimmed value. While this is correct, the set hashes don't | |
162 | // match those from the Schema. | |
163 | s := terraform.NewInstanceStateShimmedFromValue(state, r.SchemaVersion) | |
164 | ||
165 | // We now rebuild the state through the ResourceData, so that the set indexes | |
166 | // match what helper/schema expects. | |
167 | data, err := schemaMap(r.Schema).Data(s, nil) | |
168 | if err != nil { | |
169 | return nil, err | |
170 | } | |
171 | ||
172 | s = data.State() | |
173 | if s == nil { | |
174 | s = &terraform.InstanceState{} | |
175 | } | |
176 | return s, nil | |
177 | } | |
178 | ||
bae9f6d2 JC |
179 | // See Resource documentation. |
180 | type CreateFunc func(*ResourceData, interface{}) error | |
181 | ||
182 | // See Resource documentation. | |
183 | type ReadFunc func(*ResourceData, interface{}) error | |
184 | ||
185 | // See Resource documentation. | |
186 | type UpdateFunc func(*ResourceData, interface{}) error | |
187 | ||
188 | // See Resource documentation. | |
189 | type DeleteFunc func(*ResourceData, interface{}) error | |
190 | ||
191 | // See Resource documentation. | |
192 | type ExistsFunc func(*ResourceData, interface{}) (bool, error) | |
193 | ||
194 | // See Resource documentation. | |
195 | type StateMigrateFunc func( | |
196 | int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error) | |
197 | ||
107c1cdb ND |
198 | type StateUpgrader struct { |
199 | // Version is the version schema that this Upgrader will handle, converting | |
200 | // it to Version+1. | |
201 | Version int | |
202 | ||
203 | // Type describes the schema that this function can upgrade. Type is | |
204 | // required to decode the schema if the state was stored in a legacy | |
205 | // flatmap format. | |
206 | Type cty.Type | |
207 | ||
208 | // Upgrade takes the JSON encoded state and the provider meta value, and | |
209 | // upgrades the state one single schema version. The provided state is | |
210 | // deocded into the default json types using a map[string]interface{}. It | |
211 | // is up to the StateUpgradeFunc to ensure that the returned value can be | |
212 | // encoded using the new schema. | |
213 | Upgrade StateUpgradeFunc | |
214 | } | |
215 | ||
216 | // See StateUpgrader | |
217 | type StateUpgradeFunc func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) | |
218 | ||
15c0b25d AP |
219 | // See Resource documentation. |
220 | type CustomizeDiffFunc func(*ResourceDiff, interface{}) error | |
221 | ||
bae9f6d2 JC |
222 | // Apply creates, updates, and/or deletes a resource. |
223 | func (r *Resource) Apply( | |
224 | s *terraform.InstanceState, | |
225 | d *terraform.InstanceDiff, | |
226 | meta interface{}) (*terraform.InstanceState, error) { | |
227 | data, err := schemaMap(r.Schema).Data(s, d) | |
228 | if err != nil { | |
229 | return s, err | |
230 | } | |
231 | ||
232 | // Instance Diff shoould have the timeout info, need to copy it over to the | |
233 | // ResourceData meta | |
234 | rt := ResourceTimeout{} | |
235 | if _, ok := d.Meta[TimeoutKey]; ok { | |
236 | if err := rt.DiffDecode(d); err != nil { | |
237 | log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) | |
238 | } | |
c680a8e1 RS |
239 | } else if s != nil { |
240 | if _, ok := s.Meta[TimeoutKey]; ok { | |
241 | if err := rt.StateDecode(s); err != nil { | |
242 | log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) | |
243 | } | |
244 | } | |
bae9f6d2 JC |
245 | } else { |
246 | log.Printf("[DEBUG] No meta timeoutkey found in Apply()") | |
247 | } | |
248 | data.timeouts = &rt | |
249 | ||
250 | if s == nil { | |
251 | // The Terraform API dictates that this should never happen, but | |
252 | // it doesn't hurt to be safe in this case. | |
253 | s = new(terraform.InstanceState) | |
254 | } | |
255 | ||
256 | if d.Destroy || d.RequiresNew() { | |
257 | if s.ID != "" { | |
258 | // Destroy the resource since it is created | |
259 | if err := r.Delete(data, meta); err != nil { | |
260 | return r.recordCurrentSchemaVersion(data.State()), err | |
261 | } | |
262 | ||
263 | // Make sure the ID is gone. | |
264 | data.SetId("") | |
265 | } | |
266 | ||
267 | // If we're only destroying, and not creating, then return | |
268 | // now since we're done! | |
269 | if !d.RequiresNew() { | |
270 | return nil, nil | |
271 | } | |
272 | ||
273 | // Reset the data to be stateless since we just destroyed | |
274 | data, err = schemaMap(r.Schema).Data(nil, d) | |
275 | // data was reset, need to re-apply the parsed timeouts | |
276 | data.timeouts = &rt | |
277 | if err != nil { | |
278 | return nil, err | |
279 | } | |
280 | } | |
281 | ||
282 | err = nil | |
283 | if data.Id() == "" { | |
284 | // We're creating, it is a new resource. | |
285 | data.MarkNewResource() | |
286 | err = r.Create(data, meta) | |
287 | } else { | |
288 | if r.Update == nil { | |
289 | return s, fmt.Errorf("doesn't support update") | |
290 | } | |
291 | ||
292 | err = r.Update(data, meta) | |
293 | } | |
294 | ||
295 | return r.recordCurrentSchemaVersion(data.State()), err | |
296 | } | |
297 | ||
15c0b25d | 298 | // Diff returns a diff of this resource. |
bae9f6d2 JC |
299 | func (r *Resource) Diff( |
300 | s *terraform.InstanceState, | |
15c0b25d AP |
301 | c *terraform.ResourceConfig, |
302 | meta interface{}) (*terraform.InstanceDiff, error) { | |
bae9f6d2 JC |
303 | |
304 | t := &ResourceTimeout{} | |
305 | err := t.ConfigDecode(r, c) | |
306 | ||
307 | if err != nil { | |
308 | return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err) | |
309 | } | |
310 | ||
107c1cdb | 311 | instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta, true) |
bae9f6d2 JC |
312 | if err != nil { |
313 | return instanceDiff, err | |
314 | } | |
315 | ||
316 | if instanceDiff != nil { | |
317 | if err := t.DiffEncode(instanceDiff); err != nil { | |
318 | log.Printf("[ERR] Error encoding timeout to instance diff: %s", err) | |
319 | } | |
320 | } else { | |
321 | log.Printf("[DEBUG] Instance Diff is nil in Diff()") | |
322 | } | |
323 | ||
324 | return instanceDiff, err | |
325 | } | |
326 | ||
107c1cdb ND |
327 | func (r *Resource) simpleDiff( |
328 | s *terraform.InstanceState, | |
329 | c *terraform.ResourceConfig, | |
330 | meta interface{}) (*terraform.InstanceDiff, error) { | |
331 | ||
332 | t := &ResourceTimeout{} | |
333 | err := t.ConfigDecode(r, c) | |
334 | ||
335 | if err != nil { | |
336 | return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err) | |
337 | } | |
338 | ||
339 | instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta, false) | |
340 | if err != nil { | |
341 | return instanceDiff, err | |
342 | } | |
343 | ||
344 | if instanceDiff == nil { | |
345 | log.Printf("[DEBUG] Instance Diff is nil in SimpleDiff()") | |
346 | return nil, err | |
347 | } | |
348 | ||
349 | // Make sure the old value is set in each of the instance diffs. | |
350 | // This was done by the RequiresNew logic in the full legacy Diff. | |
351 | for k, attr := range instanceDiff.Attributes { | |
352 | if attr == nil { | |
353 | continue | |
354 | } | |
355 | if s != nil { | |
356 | attr.Old = s.Attributes[k] | |
357 | } | |
358 | } | |
359 | ||
360 | if err := t.DiffEncode(instanceDiff); err != nil { | |
361 | log.Printf("[ERR] Error encoding timeout to instance diff: %s", err) | |
362 | } | |
363 | return instanceDiff, err | |
364 | } | |
365 | ||
bae9f6d2 JC |
366 | // Validate validates the resource configuration against the schema. |
367 | func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) { | |
368 | warns, errs := schemaMap(r.Schema).Validate(c) | |
369 | ||
15c0b25d AP |
370 | if r.DeprecationMessage != "" { |
371 | warns = append(warns, r.DeprecationMessage) | |
bae9f6d2 JC |
372 | } |
373 | ||
374 | return warns, errs | |
375 | } | |
376 | ||
377 | // ReadDataApply loads the data for a data source, given a diff that | |
378 | // describes the configuration arguments and desired computed attributes. | |
379 | func (r *Resource) ReadDataApply( | |
380 | d *terraform.InstanceDiff, | |
381 | meta interface{}, | |
382 | ) (*terraform.InstanceState, error) { | |
bae9f6d2 JC |
383 | // Data sources are always built completely from scratch |
384 | // on each read, so the source state is always nil. | |
385 | data, err := schemaMap(r.Schema).Data(nil, d) | |
386 | if err != nil { | |
387 | return nil, err | |
388 | } | |
389 | ||
390 | err = r.Read(data, meta) | |
391 | state := data.State() | |
392 | if state != nil && state.ID == "" { | |
393 | // Data sources can set an ID if they want, but they aren't | |
394 | // required to; we'll provide a placeholder if they don't, | |
395 | // to preserve the invariant that all resources have non-empty | |
396 | // ids. | |
397 | state.ID = "-" | |
398 | } | |
399 | ||
400 | return r.recordCurrentSchemaVersion(state), err | |
401 | } | |
402 | ||
107c1cdb ND |
403 | // RefreshWithoutUpgrade reads the instance state, but does not call |
404 | // MigrateState or the StateUpgraders, since those are now invoked in a | |
405 | // separate API call. | |
406 | // RefreshWithoutUpgrade is part of the new plugin shims. | |
407 | func (r *Resource) RefreshWithoutUpgrade( | |
bae9f6d2 JC |
408 | s *terraform.InstanceState, |
409 | meta interface{}) (*terraform.InstanceState, error) { | |
410 | // If the ID is already somehow blank, it doesn't exist | |
411 | if s.ID == "" { | |
412 | return nil, nil | |
413 | } | |
414 | ||
415 | rt := ResourceTimeout{} | |
416 | if _, ok := s.Meta[TimeoutKey]; ok { | |
417 | if err := rt.StateDecode(s); err != nil { | |
418 | log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) | |
419 | } | |
420 | } | |
421 | ||
422 | if r.Exists != nil { | |
423 | // Make a copy of data so that if it is modified it doesn't | |
424 | // affect our Read later. | |
425 | data, err := schemaMap(r.Schema).Data(s, nil) | |
426 | data.timeouts = &rt | |
427 | ||
428 | if err != nil { | |
429 | return s, err | |
430 | } | |
431 | ||
432 | exists, err := r.Exists(data, meta) | |
433 | if err != nil { | |
434 | return s, err | |
435 | } | |
436 | if !exists { | |
437 | return nil, nil | |
438 | } | |
439 | } | |
440 | ||
107c1cdb ND |
441 | data, err := schemaMap(r.Schema).Data(s, nil) |
442 | data.timeouts = &rt | |
443 | if err != nil { | |
444 | return s, err | |
445 | } | |
446 | ||
447 | err = r.Read(data, meta) | |
448 | state := data.State() | |
449 | if state != nil && state.ID == "" { | |
450 | state = nil | |
451 | } | |
452 | ||
453 | return r.recordCurrentSchemaVersion(state), err | |
454 | } | |
455 | ||
456 | // Refresh refreshes the state of the resource. | |
457 | func (r *Resource) Refresh( | |
458 | s *terraform.InstanceState, | |
459 | meta interface{}) (*terraform.InstanceState, error) { | |
460 | // If the ID is already somehow blank, it doesn't exist | |
461 | if s.ID == "" { | |
462 | return nil, nil | |
463 | } | |
464 | ||
465 | rt := ResourceTimeout{} | |
466 | if _, ok := s.Meta[TimeoutKey]; ok { | |
467 | if err := rt.StateDecode(s); err != nil { | |
468 | log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) | |
469 | } | |
470 | } | |
471 | ||
472 | if r.Exists != nil { | |
473 | // Make a copy of data so that if it is modified it doesn't | |
474 | // affect our Read later. | |
475 | data, err := schemaMap(r.Schema).Data(s, nil) | |
476 | data.timeouts = &rt | |
477 | ||
bae9f6d2 JC |
478 | if err != nil { |
479 | return s, err | |
480 | } | |
107c1cdb ND |
481 | |
482 | exists, err := r.Exists(data, meta) | |
483 | if err != nil { | |
484 | return s, err | |
485 | } | |
486 | if !exists { | |
487 | return nil, nil | |
488 | } | |
489 | } | |
490 | ||
491 | // there may be new StateUpgraders that need to be run | |
492 | s, err := r.upgradeState(s, meta) | |
493 | if err != nil { | |
494 | return s, err | |
bae9f6d2 JC |
495 | } |
496 | ||
497 | data, err := schemaMap(r.Schema).Data(s, nil) | |
498 | data.timeouts = &rt | |
499 | if err != nil { | |
500 | return s, err | |
501 | } | |
502 | ||
503 | err = r.Read(data, meta) | |
504 | state := data.State() | |
505 | if state != nil && state.ID == "" { | |
506 | state = nil | |
507 | } | |
508 | ||
509 | return r.recordCurrentSchemaVersion(state), err | |
510 | } | |
511 | ||
107c1cdb ND |
512 | func (r *Resource) upgradeState(s *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { |
513 | var err error | |
514 | ||
515 | needsMigration, stateSchemaVersion := r.checkSchemaVersion(s) | |
516 | migrate := needsMigration && r.MigrateState != nil | |
517 | ||
518 | if migrate { | |
519 | s, err = r.MigrateState(stateSchemaVersion, s, meta) | |
520 | if err != nil { | |
521 | return s, err | |
522 | } | |
523 | } | |
524 | ||
525 | if len(r.StateUpgraders) == 0 { | |
526 | return s, nil | |
527 | } | |
528 | ||
529 | // If we ran MigrateState, then the stateSchemaVersion value is no longer | |
530 | // correct. We can expect the first upgrade function to be the correct | |
531 | // schema type version. | |
532 | if migrate { | |
533 | stateSchemaVersion = r.StateUpgraders[0].Version | |
534 | } | |
535 | ||
536 | schemaType := r.CoreConfigSchema().ImpliedType() | |
537 | // find the expected type to convert the state | |
538 | for _, upgrader := range r.StateUpgraders { | |
539 | if stateSchemaVersion == upgrader.Version { | |
540 | schemaType = upgrader.Type | |
541 | } | |
542 | } | |
543 | ||
544 | // StateUpgraders only operate on the new JSON format state, so the state | |
545 | // need to be converted. | |
546 | stateVal, err := StateValueFromInstanceState(s, schemaType) | |
547 | if err != nil { | |
548 | return nil, err | |
549 | } | |
550 | ||
551 | jsonState, err := StateValueToJSONMap(stateVal, schemaType) | |
552 | if err != nil { | |
553 | return nil, err | |
554 | } | |
555 | ||
556 | for _, upgrader := range r.StateUpgraders { | |
557 | if stateSchemaVersion != upgrader.Version { | |
558 | continue | |
559 | } | |
560 | ||
561 | jsonState, err = upgrader.Upgrade(jsonState, meta) | |
562 | if err != nil { | |
563 | return nil, err | |
564 | } | |
565 | stateSchemaVersion++ | |
566 | } | |
567 | ||
568 | // now we need to re-flatmap the new state | |
569 | stateVal, err = JSONMapToStateValue(jsonState, r.CoreConfigSchema()) | |
570 | if err != nil { | |
571 | return nil, err | |
572 | } | |
573 | ||
574 | return r.ShimInstanceStateFromValue(stateVal) | |
575 | } | |
576 | ||
bae9f6d2 JC |
577 | // InternalValidate should be called to validate the structure |
578 | // of the resource. | |
579 | // | |
580 | // This should be called in a unit test for any resource to verify | |
581 | // before release that a resource is properly configured for use with | |
582 | // this library. | |
583 | // | |
584 | // Provider.InternalValidate() will automatically call this for all of | |
585 | // the resources it manages, so you don't need to call this manually if it | |
586 | // is part of a Provider. | |
587 | func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error { | |
588 | if r == nil { | |
589 | return errors.New("resource is nil") | |
590 | } | |
591 | ||
592 | if !writable { | |
593 | if r.Create != nil || r.Update != nil || r.Delete != nil { | |
594 | return fmt.Errorf("must not implement Create, Update or Delete") | |
595 | } | |
15c0b25d AP |
596 | |
597 | // CustomizeDiff cannot be defined for read-only resources | |
598 | if r.CustomizeDiff != nil { | |
599 | return fmt.Errorf("cannot implement CustomizeDiff") | |
600 | } | |
bae9f6d2 JC |
601 | } |
602 | ||
603 | tsm := topSchemaMap | |
604 | ||
605 | if r.isTopLevel() && writable { | |
606 | // All non-Computed attributes must be ForceNew if Update is not defined | |
607 | if r.Update == nil { | |
608 | nonForceNewAttrs := make([]string, 0) | |
609 | for k, v := range r.Schema { | |
610 | if !v.ForceNew && !v.Computed { | |
611 | nonForceNewAttrs = append(nonForceNewAttrs, k) | |
612 | } | |
613 | } | |
614 | if len(nonForceNewAttrs) > 0 { | |
615 | return fmt.Errorf( | |
616 | "No Update defined, must set ForceNew on: %#v", nonForceNewAttrs) | |
617 | } | |
618 | } else { | |
619 | nonUpdateableAttrs := make([]string, 0) | |
620 | for k, v := range r.Schema { | |
621 | if v.ForceNew || v.Computed && !v.Optional { | |
622 | nonUpdateableAttrs = append(nonUpdateableAttrs, k) | |
623 | } | |
624 | } | |
625 | updateableAttrs := len(r.Schema) - len(nonUpdateableAttrs) | |
626 | if updateableAttrs == 0 { | |
627 | return fmt.Errorf( | |
628 | "All fields are ForceNew or Computed w/out Optional, Update is superfluous") | |
629 | } | |
630 | } | |
631 | ||
632 | tsm = schemaMap(r.Schema) | |
633 | ||
634 | // Destroy, and Read are required | |
635 | if r.Read == nil { | |
636 | return fmt.Errorf("Read must be implemented") | |
637 | } | |
638 | if r.Delete == nil { | |
639 | return fmt.Errorf("Delete must be implemented") | |
640 | } | |
641 | ||
642 | // If we have an importer, we need to verify the importer. | |
643 | if r.Importer != nil { | |
644 | if err := r.Importer.InternalValidate(); err != nil { | |
645 | return err | |
646 | } | |
647 | } | |
15c0b25d AP |
648 | |
649 | for k, f := range tsm { | |
650 | if isReservedResourceFieldName(k, f) { | |
651 | return fmt.Errorf("%s is a reserved field name", k) | |
652 | } | |
653 | } | |
bae9f6d2 JC |
654 | } |
655 | ||
107c1cdb ND |
656 | lastVersion := -1 |
657 | for _, u := range r.StateUpgraders { | |
658 | if lastVersion >= 0 && u.Version-lastVersion > 1 { | |
659 | return fmt.Errorf("missing schema version between %d and %d", lastVersion, u.Version) | |
660 | } | |
661 | ||
662 | if u.Version >= r.SchemaVersion { | |
663 | return fmt.Errorf("StateUpgrader version %d is >= current version %d", u.Version, r.SchemaVersion) | |
664 | } | |
665 | ||
666 | if !u.Type.IsObjectType() { | |
667 | return fmt.Errorf("StateUpgrader %d type is not cty.Object", u.Version) | |
668 | } | |
669 | ||
670 | if u.Upgrade == nil { | |
671 | return fmt.Errorf("StateUpgrader %d missing StateUpgradeFunc", u.Version) | |
672 | } | |
673 | ||
674 | lastVersion = u.Version | |
675 | } | |
676 | ||
677 | if lastVersion >= 0 && lastVersion != r.SchemaVersion-1 { | |
678 | return fmt.Errorf("missing StateUpgrader between %d and %d", lastVersion, r.SchemaVersion) | |
679 | } | |
680 | ||
15c0b25d AP |
681 | // Data source |
682 | if r.isTopLevel() && !writable { | |
683 | tsm = schemaMap(r.Schema) | |
684 | for k, _ := range tsm { | |
685 | if isReservedDataSourceFieldName(k) { | |
686 | return fmt.Errorf("%s is a reserved field name", k) | |
687 | } | |
c680a8e1 RS |
688 | } |
689 | } | |
690 | ||
bae9f6d2 JC |
691 | return schemaMap(r.Schema).InternalValidate(tsm) |
692 | } | |
693 | ||
15c0b25d AP |
694 | func isReservedDataSourceFieldName(name string) bool { |
695 | for _, reservedName := range config.ReservedDataSourceFields { | |
696 | if name == reservedName { | |
697 | return true | |
698 | } | |
699 | } | |
700 | return false | |
701 | } | |
702 | ||
703 | func isReservedResourceFieldName(name string, s *Schema) bool { | |
704 | // Allow phasing out "id" | |
705 | // See https://github.com/terraform-providers/terraform-provider-aws/pull/1626#issuecomment-328881415 | |
706 | if name == "id" && (s.Deprecated != "" || s.Removed != "") { | |
707 | return false | |
708 | } | |
709 | ||
c680a8e1 RS |
710 | for _, reservedName := range config.ReservedResourceFields { |
711 | if name == reservedName { | |
712 | return true | |
713 | } | |
714 | } | |
715 | return false | |
716 | } | |
717 | ||
bae9f6d2 JC |
718 | // Data returns a ResourceData struct for this Resource. Each return value |
719 | // is a separate copy and can be safely modified differently. | |
720 | // | |
721 | // The data returned from this function has no actual affect on the Resource | |
722 | // itself (including the state given to this function). | |
723 | // | |
724 | // This function is useful for unit tests and ResourceImporter functions. | |
725 | func (r *Resource) Data(s *terraform.InstanceState) *ResourceData { | |
726 | result, err := schemaMap(r.Schema).Data(s, nil) | |
727 | if err != nil { | |
728 | // At the time of writing, this isn't possible (Data never returns | |
729 | // non-nil errors). We panic to find this in the future if we have to. | |
730 | // I don't see a reason for Data to ever return an error. | |
731 | panic(err) | |
732 | } | |
733 | ||
15c0b25d AP |
734 | // load the Resource timeouts |
735 | result.timeouts = r.Timeouts | |
736 | if result.timeouts == nil { | |
737 | result.timeouts = &ResourceTimeout{} | |
738 | } | |
739 | ||
bae9f6d2 JC |
740 | // Set the schema version to latest by default |
741 | result.meta = map[string]interface{}{ | |
742 | "schema_version": strconv.Itoa(r.SchemaVersion), | |
743 | } | |
744 | ||
745 | return result | |
746 | } | |
747 | ||
748 | // TestResourceData Yields a ResourceData filled with this resource's schema for use in unit testing | |
749 | // | |
750 | // TODO: May be able to be removed with the above ResourceData function. | |
751 | func (r *Resource) TestResourceData() *ResourceData { | |
752 | return &ResourceData{ | |
753 | schema: r.Schema, | |
754 | } | |
755 | } | |
756 | ||
107c1cdb ND |
757 | // SchemasForFlatmapPath tries its best to find a sequence of schemas that |
758 | // the given dot-delimited attribute path traverses through in the schema | |
759 | // of the receiving Resource. | |
760 | func (r *Resource) SchemasForFlatmapPath(path string) []*Schema { | |
761 | return SchemasForFlatmapPath(path, r.Schema) | |
762 | } | |
763 | ||
bae9f6d2 JC |
764 | // Returns true if the resource is "top level" i.e. not a sub-resource. |
765 | func (r *Resource) isTopLevel() bool { | |
766 | // TODO: This is a heuristic; replace with a definitive attribute? | |
15c0b25d | 767 | return (r.Create != nil || r.Read != nil) |
bae9f6d2 JC |
768 | } |
769 | ||
770 | // Determines if a given InstanceState needs to be migrated by checking the | |
771 | // stored version number with the current SchemaVersion | |
772 | func (r *Resource) checkSchemaVersion(is *terraform.InstanceState) (bool, int) { | |
773 | // Get the raw interface{} value for the schema version. If it doesn't | |
774 | // exist or is nil then set it to zero. | |
775 | raw := is.Meta["schema_version"] | |
776 | if raw == nil { | |
777 | raw = "0" | |
778 | } | |
779 | ||
780 | // Try to convert it to a string. If it isn't a string then we pretend | |
781 | // that it isn't set at all. It should never not be a string unless it | |
782 | // was manually tampered with. | |
783 | rawString, ok := raw.(string) | |
784 | if !ok { | |
785 | rawString = "0" | |
786 | } | |
787 | ||
788 | stateSchemaVersion, _ := strconv.Atoi(rawString) | |
107c1cdb ND |
789 | |
790 | // Don't run MigrateState if the version is handled by a StateUpgrader, | |
791 | // since StateMigrateFuncs are not required to handle unknown versions | |
792 | maxVersion := r.SchemaVersion | |
793 | if len(r.StateUpgraders) > 0 { | |
794 | maxVersion = r.StateUpgraders[0].Version | |
795 | } | |
796 | ||
797 | return stateSchemaVersion < maxVersion, stateSchemaVersion | |
bae9f6d2 JC |
798 | } |
799 | ||
800 | func (r *Resource) recordCurrentSchemaVersion( | |
801 | state *terraform.InstanceState) *terraform.InstanceState { | |
802 | if state != nil && r.SchemaVersion > 0 { | |
803 | if state.Meta == nil { | |
804 | state.Meta = make(map[string]interface{}) | |
805 | } | |
806 | state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion) | |
807 | } | |
808 | return state | |
809 | } | |
810 | ||
811 | // Noop is a convenience implementation of resource function which takes | |
812 | // no action and returns no error. | |
813 | func Noop(*ResourceData, interface{}) error { | |
814 | return nil | |
815 | } | |
816 | ||
817 | // RemoveFromState is a convenience implementation of a resource function | |
818 | // which sets the resource ID to empty string (to remove it from state) | |
819 | // and returns no error. | |
820 | func RemoveFromState(d *ResourceData, _ interface{}) error { | |
821 | d.SetId("") | |
822 | return nil | |
823 | } |