From 10794bed76f11a8eca89a49a1f5ae8435346c64e Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 5 Jun 2024 17:36:55 -0400 Subject: [PATCH] Add documentation for deferred actions --- .../plugin/framework/data-sources/index.mdx | 34 +++++++++ .../docs/plugin/framework/providers/index.mdx | 48 ++++++++++++ .../plugin/framework/resources/import.mdx | 26 +++++++ .../framework/resources/plan-modification.mdx | 38 ++++++++++ .../docs/plugin/framework/resources/read.mdx | 75 +++++++++++++++++++ 5 files changed, 221 insertions(+) diff --git a/website/docs/plugin/framework/data-sources/index.mdx b/website/docs/plugin/framework/data-sources/index.mdx index 823f8c311..6e48dd23f 100644 --- a/website/docs/plugin/framework/data-sources/index.mdx +++ b/website/docs/plugin/framework/data-sources/index.mdx @@ -111,6 +111,40 @@ Implement the `Read` method by: If the logic needs to return [warning or error diagnostics](/terraform/plugin/framework/diagnostics), they can added into the [`datasource.ReadResponse.Diagnostics` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource#ReadResponse.Diagnostics). +#### Data Source Deferral +-> Note: This functionality is related to deferred action support, which is currently experimental and is subject to change or break without warning. It is not protected by version compatibility guarantees. + +The `Read` method can indicate a deferral which will defer the data source and all of its dependencies. + +To indicate a deferral, +first check the [`datasource.ReadRequest.ClientCapabilities.DeferralAllowed` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource#ReadClientCapabilities.DeferralAllowed) +to verify that the calling Terraform client supports deferred action functionality. +Then indicate a data source deferral +by setting the [`datasource.ReadResponse.Deferral` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource#ReadResponse.Deferred). + +In the following example, the data source will defer if the `example_attribute` value is unknown: + +```go +func (d *ThingDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data ThingDataSourceModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + // Check that the Terraform client supports deferred actions and if so, + // defer the data source if example_attribute is unknown + if req.ClientCapabilities.DeferralAllowed && data.ExampleAttribute.IsUnknown() { + resp.Deferred = &datasource.Deferred{ + Reason: datasource.DeferredReasonDataSourceConfigUnknown, + } + // Return early for deferral + return + } + + // Set the Terraform state as appropriate +} +``` + ## Add Data Source to Provider Data sources become available to practitioners when they are included in the [provider](/terraform/plugin/framework/providers) implementation via the [`provider.ProviderWithDataSources` interface `DataSources` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider#ProviderWithDataSources.DataSources). diff --git a/website/docs/plugin/framework/providers/index.mdx b/website/docs/plugin/framework/providers/index.mdx index dc3352c33..e2f8b0380 100644 --- a/website/docs/plugin/framework/providers/index.mdx +++ b/website/docs/plugin/framework/providers/index.mdx @@ -228,6 +228,54 @@ to use it. If resources and data sources can't provide any functionality without knowing that value, it's often better to [return an error](/terraform/plugin/framework/diagnostics), which will halt the apply. +#### Provider Deferral +-> Note: This functionality is related to deferred action support, which is currently experimental and is subject to change or break without warning. It is not protected by version compatibility guarantees. + +A provider can indicate an automatic deferral in the `Configure` method, +which will automatically defer all resources and data sources for the provider. + +To indicate a deferral, +first check the [`provider.ConfigureRequest.ClientCapabilities.DeferralAllowed` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider#ConfigureProviderClientCapabilities.DeferralAllowed) +to verify that the calling Terraform client supports deferred action functionality. +Then indicate a provider deferral +by setting the [`provider.ConfigureResponse.Deferral` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider#ConfigureResponse.Deferred). + +In the following example, the provider will automatically defer if the API token value is unknown: + +```go +func (p *ExampleCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + var data ExampleCloudProviderModel + + // Read configuration data into model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + // Check that the Terraform client supports deferred actions + if req.ClientCapabilities.DeferralAllowed && config.ApiToken.isUnknown() { + resp.Deferred = &provider.Deferred{ + Reason: provider.DeferredReasonProviderConfigUnknown, + } + // Return early for deferral since config is not completely known + return + } + + // Create data/clients and persist to resp.DataSourceData and + // resp.ResourceData as appropriate. +} +``` + +##### Opt-in for Plan Modification +By default, +automatically deferred resources will skip plan modification logic defined in a resource's `ModifyPlan` method, +but a resource can opt in for this logic to be called +by setting the [`ResourceBehavior.ProviderDeferred.EnablePlanModification` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource#ProviderDeferredBehavior.EnablePlanModification) in each resource’s `Metadata` method: + +```go +func (r *ThingResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_thing" + resp.ResourceBehavior.ProviderDeferred.EnablePlanModification = true +} +``` + ### Resources The [`provider.Provider` interface `Resources` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider#Provider.Resources) returns a slice of [resources](/terraform/plugin/framework/resources). Each element in the slice is a function to create a new `resource.Resource` so data is not inadvertently shared across multiple, disjointed resource instance operations unless explicitly coded. Information such as the resource type name is managed by the `resource.Resource` implementation. diff --git a/website/docs/plugin/framework/resources/import.mdx b/website/docs/plugin/framework/resources/import.mdx index c0e719d2d..8e0aea015 100644 --- a/website/docs/plugin/framework/resources/import.mdx +++ b/website/docs/plugin/framework/resources/import.mdx @@ -83,6 +83,32 @@ func (r *ThingResource) ImportState(ctx context.Context, req resource.ImportStat } ``` +### Resource Deferral +-> Note: This functionality is related to deferred action support, which is currently experimental and is subject to change or break without warning. It is not protected by version compatibility guarantees. + +The `ImportState` method can indicate a deferral which will defer the resource and all of its dependencies. + +To indicate a deferral, +first check the [`resource.ImportStateRequest.ClientCapabilities.DeferralAllowed` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource#ImportStateClientCapabilities.DeferralAllowed) +to verify that the calling Terraform client supports deferred action functionality. +Then indicate a resource deferral +by setting the [`resource.ImportStateResponse.Deferral` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource#ImportStateResponse.Deferred): + +```go +func (r *ThingResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Check that the Terraform client supports deferred actions + if req.ClientCapabilities.DeferralAllowed { + resp.Deferred = &resource.Deferred{ + Reason: resource.DeferredReasonResourceConfigUnknown, + } + // Return early for deferral + return + } + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} +``` + ## Not Implemented If the resource does not support `terraform import`, skip the `ImportState` method implementation. diff --git a/website/docs/plugin/framework/resources/plan-modification.mdx b/website/docs/plugin/framework/resources/plan-modification.mdx index 7da986333..dcaf2c2f8 100644 --- a/website/docs/plugin/framework/resources/plan-modification.mdx +++ b/website/docs/plugin/framework/resources/plan-modification.mdx @@ -204,3 +204,41 @@ func (r ThingResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRe ``` Ensure the response plan remains entirely `null` when the request plan is entirely `null`. + +### Resource Deferral +-> Note: This functionality is related to deferred action support, which is currently experimental and is subject to change or break without warning. It is not protected by version compatibility guarantees. + +The `ModifyPlan` method can indicate a deferral which will defer the resource and all of its dependencies. + +To indicate a deferral, +first check the [`resource.ModifyPlanRequest.ClientCapabilities.DeferralAllowed` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource#ModifyPlanClientCapabilities.DeferralAllowed) +to verify that the calling Terraform client supports deferred action functionality. +Then indicate a resource deferral +by setting the [`resource.ModifyPlanResponse.Deferral` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource#ModifyPlanResponse.Deferred). + +In the following example, the resource will defer if `example_attribute` value is unknown: + +```go +func (r ThingResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var data ThingResourceModel + + // Read Terraform plan into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + // Check that the Terraform client supports deferred actions and if so, + // defer the resource if example_attribute is unknown + if req.ClientCapabilities.DeferralAllowed && data.ExampleAttribute.IsUnknown() { + resp.Deferred = &resource.Deferred{ + Reason: resource.DeferredReasonResourceConfigUnknown, + } + // Return early for deferral + return + } + + // Modify plan as appropriate +} +``` + +If a [provider deferral](/terraform/plugin/framework/providers/index#provider-deferral) is set in the provider's `Configure` method with plan modification behavior enabled +and a resource indicates a deferral in the `ModifyPlan` method, +then the `Deferred.Reason` set in the resource's `ModifyPlan` will take precedence over the provider's `Deferred.Reason` in the response to the Terraform client. diff --git a/website/docs/plugin/framework/resources/read.mdx b/website/docs/plugin/framework/resources/read.mdx index f9e6bed80..d39ea5d90 100644 --- a/website/docs/plugin/framework/resources/read.mdx +++ b/website/docs/plugin/framework/resources/read.mdx @@ -161,3 +161,78 @@ Note these recommendations when implementing the `Read` method: * Ignore returning errors that signify the resource is no longer existent, call the response state `RemoveResource()` method, and return early. The next Terraform plan will recreate the resource. * Refresh all possible values. This will ensure Terraform shows configuration drift and reduces import logic. * Preserve the prior state value if the updated value is semantically equal. For example, JSON strings that have inconsequential object property reordering or whitespace differences. This prevents Terraform from showing extraneous drift in plans. + +## Resource Deferral +-> Note: This functionality is related to deferred action support, which is currently experimental and is subject to change or break without warning. It is not protected by version compatibility guarantees. + +The `Read` method can indicate a deferral which will defer the resource and all of its dependencies. + +To indicate a deferral, +first check the [`resource.ReadRequest.ClientCapabilities.DeferralAllowed` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource#ReadClientCapabilities.DeferralAllowed) +to verify that the calling Terraform client supports deferred action functionality. +Then indicate a resource deferral +by setting the [`resource.ReadResponse.Deferral` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource#ReadResponse.Deferred). + +In the following example, the resource will defer if the HTTP call returns a 404 Not Found status: + +```go +func (r *ThingResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data ThingResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + // Convert from Terraform data model into API data model + readReq := ThingResourceAPIModel{ + Id: data.Id.ValueString(), + Name: data.Name.ValueString(), + } + + httpReqBody, err := json.Marshal(readReq) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Refresh Resource", + "An unexpected error occurred while creating the resource read request. "+ + "Please report this issue to the provider developers.\n\n"+ + "JSON Error: "+err.Error(), + ) + + return + } + + // Create HTTP request + httpReq := http.NewRequestWithContext( + ctx, + http.MethodPut, + "http://example.com/things", + bytes.NewBuffer(httpReqBody), + ) + + httpResp, err := d.client.Do(httpReq) + defer httpResp.Body.Close() + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Refresh Resource", + "An unexpected error occurred while attempting to refresh resource state. "+ + "Please retry the operation or report this issue to the provider developers.\n\n"+ + "HTTP Error: "+err.Error(), + ) + + return + } + + // Check that the Terraform client supports deferred actions and if so, + // defer resource on a 404 response + if req.ClientCapabilities.DeferralAllowed && httpResp.StatusCode == http.StatusNotFound) { + resp.Deferred = &resource.Deferred{ + Reason: resource.DeferredReasonAbsentPrereq, + } + // Return early for deferral + return + } + + // Refresh state as appropriate +} +```