]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blob - vendor/github.com/hashicorp/terraform/helper/schema/resource.go
deps: github.com/hashicorp/terraform@sdk-v0.11-with-go-modules
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / hashicorp / terraform / helper / schema / resource.go
1 package schema
2
3 import (
4 "errors"
5 "fmt"
6 "log"
7 "strconv"
8
9 "github.com/hashicorp/terraform/config"
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
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
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.
127 DeprecationMessage string
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
158 // See Resource documentation.
159 type CustomizeDiffFunc func(*ResourceDiff, interface{}) error
160
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 }
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 }
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
237 // Diff returns a diff of this resource.
238 func (r *Resource) Diff(
239 s *terraform.InstanceState,
240 c *terraform.ResourceConfig,
241 meta interface{}) (*terraform.InstanceDiff, error) {
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
250 instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta)
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
270 if r.DeprecationMessage != "" {
271 warns = append(warns, r.DeprecationMessage)
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) {
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 }
380
381 // CustomizeDiff cannot be defined for read-only resources
382 if r.CustomizeDiff != nil {
383 return fmt.Errorf("cannot implement CustomizeDiff")
384 }
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 }
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 }
438 }
439
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 }
447 }
448 }
449
450 return schemaMap(r.Schema).InternalValidate(tsm)
451 }
452
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
469 for _, reservedName := range config.ReservedResourceFields {
470 if name == reservedName {
471 return true
472 }
473 }
474 return false
475 }
476
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
493 // load the Resource timeouts
494 result.timeouts = r.Timeouts
495 if result.timeouts == nil {
496 result.timeouts = &ResourceTimeout{}
497 }
498
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?
519 return (r.Create != nil || r.Read != nil)
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 }