]>
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 JC |
10 | "github.com/hashicorp/terraform/terraform" |
11 | ) | |
12 | ||
13 | // Resource represents a thing in Terraform that has a set of configurable | |
14 | // attributes and a lifecycle (create, read, update, delete). | |
15 | // | |
16 | // The Resource schema is an abstraction that allows provider writers to | |
17 | // worry only about CRUD operations while off-loading validation, diff | |
18 | // generation, etc. to this higher level library. | |
19 | // | |
20 | // In spite of the name, this struct is not used only for terraform resources, | |
21 | // but also for data sources. In the case of data sources, the Create, | |
22 | // Update and Delete functions must not be provided. | |
23 | type Resource struct { | |
24 | // Schema is the schema for the configuration of this resource. | |
25 | // | |
26 | // The keys of this map are the configuration keys, and the values | |
27 | // describe the schema of the configuration value. | |
28 | // | |
29 | // The schema is used to represent both configurable data as well | |
30 | // as data that might be computed in the process of creating this | |
31 | // resource. | |
32 | Schema map[string]*Schema | |
33 | ||
34 | // SchemaVersion is the version number for this resource's Schema | |
35 | // definition. The current SchemaVersion stored in the state for each | |
36 | // resource. Provider authors can increment this version number | |
37 | // when Schema semantics change. If the State's SchemaVersion is less than | |
38 | // the current SchemaVersion, the InstanceState is yielded to the | |
39 | // MigrateState callback, where the provider can make whatever changes it | |
40 | // needs to update the state to be compatible to the latest version of the | |
41 | // Schema. | |
42 | // | |
43 | // When unset, SchemaVersion defaults to 0, so provider authors can start | |
44 | // their Versioning at any integer >= 1 | |
45 | SchemaVersion int | |
46 | ||
47 | // MigrateState is responsible for updating an InstanceState with an old | |
48 | // version to the format expected by the current version of the Schema. | |
49 | // | |
50 | // It is called during Refresh if the State's stored SchemaVersion is less | |
51 | // than the current SchemaVersion of the Resource. | |
52 | // | |
53 | // The function is yielded the state's stored SchemaVersion and a pointer to | |
54 | // the InstanceState that needs updating, as well as the configured | |
55 | // provider's configured meta interface{}, in case the migration process | |
56 | // needs to make any remote API calls. | |
57 | MigrateState StateMigrateFunc | |
58 | ||
59 | // The functions below are the CRUD operations for this resource. | |
60 | // | |
61 | // The only optional operation is Update. If Update is not implemented, | |
62 | // then updates will not be supported for this resource. | |
63 | // | |
64 | // The ResourceData parameter in the functions below are used to | |
65 | // query configuration and changes for the resource as well as to set | |
66 | // the ID, computed data, etc. | |
67 | // | |
68 | // The interface{} parameter is the result of the ConfigureFunc in | |
69 | // the provider for this resource. If the provider does not define | |
70 | // a ConfigureFunc, this will be nil. This parameter should be used | |
71 | // to store API clients, configuration structures, etc. | |
72 | // | |
73 | // If any errors occur during each of the operation, an error should be | |
74 | // returned. If a resource was partially updated, be careful to enable | |
75 | // partial state mode for ResourceData and use it accordingly. | |
76 | // | |
77 | // Exists is a function that is called to check if a resource still | |
78 | // exists. If this returns false, then this will affect the diff | |
79 | // accordingly. If this function isn't set, it will not be called. It | |
80 | // is highly recommended to set it. The *ResourceData passed to Exists | |
81 | // should _not_ be modified. | |
82 | Create CreateFunc | |
83 | Read ReadFunc | |
84 | Update UpdateFunc | |
85 | Delete DeleteFunc | |
86 | Exists ExistsFunc | |
87 | ||
15c0b25d AP |
88 | // CustomizeDiff is a custom function for working with the diff that |
89 | // Terraform has created for this resource - it can be used to customize the | |
90 | // diff that has been created, diff values not controlled by configuration, | |
91 | // or even veto the diff altogether and abort the plan. It is passed a | |
92 | // *ResourceDiff, a structure similar to ResourceData but lacking most write | |
93 | // functions like Set, while introducing new functions that work with the | |
94 | // diff such as SetNew, SetNewComputed, and ForceNew. | |
95 | // | |
96 | // The phases Terraform runs this in, and the state available via functions | |
97 | // like Get and GetChange, are as follows: | |
98 | // | |
99 | // * New resource: One run with no state | |
100 | // * Existing resource: One run with state | |
101 | // * Existing resource, forced new: One run with state (before ForceNew), | |
102 | // then one run without state (as if new resource) | |
103 | // * Tainted resource: No runs (custom diff logic is skipped) | |
104 | // * Destroy: No runs (standard diff logic is skipped on destroy diffs) | |
105 | // | |
106 | // This function needs to be resilient to support all scenarios. | |
107 | // | |
108 | // If this function needs to access external API resources, remember to flag | |
109 | // the RequiresRefresh attribute mentioned below to ensure that | |
110 | // -refresh=false is blocked when running plan or apply, as this means that | |
111 | // this resource requires refresh-like behaviour to work effectively. | |
112 | // | |
113 | // For the most part, only computed fields can be customized by this | |
114 | // function. | |
115 | // | |
116 | // This function is only allowed on regular resources (not data sources). | |
117 | CustomizeDiff CustomizeDiffFunc | |
118 | ||
bae9f6d2 JC |
119 | // Importer is the ResourceImporter implementation for this resource. |
120 | // If this is nil, then this resource does not support importing. If | |
121 | // this is non-nil, then it supports importing and ResourceImporter | |
122 | // must be validated. The validity of ResourceImporter is verified | |
123 | // by InternalValidate on Resource. | |
124 | Importer *ResourceImporter | |
125 | ||
126 | // If non-empty, this string is emitted as a warning during Validate. | |
15c0b25d | 127 | DeprecationMessage string |
bae9f6d2 JC |
128 | |
129 | // Timeouts allow users to specify specific time durations in which an | |
130 | // operation should time out, to allow them to extend an action to suit their | |
131 | // usage. For example, a user may specify a large Creation timeout for their | |
132 | // AWS RDS Instance due to it's size, or restoring from a snapshot. | |
133 | // Resource implementors must enable Timeout support by adding the allowed | |
134 | // actions (Create, Read, Update, Delete, Default) to the Resource struct, and | |
135 | // accessing them in the matching methods. | |
136 | Timeouts *ResourceTimeout | |
137 | } | |
138 | ||
139 | // See Resource documentation. | |
140 | type CreateFunc func(*ResourceData, interface{}) error | |
141 | ||
142 | // See Resource documentation. | |
143 | type ReadFunc func(*ResourceData, interface{}) error | |
144 | ||
145 | // See Resource documentation. | |
146 | type UpdateFunc func(*ResourceData, interface{}) error | |
147 | ||
148 | // See Resource documentation. | |
149 | type DeleteFunc func(*ResourceData, interface{}) error | |
150 | ||
151 | // See Resource documentation. | |
152 | type ExistsFunc func(*ResourceData, interface{}) (bool, error) | |
153 | ||
154 | // See Resource documentation. | |
155 | type StateMigrateFunc func( | |
156 | int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error) | |
157 | ||
15c0b25d AP |
158 | // See Resource documentation. |
159 | type CustomizeDiffFunc func(*ResourceDiff, interface{}) error | |
160 | ||
bae9f6d2 JC |
161 | // Apply creates, updates, and/or deletes a resource. |
162 | func (r *Resource) Apply( | |
163 | s *terraform.InstanceState, | |
164 | d *terraform.InstanceDiff, | |
165 | meta interface{}) (*terraform.InstanceState, error) { | |
166 | data, err := schemaMap(r.Schema).Data(s, d) | |
167 | if err != nil { | |
168 | return s, err | |
169 | } | |
170 | ||
171 | // Instance Diff shoould have the timeout info, need to copy it over to the | |
172 | // ResourceData meta | |
173 | rt := ResourceTimeout{} | |
174 | if _, ok := d.Meta[TimeoutKey]; ok { | |
175 | if err := rt.DiffDecode(d); err != nil { | |
176 | log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) | |
177 | } | |
c680a8e1 RS |
178 | } else if s != nil { |
179 | if _, ok := s.Meta[TimeoutKey]; ok { | |
180 | if err := rt.StateDecode(s); err != nil { | |
181 | log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) | |
182 | } | |
183 | } | |
bae9f6d2 JC |
184 | } else { |
185 | log.Printf("[DEBUG] No meta timeoutkey found in Apply()") | |
186 | } | |
187 | data.timeouts = &rt | |
188 | ||
189 | if s == nil { | |
190 | // The Terraform API dictates that this should never happen, but | |
191 | // it doesn't hurt to be safe in this case. | |
192 | s = new(terraform.InstanceState) | |
193 | } | |
194 | ||
195 | if d.Destroy || d.RequiresNew() { | |
196 | if s.ID != "" { | |
197 | // Destroy the resource since it is created | |
198 | if err := r.Delete(data, meta); err != nil { | |
199 | return r.recordCurrentSchemaVersion(data.State()), err | |
200 | } | |
201 | ||
202 | // Make sure the ID is gone. | |
203 | data.SetId("") | |
204 | } | |
205 | ||
206 | // If we're only destroying, and not creating, then return | |
207 | // now since we're done! | |
208 | if !d.RequiresNew() { | |
209 | return nil, nil | |
210 | } | |
211 | ||
212 | // Reset the data to be stateless since we just destroyed | |
213 | data, err = schemaMap(r.Schema).Data(nil, d) | |
214 | // data was reset, need to re-apply the parsed timeouts | |
215 | data.timeouts = &rt | |
216 | if err != nil { | |
217 | return nil, err | |
218 | } | |
219 | } | |
220 | ||
221 | err = nil | |
222 | if data.Id() == "" { | |
223 | // We're creating, it is a new resource. | |
224 | data.MarkNewResource() | |
225 | err = r.Create(data, meta) | |
226 | } else { | |
227 | if r.Update == nil { | |
228 | return s, fmt.Errorf("doesn't support update") | |
229 | } | |
230 | ||
231 | err = r.Update(data, meta) | |
232 | } | |
233 | ||
234 | return r.recordCurrentSchemaVersion(data.State()), err | |
235 | } | |
236 | ||
15c0b25d | 237 | // Diff returns a diff of this resource. |
bae9f6d2 JC |
238 | func (r *Resource) Diff( |
239 | s *terraform.InstanceState, | |
15c0b25d AP |
240 | c *terraform.ResourceConfig, |
241 | meta interface{}) (*terraform.InstanceDiff, error) { | |
bae9f6d2 JC |
242 | |
243 | t := &ResourceTimeout{} | |
244 | err := t.ConfigDecode(r, c) | |
245 | ||
246 | if err != nil { | |
247 | return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err) | |
248 | } | |
249 | ||
15c0b25d | 250 | instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta) |
bae9f6d2 JC |
251 | if err != nil { |
252 | return instanceDiff, err | |
253 | } | |
254 | ||
255 | if instanceDiff != nil { | |
256 | if err := t.DiffEncode(instanceDiff); err != nil { | |
257 | log.Printf("[ERR] Error encoding timeout to instance diff: %s", err) | |
258 | } | |
259 | } else { | |
260 | log.Printf("[DEBUG] Instance Diff is nil in Diff()") | |
261 | } | |
262 | ||
263 | return instanceDiff, err | |
264 | } | |
265 | ||
266 | // Validate validates the resource configuration against the schema. | |
267 | func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) { | |
268 | warns, errs := schemaMap(r.Schema).Validate(c) | |
269 | ||
15c0b25d AP |
270 | if r.DeprecationMessage != "" { |
271 | warns = append(warns, r.DeprecationMessage) | |
bae9f6d2 JC |
272 | } |
273 | ||
274 | return warns, errs | |
275 | } | |
276 | ||
277 | // ReadDataApply loads the data for a data source, given a diff that | |
278 | // describes the configuration arguments and desired computed attributes. | |
279 | func (r *Resource) ReadDataApply( | |
280 | d *terraform.InstanceDiff, | |
281 | meta interface{}, | |
282 | ) (*terraform.InstanceState, error) { | |
bae9f6d2 JC |
283 | // Data sources are always built completely from scratch |
284 | // on each read, so the source state is always nil. | |
285 | data, err := schemaMap(r.Schema).Data(nil, d) | |
286 | if err != nil { | |
287 | return nil, err | |
288 | } | |
289 | ||
290 | err = r.Read(data, meta) | |
291 | state := data.State() | |
292 | if state != nil && state.ID == "" { | |
293 | // Data sources can set an ID if they want, but they aren't | |
294 | // required to; we'll provide a placeholder if they don't, | |
295 | // to preserve the invariant that all resources have non-empty | |
296 | // ids. | |
297 | state.ID = "-" | |
298 | } | |
299 | ||
300 | return r.recordCurrentSchemaVersion(state), err | |
301 | } | |
302 | ||
303 | // Refresh refreshes the state of the resource. | |
304 | func (r *Resource) Refresh( | |
305 | s *terraform.InstanceState, | |
306 | meta interface{}) (*terraform.InstanceState, error) { | |
307 | // If the ID is already somehow blank, it doesn't exist | |
308 | if s.ID == "" { | |
309 | return nil, nil | |
310 | } | |
311 | ||
312 | rt := ResourceTimeout{} | |
313 | if _, ok := s.Meta[TimeoutKey]; ok { | |
314 | if err := rt.StateDecode(s); err != nil { | |
315 | log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) | |
316 | } | |
317 | } | |
318 | ||
319 | if r.Exists != nil { | |
320 | // Make a copy of data so that if it is modified it doesn't | |
321 | // affect our Read later. | |
322 | data, err := schemaMap(r.Schema).Data(s, nil) | |
323 | data.timeouts = &rt | |
324 | ||
325 | if err != nil { | |
326 | return s, err | |
327 | } | |
328 | ||
329 | exists, err := r.Exists(data, meta) | |
330 | if err != nil { | |
331 | return s, err | |
332 | } | |
333 | if !exists { | |
334 | return nil, nil | |
335 | } | |
336 | } | |
337 | ||
338 | needsMigration, stateSchemaVersion := r.checkSchemaVersion(s) | |
339 | if needsMigration && r.MigrateState != nil { | |
340 | s, err := r.MigrateState(stateSchemaVersion, s, meta) | |
341 | if err != nil { | |
342 | return s, err | |
343 | } | |
344 | } | |
345 | ||
346 | data, err := schemaMap(r.Schema).Data(s, nil) | |
347 | data.timeouts = &rt | |
348 | if err != nil { | |
349 | return s, err | |
350 | } | |
351 | ||
352 | err = r.Read(data, meta) | |
353 | state := data.State() | |
354 | if state != nil && state.ID == "" { | |
355 | state = nil | |
356 | } | |
357 | ||
358 | return r.recordCurrentSchemaVersion(state), err | |
359 | } | |
360 | ||
361 | // InternalValidate should be called to validate the structure | |
362 | // of the resource. | |
363 | // | |
364 | // This should be called in a unit test for any resource to verify | |
365 | // before release that a resource is properly configured for use with | |
366 | // this library. | |
367 | // | |
368 | // Provider.InternalValidate() will automatically call this for all of | |
369 | // the resources it manages, so you don't need to call this manually if it | |
370 | // is part of a Provider. | |
371 | func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error { | |
372 | if r == nil { | |
373 | return errors.New("resource is nil") | |
374 | } | |
375 | ||
376 | if !writable { | |
377 | if r.Create != nil || r.Update != nil || r.Delete != nil { | |
378 | return fmt.Errorf("must not implement Create, Update or Delete") | |
379 | } | |
15c0b25d AP |
380 | |
381 | // CustomizeDiff cannot be defined for read-only resources | |
382 | if r.CustomizeDiff != nil { | |
383 | return fmt.Errorf("cannot implement CustomizeDiff") | |
384 | } | |
bae9f6d2 JC |
385 | } |
386 | ||
387 | tsm := topSchemaMap | |
388 | ||
389 | if r.isTopLevel() && writable { | |
390 | // All non-Computed attributes must be ForceNew if Update is not defined | |
391 | if r.Update == nil { | |
392 | nonForceNewAttrs := make([]string, 0) | |
393 | for k, v := range r.Schema { | |
394 | if !v.ForceNew && !v.Computed { | |
395 | nonForceNewAttrs = append(nonForceNewAttrs, k) | |
396 | } | |
397 | } | |
398 | if len(nonForceNewAttrs) > 0 { | |
399 | return fmt.Errorf( | |
400 | "No Update defined, must set ForceNew on: %#v", nonForceNewAttrs) | |
401 | } | |
402 | } else { | |
403 | nonUpdateableAttrs := make([]string, 0) | |
404 | for k, v := range r.Schema { | |
405 | if v.ForceNew || v.Computed && !v.Optional { | |
406 | nonUpdateableAttrs = append(nonUpdateableAttrs, k) | |
407 | } | |
408 | } | |
409 | updateableAttrs := len(r.Schema) - len(nonUpdateableAttrs) | |
410 | if updateableAttrs == 0 { | |
411 | return fmt.Errorf( | |
412 | "All fields are ForceNew or Computed w/out Optional, Update is superfluous") | |
413 | } | |
414 | } | |
415 | ||
416 | tsm = schemaMap(r.Schema) | |
417 | ||
418 | // Destroy, and Read are required | |
419 | if r.Read == nil { | |
420 | return fmt.Errorf("Read must be implemented") | |
421 | } | |
422 | if r.Delete == nil { | |
423 | return fmt.Errorf("Delete must be implemented") | |
424 | } | |
425 | ||
426 | // If we have an importer, we need to verify the importer. | |
427 | if r.Importer != nil { | |
428 | if err := r.Importer.InternalValidate(); err != nil { | |
429 | return err | |
430 | } | |
431 | } | |
15c0b25d AP |
432 | |
433 | for k, f := range tsm { | |
434 | if isReservedResourceFieldName(k, f) { | |
435 | return fmt.Errorf("%s is a reserved field name", k) | |
436 | } | |
437 | } | |
bae9f6d2 JC |
438 | } |
439 | ||
15c0b25d AP |
440 | // Data source |
441 | if r.isTopLevel() && !writable { | |
442 | tsm = schemaMap(r.Schema) | |
443 | for k, _ := range tsm { | |
444 | if isReservedDataSourceFieldName(k) { | |
445 | return fmt.Errorf("%s is a reserved field name", k) | |
446 | } | |
c680a8e1 RS |
447 | } |
448 | } | |
449 | ||
bae9f6d2 JC |
450 | return schemaMap(r.Schema).InternalValidate(tsm) |
451 | } | |
452 | ||
15c0b25d AP |
453 | func isReservedDataSourceFieldName(name string) bool { |
454 | for _, reservedName := range config.ReservedDataSourceFields { | |
455 | if name == reservedName { | |
456 | return true | |
457 | } | |
458 | } | |
459 | return false | |
460 | } | |
461 | ||
462 | func isReservedResourceFieldName(name string, s *Schema) bool { | |
463 | // Allow phasing out "id" | |
464 | // See https://github.com/terraform-providers/terraform-provider-aws/pull/1626#issuecomment-328881415 | |
465 | if name == "id" && (s.Deprecated != "" || s.Removed != "") { | |
466 | return false | |
467 | } | |
468 | ||
c680a8e1 RS |
469 | for _, reservedName := range config.ReservedResourceFields { |
470 | if name == reservedName { | |
471 | return true | |
472 | } | |
473 | } | |
474 | return false | |
475 | } | |
476 | ||
bae9f6d2 JC |
477 | // Data returns a ResourceData struct for this Resource. Each return value |
478 | // is a separate copy and can be safely modified differently. | |
479 | // | |
480 | // The data returned from this function has no actual affect on the Resource | |
481 | // itself (including the state given to this function). | |
482 | // | |
483 | // This function is useful for unit tests and ResourceImporter functions. | |
484 | func (r *Resource) Data(s *terraform.InstanceState) *ResourceData { | |
485 | result, err := schemaMap(r.Schema).Data(s, nil) | |
486 | if err != nil { | |
487 | // At the time of writing, this isn't possible (Data never returns | |
488 | // non-nil errors). We panic to find this in the future if we have to. | |
489 | // I don't see a reason for Data to ever return an error. | |
490 | panic(err) | |
491 | } | |
492 | ||
15c0b25d AP |
493 | // load the Resource timeouts |
494 | result.timeouts = r.Timeouts | |
495 | if result.timeouts == nil { | |
496 | result.timeouts = &ResourceTimeout{} | |
497 | } | |
498 | ||
bae9f6d2 JC |
499 | // Set the schema version to latest by default |
500 | result.meta = map[string]interface{}{ | |
501 | "schema_version": strconv.Itoa(r.SchemaVersion), | |
502 | } | |
503 | ||
504 | return result | |
505 | } | |
506 | ||
507 | // TestResourceData Yields a ResourceData filled with this resource's schema for use in unit testing | |
508 | // | |
509 | // TODO: May be able to be removed with the above ResourceData function. | |
510 | func (r *Resource) TestResourceData() *ResourceData { | |
511 | return &ResourceData{ | |
512 | schema: r.Schema, | |
513 | } | |
514 | } | |
515 | ||
516 | // Returns true if the resource is "top level" i.e. not a sub-resource. | |
517 | func (r *Resource) isTopLevel() bool { | |
518 | // TODO: This is a heuristic; replace with a definitive attribute? | |
15c0b25d | 519 | return (r.Create != nil || r.Read != nil) |
bae9f6d2 JC |
520 | } |
521 | ||
522 | // Determines if a given InstanceState needs to be migrated by checking the | |
523 | // stored version number with the current SchemaVersion | |
524 | func (r *Resource) checkSchemaVersion(is *terraform.InstanceState) (bool, int) { | |
525 | // Get the raw interface{} value for the schema version. If it doesn't | |
526 | // exist or is nil then set it to zero. | |
527 | raw := is.Meta["schema_version"] | |
528 | if raw == nil { | |
529 | raw = "0" | |
530 | } | |
531 | ||
532 | // Try to convert it to a string. If it isn't a string then we pretend | |
533 | // that it isn't set at all. It should never not be a string unless it | |
534 | // was manually tampered with. | |
535 | rawString, ok := raw.(string) | |
536 | if !ok { | |
537 | rawString = "0" | |
538 | } | |
539 | ||
540 | stateSchemaVersion, _ := strconv.Atoi(rawString) | |
541 | return stateSchemaVersion < r.SchemaVersion, stateSchemaVersion | |
542 | } | |
543 | ||
544 | func (r *Resource) recordCurrentSchemaVersion( | |
545 | state *terraform.InstanceState) *terraform.InstanceState { | |
546 | if state != nil && r.SchemaVersion > 0 { | |
547 | if state.Meta == nil { | |
548 | state.Meta = make(map[string]interface{}) | |
549 | } | |
550 | state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion) | |
551 | } | |
552 | return state | |
553 | } | |
554 | ||
555 | // Noop is a convenience implementation of resource function which takes | |
556 | // no action and returns no error. | |
557 | func Noop(*ResourceData, interface{}) error { | |
558 | return nil | |
559 | } | |
560 | ||
561 | // RemoveFromState is a convenience implementation of a resource function | |
562 | // which sets the resource ID to empty string (to remove it from state) | |
563 | // and returns no error. | |
564 | func RemoveFromState(d *ResourceData, _ interface{}) error { | |
565 | d.SetId("") | |
566 | return nil | |
567 | } |