9 "github.com/hashicorp/terraform/config"
10 "github.com/hashicorp/terraform/terraform"
13 // Resource represents a thing in Terraform that has a set of configurable
14 // attributes and a lifecycle (create, read, update, delete).
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.
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.
26 // The keys of this map are the configuration keys, and the values
27 // describe the schema of the configuration value.
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
32 Schema map[string]*Schema
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
43 // When unset, SchemaVersion defaults to 0, so provider authors can start
44 // their Versioning at any integer >= 1
47 // MigrateState is responsible for updating an InstanceState with an old
48 // version to the format expected by the current version of the Schema.
50 // It is called during Refresh if the State's stored SchemaVersion is less
51 // than the current SchemaVersion of the Resource.
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
59 // The functions below are the CRUD operations for this resource.
61 // The only optional operation is Update. If Update is not implemented,
62 // then updates will not be supported for this resource.
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.
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.
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.
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.
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.
96 // The phases Terraform runs this in, and the state available via functions
97 // like Get and GetChange, are as follows:
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)
106 // This function needs to be resilient to support all scenarios.
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.
113 // For the most part, only computed fields can be customized by this
116 // This function is only allowed on regular resources (not data sources).
117 CustomizeDiff CustomizeDiffFunc
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
126 // If non-empty, this string is emitted as a warning during Validate.
127 DeprecationMessage string
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
139 // See Resource documentation.
140 type CreateFunc func(*ResourceData, interface{}) error
142 // See Resource documentation.
143 type ReadFunc func(*ResourceData, interface{}) error
145 // See Resource documentation.
146 type UpdateFunc func(*ResourceData, interface{}) error
148 // See Resource documentation.
149 type DeleteFunc func(*ResourceData, interface{}) error
151 // See Resource documentation.
152 type ExistsFunc func(*ResourceData, interface{}) (bool, error)
154 // See Resource documentation.
155 type StateMigrateFunc func(
156 int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error)
158 // See Resource documentation.
159 type CustomizeDiffFunc func(*ResourceDiff, interface{}) error
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)
171 // Instance Diff shoould have the timeout info, need to copy it over to the
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)
179 if _, ok := s.Meta[TimeoutKey]; ok {
180 if err := rt.StateDecode(s); err != nil {
181 log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
185 log.Printf("[DEBUG] No meta timeoutkey found in Apply()")
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)
195 if d.Destroy || d.RequiresNew() {
197 // Destroy the resource since it is created
198 if err := r.Delete(data, meta); err != nil {
199 return r.recordCurrentSchemaVersion(data.State()), err
202 // Make sure the ID is gone.
206 // If we're only destroying, and not creating, then return
207 // now since we're done!
208 if !d.RequiresNew() {
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
223 // We're creating, it is a new resource.
224 data.MarkNewResource()
225 err = r.Create(data, meta)
228 return s, fmt.Errorf("doesn't support update")
231 err = r.Update(data, meta)
234 return r.recordCurrentSchemaVersion(data.State()), err
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) {
243 t := &ResourceTimeout{}
244 err := t.ConfigDecode(r, c)
247 return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err)
250 instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta)
252 return instanceDiff, err
255 if instanceDiff != nil {
256 if err := t.DiffEncode(instanceDiff); err != nil {
257 log.Printf("[ERR] Error encoding timeout to instance diff: %s", err)
260 log.Printf("[DEBUG] Instance Diff is nil in Diff()")
263 return instanceDiff, err
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)
270 if r.DeprecationMessage != "" {
271 warns = append(warns, r.DeprecationMessage)
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,
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)
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
300 return r.recordCurrentSchemaVersion(state), err
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
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)
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)
329 exists, err := r.Exists(data, meta)
338 needsMigration, stateSchemaVersion := r.checkSchemaVersion(s)
339 if needsMigration && r.MigrateState != nil {
340 s, err := r.MigrateState(stateSchemaVersion, s, meta)
346 data, err := schemaMap(r.Schema).Data(s, nil)
352 err = r.Read(data, meta)
353 state := data.State()
354 if state != nil && state.ID == "" {
358 return r.recordCurrentSchemaVersion(state), err
361 // InternalValidate should be called to validate the structure
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
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 {
373 return errors.New("resource is nil")
377 if r.Create != nil || r.Update != nil || r.Delete != nil {
378 return fmt.Errorf("must not implement Create, Update or Delete")
381 // CustomizeDiff cannot be defined for read-only resources
382 if r.CustomizeDiff != nil {
383 return fmt.Errorf("cannot implement CustomizeDiff")
389 if r.isTopLevel() && writable {
390 // All non-Computed attributes must be ForceNew if Update is not defined
392 nonForceNewAttrs := make([]string, 0)
393 for k, v := range r.Schema {
394 if !v.ForceNew && !v.Computed {
395 nonForceNewAttrs = append(nonForceNewAttrs, k)
398 if len(nonForceNewAttrs) > 0 {
400 "No Update defined, must set ForceNew on: %#v", nonForceNewAttrs)
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)
409 updateableAttrs := len(r.Schema) - len(nonUpdateableAttrs)
410 if updateableAttrs == 0 {
412 "All fields are ForceNew or Computed w/out Optional, Update is superfluous")
416 tsm = schemaMap(r.Schema)
418 // Destroy, and Read are required
420 return fmt.Errorf("Read must be implemented")
423 return fmt.Errorf("Delete must be implemented")
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 {
433 for k, f := range tsm {
434 if isReservedResourceFieldName(k, f) {
435 return fmt.Errorf("%s is a reserved field name", k)
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)
450 return schemaMap(r.Schema).InternalValidate(tsm)
453 func isReservedDataSourceFieldName(name string) bool {
454 for _, reservedName := range config.ReservedDataSourceFields {
455 if name == reservedName {
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 != "") {
469 for _, reservedName := range config.ReservedResourceFields {
470 if name == reservedName {
477 // Data returns a ResourceData struct for this Resource. Each return value
478 // is a separate copy and can be safely modified differently.
480 // The data returned from this function has no actual affect on the Resource
481 // itself (including the state given to this function).
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)
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.
493 // load the Resource timeouts
494 result.timeouts = r.Timeouts
495 if result.timeouts == nil {
496 result.timeouts = &ResourceTimeout{}
499 // Set the schema version to latest by default
500 result.meta = map[string]interface{}{
501 "schema_version": strconv.Itoa(r.SchemaVersion),
507 // TestResourceData Yields a ResourceData filled with this resource's schema for use in unit testing
509 // TODO: May be able to be removed with the above ResourceData function.
510 func (r *Resource) TestResourceData() *ResourceData {
511 return &ResourceData{
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)
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"]
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)
540 stateSchemaVersion, _ := strconv.Atoi(rawString)
541 return stateSchemaVersion < r.SchemaVersion, stateSchemaVersion
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{})
550 state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion)
555 // Noop is a convenience implementation of resource function which takes
556 // no action and returns no error.
557 func Noop(*ResourceData, interface{}) error {
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 {