package schema import ( "context" "errors" "fmt" "strconv" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) var ReservedDataSourceFields = []string{ "connection", "count", "depends_on", "lifecycle", "provider", "provisioner", } var ReservedResourceFields = []string{ "connection", "count", "depends_on", "lifecycle", "provider", "provisioner", } // Resource is an abstraction for multiple Terraform concepts: // // - Managed Resource: An infrastructure component with a schema, lifecycle // operations such as create, read, update, and delete // (CRUD), and optional implementation details such as // import support, upgrade state support, and difference // customization. // - Data Resource: Also known as a data source. An infrastructure component // with a schema and only the read lifecycle operation. // - Block: When implemented within a Schema type Elem field, a configuration // block that contains nested schema information such as attributes // and blocks. // // To fully implement managed resources, the Provider type ResourcesMap field // should include a reference to an implementation of this type. To fully // implement data resources, the Provider type DataSourcesMap field should // include a reference to an implementation of this type. // // Each field further documents any constraints based on the Terraform concept // being implemented. type Resource struct { // Schema is the structure and type information for this component. This // field is required for all Resource concepts. // // The keys of this map are the names used in a practitioner configuration, // such as the attribute or block name. The values describe the structure // and type information of that attribute or block. Schema map[string]*Schema // SchemaVersion is the version number for this resource's Schema // definition. This field is only valid when the Resource is a managed // resource. // // The current SchemaVersion stored in the state for each resource. // Provider authors can increment this version number when Schema semantics // change in an incompatible manner. If the state's SchemaVersion is less // than the current SchemaVersion, the MigrateState and StateUpgraders // functionality is executed to upgrade the state information. // // When unset, SchemaVersion defaults to 0, so provider authors can start // their Versioning at any integer >= 1 SchemaVersion int // MigrateState is responsible for updating an InstanceState with an old // version to the format expected by the current version of the Schema. // This field is only valid when the Resource is a managed resource. // // It is called during Refresh if the State's stored SchemaVersion is less // than the current SchemaVersion of the Resource. // // The function is yielded the state's stored SchemaVersion and a pointer to // the InstanceState that needs updating, as well as the configured // provider's configured meta interface{}, in case the migration process // needs to make any remote API calls. // // Deprecated: MigrateState is deprecated and any new changes to a resource's schema // should be handled by StateUpgraders. Existing MigrateState implementations // should remain for compatibility with existing state. MigrateState will // still be called if the stored SchemaVersion is less than the // first version of the StateUpgraders. MigrateState StateMigrateFunc // StateUpgraders contains the functions responsible for upgrading an // existing state with an old schema version to a newer schema. It is // called specifically by Terraform when the stored schema version is less // than the current SchemaVersion of the Resource. This field is only valid // when the Resource is a managed resource. // // StateUpgraders map specific schema versions to a StateUpgrader // function. The registered versions are expected to be ordered, // consecutive values. The initial value may be greater than 0 to account // for legacy schemas that weren't recorded and can be handled by // MigrateState. StateUpgraders []StateUpgrader // Create is called when the provider must create a new instance of a // managed resource. This field is only valid when the Resource is a // managed resource. Only one of Create, CreateContext, or // CreateWithoutTimeout should be implemented. // // The *ResourceData parameter contains the plan and state data for this // managed resource instance. The available data in the Get* methods is the // the proposed state, which is the merged data of the practitioner // configuration and any CustomizeDiff field logic. // // The SetId method must be called with a non-empty value for the managed // resource instance to be properly saved into the Terraform state and // avoid a "inconsistent result after apply" error. // // The interface{} parameter is the result of the Provider type // ConfigureFunc field execution. If the Provider does not define // a ConfigureFunc, this will be nil. This parameter is conventionally // used to store API clients and other provider instance specific data. // // The error return parameter, if not nil, will be converted into an error // diagnostic when passed back to Terraform. // // Deprecated: Use CreateContext or CreateWithoutTimeout instead. This // implementation does not support request cancellation initiated by // Terraform, such as a system or practitioner sending SIGINT (Ctrl-c). // This implementation also does not support warning diagnostics. Create CreateFunc // Read is called when the provider must refresh the state of a managed // resource instance or data resource instance. This field is only valid // when the Resource is a managed resource or data resource. Only one of // Read, ReadContext, or ReadWithoutTimeout should be implemented. // // The *ResourceData parameter contains the state data for this managed // resource instance or data resource instance. // // Managed resources can signal to Terraform that the managed resource // instance no longer exists and potentially should be recreated by calling // the SetId method with an empty string ("") parameter and without // returning an error. // // Data resources that are designed to return state for a singular // infrastructure component should conventionally return an error if that // infrastructure does not exist and omit any calls to the // SetId method. // // The interface{} parameter is the result of the Provider type // ConfigureFunc field execution. If the Provider does not define // a ConfigureFunc, this will be nil. This parameter is conventionally // used to store API clients and other provider instance specific data. // // The error return parameter, if not nil, will be converted into an error // diagnostic when passed back to Terraform. // // Deprecated: Use ReadContext or ReadWithoutTimeout instead. This // implementation does not support request cancellation initiated by // Terraform, such as a system or practitioner sending SIGINT (Ctrl-c). // This implementation also does not support warning diagnostics. Read ReadFunc // Update is called when the provider must update an instance of a // managed resource. This field is only valid when the Resource is a // managed resource. Only one of Update, UpdateContext, or // UpdateWithoutTimeout should be implemented. // // This implementation is optional. If omitted, all Schema must enable // the ForceNew field and any practitioner changes that would have // caused and update will instead destroy and recreate the infrastructure // compontent. // // The *ResourceData parameter contains the plan and state data for this // managed resource instance. The available data in the Get* methods is the // the proposed state, which is the merged data of the prior state, // practitioner configuration, and any CustomizeDiff field logic. The // available data for the GetChange* and HasChange* methods is the prior // state and proposed state. // // The interface{} parameter is the result of the Provider type // ConfigureFunc field execution. If the Provider does not define // a ConfigureFunc, this will be nil. This parameter is conventionally // used to store API clients and other provider instance specific data. // // The error return parameter, if not nil, will be converted into an error // diagnostic when passed back to Terraform. // // Deprecated: Use UpdateContext or UpdateWithoutTimeout instead. This // implementation does not support request cancellation initiated by // Terraform, such as a system or practitioner sending SIGINT (Ctrl-c). // This implementation also does not support warning diagnostics. Update UpdateFunc // Delete is called when the provider must destroy the instance of a // managed resource. This field is only valid when the Resource is a // managed resource. Only one of Delete, DeleteContext, or // DeleteWithoutTimeout should be implemented. // // The *ResourceData parameter contains the state data for this managed // resource instance. // // The interface{} parameter is the result of the Provider type // ConfigureFunc field execution. If the Provider does not define // a ConfigureFunc, this will be nil. This parameter is conventionally // used to store API clients and other provider instance specific data. // // The error return parameter, if not nil, will be converted into an error // diagnostic when passed back to Terraform. // // Deprecated: Use DeleteContext or DeleteWithoutTimeout instead. This // implementation does not support request cancellation initiated by // Terraform, such as a system or practitioner sending SIGINT (Ctrl-c). // This implementation also does not support warning diagnostics. Delete DeleteFunc // Exists is a function that is called to check if a resource still // exists. This field is only valid when the Resource is a managed // resource. // // If this returns false, then this will affect the diff // accordingly. If this function isn't set, it will not be called. You // can also signal existence in the Read method by calling d.SetId("") // if the Resource is no longer present and should be removed from state. // The *ResourceData passed to Exists should _not_ be modified. // // Deprecated: Remove in preference of ReadContext or ReadWithoutTimeout. Exists ExistsFunc // CreateContext is called when the provider must create a new instance of // a managed resource. This field is only valid when the Resource is a // managed resource. Only one of Create, CreateContext, or // CreateWithoutTimeout should be implemented. // // The Context parameter stores SDK information, such as loggers and // timeout deadlines. It also is wired to receive any cancellation from // Terraform such as a system or practitioner sending SIGINT (Ctrl-c). // // By default, CreateContext has a 20 minute timeout. Use the Timeouts // field to control the default duration or implement CreateWithoutTimeout // instead of CreateContext to remove the default timeout. // // The *ResourceData parameter contains the plan and state data for this // managed resource instance. The available data in the Get* methods is the // the proposed state, which is the merged data of the practitioner // configuration and any CustomizeDiff field logic. // // The SetId method must be called with a non-empty value for the managed // resource instance to be properly saved into the Terraform state and // avoid a "inconsistent result after apply" error. // // The interface{} parameter is the result of the Provider type // ConfigureFunc field execution. If the Provider does not define // a ConfigureFunc, this will be nil. This parameter is conventionally // used to store API clients and other provider instance specific data. // // The diagnostics return parameter, if not nil, can contain any // combination and multiple of warning and/or error diagnostics. CreateContext CreateContextFunc // ReadContext is called when the provider must refresh the state of a managed // resource instance or data resource instance. This field is only valid // when the Resource is a managed resource or data resource. Only one of // Read, ReadContext, or ReadWithoutTimeout should be implemented. // // The Context parameter stores SDK information, such as loggers and // timeout deadlines. It also is wired to receive any cancellation from // Terraform such as a system or practitioner sending SIGINT (Ctrl-c). // // By default, ReadContext has a 20 minute timeout. Use the Timeouts // field to control the default duration or implement ReadWithoutTimeout // instead of ReadContext to remove the default timeout. // // The *ResourceData parameter contains the state data for this managed // resource instance or data resource instance. // // Managed resources can signal to Terraform that the managed resource // instance no longer exists and potentially should be recreated by calling // the SetId method with an empty string ("") parameter and without // returning an error. // // Data resources that are designed to return state for a singular // infrastructure component should conventionally return an error if that // infrastructure does not exist and omit any calls to the // SetId method. // // The interface{} parameter is the result of the Provider type // ConfigureFunc field execution. If the Provider does not define // a ConfigureFunc, this will be nil. This parameter is conventionally // used to store API clients and other provider instance specific data. // // The diagnostics return parameter, if not nil, can contain any // combination and multiple of warning and/or error diagnostics. ReadContext ReadContextFunc // UpdateContext is called when the provider must update an instance of a // managed resource. This field is only valid when the Resource is a // managed resource. Only one of Update, UpdateContext, or // UpdateWithoutTimeout should be implemented. // // This implementation is optional. If omitted, all Schema must enable // the ForceNew field and any practitioner changes that would have // caused and update will instead destroy and recreate the infrastructure // compontent. // // The Context parameter stores SDK information, such as loggers and // timeout deadlines. It also is wired to receive any cancellation from // Terraform such as a system or practitioner sending SIGINT (Ctrl-c). // // By default, UpdateContext has a 20 minute timeout. Use the Timeouts // field to control the default duration or implement UpdateWithoutTimeout // instead of UpdateContext to remove the default timeout. // // The *ResourceData parameter contains the plan and state data for this // managed resource instance. The available data in the Get* methods is the // the proposed state, which is the merged data of the prior state, // practitioner configuration, and any CustomizeDiff field logic. The // available data for the GetChange* and HasChange* methods is the prior // state and proposed state. // // The interface{} parameter is the result of the Provider type // ConfigureFunc field execution. If the Provider does not define // a ConfigureFunc, this will be nil. This parameter is conventionally // used to store API clients and other provider instance specific data. // // The diagnostics return parameter, if not nil, can contain any // combination and multiple of warning and/or error diagnostics. UpdateContext UpdateContextFunc // DeleteContext is called when the provider must destroy the instance of a // managed resource. This field is only valid when the Resource is a // managed resource. Only one of Delete, DeleteContext, or // DeleteWithoutTimeout should be implemented. // // The Context parameter stores SDK information, such as loggers and // timeout deadlines. It also is wired to receive any cancellation from // Terraform such as a system or practitioner sending SIGINT (Ctrl-c). // // By default, DeleteContext has a 20 minute timeout. Use the Timeouts // field to control the default duration or implement DeleteWithoutTimeout // instead of DeleteContext to remove the default timeout. // // The *ResourceData parameter contains the state data for this managed // resource instance. // // The interface{} parameter is the result of the Provider type // ConfigureFunc field execution. If the Provider does not define // a ConfigureFunc, this will be nil. This parameter is conventionally // used to store API clients and other provider instance specific data. // // The diagnostics return parameter, if not nil, can contain any // combination and multiple of warning and/or error diagnostics. DeleteContext DeleteContextFunc // CreateWithoutTimeout is called when the provider must create a new // instance of a managed resource. This field is only valid when the // Resource is a managed resource. Only one of Create, CreateContext, or // CreateWithoutTimeout should be implemented. // // Most resources should prefer CreateContext with properly implemented // operation timeout values, however there are cases where operation // synchronization across concurrent resources is necessary in the resource // logic, such as a mutex, to prevent remote system errors. Since these // operations would have an indeterminate timeout that scales with the // number of resources, this allows resources to control timeout behavior. // // The Context parameter stores SDK information, such as loggers. It also // is wired to receive any cancellation from Terraform such as a system or // practitioner sending SIGINT (Ctrl-c). // // The *ResourceData parameter contains the plan and state data for this // managed resource instance. The available data in the Get* methods is the // the proposed state, which is the merged data of the practitioner // configuration and any CustomizeDiff field logic. // // The SetId method must be called with a non-empty value for the managed // resource instance to be properly saved into the Terraform state and // avoid a "inconsistent result after apply" error. // // The interface{} parameter is the result of the Provider type // ConfigureFunc field execution. If the Provider does not define // a ConfigureFunc, this will be nil. This parameter is conventionally // used to store API clients and other provider instance specific data. // // The diagnostics return parameter, if not nil, can contain any // combination and multiple of warning and/or error diagnostics. CreateWithoutTimeout CreateContextFunc // ReadWithoutTimeout is called when the provider must refresh the state of // a managed resource instance or data resource instance. This field is // only valid when the Resource is a managed resource or data resource. // Only one of Read, ReadContext, or ReadWithoutTimeout should be // implemented. // // Most resources should prefer ReadContext with properly implemented // operation timeout values, however there are cases where operation // synchronization across concurrent resources is necessary in the resource // logic, such as a mutex, to prevent remote system errors. Since these // operations would have an indeterminate timeout that scales with the // number of resources, this allows resources to control timeout behavior. // // The Context parameter stores SDK information, such as loggers. It also // is wired to receive any cancellation from Terraform such as a system or // practitioner sending SIGINT (Ctrl-c). // // The *ResourceData parameter contains the state data for this managed // resource instance or data resource instance. // // Managed resources can signal to Terraform that the managed resource // instance no longer exists and potentially should be recreated by calling // the SetId method with an empty string ("") parameter and without // returning an error. // // Data resources that are designed to return state for a singular // infrastructure component should conventionally return an error if that // infrastructure does not exist and omit any calls to the // SetId method. // // The interface{} parameter is the result of the Provider type // ConfigureFunc field execution. If the Provider does not define // a ConfigureFunc, this will be nil. This parameter is conventionally // used to store API clients and other provider instance specific data. // // The diagnostics return parameter, if not nil, can contain any // combination and multiple of warning and/or error diagnostics. ReadWithoutTimeout ReadContextFunc // UpdateWithoutTimeout is called when the provider must update an instance // of a managed resource. This field is only valid when the Resource is a // managed resource. Only one of Update, UpdateContext, or // UpdateWithoutTimeout should be implemented. // // Most resources should prefer UpdateContext with properly implemented // operation timeout values, however there are cases where operation // synchronization across concurrent resources is necessary in the resource // logic, such as a mutex, to prevent remote system errors. Since these // operations would have an indeterminate timeout that scales with the // number of resources, this allows resources to control timeout behavior. // // This implementation is optional. If omitted, all Schema must enable // the ForceNew field and any practitioner changes that would have // caused and update will instead destroy and recreate the infrastructure // compontent. // // The Context parameter stores SDK information, such as loggers. It also // is wired to receive any cancellation from Terraform such as a system or // practitioner sending SIGINT (Ctrl-c). // // The *ResourceData parameter contains the plan and state data for this // managed resource instance. The available data in the Get* methods is the // the proposed state, which is the merged data of the prior state, // practitioner configuration, and any CustomizeDiff field logic. The // available data for the GetChange* and HasChange* methods is the prior // state and proposed state. // // The interface{} parameter is the result of the Provider type // ConfigureFunc field execution. If the Provider does not define // a ConfigureFunc, this will be nil. This parameter is conventionally // used to store API clients and other provider instance specific data. // // The diagnostics return parameter, if not nil, can contain any // combination and multiple of warning and/or error diagnostics. UpdateWithoutTimeout UpdateContextFunc // DeleteWithoutTimeout is called when the provider must destroy the // instance of a managed resource. This field is only valid when the // Resource is a managed resource. Only one of Delete, DeleteContext, or // DeleteWithoutTimeout should be implemented. // // Most resources should prefer DeleteContext with properly implemented // operation timeout values, however there are cases where operation // synchronization across concurrent resources is necessary in the resource // logic, such as a mutex, to prevent remote system errors. Since these // operations would have an indeterminate timeout that scales with the // number of resources, this allows resources to control timeout behavior. // // The Context parameter stores SDK information, such as loggers. It also // is wired to receive any cancellation from Terraform such as a system or // practitioner sending SIGINT (Ctrl-c). // // The *ResourceData parameter contains the state data for this managed // resource instance. // // The interface{} parameter is the result of the Provider type // ConfigureFunc field execution. If the Provider does not define // a ConfigureFunc, this will be nil. This parameter is conventionally // used to store API clients and other provider instance specific data. // // The diagnostics return parameter, if not nil, can contain any // combination and multiple of warning and/or error diagnostics. DeleteWithoutTimeout DeleteContextFunc // CustomizeDiff is called after a difference (plan) has been generated // for the Resource and allows for customizations, such as setting values // not controlled by configuration, conditionally triggering resource // recreation, or implementing additional validation logic to abort a plan. // This field is only valid when the Resource is a managed resource. // // The Context parameter stores SDK information, such as loggers. It also // is wired to receive any cancellation from Terraform such as a system or // practitioner sending SIGINT (Ctrl-c). // // The *ResourceDiff parameter is similar to ResourceData but replaces the // Set method with other difference handling methods, such as SetNew, // SetNewComputed, and ForceNew. In general, only Schema with Computed // enabled can have those methods executed against them. // // The phases Terraform runs this in, and the state available via functions // like Get and GetChange, are as follows: // // * New resource: One run with no state // * Existing resource: One run with state // * Existing resource, forced new: One run with state (before ForceNew), // then one run without state (as if new resource) // * Tainted resource: No runs (custom diff logic is skipped) // * Destroy: No runs (standard diff logic is skipped on destroy diffs) // // This function needs to be resilient to support all scenarios. // // The interface{} parameter is the result of the Provider type // ConfigureFunc field execution. If the Provider does not define // a ConfigureFunc, this will be nil. This parameter is conventionally // used to store API clients and other provider instance specific data. // // The error return parameter, if not nil, will be converted into an error // diagnostic when passed back to Terraform. CustomizeDiff CustomizeDiffFunc // Importer is called when the provider must import an instance of a // managed resource. This field is only valid when the Resource is a // managed resource. // // If this is nil, then this resource does not support importing. If // this is non-nil, then it supports importing and ResourceImporter // must be validated. The validity of ResourceImporter is verified // by InternalValidate on Resource. Importer *ResourceImporter // If non-empty, this string is emitted as the details of a warning // diagnostic during validation (validate, plan, and apply operations). // This field is only valid when the Resource is a managed resource or // data resource. DeprecationMessage string // Timeouts configures the default time duration allowed before a create, // read, update, or delete operation is considered timed out, which returns // an error to practitioners. This field is only valid when the Resource is // a managed resource or data resource. // // When implemented, practitioners can add a timeouts configuration block // within their managed resource or data resource configuration to further // customize the create, read, update, or delete operation timeouts. For // example, a configuration may specify a longer create timeout for a // database resource due to its data size. // // The ResourceData that is passed to create, read, update, and delete // functionality can access the merged time duration of the Resource // default timeouts configured in this field and the practitioner timeouts // configuration via the Timeout method. Practitioner configuration // always overrides any default values set here, whether shorter or longer. Timeouts *ResourceTimeout // Description is used as the description for docs, the language server and // other user facing usage. It can be plain-text or markdown depending on the // global DescriptionKind setting. This field is valid for any Resource. Description string // UseJSONNumber should be set when state upgraders will expect // json.Numbers instead of float64s for numbers. This is added as a // toggle for backwards compatibility for type assertions, but should // be used in all new resources to avoid bugs with sufficiently large // user input. This field is only valid when the Resource is a managed // resource. // // See github.com/hashicorp/terraform-plugin-sdk/issues/655 for more // details. UseJSONNumber bool } // ShimInstanceStateFromValue converts a cty.Value to a // terraform.InstanceState. func (r *Resource) ShimInstanceStateFromValue(state cty.Value) (*terraform.InstanceState, error) { // Get the raw shimmed value. While this is correct, the set hashes don't // match those from the Schema. s := terraform.NewInstanceStateShimmedFromValue(state, r.SchemaVersion) // We now rebuild the state through the ResourceData, so that the set indexes // match what helper/schema expects. data, err := schemaMap(r.Schema).Data(s, nil) if err != nil { return nil, err } s = data.State() if s == nil { s = &terraform.InstanceState{} } return s, nil } // The following function types are of the legacy CRUD operations. // // Deprecated: Please use the context aware equivalents instead. type CreateFunc func(*ResourceData, interface{}) error // Deprecated: Please use the context aware equivalents instead. type ReadFunc func(*ResourceData, interface{}) error // Deprecated: Please use the context aware equivalents instead. type UpdateFunc func(*ResourceData, interface{}) error // Deprecated: Please use the context aware equivalents instead. type DeleteFunc func(*ResourceData, interface{}) error // Deprecated: Please use the context aware equivalents instead. type ExistsFunc func(*ResourceData, interface{}) (bool, error) // See Resource documentation. type CreateContextFunc func(context.Context, *ResourceData, interface{}) diag.Diagnostics // See Resource documentation. type ReadContextFunc func(context.Context, *ResourceData, interface{}) diag.Diagnostics // See Resource documentation. type UpdateContextFunc func(context.Context, *ResourceData, interface{}) diag.Diagnostics // See Resource documentation. type DeleteContextFunc func(context.Context, *ResourceData, interface{}) diag.Diagnostics // See Resource documentation. type StateMigrateFunc func( int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error) // Implementation of a single schema version state upgrade. type StateUpgrader struct { // Version is the version schema that this Upgrader will handle, converting // it to Version+1. Version int // Type describes the schema that this function can upgrade. Type is // required to decode the schema if the state was stored in a legacy // flatmap format. Type cty.Type // Upgrade takes the JSON encoded state and the provider meta value, and // upgrades the state one single schema version. The provided state is // deocded into the default json types using a map[string]interface{}. It // is up to the StateUpgradeFunc to ensure that the returned value can be // encoded using the new schema. Upgrade StateUpgradeFunc } // Function signature for a schema version state upgrade handler. // // The Context parameter stores SDK information, such as loggers. It also // is wired to receive any cancellation from Terraform such as a system or // practitioner sending SIGINT (Ctrl-c). // // The map[string]interface{} parameter contains the previous schema version // state data for a managed resource instance. The keys are top level attribute // or block names mapped to values that can be type asserted similar to // fetching values using the ResourceData Get* methods: // // - TypeBool: bool // - TypeFloat: float // - TypeInt: int // - TypeList: []interface{} // - TypeMap: map[string]interface{} // - TypeSet: *Set // - TypeString: string // // In certain scenarios, the map may be nil, so checking for that condition // upfront is recommended to prevent potential panics. // // The interface{} parameter is the result of the Provider type // ConfigureFunc field execution. If the Provider does not define // a ConfigureFunc, this will be nil. This parameter is conventionally // used to store API clients and other provider instance specific data. // // The map[string]interface{} return parameter should contain the upgraded // schema version state data for a managed resource instance. Values must // align to the typing mentioned above. type StateUpgradeFunc func(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) // See Resource documentation. type CustomizeDiffFunc func(context.Context, *ResourceDiff, interface{}) error func (r *Resource) create(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { if r.Create != nil { if err := r.Create(d, meta); err != nil { return diag.FromErr(err) } return nil } if r.CreateWithoutTimeout != nil { return r.CreateWithoutTimeout(ctx, d, meta) } ctx, cancel := context.WithTimeout(ctx, d.Timeout(TimeoutCreate)) defer cancel() return r.CreateContext(ctx, d, meta) } func (r *Resource) read(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { if r.Read != nil { if err := r.Read(d, meta); err != nil { return diag.FromErr(err) } return nil } if r.ReadWithoutTimeout != nil { return r.ReadWithoutTimeout(ctx, d, meta) } ctx, cancel := context.WithTimeout(ctx, d.Timeout(TimeoutRead)) defer cancel() return r.ReadContext(ctx, d, meta) } func (r *Resource) update(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { if r.Update != nil { if err := r.Update(d, meta); err != nil { return diag.FromErr(err) } return nil } if r.UpdateWithoutTimeout != nil { return r.UpdateWithoutTimeout(ctx, d, meta) } ctx, cancel := context.WithTimeout(ctx, d.Timeout(TimeoutUpdate)) defer cancel() return r.UpdateContext(ctx, d, meta) } func (r *Resource) delete(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { if r.Delete != nil { if err := r.Delete(d, meta); err != nil { return diag.FromErr(err) } return nil } if r.DeleteWithoutTimeout != nil { return r.DeleteWithoutTimeout(ctx, d, meta) } ctx, cancel := context.WithTimeout(ctx, d.Timeout(TimeoutDelete)) defer cancel() return r.DeleteContext(ctx, d, meta) } // Apply creates, updates, and/or deletes a resource. func (r *Resource) Apply( ctx context.Context, s *terraform.InstanceState, d *terraform.InstanceDiff, meta interface{}) (*terraform.InstanceState, diag.Diagnostics) { data, err := schemaMap(r.Schema).Data(s, d) if err != nil { return s, diag.FromErr(err) } if s != nil && data != nil { data.providerMeta = s.ProviderMeta } // Instance Diff shoould have the timeout info, need to copy it over to the // ResourceData meta rt := ResourceTimeout{} if _, ok := d.Meta[TimeoutKey]; ok { if err := rt.DiffDecode(d); err != nil { logging.HelperSchemaError(ctx, "Error decoding ResourceTimeout", map[string]interface{}{logging.KeyError: err}) } } else if s != nil { if _, ok := s.Meta[TimeoutKey]; ok { if err := rt.StateDecode(s); err != nil { logging.HelperSchemaError(ctx, "Error decoding ResourceTimeout", map[string]interface{}{logging.KeyError: err}) } } } else { logging.HelperSchemaDebug(ctx, "No meta timeoutkey found in Apply()") } data.timeouts = &rt if s == nil { // The Terraform API dictates that this should never happen, but // it doesn't hurt to be safe in this case. s = new(terraform.InstanceState) } var diags diag.Diagnostics if d.Destroy || d.RequiresNew() { if s.ID != "" { // Destroy the resource since it is created logging.HelperSchemaTrace(ctx, "Calling downstream") diags = append(diags, r.delete(ctx, data, meta)...) logging.HelperSchemaTrace(ctx, "Called downstream") if diags.HasError() { return r.recordCurrentSchemaVersion(data.State()), diags } // Make sure the ID is gone. data.SetId("") } // If we're only destroying, and not creating, then return // now since we're done! if !d.RequiresNew() { return nil, diags } // Reset the data to be stateless since we just destroyed data, err = schemaMap(r.Schema).Data(nil, d) if err != nil { return nil, append(diags, diag.FromErr(err)...) } // data was reset, need to re-apply the parsed timeouts data.timeouts = &rt } if data.Id() == "" { // We're creating, it is a new resource. data.MarkNewResource() logging.HelperSchemaTrace(ctx, "Calling downstream") diags = append(diags, r.create(ctx, data, meta)...) logging.HelperSchemaTrace(ctx, "Called downstream") } else { if !r.updateFuncSet() { return s, append(diags, diag.Diagnostic{ Severity: diag.Error, Summary: "doesn't support update", }) } logging.HelperSchemaTrace(ctx, "Calling downstream") diags = append(diags, r.update(ctx, data, meta)...) logging.HelperSchemaTrace(ctx, "Called downstream") } return r.recordCurrentSchemaVersion(data.State()), diags } // Diff returns a diff of this resource. func (r *Resource) Diff( ctx context.Context, s *terraform.InstanceState, c *terraform.ResourceConfig, meta interface{}) (*terraform.InstanceDiff, error) { t := &ResourceTimeout{} err := t.ConfigDecode(r, c) if err != nil { return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err) } instanceDiff, err := schemaMap(r.Schema).Diff(ctx, s, c, r.CustomizeDiff, meta, true) if err != nil { return instanceDiff, err } if instanceDiff != nil { if err := t.DiffEncode(instanceDiff); err != nil { logging.HelperSchemaError(ctx, "Error encoding timeout to instance diff", map[string]interface{}{logging.KeyError: err}) } } else { logging.HelperSchemaDebug(ctx, "Instance Diff is nil in Diff()") } return instanceDiff, err } func (r *Resource) SimpleDiff( ctx context.Context, s *terraform.InstanceState, c *terraform.ResourceConfig, meta interface{}) (*terraform.InstanceDiff, error) { instanceDiff, err := schemaMap(r.Schema).Diff(ctx, s, c, r.CustomizeDiff, meta, false) if err != nil { return instanceDiff, err } if instanceDiff == nil { instanceDiff = terraform.NewInstanceDiff() } // Make sure the old value is set in each of the instance diffs. // This was done by the RequiresNew logic in the full legacy Diff. for k, attr := range instanceDiff.Attributes { if attr == nil { continue } if s != nil { attr.Old = s.Attributes[k] } } return instanceDiff, nil } // Validate validates the resource configuration against the schema. func (r *Resource) Validate(c *terraform.ResourceConfig) diag.Diagnostics { diags := schemaMap(r.Schema).Validate(c) if r.DeprecationMessage != "" { diags = append(diags, diag.Diagnostic{ Severity: diag.Warning, Summary: "Deprecated Resource", Detail: r.DeprecationMessage, }) } return diags } // ReadDataApply loads the data for a data source, given a diff that // describes the configuration arguments and desired computed attributes. func (r *Resource) ReadDataApply( ctx context.Context, d *terraform.InstanceDiff, meta interface{}, ) (*terraform.InstanceState, diag.Diagnostics) { // Data sources are always built completely from scratch // on each read, so the source state is always nil. data, err := schemaMap(r.Schema).Data(nil, d) if err != nil { return nil, diag.FromErr(err) } logging.HelperSchemaTrace(ctx, "Calling downstream") diags := r.read(ctx, data, meta) logging.HelperSchemaTrace(ctx, "Called downstream") state := data.State() if state != nil && state.ID == "" { // Data sources can set an ID if they want, but they aren't // required to; we'll provide a placeholder if they don't, // to preserve the invariant that all resources have non-empty // ids. state.ID = "-" } return r.recordCurrentSchemaVersion(state), diags } // RefreshWithoutUpgrade reads the instance state, but does not call // MigrateState or the StateUpgraders, since those are now invoked in a // separate API call. // RefreshWithoutUpgrade is part of the new plugin shims. func (r *Resource) RefreshWithoutUpgrade( ctx context.Context, s *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, diag.Diagnostics) { // If the ID is already somehow blank, it doesn't exist if s.ID == "" { return nil, nil } rt := ResourceTimeout{} if _, ok := s.Meta[TimeoutKey]; ok { if err := rt.StateDecode(s); err != nil { logging.HelperSchemaError(ctx, "Error decoding ResourceTimeout", map[string]interface{}{logging.KeyError: err}) } } if r.Exists != nil { // Make a copy of data so that if it is modified it doesn't // affect our Read later. data, err := schemaMap(r.Schema).Data(s, nil) if err != nil { return s, diag.FromErr(err) } data.timeouts = &rt if s != nil { data.providerMeta = s.ProviderMeta } logging.HelperSchemaTrace(ctx, "Calling downstream") exists, err := r.Exists(data, meta) logging.HelperSchemaTrace(ctx, "Called downstream") if err != nil { return s, diag.FromErr(err) } if !exists { return nil, nil } } data, err := schemaMap(r.Schema).Data(s, nil) if err != nil { return s, diag.FromErr(err) } data.timeouts = &rt if s != nil { data.providerMeta = s.ProviderMeta } logging.HelperSchemaTrace(ctx, "Calling downstream") diags := r.read(ctx, data, meta) logging.HelperSchemaTrace(ctx, "Called downstream") state := data.State() if state != nil && state.ID == "" { state = nil } schemaMap(r.Schema).handleDiffSuppressOnRefresh(ctx, s, state) return r.recordCurrentSchemaVersion(state), diags } func (r *Resource) createFuncSet() bool { return (r.Create != nil || r.CreateContext != nil || r.CreateWithoutTimeout != nil) } func (r *Resource) readFuncSet() bool { return (r.Read != nil || r.ReadContext != nil || r.ReadWithoutTimeout != nil) } func (r *Resource) updateFuncSet() bool { return (r.Update != nil || r.UpdateContext != nil || r.UpdateWithoutTimeout != nil) } func (r *Resource) deleteFuncSet() bool { return (r.Delete != nil || r.DeleteContext != nil || r.DeleteWithoutTimeout != nil) } // InternalValidate should be called to validate the structure // of the resource. // // This should be called in a unit test for any resource to verify // before release that a resource is properly configured for use with // this library. // // Provider.InternalValidate() will automatically call this for all of // the resources it manages, so you don't need to call this manually if it // is part of a Provider. func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error { if r == nil { return errors.New("resource is nil") } if !writable { if r.createFuncSet() || r.updateFuncSet() || r.deleteFuncSet() { return fmt.Errorf("must not implement Create, Update or Delete") } // CustomizeDiff cannot be defined for read-only resources if r.CustomizeDiff != nil { return fmt.Errorf("cannot implement CustomizeDiff") } } tsm := topSchemaMap if r.isTopLevel() && writable { // All non-Computed attributes must be ForceNew if Update is not defined if !r.updateFuncSet() { nonForceNewAttrs := make([]string, 0) for k, v := range r.Schema { if !v.ForceNew && !v.Computed { nonForceNewAttrs = append(nonForceNewAttrs, k) } } if len(nonForceNewAttrs) > 0 { return fmt.Errorf( "No Update defined, must set ForceNew on: %#v", nonForceNewAttrs) } } else { nonUpdateableAttrs := make([]string, 0) for k, v := range r.Schema { if v.ForceNew || v.Computed && !v.Optional { nonUpdateableAttrs = append(nonUpdateableAttrs, k) } } updateableAttrs := len(r.Schema) - len(nonUpdateableAttrs) if updateableAttrs == 0 { return fmt.Errorf( "All fields are ForceNew or Computed w/out Optional, Update is superfluous") } } tsm = schemaMap(r.Schema) // Destroy, and Read are required if !r.readFuncSet() { return fmt.Errorf("Read must be implemented") } if !r.deleteFuncSet() { return fmt.Errorf("Delete must be implemented") } // If we have an importer, we need to verify the importer. if r.Importer != nil { if err := r.Importer.InternalValidate(); err != nil { return err } } if f, ok := tsm["id"]; ok { // if there is an explicit ID, validate it... err := validateResourceID(f) if err != nil { return err } } for k := range tsm { if isReservedResourceFieldName(k) { return fmt.Errorf("%s is a reserved field name", k) } } } lastVersion := -1 for _, u := range r.StateUpgraders { if lastVersion >= 0 && u.Version-lastVersion > 1 { return fmt.Errorf("missing schema version between %d and %d", lastVersion, u.Version) } if u.Version >= r.SchemaVersion { return fmt.Errorf("StateUpgrader version %d is >= current version %d", u.Version, r.SchemaVersion) } if !u.Type.IsObjectType() { return fmt.Errorf("StateUpgrader %d type is not cty.Object", u.Version) } if u.Upgrade == nil { return fmt.Errorf("StateUpgrader %d missing StateUpgradeFunc", u.Version) } lastVersion = u.Version } if lastVersion >= 0 && lastVersion != r.SchemaVersion-1 { return fmt.Errorf("missing StateUpgrader between %d and %d", lastVersion, r.SchemaVersion) } // Data source if r.isTopLevel() && !writable { tsm = schemaMap(r.Schema) for k := range tsm { if isReservedDataSourceFieldName(k) { return fmt.Errorf("%s is a reserved field name", k) } } } // check context funcs are not set alongside their nonctx counterparts if r.CreateContext != nil && r.Create != nil { return fmt.Errorf("CreateContext and Create should not both be set") } if r.ReadContext != nil && r.Read != nil { return fmt.Errorf("ReadContext and Read should not both be set") } if r.UpdateContext != nil && r.Update != nil { return fmt.Errorf("UpdateContext and Update should not both be set") } if r.DeleteContext != nil && r.Delete != nil { return fmt.Errorf("DeleteContext and Delete should not both be set") } // check context funcs are not set alongside their without timeout counterparts if r.CreateContext != nil && r.CreateWithoutTimeout != nil { return fmt.Errorf("CreateContext and CreateWithoutTimeout should not both be set") } if r.ReadContext != nil && r.ReadWithoutTimeout != nil { return fmt.Errorf("ReadContext and ReadWithoutTimeout should not both be set") } if r.UpdateContext != nil && r.UpdateWithoutTimeout != nil { return fmt.Errorf("UpdateContext and UpdateWithoutTimeout should not both be set") } if r.DeleteContext != nil && r.DeleteWithoutTimeout != nil { return fmt.Errorf("DeleteContext and DeleteWithoutTimeout should not both be set") } // check non-context funcs are not set alongside the context without timeout counterparts if r.Create != nil && r.CreateWithoutTimeout != nil { return fmt.Errorf("Create and CreateWithoutTimeout should not both be set") } if r.Read != nil && r.ReadWithoutTimeout != nil { return fmt.Errorf("Read and ReadWithoutTimeout should not both be set") } if r.Update != nil && r.UpdateWithoutTimeout != nil { return fmt.Errorf("Update and UpdateWithoutTimeout should not both be set") } if r.Delete != nil && r.DeleteWithoutTimeout != nil { return fmt.Errorf("Delete and DeleteWithoutTimeout should not both be set") } return schemaMap(r.Schema).InternalValidate(tsm) } func isReservedDataSourceFieldName(name string) bool { for _, reservedName := range ReservedDataSourceFields { if name == reservedName { return true } } return false } func validateResourceID(s *Schema) error { if s.Type != TypeString { return fmt.Errorf(`the "id" attribute must be of TypeString`) } if s.Required { return fmt.Errorf(`the "id" attribute cannot be marked Required`) } // ID should at least be computed. If unspecified it will be set to Computed and Optional, // but Optional is unnecessary if undesired. if !s.Computed { return fmt.Errorf(`the "id" attribute must be marked Computed`) } return nil } func isReservedResourceFieldName(name string) bool { for _, reservedName := range ReservedResourceFields { if name == reservedName { return true } } return false } // Data returns a ResourceData struct for this Resource. Each return value // is a separate copy and can be safely modified differently. // // The data returned from this function has no actual affect on the Resource // itself (including the state given to this function). // // This function is useful for unit tests and ResourceImporter functions. func (r *Resource) Data(s *terraform.InstanceState) *ResourceData { result, err := schemaMap(r.Schema).Data(s, nil) if err != nil { // At the time of writing, this isn't possible (Data never returns // non-nil errors). We panic to find this in the future if we have to. // I don't see a reason for Data to ever return an error. panic(err) } // load the Resource timeouts result.timeouts = r.Timeouts if result.timeouts == nil { result.timeouts = &ResourceTimeout{} } // Set the schema version to latest by default result.meta = map[string]interface{}{ "schema_version": strconv.Itoa(r.SchemaVersion), } return result } // TestResourceData Yields a ResourceData filled with this resource's schema for use in unit testing // // TODO: May be able to be removed with the above ResourceData function. func (r *Resource) TestResourceData() *ResourceData { return &ResourceData{ schema: r.Schema, } } // Returns true if the resource is "top level" i.e. not a sub-resource. func (r *Resource) isTopLevel() bool { // TODO: This is a heuristic; replace with a definitive attribute? return (r.createFuncSet() || r.readFuncSet()) } func (r *Resource) recordCurrentSchemaVersion( state *terraform.InstanceState) *terraform.InstanceState { if state != nil && r.SchemaVersion > 0 { if state.Meta == nil { state.Meta = make(map[string]interface{}) } state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion) } return state } // Noop is a convenience implementation of resource function which takes // no action and returns no error. func Noop(*ResourceData, interface{}) error { return nil } // NoopContext is a convenience implementation of context aware resource function which takes // no action and returns no error. func NoopContext(context.Context, *ResourceData, interface{}) diag.Diagnostics { return nil } // RemoveFromState is a convenience implementation of a resource function // which sets the resource ID to empty string (to remove it from state) // and returns no error. func RemoveFromState(d *ResourceData, _ interface{}) error { d.SetId("") return nil }