]>
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 | ||
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 | } | |
c680a8e1 RS |
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 | } | |
bae9f6d2 JC |
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 | ||
c680a8e1 RS |
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 | ||
bae9f6d2 JC |
405 | return schemaMap(r.Schema).InternalValidate(tsm) |
406 | } | |
407 | ||
c680a8e1 RS |
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 | ||
bae9f6d2 JC |
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 | } |