]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blob - vendor/github.com/hashicorp/terraform/helper/schema/resource.go
vendor: github.com/hashicorp/terraform/...@v0.10.0
[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 // Importer is the ResourceImporter implementation for this resource.
89 // If this is nil, then this resource does not support importing. If
90 // this is non-nil, then it supports importing and ResourceImporter
91 // must be validated. The validity of ResourceImporter is verified
92 // by InternalValidate on Resource.
93 Importer *ResourceImporter
94
95 // If non-empty, this string is emitted as a warning during Validate.
96 // This is a private interface for now, for use by DataSourceResourceShim,
97 // and not for general use. (But maybe later...)
98 deprecationMessage string
99
100 // Timeouts allow users to specify specific time durations in which an
101 // operation should time out, to allow them to extend an action to suit their
102 // usage. For example, a user may specify a large Creation timeout for their
103 // AWS RDS Instance due to it's size, or restoring from a snapshot.
104 // Resource implementors must enable Timeout support by adding the allowed
105 // actions (Create, Read, Update, Delete, Default) to the Resource struct, and
106 // accessing them in the matching methods.
107 Timeouts *ResourceTimeout
108 }
109
110 // See Resource documentation.
111 type CreateFunc func(*ResourceData, interface{}) error
112
113 // See Resource documentation.
114 type ReadFunc func(*ResourceData, interface{}) error
115
116 // See Resource documentation.
117 type UpdateFunc func(*ResourceData, interface{}) error
118
119 // See Resource documentation.
120 type DeleteFunc func(*ResourceData, interface{}) error
121
122 // See Resource documentation.
123 type ExistsFunc func(*ResourceData, interface{}) (bool, error)
124
125 // See Resource documentation.
126 type StateMigrateFunc func(
127 int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error)
128
129 // Apply creates, updates, and/or deletes a resource.
130 func (r *Resource) Apply(
131 s *terraform.InstanceState,
132 d *terraform.InstanceDiff,
133 meta interface{}) (*terraform.InstanceState, error) {
134 data, err := schemaMap(r.Schema).Data(s, d)
135 if err != nil {
136 return s, err
137 }
138
139 // Instance Diff shoould have the timeout info, need to copy it over to the
140 // ResourceData meta
141 rt := ResourceTimeout{}
142 if _, ok := d.Meta[TimeoutKey]; ok {
143 if err := rt.DiffDecode(d); err != nil {
144 log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
145 }
146 } else if s != nil {
147 if _, ok := s.Meta[TimeoutKey]; ok {
148 if err := rt.StateDecode(s); err != nil {
149 log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
150 }
151 }
152 } else {
153 log.Printf("[DEBUG] No meta timeoutkey found in Apply()")
154 }
155 data.timeouts = &rt
156
157 if s == nil {
158 // The Terraform API dictates that this should never happen, but
159 // it doesn't hurt to be safe in this case.
160 s = new(terraform.InstanceState)
161 }
162
163 if d.Destroy || d.RequiresNew() {
164 if s.ID != "" {
165 // Destroy the resource since it is created
166 if err := r.Delete(data, meta); err != nil {
167 return r.recordCurrentSchemaVersion(data.State()), err
168 }
169
170 // Make sure the ID is gone.
171 data.SetId("")
172 }
173
174 // If we're only destroying, and not creating, then return
175 // now since we're done!
176 if !d.RequiresNew() {
177 return nil, nil
178 }
179
180 // Reset the data to be stateless since we just destroyed
181 data, err = schemaMap(r.Schema).Data(nil, d)
182 // data was reset, need to re-apply the parsed timeouts
183 data.timeouts = &rt
184 if err != nil {
185 return nil, err
186 }
187 }
188
189 err = nil
190 if data.Id() == "" {
191 // We're creating, it is a new resource.
192 data.MarkNewResource()
193 err = r.Create(data, meta)
194 } else {
195 if r.Update == nil {
196 return s, fmt.Errorf("doesn't support update")
197 }
198
199 err = r.Update(data, meta)
200 }
201
202 return r.recordCurrentSchemaVersion(data.State()), err
203 }
204
205 // Diff returns a diff of this resource and is API compatible with the
206 // ResourceProvider interface.
207 func (r *Resource) Diff(
208 s *terraform.InstanceState,
209 c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) {
210
211 t := &ResourceTimeout{}
212 err := t.ConfigDecode(r, c)
213
214 if err != nil {
215 return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err)
216 }
217
218 instanceDiff, err := schemaMap(r.Schema).Diff(s, c)
219 if err != nil {
220 return instanceDiff, err
221 }
222
223 if instanceDiff != nil {
224 if err := t.DiffEncode(instanceDiff); err != nil {
225 log.Printf("[ERR] Error encoding timeout to instance diff: %s", err)
226 }
227 } else {
228 log.Printf("[DEBUG] Instance Diff is nil in Diff()")
229 }
230
231 return instanceDiff, err
232 }
233
234 // Validate validates the resource configuration against the schema.
235 func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) {
236 warns, errs := schemaMap(r.Schema).Validate(c)
237
238 if r.deprecationMessage != "" {
239 warns = append(warns, r.deprecationMessage)
240 }
241
242 return warns, errs
243 }
244
245 // ReadDataApply loads the data for a data source, given a diff that
246 // describes the configuration arguments and desired computed attributes.
247 func (r *Resource) ReadDataApply(
248 d *terraform.InstanceDiff,
249 meta interface{},
250 ) (*terraform.InstanceState, error) {
251
252 // Data sources are always built completely from scratch
253 // on each read, so the source state is always nil.
254 data, err := schemaMap(r.Schema).Data(nil, d)
255 if err != nil {
256 return nil, err
257 }
258
259 err = r.Read(data, meta)
260 state := data.State()
261 if state != nil && state.ID == "" {
262 // Data sources can set an ID if they want, but they aren't
263 // required to; we'll provide a placeholder if they don't,
264 // to preserve the invariant that all resources have non-empty
265 // ids.
266 state.ID = "-"
267 }
268
269 return r.recordCurrentSchemaVersion(state), err
270 }
271
272 // Refresh refreshes the state of the resource.
273 func (r *Resource) Refresh(
274 s *terraform.InstanceState,
275 meta interface{}) (*terraform.InstanceState, error) {
276 // If the ID is already somehow blank, it doesn't exist
277 if s.ID == "" {
278 return nil, nil
279 }
280
281 rt := ResourceTimeout{}
282 if _, ok := s.Meta[TimeoutKey]; ok {
283 if err := rt.StateDecode(s); err != nil {
284 log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
285 }
286 }
287
288 if r.Exists != nil {
289 // Make a copy of data so that if it is modified it doesn't
290 // affect our Read later.
291 data, err := schemaMap(r.Schema).Data(s, nil)
292 data.timeouts = &rt
293
294 if err != nil {
295 return s, err
296 }
297
298 exists, err := r.Exists(data, meta)
299 if err != nil {
300 return s, err
301 }
302 if !exists {
303 return nil, nil
304 }
305 }
306
307 needsMigration, stateSchemaVersion := r.checkSchemaVersion(s)
308 if needsMigration && r.MigrateState != nil {
309 s, err := r.MigrateState(stateSchemaVersion, s, meta)
310 if err != nil {
311 return s, err
312 }
313 }
314
315 data, err := schemaMap(r.Schema).Data(s, nil)
316 data.timeouts = &rt
317 if err != nil {
318 return s, err
319 }
320
321 err = r.Read(data, meta)
322 state := data.State()
323 if state != nil && state.ID == "" {
324 state = nil
325 }
326
327 return r.recordCurrentSchemaVersion(state), err
328 }
329
330 // InternalValidate should be called to validate the structure
331 // of the resource.
332 //
333 // This should be called in a unit test for any resource to verify
334 // before release that a resource is properly configured for use with
335 // this library.
336 //
337 // Provider.InternalValidate() will automatically call this for all of
338 // the resources it manages, so you don't need to call this manually if it
339 // is part of a Provider.
340 func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error {
341 if r == nil {
342 return errors.New("resource is nil")
343 }
344
345 if !writable {
346 if r.Create != nil || r.Update != nil || r.Delete != nil {
347 return fmt.Errorf("must not implement Create, Update or Delete")
348 }
349 }
350
351 tsm := topSchemaMap
352
353 if r.isTopLevel() && writable {
354 // All non-Computed attributes must be ForceNew if Update is not defined
355 if r.Update == nil {
356 nonForceNewAttrs := make([]string, 0)
357 for k, v := range r.Schema {
358 if !v.ForceNew && !v.Computed {
359 nonForceNewAttrs = append(nonForceNewAttrs, k)
360 }
361 }
362 if len(nonForceNewAttrs) > 0 {
363 return fmt.Errorf(
364 "No Update defined, must set ForceNew on: %#v", nonForceNewAttrs)
365 }
366 } else {
367 nonUpdateableAttrs := make([]string, 0)
368 for k, v := range r.Schema {
369 if v.ForceNew || v.Computed && !v.Optional {
370 nonUpdateableAttrs = append(nonUpdateableAttrs, k)
371 }
372 }
373 updateableAttrs := len(r.Schema) - len(nonUpdateableAttrs)
374 if updateableAttrs == 0 {
375 return fmt.Errorf(
376 "All fields are ForceNew or Computed w/out Optional, Update is superfluous")
377 }
378 }
379
380 tsm = schemaMap(r.Schema)
381
382 // Destroy, and Read are required
383 if r.Read == nil {
384 return fmt.Errorf("Read must be implemented")
385 }
386 if r.Delete == nil {
387 return fmt.Errorf("Delete must be implemented")
388 }
389
390 // If we have an importer, we need to verify the importer.
391 if r.Importer != nil {
392 if err := r.Importer.InternalValidate(); err != nil {
393 return err
394 }
395 }
396 }
397
398 // Resource-specific checks
399 for k, _ := range tsm {
400 if isReservedResourceFieldName(k) {
401 return fmt.Errorf("%s is a reserved field name for a resource", k)
402 }
403 }
404
405 return schemaMap(r.Schema).InternalValidate(tsm)
406 }
407
408 func isReservedResourceFieldName(name string) bool {
409 for _, reservedName := range config.ReservedResourceFields {
410 if name == reservedName {
411 return true
412 }
413 }
414 return false
415 }
416
417 // Data returns a ResourceData struct for this Resource. Each return value
418 // is a separate copy and can be safely modified differently.
419 //
420 // The data returned from this function has no actual affect on the Resource
421 // itself (including the state given to this function).
422 //
423 // This function is useful for unit tests and ResourceImporter functions.
424 func (r *Resource) Data(s *terraform.InstanceState) *ResourceData {
425 result, err := schemaMap(r.Schema).Data(s, nil)
426 if err != nil {
427 // At the time of writing, this isn't possible (Data never returns
428 // non-nil errors). We panic to find this in the future if we have to.
429 // I don't see a reason for Data to ever return an error.
430 panic(err)
431 }
432
433 // Set the schema version to latest by default
434 result.meta = map[string]interface{}{
435 "schema_version": strconv.Itoa(r.SchemaVersion),
436 }
437
438 return result
439 }
440
441 // TestResourceData Yields a ResourceData filled with this resource's schema for use in unit testing
442 //
443 // TODO: May be able to be removed with the above ResourceData function.
444 func (r *Resource) TestResourceData() *ResourceData {
445 return &ResourceData{
446 schema: r.Schema,
447 }
448 }
449
450 // Returns true if the resource is "top level" i.e. not a sub-resource.
451 func (r *Resource) isTopLevel() bool {
452 // TODO: This is a heuristic; replace with a definitive attribute?
453 return r.Create != nil
454 }
455
456 // Determines if a given InstanceState needs to be migrated by checking the
457 // stored version number with the current SchemaVersion
458 func (r *Resource) checkSchemaVersion(is *terraform.InstanceState) (bool, int) {
459 // Get the raw interface{} value for the schema version. If it doesn't
460 // exist or is nil then set it to zero.
461 raw := is.Meta["schema_version"]
462 if raw == nil {
463 raw = "0"
464 }
465
466 // Try to convert it to a string. If it isn't a string then we pretend
467 // that it isn't set at all. It should never not be a string unless it
468 // was manually tampered with.
469 rawString, ok := raw.(string)
470 if !ok {
471 rawString = "0"
472 }
473
474 stateSchemaVersion, _ := strconv.Atoi(rawString)
475 return stateSchemaVersion < r.SchemaVersion, stateSchemaVersion
476 }
477
478 func (r *Resource) recordCurrentSchemaVersion(
479 state *terraform.InstanceState) *terraform.InstanceState {
480 if state != nil && r.SchemaVersion > 0 {
481 if state.Meta == nil {
482 state.Meta = make(map[string]interface{})
483 }
484 state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion)
485 }
486 return state
487 }
488
489 // Noop is a convenience implementation of resource function which takes
490 // no action and returns no error.
491 func Noop(*ResourceData, interface{}) error {
492 return nil
493 }
494
495 // RemoveFromState is a convenience implementation of a resource function
496 // which sets the resource ID to empty string (to remove it from state)
497 // and returns no error.
498 func RemoveFromState(d *ResourceData, _ interface{}) error {
499 d.SetId("")
500 return nil
501 }