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