9 "github.com/hashicorp/terraform/config"
10 "github.com/hashicorp/terraform/terraform"
11 "github.com/zclconf/go-cty/cty"
14 // Resource represents a thing in Terraform that has a set of configurable
15 // attributes and a lifecycle (create, read, update, delete).
17 // The Resource schema is an abstraction that allows provider writers to
18 // worry only about CRUD operations while off-loading validation, diff
19 // generation, etc. to this higher level library.
21 // In spite of the name, this struct is not used only for terraform resources,
22 // but also for data sources. In the case of data sources, the Create,
23 // Update and Delete functions must not be provided.
24 type Resource struct {
25 // Schema is the schema for the configuration of this resource.
27 // The keys of this map are the configuration keys, and the values
28 // describe the schema of the configuration value.
30 // The schema is used to represent both configurable data as well
31 // as data that might be computed in the process of creating this
33 Schema map[string]*Schema
35 // SchemaVersion is the version number for this resource's Schema
36 // definition. The current SchemaVersion stored in the state for each
37 // resource. Provider authors can increment this version number
38 // when Schema semantics change. If the State's SchemaVersion is less than
39 // the current SchemaVersion, the InstanceState is yielded to the
40 // MigrateState callback, where the provider can make whatever changes it
41 // needs to update the state to be compatible to the latest version of the
44 // When unset, SchemaVersion defaults to 0, so provider authors can start
45 // their Versioning at any integer >= 1
48 // MigrateState is deprecated and any new changes to a resource's schema
49 // should be handled by StateUpgraders. Existing MigrateState implementations
50 // should remain for compatibility with existing state. MigrateState will
51 // still be called if the stored SchemaVersion is less than the
52 // first version of the StateUpgraders.
54 // MigrateState is responsible for updating an InstanceState with an old
55 // version to the format expected by the current version of the Schema.
57 // It is called during Refresh if the State's stored SchemaVersion is less
58 // than the current SchemaVersion of the Resource.
60 // The function is yielded the state's stored SchemaVersion and a pointer to
61 // the InstanceState that needs updating, as well as the configured
62 // provider's configured meta interface{}, in case the migration process
63 // needs to make any remote API calls.
64 MigrateState StateMigrateFunc
66 // StateUpgraders contains the functions responsible for upgrading an
67 // existing state with an old schema version to a newer schema. It is
68 // called specifically by Terraform when the stored schema version is less
69 // than the current SchemaVersion of the Resource.
71 // StateUpgraders map specific schema versions to a StateUpgrader
72 // function. The registered versions are expected to be ordered,
73 // consecutive values. The initial value may be greater than 0 to account
74 // for legacy schemas that weren't recorded and can be handled by
76 StateUpgraders []StateUpgrader
78 // The functions below are the CRUD operations for this resource.
80 // The only optional operation is Update. If Update is not implemented,
81 // then updates will not be supported for this resource.
83 // The ResourceData parameter in the functions below are used to
84 // query configuration and changes for the resource as well as to set
85 // the ID, computed data, etc.
87 // The interface{} parameter is the result of the ConfigureFunc in
88 // the provider for this resource. If the provider does not define
89 // a ConfigureFunc, this will be nil. This parameter should be used
90 // to store API clients, configuration structures, etc.
92 // If any errors occur during each of the operation, an error should be
93 // returned. If a resource was partially updated, be careful to enable
94 // partial state mode for ResourceData and use it accordingly.
96 // Exists is a function that is called to check if a resource still
97 // exists. If this returns false, then this will affect the diff
98 // accordingly. If this function isn't set, it will not be called. You
99 // can also signal existence in the Read method by calling d.SetId("")
100 // if the Resource is no longer present and should be removed from state.
101 // The *ResourceData passed to Exists should _not_ be modified.
108 // CustomizeDiff is a custom function for working with the diff that
109 // Terraform has created for this resource - it can be used to customize the
110 // diff that has been created, diff values not controlled by configuration,
111 // or even veto the diff altogether and abort the plan. It is passed a
112 // *ResourceDiff, a structure similar to ResourceData but lacking most write
113 // functions like Set, while introducing new functions that work with the
114 // diff such as SetNew, SetNewComputed, and ForceNew.
116 // The phases Terraform runs this in, and the state available via functions
117 // like Get and GetChange, are as follows:
119 // * New resource: One run with no state
120 // * Existing resource: One run with state
121 // * Existing resource, forced new: One run with state (before ForceNew),
122 // then one run without state (as if new resource)
123 // * Tainted resource: No runs (custom diff logic is skipped)
124 // * Destroy: No runs (standard diff logic is skipped on destroy diffs)
126 // This function needs to be resilient to support all scenarios.
128 // If this function needs to access external API resources, remember to flag
129 // the RequiresRefresh attribute mentioned below to ensure that
130 // -refresh=false is blocked when running plan or apply, as this means that
131 // this resource requires refresh-like behaviour to work effectively.
133 // For the most part, only computed fields can be customized by this
136 // This function is only allowed on regular resources (not data sources).
137 CustomizeDiff CustomizeDiffFunc
139 // Importer is the ResourceImporter implementation for this resource.
140 // If this is nil, then this resource does not support importing. If
141 // this is non-nil, then it supports importing and ResourceImporter
142 // must be validated. The validity of ResourceImporter is verified
143 // by InternalValidate on Resource.
144 Importer *ResourceImporter
146 // If non-empty, this string is emitted as a warning during Validate.
147 DeprecationMessage string
149 // Timeouts allow users to specify specific time durations in which an
150 // operation should time out, to allow them to extend an action to suit their
151 // usage. For example, a user may specify a large Creation timeout for their
152 // AWS RDS Instance due to it's size, or restoring from a snapshot.
153 // Resource implementors must enable Timeout support by adding the allowed
154 // actions (Create, Read, Update, Delete, Default) to the Resource struct, and
155 // accessing them in the matching methods.
156 Timeouts *ResourceTimeout
159 // ShimInstanceStateFromValue converts a cty.Value to a
160 // terraform.InstanceState.
161 func (r *Resource) ShimInstanceStateFromValue(state cty.Value) (*terraform.InstanceState, error) {
162 // Get the raw shimmed value. While this is correct, the set hashes don't
163 // match those from the Schema.
164 s := terraform.NewInstanceStateShimmedFromValue(state, r.SchemaVersion)
166 // We now rebuild the state through the ResourceData, so that the set indexes
167 // match what helper/schema expects.
168 data, err := schemaMap(r.Schema).Data(s, nil)
175 s = &terraform.InstanceState{}
180 // See Resource documentation.
181 type CreateFunc func(*ResourceData, interface{}) error
183 // See Resource documentation.
184 type ReadFunc func(*ResourceData, interface{}) error
186 // See Resource documentation.
187 type UpdateFunc func(*ResourceData, interface{}) error
189 // See Resource documentation.
190 type DeleteFunc func(*ResourceData, interface{}) error
192 // See Resource documentation.
193 type ExistsFunc func(*ResourceData, interface{}) (bool, error)
195 // See Resource documentation.
196 type StateMigrateFunc func(
197 int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error)
199 type StateUpgrader struct {
200 // Version is the version schema that this Upgrader will handle, converting
204 // Type describes the schema that this function can upgrade. Type is
205 // required to decode the schema if the state was stored in a legacy
209 // Upgrade takes the JSON encoded state and the provider meta value, and
210 // upgrades the state one single schema version. The provided state is
211 // deocded into the default json types using a map[string]interface{}. It
212 // is up to the StateUpgradeFunc to ensure that the returned value can be
213 // encoded using the new schema.
214 Upgrade StateUpgradeFunc
218 type StateUpgradeFunc func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error)
220 // See Resource documentation.
221 type CustomizeDiffFunc func(*ResourceDiff, interface{}) error
223 // Apply creates, updates, and/or deletes a resource.
224 func (r *Resource) Apply(
225 s *terraform.InstanceState,
226 d *terraform.InstanceDiff,
227 meta interface{}) (*terraform.InstanceState, error) {
228 data, err := schemaMap(r.Schema).Data(s, d)
233 // Instance Diff shoould have the timeout info, need to copy it over to the
235 rt := ResourceTimeout{}
236 if _, ok := d.Meta[TimeoutKey]; ok {
237 if err := rt.DiffDecode(d); err != nil {
238 log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
241 if _, ok := s.Meta[TimeoutKey]; ok {
242 if err := rt.StateDecode(s); err != nil {
243 log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
247 log.Printf("[DEBUG] No meta timeoutkey found in Apply()")
252 // The Terraform API dictates that this should never happen, but
253 // it doesn't hurt to be safe in this case.
254 s = new(terraform.InstanceState)
257 if d.Destroy || d.RequiresNew() {
259 // Destroy the resource since it is created
260 if err := r.Delete(data, meta); err != nil {
261 return r.recordCurrentSchemaVersion(data.State()), err
264 // Make sure the ID is gone.
268 // If we're only destroying, and not creating, then return
269 // now since we're done!
270 if !d.RequiresNew() {
274 // Reset the data to be stateless since we just destroyed
275 data, err = schemaMap(r.Schema).Data(nil, d)
276 // data was reset, need to re-apply the parsed timeouts
285 // We're creating, it is a new resource.
286 data.MarkNewResource()
287 err = r.Create(data, meta)
290 return s, fmt.Errorf("doesn't support update")
293 err = r.Update(data, meta)
296 return r.recordCurrentSchemaVersion(data.State()), err
299 // Diff returns a diff of this resource.
300 func (r *Resource) Diff(
301 s *terraform.InstanceState,
302 c *terraform.ResourceConfig,
303 meta interface{}) (*terraform.InstanceDiff, error) {
305 t := &ResourceTimeout{}
306 err := t.ConfigDecode(r, c)
309 return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err)
312 instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta, true)
314 return instanceDiff, err
317 if instanceDiff != nil {
318 if err := t.DiffEncode(instanceDiff); err != nil {
319 log.Printf("[ERR] Error encoding timeout to instance diff: %s", err)
322 log.Printf("[DEBUG] Instance Diff is nil in Diff()")
325 return instanceDiff, err
328 func (r *Resource) simpleDiff(
329 s *terraform.InstanceState,
330 c *terraform.ResourceConfig,
331 meta interface{}) (*terraform.InstanceDiff, error) {
333 instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta, false)
335 return instanceDiff, err
338 if instanceDiff == nil {
339 instanceDiff = terraform.NewInstanceDiff()
342 // Make sure the old value is set in each of the instance diffs.
343 // This was done by the RequiresNew logic in the full legacy Diff.
344 for k, attr := range instanceDiff.Attributes {
349 attr.Old = s.Attributes[k]
353 return instanceDiff, nil
356 // Validate validates the resource configuration against the schema.
357 func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) {
358 warns, errs := schemaMap(r.Schema).Validate(c)
360 if r.DeprecationMessage != "" {
361 warns = append(warns, r.DeprecationMessage)
367 // ReadDataApply loads the data for a data source, given a diff that
368 // describes the configuration arguments and desired computed attributes.
369 func (r *Resource) ReadDataApply(
370 d *terraform.InstanceDiff,
372 ) (*terraform.InstanceState, error) {
373 // Data sources are always built completely from scratch
374 // on each read, so the source state is always nil.
375 data, err := schemaMap(r.Schema).Data(nil, d)
380 err = r.Read(data, meta)
381 state := data.State()
382 if state != nil && state.ID == "" {
383 // Data sources can set an ID if they want, but they aren't
384 // required to; we'll provide a placeholder if they don't,
385 // to preserve the invariant that all resources have non-empty
390 return r.recordCurrentSchemaVersion(state), err
393 // RefreshWithoutUpgrade reads the instance state, but does not call
394 // MigrateState or the StateUpgraders, since those are now invoked in a
395 // separate API call.
396 // RefreshWithoutUpgrade is part of the new plugin shims.
397 func (r *Resource) RefreshWithoutUpgrade(
398 s *terraform.InstanceState,
399 meta interface{}) (*terraform.InstanceState, error) {
400 // If the ID is already somehow blank, it doesn't exist
405 rt := ResourceTimeout{}
406 if _, ok := s.Meta[TimeoutKey]; ok {
407 if err := rt.StateDecode(s); err != nil {
408 log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
413 // Make a copy of data so that if it is modified it doesn't
414 // affect our Read later.
415 data, err := schemaMap(r.Schema).Data(s, nil)
422 exists, err := r.Exists(data, meta)
431 data, err := schemaMap(r.Schema).Data(s, nil)
437 err = r.Read(data, meta)
438 state := data.State()
439 if state != nil && state.ID == "" {
443 return r.recordCurrentSchemaVersion(state), err
446 // Refresh refreshes the state of the resource.
447 func (r *Resource) Refresh(
448 s *terraform.InstanceState,
449 meta interface{}) (*terraform.InstanceState, error) {
450 // If the ID is already somehow blank, it doesn't exist
455 rt := ResourceTimeout{}
456 if _, ok := s.Meta[TimeoutKey]; ok {
457 if err := rt.StateDecode(s); err != nil {
458 log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
463 // Make a copy of data so that if it is modified it doesn't
464 // affect our Read later.
465 data, err := schemaMap(r.Schema).Data(s, nil)
472 exists, err := r.Exists(data, meta)
481 // there may be new StateUpgraders that need to be run
482 s, err := r.upgradeState(s, meta)
487 data, err := schemaMap(r.Schema).Data(s, nil)
493 err = r.Read(data, meta)
494 state := data.State()
495 if state != nil && state.ID == "" {
499 return r.recordCurrentSchemaVersion(state), err
502 func (r *Resource) upgradeState(s *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) {
505 needsMigration, stateSchemaVersion := r.checkSchemaVersion(s)
506 migrate := needsMigration && r.MigrateState != nil
509 s, err = r.MigrateState(stateSchemaVersion, s, meta)
515 if len(r.StateUpgraders) == 0 {
519 // If we ran MigrateState, then the stateSchemaVersion value is no longer
520 // correct. We can expect the first upgrade function to be the correct
521 // schema type version.
523 stateSchemaVersion = r.StateUpgraders[0].Version
526 schemaType := r.CoreConfigSchema().ImpliedType()
527 // find the expected type to convert the state
528 for _, upgrader := range r.StateUpgraders {
529 if stateSchemaVersion == upgrader.Version {
530 schemaType = upgrader.Type
534 // StateUpgraders only operate on the new JSON format state, so the state
535 // need to be converted.
536 stateVal, err := StateValueFromInstanceState(s, schemaType)
541 jsonState, err := StateValueToJSONMap(stateVal, schemaType)
546 for _, upgrader := range r.StateUpgraders {
547 if stateSchemaVersion != upgrader.Version {
551 jsonState, err = upgrader.Upgrade(jsonState, meta)
558 // now we need to re-flatmap the new state
559 stateVal, err = JSONMapToStateValue(jsonState, r.CoreConfigSchema())
564 return r.ShimInstanceStateFromValue(stateVal)
567 // InternalValidate should be called to validate the structure
570 // This should be called in a unit test for any resource to verify
571 // before release that a resource is properly configured for use with
574 // Provider.InternalValidate() will automatically call this for all of
575 // the resources it manages, so you don't need to call this manually if it
576 // is part of a Provider.
577 func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error {
579 return errors.New("resource is nil")
583 if r.Create != nil || r.Update != nil || r.Delete != nil {
584 return fmt.Errorf("must not implement Create, Update or Delete")
587 // CustomizeDiff cannot be defined for read-only resources
588 if r.CustomizeDiff != nil {
589 return fmt.Errorf("cannot implement CustomizeDiff")
595 if r.isTopLevel() && writable {
596 // All non-Computed attributes must be ForceNew if Update is not defined
598 nonForceNewAttrs := make([]string, 0)
599 for k, v := range r.Schema {
600 if !v.ForceNew && !v.Computed {
601 nonForceNewAttrs = append(nonForceNewAttrs, k)
604 if len(nonForceNewAttrs) > 0 {
606 "No Update defined, must set ForceNew on: %#v", nonForceNewAttrs)
609 nonUpdateableAttrs := make([]string, 0)
610 for k, v := range r.Schema {
611 if v.ForceNew || v.Computed && !v.Optional {
612 nonUpdateableAttrs = append(nonUpdateableAttrs, k)
615 updateableAttrs := len(r.Schema) - len(nonUpdateableAttrs)
616 if updateableAttrs == 0 {
618 "All fields are ForceNew or Computed w/out Optional, Update is superfluous")
622 tsm = schemaMap(r.Schema)
624 // Destroy, and Read are required
626 return fmt.Errorf("Read must be implemented")
629 return fmt.Errorf("Delete must be implemented")
632 // If we have an importer, we need to verify the importer.
633 if r.Importer != nil {
634 if err := r.Importer.InternalValidate(); err != nil {
639 for k, f := range tsm {
640 if isReservedResourceFieldName(k, f) {
641 return fmt.Errorf("%s is a reserved field name", k)
647 for _, u := range r.StateUpgraders {
648 if lastVersion >= 0 && u.Version-lastVersion > 1 {
649 return fmt.Errorf("missing schema version between %d and %d", lastVersion, u.Version)
652 if u.Version >= r.SchemaVersion {
653 return fmt.Errorf("StateUpgrader version %d is >= current version %d", u.Version, r.SchemaVersion)
656 if !u.Type.IsObjectType() {
657 return fmt.Errorf("StateUpgrader %d type is not cty.Object", u.Version)
660 if u.Upgrade == nil {
661 return fmt.Errorf("StateUpgrader %d missing StateUpgradeFunc", u.Version)
664 lastVersion = u.Version
667 if lastVersion >= 0 && lastVersion != r.SchemaVersion-1 {
668 return fmt.Errorf("missing StateUpgrader between %d and %d", lastVersion, r.SchemaVersion)
672 if r.isTopLevel() && !writable {
673 tsm = schemaMap(r.Schema)
674 for k, _ := range tsm {
675 if isReservedDataSourceFieldName(k) {
676 return fmt.Errorf("%s is a reserved field name", k)
681 return schemaMap(r.Schema).InternalValidate(tsm)
684 func isReservedDataSourceFieldName(name string) bool {
685 for _, reservedName := range config.ReservedDataSourceFields {
686 if name == reservedName {
693 func isReservedResourceFieldName(name string, s *Schema) bool {
694 // Allow phasing out "id"
695 // See https://github.com/terraform-providers/terraform-provider-aws/pull/1626#issuecomment-328881415
696 if name == "id" && (s.Deprecated != "" || s.Removed != "") {
700 for _, reservedName := range config.ReservedResourceFields {
701 if name == reservedName {
708 // Data returns a ResourceData struct for this Resource. Each return value
709 // is a separate copy and can be safely modified differently.
711 // The data returned from this function has no actual affect on the Resource
712 // itself (including the state given to this function).
714 // This function is useful for unit tests and ResourceImporter functions.
715 func (r *Resource) Data(s *terraform.InstanceState) *ResourceData {
716 result, err := schemaMap(r.Schema).Data(s, nil)
718 // At the time of writing, this isn't possible (Data never returns
719 // non-nil errors). We panic to find this in the future if we have to.
720 // I don't see a reason for Data to ever return an error.
724 // load the Resource timeouts
725 result.timeouts = r.Timeouts
726 if result.timeouts == nil {
727 result.timeouts = &ResourceTimeout{}
730 // Set the schema version to latest by default
731 result.meta = map[string]interface{}{
732 "schema_version": strconv.Itoa(r.SchemaVersion),
738 // TestResourceData Yields a ResourceData filled with this resource's schema for use in unit testing
740 // TODO: May be able to be removed with the above ResourceData function.
741 func (r *Resource) TestResourceData() *ResourceData {
742 return &ResourceData{
747 // SchemasForFlatmapPath tries its best to find a sequence of schemas that
748 // the given dot-delimited attribute path traverses through in the schema
749 // of the receiving Resource.
750 func (r *Resource) SchemasForFlatmapPath(path string) []*Schema {
751 return SchemasForFlatmapPath(path, r.Schema)
754 // Returns true if the resource is "top level" i.e. not a sub-resource.
755 func (r *Resource) isTopLevel() bool {
756 // TODO: This is a heuristic; replace with a definitive attribute?
757 return (r.Create != nil || r.Read != nil)
760 // Determines if a given InstanceState needs to be migrated by checking the
761 // stored version number with the current SchemaVersion
762 func (r *Resource) checkSchemaVersion(is *terraform.InstanceState) (bool, int) {
763 // Get the raw interface{} value for the schema version. If it doesn't
764 // exist or is nil then set it to zero.
765 raw := is.Meta["schema_version"]
770 // Try to convert it to a string. If it isn't a string then we pretend
771 // that it isn't set at all. It should never not be a string unless it
772 // was manually tampered with.
773 rawString, ok := raw.(string)
778 stateSchemaVersion, _ := strconv.Atoi(rawString)
780 // Don't run MigrateState if the version is handled by a StateUpgrader,
781 // since StateMigrateFuncs are not required to handle unknown versions
782 maxVersion := r.SchemaVersion
783 if len(r.StateUpgraders) > 0 {
784 maxVersion = r.StateUpgraders[0].Version
787 return stateSchemaVersion < maxVersion, stateSchemaVersion
790 func (r *Resource) recordCurrentSchemaVersion(
791 state *terraform.InstanceState) *terraform.InstanceState {
792 if state != nil && r.SchemaVersion > 0 {
793 if state.Meta == nil {
794 state.Meta = make(map[string]interface{})
796 state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion)
801 // Noop is a convenience implementation of resource function which takes
802 // no action and returns no error.
803 func Noop(*ResourceData, interface{}) error {
807 // RemoveFromState is a convenience implementation of a resource function
808 // which sets the resource ID to empty string (to remove it from state)
809 // and returns no error.
810 func RemoveFromState(d *ResourceData, _ interface{}) error {