From df3f0cf2b664ac40b94515bfdf8c05d0fc19967c Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 1 May 2024 15:35:27 -0400 Subject: [PATCH 01/44] Implement manual deferred action support for `resource.importResourceState` --- go.mod | 2 +- go.sum | 6 +- internal/fromproto5/importresourcestate.go | 14 ++-- .../fromproto5/importresourcestate_test.go | 21 +++++- internal/fromproto6/importresourcestate.go | 14 ++-- .../fromproto6/importresourcestate_test.go | 21 +++++- .../fwserver/server_importresourcestate.go | 21 +++++- .../server_importresourcestate_test.go | 75 +++++++++++++++++++ internal/toproto5/importresourcestate.go | 7 +- internal/toproto6/importresourcestate.go | 7 +- resource/deferral.go | 28 +++++++ resource/import_state.go | 17 +++++ 12 files changed, 213 insertions(+), 20 deletions(-) create mode 100644 resource/deferral.go diff --git a/go.mod b/go.mod index aa5120093..377c56177 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.21.6 require ( github.com/google/go-cmp v0.6.0 - github.com/hashicorp/terraform-plugin-go v0.22.2 + github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240424182422-dba624001d4f github.com/hashicorp/terraform-plugin-log v0.9.0 ) diff --git a/go.sum b/go.sum index 3503fab49..7380e78b6 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,10 @@ github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDm github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-go v0.22.2 h1:5o8uveu6eZUf5J7xGPV0eY0TPXg3qpmwX9sce03Bxnc= -github.com/hashicorp/terraform-plugin-go v0.22.2/go.mod h1:drq8Snexp9HsbFZddvyLHN6LuWHHndSQg+gV+FPkcIM= +github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240419152848-9a1607db1cab h1:Q86RQOyr+03Z+MbEi1hhlDADMwc0k24XYqASQAYPE0A= +github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240419152848-9a1607db1cab/go.mod h1:drq8Snexp9HsbFZddvyLHN6LuWHHndSQg+gV+FPkcIM= +github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240424182422-dba624001d4f h1:r6EtFgZbSf+VeNrWqFwTFl5mKBdlFl+bpufwgaNBeQA= +github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240424182422-dba624001d4f/go.mod h1:DbW1zoh21fsPD35whjnDA5aR7bXQV+k+lnkZrn8ZrFM= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= diff --git a/internal/fromproto5/importresourcestate.go b/internal/fromproto5/importresourcestate.go index 4a10174b1..7cb5871c4 100644 --- a/internal/fromproto5/importresourcestate.go +++ b/internal/fromproto5/importresourcestate.go @@ -6,18 +6,19 @@ package fromproto5 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // ImportResourceStateRequest returns the *fwserver.ImportResourceStateRequest // equivalent of a *tfprotov5.ImportResourceStateRequest. -func ImportResourceStateRequest(ctx context.Context, proto5 *tfprotov5.ImportResourceStateRequest, resource resource.Resource, resourceSchema fwschema.Schema) (*fwserver.ImportResourceStateRequest, diag.Diagnostics) { +func ImportResourceStateRequest(ctx context.Context, proto5 *tfprotov5.ImportResourceStateRequest, reqResource resource.Resource, resourceSchema fwschema.Schema) (*fwserver.ImportResourceStateRequest, diag.Diagnostics) { if proto5 == nil { return nil, nil } @@ -43,8 +44,11 @@ func ImportResourceStateRequest(ctx context.Context, proto5 *tfprotov5.ImportRes Raw: tftypes.NewValue(resourceSchema.Type().TerraformType(ctx), nil), Schema: resourceSchema, }, - ID: proto5.ID, - Resource: resource, + ID: proto5.ID, + ClientCapabilities: &resource.ImportStateClientCapabilities{ + DeferralAllowed: proto5.ClientCapabilities.DeferralAllowed, + }, + Resource: reqResource, TypeName: proto5.TypeName, } diff --git a/internal/fromproto5/importresourcestate_test.go b/internal/fromproto5/importresourcestate_test.go index 23a50989b..00013ced3 100644 --- a/internal/fromproto5/importresourcestate_test.go +++ b/internal/fromproto5/importresourcestate_test.go @@ -8,6 +8,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -15,8 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestImportResourceStateRequest(t *testing.T) { @@ -86,6 +87,22 @@ func TestImportResourceStateRequest(t *testing.T) { TypeName: "test_resource", }, }, + "client-capabilities": { + input: &tfprotov5.ImportResourceStateRequest{ + ID: "test-id", + ClientCapabilities: &tfprotov5.ClientCapabilities{ + DeferralAllowed: true, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ImportResourceStateRequest{ + EmptyState: testFwEmptyState, + ID: "test-id", + ClientCapabilities: &resource.ImportStateClientCapabilities{ + DeferralAllowed: true, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fromproto6/importresourcestate.go b/internal/fromproto6/importresourcestate.go index 11018c41b..30158d0c1 100644 --- a/internal/fromproto6/importresourcestate.go +++ b/internal/fromproto6/importresourcestate.go @@ -6,18 +6,19 @@ package fromproto6 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // ImportResourceStateRequest returns the *fwserver.ImportResourceStateRequest // equivalent of a *tfprotov6.ImportResourceStateRequest. -func ImportResourceStateRequest(ctx context.Context, proto6 *tfprotov6.ImportResourceStateRequest, resource resource.Resource, resourceSchema fwschema.Schema) (*fwserver.ImportResourceStateRequest, diag.Diagnostics) { +func ImportResourceStateRequest(ctx context.Context, proto6 *tfprotov6.ImportResourceStateRequest, reqResource resource.Resource, resourceSchema fwschema.Schema) (*fwserver.ImportResourceStateRequest, diag.Diagnostics) { if proto6 == nil { return nil, nil } @@ -43,8 +44,11 @@ func ImportResourceStateRequest(ctx context.Context, proto6 *tfprotov6.ImportRes Raw: tftypes.NewValue(resourceSchema.Type().TerraformType(ctx), nil), Schema: resourceSchema, }, - ID: proto6.ID, - Resource: resource, + ID: proto6.ID, + ClientCapabilities: &resource.ImportStateClientCapabilities{ + DeferralAllowed: proto6.ClientCapabilities.DeferralAllowed, + }, + Resource: reqResource, TypeName: proto6.TypeName, } diff --git a/internal/fromproto6/importresourcestate_test.go b/internal/fromproto6/importresourcestate_test.go index e582263f8..6ef29c77e 100644 --- a/internal/fromproto6/importresourcestate_test.go +++ b/internal/fromproto6/importresourcestate_test.go @@ -8,6 +8,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -15,8 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestImportResourceStateRequest(t *testing.T) { @@ -86,6 +87,22 @@ func TestImportResourceStateRequest(t *testing.T) { TypeName: "test_resource", }, }, + "client-capabilities": { + input: &tfprotov6.ImportResourceStateRequest{ + ID: "test-id", + ClientCapabilities: &tfprotov6.ClientCapabilities{ + DeferralAllowed: true, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ImportResourceStateRequest{ + EmptyState: testFwEmptyState, + ID: "test-id", + ClientCapabilities: &resource.ImportStateClientCapabilities{ + DeferralAllowed: true, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fwserver/server_importresourcestate.go b/internal/fwserver/server_importresourcestate.go index d89cee357..65b1a0761 100644 --- a/internal/fwserver/server_importresourcestate.go +++ b/internal/fwserver/server_importresourcestate.go @@ -35,6 +35,9 @@ type ImportResourceStateRequest struct { // TypeName is the resource type name, which is necessary for populating // the ImportedResource TypeName of the ImportResourceStateResponse. TypeName string + + //TODO: doc + ClientCapabilities *resource.ImportStateClientCapabilities } // ImportResourceStateResponse is the framework server response for the @@ -42,6 +45,7 @@ type ImportResourceStateRequest struct { type ImportResourceStateResponse struct { Diagnostics diag.Diagnostics ImportedResources []ImportedResource + Deferral *resource.DeferralResponse } // ImportResourceState implements the framework server ImportResourceState RPC. @@ -90,7 +94,8 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta } importReq := resource.ImportStateRequest{ - ID: req.ID, + ID: req.ID, + ClientCapabilities: req.ClientCapabilities, } privateProviderData := privatestate.EmptyProviderData(ctx) @@ -113,6 +118,16 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta return } + if (importReq.ClientCapabilities == nil || !importReq.ClientCapabilities.DeferralAllowed) && importResp.DeferralResponse != nil { + resp.Diagnostics.AddError( + "Resource Import Deferral Not Allowed", + "An unexpected error was encountered when importing the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ + "The resource requested a deferral but the Terraform client does not support deferrals, "+ + "resource.DeferralResponse can only be set if resource.ImportStateRequest.ImportStateClientCapabilities.DeferralAllowed is true.", + ) + return + } + if importResp.State.Raw.Equal(req.EmptyState.Raw) { resp.Diagnostics.AddError( "Missing Resource Import State", @@ -128,6 +143,10 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta private.Provider = importResp.Private } + if importResp.DeferralResponse != nil { + resp.Deferral = importResp.DeferralResponse + } + resp.ImportedResources = []ImportedResource{ { State: importResp.State, diff --git a/internal/fwserver/server_importresourcestate_test.go b/internal/fwserver/server_importresourcestate_test.go index c9f00e579..bffba4e59 100644 --- a/internal/fwserver/server_importresourcestate_test.go +++ b/internal/fwserver/server_importresourcestate_test.go @@ -84,6 +84,10 @@ func TestServerImportResourceState(t *testing.T) { Provider: testEmptyProviderData, } + testDeferral := &resource.ImportStateClientCapabilities{ + DeferralAllowed: true, + } + testCases := map[string]struct { server *fwserver.Server request *fwserver.ImportResourceStateRequest @@ -299,6 +303,77 @@ func TestServerImportResourceState(t *testing.T) { }, }, }, + "request-deferral-allowed-response-deferral": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyState, + ID: "test-id", + Resource: &testprovider.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + if req.ID != "test-id" { + resp.Diagnostics.AddError("unexpected req.ID value: %s", req.ID) + } + + resp.DeferralResponse = &resource.DeferralResponse{ + Reason: resource.DeferralReasonAbsentPrereq, + } + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + + }, + }, + TypeName: "test_resource", + ClientCapabilities: testDeferral, + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: *testState, + TypeName: "test_resource", + Private: testEmptyPrivate, + }, + }, + Deferral: &resource.DeferralResponse{Reason: resource.DeferralReasonAbsentPrereq}, + }, + }, + "request-deferral-not-allowed-response-deferral": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyState, + ID: "test-id", + Resource: &testprovider.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + if req.ID != "test-id" { + resp.Diagnostics.AddError("unexpected req.ID value: %s", req.ID) + } + + resp.DeferralResponse = &resource.DeferralResponse{ + Reason: resource.DeferralReasonAbsentPrereq, + } + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Resource Import Deferral Not Allowed", + "An unexpected error was encountered when importing the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ + "The resource requested a deferral but the Terraform client does not support deferrals, "+ + "resource.DeferralResponse can only be set if resource.ImportStateRequest.ImportStateClientCapabilities.DeferralAllowed is true.", + ), + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/toproto5/importresourcestate.go b/internal/toproto5/importresourcestate.go index 654b84b63..606b5de55 100644 --- a/internal/toproto5/importresourcestate.go +++ b/internal/toproto5/importresourcestate.go @@ -6,8 +6,9 @@ package toproto5 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" ) // ImportResourceStateResponse returns the *tfprotov5.ImportResourceStateResponse @@ -33,5 +34,9 @@ func ImportResourceStateResponse(ctx context.Context, fw *fwserver.ImportResourc proto5.ImportedResources = append(proto5.ImportedResources, proto5ImportedResource) } + if fw.Deferral != nil { + proto5.Deferred.Reason = tfprotov5.DeferredReason(fw.Deferral.Reason) + } + return proto5 } diff --git a/internal/toproto6/importresourcestate.go b/internal/toproto6/importresourcestate.go index a5a29a699..f5ecd4862 100644 --- a/internal/toproto6/importresourcestate.go +++ b/internal/toproto6/importresourcestate.go @@ -6,8 +6,9 @@ package toproto6 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" ) // ImportResourceStateResponse returns the *tfprotov6.ImportResourceStateResponse @@ -33,5 +34,9 @@ func ImportResourceStateResponse(ctx context.Context, fw *fwserver.ImportResourc proto6.ImportedResources = append(proto6.ImportedResources, proto6ImportedResource) } + if fw.Deferral != nil { + proto6.Deferred.Reason = tfprotov6.DeferredReason(fw.Deferral.Reason) + } + return proto6 } diff --git a/resource/deferral.go b/resource/deferral.go new file mode 100644 index 000000000..1ce045222 --- /dev/null +++ b/resource/deferral.go @@ -0,0 +1,28 @@ +package resource + +const ( + DeferralReasonUnknown DeferralReason = 0 + DeferralReasonResourceConfigUnknown DeferralReason = 1 + DeferralReasonProviderConfigUnknown DeferralReason = 2 + DeferralReasonAbsentPrereq DeferralReason = 3 +) + +type DeferralResponse struct { + Reason DeferralReason +} + +type DeferralReason int32 + +func (d DeferralReason) String() string { + switch d { + case 0: + return "Unknown" + case 1: + return "Resource Config Unknown" + case 2: + return "Provider Config Unknown" + case 3: + return "Absent Prerequisite" + } + return "Unknown" +} diff --git a/resource/import_state.go b/resource/import_state.go index c08255741..7e9c0669a 100644 --- a/resource/import_state.go +++ b/resource/import_state.go @@ -12,6 +12,13 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) +// TODO: doc +type ImportStateClientCapabilities struct { + // DeferralAllowed indicates whether the Terraform client initiating + // the request allows a deferral response. + DeferralAllowed bool +} + // ImportStateRequest represents a request for the provider to import a // resource. An instance of this request struct is supplied as an argument to // the Resource's ImportState method. @@ -23,6 +30,9 @@ type ImportStateRequest struct { // its own type of value and parsed during import. This value // is not stored in the state unless the provider explicitly stores it. ID string + + //TODO: doc + ClientCapabilities *ImportStateClientCapabilities } // ImportStateResponse represents a response to a ImportStateRequest. @@ -44,6 +54,13 @@ type ImportStateResponse struct { // This field is not pre-populated as there is no pre-existing private state // data during the resource's Import operation. Private *privatestate.ProviderData + + // DeferralResponse indicates that Terraform should defer + // importing this resource. + // + // This field can only be set if + // `(resource.ImportStateRequest.ImportStateClientCapabilities).DeferralAllowed` is true. + DeferralResponse *DeferralResponse } // ImportStatePassthroughID is a helper function to set the import From 0a270766a78331d0a47ec75da5498d2ed91b52a4 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Fri, 3 May 2024 17:04:26 -0400 Subject: [PATCH 02/44] Implement manual deferred action support for `resource.readResource` --- internal/fromproto5/importresourcestate.go | 11 +-- internal/fromproto5/readresource.go | 10 ++- internal/fromproto5/readresource_test.go | 13 ++++ internal/fromproto6/importresourcestate.go | 11 +-- internal/fromproto6/readresource.go | 10 ++- internal/fromproto6/readresource_test.go | 13 ++++ internal/fwserver/server_readresource.go | 23 ++++-- internal/fwserver/server_readresource_test.go | 71 +++++++++++++++++++ internal/toproto5/importresourcestate.go | 4 +- internal/toproto5/readresource.go | 6 ++ internal/toproto5/readresource_test.go | 17 +++++ internal/toproto6/importresourcestate.go | 4 +- internal/toproto6/readresource.go | 6 ++ internal/toproto6/readresource_test.go | 17 +++++ resource/read.go | 17 +++++ 15 files changed, 215 insertions(+), 18 deletions(-) diff --git a/internal/fromproto5/importresourcestate.go b/internal/fromproto5/importresourcestate.go index 7cb5871c4..d074f0c9e 100644 --- a/internal/fromproto5/importresourcestate.go +++ b/internal/fromproto5/importresourcestate.go @@ -44,13 +44,16 @@ func ImportResourceStateRequest(ctx context.Context, proto5 *tfprotov5.ImportRes Raw: tftypes.NewValue(resourceSchema.Type().TerraformType(ctx), nil), Schema: resourceSchema, }, - ID: proto5.ID, - ClientCapabilities: &resource.ImportStateClientCapabilities{ - DeferralAllowed: proto5.ClientCapabilities.DeferralAllowed, - }, + ID: proto5.ID, Resource: reqResource, TypeName: proto5.TypeName, } + if proto5.ClientCapabilities != nil { + fw.ClientCapabilities = &resource.ImportStateClientCapabilities{ + DeferralAllowed: proto5.ClientCapabilities.DeferralAllowed, + } + } + return fw, diags } diff --git a/internal/fromproto5/readresource.go b/internal/fromproto5/readresource.go index 94164a2c0..a3860f6df 100644 --- a/internal/fromproto5/readresource.go +++ b/internal/fromproto5/readresource.go @@ -17,7 +17,7 @@ import ( // ReadResourceRequest returns the *fwserver.ReadResourceRequest // equivalent of a *tfprotov5.ReadResourceRequest. -func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequest, resource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema) (*fwserver.ReadResourceRequest, diag.Diagnostics) { +func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema) (*fwserver.ReadResourceRequest, diag.Diagnostics) { if proto5 == nil { return nil, nil } @@ -25,7 +25,7 @@ func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequ var diags diag.Diagnostics fw := &fwserver.ReadResourceRequest{ - Resource: resource, + Resource: reqResource, } currentState, currentStateDiags := State(ctx, proto5.CurrentState, resourceSchema) @@ -46,5 +46,11 @@ func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequ fw.Private = privateData + if proto5.ClientCapabilities != nil { + fw.ClientCapabilities = &resource.ReadClientCapabilities{ + DeferralAllowed: proto5.ClientCapabilities.DeferralAllowed, + } + } + return fw, diags } diff --git a/internal/fromproto5/readresource_test.go b/internal/fromproto5/readresource_test.go index 549417515..7a0c9534b 100644 --- a/internal/fromproto5/readresource_test.go +++ b/internal/fromproto5/readresource_test.go @@ -56,6 +56,8 @@ func TestReadResourceRequest(t *testing.T) { testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + testClientCapabilities := tfprotov5.ClientCapabilities{DeferralAllowed: true} + testCases := map[string]struct { input *tfprotov5.ReadResourceRequest resourceSchema fwschema.Schema @@ -172,6 +174,17 @@ func TestReadResourceRequest(t *testing.T) { }, }, }, + "client-capabilities": { + input: &tfprotov5.ReadResourceRequest{ + ClientCapabilities: &testClientCapabilities, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ReadResourceRequest{ + ClientCapabilities: &resource.ReadClientCapabilities{ + DeferralAllowed: true, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fromproto6/importresourcestate.go b/internal/fromproto6/importresourcestate.go index 30158d0c1..431fadd4c 100644 --- a/internal/fromproto6/importresourcestate.go +++ b/internal/fromproto6/importresourcestate.go @@ -44,13 +44,16 @@ func ImportResourceStateRequest(ctx context.Context, proto6 *tfprotov6.ImportRes Raw: tftypes.NewValue(resourceSchema.Type().TerraformType(ctx), nil), Schema: resourceSchema, }, - ID: proto6.ID, - ClientCapabilities: &resource.ImportStateClientCapabilities{ - DeferralAllowed: proto6.ClientCapabilities.DeferralAllowed, - }, + ID: proto6.ID, Resource: reqResource, TypeName: proto6.TypeName, } + if proto6.ClientCapabilities != nil { + fw.ClientCapabilities = &resource.ImportStateClientCapabilities{ + DeferralAllowed: proto6.ClientCapabilities.DeferralAllowed, + } + } + return fw, diags } diff --git a/internal/fromproto6/readresource.go b/internal/fromproto6/readresource.go index 4e48e565b..2679b6612 100644 --- a/internal/fromproto6/readresource.go +++ b/internal/fromproto6/readresource.go @@ -17,7 +17,7 @@ import ( // ReadResourceRequest returns the *fwserver.ReadResourceRequest // equivalent of a *tfprotov6.ReadResourceRequest. -func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequest, resource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema) (*fwserver.ReadResourceRequest, diag.Diagnostics) { +func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema) (*fwserver.ReadResourceRequest, diag.Diagnostics) { if proto6 == nil { return nil, nil } @@ -25,7 +25,7 @@ func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequ var diags diag.Diagnostics fw := &fwserver.ReadResourceRequest{ - Resource: resource, + Resource: reqResource, } currentState, currentStateDiags := State(ctx, proto6.CurrentState, resourceSchema) @@ -46,5 +46,11 @@ func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequ fw.Private = privateData + if proto6.ClientCapabilities != nil { + fw.ClientCapabilities = &resource.ReadClientCapabilities{ + DeferralAllowed: proto6.ClientCapabilities.DeferralAllowed, + } + } + return fw, diags } diff --git a/internal/fromproto6/readresource_test.go b/internal/fromproto6/readresource_test.go index 3c75a7b45..f3f61a017 100644 --- a/internal/fromproto6/readresource_test.go +++ b/internal/fromproto6/readresource_test.go @@ -56,6 +56,8 @@ func TestReadResourceRequest(t *testing.T) { testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + testClientCapabilities := tfprotov6.ClientCapabilities{DeferralAllowed: true} + testCases := map[string]struct { input *tfprotov6.ReadResourceRequest resourceSchema fwschema.Schema @@ -172,6 +174,17 @@ func TestReadResourceRequest(t *testing.T) { }, }, }, + "client-capabilities": { + input: &tfprotov6.ReadResourceRequest{ + ClientCapabilities: &testClientCapabilities, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ReadResourceRequest{ + ClientCapabilities: &resource.ReadClientCapabilities{ + DeferralAllowed: true, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index 172a48009..47e5554e4 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -17,10 +17,11 @@ import ( // ReadResourceRequest is the framework server request for the // ReadResource RPC. type ReadResourceRequest struct { - CurrentState *tfsdk.State - Resource resource.Resource - Private *privatestate.Data - ProviderMeta *tfsdk.Config + CurrentState *tfsdk.State + Resource resource.Resource + Private *privatestate.Data + ProviderMeta *tfsdk.Config + ClientCapabilities *resource.ReadClientCapabilities } // ReadResourceResponse is the framework server response for the @@ -29,6 +30,7 @@ type ReadResourceResponse struct { Diagnostics diag.Diagnostics NewState *tfsdk.State Private *privatestate.Data + Deferral *resource.DeferralResponse } // ReadResource implements the framework server ReadResource RPC. @@ -97,12 +99,15 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res resp.Private = req.Private } + readReq.ClientCapabilities = req.ClientCapabilities + logging.FrameworkTrace(ctx, "Calling provider defined Resource Read") req.Resource.Read(ctx, readReq, &readResp) logging.FrameworkTrace(ctx, "Called provider defined Resource Read") resp.Diagnostics = readResp.Diagnostics resp.NewState = &readResp.State + resp.Deferral = readResp.DeferralResponse if readResp.Private != nil { if resp.Private == nil { @@ -116,6 +121,16 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res return } + if (req.ClientCapabilities == nil || !req.ClientCapabilities.DeferralAllowed) && resp.Deferral != nil { + resp.Diagnostics.AddError( + "Resource Deferral Not Allowed", + "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ + "The resource requested a deferral but the Terraform client does not support deferrals, "+ + "resource.DeferralResponse can only be set if resource.ReadRequest.ReadClientCapabilities.DeferralAllowed is true.", + ) + return + } + semanticEqualityReq := SchemaSemanticEqualityRequest{ PriorData: fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, diff --git a/internal/fwserver/server_readresource_test.go b/internal/fwserver/server_readresource_test.go index e248d2baf..f95f16511 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -135,6 +135,10 @@ func TestServerReadResource(t *testing.T) { Provider: testEmptyProviderData, } + testDeferralAllowed := &resource.ReadClientCapabilities{ + DeferralAllowed: true, + } + testCases := map[string]struct { server *fwserver.Server request *fwserver.ReadResourceRequest @@ -510,6 +514,73 @@ func TestServerReadResource(t *testing.T) { Private: testPrivate, }, }, + "request-deferral-allowed-response-deferral": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + resp.DeferralResponse = &resource.DeferralResponse{Reason: resource.DeferralReasonAbsentPrereq} + + if data.TestRequired.ValueString() != "test-currentstate-value" { + resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) + } + }, + }, + ClientCapabilities: testDeferralAllowed, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: testCurrentState, + Private: testEmptyPrivate, + Deferral: &resource.DeferralResponse{Reason: resource.DeferralReasonAbsentPrereq}, + }, + }, + "request-deferral-not-allowed-response-deferral": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + resp.DeferralResponse = &resource.DeferralResponse{Reason: resource.DeferralReasonAbsentPrereq} + + if data.TestRequired.ValueString() != "test-currentstate-value" { + resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) + } + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Resource Deferral Not Allowed", + "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ + "The resource requested a deferral but the Terraform client does not support deferrals, "+ + "resource.DeferralResponse can only be set if resource.ReadRequest.ReadClientCapabilities.DeferralAllowed is true.", + ), + }, + NewState: testCurrentState, + Private: testEmptyPrivate, + Deferral: &resource.DeferralResponse{Reason: resource.DeferralReasonAbsentPrereq}, + }, + }, } for name, testCase := range testCases { diff --git a/internal/toproto5/importresourcestate.go b/internal/toproto5/importresourcestate.go index 606b5de55..49131f02f 100644 --- a/internal/toproto5/importresourcestate.go +++ b/internal/toproto5/importresourcestate.go @@ -35,7 +35,9 @@ func ImportResourceStateResponse(ctx context.Context, fw *fwserver.ImportResourc } if fw.Deferral != nil { - proto5.Deferred.Reason = tfprotov5.DeferredReason(fw.Deferral.Reason) + proto5.Deferred = &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(fw.Deferral.Reason), + } } return proto5 diff --git a/internal/toproto5/readresource.go b/internal/toproto5/readresource.go index 43401ab09..d60d2eab1 100644 --- a/internal/toproto5/readresource.go +++ b/internal/toproto5/readresource.go @@ -32,5 +32,11 @@ func ReadResourceResponse(ctx context.Context, fw *fwserver.ReadResourceResponse proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.Private = newPrivate + if fw.Deferral != nil { + proto5.Deferred = &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(fw.Deferral.Reason), + } + } + return proto5 } diff --git a/internal/toproto5/readresource_test.go b/internal/toproto5/readresource_test.go index 4967f9888..9e66c7065 100644 --- a/internal/toproto5/readresource_test.go +++ b/internal/toproto5/readresource_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -68,6 +69,14 @@ func TestReadResourceResponse(t *testing.T) { }, } + testDeferral := &resource.DeferralResponse{ + Reason: resource.DeferralReasonAbsentPrereq, + } + + testProto5Deferred := &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReasonAbsentPrereq, + } + testCases := map[string]struct { input *fwserver.ReadResourceResponse expected *tfprotov5.ReadResourceResponse @@ -167,6 +176,14 @@ func TestReadResourceResponse(t *testing.T) { }), }, }, + "deferral": { + input: &fwserver.ReadResourceResponse{ + Deferral: testDeferral, + }, + expected: &tfprotov5.ReadResourceResponse{ + Deferred: testProto5Deferred, + }, + }, } for name, testCase := range testCases { diff --git a/internal/toproto6/importresourcestate.go b/internal/toproto6/importresourcestate.go index f5ecd4862..ae003832f 100644 --- a/internal/toproto6/importresourcestate.go +++ b/internal/toproto6/importresourcestate.go @@ -35,7 +35,9 @@ func ImportResourceStateResponse(ctx context.Context, fw *fwserver.ImportResourc } if fw.Deferral != nil { - proto6.Deferred.Reason = tfprotov6.DeferredReason(fw.Deferral.Reason) + proto6.Deferred = &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReason(fw.Deferral.Reason), + } } return proto6 diff --git a/internal/toproto6/readresource.go b/internal/toproto6/readresource.go index d5e600e10..eb154d1d3 100644 --- a/internal/toproto6/readresource.go +++ b/internal/toproto6/readresource.go @@ -32,5 +32,11 @@ func ReadResourceResponse(ctx context.Context, fw *fwserver.ReadResourceResponse proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.Private = newPrivate + if fw.Deferral != nil { + proto6.Deferred = &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReason(fw.Deferral.Reason), + } + } + return proto6 } diff --git a/internal/toproto6/readresource_test.go b/internal/toproto6/readresource_test.go index c34141b4f..2fa0b46f6 100644 --- a/internal/toproto6/readresource_test.go +++ b/internal/toproto6/readresource_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -68,6 +69,14 @@ func TestReadResourceResponse(t *testing.T) { testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + testDeferral := &resource.DeferralResponse{ + Reason: resource.DeferralReasonAbsentPrereq, + } + + testProto6Deferred := &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReasonAbsentPrereq, + } + testCases := map[string]struct { input *fwserver.ReadResourceResponse expected *tfprotov6.ReadResourceResponse @@ -167,6 +176,14 @@ func TestReadResourceResponse(t *testing.T) { }), }, }, + "deferral": { + input: &fwserver.ReadResourceResponse{ + Deferral: testDeferral, + }, + expected: &tfprotov6.ReadResourceResponse{ + Deferred: testProto6Deferred, + }, + }, } for name, testCase := range testCases { diff --git a/resource/read.go b/resource/read.go index 0cc135434..0e714dfc1 100644 --- a/resource/read.go +++ b/resource/read.go @@ -9,6 +9,13 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) +// TODO: doc +type ReadClientCapabilities struct { + // DeferralAllowed indicates whether the Terraform client initiating + // the request allows a deferral response. + DeferralAllowed bool +} + // ReadRequest represents a request for the provider to read a // resource, i.e., update values in state according to the real state of the // resource. An instance of this request struct is supplied as an argument to @@ -29,6 +36,9 @@ type ReadRequest struct { // ProviderMeta is metadata from the provider_meta block of the module. ProviderMeta tfsdk.Config + + //TODO: doc + ClientCapabilities *ReadClientCapabilities } // ReadResponse represents a response to a ReadRequest. An @@ -50,4 +60,11 @@ type ReadResponse struct { // resource. An empty slice indicates a successful operation with no // warnings or errors generated. Diagnostics diag.Diagnostics + + // DeferralResponse indicates that Terraform should defer + // importing this resource. + // + // This field can only be set if + // `(resource.ReadRequest.ReadStateClientCapabilities).DeferralAllowed` is true. + DeferralResponse *DeferralResponse } From bbf6c8dcd92b9e36e2ede61812b52028a56f7714 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 7 May 2024 14:58:07 -0400 Subject: [PATCH 03/44] Implement manual deferred action support for `resource.modifyPlan` --- internal/fromproto5/plan.go | 3 +- internal/fromproto5/planresourcechange.go | 10 +- .../fromproto5/planresourcechange_test.go | 14 +++ internal/fromproto6/planresourcechange.go | 10 +- .../fromproto6/planresourcechange_test.go | 14 +++ .../fwserver/server_planresourcechange.go | 36 ++++--- .../server_planresourcechange_test.go | 93 +++++++++++++++++++ internal/fwserver/server_readresource.go | 4 +- internal/toproto5/planresourcechange.go | 6 ++ internal/toproto5/planresourcechange_test.go | 17 ++++ internal/toproto6/planresourcechange.go | 6 ++ internal/toproto6/planresourcechange_test.go | 17 ++++ resource/modify_plan.go | 17 ++++ 13 files changed, 229 insertions(+), 18 deletions(-) diff --git a/internal/fromproto5/plan.go b/internal/fromproto5/plan.go index f20a1a509..3882811eb 100644 --- a/internal/fromproto5/plan.go +++ b/internal/fromproto5/plan.go @@ -6,11 +6,12 @@ package fromproto5 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) // Plan returns the *tfsdk.Plan for a *tfprotov5.DynamicValue and diff --git a/internal/fromproto5/planresourcechange.go b/internal/fromproto5/planresourcechange.go index 67cced273..0852ad5db 100644 --- a/internal/fromproto5/planresourcechange.go +++ b/internal/fromproto5/planresourcechange.go @@ -17,7 +17,7 @@ import ( // PlanResourceChangeRequest returns the *fwserver.PlanResourceChangeRequest // equivalent of a *tfprotov5.PlanResourceChangeRequest. -func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResourceChangeRequest, resource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema) (*fwserver.PlanResourceChangeRequest, diag.Diagnostics) { +func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResourceChangeRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema) (*fwserver.PlanResourceChangeRequest, diag.Diagnostics) { if proto5 == nil { return nil, nil } @@ -40,7 +40,7 @@ func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResour fw := &fwserver.PlanResourceChangeRequest{ ResourceSchema: resourceSchema, - Resource: resource, + Resource: reqResource, } config, configDiags := Config(ctx, proto5.Config, resourceSchema) @@ -73,5 +73,11 @@ func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResour fw.PriorPrivate = privateData + if proto5.ClientCapabilities != nil { + fw.ClientCapabilities = &resource.ModifyPlanClientCapabilities{ + DeferralAllowed: proto5.ClientCapabilities.DeferralAllowed, + } + } + return fw, diags } diff --git a/internal/fromproto5/planresourcechange_test.go b/internal/fromproto5/planresourcechange_test.go index 0d7638868..f134a807e 100644 --- a/internal/fromproto5/planresourcechange_test.go +++ b/internal/fromproto5/planresourcechange_test.go @@ -54,6 +54,8 @@ func TestPlanResourceChangeRequest(t *testing.T) { testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + testClientCapabilities := tfprotov5.ClientCapabilities{DeferralAllowed: true} + testCases := map[string]struct { input *tfprotov5.PlanResourceChangeRequest resourceSchema fwschema.Schema @@ -217,6 +219,18 @@ func TestPlanResourceChangeRequest(t *testing.T) { ResourceSchema: testFwSchema, }, }, + "client-capabilities": { + input: &tfprotov5.PlanResourceChangeRequest{ + ClientCapabilities: &testClientCapabilities, + }, + resourceSchema: testFwSchema, + expected: &fwserver.PlanResourceChangeRequest{ + ClientCapabilities: &resource.ModifyPlanClientCapabilities{ + DeferralAllowed: true, + }, + ResourceSchema: testFwSchema, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fromproto6/planresourcechange.go b/internal/fromproto6/planresourcechange.go index 3c2bb3e9c..a957c9b9e 100644 --- a/internal/fromproto6/planresourcechange.go +++ b/internal/fromproto6/planresourcechange.go @@ -17,7 +17,7 @@ import ( // PlanResourceChangeRequest returns the *fwserver.PlanResourceChangeRequest // equivalent of a *tfprotov6.PlanResourceChangeRequest. -func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResourceChangeRequest, resource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema) (*fwserver.PlanResourceChangeRequest, diag.Diagnostics) { +func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResourceChangeRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema) (*fwserver.PlanResourceChangeRequest, diag.Diagnostics) { if proto6 == nil { return nil, nil } @@ -40,7 +40,7 @@ func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResour fw := &fwserver.PlanResourceChangeRequest{ ResourceSchema: resourceSchema, - Resource: resource, + Resource: reqResource, } config, configDiags := Config(ctx, proto6.Config, resourceSchema) @@ -73,5 +73,11 @@ func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResour fw.PriorPrivate = privateData + if proto6.ClientCapabilities != nil { + fw.ClientCapabilities = &resource.ModifyPlanClientCapabilities{ + DeferralAllowed: proto6.ClientCapabilities.DeferralAllowed, + } + } + return fw, diags } diff --git a/internal/fromproto6/planresourcechange_test.go b/internal/fromproto6/planresourcechange_test.go index ca8d11f59..332d7c473 100644 --- a/internal/fromproto6/planresourcechange_test.go +++ b/internal/fromproto6/planresourcechange_test.go @@ -54,6 +54,8 @@ func TestPlanResourceChangeRequest(t *testing.T) { testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + testClientCapabilities := tfprotov6.ClientCapabilities{DeferralAllowed: true} + testCases := map[string]struct { input *tfprotov6.PlanResourceChangeRequest resourceSchema fwschema.Schema @@ -217,6 +219,18 @@ func TestPlanResourceChangeRequest(t *testing.T) { ResourceSchema: testFwSchema, }, }, + "client-capabilities": { + input: &tfprotov6.PlanResourceChangeRequest{ + ClientCapabilities: &testClientCapabilities, + }, + resourceSchema: testFwSchema, + expected: &fwserver.PlanResourceChangeRequest{ + ClientCapabilities: &resource.ModifyPlanClientCapabilities{ + DeferralAllowed: true, + }, + ResourceSchema: testFwSchema, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index f769eea3f..e261a3fcf 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -26,18 +26,20 @@ import ( // PlanResourceChangeRequest is the framework server request for the // PlanResourceChange RPC. type PlanResourceChangeRequest struct { - Config *tfsdk.Config - PriorPrivate *privatestate.Data - PriorState *tfsdk.State - ProposedNewState *tfsdk.Plan - ProviderMeta *tfsdk.Config - ResourceSchema fwschema.Schema - Resource resource.Resource + ClientCapabilities *resource.ModifyPlanClientCapabilities + Config *tfsdk.Config + PriorPrivate *privatestate.Data + PriorState *tfsdk.State + ProposedNewState *tfsdk.Plan + ProviderMeta *tfsdk.Config + ResourceSchema fwschema.Schema + Resource resource.Resource } // PlanResourceChangeResponse is the framework server response for the // PlanResourceChange RPC. type PlanResourceChangeResponse struct { + Deferral *resource.DeferralResponse Diagnostics diag.Diagnostics PlannedPrivate *privatestate.Data PlannedState *tfsdk.State @@ -260,10 +262,11 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange logging.FrameworkTrace(ctx, "Resource implements ResourceWithModifyPlan") modifyPlanReq := resource.ModifyPlanRequest{ - Config: *req.Config, - Plan: stateToPlan(*resp.PlannedState), - State: *req.PriorState, - Private: resp.PlannedPrivate.Provider, + Config: *req.Config, + Plan: stateToPlan(*resp.PlannedState), + State: *req.PriorState, + Private: resp.PlannedPrivate.Provider, + ClientCapabilities: req.ClientCapabilities, } if req.ProviderMeta != nil { @@ -281,10 +284,21 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange resourceWithModifyPlan.ModifyPlan(ctx, modifyPlanReq, &modifyPlanResp) logging.FrameworkTrace(ctx, "Called provider defined Resource ModifyPlan") + if (modifyPlanReq.ClientCapabilities == nil || !modifyPlanReq.ClientCapabilities.DeferralAllowed) && modifyPlanResp.DeferralResponse != nil { + resp.Diagnostics.AddError( + "Resource Deferral Not Allowed", + "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ + "The resource requested a deferral but the Terraform client does not support deferrals, "+ + "(*resource.ModifyPlanResponse).DeferralResponse can only be set if (resource.ModifyPlanRequest).ClientCapabilities.DeferralAllowed is true.", + ) + return + } + resp.Diagnostics = modifyPlanResp.Diagnostics resp.PlannedState = planToState(modifyPlanResp.Plan) resp.RequiresReplace = append(resp.RequiresReplace, modifyPlanResp.RequiresReplace...) resp.PlannedPrivate.Provider = modifyPlanResp.Private + resp.Deferral = modifyPlanResp.DeferralResponse } // Ensure deterministic RequiresReplace by sorting and deduplicating diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index e8bd95c0c..aa62a2132 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -1230,6 +1230,10 @@ func TestServerPlanResourceChange(t *testing.T) { Provider: testEmptyProviderData, } + testDeferralAllowed := &resource.ModifyPlanClientCapabilities{ + DeferralAllowed: true, + } + testCases := map[string]struct { server *fwserver.Server request *fwserver.PlanResourceChangeRequest @@ -6015,6 +6019,95 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testPrivateProvider, }, }, + "create-resourcewithmodifyplan-request-deferral-allowed-response-deferral": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + ClientCapabilities: testDeferralAllowed, + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PriorState: testEmptyState, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if req.ClientCapabilities.DeferralAllowed == true { + resp.DeferralResponse = &resource.DeferralResponse{Reason: resource.DeferralReasonAbsentPrereq} + } + + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + Deferral: &resource.DeferralResponse{Reason: resource.DeferralReasonAbsentPrereq}, + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, + "create-resourcewithmodifyplan-request-deferral-not-allowed-response-deferral": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PriorState: testEmptyState, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + resp.DeferralResponse = &resource.DeferralResponse{Reason: resource.DeferralReasonAbsentPrereq} + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Resource Deferral Not Allowed", + "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ + "The resource requested a deferral but the Terraform client does not support deferrals, "+ + "(*resource.ModifyPlanResponse).DeferralResponse can only be set if (resource.ModifyPlanRequest).ClientCapabilities.DeferralAllowed is true.", + ), + }, + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index 47e5554e4..8ce320c72 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -17,20 +17,20 @@ import ( // ReadResourceRequest is the framework server request for the // ReadResource RPC. type ReadResourceRequest struct { + ClientCapabilities *resource.ReadClientCapabilities CurrentState *tfsdk.State Resource resource.Resource Private *privatestate.Data ProviderMeta *tfsdk.Config - ClientCapabilities *resource.ReadClientCapabilities } // ReadResourceResponse is the framework server response for the // ReadResource RPC. type ReadResourceResponse struct { + Deferral *resource.DeferralResponse Diagnostics diag.Diagnostics NewState *tfsdk.State Private *privatestate.Data - Deferral *resource.DeferralResponse } // ReadResource implements the framework server ReadResource RPC. diff --git a/internal/toproto5/planresourcechange.go b/internal/toproto5/planresourcechange.go index 1677b359a..b511c3870 100644 --- a/internal/toproto5/planresourcechange.go +++ b/internal/toproto5/planresourcechange.go @@ -38,5 +38,11 @@ func PlanResourceChangeResponse(ctx context.Context, fw *fwserver.PlanResourceCh proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.PlannedPrivate = plannedPrivate + if fw.Deferral != nil { + proto5.Deferred = &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(fw.Deferral.Reason), + } + } + return proto5 } diff --git a/internal/toproto5/planresourcechange_test.go b/internal/toproto5/planresourcechange_test.go index c0e76d83a..ca67cc258 100644 --- a/internal/toproto5/planresourcechange_test.go +++ b/internal/toproto5/planresourcechange_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -69,6 +70,14 @@ func TestPlanResourceChangeResponse(t *testing.T) { testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + testDeferral := &resource.DeferralResponse{ + Reason: resource.DeferralReasonAbsentPrereq, + } + + testProto5Deferred := &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReasonAbsentPrereq, + } + testCases := map[string]struct { input *fwserver.PlanResourceChangeResponse expected *tfprotov5.PlanResourceChangeResponse @@ -180,6 +189,14 @@ func TestPlanResourceChangeResponse(t *testing.T) { }, }, }, + "deferral": { + input: &fwserver.PlanResourceChangeResponse{ + Deferral: testDeferral, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + Deferred: testProto5Deferred, + }, + }, } for name, testCase := range testCases { diff --git a/internal/toproto6/planresourcechange.go b/internal/toproto6/planresourcechange.go index 8cb2b66b1..cc538dfec 100644 --- a/internal/toproto6/planresourcechange.go +++ b/internal/toproto6/planresourcechange.go @@ -38,5 +38,11 @@ func PlanResourceChangeResponse(ctx context.Context, fw *fwserver.PlanResourceCh proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.PlannedPrivate = plannedPrivate + if fw.Deferral != nil { + proto6.Deferred = &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReason(fw.Deferral.Reason), + } + } + return proto6 } diff --git a/internal/toproto6/planresourcechange_test.go b/internal/toproto6/planresourcechange_test.go index ecae8783e..5bd5e0963 100644 --- a/internal/toproto6/planresourcechange_test.go +++ b/internal/toproto6/planresourcechange_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -69,6 +70,14 @@ func TestPlanResourceChangeResponse(t *testing.T) { testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + testDeferral := &resource.DeferralResponse{ + Reason: resource.DeferralReasonAbsentPrereq, + } + + testProto6Deferred := &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReasonAbsentPrereq, + } + testCases := map[string]struct { input *fwserver.PlanResourceChangeResponse expected *tfprotov6.PlanResourceChangeResponse @@ -180,6 +189,14 @@ func TestPlanResourceChangeResponse(t *testing.T) { }, }, }, + "deferral": { + input: &fwserver.PlanResourceChangeResponse{ + Deferral: testDeferral, + }, + expected: &tfprotov6.PlanResourceChangeResponse{ + Deferred: testProto6Deferred, + }, + }, } for name, testCase := range testCases { diff --git a/resource/modify_plan.go b/resource/modify_plan.go index 76be450d8..655f84f5d 100644 --- a/resource/modify_plan.go +++ b/resource/modify_plan.go @@ -10,6 +10,13 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) +// TODO: doc +type ModifyPlanClientCapabilities struct { + // DeferralAllowed indicates whether the Terraform client initiating + // the request allows a deferral response. + DeferralAllowed bool +} + // ModifyPlanRequest represents a request for the provider to modify the // planned new state that Terraform has generated for the resource. type ModifyPlanRequest struct { @@ -39,6 +46,9 @@ type ModifyPlanRequest struct { // Use the GetKey method to read data. Use the SetKey method on // ModifyPlanResponse.Private to update or remove a value. Private *privatestate.ProviderData + + //TODO: doc + ClientCapabilities *ModifyPlanClientCapabilities } // ModifyPlanResponse represents a response to a @@ -65,4 +75,11 @@ type ModifyPlanResponse struct { // indicates a successful plan modification with no warnings or errors // generated. Diagnostics diag.Diagnostics + + // DeferralResponse indicates that Terraform should defer + // importing this resource. + // + // This field can only be set if + // `(resource.ModifyPlanRequest.ModifyPlanClientCapabilities).DeferralAllowed` is true. + DeferralResponse *DeferralResponse } From 29cfc746fe7b7ed6095072b356bfcd15eb50e6b3 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 7 May 2024 15:18:51 -0400 Subject: [PATCH 04/44] Rename `DeferralReason` and `DeferralResponse` to `DeferredReason` and `DeferredResponse` respectively to match protobuf definition --- internal/fwserver/server_importresourcestate.go | 10 +++++----- .../fwserver/server_importresourcestate_test.go | 8 ++++---- internal/fwserver/server_planresourcechange.go | 8 ++++---- .../fwserver/server_planresourcechange_test.go | 8 ++++---- internal/fwserver/server_readresource.go | 8 ++++---- internal/fwserver/server_readresource_test.go | 10 +++++----- internal/toproto5/importresourcestate.go | 4 ++-- internal/toproto5/planresourcechange.go | 4 ++-- internal/toproto5/planresourcechange_test.go | 4 ++-- internal/toproto5/readresource.go | 4 ++-- internal/toproto5/readresource_test.go | 4 ++-- internal/toproto6/importresourcestate.go | 4 ++-- internal/toproto6/planresourcechange.go | 4 ++-- internal/toproto6/planresourcechange_test.go | 4 ++-- internal/toproto6/readresource.go | 4 ++-- internal/toproto6/readresource_test.go | 4 ++-- resource/deferral.go | 16 ++++++++-------- resource/import_state.go | 4 ++-- resource/modify_plan.go | 4 ++-- resource/read.go | 4 ++-- 20 files changed, 60 insertions(+), 60 deletions(-) diff --git a/internal/fwserver/server_importresourcestate.go b/internal/fwserver/server_importresourcestate.go index 65b1a0761..307ccd920 100644 --- a/internal/fwserver/server_importresourcestate.go +++ b/internal/fwserver/server_importresourcestate.go @@ -45,7 +45,7 @@ type ImportResourceStateRequest struct { type ImportResourceStateResponse struct { Diagnostics diag.Diagnostics ImportedResources []ImportedResource - Deferral *resource.DeferralResponse + Deferred *resource.DeferredResponse } // ImportResourceState implements the framework server ImportResourceState RPC. @@ -118,12 +118,12 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta return } - if (importReq.ClientCapabilities == nil || !importReq.ClientCapabilities.DeferralAllowed) && importResp.DeferralResponse != nil { + if (importReq.ClientCapabilities == nil || !importReq.ClientCapabilities.DeferralAllowed) && importResp.DeferredResponse != nil { resp.Diagnostics.AddError( "Resource Import Deferral Not Allowed", "An unexpected error was encountered when importing the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "resource.DeferralResponse can only be set if resource.ImportStateRequest.ImportStateClientCapabilities.DeferralAllowed is true.", + "resource.DeferredResponse can only be set if resource.ImportStateRequest.ImportStateClientCapabilities.DeferralAllowed is true.", ) return } @@ -143,8 +143,8 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta private.Provider = importResp.Private } - if importResp.DeferralResponse != nil { - resp.Deferral = importResp.DeferralResponse + if importResp.DeferredResponse != nil { + resp.Deferred = importResp.DeferredResponse } resp.ImportedResources = []ImportedResource{ diff --git a/internal/fwserver/server_importresourcestate_test.go b/internal/fwserver/server_importresourcestate_test.go index bffba4e59..e4bed808e 100644 --- a/internal/fwserver/server_importresourcestate_test.go +++ b/internal/fwserver/server_importresourcestate_test.go @@ -317,7 +317,7 @@ func TestServerImportResourceState(t *testing.T) { resp.Diagnostics.AddError("unexpected req.ID value: %s", req.ID) } - resp.DeferralResponse = &resource.DeferralResponse{ + resp.DeferredResponse = &resource.DeferredResponse{ Reason: resource.DeferralReasonAbsentPrereq, } @@ -336,7 +336,7 @@ func TestServerImportResourceState(t *testing.T) { Private: testEmptyPrivate, }, }, - Deferral: &resource.DeferralResponse{Reason: resource.DeferralReasonAbsentPrereq}, + Deferred: &resource.DeferredResponse{Reason: resource.DeferralReasonAbsentPrereq}, }, }, "request-deferral-not-allowed-response-deferral": { @@ -353,7 +353,7 @@ func TestServerImportResourceState(t *testing.T) { resp.Diagnostics.AddError("unexpected req.ID value: %s", req.ID) } - resp.DeferralResponse = &resource.DeferralResponse{ + resp.DeferredResponse = &resource.DeferredResponse{ Reason: resource.DeferralReasonAbsentPrereq, } @@ -369,7 +369,7 @@ func TestServerImportResourceState(t *testing.T) { "Resource Import Deferral Not Allowed", "An unexpected error was encountered when importing the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "resource.DeferralResponse can only be set if resource.ImportStateRequest.ImportStateClientCapabilities.DeferralAllowed is true.", + "resource.DeferredResponse can only be set if resource.ImportStateRequest.ImportStateClientCapabilities.DeferralAllowed is true.", ), }, }, diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index e261a3fcf..dc469d449 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -39,7 +39,7 @@ type PlanResourceChangeRequest struct { // PlanResourceChangeResponse is the framework server response for the // PlanResourceChange RPC. type PlanResourceChangeResponse struct { - Deferral *resource.DeferralResponse + Deferred *resource.DeferredResponse Diagnostics diag.Diagnostics PlannedPrivate *privatestate.Data PlannedState *tfsdk.State @@ -284,12 +284,12 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange resourceWithModifyPlan.ModifyPlan(ctx, modifyPlanReq, &modifyPlanResp) logging.FrameworkTrace(ctx, "Called provider defined Resource ModifyPlan") - if (modifyPlanReq.ClientCapabilities == nil || !modifyPlanReq.ClientCapabilities.DeferralAllowed) && modifyPlanResp.DeferralResponse != nil { + if (modifyPlanReq.ClientCapabilities == nil || !modifyPlanReq.ClientCapabilities.DeferralAllowed) && modifyPlanResp.DeferredResponse != nil { resp.Diagnostics.AddError( "Resource Deferral Not Allowed", "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(*resource.ModifyPlanResponse).DeferralResponse can only be set if (resource.ModifyPlanRequest).ClientCapabilities.DeferralAllowed is true.", + "(*resource.ModifyPlanResponse).DeferredResponse can only be set if (resource.ModifyPlanRequest).ClientCapabilities.DeferralAllowed is true.", ) return } @@ -298,7 +298,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange resp.PlannedState = planToState(modifyPlanResp.Plan) resp.RequiresReplace = append(resp.RequiresReplace, modifyPlanResp.RequiresReplace...) resp.PlannedPrivate.Provider = modifyPlanResp.Private - resp.Deferral = modifyPlanResp.DeferralResponse + resp.Deferred = modifyPlanResp.DeferredResponse } // Ensure deterministic RequiresReplace by sorting and deduplicating diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index aa62a2132..40b2b1509 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -6044,14 +6044,14 @@ func TestServerPlanResourceChange(t *testing.T) { Resource: &testprovider.ResourceWithModifyPlan{ ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { if req.ClientCapabilities.DeferralAllowed == true { - resp.DeferralResponse = &resource.DeferralResponse{Reason: resource.DeferralReasonAbsentPrereq} + resp.DeferredResponse = &resource.DeferredResponse{Reason: resource.DeferralReasonAbsentPrereq} } }, }, }, expectedResponse: &fwserver.PlanResourceChangeResponse{ - Deferral: &resource.DeferralResponse{Reason: resource.DeferralReasonAbsentPrereq}, + Deferred: &resource.DeferredResponse{Reason: resource.DeferralReasonAbsentPrereq}, PlannedState: &tfsdk.State{ Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), @@ -6085,7 +6085,7 @@ func TestServerPlanResourceChange(t *testing.T) { ResourceSchema: testSchema, Resource: &testprovider.ResourceWithModifyPlan{ ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { - resp.DeferralResponse = &resource.DeferralResponse{Reason: resource.DeferralReasonAbsentPrereq} + resp.DeferredResponse = &resource.DeferredResponse{Reason: resource.DeferralReasonAbsentPrereq} }, }, }, @@ -6095,7 +6095,7 @@ func TestServerPlanResourceChange(t *testing.T) { "Resource Deferral Not Allowed", "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(*resource.ModifyPlanResponse).DeferralResponse can only be set if (resource.ModifyPlanRequest).ClientCapabilities.DeferralAllowed is true.", + "(*resource.ModifyPlanResponse).DeferredResponse can only be set if (resource.ModifyPlanRequest).ClientCapabilities.DeferralAllowed is true.", ), }, PlannedState: &tfsdk.State{ diff --git a/internal/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index 8ce320c72..7b7831a9f 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -27,7 +27,7 @@ type ReadResourceRequest struct { // ReadResourceResponse is the framework server response for the // ReadResource RPC. type ReadResourceResponse struct { - Deferral *resource.DeferralResponse + Deferred *resource.DeferredResponse Diagnostics diag.Diagnostics NewState *tfsdk.State Private *privatestate.Data @@ -107,7 +107,7 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res resp.Diagnostics = readResp.Diagnostics resp.NewState = &readResp.State - resp.Deferral = readResp.DeferralResponse + resp.Deferred = readResp.DeferredResponse if readResp.Private != nil { if resp.Private == nil { @@ -121,12 +121,12 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res return } - if (req.ClientCapabilities == nil || !req.ClientCapabilities.DeferralAllowed) && resp.Deferral != nil { + if (req.ClientCapabilities == nil || !req.ClientCapabilities.DeferralAllowed) && resp.Deferred != nil { resp.Diagnostics.AddError( "Resource Deferral Not Allowed", "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "resource.DeferralResponse can only be set if resource.ReadRequest.ReadClientCapabilities.DeferralAllowed is true.", + "resource.DeferredResponse can only be set if resource.ReadRequest.ReadClientCapabilities.DeferralAllowed is true.", ) return } diff --git a/internal/fwserver/server_readresource_test.go b/internal/fwserver/server_readresource_test.go index f95f16511..e6164d12b 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -529,7 +529,7 @@ func TestServerReadResource(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - resp.DeferralResponse = &resource.DeferralResponse{Reason: resource.DeferralReasonAbsentPrereq} + resp.DeferredResponse = &resource.DeferredResponse{Reason: resource.DeferralReasonAbsentPrereq} if data.TestRequired.ValueString() != "test-currentstate-value" { resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) @@ -541,7 +541,7 @@ func TestServerReadResource(t *testing.T) { expectedResponse: &fwserver.ReadResourceResponse{ NewState: testCurrentState, Private: testEmptyPrivate, - Deferral: &resource.DeferralResponse{Reason: resource.DeferralReasonAbsentPrereq}, + Deferred: &resource.DeferredResponse{Reason: resource.DeferralReasonAbsentPrereq}, }, }, "request-deferral-not-allowed-response-deferral": { @@ -559,7 +559,7 @@ func TestServerReadResource(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - resp.DeferralResponse = &resource.DeferralResponse{Reason: resource.DeferralReasonAbsentPrereq} + resp.DeferredResponse = &resource.DeferredResponse{Reason: resource.DeferralReasonAbsentPrereq} if data.TestRequired.ValueString() != "test-currentstate-value" { resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) @@ -573,12 +573,12 @@ func TestServerReadResource(t *testing.T) { "Resource Deferral Not Allowed", "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "resource.DeferralResponse can only be set if resource.ReadRequest.ReadClientCapabilities.DeferralAllowed is true.", + "resource.DeferredResponse can only be set if resource.ReadRequest.ReadClientCapabilities.DeferralAllowed is true.", ), }, NewState: testCurrentState, Private: testEmptyPrivate, - Deferral: &resource.DeferralResponse{Reason: resource.DeferralReasonAbsentPrereq}, + Deferred: &resource.DeferredResponse{Reason: resource.DeferralReasonAbsentPrereq}, }, }, } diff --git a/internal/toproto5/importresourcestate.go b/internal/toproto5/importresourcestate.go index 49131f02f..92e438c42 100644 --- a/internal/toproto5/importresourcestate.go +++ b/internal/toproto5/importresourcestate.go @@ -34,9 +34,9 @@ func ImportResourceStateResponse(ctx context.Context, fw *fwserver.ImportResourc proto5.ImportedResources = append(proto5.ImportedResources, proto5ImportedResource) } - if fw.Deferral != nil { + if fw.Deferred != nil { proto5.Deferred = &tfprotov5.Deferred{ - Reason: tfprotov5.DeferredReason(fw.Deferral.Reason), + Reason: tfprotov5.DeferredReason(fw.Deferred.Reason), } } diff --git a/internal/toproto5/planresourcechange.go b/internal/toproto5/planresourcechange.go index b511c3870..6b2abcc75 100644 --- a/internal/toproto5/planresourcechange.go +++ b/internal/toproto5/planresourcechange.go @@ -38,9 +38,9 @@ func PlanResourceChangeResponse(ctx context.Context, fw *fwserver.PlanResourceCh proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.PlannedPrivate = plannedPrivate - if fw.Deferral != nil { + if fw.Deferred != nil { proto5.Deferred = &tfprotov5.Deferred{ - Reason: tfprotov5.DeferredReason(fw.Deferral.Reason), + Reason: tfprotov5.DeferredReason(fw.Deferred.Reason), } } diff --git a/internal/toproto5/planresourcechange_test.go b/internal/toproto5/planresourcechange_test.go index ca67cc258..72b707a69 100644 --- a/internal/toproto5/planresourcechange_test.go +++ b/internal/toproto5/planresourcechange_test.go @@ -70,7 +70,7 @@ func TestPlanResourceChangeResponse(t *testing.T) { testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) - testDeferral := &resource.DeferralResponse{ + testDeferred := &resource.DeferredResponse{ Reason: resource.DeferralReasonAbsentPrereq, } @@ -191,7 +191,7 @@ func TestPlanResourceChangeResponse(t *testing.T) { }, "deferral": { input: &fwserver.PlanResourceChangeResponse{ - Deferral: testDeferral, + Deferred: testDeferred, }, expected: &tfprotov5.PlanResourceChangeResponse{ Deferred: testProto5Deferred, diff --git a/internal/toproto5/readresource.go b/internal/toproto5/readresource.go index d60d2eab1..5f6ff7aef 100644 --- a/internal/toproto5/readresource.go +++ b/internal/toproto5/readresource.go @@ -32,9 +32,9 @@ func ReadResourceResponse(ctx context.Context, fw *fwserver.ReadResourceResponse proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.Private = newPrivate - if fw.Deferral != nil { + if fw.Deferred != nil { proto5.Deferred = &tfprotov5.Deferred{ - Reason: tfprotov5.DeferredReason(fw.Deferral.Reason), + Reason: tfprotov5.DeferredReason(fw.Deferred.Reason), } } diff --git a/internal/toproto5/readresource_test.go b/internal/toproto5/readresource_test.go index 9e66c7065..65865d76c 100644 --- a/internal/toproto5/readresource_test.go +++ b/internal/toproto5/readresource_test.go @@ -69,7 +69,7 @@ func TestReadResourceResponse(t *testing.T) { }, } - testDeferral := &resource.DeferralResponse{ + testDeferral := &resource.DeferredResponse{ Reason: resource.DeferralReasonAbsentPrereq, } @@ -178,7 +178,7 @@ func TestReadResourceResponse(t *testing.T) { }, "deferral": { input: &fwserver.ReadResourceResponse{ - Deferral: testDeferral, + Deferred: testDeferral, }, expected: &tfprotov5.ReadResourceResponse{ Deferred: testProto5Deferred, diff --git a/internal/toproto6/importresourcestate.go b/internal/toproto6/importresourcestate.go index ae003832f..447671c37 100644 --- a/internal/toproto6/importresourcestate.go +++ b/internal/toproto6/importresourcestate.go @@ -34,9 +34,9 @@ func ImportResourceStateResponse(ctx context.Context, fw *fwserver.ImportResourc proto6.ImportedResources = append(proto6.ImportedResources, proto6ImportedResource) } - if fw.Deferral != nil { + if fw.Deferred != nil { proto6.Deferred = &tfprotov6.Deferred{ - Reason: tfprotov6.DeferredReason(fw.Deferral.Reason), + Reason: tfprotov6.DeferredReason(fw.Deferred.Reason), } } diff --git a/internal/toproto6/planresourcechange.go b/internal/toproto6/planresourcechange.go index cc538dfec..d9499f979 100644 --- a/internal/toproto6/planresourcechange.go +++ b/internal/toproto6/planresourcechange.go @@ -38,9 +38,9 @@ func PlanResourceChangeResponse(ctx context.Context, fw *fwserver.PlanResourceCh proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.PlannedPrivate = plannedPrivate - if fw.Deferral != nil { + if fw.Deferred != nil { proto6.Deferred = &tfprotov6.Deferred{ - Reason: tfprotov6.DeferredReason(fw.Deferral.Reason), + Reason: tfprotov6.DeferredReason(fw.Deferred.Reason), } } diff --git a/internal/toproto6/planresourcechange_test.go b/internal/toproto6/planresourcechange_test.go index 5bd5e0963..50f17071a 100644 --- a/internal/toproto6/planresourcechange_test.go +++ b/internal/toproto6/planresourcechange_test.go @@ -70,7 +70,7 @@ func TestPlanResourceChangeResponse(t *testing.T) { testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) - testDeferral := &resource.DeferralResponse{ + testDeferred := &resource.DeferredResponse{ Reason: resource.DeferralReasonAbsentPrereq, } @@ -191,7 +191,7 @@ func TestPlanResourceChangeResponse(t *testing.T) { }, "deferral": { input: &fwserver.PlanResourceChangeResponse{ - Deferral: testDeferral, + Deferred: testDeferred, }, expected: &tfprotov6.PlanResourceChangeResponse{ Deferred: testProto6Deferred, diff --git a/internal/toproto6/readresource.go b/internal/toproto6/readresource.go index eb154d1d3..6f515fce9 100644 --- a/internal/toproto6/readresource.go +++ b/internal/toproto6/readresource.go @@ -32,9 +32,9 @@ func ReadResourceResponse(ctx context.Context, fw *fwserver.ReadResourceResponse proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.Private = newPrivate - if fw.Deferral != nil { + if fw.Deferred != nil { proto6.Deferred = &tfprotov6.Deferred{ - Reason: tfprotov6.DeferredReason(fw.Deferral.Reason), + Reason: tfprotov6.DeferredReason(fw.Deferred.Reason), } } diff --git a/internal/toproto6/readresource_test.go b/internal/toproto6/readresource_test.go index 2fa0b46f6..49f734269 100644 --- a/internal/toproto6/readresource_test.go +++ b/internal/toproto6/readresource_test.go @@ -69,7 +69,7 @@ func TestReadResourceResponse(t *testing.T) { testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) - testDeferral := &resource.DeferralResponse{ + testDeferral := &resource.DeferredResponse{ Reason: resource.DeferralReasonAbsentPrereq, } @@ -178,7 +178,7 @@ func TestReadResourceResponse(t *testing.T) { }, "deferral": { input: &fwserver.ReadResourceResponse{ - Deferral: testDeferral, + Deferred: testDeferral, }, expected: &tfprotov6.ReadResourceResponse{ Deferred: testProto6Deferred, diff --git a/resource/deferral.go b/resource/deferral.go index 1ce045222..640f31bee 100644 --- a/resource/deferral.go +++ b/resource/deferral.go @@ -1,19 +1,19 @@ package resource const ( - DeferralReasonUnknown DeferralReason = 0 - DeferralReasonResourceConfigUnknown DeferralReason = 1 - DeferralReasonProviderConfigUnknown DeferralReason = 2 - DeferralReasonAbsentPrereq DeferralReason = 3 + DeferralReasonUnknown DeferredReason = 0 + DeferralReasonResourceConfigUnknown DeferredReason = 1 + DeferralReasonProviderConfigUnknown DeferredReason = 2 + DeferralReasonAbsentPrereq DeferredReason = 3 ) -type DeferralResponse struct { - Reason DeferralReason +type DeferredResponse struct { + Reason DeferredReason } -type DeferralReason int32 +type DeferredReason int32 -func (d DeferralReason) String() string { +func (d DeferredReason) String() string { switch d { case 0: return "Unknown" diff --git a/resource/import_state.go b/resource/import_state.go index 7e9c0669a..658cc2bb7 100644 --- a/resource/import_state.go +++ b/resource/import_state.go @@ -55,12 +55,12 @@ type ImportStateResponse struct { // data during the resource's Import operation. Private *privatestate.ProviderData - // DeferralResponse indicates that Terraform should defer + // DeferredResponse indicates that Terraform should defer // importing this resource. // // This field can only be set if // `(resource.ImportStateRequest.ImportStateClientCapabilities).DeferralAllowed` is true. - DeferralResponse *DeferralResponse + DeferredResponse *DeferredResponse } // ImportStatePassthroughID is a helper function to set the import diff --git a/resource/modify_plan.go b/resource/modify_plan.go index 655f84f5d..6f3f563e4 100644 --- a/resource/modify_plan.go +++ b/resource/modify_plan.go @@ -76,10 +76,10 @@ type ModifyPlanResponse struct { // generated. Diagnostics diag.Diagnostics - // DeferralResponse indicates that Terraform should defer + // DeferredResponse indicates that Terraform should defer // importing this resource. // // This field can only be set if // `(resource.ModifyPlanRequest.ModifyPlanClientCapabilities).DeferralAllowed` is true. - DeferralResponse *DeferralResponse + DeferredResponse *DeferredResponse } diff --git a/resource/read.go b/resource/read.go index 0e714dfc1..df26d98a4 100644 --- a/resource/read.go +++ b/resource/read.go @@ -61,10 +61,10 @@ type ReadResponse struct { // warnings or errors generated. Diagnostics diag.Diagnostics - // DeferralResponse indicates that Terraform should defer + // DeferredResponse indicates that Terraform should defer // importing this resource. // // This field can only be set if // `(resource.ReadRequest.ReadStateClientCapabilities).DeferralAllowed` is true. - DeferralResponse *DeferralResponse + DeferredResponse *DeferredResponse } From 67ff59047dcafc0da6a2ba7d22231ae9fc5f85bc Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 7 May 2024 15:24:52 -0400 Subject: [PATCH 05/44] Update `terraform-plugin-go` dependency to `v0.23.0` --- go.mod | 4 ++-- go.sum | 4 ++++ internal/fromproto5/importresourcestate_test.go | 2 +- internal/fromproto5/planresourcechange_test.go | 2 +- internal/fromproto5/readresource_test.go | 2 +- internal/fromproto6/importresourcestate_test.go | 2 +- internal/fromproto6/planresourcechange_test.go | 2 +- internal/fromproto6/readresource_test.go | 2 +- 8 files changed, 12 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 377c56177..bbe8dfada 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.21.6 require ( github.com/google/go-cmp v0.6.0 - github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240424182422-dba624001d4f + github.com/hashicorp/terraform-plugin-go v0.23.0 github.com/hashicorp/terraform-plugin-log v0.9.0 ) @@ -30,5 +30,5 @@ require ( golang.org/x/text v0.14.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/grpc v1.63.2 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/protobuf v1.34.0 // indirect ) diff --git a/go.sum b/go.sum index 7380e78b6..8ae0ec93b 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240419152848-9a1607db1cab h github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240419152848-9a1607db1cab/go.mod h1:drq8Snexp9HsbFZddvyLHN6LuWHHndSQg+gV+FPkcIM= github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240424182422-dba624001d4f h1:r6EtFgZbSf+VeNrWqFwTFl5mKBdlFl+bpufwgaNBeQA= github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240424182422-dba624001d4f/go.mod h1:DbW1zoh21fsPD35whjnDA5aR7bXQV+k+lnkZrn8ZrFM= +github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co= +github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= @@ -65,6 +67,8 @@ google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= +google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/fromproto5/importresourcestate_test.go b/internal/fromproto5/importresourcestate_test.go index 00013ced3..f3de43bcf 100644 --- a/internal/fromproto5/importresourcestate_test.go +++ b/internal/fromproto5/importresourcestate_test.go @@ -90,7 +90,7 @@ func TestImportResourceStateRequest(t *testing.T) { "client-capabilities": { input: &tfprotov5.ImportResourceStateRequest{ ID: "test-id", - ClientCapabilities: &tfprotov5.ClientCapabilities{ + ClientCapabilities: &tfprotov5.ImportResourceStateClientCapabilities{ DeferralAllowed: true, }, }, diff --git a/internal/fromproto5/planresourcechange_test.go b/internal/fromproto5/planresourcechange_test.go index f134a807e..e629b5d38 100644 --- a/internal/fromproto5/planresourcechange_test.go +++ b/internal/fromproto5/planresourcechange_test.go @@ -54,7 +54,7 @@ func TestPlanResourceChangeRequest(t *testing.T) { testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) - testClientCapabilities := tfprotov5.ClientCapabilities{DeferralAllowed: true} + testClientCapabilities := tfprotov5.PlanResourceChangeClientCapabilities{DeferralAllowed: true} testCases := map[string]struct { input *tfprotov5.PlanResourceChangeRequest diff --git a/internal/fromproto5/readresource_test.go b/internal/fromproto5/readresource_test.go index 7a0c9534b..28aadec15 100644 --- a/internal/fromproto5/readresource_test.go +++ b/internal/fromproto5/readresource_test.go @@ -56,7 +56,7 @@ func TestReadResourceRequest(t *testing.T) { testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) - testClientCapabilities := tfprotov5.ClientCapabilities{DeferralAllowed: true} + testClientCapabilities := tfprotov5.ReadResourceClientCapabilities{DeferralAllowed: true} testCases := map[string]struct { input *tfprotov5.ReadResourceRequest diff --git a/internal/fromproto6/importresourcestate_test.go b/internal/fromproto6/importresourcestate_test.go index 6ef29c77e..fb11a340d 100644 --- a/internal/fromproto6/importresourcestate_test.go +++ b/internal/fromproto6/importresourcestate_test.go @@ -90,7 +90,7 @@ func TestImportResourceStateRequest(t *testing.T) { "client-capabilities": { input: &tfprotov6.ImportResourceStateRequest{ ID: "test-id", - ClientCapabilities: &tfprotov6.ClientCapabilities{ + ClientCapabilities: &tfprotov6.ImportResourceStateClientCapabilities{ DeferralAllowed: true, }, }, diff --git a/internal/fromproto6/planresourcechange_test.go b/internal/fromproto6/planresourcechange_test.go index 332d7c473..5b96bbe79 100644 --- a/internal/fromproto6/planresourcechange_test.go +++ b/internal/fromproto6/planresourcechange_test.go @@ -54,7 +54,7 @@ func TestPlanResourceChangeRequest(t *testing.T) { testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) - testClientCapabilities := tfprotov6.ClientCapabilities{DeferralAllowed: true} + testClientCapabilities := tfprotov6.PlanResourceChangeClientCapabilities{DeferralAllowed: true} testCases := map[string]struct { input *tfprotov6.PlanResourceChangeRequest diff --git a/internal/fromproto6/readresource_test.go b/internal/fromproto6/readresource_test.go index f3f61a017..a6a238188 100644 --- a/internal/fromproto6/readresource_test.go +++ b/internal/fromproto6/readresource_test.go @@ -56,7 +56,7 @@ func TestReadResourceRequest(t *testing.T) { testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) - testClientCapabilities := tfprotov6.ClientCapabilities{DeferralAllowed: true} + testClientCapabilities := tfprotov6.ReadResourceClientCapabilities{DeferralAllowed: true} testCases := map[string]struct { input *tfprotov6.ReadResourceRequest From 4a1140873d5eafbec4395a2f10a8f513db969bac Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 7 May 2024 15:56:44 -0400 Subject: [PATCH 06/44] Rename `deferral.go` to `deferred.go` --- .../server_importresourcestate_test.go | 6 +-- .../server_planresourcechange_test.go | 6 +-- internal/fwserver/server_readresource_test.go | 8 ++-- internal/toproto5/planresourcechange_test.go | 2 +- internal/toproto5/readresource_test.go | 2 +- internal/toproto6/planresourcechange_test.go | 2 +- internal/toproto6/readresource_test.go | 2 +- resource/deferral.go | 28 ------------- resource/deferred.go | 41 +++++++++++++++++++ 9 files changed, 55 insertions(+), 42 deletions(-) delete mode 100644 resource/deferral.go create mode 100644 resource/deferred.go diff --git a/internal/fwserver/server_importresourcestate_test.go b/internal/fwserver/server_importresourcestate_test.go index e4bed808e..492b0ba3c 100644 --- a/internal/fwserver/server_importresourcestate_test.go +++ b/internal/fwserver/server_importresourcestate_test.go @@ -318,7 +318,7 @@ func TestServerImportResourceState(t *testing.T) { } resp.DeferredResponse = &resource.DeferredResponse{ - Reason: resource.DeferralReasonAbsentPrereq, + Reason: resource.DeferredReasonAbsentPrereq, } resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) @@ -336,7 +336,7 @@ func TestServerImportResourceState(t *testing.T) { Private: testEmptyPrivate, }, }, - Deferred: &resource.DeferredResponse{Reason: resource.DeferralReasonAbsentPrereq}, + Deferred: &resource.DeferredResponse{Reason: resource.DeferredReasonAbsentPrereq}, }, }, "request-deferral-not-allowed-response-deferral": { @@ -354,7 +354,7 @@ func TestServerImportResourceState(t *testing.T) { } resp.DeferredResponse = &resource.DeferredResponse{ - Reason: resource.DeferralReasonAbsentPrereq, + Reason: resource.DeferredReasonAbsentPrereq, } resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index 40b2b1509..78d6b050e 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -6044,14 +6044,14 @@ func TestServerPlanResourceChange(t *testing.T) { Resource: &testprovider.ResourceWithModifyPlan{ ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { if req.ClientCapabilities.DeferralAllowed == true { - resp.DeferredResponse = &resource.DeferredResponse{Reason: resource.DeferralReasonAbsentPrereq} + resp.DeferredResponse = &resource.DeferredResponse{Reason: resource.DeferredReasonAbsentPrereq} } }, }, }, expectedResponse: &fwserver.PlanResourceChangeResponse{ - Deferred: &resource.DeferredResponse{Reason: resource.DeferralReasonAbsentPrereq}, + Deferred: &resource.DeferredResponse{Reason: resource.DeferredReasonAbsentPrereq}, PlannedState: &tfsdk.State{ Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), @@ -6085,7 +6085,7 @@ func TestServerPlanResourceChange(t *testing.T) { ResourceSchema: testSchema, Resource: &testprovider.ResourceWithModifyPlan{ ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { - resp.DeferredResponse = &resource.DeferredResponse{Reason: resource.DeferralReasonAbsentPrereq} + resp.DeferredResponse = &resource.DeferredResponse{Reason: resource.DeferredReasonAbsentPrereq} }, }, }, diff --git a/internal/fwserver/server_readresource_test.go b/internal/fwserver/server_readresource_test.go index e6164d12b..d7f4e9e9d 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -529,7 +529,7 @@ func TestServerReadResource(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - resp.DeferredResponse = &resource.DeferredResponse{Reason: resource.DeferralReasonAbsentPrereq} + resp.DeferredResponse = &resource.DeferredResponse{Reason: resource.DeferredReasonAbsentPrereq} if data.TestRequired.ValueString() != "test-currentstate-value" { resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) @@ -541,7 +541,7 @@ func TestServerReadResource(t *testing.T) { expectedResponse: &fwserver.ReadResourceResponse{ NewState: testCurrentState, Private: testEmptyPrivate, - Deferred: &resource.DeferredResponse{Reason: resource.DeferralReasonAbsentPrereq}, + Deferred: &resource.DeferredResponse{Reason: resource.DeferredReasonAbsentPrereq}, }, }, "request-deferral-not-allowed-response-deferral": { @@ -559,7 +559,7 @@ func TestServerReadResource(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - resp.DeferredResponse = &resource.DeferredResponse{Reason: resource.DeferralReasonAbsentPrereq} + resp.DeferredResponse = &resource.DeferredResponse{Reason: resource.DeferredReasonAbsentPrereq} if data.TestRequired.ValueString() != "test-currentstate-value" { resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) @@ -578,7 +578,7 @@ func TestServerReadResource(t *testing.T) { }, NewState: testCurrentState, Private: testEmptyPrivate, - Deferred: &resource.DeferredResponse{Reason: resource.DeferralReasonAbsentPrereq}, + Deferred: &resource.DeferredResponse{Reason: resource.DeferredReasonAbsentPrereq}, }, }, } diff --git a/internal/toproto5/planresourcechange_test.go b/internal/toproto5/planresourcechange_test.go index 72b707a69..bf0ddfff4 100644 --- a/internal/toproto5/planresourcechange_test.go +++ b/internal/toproto5/planresourcechange_test.go @@ -71,7 +71,7 @@ func TestPlanResourceChangeResponse(t *testing.T) { testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) testDeferred := &resource.DeferredResponse{ - Reason: resource.DeferralReasonAbsentPrereq, + Reason: resource.DeferredReasonAbsentPrereq, } testProto5Deferred := &tfprotov5.Deferred{ diff --git a/internal/toproto5/readresource_test.go b/internal/toproto5/readresource_test.go index 65865d76c..f9b3dce77 100644 --- a/internal/toproto5/readresource_test.go +++ b/internal/toproto5/readresource_test.go @@ -70,7 +70,7 @@ func TestReadResourceResponse(t *testing.T) { } testDeferral := &resource.DeferredResponse{ - Reason: resource.DeferralReasonAbsentPrereq, + Reason: resource.DeferredReasonAbsentPrereq, } testProto5Deferred := &tfprotov5.Deferred{ diff --git a/internal/toproto6/planresourcechange_test.go b/internal/toproto6/planresourcechange_test.go index 50f17071a..d94f9c148 100644 --- a/internal/toproto6/planresourcechange_test.go +++ b/internal/toproto6/planresourcechange_test.go @@ -71,7 +71,7 @@ func TestPlanResourceChangeResponse(t *testing.T) { testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) testDeferred := &resource.DeferredResponse{ - Reason: resource.DeferralReasonAbsentPrereq, + Reason: resource.DeferredReasonAbsentPrereq, } testProto6Deferred := &tfprotov6.Deferred{ diff --git a/internal/toproto6/readresource_test.go b/internal/toproto6/readresource_test.go index 49f734269..cfd856c7b 100644 --- a/internal/toproto6/readresource_test.go +++ b/internal/toproto6/readresource_test.go @@ -70,7 +70,7 @@ func TestReadResourceResponse(t *testing.T) { testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) testDeferral := &resource.DeferredResponse{ - Reason: resource.DeferralReasonAbsentPrereq, + Reason: resource.DeferredReasonAbsentPrereq, } testProto6Deferred := &tfprotov6.Deferred{ diff --git a/resource/deferral.go b/resource/deferral.go deleted file mode 100644 index 640f31bee..000000000 --- a/resource/deferral.go +++ /dev/null @@ -1,28 +0,0 @@ -package resource - -const ( - DeferralReasonUnknown DeferredReason = 0 - DeferralReasonResourceConfigUnknown DeferredReason = 1 - DeferralReasonProviderConfigUnknown DeferredReason = 2 - DeferralReasonAbsentPrereq DeferredReason = 3 -) - -type DeferredResponse struct { - Reason DeferredReason -} - -type DeferredReason int32 - -func (d DeferredReason) String() string { - switch d { - case 0: - return "Unknown" - case 1: - return "Resource Config Unknown" - case 2: - return "Provider Config Unknown" - case 3: - return "Absent Prerequisite" - } - return "Unknown" -} diff --git a/resource/deferred.go b/resource/deferred.go new file mode 100644 index 000000000..f677e7b86 --- /dev/null +++ b/resource/deferred.go @@ -0,0 +1,41 @@ +package resource + +const ( + // DeferredReasonUnknown is used to indicate an invalid `DeferredReason`. + // Provider developers should not use it. + DeferredReasonUnknown DeferredReason = 0 + + // DeferredReasonResourceConfigUnknown is used to indicate that the resource configuration + // is partially unknown and the real values need to be known before the change can be planned. + DeferredReasonResourceConfigUnknown DeferredReason = 1 + + // DeferredReasonProviderConfigUnknown is used to indicate that the provider configuration + // is partially unknown and the real values need to be known before the change can be planned. + DeferredReasonProviderConfigUnknown DeferredReason = 2 + + // DeferredReasonAbsentPrereq is used to indicate that a hard dependency has not been satisfied. + DeferredReasonAbsentPrereq DeferredReason = 3 +) + +// DeferredResponse is used to indicate to Terraform that a change needs to be deferred for a reason. +type DeferredResponse struct { + // Reason is the reason for deferring the change. + Reason DeferredReason +} + +// DeferredReason represents different reasons for deferring a change. +type DeferredReason int32 + +func (d DeferredReason) String() string { + switch d { + case 0: + return "Unknown" + case 1: + return "Resource Config Unknown" + case 2: + return "Provider Config Unknown" + case 3: + return "Absent Prerequisite" + } + return "Unknown" +} From ce1d14b63c9f3d2c1ad12a8e2da86550965192c6 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 7 May 2024 16:40:19 -0400 Subject: [PATCH 07/44] Implement manual deferred action support for data sources --- datasource/deferred.go | 41 ++++++++++ datasource/read.go | 20 +++++ internal/fromproto5/readdatasource.go | 9 ++- internal/fromproto5/readdatasource_test.go | 20 ++++- internal/fromproto6/readdatasource.go | 9 ++- internal/fromproto6/readdatasource_test.go | 19 ++++- internal/fwserver/server_readdatasource.go | 23 +++++- .../fwserver/server_readdatasource_test.go | 74 ++++++++++++++++++- internal/toproto5/readdatasource.go | 9 ++- internal/toproto5/readdatasource_test.go | 22 +++++- internal/toproto6/readdatasource.go | 9 ++- internal/toproto6/readdatasource_test.go | 22 +++++- resource/read.go | 2 +- 13 files changed, 261 insertions(+), 18 deletions(-) create mode 100644 datasource/deferred.go diff --git a/datasource/deferred.go b/datasource/deferred.go new file mode 100644 index 000000000..8e5f275ba --- /dev/null +++ b/datasource/deferred.go @@ -0,0 +1,41 @@ +package datasource + +const ( + // DeferredReasonUnknown is used to indicate an invalid `DeferredReason`. + // Provider developers should not use it. + DeferredReasonUnknown DeferredReason = 0 + + // DeferredReasonResourceConfigUnknown is used to indicate that the resource configuration + // is partially unknown and the real values need to be known before the change can be planned. + DeferredReasonResourceConfigUnknown DeferredReason = 1 + + // DeferredReasonProviderConfigUnknown is used to indicate that the provider configuration + // is partially unknown and the real values need to be known before the change can be planned. + DeferredReasonProviderConfigUnknown DeferredReason = 2 + + // DeferredReasonAbsentPrereq is used to indicate that a hard dependency has not been satisfied. + DeferredReasonAbsentPrereq DeferredReason = 3 +) + +// DeferredResponse is used to indicate to Terraform that a change needs to be deferred for a reason. +type DeferredResponse struct { + // Reason is the reason for deferring the change. + Reason DeferredReason +} + +// DeferredReason represents different reasons for deferring a change. +type DeferredReason int32 + +func (d DeferredReason) String() string { + switch d { + case 0: + return "Unknown" + case 1: + return "Resource Config Unknown" + case 2: + return "Provider Config Unknown" + case 3: + return "Absent Prerequisite" + } + return "Unknown" +} diff --git a/datasource/read.go b/datasource/read.go index 07e28cab1..6a84cae4b 100644 --- a/datasource/read.go +++ b/datasource/read.go @@ -8,6 +8,15 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) +// ReadClientCapabilities allows Terraform to publish information +// regarding optionally supported protocol features for the ReadDataSource RPC, +// such as forward-compatible Terraform behavior changes. +type ReadClientCapabilities struct { + // DeferralAllowed signals that the request from Terraform is able to + // handle deferred responses from the provider. + DeferralAllowed bool +} + // ReadRequest represents a request for the provider to read a data // source, i.e., update values in state according to the real state of the // data source. An instance of this request struct is supplied as an argument @@ -22,6 +31,10 @@ type ReadRequest struct { // ProviderMeta is metadata from the provider_meta block of the module. ProviderMeta tfsdk.Config + + // ClientCapabilities defines optionally supported protocol features for the + // ReadDataSource RPC, such as forward-compatible Terraform behavior changes. + ClientCapabilities *ReadClientCapabilities } // ReadResponse represents a response to a ReadRequest. An @@ -37,4 +50,11 @@ type ReadResponse struct { // source. An empty slice indicates a successful operation with no // warnings or errors generated. Diagnostics diag.Diagnostics + + // DeferredResponse indicates that Terraform should defer + // importing this resource. + // + // This field can only be set if + // `(datasource.ReadRequest.ReadClientCapabilities).DeferralAllowed` is true. + DeferredResponse *DeferredResponse } diff --git a/internal/fromproto5/readdatasource.go b/internal/fromproto5/readdatasource.go index 53ac543a9..105ffad67 100644 --- a/internal/fromproto5/readdatasource.go +++ b/internal/fromproto5/readdatasource.go @@ -6,11 +6,12 @@ package fromproto5 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) // ReadDataSourceRequest returns the *fwserver.ReadDataSourceRequest @@ -53,5 +54,11 @@ func ReadDataSourceRequest(ctx context.Context, proto5 *tfprotov5.ReadDataSource fw.ProviderMeta = providerMeta + if proto5.ClientCapabilities != nil { + fw.ClientCapabilities = &datasource.ReadClientCapabilities{ + DeferralAllowed: proto5.ClientCapabilities.DeferralAllowed, + } + } + return fw, diags } diff --git a/internal/fromproto5/readdatasource_test.go b/internal/fromproto5/readdatasource_test.go index 79df02ea8..b5bdfca53 100644 --- a/internal/fromproto5/readdatasource_test.go +++ b/internal/fromproto5/readdatasource_test.go @@ -1,4 +1,5 @@ // Copyright (c) HashiCorp, Inc. + // SPDX-License-Identifier: MPL-2.0 package fromproto5_test @@ -8,6 +9,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -15,8 +19,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestReadDataSourceRequest(t *testing.T) { @@ -46,6 +48,8 @@ func TestReadDataSourceRequest(t *testing.T) { }, } + testClientCapabilities := tfprotov5.ReadDataSourceClientCapabilities{DeferralAllowed: true} + testCases := map[string]struct { input *tfprotov5.ReadDataSourceRequest dataSourceSchema fwschema.Schema @@ -135,6 +139,18 @@ func TestReadDataSourceRequest(t *testing.T) { }, }, }, + "client-capabilities": { + input: &tfprotov5.ReadDataSourceRequest{ + ClientCapabilities: &testClientCapabilities, + }, + dataSourceSchema: testFwSchema, + expected: &fwserver.ReadDataSourceRequest{ + DataSourceSchema: testFwSchema, + ClientCapabilities: &datasource.ReadClientCapabilities{ + DeferralAllowed: true, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fromproto6/readdatasource.go b/internal/fromproto6/readdatasource.go index 83c264cd7..edd540688 100644 --- a/internal/fromproto6/readdatasource.go +++ b/internal/fromproto6/readdatasource.go @@ -6,11 +6,12 @@ package fromproto6 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) // ReadDataSourceRequest returns the *fwserver.ReadDataSourceRequest @@ -53,5 +54,11 @@ func ReadDataSourceRequest(ctx context.Context, proto6 *tfprotov6.ReadDataSource fw.ProviderMeta = providerMeta + if proto6.ClientCapabilities != nil { + fw.ClientCapabilities = &datasource.ReadClientCapabilities{ + DeferralAllowed: proto6.ClientCapabilities.DeferralAllowed, + } + } + return fw, diags } diff --git a/internal/fromproto6/readdatasource_test.go b/internal/fromproto6/readdatasource_test.go index b3f996717..73332b45a 100644 --- a/internal/fromproto6/readdatasource_test.go +++ b/internal/fromproto6/readdatasource_test.go @@ -8,6 +8,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -15,8 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestReadDataSourceRequest(t *testing.T) { @@ -46,6 +47,8 @@ func TestReadDataSourceRequest(t *testing.T) { }, } + testClientCapabilities := tfprotov6.ReadDataSourceClientCapabilities{DeferralAllowed: true} + testCases := map[string]struct { input *tfprotov6.ReadDataSourceRequest dataSourceSchema fwschema.Schema @@ -135,6 +138,18 @@ func TestReadDataSourceRequest(t *testing.T) { }, }, }, + "client-capabilities": { + input: &tfprotov6.ReadDataSourceRequest{ + ClientCapabilities: &testClientCapabilities, + }, + dataSourceSchema: testFwSchema, + expected: &fwserver.ReadDataSourceRequest{ + DataSourceSchema: testFwSchema, + ClientCapabilities: &datasource.ReadClientCapabilities{ + DeferralAllowed: true, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fwserver/server_readdatasource.go b/internal/fwserver/server_readdatasource.go index a95cd35dc..b08d3caba 100644 --- a/internal/fwserver/server_readdatasource.go +++ b/internal/fwserver/server_readdatasource.go @@ -17,15 +17,17 @@ import ( // ReadDataSourceRequest is the framework server request for the // ReadDataSource RPC. type ReadDataSourceRequest struct { - Config *tfsdk.Config - DataSourceSchema fwschema.Schema - DataSource datasource.DataSource - ProviderMeta *tfsdk.Config + ClientCapabilities *datasource.ReadClientCapabilities + Config *tfsdk.Config + DataSourceSchema fwschema.Schema + DataSource datasource.DataSource + ProviderMeta *tfsdk.Config } // ReadDataSourceResponse is the framework server response for the // ReadDataSource RPC. type ReadDataSourceResponse struct { + Deferred *datasource.DeferredResponse Diagnostics diag.Diagnostics State *tfsdk.State } @@ -75,17 +77,30 @@ func (s *Server) ReadDataSource(ctx context.Context, req *ReadDataSourceRequest, readReq.ProviderMeta = *req.ProviderMeta } + readReq.ClientCapabilities = req.ClientCapabilities + logging.FrameworkTrace(ctx, "Calling provider defined DataSource Read") req.DataSource.Read(ctx, readReq, &readResp) logging.FrameworkTrace(ctx, "Called provider defined DataSource Read") resp.Diagnostics = readResp.Diagnostics resp.State = &readResp.State + resp.Deferred = readResp.DeferredResponse if resp.Diagnostics.HasError() { return } + if (req.ClientCapabilities == nil || !req.ClientCapabilities.DeferralAllowed) && resp.Deferred != nil { + resp.Diagnostics.AddError( + "Data Source Deferral Not Allowed", + "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ + "The resource requested a deferral but the Terraform client does not support deferrals, "+ + "datasource.DeferredResponse can only be set if datasource.ReadRequest.ReadClientCapabilities.DeferralAllowed is true.", + ) + return + } + semanticEqualityReq := SchemaSemanticEqualityRequest{ PriorData: fwschemadata.Data{ Description: fwschemadata.DataDescriptionConfiguration, diff --git a/internal/fwserver/server_readdatasource_test.go b/internal/fwserver/server_readdatasource_test.go index d831ef212..663f5ea77 100644 --- a/internal/fwserver/server_readdatasource_test.go +++ b/internal/fwserver/server_readdatasource_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -17,7 +19,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerReadDataSource(t *testing.T) { @@ -98,6 +99,10 @@ func TestServerReadDataSource(t *testing.T) { Schema: testSchema, } + testDeferralAllowed := &datasource.ReadClientCapabilities{ + DeferralAllowed: true, + } + testCases := map[string]struct { server *fwserver.Server request *fwserver.ReadDataSourceRequest @@ -349,6 +354,73 @@ func TestServerReadDataSource(t *testing.T) { }, }, }, + "request-deferral-allowed-response-deferral": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadDataSourceRequest{ + Config: testConfig, + DataSourceSchema: testSchema, + DataSource: &testprovider.DataSource{ + ReadMethod: func(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + resp.DeferredResponse = &datasource.DeferredResponse{Reason: datasource.DeferredReasonAbsentPrereq} + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + }, + ClientCapabilities: testDeferralAllowed, + }, + expectedResponse: &fwserver.ReadDataSourceResponse{ + State: testStateUnchanged, + Deferred: &datasource.DeferredResponse{Reason: datasource.DeferredReasonAbsentPrereq}, + }, + }, + "request-deferral-not-allowed-response-deferral": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadDataSourceRequest{ + Config: testConfig, + DataSourceSchema: testSchema, + DataSource: &testprovider.DataSource{ + ReadMethod: func(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + resp.DeferredResponse = &datasource.DeferredResponse{Reason: datasource.DeferredReasonAbsentPrereq} + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + }, + }, + expectedResponse: &fwserver.ReadDataSourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Data Source Deferral Not Allowed", + "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ + "The resource requested a deferral but the Terraform client does not support deferrals, "+ + "datasource.DeferredResponse can only be set if datasource.ReadRequest.ReadClientCapabilities.DeferralAllowed is true.", + ), + }, + State: testStateUnchanged, + Deferred: &datasource.DeferredResponse{Reason: datasource.DeferredReasonAbsentPrereq}, + }, + }, } for name, testCase := range testCases { diff --git a/internal/toproto5/readdatasource.go b/internal/toproto5/readdatasource.go index 4e977d1dd..f70e6e186 100644 --- a/internal/toproto5/readdatasource.go +++ b/internal/toproto5/readdatasource.go @@ -6,8 +6,9 @@ package toproto5 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" ) // ReadDataSourceResponse returns the *tfprotov5.ReadDataSourceResponse @@ -26,5 +27,11 @@ func ReadDataSourceResponse(ctx context.Context, fw *fwserver.ReadDataSourceResp proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.State = state + if fw.Deferred != nil { + proto5.Deferred = &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(fw.Deferred.Reason), + } + } + return proto5 } diff --git a/internal/toproto5/readdatasource_test.go b/internal/toproto5/readdatasource_test.go index 691e2e896..40e42d131 100644 --- a/internal/toproto5/readdatasource_test.go +++ b/internal/toproto5/readdatasource_test.go @@ -8,13 +8,15 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestReadDataSourceResponse(t *testing.T) { @@ -32,6 +34,14 @@ func TestReadDataSourceResponse(t *testing.T) { testProto5DynamicValue, err := tfprotov5.NewDynamicValue(testProto5Type, testProto5Value) + testDeferral := &datasource.DeferredResponse{ + Reason: datasource.DeferredReasonAbsentPrereq, + } + + testProto5Deferred := &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReasonAbsentPrereq, + } + if err != nil { t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) } @@ -131,6 +141,14 @@ func TestReadDataSourceResponse(t *testing.T) { State: &testProto5DynamicValue, }, }, + "deferral": { + input: &fwserver.ReadDataSourceResponse{ + Deferred: testDeferral, + }, + expected: &tfprotov5.ReadDataSourceResponse{ + Deferred: testProto5Deferred, + }, + }, } for name, testCase := range testCases { diff --git a/internal/toproto6/readdatasource.go b/internal/toproto6/readdatasource.go index f9a59f0e5..0f1738625 100644 --- a/internal/toproto6/readdatasource.go +++ b/internal/toproto6/readdatasource.go @@ -6,8 +6,9 @@ package toproto6 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" ) // ReadDataSourceResponse returns the *tfprotov6.ReadDataSourceResponse @@ -26,5 +27,11 @@ func ReadDataSourceResponse(ctx context.Context, fw *fwserver.ReadDataSourceResp proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.State = state + if fw.Deferred != nil { + proto6.Deferred = &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReason(fw.Deferred.Reason), + } + } + return proto6 } diff --git a/internal/toproto6/readdatasource_test.go b/internal/toproto6/readdatasource_test.go index f143ba56d..fb1ae4dd5 100644 --- a/internal/toproto6/readdatasource_test.go +++ b/internal/toproto6/readdatasource_test.go @@ -8,13 +8,15 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestReadDataSourceResponse(t *testing.T) { @@ -32,6 +34,14 @@ func TestReadDataSourceResponse(t *testing.T) { testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value) + testDeferral := &datasource.DeferredResponse{ + Reason: datasource.DeferredReasonAbsentPrereq, + } + + testProto6Deferred := &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReasonAbsentPrereq, + } + if err != nil { t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) } @@ -131,6 +141,14 @@ func TestReadDataSourceResponse(t *testing.T) { State: &testProto6DynamicValue, }, }, + "deferral": { + input: &fwserver.ReadDataSourceResponse{ + Deferred: testDeferral, + }, + expected: &tfprotov6.ReadDataSourceResponse{ + Deferred: testProto6Deferred, + }, + }, } for name, testCase := range testCases { diff --git a/resource/read.go b/resource/read.go index df26d98a4..2255e06aa 100644 --- a/resource/read.go +++ b/resource/read.go @@ -65,6 +65,6 @@ type ReadResponse struct { // importing this resource. // // This field can only be set if - // `(resource.ReadRequest.ReadStateClientCapabilities).DeferralAllowed` is true. + // `(resource.ReadRequest.ReadClientCapabilities).DeferralAllowed` is true. DeferredResponse *DeferredResponse } From 39c4bb5c58a176a2b2f54ea1423933ceb6d9ea37 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 7 May 2024 17:05:08 -0400 Subject: [PATCH 08/44] Update documentation and diagnostic messages --- datasource/read.go | 4 ++-- internal/fwserver/server_importresourcestate.go | 2 +- internal/fwserver/server_importresourcestate_test.go | 2 +- internal/fwserver/server_planresourcechange.go | 2 +- internal/fwserver/server_planresourcechange_test.go | 2 +- internal/fwserver/server_readdatasource.go | 2 +- internal/fwserver/server_readdatasource_test.go | 2 +- internal/fwserver/server_readresource.go | 2 +- internal/fwserver/server_readresource_test.go | 2 +- resource/import_state.go | 7 +++++-- resource/modify_plan.go | 7 +++++-- resource/read.go | 7 +++++-- 12 files changed, 25 insertions(+), 16 deletions(-) diff --git a/datasource/read.go b/datasource/read.go index 6a84cae4b..12ac76c1f 100644 --- a/datasource/read.go +++ b/datasource/read.go @@ -12,8 +12,8 @@ import ( // regarding optionally supported protocol features for the ReadDataSource RPC, // such as forward-compatible Terraform behavior changes. type ReadClientCapabilities struct { - // DeferralAllowed signals that the request from Terraform is able to - // handle deferred responses from the provider. + // DeferralAllowed indicates whether the Terraform client initiating + // the request allows a deferral response. DeferralAllowed bool } diff --git a/internal/fwserver/server_importresourcestate.go b/internal/fwserver/server_importresourcestate.go index 307ccd920..211060b01 100644 --- a/internal/fwserver/server_importresourcestate.go +++ b/internal/fwserver/server_importresourcestate.go @@ -123,7 +123,7 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta "Resource Import Deferral Not Allowed", "An unexpected error was encountered when importing the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "resource.DeferredResponse can only be set if resource.ImportStateRequest.ImportStateClientCapabilities.DeferralAllowed is true.", + "(resource.ImportStateResponse).DeferredResponse can only be set if (resource.ImportStateRequest.ClientCapabilities).DeferralAllowed is true.", ) return } diff --git a/internal/fwserver/server_importresourcestate_test.go b/internal/fwserver/server_importresourcestate_test.go index 492b0ba3c..7b0177837 100644 --- a/internal/fwserver/server_importresourcestate_test.go +++ b/internal/fwserver/server_importresourcestate_test.go @@ -369,7 +369,7 @@ func TestServerImportResourceState(t *testing.T) { "Resource Import Deferral Not Allowed", "An unexpected error was encountered when importing the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "resource.DeferredResponse can only be set if resource.ImportStateRequest.ImportStateClientCapabilities.DeferralAllowed is true.", + "(resource.ImportStateResponse).DeferredResponse can only be set if (resource.ImportStateRequest.ClientCapabilities).DeferralAllowed is true.", ), }, }, diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index dc469d449..f1f2b1413 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -289,7 +289,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange "Resource Deferral Not Allowed", "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(*resource.ModifyPlanResponse).DeferredResponse can only be set if (resource.ModifyPlanRequest).ClientCapabilities.DeferralAllowed is true.", + "(resource.ModifyPlanResponse).DeferredResponse can only be set if (resource.ModifyPlanRequest.ClientCapabilities.DeferralAllowed is true.", ) return } diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index 78d6b050e..2d24da5af 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -6095,7 +6095,7 @@ func TestServerPlanResourceChange(t *testing.T) { "Resource Deferral Not Allowed", "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(*resource.ModifyPlanResponse).DeferredResponse can only be set if (resource.ModifyPlanRequest).ClientCapabilities.DeferralAllowed is true.", + "(resource.ModifyPlanResponse).DeferredResponse can only be set if (resource.ModifyPlanRequest.ClientCapabilities.DeferralAllowed is true.", ), }, PlannedState: &tfsdk.State{ diff --git a/internal/fwserver/server_readdatasource.go b/internal/fwserver/server_readdatasource.go index b08d3caba..15693e42d 100644 --- a/internal/fwserver/server_readdatasource.go +++ b/internal/fwserver/server_readdatasource.go @@ -96,7 +96,7 @@ func (s *Server) ReadDataSource(ctx context.Context, req *ReadDataSourceRequest, "Data Source Deferral Not Allowed", "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "datasource.DeferredResponse can only be set if datasource.ReadRequest.ReadClientCapabilities.DeferralAllowed is true.", + "(datasource.ReadResponse).DeferredResponse can only be set if (datasource.ReadRequest.ClientCapabilities).DeferralAllowed is true.", ) return } diff --git a/internal/fwserver/server_readdatasource_test.go b/internal/fwserver/server_readdatasource_test.go index 663f5ea77..6ad360b5d 100644 --- a/internal/fwserver/server_readdatasource_test.go +++ b/internal/fwserver/server_readdatasource_test.go @@ -414,7 +414,7 @@ func TestServerReadDataSource(t *testing.T) { "Data Source Deferral Not Allowed", "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "datasource.DeferredResponse can only be set if datasource.ReadRequest.ReadClientCapabilities.DeferralAllowed is true.", + "(datasource.ReadResponse).DeferredResponse can only be set if (datasource.ReadRequest.ClientCapabilities).DeferralAllowed is true.", ), }, State: testStateUnchanged, diff --git a/internal/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index 7b7831a9f..1381e2f27 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -126,7 +126,7 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res "Resource Deferral Not Allowed", "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "resource.DeferredResponse can only be set if resource.ReadRequest.ReadClientCapabilities.DeferralAllowed is true.", + "(resource.ReadResponse).DeferredResponse can only be set if (resource.ReadRequest.ClientCapabilities).DeferralAllowed is true.", ) return } diff --git a/internal/fwserver/server_readresource_test.go b/internal/fwserver/server_readresource_test.go index d7f4e9e9d..2708ad22a 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -573,7 +573,7 @@ func TestServerReadResource(t *testing.T) { "Resource Deferral Not Allowed", "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "resource.DeferredResponse can only be set if resource.ReadRequest.ReadClientCapabilities.DeferralAllowed is true.", + "(resource.ReadResponse).DeferredResponse can only be set if (resource.ReadRequest.ClientCapabilities).DeferralAllowed is true.", ), }, NewState: testCurrentState, diff --git a/resource/import_state.go b/resource/import_state.go index 658cc2bb7..58ab58dd4 100644 --- a/resource/import_state.go +++ b/resource/import_state.go @@ -12,7 +12,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) -// TODO: doc +// ImportStateClientCapabilities allows Terraform to publish information +// regarding optionally supported protocol features for the ImportResourceState RPC, +// such as forward-compatible Terraform behavior changes. type ImportStateClientCapabilities struct { // DeferralAllowed indicates whether the Terraform client initiating // the request allows a deferral response. @@ -31,7 +33,8 @@ type ImportStateRequest struct { // is not stored in the state unless the provider explicitly stores it. ID string - //TODO: doc + // ClientCapabilities defines optionally supported protocol features for the + // ImportResourceState RPC, such as forward-compatible Terraform behavior changes. ClientCapabilities *ImportStateClientCapabilities } diff --git a/resource/modify_plan.go b/resource/modify_plan.go index 6f3f563e4..b620677ee 100644 --- a/resource/modify_plan.go +++ b/resource/modify_plan.go @@ -10,7 +10,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) -// TODO: doc +// ModifyPlanClientCapabilities allows Terraform to publish information +// regarding optionally supported protocol features for the PlanResourceChange RPC, +// such as forward-compatible Terraform behavior changes. type ModifyPlanClientCapabilities struct { // DeferralAllowed indicates whether the Terraform client initiating // the request allows a deferral response. @@ -47,7 +49,8 @@ type ModifyPlanRequest struct { // ModifyPlanResponse.Private to update or remove a value. Private *privatestate.ProviderData - //TODO: doc + // ClientCapabilities defines optionally supported protocol features for the + // PlanResourceChange RPC, such as forward-compatible Terraform behavior changes. ClientCapabilities *ModifyPlanClientCapabilities } diff --git a/resource/read.go b/resource/read.go index 2255e06aa..4a1182a76 100644 --- a/resource/read.go +++ b/resource/read.go @@ -9,7 +9,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) -// TODO: doc +// ReadClientCapabilities allows Terraform to publish information +// regarding optionally supported protocol features for the ReadResource RPC, +// such as forward-compatible Terraform behavior changes. type ReadClientCapabilities struct { // DeferralAllowed indicates whether the Terraform client initiating // the request allows a deferral response. @@ -37,7 +39,8 @@ type ReadRequest struct { // ProviderMeta is metadata from the provider_meta block of the module. ProviderMeta tfsdk.Config - //TODO: doc + // ClientCapabilities defines optionally supported protocol features for the + // ReadResource RPC, such as forward-compatible Terraform behavior changes. ClientCapabilities *ReadClientCapabilities } From 3a5a8c30ade0dd4210f4ec4c6c969179359d940a Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 7 May 2024 17:22:29 -0400 Subject: [PATCH 09/44] Add copyright headers --- datasource/deferred.go | 3 +++ resource/deferred.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/datasource/deferred.go b/datasource/deferred.go index 8e5f275ba..f92eb852e 100644 --- a/datasource/deferred.go +++ b/datasource/deferred.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package datasource const ( diff --git a/resource/deferred.go b/resource/deferred.go index f677e7b86..723888b58 100644 --- a/resource/deferred.go +++ b/resource/deferred.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package resource const ( From 04042c2b41e2eaf21caf9e77d1486ea979cc3a1c Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 8 May 2024 11:12:06 -0400 Subject: [PATCH 10/44] Add changelog entries --- .changes/unreleased/FEATURES-20240508-105105.yaml | 6 ++++++ .changes/unreleased/FEATURES-20240508-105141.yaml | 6 ++++++ .changes/unreleased/FEATURES-20240508-110715.yaml | 6 ++++++ .changes/unreleased/FEATURES-20240508-111023.yaml | 6 ++++++ 4 files changed, 24 insertions(+) create mode 100644 .changes/unreleased/FEATURES-20240508-105105.yaml create mode 100644 .changes/unreleased/FEATURES-20240508-105141.yaml create mode 100644 .changes/unreleased/FEATURES-20240508-110715.yaml create mode 100644 .changes/unreleased/FEATURES-20240508-111023.yaml diff --git a/.changes/unreleased/FEATURES-20240508-105105.yaml b/.changes/unreleased/FEATURES-20240508-105105.yaml new file mode 100644 index 000000000..9b1c418bb --- /dev/null +++ b/.changes/unreleased/FEATURES-20240508-105105.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'resource: Add `DeferredResponse` field to `ReadResponse`, `ModifyPlanResponse`, and `ImportStateResponse` + which indicates a resource deferred action to the Terraform client' +time: 2024-05-08T10:51:05.90518-04:00 +custom: + Issue: "999" diff --git a/.changes/unreleased/FEATURES-20240508-105141.yaml b/.changes/unreleased/FEATURES-20240508-105141.yaml new file mode 100644 index 000000000..62ab936f6 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240508-105141.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'datasource: Add `DeferredResponse` field to `ReadResponse` which indicates a data source deferred action + to the Terraform client' +time: 2024-05-08T10:51:41.663935-04:00 +custom: + Issue: "999" diff --git a/.changes/unreleased/FEATURES-20240508-110715.yaml b/.changes/unreleased/FEATURES-20240508-110715.yaml new file mode 100644 index 000000000..15f79b2de --- /dev/null +++ b/.changes/unreleased/FEATURES-20240508-110715.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'resource: Add `ClientCapabilities` field to `ReadRequest`, `ModifyPlanRequest`, and `ImportStateRequest` + which specifies optionally supported protocol features for the Terraform client' +time: 2024-05-08T11:07:15.955126-04:00 +custom: + Issue: "999" diff --git a/.changes/unreleased/FEATURES-20240508-111023.yaml b/.changes/unreleased/FEATURES-20240508-111023.yaml new file mode 100644 index 000000000..2007aded5 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240508-111023.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'datasource: Add `ClientCapabilities` field to `ReadRequest` which specifies + optionally supported protocol features for the Terraform client' +time: 2024-05-08T11:10:23.66584-04:00 +custom: + Issue: "999" From ea6400558d409037ed30c53c695425652ed7e995 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Fri, 10 May 2024 14:29:20 -0400 Subject: [PATCH 11/44] Apply suggestions from code review Co-authored-by: Austin Valle --- datasource/read.go | 4 ++-- internal/fromproto5/readdatasource_test.go | 1 - resource/import_state.go | 2 +- resource/modify_plan.go | 2 +- resource/read.go | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/datasource/read.go b/datasource/read.go index 12ac76c1f..7c7c9a3db 100644 --- a/datasource/read.go +++ b/datasource/read.go @@ -52,9 +52,9 @@ type ReadResponse struct { Diagnostics diag.Diagnostics // DeferredResponse indicates that Terraform should defer - // importing this resource. + // reading this data source. // // This field can only be set if - // `(datasource.ReadRequest.ReadClientCapabilities).DeferralAllowed` is true. + // `(datasource.ReadRequest).ClientCapabilities.DeferralAllowed` is true. DeferredResponse *DeferredResponse } diff --git a/internal/fromproto5/readdatasource_test.go b/internal/fromproto5/readdatasource_test.go index b5bdfca53..a398674da 100644 --- a/internal/fromproto5/readdatasource_test.go +++ b/internal/fromproto5/readdatasource_test.go @@ -1,5 +1,4 @@ // Copyright (c) HashiCorp, Inc. - // SPDX-License-Identifier: MPL-2.0 package fromproto5_test diff --git a/resource/import_state.go b/resource/import_state.go index 58ab58dd4..ae772950d 100644 --- a/resource/import_state.go +++ b/resource/import_state.go @@ -62,7 +62,7 @@ type ImportStateResponse struct { // importing this resource. // // This field can only be set if - // `(resource.ImportStateRequest.ImportStateClientCapabilities).DeferralAllowed` is true. + // `(resource.ImportStateRequest).ClientCapabilities.DeferralAllowed` is true. DeferredResponse *DeferredResponse } diff --git a/resource/modify_plan.go b/resource/modify_plan.go index b620677ee..167387e0c 100644 --- a/resource/modify_plan.go +++ b/resource/modify_plan.go @@ -83,6 +83,6 @@ type ModifyPlanResponse struct { // importing this resource. // // This field can only be set if - // `(resource.ModifyPlanRequest.ModifyPlanClientCapabilities).DeferralAllowed` is true. + // `(resource.ModifyPlanRequest).ClientCapabilities.DeferralAllowed` is true. DeferredResponse *DeferredResponse } diff --git a/resource/read.go b/resource/read.go index 4a1182a76..893b1e68c 100644 --- a/resource/read.go +++ b/resource/read.go @@ -68,6 +68,6 @@ type ReadResponse struct { // importing this resource. // // This field can only be set if - // `(resource.ReadRequest.ReadClientCapabilities).DeferralAllowed` is true. + // `(resource.ReadRequest).ClientCapabilities.DeferralAllowed` is true. DeferredResponse *DeferredResponse } From ac952fceb6e7093422a7e93c3ee20c2cb44826ee Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Fri, 10 May 2024 14:41:13 -0400 Subject: [PATCH 12/44] Add comment and changelog notes to indicate experimental nature of deferred actions. --- .changes/unreleased/NOTES-20240510-143136.yaml | 7 +++++++ datasource/deferred.go | 6 ++++++ datasource/read.go | 6 ++++++ resource/deferred.go | 6 ++++++ resource/import_state.go | 6 ++++++ resource/modify_plan.go | 6 ++++++ resource/read.go | 6 ++++++ 7 files changed, 43 insertions(+) create mode 100644 .changes/unreleased/NOTES-20240510-143136.yaml diff --git a/.changes/unreleased/NOTES-20240510-143136.yaml b/.changes/unreleased/NOTES-20240510-143136.yaml new file mode 100644 index 000000000..d073e42aa --- /dev/null +++ b/.changes/unreleased/NOTES-20240510-143136.yaml @@ -0,0 +1,7 @@ +kind: NOTES +body: This release contains support for deferred actions, which is an experimental + feature only available in prerelease builds of Terraform 1.9 and later. This functionality + is subject to change and is not protected by version compatibility guarantees. +time: 2024-05-10T14:31:36.644869-04:00 +custom: + Issue: "999" diff --git a/datasource/deferred.go b/datasource/deferred.go index f92eb852e..b1b03ee72 100644 --- a/datasource/deferred.go +++ b/datasource/deferred.go @@ -21,12 +21,18 @@ const ( ) // DeferredResponse is used to indicate to Terraform that a change needs to be deferred for a reason. +// +// 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. type DeferredResponse struct { // Reason is the reason for deferring the change. Reason DeferredReason } // DeferredReason represents different reasons for deferring a change. +// +// 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. type DeferredReason int32 func (d DeferredReason) String() string { diff --git a/datasource/read.go b/datasource/read.go index 7c7c9a3db..241ff91a8 100644 --- a/datasource/read.go +++ b/datasource/read.go @@ -14,6 +14,9 @@ import ( type ReadClientCapabilities struct { // DeferralAllowed indicates whether the Terraform client initiating // the request allows a deferral response. + // + // 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. DeferralAllowed bool } @@ -56,5 +59,8 @@ type ReadResponse struct { // // This field can only be set if // `(datasource.ReadRequest).ClientCapabilities.DeferralAllowed` is true. + // + // 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. DeferredResponse *DeferredResponse } diff --git a/resource/deferred.go b/resource/deferred.go index 723888b58..0009f6872 100644 --- a/resource/deferred.go +++ b/resource/deferred.go @@ -21,12 +21,18 @@ const ( ) // DeferredResponse is used to indicate to Terraform that a change needs to be deferred for a reason. +// +// 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. type DeferredResponse struct { // Reason is the reason for deferring the change. Reason DeferredReason } // DeferredReason represents different reasons for deferring a change. +// +// 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. type DeferredReason int32 func (d DeferredReason) String() string { diff --git a/resource/import_state.go b/resource/import_state.go index ae772950d..22a5f7aa2 100644 --- a/resource/import_state.go +++ b/resource/import_state.go @@ -18,6 +18,9 @@ import ( type ImportStateClientCapabilities struct { // DeferralAllowed indicates whether the Terraform client initiating // the request allows a deferral response. + // + // 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. DeferralAllowed bool } @@ -63,6 +66,9 @@ type ImportStateResponse struct { // // This field can only be set if // `(resource.ImportStateRequest).ClientCapabilities.DeferralAllowed` is true. + // + // 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. DeferredResponse *DeferredResponse } diff --git a/resource/modify_plan.go b/resource/modify_plan.go index 167387e0c..1cde9acd1 100644 --- a/resource/modify_plan.go +++ b/resource/modify_plan.go @@ -16,6 +16,9 @@ import ( type ModifyPlanClientCapabilities struct { // DeferralAllowed indicates whether the Terraform client initiating // the request allows a deferral response. + // + // 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. DeferralAllowed bool } @@ -84,5 +87,8 @@ type ModifyPlanResponse struct { // // This field can only be set if // `(resource.ModifyPlanRequest).ClientCapabilities.DeferralAllowed` is true. + // + // 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. DeferredResponse *DeferredResponse } diff --git a/resource/read.go b/resource/read.go index 893b1e68c..661b9d1bb 100644 --- a/resource/read.go +++ b/resource/read.go @@ -15,6 +15,9 @@ import ( type ReadClientCapabilities struct { // DeferralAllowed indicates whether the Terraform client initiating // the request allows a deferral response. + // + // 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. DeferralAllowed bool } @@ -69,5 +72,8 @@ type ReadResponse struct { // // This field can only be set if // `(resource.ReadRequest).ClientCapabilities.DeferralAllowed` is true. + // + // 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. DeferredResponse *DeferredResponse } From bcadaeeb9f5da5062116ebcb1ad44682830b7e0a Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 13 May 2024 16:02:12 -0400 Subject: [PATCH 13/44] Rename constant to be specific to data sources --- datasource/deferred.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/datasource/deferred.go b/datasource/deferred.go index b1b03ee72..9631c0210 100644 --- a/datasource/deferred.go +++ b/datasource/deferred.go @@ -8,9 +8,9 @@ const ( // Provider developers should not use it. DeferredReasonUnknown DeferredReason = 0 - // DeferredReasonResourceConfigUnknown is used to indicate that the resource configuration + // DeferredReasonDataSourceConfigUnknown is used to indicate that the resource configuration // is partially unknown and the real values need to be known before the change can be planned. - DeferredReasonResourceConfigUnknown DeferredReason = 1 + DeferredReasonDataSourceConfigUnknown DeferredReason = 1 // DeferredReasonProviderConfigUnknown is used to indicate that the provider configuration // is partially unknown and the real values need to be known before the change can be planned. @@ -40,7 +40,7 @@ func (d DeferredReason) String() string { case 0: return "Unknown" case 1: - return "Resource Config Unknown" + return "Data Source Config Unknown" case 2: return "Provider Config Unknown" case 3: From 3777d5d0096250acdb97f77f6adce078998ef175 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 13 May 2024 16:06:18 -0400 Subject: [PATCH 14/44] Remove TODO comment --- internal/fwserver/server_importresourcestate.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/fwserver/server_importresourcestate.go b/internal/fwserver/server_importresourcestate.go index 211060b01..d6de73851 100644 --- a/internal/fwserver/server_importresourcestate.go +++ b/internal/fwserver/server_importresourcestate.go @@ -36,7 +36,6 @@ type ImportResourceStateRequest struct { // the ImportedResource TypeName of the ImportResourceStateResponse. TypeName string - //TODO: doc ClientCapabilities *resource.ImportStateClientCapabilities } From 0c4891542173b5993939fd3d7a35b100366ab63d Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 13 May 2024 16:08:12 -0400 Subject: [PATCH 15/44] Remove unnecessary nil check --- internal/fwserver/server_importresourcestate.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/internal/fwserver/server_importresourcestate.go b/internal/fwserver/server_importresourcestate.go index d6de73851..528587d72 100644 --- a/internal/fwserver/server_importresourcestate.go +++ b/internal/fwserver/server_importresourcestate.go @@ -142,10 +142,7 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta private.Provider = importResp.Private } - if importResp.DeferredResponse != nil { - resp.Deferred = importResp.DeferredResponse - } - + resp.Deferred = importResp.DeferredResponse resp.ImportedResources = []ImportedResource{ { State: importResp.State, From 95ef72ee9037867e44b622879b0af1cf03360ddc Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 13 May 2024 16:54:51 -0400 Subject: [PATCH 16/44] Add default values for `ClientCapabilities` request fields --- datasource/read.go | 2 +- internal/fromproto5/client_capabilities.go | 56 +++++++++++++++++++ internal/fromproto5/importresourcestate.go | 13 ++--- internal/fromproto5/planresourcechange.go | 11 +--- internal/fromproto5/readdatasource.go | 11 +--- internal/fromproto5/readresource.go | 9 +-- internal/fromproto6/client_capabilities.go | 56 +++++++++++++++++++ internal/fromproto6/importresourcestate.go | 13 ++--- internal/fromproto6/planresourcechange.go | 11 +--- internal/fromproto6/readdatasource.go | 11 +--- internal/fromproto6/readresource.go | 9 +-- .../fwserver/server_importresourcestate.go | 9 ++- .../fwserver/server_planresourcechange.go | 15 +++-- internal/fwserver/server_readdatasource.go | 4 +- internal/fwserver/server_readresource.go | 6 +- resource/import_state.go | 2 +- resource/modify_plan.go | 2 +- resource/read.go | 2 +- 18 files changed, 162 insertions(+), 80 deletions(-) create mode 100644 internal/fromproto5/client_capabilities.go create mode 100644 internal/fromproto6/client_capabilities.go diff --git a/datasource/read.go b/datasource/read.go index 241ff91a8..496cd2bd8 100644 --- a/datasource/read.go +++ b/datasource/read.go @@ -37,7 +37,7 @@ type ReadRequest struct { // ClientCapabilities defines optionally supported protocol features for the // ReadDataSource RPC, such as forward-compatible Terraform behavior changes. - ClientCapabilities *ReadClientCapabilities + ClientCapabilities ReadClientCapabilities } // ReadResponse represents a response to a ReadRequest. An diff --git a/internal/fromproto5/client_capabilities.go b/internal/fromproto5/client_capabilities.go new file mode 100644 index 000000000..fde821ac9 --- /dev/null +++ b/internal/fromproto5/client_capabilities.go @@ -0,0 +1,56 @@ +package fromproto5 + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func ReadDataSourceClientCapabilities(in *tfprotov5.ReadDataSourceClientCapabilities) *datasource.ReadClientCapabilities { + if in == nil { + return nil + } + + resp := &datasource.ReadClientCapabilities{ + DeferralAllowed: in.DeferralAllowed, + } + + return resp +} + +func ReadResourceClientCapabilities(in *tfprotov5.ReadResourceClientCapabilities) *resource.ReadClientCapabilities { + if in == nil { + return nil + } + + resp := &resource.ReadClientCapabilities{ + DeferralAllowed: in.DeferralAllowed, + } + + return resp +} + +func ModifyPlanClientCapabilities(in *tfprotov5.PlanResourceChangeClientCapabilities) *resource.ModifyPlanClientCapabilities { + if in == nil { + return nil + } + + resp := &resource.ModifyPlanClientCapabilities{ + DeferralAllowed: in.DeferralAllowed, + } + + return resp +} + +func ImportStateClientCapabilities(in *tfprotov5.ImportResourceStateClientCapabilities) *resource.ImportStateClientCapabilities { + if in == nil { + return nil + } + + resp := &resource.ImportStateClientCapabilities{ + DeferralAllowed: in.DeferralAllowed, + } + + return resp +} diff --git a/internal/fromproto5/importresourcestate.go b/internal/fromproto5/importresourcestate.go index d074f0c9e..ec40c2119 100644 --- a/internal/fromproto5/importresourcestate.go +++ b/internal/fromproto5/importresourcestate.go @@ -44,15 +44,10 @@ func ImportResourceStateRequest(ctx context.Context, proto5 *tfprotov5.ImportRes Raw: tftypes.NewValue(resourceSchema.Type().TerraformType(ctx), nil), Schema: resourceSchema, }, - ID: proto5.ID, - Resource: reqResource, - TypeName: proto5.TypeName, - } - - if proto5.ClientCapabilities != nil { - fw.ClientCapabilities = &resource.ImportStateClientCapabilities{ - DeferralAllowed: proto5.ClientCapabilities.DeferralAllowed, - } + ID: proto5.ID, + Resource: reqResource, + TypeName: proto5.TypeName, + ClientCapabilities: ImportStateClientCapabilities(proto5.ClientCapabilities), } return fw, diags diff --git a/internal/fromproto5/planresourcechange.go b/internal/fromproto5/planresourcechange.go index 0852ad5db..3aaed62b8 100644 --- a/internal/fromproto5/planresourcechange.go +++ b/internal/fromproto5/planresourcechange.go @@ -39,8 +39,9 @@ func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResour } fw := &fwserver.PlanResourceChangeRequest{ - ResourceSchema: resourceSchema, - Resource: reqResource, + ResourceSchema: resourceSchema, + Resource: reqResource, + ClientCapabilities: ModifyPlanClientCapabilities(proto5.ClientCapabilities), } config, configDiags := Config(ctx, proto5.Config, resourceSchema) @@ -73,11 +74,5 @@ func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResour fw.PriorPrivate = privateData - if proto5.ClientCapabilities != nil { - fw.ClientCapabilities = &resource.ModifyPlanClientCapabilities{ - DeferralAllowed: proto5.ClientCapabilities.DeferralAllowed, - } - } - return fw, diags } diff --git a/internal/fromproto5/readdatasource.go b/internal/fromproto5/readdatasource.go index 105ffad67..9f6fa4ddc 100644 --- a/internal/fromproto5/readdatasource.go +++ b/internal/fromproto5/readdatasource.go @@ -38,8 +38,9 @@ func ReadDataSourceRequest(ctx context.Context, proto5 *tfprotov5.ReadDataSource } fw := &fwserver.ReadDataSourceRequest{ - DataSource: dataSource, - DataSourceSchema: dataSourceSchema, + DataSource: dataSource, + DataSourceSchema: dataSourceSchema, + ClientCapabilities: ReadDataSourceClientCapabilities(proto5.ClientCapabilities), } config, configDiags := Config(ctx, proto5.Config, dataSourceSchema) @@ -54,11 +55,5 @@ func ReadDataSourceRequest(ctx context.Context, proto5 *tfprotov5.ReadDataSource fw.ProviderMeta = providerMeta - if proto5.ClientCapabilities != nil { - fw.ClientCapabilities = &datasource.ReadClientCapabilities{ - DeferralAllowed: proto5.ClientCapabilities.DeferralAllowed, - } - } - return fw, diags } diff --git a/internal/fromproto5/readresource.go b/internal/fromproto5/readresource.go index a3860f6df..04d4b4d2e 100644 --- a/internal/fromproto5/readresource.go +++ b/internal/fromproto5/readresource.go @@ -25,7 +25,8 @@ func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequ var diags diag.Diagnostics fw := &fwserver.ReadResourceRequest{ - Resource: reqResource, + Resource: reqResource, + ClientCapabilities: ReadResourceClientCapabilities(proto5.ClientCapabilities), } currentState, currentStateDiags := State(ctx, proto5.CurrentState, resourceSchema) @@ -46,11 +47,5 @@ func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequ fw.Private = privateData - if proto5.ClientCapabilities != nil { - fw.ClientCapabilities = &resource.ReadClientCapabilities{ - DeferralAllowed: proto5.ClientCapabilities.DeferralAllowed, - } - } - return fw, diags } diff --git a/internal/fromproto6/client_capabilities.go b/internal/fromproto6/client_capabilities.go new file mode 100644 index 000000000..c2f54e498 --- /dev/null +++ b/internal/fromproto6/client_capabilities.go @@ -0,0 +1,56 @@ +package fromproto6 + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func ReadDataSourceClientCapabilities(in *tfprotov6.ReadDataSourceClientCapabilities) *datasource.ReadClientCapabilities { + if in == nil { + return nil + } + + resp := &datasource.ReadClientCapabilities{ + DeferralAllowed: in.DeferralAllowed, + } + + return resp +} + +func ReadResourceClientCapabilities(in *tfprotov6.ReadResourceClientCapabilities) *resource.ReadClientCapabilities { + if in == nil { + return nil + } + + resp := &resource.ReadClientCapabilities{ + DeferralAllowed: in.DeferralAllowed, + } + + return resp +} + +func ModifyPlanClientCapabilities(in *tfprotov6.PlanResourceChangeClientCapabilities) *resource.ModifyPlanClientCapabilities { + if in == nil { + return nil + } + + resp := &resource.ModifyPlanClientCapabilities{ + DeferralAllowed: in.DeferralAllowed, + } + + return resp +} + +func ImportStateClientCapabilities(in *tfprotov6.ImportResourceStateClientCapabilities) *resource.ImportStateClientCapabilities { + if in == nil { + return nil + } + + resp := &resource.ImportStateClientCapabilities{ + DeferralAllowed: in.DeferralAllowed, + } + + return resp +} diff --git a/internal/fromproto6/importresourcestate.go b/internal/fromproto6/importresourcestate.go index 431fadd4c..7070901cd 100644 --- a/internal/fromproto6/importresourcestate.go +++ b/internal/fromproto6/importresourcestate.go @@ -44,15 +44,10 @@ func ImportResourceStateRequest(ctx context.Context, proto6 *tfprotov6.ImportRes Raw: tftypes.NewValue(resourceSchema.Type().TerraformType(ctx), nil), Schema: resourceSchema, }, - ID: proto6.ID, - Resource: reqResource, - TypeName: proto6.TypeName, - } - - if proto6.ClientCapabilities != nil { - fw.ClientCapabilities = &resource.ImportStateClientCapabilities{ - DeferralAllowed: proto6.ClientCapabilities.DeferralAllowed, - } + ID: proto6.ID, + Resource: reqResource, + TypeName: proto6.TypeName, + ClientCapabilities: ImportStateClientCapabilities(proto6.ClientCapabilities), } return fw, diags diff --git a/internal/fromproto6/planresourcechange.go b/internal/fromproto6/planresourcechange.go index a957c9b9e..6a10ee180 100644 --- a/internal/fromproto6/planresourcechange.go +++ b/internal/fromproto6/planresourcechange.go @@ -39,8 +39,9 @@ func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResour } fw := &fwserver.PlanResourceChangeRequest{ - ResourceSchema: resourceSchema, - Resource: reqResource, + ResourceSchema: resourceSchema, + Resource: reqResource, + ClientCapabilities: ModifyPlanClientCapabilities(proto6.ClientCapabilities), } config, configDiags := Config(ctx, proto6.Config, resourceSchema) @@ -73,11 +74,5 @@ func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResour fw.PriorPrivate = privateData - if proto6.ClientCapabilities != nil { - fw.ClientCapabilities = &resource.ModifyPlanClientCapabilities{ - DeferralAllowed: proto6.ClientCapabilities.DeferralAllowed, - } - } - return fw, diags } diff --git a/internal/fromproto6/readdatasource.go b/internal/fromproto6/readdatasource.go index edd540688..b84b7b108 100644 --- a/internal/fromproto6/readdatasource.go +++ b/internal/fromproto6/readdatasource.go @@ -38,8 +38,9 @@ func ReadDataSourceRequest(ctx context.Context, proto6 *tfprotov6.ReadDataSource } fw := &fwserver.ReadDataSourceRequest{ - DataSourceSchema: dataSourceSchema, - DataSource: dataSource, + DataSourceSchema: dataSourceSchema, + DataSource: dataSource, + ClientCapabilities: ReadDataSourceClientCapabilities(proto6.ClientCapabilities), } config, configDiags := Config(ctx, proto6.Config, dataSourceSchema) @@ -54,11 +55,5 @@ func ReadDataSourceRequest(ctx context.Context, proto6 *tfprotov6.ReadDataSource fw.ProviderMeta = providerMeta - if proto6.ClientCapabilities != nil { - fw.ClientCapabilities = &datasource.ReadClientCapabilities{ - DeferralAllowed: proto6.ClientCapabilities.DeferralAllowed, - } - } - return fw, diags } diff --git a/internal/fromproto6/readresource.go b/internal/fromproto6/readresource.go index 2679b6612..a42f669d6 100644 --- a/internal/fromproto6/readresource.go +++ b/internal/fromproto6/readresource.go @@ -25,7 +25,8 @@ func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequ var diags diag.Diagnostics fw := &fwserver.ReadResourceRequest{ - Resource: reqResource, + Resource: reqResource, + ClientCapabilities: ReadResourceClientCapabilities(proto6.ClientCapabilities), } currentState, currentStateDiags := State(ctx, proto6.CurrentState, resourceSchema) @@ -46,11 +47,5 @@ func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequ fw.Private = privateData - if proto6.ClientCapabilities != nil { - fw.ClientCapabilities = &resource.ReadClientCapabilities{ - DeferralAllowed: proto6.ClientCapabilities.DeferralAllowed, - } - } - return fw, diags } diff --git a/internal/fwserver/server_importresourcestate.go b/internal/fwserver/server_importresourcestate.go index 528587d72..4a8c0a5dd 100644 --- a/internal/fwserver/server_importresourcestate.go +++ b/internal/fwserver/server_importresourcestate.go @@ -93,8 +93,11 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta } importReq := resource.ImportStateRequest{ - ID: req.ID, - ClientCapabilities: req.ClientCapabilities, + ID: req.ID, + } + + if req.ClientCapabilities != nil { + importReq.ClientCapabilities = *req.ClientCapabilities } privateProviderData := privatestate.EmptyProviderData(ctx) @@ -117,7 +120,7 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta return } - if (importReq.ClientCapabilities == nil || !importReq.ClientCapabilities.DeferralAllowed) && importResp.DeferredResponse != nil { + if !importReq.ClientCapabilities.DeferralAllowed && importResp.DeferredResponse != nil { resp.Diagnostics.AddError( "Resource Import Deferral Not Allowed", "An unexpected error was encountered when importing the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index f1f2b1413..0549fe3d3 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -262,17 +262,20 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange logging.FrameworkTrace(ctx, "Resource implements ResourceWithModifyPlan") modifyPlanReq := resource.ModifyPlanRequest{ - Config: *req.Config, - Plan: stateToPlan(*resp.PlannedState), - State: *req.PriorState, - Private: resp.PlannedPrivate.Provider, - ClientCapabilities: req.ClientCapabilities, + Config: *req.Config, + Plan: stateToPlan(*resp.PlannedState), + State: *req.PriorState, + Private: resp.PlannedPrivate.Provider, } if req.ProviderMeta != nil { modifyPlanReq.ProviderMeta = *req.ProviderMeta } + if req.ClientCapabilities != nil { + modifyPlanReq.ClientCapabilities = *req.ClientCapabilities + } + modifyPlanResp := resource.ModifyPlanResponse{ Diagnostics: resp.Diagnostics, Plan: modifyPlanReq.Plan, @@ -284,7 +287,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange resourceWithModifyPlan.ModifyPlan(ctx, modifyPlanReq, &modifyPlanResp) logging.FrameworkTrace(ctx, "Called provider defined Resource ModifyPlan") - if (modifyPlanReq.ClientCapabilities == nil || !modifyPlanReq.ClientCapabilities.DeferralAllowed) && modifyPlanResp.DeferredResponse != nil { + if !modifyPlanReq.ClientCapabilities.DeferralAllowed && modifyPlanResp.DeferredResponse != nil { resp.Diagnostics.AddError( "Resource Deferral Not Allowed", "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ diff --git a/internal/fwserver/server_readdatasource.go b/internal/fwserver/server_readdatasource.go index 15693e42d..4ff33fe45 100644 --- a/internal/fwserver/server_readdatasource.go +++ b/internal/fwserver/server_readdatasource.go @@ -77,7 +77,9 @@ func (s *Server) ReadDataSource(ctx context.Context, req *ReadDataSourceRequest, readReq.ProviderMeta = *req.ProviderMeta } - readReq.ClientCapabilities = req.ClientCapabilities + if req.ClientCapabilities != nil { + readReq.ClientCapabilities = *req.ClientCapabilities + } logging.FrameworkTrace(ctx, "Calling provider defined DataSource Read") req.DataSource.Read(ctx, readReq, &readResp) diff --git a/internal/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index 1381e2f27..1991cbfd0 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -85,6 +85,10 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res readReq.ProviderMeta = *req.ProviderMeta } + if req.ClientCapabilities != nil { + readReq.ClientCapabilities = *req.ClientCapabilities + } + privateProviderData := privatestate.EmptyProviderData(ctx) readReq.Private = privateProviderData @@ -99,8 +103,6 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res resp.Private = req.Private } - readReq.ClientCapabilities = req.ClientCapabilities - logging.FrameworkTrace(ctx, "Calling provider defined Resource Read") req.Resource.Read(ctx, readReq, &readResp) logging.FrameworkTrace(ctx, "Called provider defined Resource Read") diff --git a/resource/import_state.go b/resource/import_state.go index 22a5f7aa2..0c425715b 100644 --- a/resource/import_state.go +++ b/resource/import_state.go @@ -38,7 +38,7 @@ type ImportStateRequest struct { // ClientCapabilities defines optionally supported protocol features for the // ImportResourceState RPC, such as forward-compatible Terraform behavior changes. - ClientCapabilities *ImportStateClientCapabilities + ClientCapabilities ImportStateClientCapabilities } // ImportStateResponse represents a response to a ImportStateRequest. diff --git a/resource/modify_plan.go b/resource/modify_plan.go index 1cde9acd1..28d9fc0f7 100644 --- a/resource/modify_plan.go +++ b/resource/modify_plan.go @@ -54,7 +54,7 @@ type ModifyPlanRequest struct { // ClientCapabilities defines optionally supported protocol features for the // PlanResourceChange RPC, such as forward-compatible Terraform behavior changes. - ClientCapabilities *ModifyPlanClientCapabilities + ClientCapabilities ModifyPlanClientCapabilities } // ModifyPlanResponse represents a response to a diff --git a/resource/read.go b/resource/read.go index 661b9d1bb..7e61aac55 100644 --- a/resource/read.go +++ b/resource/read.go @@ -44,7 +44,7 @@ type ReadRequest struct { // ClientCapabilities defines optionally supported protocol features for the // ReadResource RPC, such as forward-compatible Terraform behavior changes. - ClientCapabilities *ReadClientCapabilities + ClientCapabilities ReadClientCapabilities } // ReadResponse represents a response to a ReadRequest. An From c5156c533f18ebf83a833e9b913eea8f585930e9 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 13 May 2024 16:58:42 -0400 Subject: [PATCH 17/44] Rename `DeferredResponse` to `Deferred` --- .changes/unreleased/FEATURES-20240508-105105.yaml | 2 +- .changes/unreleased/FEATURES-20240508-105141.yaml | 2 +- datasource/deferred.go | 4 ++-- datasource/read.go | 4 ++-- internal/fwserver/server_importresourcestate.go | 8 ++++---- internal/fwserver/server_importresourcestate_test.go | 8 ++++---- internal/fwserver/server_planresourcechange.go | 8 ++++---- internal/fwserver/server_planresourcechange_test.go | 8 ++++---- internal/fwserver/server_readdatasource.go | 6 +++--- internal/fwserver/server_readdatasource_test.go | 10 +++++----- internal/fwserver/server_readresource.go | 6 +++--- internal/fwserver/server_readresource_test.go | 10 +++++----- internal/toproto5/planresourcechange_test.go | 2 +- internal/toproto5/readdatasource_test.go | 2 +- internal/toproto5/readresource_test.go | 2 +- internal/toproto6/planresourcechange_test.go | 2 +- internal/toproto6/readdatasource_test.go | 2 +- internal/toproto6/readresource_test.go | 2 +- resource/deferred.go | 4 ++-- resource/import_state.go | 4 ++-- resource/modify_plan.go | 4 ++-- resource/read.go | 4 ++-- 22 files changed, 52 insertions(+), 52 deletions(-) diff --git a/.changes/unreleased/FEATURES-20240508-105105.yaml b/.changes/unreleased/FEATURES-20240508-105105.yaml index 9b1c418bb..67d8700bb 100644 --- a/.changes/unreleased/FEATURES-20240508-105105.yaml +++ b/.changes/unreleased/FEATURES-20240508-105105.yaml @@ -1,5 +1,5 @@ kind: FEATURES -body: 'resource: Add `DeferredResponse` field to `ReadResponse`, `ModifyPlanResponse`, and `ImportStateResponse` +body: 'resource: Add `Deferred` field to `ReadResponse`, `ModifyPlanResponse`, and `ImportStateResponse` which indicates a resource deferred action to the Terraform client' time: 2024-05-08T10:51:05.90518-04:00 custom: diff --git a/.changes/unreleased/FEATURES-20240508-105141.yaml b/.changes/unreleased/FEATURES-20240508-105141.yaml index 62ab936f6..82f1ccbcd 100644 --- a/.changes/unreleased/FEATURES-20240508-105141.yaml +++ b/.changes/unreleased/FEATURES-20240508-105141.yaml @@ -1,5 +1,5 @@ kind: FEATURES -body: 'datasource: Add `DeferredResponse` field to `ReadResponse` which indicates a data source deferred action +body: 'datasource: Add `Deferred` field to `ReadResponse` which indicates a data source deferred action to the Terraform client' time: 2024-05-08T10:51:41.663935-04:00 custom: diff --git a/datasource/deferred.go b/datasource/deferred.go index 9631c0210..ee740f694 100644 --- a/datasource/deferred.go +++ b/datasource/deferred.go @@ -20,11 +20,11 @@ const ( DeferredReasonAbsentPrereq DeferredReason = 3 ) -// DeferredResponse is used to indicate to Terraform that a change needs to be deferred for a reason. +// Deferred is used to indicate to Terraform that a change needs to be deferred for a reason. // // 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. -type DeferredResponse struct { +type Deferred struct { // Reason is the reason for deferring the change. Reason DeferredReason } diff --git a/datasource/read.go b/datasource/read.go index 496cd2bd8..751b9754f 100644 --- a/datasource/read.go +++ b/datasource/read.go @@ -54,7 +54,7 @@ type ReadResponse struct { // warnings or errors generated. Diagnostics diag.Diagnostics - // DeferredResponse indicates that Terraform should defer + // Deferred indicates that Terraform should defer // reading this data source. // // This field can only be set if @@ -62,5 +62,5 @@ type ReadResponse struct { // // 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. - DeferredResponse *DeferredResponse + Deferred *Deferred } diff --git a/internal/fwserver/server_importresourcestate.go b/internal/fwserver/server_importresourcestate.go index 4a8c0a5dd..0a2c91e2c 100644 --- a/internal/fwserver/server_importresourcestate.go +++ b/internal/fwserver/server_importresourcestate.go @@ -44,7 +44,7 @@ type ImportResourceStateRequest struct { type ImportResourceStateResponse struct { Diagnostics diag.Diagnostics ImportedResources []ImportedResource - Deferred *resource.DeferredResponse + Deferred *resource.Deferred } // ImportResourceState implements the framework server ImportResourceState RPC. @@ -120,12 +120,12 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta return } - if !importReq.ClientCapabilities.DeferralAllowed && importResp.DeferredResponse != nil { + if !importReq.ClientCapabilities.DeferralAllowed && importResp.Deferred != nil { resp.Diagnostics.AddError( "Resource Import Deferral Not Allowed", "An unexpected error was encountered when importing the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(resource.ImportStateResponse).DeferredResponse can only be set if (resource.ImportStateRequest.ClientCapabilities).DeferralAllowed is true.", + "(resource.ImportStateResponse).Deferred can only be set if (resource.ImportStateRequest.ClientCapabilities).DeferralAllowed is true.", ) return } @@ -145,7 +145,7 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta private.Provider = importResp.Private } - resp.Deferred = importResp.DeferredResponse + resp.Deferred = importResp.Deferred resp.ImportedResources = []ImportedResource{ { State: importResp.State, diff --git a/internal/fwserver/server_importresourcestate_test.go b/internal/fwserver/server_importresourcestate_test.go index 7b0177837..d1f7dc097 100644 --- a/internal/fwserver/server_importresourcestate_test.go +++ b/internal/fwserver/server_importresourcestate_test.go @@ -317,7 +317,7 @@ func TestServerImportResourceState(t *testing.T) { resp.Diagnostics.AddError("unexpected req.ID value: %s", req.ID) } - resp.DeferredResponse = &resource.DeferredResponse{ + resp.Deferred = &resource.Deferred{ Reason: resource.DeferredReasonAbsentPrereq, } @@ -336,7 +336,7 @@ func TestServerImportResourceState(t *testing.T) { Private: testEmptyPrivate, }, }, - Deferred: &resource.DeferredResponse{Reason: resource.DeferredReasonAbsentPrereq}, + Deferred: &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq}, }, }, "request-deferral-not-allowed-response-deferral": { @@ -353,7 +353,7 @@ func TestServerImportResourceState(t *testing.T) { resp.Diagnostics.AddError("unexpected req.ID value: %s", req.ID) } - resp.DeferredResponse = &resource.DeferredResponse{ + resp.Deferred = &resource.Deferred{ Reason: resource.DeferredReasonAbsentPrereq, } @@ -369,7 +369,7 @@ func TestServerImportResourceState(t *testing.T) { "Resource Import Deferral Not Allowed", "An unexpected error was encountered when importing the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(resource.ImportStateResponse).DeferredResponse can only be set if (resource.ImportStateRequest.ClientCapabilities).DeferralAllowed is true.", + "(resource.ImportStateResponse).Deferred can only be set if (resource.ImportStateRequest.ClientCapabilities).DeferralAllowed is true.", ), }, }, diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index 0549fe3d3..77df93039 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -39,7 +39,7 @@ type PlanResourceChangeRequest struct { // PlanResourceChangeResponse is the framework server response for the // PlanResourceChange RPC. type PlanResourceChangeResponse struct { - Deferred *resource.DeferredResponse + Deferred *resource.Deferred Diagnostics diag.Diagnostics PlannedPrivate *privatestate.Data PlannedState *tfsdk.State @@ -287,12 +287,12 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange resourceWithModifyPlan.ModifyPlan(ctx, modifyPlanReq, &modifyPlanResp) logging.FrameworkTrace(ctx, "Called provider defined Resource ModifyPlan") - if !modifyPlanReq.ClientCapabilities.DeferralAllowed && modifyPlanResp.DeferredResponse != nil { + if !modifyPlanReq.ClientCapabilities.DeferralAllowed && modifyPlanResp.Deferred != nil { resp.Diagnostics.AddError( "Resource Deferral Not Allowed", "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(resource.ModifyPlanResponse).DeferredResponse can only be set if (resource.ModifyPlanRequest.ClientCapabilities.DeferralAllowed is true.", + "(resource.ModifyPlanResponse).Deferred can only be set if (resource.ModifyPlanRequest.ClientCapabilities.DeferralAllowed is true.", ) return } @@ -301,7 +301,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange resp.PlannedState = planToState(modifyPlanResp.Plan) resp.RequiresReplace = append(resp.RequiresReplace, modifyPlanResp.RequiresReplace...) resp.PlannedPrivate.Provider = modifyPlanResp.Private - resp.Deferred = modifyPlanResp.DeferredResponse + resp.Deferred = modifyPlanResp.Deferred } // Ensure deterministic RequiresReplace by sorting and deduplicating diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index d23a73137..60c6e451a 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -6044,14 +6044,14 @@ func TestServerPlanResourceChange(t *testing.T) { Resource: &testprovider.ResourceWithModifyPlan{ ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { if req.ClientCapabilities.DeferralAllowed == true { - resp.DeferredResponse = &resource.DeferredResponse{Reason: resource.DeferredReasonAbsentPrereq} + resp.Deferred = &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq} } }, }, }, expectedResponse: &fwserver.PlanResourceChangeResponse{ - Deferred: &resource.DeferredResponse{Reason: resource.DeferredReasonAbsentPrereq}, + Deferred: &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq}, PlannedState: &tfsdk.State{ Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), @@ -6085,7 +6085,7 @@ func TestServerPlanResourceChange(t *testing.T) { ResourceSchema: testSchema, Resource: &testprovider.ResourceWithModifyPlan{ ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { - resp.DeferredResponse = &resource.DeferredResponse{Reason: resource.DeferredReasonAbsentPrereq} + resp.Deferred = &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq} }, }, }, @@ -6095,7 +6095,7 @@ func TestServerPlanResourceChange(t *testing.T) { "Resource Deferral Not Allowed", "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(resource.ModifyPlanResponse).DeferredResponse can only be set if (resource.ModifyPlanRequest.ClientCapabilities.DeferralAllowed is true.", + "(resource.ModifyPlanResponse).Deferred can only be set if (resource.ModifyPlanRequest.ClientCapabilities.DeferralAllowed is true.", ), }, PlannedState: &tfsdk.State{ diff --git a/internal/fwserver/server_readdatasource.go b/internal/fwserver/server_readdatasource.go index 4ff33fe45..6fae4b9c6 100644 --- a/internal/fwserver/server_readdatasource.go +++ b/internal/fwserver/server_readdatasource.go @@ -27,7 +27,7 @@ type ReadDataSourceRequest struct { // ReadDataSourceResponse is the framework server response for the // ReadDataSource RPC. type ReadDataSourceResponse struct { - Deferred *datasource.DeferredResponse + Deferred *datasource.Deferred Diagnostics diag.Diagnostics State *tfsdk.State } @@ -87,7 +87,7 @@ func (s *Server) ReadDataSource(ctx context.Context, req *ReadDataSourceRequest, resp.Diagnostics = readResp.Diagnostics resp.State = &readResp.State - resp.Deferred = readResp.DeferredResponse + resp.Deferred = readResp.Deferred if resp.Diagnostics.HasError() { return @@ -98,7 +98,7 @@ func (s *Server) ReadDataSource(ctx context.Context, req *ReadDataSourceRequest, "Data Source Deferral Not Allowed", "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(datasource.ReadResponse).DeferredResponse can only be set if (datasource.ReadRequest.ClientCapabilities).DeferralAllowed is true.", + "(datasource.ReadResponse).Deferred can only be set if (datasource.ReadRequest.ClientCapabilities).DeferralAllowed is true.", ) return } diff --git a/internal/fwserver/server_readdatasource_test.go b/internal/fwserver/server_readdatasource_test.go index 6ad360b5d..f946c9314 100644 --- a/internal/fwserver/server_readdatasource_test.go +++ b/internal/fwserver/server_readdatasource_test.go @@ -370,7 +370,7 @@ func TestServerReadDataSource(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - resp.DeferredResponse = &datasource.DeferredResponse{Reason: datasource.DeferredReasonAbsentPrereq} + resp.Deferred = &datasource.Deferred{Reason: datasource.DeferredReasonAbsentPrereq} if config.TestRequired.ValueString() != "test-config-value" { resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) @@ -381,7 +381,7 @@ func TestServerReadDataSource(t *testing.T) { }, expectedResponse: &fwserver.ReadDataSourceResponse{ State: testStateUnchanged, - Deferred: &datasource.DeferredResponse{Reason: datasource.DeferredReasonAbsentPrereq}, + Deferred: &datasource.Deferred{Reason: datasource.DeferredReasonAbsentPrereq}, }, }, "request-deferral-not-allowed-response-deferral": { @@ -400,7 +400,7 @@ func TestServerReadDataSource(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - resp.DeferredResponse = &datasource.DeferredResponse{Reason: datasource.DeferredReasonAbsentPrereq} + resp.Deferred = &datasource.Deferred{Reason: datasource.DeferredReasonAbsentPrereq} if config.TestRequired.ValueString() != "test-config-value" { resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) @@ -414,11 +414,11 @@ func TestServerReadDataSource(t *testing.T) { "Data Source Deferral Not Allowed", "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(datasource.ReadResponse).DeferredResponse can only be set if (datasource.ReadRequest.ClientCapabilities).DeferralAllowed is true.", + "(datasource.ReadResponse).Deferred can only be set if (datasource.ReadRequest.ClientCapabilities).DeferralAllowed is true.", ), }, State: testStateUnchanged, - Deferred: &datasource.DeferredResponse{Reason: datasource.DeferredReasonAbsentPrereq}, + Deferred: &datasource.Deferred{Reason: datasource.DeferredReasonAbsentPrereq}, }, }, } diff --git a/internal/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index 1991cbfd0..c3aa84afd 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -27,7 +27,7 @@ type ReadResourceRequest struct { // ReadResourceResponse is the framework server response for the // ReadResource RPC. type ReadResourceResponse struct { - Deferred *resource.DeferredResponse + Deferred *resource.Deferred Diagnostics diag.Diagnostics NewState *tfsdk.State Private *privatestate.Data @@ -109,7 +109,7 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res resp.Diagnostics = readResp.Diagnostics resp.NewState = &readResp.State - resp.Deferred = readResp.DeferredResponse + resp.Deferred = readResp.Deferred if readResp.Private != nil { if resp.Private == nil { @@ -128,7 +128,7 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res "Resource Deferral Not Allowed", "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(resource.ReadResponse).DeferredResponse can only be set if (resource.ReadRequest.ClientCapabilities).DeferralAllowed is true.", + "(resource.ReadResponse).Deferred can only be set if (resource.ReadRequest.ClientCapabilities).DeferralAllowed is true.", ) return } diff --git a/internal/fwserver/server_readresource_test.go b/internal/fwserver/server_readresource_test.go index 2708ad22a..4ecacd20d 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -529,7 +529,7 @@ func TestServerReadResource(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - resp.DeferredResponse = &resource.DeferredResponse{Reason: resource.DeferredReasonAbsentPrereq} + resp.Deferred = &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq} if data.TestRequired.ValueString() != "test-currentstate-value" { resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) @@ -541,7 +541,7 @@ func TestServerReadResource(t *testing.T) { expectedResponse: &fwserver.ReadResourceResponse{ NewState: testCurrentState, Private: testEmptyPrivate, - Deferred: &resource.DeferredResponse{Reason: resource.DeferredReasonAbsentPrereq}, + Deferred: &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq}, }, }, "request-deferral-not-allowed-response-deferral": { @@ -559,7 +559,7 @@ func TestServerReadResource(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - resp.DeferredResponse = &resource.DeferredResponse{Reason: resource.DeferredReasonAbsentPrereq} + resp.Deferred = &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq} if data.TestRequired.ValueString() != "test-currentstate-value" { resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) @@ -573,12 +573,12 @@ func TestServerReadResource(t *testing.T) { "Resource Deferral Not Allowed", "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(resource.ReadResponse).DeferredResponse can only be set if (resource.ReadRequest.ClientCapabilities).DeferralAllowed is true.", + "(resource.ReadResponse).Deferred can only be set if (resource.ReadRequest.ClientCapabilities).DeferralAllowed is true.", ), }, NewState: testCurrentState, Private: testEmptyPrivate, - Deferred: &resource.DeferredResponse{Reason: resource.DeferredReasonAbsentPrereq}, + Deferred: &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq}, }, }, } diff --git a/internal/toproto5/planresourcechange_test.go b/internal/toproto5/planresourcechange_test.go index bf0ddfff4..d4ed9e674 100644 --- a/internal/toproto5/planresourcechange_test.go +++ b/internal/toproto5/planresourcechange_test.go @@ -70,7 +70,7 @@ func TestPlanResourceChangeResponse(t *testing.T) { testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) - testDeferred := &resource.DeferredResponse{ + testDeferred := &resource.Deferred{ Reason: resource.DeferredReasonAbsentPrereq, } diff --git a/internal/toproto5/readdatasource_test.go b/internal/toproto5/readdatasource_test.go index 40e42d131..bc39fd51a 100644 --- a/internal/toproto5/readdatasource_test.go +++ b/internal/toproto5/readdatasource_test.go @@ -34,7 +34,7 @@ func TestReadDataSourceResponse(t *testing.T) { testProto5DynamicValue, err := tfprotov5.NewDynamicValue(testProto5Type, testProto5Value) - testDeferral := &datasource.DeferredResponse{ + testDeferral := &datasource.Deferred{ Reason: datasource.DeferredReasonAbsentPrereq, } diff --git a/internal/toproto5/readresource_test.go b/internal/toproto5/readresource_test.go index f9b3dce77..582e7090a 100644 --- a/internal/toproto5/readresource_test.go +++ b/internal/toproto5/readresource_test.go @@ -69,7 +69,7 @@ func TestReadResourceResponse(t *testing.T) { }, } - testDeferral := &resource.DeferredResponse{ + testDeferral := &resource.Deferred{ Reason: resource.DeferredReasonAbsentPrereq, } diff --git a/internal/toproto6/planresourcechange_test.go b/internal/toproto6/planresourcechange_test.go index d94f9c148..ad8376e16 100644 --- a/internal/toproto6/planresourcechange_test.go +++ b/internal/toproto6/planresourcechange_test.go @@ -70,7 +70,7 @@ func TestPlanResourceChangeResponse(t *testing.T) { testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) - testDeferred := &resource.DeferredResponse{ + testDeferred := &resource.Deferred{ Reason: resource.DeferredReasonAbsentPrereq, } diff --git a/internal/toproto6/readdatasource_test.go b/internal/toproto6/readdatasource_test.go index fb1ae4dd5..593452609 100644 --- a/internal/toproto6/readdatasource_test.go +++ b/internal/toproto6/readdatasource_test.go @@ -34,7 +34,7 @@ func TestReadDataSourceResponse(t *testing.T) { testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value) - testDeferral := &datasource.DeferredResponse{ + testDeferral := &datasource.Deferred{ Reason: datasource.DeferredReasonAbsentPrereq, } diff --git a/internal/toproto6/readresource_test.go b/internal/toproto6/readresource_test.go index cfd856c7b..46d928c31 100644 --- a/internal/toproto6/readresource_test.go +++ b/internal/toproto6/readresource_test.go @@ -69,7 +69,7 @@ func TestReadResourceResponse(t *testing.T) { testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) - testDeferral := &resource.DeferredResponse{ + testDeferral := &resource.Deferred{ Reason: resource.DeferredReasonAbsentPrereq, } diff --git a/resource/deferred.go b/resource/deferred.go index 0009f6872..ca8d8d4ed 100644 --- a/resource/deferred.go +++ b/resource/deferred.go @@ -20,11 +20,11 @@ const ( DeferredReasonAbsentPrereq DeferredReason = 3 ) -// DeferredResponse is used to indicate to Terraform that a change needs to be deferred for a reason. +// Deferred is used to indicate to Terraform that a change needs to be deferred for a reason. // // 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. -type DeferredResponse struct { +type Deferred struct { // Reason is the reason for deferring the change. Reason DeferredReason } diff --git a/resource/import_state.go b/resource/import_state.go index 0c425715b..63f1b9fc5 100644 --- a/resource/import_state.go +++ b/resource/import_state.go @@ -61,7 +61,7 @@ type ImportStateResponse struct { // data during the resource's Import operation. Private *privatestate.ProviderData - // DeferredResponse indicates that Terraform should defer + // Deferred indicates that Terraform should defer // importing this resource. // // This field can only be set if @@ -69,7 +69,7 @@ type ImportStateResponse struct { // // 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. - DeferredResponse *DeferredResponse + Deferred *Deferred } // ImportStatePassthroughID is a helper function to set the import diff --git a/resource/modify_plan.go b/resource/modify_plan.go index 28d9fc0f7..26540513b 100644 --- a/resource/modify_plan.go +++ b/resource/modify_plan.go @@ -82,7 +82,7 @@ type ModifyPlanResponse struct { // generated. Diagnostics diag.Diagnostics - // DeferredResponse indicates that Terraform should defer + // Deferred indicates that Terraform should defer // importing this resource. // // This field can only be set if @@ -90,5 +90,5 @@ type ModifyPlanResponse struct { // // 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. - DeferredResponse *DeferredResponse + Deferred *Deferred } diff --git a/resource/read.go b/resource/read.go index 7e61aac55..4e4240036 100644 --- a/resource/read.go +++ b/resource/read.go @@ -67,7 +67,7 @@ type ReadResponse struct { // warnings or errors generated. Diagnostics diag.Diagnostics - // DeferredResponse indicates that Terraform should defer + // Deferred indicates that Terraform should defer // importing this resource. // // This field can only be set if @@ -75,5 +75,5 @@ type ReadResponse struct { // // 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. - DeferredResponse *DeferredResponse + Deferred *Deferred } From 96166fb831153878e4a4b80fa86cf84b70fc824d Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 13 May 2024 17:11:38 -0400 Subject: [PATCH 18/44] Remove error handling for deferral response without deferral capability --- .../fwserver/server_importresourcestate.go | 10 ---- .../server_importresourcestate_test.go | 37 +------------- .../fwserver/server_planresourcechange.go | 10 ---- .../server_planresourcechange_test.go | 48 +------------------ internal/fwserver/server_readdatasource.go | 10 ---- .../fwserver/server_readdatasource_test.go | 39 +-------------- internal/fwserver/server_readresource.go | 10 ---- internal/fwserver/server_readresource_test.go | 39 +-------------- 8 files changed, 4 insertions(+), 199 deletions(-) diff --git a/internal/fwserver/server_importresourcestate.go b/internal/fwserver/server_importresourcestate.go index 0a2c91e2c..54782c572 100644 --- a/internal/fwserver/server_importresourcestate.go +++ b/internal/fwserver/server_importresourcestate.go @@ -120,16 +120,6 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta return } - if !importReq.ClientCapabilities.DeferralAllowed && importResp.Deferred != nil { - resp.Diagnostics.AddError( - "Resource Import Deferral Not Allowed", - "An unexpected error was encountered when importing the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ - "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(resource.ImportStateResponse).Deferred can only be set if (resource.ImportStateRequest.ClientCapabilities).DeferralAllowed is true.", - ) - return - } - if importResp.State.Raw.Equal(req.EmptyState.Raw) { resp.Diagnostics.AddError( "Missing Resource Import State", diff --git a/internal/fwserver/server_importresourcestate_test.go b/internal/fwserver/server_importresourcestate_test.go index d1f7dc097..6d5ad88a8 100644 --- a/internal/fwserver/server_importresourcestate_test.go +++ b/internal/fwserver/server_importresourcestate_test.go @@ -303,7 +303,7 @@ func TestServerImportResourceState(t *testing.T) { }, }, }, - "request-deferral-allowed-response-deferral": { + "response-importedresources-deferral": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, @@ -339,41 +339,6 @@ func TestServerImportResourceState(t *testing.T) { Deferred: &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq}, }, }, - "request-deferral-not-allowed-response-deferral": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.ImportResourceStateRequest{ - EmptyState: *testEmptyState, - ID: "test-id", - Resource: &testprovider.ResourceWithImportState{ - Resource: &testprovider.Resource{}, - ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - if req.ID != "test-id" { - resp.Diagnostics.AddError("unexpected req.ID value: %s", req.ID) - } - - resp.Deferred = &resource.Deferred{ - Reason: resource.DeferredReasonAbsentPrereq, - } - - resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) - - }, - }, - TypeName: "test_resource", - }, - expectedResponse: &fwserver.ImportResourceStateResponse{ - Diagnostics: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Resource Import Deferral Not Allowed", - "An unexpected error was encountered when importing the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ - "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(resource.ImportStateResponse).Deferred can only be set if (resource.ImportStateRequest.ClientCapabilities).DeferralAllowed is true.", - ), - }, - }, - }, } for name, testCase := range testCases { diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index 77df93039..84477e33d 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -287,16 +287,6 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange resourceWithModifyPlan.ModifyPlan(ctx, modifyPlanReq, &modifyPlanResp) logging.FrameworkTrace(ctx, "Called provider defined Resource ModifyPlan") - if !modifyPlanReq.ClientCapabilities.DeferralAllowed && modifyPlanResp.Deferred != nil { - resp.Diagnostics.AddError( - "Resource Deferral Not Allowed", - "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ - "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(resource.ModifyPlanResponse).Deferred can only be set if (resource.ModifyPlanRequest.ClientCapabilities.DeferralAllowed is true.", - ) - return - } - resp.Diagnostics = modifyPlanResp.Diagnostics resp.PlannedState = planToState(modifyPlanResp.Plan) resp.RequiresReplace = append(resp.RequiresReplace, modifyPlanResp.RequiresReplace...) diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index 60c6e451a..8342f2c9f 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -6019,7 +6019,7 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testPrivateProvider, }, }, - "create-resourcewithmodifyplan-request-deferral-allowed-response-deferral": { + "create-resourcewithmodifyplan-response-deferral": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, @@ -6062,52 +6062,6 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testEmptyPrivate, }, }, - "create-resourcewithmodifyplan-request-deferral-not-allowed-response-deferral": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.PlanResourceChangeRequest{ - Config: &tfsdk.Config{ - Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ - "test_computed": tftypes.NewValue(tftypes.String, nil), - "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), - }), - Schema: testSchema, - }, - ProposedNewState: &tfsdk.Plan{ - Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ - "test_computed": tftypes.NewValue(tftypes.String, nil), - "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), - }), - Schema: testSchema, - }, - PriorState: testEmptyState, - ResourceSchema: testSchema, - Resource: &testprovider.ResourceWithModifyPlan{ - ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { - resp.Deferred = &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq} - }, - }, - }, - expectedResponse: &fwserver.PlanResourceChangeResponse{ - Diagnostics: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Resource Deferral Not Allowed", - "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ - "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(resource.ModifyPlanResponse).Deferred can only be set if (resource.ModifyPlanRequest.ClientCapabilities.DeferralAllowed is true.", - ), - }, - PlannedState: &tfsdk.State{ - Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ - "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), - "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), - }), - Schema: testSchema, - }, - PlannedPrivate: testEmptyPrivate, - }, - }, } for name, testCase := range testCases { diff --git a/internal/fwserver/server_readdatasource.go b/internal/fwserver/server_readdatasource.go index 6fae4b9c6..c640179d4 100644 --- a/internal/fwserver/server_readdatasource.go +++ b/internal/fwserver/server_readdatasource.go @@ -93,16 +93,6 @@ func (s *Server) ReadDataSource(ctx context.Context, req *ReadDataSourceRequest, return } - if (req.ClientCapabilities == nil || !req.ClientCapabilities.DeferralAllowed) && resp.Deferred != nil { - resp.Diagnostics.AddError( - "Data Source Deferral Not Allowed", - "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ - "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(datasource.ReadResponse).Deferred can only be set if (datasource.ReadRequest.ClientCapabilities).DeferralAllowed is true.", - ) - return - } - semanticEqualityReq := SchemaSemanticEqualityRequest{ PriorData: fwschemadata.Data{ Description: fwschemadata.DataDescriptionConfiguration, diff --git a/internal/fwserver/server_readdatasource_test.go b/internal/fwserver/server_readdatasource_test.go index f946c9314..11987b211 100644 --- a/internal/fwserver/server_readdatasource_test.go +++ b/internal/fwserver/server_readdatasource_test.go @@ -354,7 +354,7 @@ func TestServerReadDataSource(t *testing.T) { }, }, }, - "request-deferral-allowed-response-deferral": { + "response-deferral": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, @@ -384,43 +384,6 @@ func TestServerReadDataSource(t *testing.T) { Deferred: &datasource.Deferred{Reason: datasource.DeferredReasonAbsentPrereq}, }, }, - "request-deferral-not-allowed-response-deferral": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.ReadDataSourceRequest{ - Config: testConfig, - DataSourceSchema: testSchema, - DataSource: &testprovider.DataSource{ - ReadMethod: func(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var config struct { - TestComputed types.String `tfsdk:"test_computed"` - TestRequired types.String `tfsdk:"test_required"` - } - - resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - - resp.Deferred = &datasource.Deferred{Reason: datasource.DeferredReasonAbsentPrereq} - - if config.TestRequired.ValueString() != "test-config-value" { - resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) - } - }, - }, - }, - expectedResponse: &fwserver.ReadDataSourceResponse{ - Diagnostics: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Data Source Deferral Not Allowed", - "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ - "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(datasource.ReadResponse).Deferred can only be set if (datasource.ReadRequest.ClientCapabilities).DeferralAllowed is true.", - ), - }, - State: testStateUnchanged, - Deferred: &datasource.Deferred{Reason: datasource.DeferredReasonAbsentPrereq}, - }, - }, } for name, testCase := range testCases { diff --git a/internal/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index c3aa84afd..f2998c4d1 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -123,16 +123,6 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res return } - if (req.ClientCapabilities == nil || !req.ClientCapabilities.DeferralAllowed) && resp.Deferred != nil { - resp.Diagnostics.AddError( - "Resource Deferral Not Allowed", - "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ - "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(resource.ReadResponse).Deferred can only be set if (resource.ReadRequest.ClientCapabilities).DeferralAllowed is true.", - ) - return - } - semanticEqualityReq := SchemaSemanticEqualityRequest{ PriorData: fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, diff --git a/internal/fwserver/server_readresource_test.go b/internal/fwserver/server_readresource_test.go index 4ecacd20d..8ab2d910c 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -514,7 +514,7 @@ func TestServerReadResource(t *testing.T) { Private: testPrivate, }, }, - "request-deferral-allowed-response-deferral": { + "response-deferral": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, @@ -544,43 +544,6 @@ func TestServerReadResource(t *testing.T) { Deferred: &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq}, }, }, - "request-deferral-not-allowed-response-deferral": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.ReadResourceRequest{ - CurrentState: testCurrentState, - Resource: &testprovider.Resource{ - ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data struct { - TestComputed types.String `tfsdk:"test_computed"` - TestRequired types.String `tfsdk:"test_required"` - } - - resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - - resp.Deferred = &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq} - - if data.TestRequired.ValueString() != "test-currentstate-value" { - resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) - } - }, - }, - }, - expectedResponse: &fwserver.ReadResourceResponse{ - Diagnostics: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Resource Deferral Not Allowed", - "An unexpected error was encountered when reading the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ - "The resource requested a deferral but the Terraform client does not support deferrals, "+ - "(resource.ReadResponse).Deferred can only be set if (resource.ReadRequest.ClientCapabilities).DeferralAllowed is true.", - ), - }, - NewState: testCurrentState, - Private: testEmptyPrivate, - Deferred: &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq}, - }, - }, } for name, testCase := range testCases { From 0a6b8a558992735f73c8a208578c7c635211e1d7 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 13 May 2024 17:16:28 -0400 Subject: [PATCH 19/44] Remove variable indirection in tests --- internal/fromproto5/planresourcechange_test.go | 6 +++--- internal/fromproto5/readdatasource_test.go | 6 +++--- internal/fromproto5/readresource_test.go | 6 +++--- internal/fromproto6/planresourcechange_test.go | 6 +++--- internal/fromproto6/readdatasource_test.go | 6 +++--- internal/fromproto6/readresource_test.go | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/internal/fromproto5/planresourcechange_test.go b/internal/fromproto5/planresourcechange_test.go index e629b5d38..3b9e9f861 100644 --- a/internal/fromproto5/planresourcechange_test.go +++ b/internal/fromproto5/planresourcechange_test.go @@ -54,8 +54,6 @@ func TestPlanResourceChangeRequest(t *testing.T) { testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) - testClientCapabilities := tfprotov5.PlanResourceChangeClientCapabilities{DeferralAllowed: true} - testCases := map[string]struct { input *tfprotov5.PlanResourceChangeRequest resourceSchema fwschema.Schema @@ -221,7 +219,9 @@ func TestPlanResourceChangeRequest(t *testing.T) { }, "client-capabilities": { input: &tfprotov5.PlanResourceChangeRequest{ - ClientCapabilities: &testClientCapabilities, + ClientCapabilities: &tfprotov5.PlanResourceChangeClientCapabilities{ + DeferralAllowed: true, + }, }, resourceSchema: testFwSchema, expected: &fwserver.PlanResourceChangeRequest{ diff --git a/internal/fromproto5/readdatasource_test.go b/internal/fromproto5/readdatasource_test.go index a398674da..aaa9ccd0f 100644 --- a/internal/fromproto5/readdatasource_test.go +++ b/internal/fromproto5/readdatasource_test.go @@ -47,8 +47,6 @@ func TestReadDataSourceRequest(t *testing.T) { }, } - testClientCapabilities := tfprotov5.ReadDataSourceClientCapabilities{DeferralAllowed: true} - testCases := map[string]struct { input *tfprotov5.ReadDataSourceRequest dataSourceSchema fwschema.Schema @@ -140,7 +138,9 @@ func TestReadDataSourceRequest(t *testing.T) { }, "client-capabilities": { input: &tfprotov5.ReadDataSourceRequest{ - ClientCapabilities: &testClientCapabilities, + ClientCapabilities: &tfprotov5.ReadDataSourceClientCapabilities{ + DeferralAllowed: true, + }, }, dataSourceSchema: testFwSchema, expected: &fwserver.ReadDataSourceRequest{ diff --git a/internal/fromproto5/readresource_test.go b/internal/fromproto5/readresource_test.go index 28aadec15..00fbbeb27 100644 --- a/internal/fromproto5/readresource_test.go +++ b/internal/fromproto5/readresource_test.go @@ -56,8 +56,6 @@ func TestReadResourceRequest(t *testing.T) { testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) - testClientCapabilities := tfprotov5.ReadResourceClientCapabilities{DeferralAllowed: true} - testCases := map[string]struct { input *tfprotov5.ReadResourceRequest resourceSchema fwschema.Schema @@ -176,7 +174,9 @@ func TestReadResourceRequest(t *testing.T) { }, "client-capabilities": { input: &tfprotov5.ReadResourceRequest{ - ClientCapabilities: &testClientCapabilities, + ClientCapabilities: &tfprotov5.ReadResourceClientCapabilities{ + DeferralAllowed: true, + }, }, resourceSchema: testFwSchema, expected: &fwserver.ReadResourceRequest{ diff --git a/internal/fromproto6/planresourcechange_test.go b/internal/fromproto6/planresourcechange_test.go index 5b96bbe79..5edfd149e 100644 --- a/internal/fromproto6/planresourcechange_test.go +++ b/internal/fromproto6/planresourcechange_test.go @@ -54,8 +54,6 @@ func TestPlanResourceChangeRequest(t *testing.T) { testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) - testClientCapabilities := tfprotov6.PlanResourceChangeClientCapabilities{DeferralAllowed: true} - testCases := map[string]struct { input *tfprotov6.PlanResourceChangeRequest resourceSchema fwschema.Schema @@ -221,7 +219,9 @@ func TestPlanResourceChangeRequest(t *testing.T) { }, "client-capabilities": { input: &tfprotov6.PlanResourceChangeRequest{ - ClientCapabilities: &testClientCapabilities, + ClientCapabilities: &tfprotov6.PlanResourceChangeClientCapabilities{ + DeferralAllowed: true, + }, }, resourceSchema: testFwSchema, expected: &fwserver.PlanResourceChangeRequest{ diff --git a/internal/fromproto6/readdatasource_test.go b/internal/fromproto6/readdatasource_test.go index 73332b45a..869092f60 100644 --- a/internal/fromproto6/readdatasource_test.go +++ b/internal/fromproto6/readdatasource_test.go @@ -47,8 +47,6 @@ func TestReadDataSourceRequest(t *testing.T) { }, } - testClientCapabilities := tfprotov6.ReadDataSourceClientCapabilities{DeferralAllowed: true} - testCases := map[string]struct { input *tfprotov6.ReadDataSourceRequest dataSourceSchema fwschema.Schema @@ -140,7 +138,9 @@ func TestReadDataSourceRequest(t *testing.T) { }, "client-capabilities": { input: &tfprotov6.ReadDataSourceRequest{ - ClientCapabilities: &testClientCapabilities, + ClientCapabilities: &tfprotov6.ReadDataSourceClientCapabilities{ + DeferralAllowed: true, + }, }, dataSourceSchema: testFwSchema, expected: &fwserver.ReadDataSourceRequest{ diff --git a/internal/fromproto6/readresource_test.go b/internal/fromproto6/readresource_test.go index a6a238188..6a3eeab98 100644 --- a/internal/fromproto6/readresource_test.go +++ b/internal/fromproto6/readresource_test.go @@ -56,8 +56,6 @@ func TestReadResourceRequest(t *testing.T) { testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) - testClientCapabilities := tfprotov6.ReadResourceClientCapabilities{DeferralAllowed: true} - testCases := map[string]struct { input *tfprotov6.ReadResourceRequest resourceSchema fwschema.Schema @@ -176,7 +174,9 @@ func TestReadResourceRequest(t *testing.T) { }, "client-capabilities": { input: &tfprotov6.ReadResourceRequest{ - ClientCapabilities: &testClientCapabilities, + ClientCapabilities: &tfprotov6.ReadResourceClientCapabilities{ + DeferralAllowed: true, + }, }, resourceSchema: testFwSchema, expected: &fwserver.ReadResourceRequest{ From f1112d5d8e6b51fd51f0a898e62fa54ba2a94c6a Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 13 May 2024 17:17:57 -0400 Subject: [PATCH 20/44] Add copyright headers --- internal/fromproto5/client_capabilities.go | 3 +++ internal/fromproto6/client_capabilities.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/internal/fromproto5/client_capabilities.go b/internal/fromproto5/client_capabilities.go index fde821ac9..8faebd822 100644 --- a/internal/fromproto5/client_capabilities.go +++ b/internal/fromproto5/client_capabilities.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package fromproto5 import ( diff --git a/internal/fromproto6/client_capabilities.go b/internal/fromproto6/client_capabilities.go index c2f54e498..eac8ff85d 100644 --- a/internal/fromproto6/client_capabilities.go +++ b/internal/fromproto6/client_capabilities.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package fromproto6 import ( From 2433dcaa4edf8602d81d50581d5dd504a7a12304 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 14 May 2024 13:33:11 -0400 Subject: [PATCH 21/44] Apply suggestions from code review Co-authored-by: Brian Flad --- datasource/read.go | 4 ++-- resource/modify_plan.go | 4 ++-- resource/read.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/datasource/read.go b/datasource/read.go index 751b9754f..43e002368 100644 --- a/datasource/read.go +++ b/datasource/read.go @@ -54,8 +54,8 @@ type ReadResponse struct { // warnings or errors generated. Diagnostics diag.Diagnostics - // Deferred indicates that Terraform should defer - // reading this data source. + // Deferred indicates that Terraform should defer reading this + // data source until a followup apply operation. // // This field can only be set if // `(datasource.ReadRequest).ClientCapabilities.DeferralAllowed` is true. diff --git a/resource/modify_plan.go b/resource/modify_plan.go index 26540513b..28843cec1 100644 --- a/resource/modify_plan.go +++ b/resource/modify_plan.go @@ -82,8 +82,8 @@ type ModifyPlanResponse struct { // generated. Diagnostics diag.Diagnostics - // Deferred indicates that Terraform should defer - // importing this resource. + // Deferred indicates that Terraform should defer importing this + // resource until a followup apply operation. // // This field can only be set if // `(resource.ModifyPlanRequest).ClientCapabilities.DeferralAllowed` is true. diff --git a/resource/read.go b/resource/read.go index 4e4240036..53c4cb832 100644 --- a/resource/read.go +++ b/resource/read.go @@ -67,8 +67,8 @@ type ReadResponse struct { // warnings or errors generated. Diagnostics diag.Diagnostics - // Deferred indicates that Terraform should defer - // importing this resource. + // Deferred indicates that Terraform should defer refreshing this + // resource until a followup plan operation. // // This field can only be set if // `(resource.ReadRequest).ClientCapabilities.DeferralAllowed` is true. From 50aaca97b974b3084f8119bbd949058b67049bcf Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 14 May 2024 14:10:29 -0400 Subject: [PATCH 22/44] Add unit tests for client capabilities --- .../server_importresourcestate_test.go | 127 +++++++++---- .../server_planresourcechange_test.go | 171 +++++++++++++----- .../fwserver/server_readdatasource_test.go | 115 +++++++++--- internal/fwserver/server_readresource_test.go | 113 +++++++++--- 4 files changed, 390 insertions(+), 136 deletions(-) diff --git a/internal/fwserver/server_importresourcestate_test.go b/internal/fwserver/server_importresourcestate_test.go index 6d5ad88a8..14e63148a 100644 --- a/internal/fwserver/server_importresourcestate_test.go +++ b/internal/fwserver/server_importresourcestate_test.go @@ -99,6 +99,67 @@ func TestServerImportResourceState(t *testing.T) { }, expectedResponse: &fwserver.ImportResourceStateResponse{}, }, + "request-client-capabilities-deferral-allowed": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + ClientCapabilities: testDeferral, + EmptyState: *testEmptyState, + ID: "test-id", + Resource: &testprovider.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + if req.ClientCapabilities.DeferralAllowed != true { + resp.Diagnostics.AddError("Unexpected req.ClientCapabilities.DeferralAllowed value", + "expected: true but got: false") + } + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: *testState, + TypeName: "test_resource", + Private: testEmptyPrivate, + }, + }, + }, + }, + "request-client-capabilities-unset": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyState, + ID: "test-id", + Resource: &testprovider.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + if req.ClientCapabilities.DeferralAllowed != false { + resp.Diagnostics.AddError("Unexpected req.ClientCapabilities.DeferralAllowed value", + "expected: false but got: true") + } + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: *testState, + TypeName: "test_resource", + Private: testEmptyPrivate, + }, + }, + }, + }, "request-id": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -249,7 +310,7 @@ func TestServerImportResourceState(t *testing.T) { }, }, }, - "response-importedresources-private": { + "response-importedresources-deferral": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, @@ -259,26 +320,33 @@ func TestServerImportResourceState(t *testing.T) { Resource: &testprovider.ResourceWithImportState{ Resource: &testprovider.Resource{}, ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + if req.ID != "test-id" { + resp.Diagnostics.AddError("unexpected req.ID value: %s", req.ID) + } - resp.Diagnostics.Append(diags...) + resp.Deferred = &resource.Deferred{ + Reason: resource.DeferredReasonAbsentPrereq, + } resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + }, }, - TypeName: "test_resource", + TypeName: "test_resource", + ClientCapabilities: testDeferral, }, expectedResponse: &fwserver.ImportResourceStateResponse{ ImportedResources: []fwserver.ImportedResource{ { State: *testState, TypeName: "test_resource", - Private: testPrivate, + Private: testEmptyPrivate, }, }, + Deferred: &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq}, }, }, - "response-importedresources-empty-state": { + "response-importedresources-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, @@ -288,22 +356,26 @@ func TestServerImportResourceState(t *testing.T) { Resource: &testprovider.ResourceWithImportState{ Resource: &testprovider.Resource{}, ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - // Intentionally empty + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) }, }, TypeName: "test_resource", }, expectedResponse: &fwserver.ImportResourceStateResponse{ - Diagnostics: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Missing Resource Import State", - "An unexpected error was encountered when importing the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ - "Resource ImportState method returned no State in response. If import is intentionally not supported, remove the Resource type ImportState method or return an error.", - ), + ImportedResources: []fwserver.ImportedResource{ + { + State: *testState, + TypeName: "test_resource", + Private: testPrivate, + }, }, }, }, - "response-importedresources-deferral": { + "response-importedresources-empty-state": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, @@ -313,30 +385,19 @@ func TestServerImportResourceState(t *testing.T) { Resource: &testprovider.ResourceWithImportState{ Resource: &testprovider.Resource{}, ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - if req.ID != "test-id" { - resp.Diagnostics.AddError("unexpected req.ID value: %s", req.ID) - } - - resp.Deferred = &resource.Deferred{ - Reason: resource.DeferredReasonAbsentPrereq, - } - - resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) - + // Intentionally empty }, }, - TypeName: "test_resource", - ClientCapabilities: testDeferral, + TypeName: "test_resource", }, expectedResponse: &fwserver.ImportResourceStateResponse{ - ImportedResources: []fwserver.ImportedResource{ - { - State: *testState, - TypeName: "test_resource", - Private: testEmptyPrivate, - }, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Resource Import State", + "An unexpected error was encountered when importing the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ + "Resource ImportState method returned no State in response. If import is intentionally not supported, remove the Resource type ImportState method or return an error.", + ), }, - Deferred: &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq}, }, }, } diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index 8342f2c9f..dfbe15ab1 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -2783,6 +2783,91 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testEmptyPrivate, }, }, + "create-resourcewithmodifyplan-request-client-capabilities-deferral-allowed": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + ClientCapabilities: testDeferralAllowed, + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PriorState: testEmptyState, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if req.ClientCapabilities.DeferralAllowed != true { + resp.Diagnostics.AddError("Unexpected req.ClientCapabilities.DeferralAllowed value", + "expected: true but got: false") + } + + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, + "create-resourcewithmodifyplan-request-client-capabilities-unset": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PriorState: testEmptyState, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if req.ClientCapabilities.DeferralAllowed != false { + resp.Diagnostics.AddError("Unexpected req.ClientCapabilities.DeferralAllowed value", + "expected: false but got: true") + } + + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, "create-resourcewithmodifyplan-request-config": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -2964,6 +3049,49 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testEmptyPrivate, }, }, + "create-resourcewithmodifyplan-response-deferral": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + ClientCapabilities: testDeferralAllowed, + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PriorState: testEmptyState, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if req.ClientCapabilities.DeferralAllowed == true { + resp.Deferred = &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq} + } + + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + Deferred: &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq}, + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, "create-resourcewithmodifyplan-response-diagnostics": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -6019,49 +6147,6 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testPrivateProvider, }, }, - "create-resourcewithmodifyplan-response-deferral": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.PlanResourceChangeRequest{ - ClientCapabilities: testDeferralAllowed, - Config: &tfsdk.Config{ - Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ - "test_computed": tftypes.NewValue(tftypes.String, nil), - "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), - }), - Schema: testSchema, - }, - ProposedNewState: &tfsdk.Plan{ - Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ - "test_computed": tftypes.NewValue(tftypes.String, nil), - "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), - }), - Schema: testSchema, - }, - PriorState: testEmptyState, - ResourceSchema: testSchema, - Resource: &testprovider.ResourceWithModifyPlan{ - ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { - if req.ClientCapabilities.DeferralAllowed == true { - resp.Deferred = &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq} - } - - }, - }, - }, - expectedResponse: &fwserver.PlanResourceChangeResponse{ - Deferred: &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq}, - PlannedState: &tfsdk.State{ - Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ - "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), - "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), - }), - Schema: testSchema, - }, - PlannedPrivate: testEmptyPrivate, - }, - }, } for name, testCase := range testCases { diff --git a/internal/fwserver/server_readdatasource_test.go b/internal/fwserver/server_readdatasource_test.go index 11987b211..6cca8863e 100644 --- a/internal/fwserver/server_readdatasource_test.go +++ b/internal/fwserver/server_readdatasource_test.go @@ -114,6 +114,61 @@ func TestServerReadDataSource(t *testing.T) { }, expectedResponse: &fwserver.ReadDataSourceResponse{}, }, + "request-client-capabilities-deferral-allowed": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadDataSourceRequest{ + ClientCapabilities: testDeferralAllowed, + Config: testConfig, + DataSourceSchema: testSchema, + DataSource: &testprovider.DataSource{ + ReadMethod: func(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + if req.ClientCapabilities.DeferralAllowed != true { + resp.Diagnostics.AddError("Unexpected req.ClientCapabilities.DeferralAllowed value", + "expected: true but got: false") + } + + var config struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + }, + }, + }, + expectedResponse: &fwserver.ReadDataSourceResponse{ + State: testStateUnchanged, + }, + }, + "request-client-capabilities-unset": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadDataSourceRequest{ + Config: testConfig, + DataSourceSchema: testSchema, + DataSource: &testprovider.DataSource{ + ReadMethod: func(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + if req.ClientCapabilities.DeferralAllowed != false { + resp.Diagnostics.AddError("Unexpected req.ClientCapabilities.DeferralAllowed value", + "expected: false but got: true") + } + + var config struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + }, + }, + }, + expectedResponse: &fwserver.ReadDataSourceResponse{ + State: testStateUnchanged, + }, + }, "request-config": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -208,6 +263,36 @@ func TestServerReadDataSource(t *testing.T) { State: testStateUnchanged, }, }, + "response-deferral": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadDataSourceRequest{ + Config: testConfig, + DataSourceSchema: testSchema, + DataSource: &testprovider.DataSource{ + ReadMethod: func(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + resp.Deferred = &datasource.Deferred{Reason: datasource.DeferredReasonAbsentPrereq} + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + }, + ClientCapabilities: testDeferralAllowed, + }, + expectedResponse: &fwserver.ReadDataSourceResponse{ + State: testStateUnchanged, + Deferred: &datasource.Deferred{Reason: datasource.DeferredReasonAbsentPrereq}, + }, + }, "response-diagnostics": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -354,36 +439,6 @@ func TestServerReadDataSource(t *testing.T) { }, }, }, - "response-deferral": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.ReadDataSourceRequest{ - Config: testConfig, - DataSourceSchema: testSchema, - DataSource: &testprovider.DataSource{ - ReadMethod: func(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var config struct { - TestComputed types.String `tfsdk:"test_computed"` - TestRequired types.String `tfsdk:"test_required"` - } - - resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - - resp.Deferred = &datasource.Deferred{Reason: datasource.DeferredReasonAbsentPrereq} - - if config.TestRequired.ValueString() != "test-config-value" { - resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) - } - }, - }, - ClientCapabilities: testDeferralAllowed, - }, - expectedResponse: &fwserver.ReadDataSourceResponse{ - State: testStateUnchanged, - Deferred: &datasource.Deferred{Reason: datasource.DeferredReasonAbsentPrereq}, - }, - }, } for name, testCase := range testCases { diff --git a/internal/fwserver/server_readresource_test.go b/internal/fwserver/server_readresource_test.go index 8ab2d910c..e05de42ca 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -150,6 +150,59 @@ func TestServerReadResource(t *testing.T) { }, expectedResponse: &fwserver.ReadResourceResponse{}, }, + "request-client-capabilities-deferral-allowed": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + ClientCapabilities: testDeferralAllowed, + CurrentState: testCurrentState, + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + if req.ClientCapabilities.DeferralAllowed != true { + resp.Diagnostics.AddError("Unexpected req.ClientCapabilities.DeferralAllowed value", + "expected: true but got: false") + } + var data struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: testCurrentState, + Private: testEmptyPrivate, + }, + }, + "request-client-capabilities-unset": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + if req.ClientCapabilities.DeferralAllowed != false { + resp.Diagnostics.AddError("Unexpected req.ClientCapabilities.DeferralAllowed value", + "expected: false but got: true") + } + var data struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: testCurrentState, + Private: testEmptyPrivate, + }, + }, "request-currentstate-missing": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -312,6 +365,36 @@ func TestServerReadResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-deferral": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + resp.Deferred = &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq} + + if data.TestRequired.ValueString() != "test-currentstate-value" { + resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) + } + }, + }, + ClientCapabilities: testDeferralAllowed, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: testCurrentState, + Private: testEmptyPrivate, + Deferred: &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq}, + }, + }, "response-diagnostics": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -514,36 +597,6 @@ func TestServerReadResource(t *testing.T) { Private: testPrivate, }, }, - "response-deferral": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.ReadResourceRequest{ - CurrentState: testCurrentState, - Resource: &testprovider.Resource{ - ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data struct { - TestComputed types.String `tfsdk:"test_computed"` - TestRequired types.String `tfsdk:"test_required"` - } - - resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - - resp.Deferred = &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq} - - if data.TestRequired.ValueString() != "test-currentstate-value" { - resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) - } - }, - }, - ClientCapabilities: testDeferralAllowed, - }, - expectedResponse: &fwserver.ReadResourceResponse{ - NewState: testCurrentState, - Private: testEmptyPrivate, - Deferred: &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq}, - }, - }, } for name, testCase := range testCases { From a705a12360b191bce57e7bc03071580dc8e62730 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 14 May 2024 16:00:29 -0400 Subject: [PATCH 23/44] Implement deferred action support for `provider.configureProvider` --- internal/fromproto5/client_capabilities.go | 13 +++++ internal/fromproto5/configureprovider.go | 6 +- internal/fromproto5/configureprovider_test.go | 17 +++++- internal/fromproto6/client_capabilities.go | 13 +++++ internal/fromproto6/configureprovider.go | 6 +- internal/fromproto6/configureprovider_test.go | 17 +++++- internal/fwserver/server.go | 5 ++ internal/fwserver/server_configureprovider.go | 1 + .../fwserver/server_configureprovider_test.go | 55 ++++++++++++++++++- provider/configure.go | 26 +++++++++ provider/deferred.go | 43 +++++++++++++++ 11 files changed, 193 insertions(+), 9 deletions(-) create mode 100644 provider/deferred.go diff --git a/internal/fromproto5/client_capabilities.go b/internal/fromproto5/client_capabilities.go index 8faebd822..778f6fc6b 100644 --- a/internal/fromproto5/client_capabilities.go +++ b/internal/fromproto5/client_capabilities.go @@ -7,9 +7,22 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" ) +func ConfigureProviderClientCapabilities(in *tfprotov5.ConfigureProviderClientCapabilities) provider.ConfigureProviderClientCapabilities { + if in == nil { + return provider.ConfigureProviderClientCapabilities{} + } + + resp := provider.ConfigureProviderClientCapabilities{ + DeferralAllowed: in.DeferralAllowed, + } + + return resp +} + func ReadDataSourceClientCapabilities(in *tfprotov5.ReadDataSourceClientCapabilities) *datasource.ReadClientCapabilities { if in == nil { return nil diff --git a/internal/fromproto5/configureprovider.go b/internal/fromproto5/configureprovider.go index 9dc0f1115..bb6b8835e 100644 --- a/internal/fromproto5/configureprovider.go +++ b/internal/fromproto5/configureprovider.go @@ -6,10 +6,11 @@ package fromproto5 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/provider" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) // ConfigureProviderRequest returns the *fwserver.ConfigureProviderRequest @@ -20,7 +21,8 @@ func ConfigureProviderRequest(ctx context.Context, proto5 *tfprotov5.ConfigurePr } fw := &provider.ConfigureRequest{ - TerraformVersion: proto5.TerraformVersion, + TerraformVersion: proto5.TerraformVersion, + ClientCapabilities: ConfigureProviderClientCapabilities(proto5.ClientCapabilities), } config, diags := Config(ctx, proto5.Config, providerSchema) diff --git a/internal/fromproto5/configureprovider_test.go b/internal/fromproto5/configureprovider_test.go index b1c6ce6a4..ab48a334a 100644 --- a/internal/fromproto5/configureprovider_test.go +++ b/internal/fromproto5/configureprovider_test.go @@ -8,14 +8,15 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestConfigureProviderRequest(t *testing.T) { @@ -94,6 +95,18 @@ func TestConfigureProviderRequest(t *testing.T) { TerraformVersion: "99.99.99", }, }, + "client-capabilities": { + input: &tfprotov5.ConfigureProviderRequest{ + ClientCapabilities: &tfprotov5.ConfigureProviderClientCapabilities{ + DeferralAllowed: true, + }, + }, + expected: &provider.ConfigureRequest{ + ClientCapabilities: provider.ConfigureProviderClientCapabilities{ + DeferralAllowed: true, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fromproto6/client_capabilities.go b/internal/fromproto6/client_capabilities.go index eac8ff85d..f803600f8 100644 --- a/internal/fromproto6/client_capabilities.go +++ b/internal/fromproto6/client_capabilities.go @@ -7,9 +7,22 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" ) +func ConfigureProviderClientCapabilities(in *tfprotov6.ConfigureProviderClientCapabilities) provider.ConfigureProviderClientCapabilities { + if in == nil { + return provider.ConfigureProviderClientCapabilities{} + } + + resp := provider.ConfigureProviderClientCapabilities{ + DeferralAllowed: in.DeferralAllowed, + } + + return resp +} + func ReadDataSourceClientCapabilities(in *tfprotov6.ReadDataSourceClientCapabilities) *datasource.ReadClientCapabilities { if in == nil { return nil diff --git a/internal/fromproto6/configureprovider.go b/internal/fromproto6/configureprovider.go index 733b76ca2..19e470a21 100644 --- a/internal/fromproto6/configureprovider.go +++ b/internal/fromproto6/configureprovider.go @@ -6,10 +6,11 @@ package fromproto6 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/provider" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) // ConfigureProviderRequest returns the *fwserver.ConfigureProviderRequest @@ -20,7 +21,8 @@ func ConfigureProviderRequest(ctx context.Context, proto6 *tfprotov6.ConfigurePr } fw := &provider.ConfigureRequest{ - TerraformVersion: proto6.TerraformVersion, + TerraformVersion: proto6.TerraformVersion, + ClientCapabilities: ConfigureProviderClientCapabilities(proto6.ClientCapabilities), } config, diags := Config(ctx, proto6.Config, providerSchema) diff --git a/internal/fromproto6/configureprovider_test.go b/internal/fromproto6/configureprovider_test.go index afceedbcd..7302ba466 100644 --- a/internal/fromproto6/configureprovider_test.go +++ b/internal/fromproto6/configureprovider_test.go @@ -8,14 +8,15 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestConfigureProviderRequest(t *testing.T) { @@ -94,6 +95,18 @@ func TestConfigureProviderRequest(t *testing.T) { TerraformVersion: "99.99.99", }, }, + "client-capabilities": { + input: &tfprotov6.ConfigureProviderRequest{ + ClientCapabilities: &tfprotov6.ConfigureProviderClientCapabilities{ + DeferralAllowed: true, + }, + }, + expected: &provider.ConfigureRequest{ + ClientCapabilities: provider.ConfigureProviderClientCapabilities{ + DeferralAllowed: true, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index 40fa631f8..0e815957b 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -56,6 +56,11 @@ type Server struct { // access from race conditions. dataSourceTypesMutex sync.Mutex + // deferred indicates an automatic provider deferral. When this is set, + // the provider will automatically defer the PlanResourceChange, ReadResource, + // ImportResourceState, and ReadDataSource RPCs. + deferred *provider.Deferred + // functionDefinitions is the cached Function Definitions for RPCs that need to // convert data from the protocol. If not found, it will be fetched from the // Function.Definition() method. diff --git a/internal/fwserver/server_configureprovider.go b/internal/fwserver/server_configureprovider.go index 3f841887f..2d4e13d74 100644 --- a/internal/fwserver/server_configureprovider.go +++ b/internal/fwserver/server_configureprovider.go @@ -22,6 +22,7 @@ func (s *Server) ConfigureProvider(ctx context.Context, req *provider.ConfigureR logging.FrameworkTrace(ctx, "Called provider defined Provider Configure") + s.deferred = resp.Deferred s.DataSourceConfigureData = resp.DataSourceData s.ResourceConfigureData = resp.ResourceData } diff --git a/internal/fwserver/server_configureprovider_test.go b/internal/fwserver/server_configureprovider_test.go index 605fe719d..6ac0b31ca 100644 --- a/internal/fwserver/server_configureprovider_test.go +++ b/internal/fwserver/server_configureprovider_test.go @@ -8,6 +8,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerConfigureProvider(t *testing.T) { @@ -56,6 +57,40 @@ func TestServerConfigureProvider(t *testing.T) { }, expectedResponse: &provider.ConfigureResponse{}, }, + "request-client-capabilities-deferral-allowed": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {}, + ConfigureMethod: func(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + if req.ClientCapabilities.DeferralAllowed != true { + resp.Diagnostics.AddError("Unexpected req.ClientCapabilities.DeferralAllowed value", + "expected: true but got: false") + } + }, + }, + }, + request: &provider.ConfigureRequest{ + ClientCapabilities: provider.ConfigureProviderClientCapabilities{ + DeferralAllowed: true, + }, + }, + expectedResponse: &provider.ConfigureResponse{}, + }, + "request-client-capabilities-unset": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {}, + ConfigureMethod: func(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + if req.ClientCapabilities.DeferralAllowed != false { + resp.Diagnostics.AddError("Unexpected req.ClientCapabilities.DeferralAllowed value", + "expected: false but got: true") + } + }, + }, + }, + request: &provider.ConfigureRequest{}, + expectedResponse: &provider.ConfigureResponse{}, + }, "request-config": { server: &fwserver.Server{ Provider: &testprovider.Provider{ @@ -112,6 +147,24 @@ func TestServerConfigureProvider(t *testing.T) { DataSourceData: "test-provider-configure-value", }, }, + "response-deferral": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {}, + ConfigureMethod: func(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + resp.Deferred = &provider.Deferred{Reason: provider.DeferredReasonProviderConfigUnknown} + resp.DataSourceData = "test-provider-configure-value" + }, + }, + }, + request: &provider.ConfigureRequest{}, + expectedResponse: &provider.ConfigureResponse{ + Deferred: &provider.Deferred{ + Reason: provider.DeferredReasonProviderConfigUnknown, + }, + DataSourceData: "test-provider-configure-value", + }, + }, "response-diagnostics": { server: &fwserver.Server{ Provider: &testprovider.Provider{ diff --git a/provider/configure.go b/provider/configure.go index 600f402ff..9b6678bf7 100644 --- a/provider/configure.go +++ b/provider/configure.go @@ -8,6 +8,18 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) +// ConfigureProviderClientCapabilities allows Terraform to publish information +// regarding optionally supported protocol features for the ConfigureProvider RPC, +// such as forward-compatible Terraform behavior changes. +type ConfigureProviderClientCapabilities struct { + // DeferralAllowed indicates whether the Terraform client initiating + // the request allows a deferral response. + // + // 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. + DeferralAllowed bool +} + // ConfigureRequest represents a request containing the values the user // specified for the provider configuration block, along with other runtime // information from Terraform or the Plugin SDK. An instance of this request @@ -24,6 +36,10 @@ type ConfigureRequest struct { // that's implementing the Provider interface, for use in later // resource CRUD operations. Config tfsdk.Config + + // ClientCapabilities defines optionally supported protocol features for the + // ConfigureProvider RPC, such as forward-compatible Terraform behavior changes. + ClientCapabilities ConfigureProviderClientCapabilities } // ConfigureResponse represents a response to a @@ -45,4 +61,14 @@ type ConfigureResponse struct { // to [resource.ConfigureRequest.ProviderData] for each Resource type // that implements the Configure method. ResourceData any + + // Deferred indicates that Terraform should automatically defer + // all resources and data sources for this provider. + // + // This field can only be set if + // `(provider.ConfigureRequest).ClientCapabilities.DeferralAllowed` is true. + // + // 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. + Deferred *Deferred } diff --git a/provider/deferred.go b/provider/deferred.go new file mode 100644 index 000000000..5c5fceb5d --- /dev/null +++ b/provider/deferred.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +// MAINTAINER NOTE: The deferred reason at enum value 1 in the plugin-protocol +// is not relevant for provider-level automatic deferred responses. +// provider.DeferredReason is directly mapped to the plugin-protocol which is +// why enum value 1 is skipped here +const ( + // DeferredReasonUnknown is used to indicate an invalid `DeferredReason`. + // Provider developers should not use it. + DeferredReasonUnknown DeferredReason = 0 + + // DeferredReasonProviderConfigUnknown is used to indicate that the provider configuration + // is partially unknown and the real values need to be known before the change can be planned. + DeferredReasonProviderConfigUnknown DeferredReason = 2 +) + +// Deferred is used to indicate to Terraform that a change needs to be deferred for a reason. +// +// 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. +type Deferred struct { + // Reason is the reason for deferring the change. + Reason DeferredReason +} + +// DeferredReason represents different reasons for deferring a change. +// +// 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. +type DeferredReason int32 + +func (d DeferredReason) String() string { + switch d { + case 0: + return "Unknown" + case 2: + return "Provider Config Unknown" + } + return "Unknown" +} From 5ab0c24a87d972487d3e50c3aa64193e963e35d9 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 14 May 2024 16:23:48 -0400 Subject: [PATCH 24/44] Move client capabilities defaulting behavior to `fromproto5/6` package --- internal/fromproto5/client_capabilities.go | 44 ++++++++++--------- .../fromproto5/importresourcestate_test.go | 15 ++++++- .../fromproto5/planresourcechange_test.go | 12 ++++- internal/fromproto5/readdatasource_test.go | 12 ++++- internal/fromproto5/readresource_test.go | 11 ++++- internal/fromproto6/client_capabilities.go | 44 ++++++++++--------- .../fromproto6/importresourcestate_test.go | 15 ++++++- .../fromproto6/planresourcechange_test.go | 12 ++++- internal/fromproto6/readdatasource_test.go | 12 ++++- internal/fromproto6/readresource_test.go | 11 ++++- .../fwserver/server_importresourcestate.go | 9 ++-- .../server_importresourcestate_test.go | 32 +------------- .../fwserver/server_planresourcechange.go | 15 +++---- .../server_planresourcechange_test.go | 44 +------------------ internal/fwserver/server_readdatasource.go | 7 +-- .../fwserver/server_readdatasource_test.go | 29 +----------- internal/fwserver/server_readresource.go | 7 +-- internal/fwserver/server_readresource_test.go | 28 +----------- 18 files changed, 157 insertions(+), 202 deletions(-) diff --git a/internal/fromproto5/client_capabilities.go b/internal/fromproto5/client_capabilities.go index 8faebd822..8f1075737 100644 --- a/internal/fromproto5/client_capabilities.go +++ b/internal/fromproto5/client_capabilities.go @@ -10,50 +10,54 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" ) -func ReadDataSourceClientCapabilities(in *tfprotov5.ReadDataSourceClientCapabilities) *datasource.ReadClientCapabilities { +func ReadDataSourceClientCapabilities(in *tfprotov5.ReadDataSourceClientCapabilities) datasource.ReadClientCapabilities { if in == nil { - return nil + // Client did not indicate any supported capabilities + return datasource.ReadClientCapabilities{ + DeferralAllowed: false, + } } - resp := &datasource.ReadClientCapabilities{ + return datasource.ReadClientCapabilities{ DeferralAllowed: in.DeferralAllowed, } - - return resp } -func ReadResourceClientCapabilities(in *tfprotov5.ReadResourceClientCapabilities) *resource.ReadClientCapabilities { +func ReadResourceClientCapabilities(in *tfprotov5.ReadResourceClientCapabilities) resource.ReadClientCapabilities { if in == nil { - return nil + // Client did not indicate any supported capabilities + return resource.ReadClientCapabilities{ + DeferralAllowed: false, + } } - resp := &resource.ReadClientCapabilities{ + return resource.ReadClientCapabilities{ DeferralAllowed: in.DeferralAllowed, } - - return resp } -func ModifyPlanClientCapabilities(in *tfprotov5.PlanResourceChangeClientCapabilities) *resource.ModifyPlanClientCapabilities { +func ModifyPlanClientCapabilities(in *tfprotov5.PlanResourceChangeClientCapabilities) resource.ModifyPlanClientCapabilities { if in == nil { - return nil + // Client did not indicate any supported capabilities + return resource.ModifyPlanClientCapabilities{ + DeferralAllowed: false, + } } - resp := &resource.ModifyPlanClientCapabilities{ + return resource.ModifyPlanClientCapabilities{ DeferralAllowed: in.DeferralAllowed, } - - return resp } -func ImportStateClientCapabilities(in *tfprotov5.ImportResourceStateClientCapabilities) *resource.ImportStateClientCapabilities { +func ImportStateClientCapabilities(in *tfprotov5.ImportResourceStateClientCapabilities) resource.ImportStateClientCapabilities { if in == nil { - return nil + // Client did not indicate any supported capabilities + return resource.ImportStateClientCapabilities{ + DeferralAllowed: false, + } } - resp := &resource.ImportStateClientCapabilities{ + return resource.ImportStateClientCapabilities{ DeferralAllowed: in.DeferralAllowed, } - - return resp } diff --git a/internal/fromproto5/importresourcestate_test.go b/internal/fromproto5/importresourcestate_test.go index f3de43bcf..ba58522af 100644 --- a/internal/fromproto5/importresourcestate_test.go +++ b/internal/fromproto5/importresourcestate_test.go @@ -98,11 +98,24 @@ func TestImportResourceStateRequest(t *testing.T) { expected: &fwserver.ImportResourceStateRequest{ EmptyState: testFwEmptyState, ID: "test-id", - ClientCapabilities: &resource.ImportStateClientCapabilities{ + ClientCapabilities: resource.ImportStateClientCapabilities{ DeferralAllowed: true, }, }, }, + "client-capabilities-unset": { + input: &tfprotov5.ImportResourceStateRequest{ + ID: "test-id", + }, + resourceSchema: testFwSchema, + expected: &fwserver.ImportResourceStateRequest{ + EmptyState: testFwEmptyState, + ID: "test-id", + ClientCapabilities: resource.ImportStateClientCapabilities{ + DeferralAllowed: false, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fromproto5/planresourcechange_test.go b/internal/fromproto5/planresourcechange_test.go index 3b9e9f861..f20a94c7e 100644 --- a/internal/fromproto5/planresourcechange_test.go +++ b/internal/fromproto5/planresourcechange_test.go @@ -225,12 +225,22 @@ func TestPlanResourceChangeRequest(t *testing.T) { }, resourceSchema: testFwSchema, expected: &fwserver.PlanResourceChangeRequest{ - ClientCapabilities: &resource.ModifyPlanClientCapabilities{ + ClientCapabilities: resource.ModifyPlanClientCapabilities{ DeferralAllowed: true, }, ResourceSchema: testFwSchema, }, }, + "client-capabilities-unset": { + input: &tfprotov5.PlanResourceChangeRequest{}, + resourceSchema: testFwSchema, + expected: &fwserver.PlanResourceChangeRequest{ + ClientCapabilities: resource.ModifyPlanClientCapabilities{ + DeferralAllowed: false, + }, + ResourceSchema: testFwSchema, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fromproto5/readdatasource_test.go b/internal/fromproto5/readdatasource_test.go index aaa9ccd0f..cbc054e2d 100644 --- a/internal/fromproto5/readdatasource_test.go +++ b/internal/fromproto5/readdatasource_test.go @@ -145,11 +145,21 @@ func TestReadDataSourceRequest(t *testing.T) { dataSourceSchema: testFwSchema, expected: &fwserver.ReadDataSourceRequest{ DataSourceSchema: testFwSchema, - ClientCapabilities: &datasource.ReadClientCapabilities{ + ClientCapabilities: datasource.ReadClientCapabilities{ DeferralAllowed: true, }, }, }, + "client-capabilities-unset": { + input: &tfprotov5.ReadDataSourceRequest{}, + dataSourceSchema: testFwSchema, + expected: &fwserver.ReadDataSourceRequest{ + DataSourceSchema: testFwSchema, + ClientCapabilities: datasource.ReadClientCapabilities{ + DeferralAllowed: false, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fromproto5/readresource_test.go b/internal/fromproto5/readresource_test.go index 00fbbeb27..fb19e9041 100644 --- a/internal/fromproto5/readresource_test.go +++ b/internal/fromproto5/readresource_test.go @@ -180,11 +180,20 @@ func TestReadResourceRequest(t *testing.T) { }, resourceSchema: testFwSchema, expected: &fwserver.ReadResourceRequest{ - ClientCapabilities: &resource.ReadClientCapabilities{ + ClientCapabilities: resource.ReadClientCapabilities{ DeferralAllowed: true, }, }, }, + "client-capabilities-unset": { + input: &tfprotov5.ReadResourceRequest{}, + resourceSchema: testFwSchema, + expected: &fwserver.ReadResourceRequest{ + ClientCapabilities: resource.ReadClientCapabilities{ + DeferralAllowed: false, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fromproto6/client_capabilities.go b/internal/fromproto6/client_capabilities.go index eac8ff85d..f459291ab 100644 --- a/internal/fromproto6/client_capabilities.go +++ b/internal/fromproto6/client_capabilities.go @@ -10,50 +10,54 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" ) -func ReadDataSourceClientCapabilities(in *tfprotov6.ReadDataSourceClientCapabilities) *datasource.ReadClientCapabilities { +func ReadDataSourceClientCapabilities(in *tfprotov6.ReadDataSourceClientCapabilities) datasource.ReadClientCapabilities { if in == nil { - return nil + // Client did not indicate any supported capabilities + return datasource.ReadClientCapabilities{ + DeferralAllowed: false, + } } - resp := &datasource.ReadClientCapabilities{ + return datasource.ReadClientCapabilities{ DeferralAllowed: in.DeferralAllowed, } - - return resp } -func ReadResourceClientCapabilities(in *tfprotov6.ReadResourceClientCapabilities) *resource.ReadClientCapabilities { +func ReadResourceClientCapabilities(in *tfprotov6.ReadResourceClientCapabilities) resource.ReadClientCapabilities { if in == nil { - return nil + // Client did not indicate any supported capabilities + return resource.ReadClientCapabilities{ + DeferralAllowed: false, + } } - resp := &resource.ReadClientCapabilities{ + return resource.ReadClientCapabilities{ DeferralAllowed: in.DeferralAllowed, } - - return resp } -func ModifyPlanClientCapabilities(in *tfprotov6.PlanResourceChangeClientCapabilities) *resource.ModifyPlanClientCapabilities { +func ModifyPlanClientCapabilities(in *tfprotov6.PlanResourceChangeClientCapabilities) resource.ModifyPlanClientCapabilities { if in == nil { - return nil + // Client did not indicate any supported capabilities + return resource.ModifyPlanClientCapabilities{ + DeferralAllowed: false, + } } - resp := &resource.ModifyPlanClientCapabilities{ + return resource.ModifyPlanClientCapabilities{ DeferralAllowed: in.DeferralAllowed, } - - return resp } -func ImportStateClientCapabilities(in *tfprotov6.ImportResourceStateClientCapabilities) *resource.ImportStateClientCapabilities { +func ImportStateClientCapabilities(in *tfprotov6.ImportResourceStateClientCapabilities) resource.ImportStateClientCapabilities { if in == nil { - return nil + // Client did not indicate any supported capabilities + return resource.ImportStateClientCapabilities{ + DeferralAllowed: false, + } } - resp := &resource.ImportStateClientCapabilities{ + return resource.ImportStateClientCapabilities{ DeferralAllowed: in.DeferralAllowed, } - - return resp } diff --git a/internal/fromproto6/importresourcestate_test.go b/internal/fromproto6/importresourcestate_test.go index fb11a340d..6c72913f7 100644 --- a/internal/fromproto6/importresourcestate_test.go +++ b/internal/fromproto6/importresourcestate_test.go @@ -98,11 +98,24 @@ func TestImportResourceStateRequest(t *testing.T) { expected: &fwserver.ImportResourceStateRequest{ EmptyState: testFwEmptyState, ID: "test-id", - ClientCapabilities: &resource.ImportStateClientCapabilities{ + ClientCapabilities: resource.ImportStateClientCapabilities{ DeferralAllowed: true, }, }, }, + "client-capabilities-unset": { + input: &tfprotov6.ImportResourceStateRequest{ + ID: "test-id", + }, + resourceSchema: testFwSchema, + expected: &fwserver.ImportResourceStateRequest{ + EmptyState: testFwEmptyState, + ID: "test-id", + ClientCapabilities: resource.ImportStateClientCapabilities{ + DeferralAllowed: false, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fromproto6/planresourcechange_test.go b/internal/fromproto6/planresourcechange_test.go index 5edfd149e..e30dfe99d 100644 --- a/internal/fromproto6/planresourcechange_test.go +++ b/internal/fromproto6/planresourcechange_test.go @@ -225,12 +225,22 @@ func TestPlanResourceChangeRequest(t *testing.T) { }, resourceSchema: testFwSchema, expected: &fwserver.PlanResourceChangeRequest{ - ClientCapabilities: &resource.ModifyPlanClientCapabilities{ + ClientCapabilities: resource.ModifyPlanClientCapabilities{ DeferralAllowed: true, }, ResourceSchema: testFwSchema, }, }, + "client-capabilities-unset": { + input: &tfprotov6.PlanResourceChangeRequest{}, + resourceSchema: testFwSchema, + expected: &fwserver.PlanResourceChangeRequest{ + ClientCapabilities: resource.ModifyPlanClientCapabilities{ + DeferralAllowed: false, + }, + ResourceSchema: testFwSchema, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fromproto6/readdatasource_test.go b/internal/fromproto6/readdatasource_test.go index 869092f60..08ef5124a 100644 --- a/internal/fromproto6/readdatasource_test.go +++ b/internal/fromproto6/readdatasource_test.go @@ -145,11 +145,21 @@ func TestReadDataSourceRequest(t *testing.T) { dataSourceSchema: testFwSchema, expected: &fwserver.ReadDataSourceRequest{ DataSourceSchema: testFwSchema, - ClientCapabilities: &datasource.ReadClientCapabilities{ + ClientCapabilities: datasource.ReadClientCapabilities{ DeferralAllowed: true, }, }, }, + "client-capabilities-unset": { + input: &tfprotov6.ReadDataSourceRequest{}, + dataSourceSchema: testFwSchema, + expected: &fwserver.ReadDataSourceRequest{ + DataSourceSchema: testFwSchema, + ClientCapabilities: datasource.ReadClientCapabilities{ + DeferralAllowed: false, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fromproto6/readresource_test.go b/internal/fromproto6/readresource_test.go index 6a3eeab98..03d6eea86 100644 --- a/internal/fromproto6/readresource_test.go +++ b/internal/fromproto6/readresource_test.go @@ -180,11 +180,20 @@ func TestReadResourceRequest(t *testing.T) { }, resourceSchema: testFwSchema, expected: &fwserver.ReadResourceRequest{ - ClientCapabilities: &resource.ReadClientCapabilities{ + ClientCapabilities: resource.ReadClientCapabilities{ DeferralAllowed: true, }, }, }, + "client-capabilities-unset": { + input: &tfprotov6.ReadResourceRequest{}, + resourceSchema: testFwSchema, + expected: &fwserver.ReadResourceRequest{ + ClientCapabilities: resource.ReadClientCapabilities{ + DeferralAllowed: false, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fwserver/server_importresourcestate.go b/internal/fwserver/server_importresourcestate.go index 54782c572..22d8da5a1 100644 --- a/internal/fwserver/server_importresourcestate.go +++ b/internal/fwserver/server_importresourcestate.go @@ -36,7 +36,7 @@ type ImportResourceStateRequest struct { // the ImportedResource TypeName of the ImportResourceStateResponse. TypeName string - ClientCapabilities *resource.ImportStateClientCapabilities + ClientCapabilities resource.ImportStateClientCapabilities } // ImportResourceStateResponse is the framework server response for the @@ -93,11 +93,8 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta } importReq := resource.ImportStateRequest{ - ID: req.ID, - } - - if req.ClientCapabilities != nil { - importReq.ClientCapabilities = *req.ClientCapabilities + ID: req.ID, + ClientCapabilities: req.ClientCapabilities, } privateProviderData := privatestate.EmptyProviderData(ctx) diff --git a/internal/fwserver/server_importresourcestate_test.go b/internal/fwserver/server_importresourcestate_test.go index 14e63148a..26eca6913 100644 --- a/internal/fwserver/server_importresourcestate_test.go +++ b/internal/fwserver/server_importresourcestate_test.go @@ -84,7 +84,7 @@ func TestServerImportResourceState(t *testing.T) { Provider: testEmptyProviderData, } - testDeferral := &resource.ImportStateClientCapabilities{ + testDeferral := resource.ImportStateClientCapabilities{ DeferralAllowed: true, } @@ -130,36 +130,6 @@ func TestServerImportResourceState(t *testing.T) { }, }, }, - "request-client-capabilities-unset": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.ImportResourceStateRequest{ - EmptyState: *testEmptyState, - ID: "test-id", - Resource: &testprovider.ResourceWithImportState{ - Resource: &testprovider.Resource{}, - ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - if req.ClientCapabilities.DeferralAllowed != false { - resp.Diagnostics.AddError("Unexpected req.ClientCapabilities.DeferralAllowed value", - "expected: false but got: true") - } - - resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) - }, - }, - TypeName: "test_resource", - }, - expectedResponse: &fwserver.ImportResourceStateResponse{ - ImportedResources: []fwserver.ImportedResource{ - { - State: *testState, - TypeName: "test_resource", - Private: testEmptyPrivate, - }, - }, - }, - }, "request-id": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index 84477e33d..3a0acb6ca 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -26,7 +26,7 @@ import ( // PlanResourceChangeRequest is the framework server request for the // PlanResourceChange RPC. type PlanResourceChangeRequest struct { - ClientCapabilities *resource.ModifyPlanClientCapabilities + ClientCapabilities resource.ModifyPlanClientCapabilities Config *tfsdk.Config PriorPrivate *privatestate.Data PriorState *tfsdk.State @@ -262,20 +262,17 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange logging.FrameworkTrace(ctx, "Resource implements ResourceWithModifyPlan") modifyPlanReq := resource.ModifyPlanRequest{ - Config: *req.Config, - Plan: stateToPlan(*resp.PlannedState), - State: *req.PriorState, - Private: resp.PlannedPrivate.Provider, + ClientCapabilities: req.ClientCapabilities, + Config: *req.Config, + Plan: stateToPlan(*resp.PlannedState), + State: *req.PriorState, + Private: resp.PlannedPrivate.Provider, } if req.ProviderMeta != nil { modifyPlanReq.ProviderMeta = *req.ProviderMeta } - if req.ClientCapabilities != nil { - modifyPlanReq.ClientCapabilities = *req.ClientCapabilities - } - modifyPlanResp := resource.ModifyPlanResponse{ Diagnostics: resp.Diagnostics, Plan: modifyPlanReq.Plan, diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index dfbe15ab1..1a60eda38 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -1230,7 +1230,7 @@ func TestServerPlanResourceChange(t *testing.T) { Provider: testEmptyProviderData, } - testDeferralAllowed := &resource.ModifyPlanClientCapabilities{ + testDeferralAllowed := resource.ModifyPlanClientCapabilities{ DeferralAllowed: true, } @@ -2826,48 +2826,6 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testEmptyPrivate, }, }, - "create-resourcewithmodifyplan-request-client-capabilities-unset": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.PlanResourceChangeRequest{ - Config: &tfsdk.Config{ - Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ - "test_computed": tftypes.NewValue(tftypes.String, nil), - "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), - }), - Schema: testSchema, - }, - ProposedNewState: &tfsdk.Plan{ - Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ - "test_computed": tftypes.NewValue(tftypes.String, nil), - "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), - }), - Schema: testSchema, - }, - PriorState: testEmptyState, - ResourceSchema: testSchema, - Resource: &testprovider.ResourceWithModifyPlan{ - ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { - if req.ClientCapabilities.DeferralAllowed != false { - resp.Diagnostics.AddError("Unexpected req.ClientCapabilities.DeferralAllowed value", - "expected: false but got: true") - } - - }, - }, - }, - expectedResponse: &fwserver.PlanResourceChangeResponse{ - PlannedState: &tfsdk.State{ - Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ - "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), - "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), - }), - Schema: testSchema, - }, - PlannedPrivate: testEmptyPrivate, - }, - }, "create-resourcewithmodifyplan-request-config": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/server_readdatasource.go b/internal/fwserver/server_readdatasource.go index c640179d4..ee0c4fb07 100644 --- a/internal/fwserver/server_readdatasource.go +++ b/internal/fwserver/server_readdatasource.go @@ -17,7 +17,7 @@ import ( // ReadDataSourceRequest is the framework server request for the // ReadDataSource RPC. type ReadDataSourceRequest struct { - ClientCapabilities *datasource.ReadClientCapabilities + ClientCapabilities datasource.ReadClientCapabilities Config *tfsdk.Config DataSourceSchema fwschema.Schema DataSource datasource.DataSource @@ -58,6 +58,7 @@ func (s *Server) ReadDataSource(ctx context.Context, req *ReadDataSourceRequest, } readReq := datasource.ReadRequest{ + ClientCapabilities: req.ClientCapabilities, Config: tfsdk.Config{ Schema: req.DataSourceSchema, }, @@ -77,10 +78,6 @@ func (s *Server) ReadDataSource(ctx context.Context, req *ReadDataSourceRequest, readReq.ProviderMeta = *req.ProviderMeta } - if req.ClientCapabilities != nil { - readReq.ClientCapabilities = *req.ClientCapabilities - } - logging.FrameworkTrace(ctx, "Calling provider defined DataSource Read") req.DataSource.Read(ctx, readReq, &readResp) logging.FrameworkTrace(ctx, "Called provider defined DataSource Read") diff --git a/internal/fwserver/server_readdatasource_test.go b/internal/fwserver/server_readdatasource_test.go index 6cca8863e..2a7a645e4 100644 --- a/internal/fwserver/server_readdatasource_test.go +++ b/internal/fwserver/server_readdatasource_test.go @@ -99,7 +99,7 @@ func TestServerReadDataSource(t *testing.T) { Schema: testSchema, } - testDeferralAllowed := &datasource.ReadClientCapabilities{ + testDeferralAllowed := datasource.ReadClientCapabilities{ DeferralAllowed: true, } @@ -142,33 +142,6 @@ func TestServerReadDataSource(t *testing.T) { State: testStateUnchanged, }, }, - "request-client-capabilities-unset": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.ReadDataSourceRequest{ - Config: testConfig, - DataSourceSchema: testSchema, - DataSource: &testprovider.DataSource{ - ReadMethod: func(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - if req.ClientCapabilities.DeferralAllowed != false { - resp.Diagnostics.AddError("Unexpected req.ClientCapabilities.DeferralAllowed value", - "expected: false but got: true") - } - - var config struct { - TestComputed types.String `tfsdk:"test_computed"` - TestRequired types.String `tfsdk:"test_required"` - } - - resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - }, - }, - }, - expectedResponse: &fwserver.ReadDataSourceResponse{ - State: testStateUnchanged, - }, - }, "request-config": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index f2998c4d1..2ec8e6cc7 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -17,7 +17,7 @@ import ( // ReadResourceRequest is the framework server request for the // ReadResource RPC. type ReadResourceRequest struct { - ClientCapabilities *resource.ReadClientCapabilities + ClientCapabilities resource.ReadClientCapabilities CurrentState *tfsdk.State Resource resource.Resource Private *privatestate.Data @@ -69,6 +69,7 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res } readReq := resource.ReadRequest{ + ClientCapabilities: req.ClientCapabilities, State: tfsdk.State{ Schema: req.CurrentState.Schema, Raw: req.CurrentState.Raw.Copy(), @@ -85,10 +86,6 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res readReq.ProviderMeta = *req.ProviderMeta } - if req.ClientCapabilities != nil { - readReq.ClientCapabilities = *req.ClientCapabilities - } - privateProviderData := privatestate.EmptyProviderData(ctx) readReq.Private = privateProviderData diff --git a/internal/fwserver/server_readresource_test.go b/internal/fwserver/server_readresource_test.go index e05de42ca..964de02dc 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -135,7 +135,7 @@ func TestServerReadResource(t *testing.T) { Provider: testEmptyProviderData, } - testDeferralAllowed := &resource.ReadClientCapabilities{ + testDeferralAllowed := resource.ReadClientCapabilities{ DeferralAllowed: true, } @@ -177,32 +177,6 @@ func TestServerReadResource(t *testing.T) { Private: testEmptyPrivate, }, }, - "request-client-capabilities-unset": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.ReadResourceRequest{ - CurrentState: testCurrentState, - Resource: &testprovider.Resource{ - ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - if req.ClientCapabilities.DeferralAllowed != false { - resp.Diagnostics.AddError("Unexpected req.ClientCapabilities.DeferralAllowed value", - "expected: false but got: true") - } - var data struct { - TestComputed types.String `tfsdk:"test_computed"` - TestRequired types.String `tfsdk:"test_required"` - } - - resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - }, - }, - }, - expectedResponse: &fwserver.ReadResourceResponse{ - NewState: testCurrentState, - Private: testEmptyPrivate, - }, - }, "request-currentstate-missing": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, From 038d486cc33cb09a72141cc4df9bad23f578750b Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 14 May 2024 16:36:14 -0400 Subject: [PATCH 25/44] Move `toproto5/6` `Deferred` conversion handling to its own files --- internal/toproto5/deferred.go | 29 ++++++++++++++++++++++++ internal/toproto5/importresourcestate.go | 7 +----- internal/toproto5/planresourcechange.go | 7 +----- internal/toproto5/readdatasource.go | 7 +----- internal/toproto5/readresource.go | 7 +----- internal/toproto6/deferred.go | 29 ++++++++++++++++++++++++ 6 files changed, 62 insertions(+), 24 deletions(-) create mode 100644 internal/toproto5/deferred.go create mode 100644 internal/toproto6/deferred.go diff --git a/internal/toproto5/deferred.go b/internal/toproto5/deferred.go new file mode 100644 index 000000000..3c5c4b1dc --- /dev/null +++ b/internal/toproto5/deferred.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func DataSourceDeferred(fw *datasource.Deferred) *tfprotov5.Deferred { + if fw == nil { + return nil + } + return &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(fw.Reason), + } +} + +func ResourceDeferred(fw *resource.Deferred) *tfprotov5.Deferred { + if fw == nil { + return nil + } + return &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(fw.Reason), + } +} diff --git a/internal/toproto5/importresourcestate.go b/internal/toproto5/importresourcestate.go index 92e438c42..7d29a5ca1 100644 --- a/internal/toproto5/importresourcestate.go +++ b/internal/toproto5/importresourcestate.go @@ -19,6 +19,7 @@ func ImportResourceStateResponse(ctx context.Context, fw *fwserver.ImportResourc } proto5 := &tfprotov5.ImportResourceStateResponse{ + Deferred: ResourceDeferred(fw.Deferred), Diagnostics: Diagnostics(ctx, fw.Diagnostics), } @@ -34,11 +35,5 @@ func ImportResourceStateResponse(ctx context.Context, fw *fwserver.ImportResourc proto5.ImportedResources = append(proto5.ImportedResources, proto5ImportedResource) } - if fw.Deferred != nil { - proto5.Deferred = &tfprotov5.Deferred{ - Reason: tfprotov5.DeferredReason(fw.Deferred.Reason), - } - } - return proto5 } diff --git a/internal/toproto5/planresourcechange.go b/internal/toproto5/planresourcechange.go index 6b2abcc75..f3292a963 100644 --- a/internal/toproto5/planresourcechange.go +++ b/internal/toproto5/planresourcechange.go @@ -20,6 +20,7 @@ func PlanResourceChangeResponse(ctx context.Context, fw *fwserver.PlanResourceCh } proto5 := &tfprotov5.PlanResourceChangeResponse{ + Deferred: ResourceDeferred(fw.Deferred), Diagnostics: Diagnostics(ctx, fw.Diagnostics), } @@ -38,11 +39,5 @@ func PlanResourceChangeResponse(ctx context.Context, fw *fwserver.PlanResourceCh proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.PlannedPrivate = plannedPrivate - if fw.Deferred != nil { - proto5.Deferred = &tfprotov5.Deferred{ - Reason: tfprotov5.DeferredReason(fw.Deferred.Reason), - } - } - return proto5 } diff --git a/internal/toproto5/readdatasource.go b/internal/toproto5/readdatasource.go index f70e6e186..3c3e28629 100644 --- a/internal/toproto5/readdatasource.go +++ b/internal/toproto5/readdatasource.go @@ -19,6 +19,7 @@ func ReadDataSourceResponse(ctx context.Context, fw *fwserver.ReadDataSourceResp } proto5 := &tfprotov5.ReadDataSourceResponse{ + Deferred: DataSourceDeferred(fw.Deferred), Diagnostics: Diagnostics(ctx, fw.Diagnostics), } @@ -27,11 +28,5 @@ func ReadDataSourceResponse(ctx context.Context, fw *fwserver.ReadDataSourceResp proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.State = state - if fw.Deferred != nil { - proto5.Deferred = &tfprotov5.Deferred{ - Reason: tfprotov5.DeferredReason(fw.Deferred.Reason), - } - } - return proto5 } diff --git a/internal/toproto5/readresource.go b/internal/toproto5/readresource.go index 5f6ff7aef..9193a3950 100644 --- a/internal/toproto5/readresource.go +++ b/internal/toproto5/readresource.go @@ -19,6 +19,7 @@ func ReadResourceResponse(ctx context.Context, fw *fwserver.ReadResourceResponse } proto5 := &tfprotov5.ReadResourceResponse{ + Deferred: ResourceDeferred(fw.Deferred), Diagnostics: Diagnostics(ctx, fw.Diagnostics), } @@ -32,11 +33,5 @@ func ReadResourceResponse(ctx context.Context, fw *fwserver.ReadResourceResponse proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.Private = newPrivate - if fw.Deferred != nil { - proto5.Deferred = &tfprotov5.Deferred{ - Reason: tfprotov5.DeferredReason(fw.Deferred.Reason), - } - } - return proto5 } diff --git a/internal/toproto6/deferred.go b/internal/toproto6/deferred.go new file mode 100644 index 000000000..10afa8fdc --- /dev/null +++ b/internal/toproto6/deferred.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func DataSourceDeferred(fw *datasource.Deferred) *tfprotov6.Deferred { + if fw == nil { + return nil + } + return &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReason(fw.Reason), + } +} + +func ResourceDeferred(fw *resource.Deferred) *tfprotov6.Deferred { + if fw == nil { + return nil + } + return &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReason(fw.Reason), + } +} From 8f0b6aa50ab9e7e72014e928ae4ab7830f253bde Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 15 May 2024 11:26:54 -0400 Subject: [PATCH 26/44] Use `ResourceDeferred()` and `DataSourceDeferred()` functions in `toproto6` package --- internal/toproto6/importresourcestate.go | 7 +------ internal/toproto6/planresourcechange.go | 7 +------ internal/toproto6/readdatasource.go | 7 +------ internal/toproto6/readresource.go | 7 +------ 4 files changed, 4 insertions(+), 24 deletions(-) diff --git a/internal/toproto6/importresourcestate.go b/internal/toproto6/importresourcestate.go index 447671c37..125f25ffb 100644 --- a/internal/toproto6/importresourcestate.go +++ b/internal/toproto6/importresourcestate.go @@ -19,6 +19,7 @@ func ImportResourceStateResponse(ctx context.Context, fw *fwserver.ImportResourc } proto6 := &tfprotov6.ImportResourceStateResponse{ + Deferred: ResourceDeferred(fw.Deferred), Diagnostics: Diagnostics(ctx, fw.Diagnostics), } @@ -34,11 +35,5 @@ func ImportResourceStateResponse(ctx context.Context, fw *fwserver.ImportResourc proto6.ImportedResources = append(proto6.ImportedResources, proto6ImportedResource) } - if fw.Deferred != nil { - proto6.Deferred = &tfprotov6.Deferred{ - Reason: tfprotov6.DeferredReason(fw.Deferred.Reason), - } - } - return proto6 } diff --git a/internal/toproto6/planresourcechange.go b/internal/toproto6/planresourcechange.go index d9499f979..486f7ab02 100644 --- a/internal/toproto6/planresourcechange.go +++ b/internal/toproto6/planresourcechange.go @@ -20,6 +20,7 @@ func PlanResourceChangeResponse(ctx context.Context, fw *fwserver.PlanResourceCh } proto6 := &tfprotov6.PlanResourceChangeResponse{ + Deferred: ResourceDeferred(fw.Deferred), Diagnostics: Diagnostics(ctx, fw.Diagnostics), } @@ -38,11 +39,5 @@ func PlanResourceChangeResponse(ctx context.Context, fw *fwserver.PlanResourceCh proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.PlannedPrivate = plannedPrivate - if fw.Deferred != nil { - proto6.Deferred = &tfprotov6.Deferred{ - Reason: tfprotov6.DeferredReason(fw.Deferred.Reason), - } - } - return proto6 } diff --git a/internal/toproto6/readdatasource.go b/internal/toproto6/readdatasource.go index 0f1738625..051f004a5 100644 --- a/internal/toproto6/readdatasource.go +++ b/internal/toproto6/readdatasource.go @@ -19,6 +19,7 @@ func ReadDataSourceResponse(ctx context.Context, fw *fwserver.ReadDataSourceResp } proto6 := &tfprotov6.ReadDataSourceResponse{ + Deferred: DataSourceDeferred(fw.Deferred), Diagnostics: Diagnostics(ctx, fw.Diagnostics), } @@ -27,11 +28,5 @@ func ReadDataSourceResponse(ctx context.Context, fw *fwserver.ReadDataSourceResp proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.State = state - if fw.Deferred != nil { - proto6.Deferred = &tfprotov6.Deferred{ - Reason: tfprotov6.DeferredReason(fw.Deferred.Reason), - } - } - return proto6 } diff --git a/internal/toproto6/readresource.go b/internal/toproto6/readresource.go index 6f515fce9..2de7adf84 100644 --- a/internal/toproto6/readresource.go +++ b/internal/toproto6/readresource.go @@ -19,6 +19,7 @@ func ReadResourceResponse(ctx context.Context, fw *fwserver.ReadResourceResponse } proto6 := &tfprotov6.ReadResourceResponse{ + Deferred: ResourceDeferred(fw.Deferred), Diagnostics: Diagnostics(ctx, fw.Diagnostics), } @@ -32,11 +33,5 @@ func ReadResourceResponse(ctx context.Context, fw *fwserver.ReadResourceResponse proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.Private = newPrivate - if fw.Deferred != nil { - proto6.Deferred = &tfprotov6.Deferred{ - Reason: tfprotov6.DeferredReason(fw.Deferred.Reason), - } - } - return proto6 } From b92fc3802ab4e247bcad949569a06c894e894d8f Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 15 May 2024 15:16:56 -0400 Subject: [PATCH 27/44] move configure provider client capabilities unset test to `fromproto5/6` --- internal/fromproto5/configureprovider_test.go | 8 ++++++++ internal/fromproto6/configureprovider_test.go | 8 ++++++++ .../fwserver/server_configureprovider_test.go | 15 --------------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/internal/fromproto5/configureprovider_test.go b/internal/fromproto5/configureprovider_test.go index ab48a334a..f61f925a3 100644 --- a/internal/fromproto5/configureprovider_test.go +++ b/internal/fromproto5/configureprovider_test.go @@ -107,6 +107,14 @@ func TestConfigureProviderRequest(t *testing.T) { }, }, }, + "client-capabilities-unset": { + input: &tfprotov5.ConfigureProviderRequest{}, + expected: &provider.ConfigureRequest{ + ClientCapabilities: provider.ConfigureProviderClientCapabilities{ + DeferralAllowed: false, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fromproto6/configureprovider_test.go b/internal/fromproto6/configureprovider_test.go index 7302ba466..47a3d778a 100644 --- a/internal/fromproto6/configureprovider_test.go +++ b/internal/fromproto6/configureprovider_test.go @@ -107,6 +107,14 @@ func TestConfigureProviderRequest(t *testing.T) { }, }, }, + "client-capabilities-unset": { + input: &tfprotov6.ConfigureProviderRequest{}, + expected: &provider.ConfigureRequest{ + ClientCapabilities: provider.ConfigureProviderClientCapabilities{ + DeferralAllowed: false, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fwserver/server_configureprovider_test.go b/internal/fwserver/server_configureprovider_test.go index 6ac0b31ca..42c897d17 100644 --- a/internal/fwserver/server_configureprovider_test.go +++ b/internal/fwserver/server_configureprovider_test.go @@ -76,21 +76,6 @@ func TestServerConfigureProvider(t *testing.T) { }, expectedResponse: &provider.ConfigureResponse{}, }, - "request-client-capabilities-unset": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{ - SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {}, - ConfigureMethod: func(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { - if req.ClientCapabilities.DeferralAllowed != false { - resp.Diagnostics.AddError("Unexpected req.ClientCapabilities.DeferralAllowed value", - "expected: false but got: true") - } - }, - }, - }, - request: &provider.ConfigureRequest{}, - expectedResponse: &provider.ConfigureResponse{}, - }, "request-config": { server: &fwserver.Server{ Provider: &testprovider.Provider{ From c89dcdce5614d1304bc0fc525d4e8b596f851676 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 15 May 2024 15:45:50 -0400 Subject: [PATCH 28/44] Add throw an error diagnostic in server_configureprovider.go if a deferral is set without the proper capability --- internal/fwserver/server_configureprovider.go | 11 +++++++ .../fwserver/server_configureprovider_test.go | 29 ++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/internal/fwserver/server_configureprovider.go b/internal/fwserver/server_configureprovider.go index 2d4e13d74..d465dc77d 100644 --- a/internal/fwserver/server_configureprovider.go +++ b/internal/fwserver/server_configureprovider.go @@ -22,6 +22,17 @@ func (s *Server) ConfigureProvider(ctx context.Context, req *provider.ConfigureR logging.FrameworkTrace(ctx, "Called provider defined Provider Configure") + if resp.Deferred != nil { + if !req.ClientCapabilities.DeferralAllowed { + resp.Diagnostics.AddError("Invalid Deferred Provider Response", + "Provider configured a deferred response for all resources and data sources but the Terraform request "+ + "did not indicate support for deferred actions. This is an issue with the provider and should be reported to the provider developers.") + } else { + logging.FrameworkDebug(ctx, "Provider has configured a deferred response, "+ + "all associated resources and data sources will automatically return a deferred response.") + } + } + s.deferred = resp.Deferred s.DataSourceConfigureData = resp.DataSourceData s.ResourceConfigureData = resp.ResourceData diff --git a/internal/fwserver/server_configureprovider_test.go b/internal/fwserver/server_configureprovider_test.go index 42c897d17..560eac499 100644 --- a/internal/fwserver/server_configureprovider_test.go +++ b/internal/fwserver/server_configureprovider_test.go @@ -142,7 +142,11 @@ func TestServerConfigureProvider(t *testing.T) { }, }, }, - request: &provider.ConfigureRequest{}, + request: &provider.ConfigureRequest{ + ClientCapabilities: provider.ConfigureProviderClientCapabilities{ + DeferralAllowed: true, + }, + }, expectedResponse: &provider.ConfigureResponse{ Deferred: &provider.Deferred{ Reason: provider.DeferredReasonProviderConfigUnknown, @@ -174,6 +178,29 @@ func TestServerConfigureProvider(t *testing.T) { }, }, }, + "response-invalid-deferral-diagnostic": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {}, + ConfigureMethod: func(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + resp.Deferred = &provider.Deferred{Reason: provider.DeferredReasonProviderConfigUnknown} + resp.DataSourceData = "test-provider-configure-value" + }, + }, + }, + request: &provider.ConfigureRequest{}, + expectedResponse: &provider.ConfigureResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic("Invalid Deferred Provider Response", + "Provider configured a deferred response for all resources and data sources but the Terraform request "+ + "did not indicate support for deferred actions. This is an issue with the provider and should be reported to the provider developers."), + }, + Deferred: &provider.Deferred{ + Reason: provider.DeferredReasonProviderConfigUnknown, + }, + DataSourceData: "test-provider-configure-value", + }, + }, "response-resourcedata": { server: &fwserver.Server{ Provider: &testprovider.Provider{ From 188522ef405d13654552ac7f4dc565aeaada975d Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 16 May 2024 16:24:00 -0400 Subject: [PATCH 29/44] Add `ResourceBehavior` field to `MetadataResponse` --- resource/metadata.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/resource/metadata.go b/resource/metadata.go index 9750a46cb..9e2bf9a1a 100644 --- a/resource/metadata.go +++ b/resource/metadata.go @@ -21,4 +21,24 @@ type MetadataResponse struct { // TypeName should be the full resource type, including the provider // type prefix and an underscore. For example, examplecloud_thing. TypeName string + + // ResourceBehavior is used to control framework-specific logic when + // interacting with this resource. + ResourceBehavior ResourceBehavior +} + +// ResourceBehavior controls framework-specific logic when interacting +// with a resource. +type ResourceBehavior struct { + // ProviderDeferred enables provider-defined logic to be executed + // in the case of an automatic deferred response from provider configure. + ProviderDeferred ProviderDeferredBehavior +} + +type ProviderDeferredBehavior struct { + // When EnablePlanModification is true, framework will still execute + // provider-defined resource plan modification logic if + // provider.Configure defers. Framework will then automatically return a + // deferred response along with the modified plan. + EnablePlanModification bool } From 693e708c8f887907352c2aea40151a6c33205559 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 16 May 2024 16:25:01 -0400 Subject: [PATCH 30/44] Initial implementation of automatic deferrals for resource/datasource RPCs --- internal/fwserver/server_importresourcestate.go | 13 +++++++++++++ internal/fwserver/server_planresourcechange.go | 7 +++++++ internal/fwserver/server_readdatasource.go | 7 +++++++ internal/fwserver/server_readresource.go | 7 +++++++ 4 files changed, 34 insertions(+) diff --git a/internal/fwserver/server_importresourcestate.go b/internal/fwserver/server_importresourcestate.go index 22d8da5a1..3af99a3b4 100644 --- a/internal/fwserver/server_importresourcestate.go +++ b/internal/fwserver/server_importresourcestate.go @@ -53,6 +53,13 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta return } + //TODO: add logging for replacing deferred reason + if s.deferred != nil { + resp.Deferred = &resource.Deferred{ + Reason: resp.Deferred.Reason, + } + } + if resourceWithConfigure, ok := req.Resource.(resource.ResourceWithConfigure); ok { logging.FrameworkTrace(ctx, "Resource implements ResourceWithConfigure") @@ -133,6 +140,12 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta } resp.Deferred = importResp.Deferred + if s.deferred != nil { + resp.Deferred = &resource.Deferred{ + Reason: resp.Deferred.Reason, + } + } + resp.ImportedResources = []ImportedResource{ { State: importResp.State, diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index 3a0acb6ca..53ed46b62 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -215,6 +215,13 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange resp.PlannedState.Raw = modifiedPlan } + // TODO: Finish Implementation + // Skip plan modification for automatic deferrals + // unless ProviderDeferredBehavior.EnablePlanModification is true + if s.deferred != nil { + + } + // Execute any schema-based plan modifiers. This allows overwriting // any unknown values. // diff --git a/internal/fwserver/server_readdatasource.go b/internal/fwserver/server_readdatasource.go index ee0c4fb07..445421216 100644 --- a/internal/fwserver/server_readdatasource.go +++ b/internal/fwserver/server_readdatasource.go @@ -38,6 +38,13 @@ func (s *Server) ReadDataSource(ctx context.Context, req *ReadDataSourceRequest, return } + //TODO: add logging for replacing deferred reason + if s.deferred != nil { + resp.Deferred = &datasource.Deferred{ + Reason: resp.Deferred.Reason, + } + } + if dataSourceWithConfigure, ok := req.DataSource.(datasource.DataSourceWithConfigure); ok { logging.FrameworkTrace(ctx, "DataSource implements DataSourceWithConfigure") diff --git a/internal/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index 2ec8e6cc7..a390d8fee 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -49,6 +49,13 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res return } + //TODO: add logging for replacing deferred reason + if s.deferred != nil { + resp.Deferred = &resource.Deferred{ + Reason: resp.Deferred.Reason, + } + } + if resourceWithConfigure, ok := req.Resource.(resource.ResourceWithConfigure); ok { logging.FrameworkTrace(ctx, "Resource implements ResourceWithConfigure") From a5089d67922e571e2670b47ce8231201b2b8cb32 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Fri, 17 May 2024 10:49:47 -0400 Subject: [PATCH 31/44] Implement provider automatic deferral for `PlanResourceChange` RPC --- internal/fromproto5/planresourcechange.go | 3 +- .../fromproto5/planresourcechange_test.go | 20 ++++- internal/fwserver/server.go | 85 +++++++++++++++++++ .../fwserver/server_planresourcechange.go | 24 +++++- .../proto5server/server_planresourcechange.go | 13 ++- 5 files changed, 137 insertions(+), 8 deletions(-) diff --git a/internal/fromproto5/planresourcechange.go b/internal/fromproto5/planresourcechange.go index 3aaed62b8..5bd24c1dd 100644 --- a/internal/fromproto5/planresourcechange.go +++ b/internal/fromproto5/planresourcechange.go @@ -17,7 +17,7 @@ import ( // PlanResourceChangeRequest returns the *fwserver.PlanResourceChangeRequest // equivalent of a *tfprotov5.PlanResourceChangeRequest. -func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResourceChangeRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema) (*fwserver.PlanResourceChangeRequest, diag.Diagnostics) { +func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResourceChangeRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, resourceBehavior resource.ResourceBehavior) (*fwserver.PlanResourceChangeRequest, diag.Diagnostics) { if proto5 == nil { return nil, nil } @@ -39,6 +39,7 @@ func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResour } fw := &fwserver.PlanResourceChangeRequest{ + ResourceBehavior: resourceBehavior, ResourceSchema: resourceSchema, Resource: reqResource, ClientCapabilities: ModifyPlanClientCapabilities(proto5.ClientCapabilities), diff --git a/internal/fromproto5/planresourcechange_test.go b/internal/fromproto5/planresourcechange_test.go index f20a94c7e..64dcad0f8 100644 --- a/internal/fromproto5/planresourcechange_test.go +++ b/internal/fromproto5/planresourcechange_test.go @@ -56,6 +56,7 @@ func TestPlanResourceChangeRequest(t *testing.T) { testCases := map[string]struct { input *tfprotov5.PlanResourceChangeRequest + resourceBehavior resource.ResourceBehavior resourceSchema fwschema.Schema resource resource.Resource providerMetaSchema fwschema.Schema @@ -241,6 +242,23 @@ func TestPlanResourceChangeRequest(t *testing.T) { ResourceSchema: testFwSchema, }, }, + "resource-behavior": { + input: &tfprotov5.PlanResourceChangeRequest{}, + resourceSchema: testFwSchema, + resourceBehavior: resource.ResourceBehavior{ + ProviderDeferred: resource.ProviderDeferredBehavior{ + EnablePlanModification: true, + }, + }, + expected: &fwserver.PlanResourceChangeRequest{ + ResourceBehavior: resource.ResourceBehavior{ + ProviderDeferred: resource.ProviderDeferredBehavior{ + EnablePlanModification: true, + }, + }, + ResourceSchema: testFwSchema, + }, + }, } for name, testCase := range testCases { @@ -249,7 +267,7 @@ func TestPlanResourceChangeRequest(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto5.PlanResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema) + got, diags := fromproto5.PlanResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.resourceBehavior) if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index 0e815957b..5a0f90722 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -142,6 +142,19 @@ type Server struct { // resourceTypesMutex is a mutex to protect concurrent resourceTypes // access from race conditions. resourceTypesMutex sync.Mutex + + // resourceBehaviors is the cached Resource behaviors for RPCs that need to + // control framework-specific logic when interacting with a resource. + resourceBehaviors map[string]resource.ResourceBehavior + + // resourceBehaviorsDiags is the cached Diagnostics obtained while populating + // resourceBehaviors. This is to ensure any warnings or errors are also + // returned appropriately when fetching resourceBehaviors. + resourceBehaviorsDiags diag.Diagnostics + + // resourceBehaviorsMutex is a mutex to protect concurrent resourceBehaviors + // access from race conditions. + resourceBehaviorsMutex sync.Mutex } // DataSource returns the DataSource for a given type name. @@ -419,6 +432,78 @@ func (s *Server) Resource(ctx context.Context, typeName string) (resource.Resour return resourceFunc(), diags } +// ResourceBehavior returns the ResourceBehavior for a given type name. +func (s *Server) ResourceBehavior(ctx context.Context, typeName string) (resource.ResourceBehavior, diag.Diagnostics) { + resourceBehaviors, diags := s.ResourceBehaviors(ctx) + + resourceBehavior, ok := resourceBehaviors[typeName] + + if !ok { + diags.AddError( + "Resource Type Not Found", + fmt.Sprintf("No resource type named %q was found in the provider.", typeName), + ) + + return resource.ResourceBehavior{}, diags + } + + return resourceBehavior, diags +} + +// ResourceBehaviors returns a map of ResourceBehavior. The results are cached +// on first use. +func (s *Server) ResourceBehaviors(ctx context.Context) (map[string]resource.ResourceBehavior, diag.Diagnostics) { + logging.FrameworkTrace(ctx, "Checking ResourceBehaviors lock") + s.resourceBehaviorsMutex.Lock() + defer s.resourceBehaviorsMutex.Unlock() + + if s.resourceBehaviors != nil { + return s.resourceBehaviors, s.resourceBehaviorsDiags + } + + providerTypeName := s.ProviderTypeName(ctx) + s.resourceBehaviors = make(map[string]resource.ResourceBehavior) + + resourceFuncs, diags := s.ResourceFuncs(ctx) + s.resourceBehaviorsDiags.Append(diags...) + + for _, resourceFunc := range resourceFuncs { + res := resourceFunc() + + metadataRequest := resource.MetadataRequest{ + ProviderTypeName: providerTypeName, + } + metadataResponse := resource.MetadataResponse{} + + res.Metadata(ctx, metadataRequest, &metadataResponse) + + if metadataResponse.TypeName == "" { + s.resourceBehaviorsDiags.AddError( + "Resource Type Name Missing", + fmt.Sprintf("The %T Resource returned an empty string from the Metadata method. ", res)+ + "This is always an issue with the provider and should be reported to the provider developers.", + ) + continue + } + + logging.FrameworkTrace(ctx, "Found resource type", map[string]interface{}{logging.KeyResourceType: metadataResponse.TypeName}) + + if _, ok := s.resourceBehaviors[metadataResponse.TypeName]; ok { + s.resourceBehaviorsDiags.AddError( + "Duplicate Resource Type Defined", + fmt.Sprintf("The %s resource type name was returned for multiple resources. ", metadataResponse.TypeName)+ + "Resource type names must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ) + continue + } + + s.resourceBehaviors[metadataResponse.TypeName] = metadataResponse.ResourceBehavior + } + + return s.resourceBehaviors, s.resourceBehaviorsDiags +} + // ResourceFuncs returns a map of Resource functions. The results are cached // on first use. func (s *Server) ResourceFuncs(ctx context.Context) (map[string]func() resource.Resource, diag.Diagnostics) { diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index 53ed46b62..83f8adc04 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -34,6 +34,7 @@ type PlanResourceChangeRequest struct { ProviderMeta *tfsdk.Config ResourceSchema fwschema.Schema Resource resource.Resource + ResourceBehavior resource.ResourceBehavior } // PlanResourceChangeResponse is the framework server response for the @@ -215,11 +216,13 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange resp.PlannedState.Raw = modifiedPlan } - // TODO: Finish Implementation // Skip plan modification for automatic deferrals // unless ProviderDeferredBehavior.EnablePlanModification is true - if s.deferred != nil { - + if s.deferred != nil && req.ResourceBehavior.ProviderDeferred.EnablePlanModification == false { + logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.") + resp.Deferred = &resource.Deferred{ + Reason: resource.DeferredReason(s.deferred.Reason), + } } // Execute any schema-based plan modifiers. This allows overwriting @@ -295,7 +298,20 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange resp.PlannedState = planToState(modifyPlanResp.Plan) resp.RequiresReplace = append(resp.RequiresReplace, modifyPlanResp.RequiresReplace...) resp.PlannedPrivate.Provider = modifyPlanResp.Private - resp.Deferred = modifyPlanResp.Deferred + + // Provider deferred response is present, add the deferred response alongside the provider-modified plan + if s.deferred != nil { + logging.FrameworkDebug(ctx, "Provider has deferred response configured, returning deferred response with modified plan.") + if modifyPlanResp.Deferred != nil { + logging.FrameworkDebug(ctx, fmt.Sprintf("Provider deferred response reason: %s replaced resource deferred response reason: %s", + s.deferred.Reason.String(), modifyPlanResp.Deferred.Reason.String())) + } + resp.Deferred = &resource.Deferred{ + Reason: resource.DeferredReason(s.deferred.Reason), + } + } else { + resp.Deferred = modifyPlanResp.Deferred + } } // Ensure deterministic RequiresReplace by sorting and deduplicating diff --git a/internal/proto5server/server_planresourcechange.go b/internal/proto5server/server_planresourcechange.go index 6cc995daa..a5cec1987 100644 --- a/internal/proto5server/server_planresourcechange.go +++ b/internal/proto5server/server_planresourcechange.go @@ -6,11 +6,12 @@ package proto5server import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) // PlanResourceChange satisfies the tfprotov5.ProviderServer interface. @@ -44,7 +45,15 @@ func (s *Server) PlanResourceChange(ctx context.Context, proto5Req *tfprotov5.Pl return toproto5.PlanResourceChangeResponse(ctx, fwResp), nil } - fwReq, diags := fromproto5.PlanResourceChangeRequest(ctx, proto5Req, resource, resourceSchema, providerMetaSchema) + resourceBehavior, diags := s.FrameworkServer.ResourceBehavior(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.PlanResourceChangeResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto5.PlanResourceChangeRequest(ctx, proto5Req, resource, resourceSchema, providerMetaSchema, resourceBehavior) fwResp.Diagnostics.Append(diags...) From 9fd5971ade0c629cd0cbdbfe620c741262409bda Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 20 May 2024 13:42:48 -0400 Subject: [PATCH 32/44] Add default values for automatic deferrals --- internal/fwserver/server_configureprovider.go | 3 +- .../fwserver/server_configureprovider_test.go | 2 - .../fwserver/server_importresourcestate.go | 18 ++- .../server_importresourcestate_test.go | 68 ++++++++- .../fwserver/server_planresourcechange.go | 19 +-- .../server_planresourcechange_test.go | 138 +++++++++++++++++- internal/fwserver/server_readdatasource.go | 12 +- .../fwserver/server_readdatasource_test.go | 62 +++++++- internal/fwserver/server_readresource.go | 6 +- internal/fwserver/server_readresource_test.go | 54 ++++++- .../proto5server/server_configureprovider.go | 3 +- .../server_planresourcechange_test.go | 5 +- 12 files changed, 353 insertions(+), 37 deletions(-) diff --git a/internal/fwserver/server_configureprovider.go b/internal/fwserver/server_configureprovider.go index d465dc77d..a437fbff0 100644 --- a/internal/fwserver/server_configureprovider.go +++ b/internal/fwserver/server_configureprovider.go @@ -27,13 +27,14 @@ func (s *Server) ConfigureProvider(ctx context.Context, req *provider.ConfigureR resp.Diagnostics.AddError("Invalid Deferred Provider Response", "Provider configured a deferred response for all resources and data sources but the Terraform request "+ "did not indicate support for deferred actions. This is an issue with the provider and should be reported to the provider developers.") + return } else { logging.FrameworkDebug(ctx, "Provider has configured a deferred response, "+ "all associated resources and data sources will automatically return a deferred response.") + s.deferred = resp.Deferred } } - s.deferred = resp.Deferred s.DataSourceConfigureData = resp.DataSourceData s.ResourceConfigureData = resp.ResourceData } diff --git a/internal/fwserver/server_configureprovider_test.go b/internal/fwserver/server_configureprovider_test.go index 560eac499..f7e29568e 100644 --- a/internal/fwserver/server_configureprovider_test.go +++ b/internal/fwserver/server_configureprovider_test.go @@ -184,7 +184,6 @@ func TestServerConfigureProvider(t *testing.T) { SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {}, ConfigureMethod: func(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { resp.Deferred = &provider.Deferred{Reason: provider.DeferredReasonProviderConfigUnknown} - resp.DataSourceData = "test-provider-configure-value" }, }, }, @@ -198,7 +197,6 @@ func TestServerConfigureProvider(t *testing.T) { Deferred: &provider.Deferred{ Reason: provider.DeferredReasonProviderConfigUnknown, }, - DataSourceData: "test-provider-configure-value", }, }, "response-resourcedata": { diff --git a/internal/fwserver/server_importresourcestate.go b/internal/fwserver/server_importresourcestate.go index 8aca5fcc6..7965bf7eb 100644 --- a/internal/fwserver/server_importresourcestate.go +++ b/internal/fwserver/server_importresourcestate.go @@ -6,6 +6,8 @@ package fwserver import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" @@ -53,11 +55,23 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta return } - //TODO: add logging for replacing deferred reason if s.deferred != nil { + logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.") + // Send an unknown value for the imported object + resp.ImportedResources = []ImportedResource{ + { + State: tfsdk.State{ + Raw: tftypes.NewValue(req.EmptyState.Schema.Type().TerraformType(ctx), tftypes.UnknownValue), + Schema: req.EmptyState.Schema, + }, + TypeName: req.TypeName, + Private: &privatestate.Data{}, + }, + } resp.Deferred = &resource.Deferred{ - Reason: resp.Deferred.Reason, + Reason: resource.DeferredReason(s.deferred.Reason), } + return } if resourceWithConfigure, ok := req.Resource.(resource.ResourceWithConfigure); ok { diff --git a/internal/fwserver/server_importresourcestate_test.go b/internal/fwserver/server_importresourcestate_test.go index 26eca6913..33dcb167c 100644 --- a/internal/fwserver/server_importresourcestate_test.go +++ b/internal/fwserver/server_importresourcestate_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -38,6 +39,8 @@ func TestServerImportResourceState(t *testing.T) { "required": tftypes.NewValue(tftypes.String, nil), }) + testUnknownStateValue := tftypes.NewValue(testType, tftypes.UnknownValue) + testStateValue := tftypes.NewValue(testType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "test-id"), "optional": tftypes.NewValue(tftypes.String, nil), @@ -63,6 +66,11 @@ func TestServerImportResourceState(t *testing.T) { Schema: testSchema, } + testUnknownState := &tfsdk.State{ + Raw: testUnknownStateValue, + Schema: testSchema, + } + testState := &tfsdk.State{ Raw: testStateValue, Schema: testSchema, @@ -89,9 +97,10 @@ func TestServerImportResourceState(t *testing.T) { } testCases := map[string]struct { - server *fwserver.Server - request *fwserver.ImportResourceStateRequest - expectedResponse *fwserver.ImportResourceStateResponse + server *fwserver.Server + request *fwserver.ImportResourceStateRequest + expectedResponse *fwserver.ImportResourceStateResponse + configureProviderReq *provider.ConfigureRequest }{ "nil": { server: &fwserver.Server{ @@ -280,7 +289,53 @@ func TestServerImportResourceState(t *testing.T) { }, }, }, - "response-importedresources-deferral": { + "response-importedresources-deferral-automatic": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {}, + ConfigureMethod: func(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + resp.Deferred = &provider.Deferred{Reason: provider.DeferredReasonProviderConfigUnknown} + }, + }, + }, + configureProviderReq: &provider.ConfigureRequest{ + ClientCapabilities: provider.ConfigureProviderClientCapabilities{ + DeferralAllowed: true, + }, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyState, + ID: "test-id", + Resource: &testprovider.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + if req.ID != "test-id" { + resp.Diagnostics.AddError("unexpected req.ID value: %s", req.ID) + } + + resp.Deferred = &resource.Deferred{ + Reason: resource.DeferredReasonAbsentPrereq, + } + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + + }, + }, + TypeName: "test_resource", + ClientCapabilities: testDeferral, + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: *testUnknownState, + TypeName: "test_resource", + Private: &privatestate.Data{}, + }, + }, + Deferred: &resource.Deferred{Reason: resource.DeferredReasonProviderConfigUnknown}, + }, + }, + "response-importedresources-deferral-manual": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, @@ -378,6 +433,11 @@ func TestServerImportResourceState(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() + if testCase.configureProviderReq != nil { + configureProviderResp := &provider.ConfigureResponse{} + testCase.server.ConfigureProvider(context.Background(), testCase.configureProviderReq, configureProviderResp) + } + response := &fwserver.ImportResourceStateResponse{} testCase.server.ImportResourceState(context.Background(), testCase.request, response) diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index 83f8adc04..64a70856f 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -216,15 +216,6 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange resp.PlannedState.Raw = modifiedPlan } - // Skip plan modification for automatic deferrals - // unless ProviderDeferredBehavior.EnablePlanModification is true - if s.deferred != nil && req.ResourceBehavior.ProviderDeferred.EnablePlanModification == false { - logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.") - resp.Deferred = &resource.Deferred{ - Reason: resource.DeferredReason(s.deferred.Reason), - } - } - // Execute any schema-based plan modifiers. This allows overwriting // any unknown values. // @@ -260,6 +251,16 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange } } + // Skip resource-level ModifyPlan for automatic deferrals + // unless ProviderDeferredBehavior.EnablePlanModification is true + if s.deferred != nil && !req.ResourceBehavior.ProviderDeferred.EnablePlanModification { + logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.") + resp.Deferred = &resource.Deferred{ + Reason: resource.DeferredReason(s.deferred.Reason), + } + return + } + // Execute any resource-level ModifyPlan method. This allows // overwriting any unknown values. // diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index 1a60eda38..bf412180f 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -21,6 +21,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -1235,9 +1236,10 @@ func TestServerPlanResourceChange(t *testing.T) { } testCases := map[string]struct { - server *fwserver.Server - request *fwserver.PlanResourceChangeRequest - expectedResponse *fwserver.PlanResourceChangeResponse + server *fwserver.Server + request *fwserver.PlanResourceChangeRequest + expectedResponse *fwserver.PlanResourceChangeResponse + configureProviderReq *provider.ConfigureRequest }{ "resource-configure-data": { server: &fwserver.Server{ @@ -3007,7 +3009,130 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testEmptyPrivate, }, }, - "create-resourcewithmodifyplan-response-deferral": { + "create-resourcewithmodifyplan-response-deferral-automatic-plan-modification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {}, + ConfigureMethod: func(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + resp.Deferred = &provider.Deferred{Reason: provider.DeferredReasonProviderConfigUnknown} + }, + }, + }, + configureProviderReq: &provider.ConfigureRequest{ + ClientCapabilities: provider.ConfigureProviderClientCapabilities{ + DeferralAllowed: true, + }, + }, + request: &fwserver.PlanResourceChangeRequest{ + ResourceBehavior: resource.ResourceBehavior{ + ProviderDeferred: resource.ProviderDeferredBehavior{ + EnablePlanModification: true, + }, + }, + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PriorState: testEmptyState, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if req.ClientCapabilities.DeferralAllowed == true { + resp.Deferred = &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq} + } + + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + data.TestComputed = types.StringValue("test-plannedstate-value") + + resp.Diagnostics.Append(resp.Plan.Set(ctx, &data)...) + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + Deferred: &resource.Deferred{Reason: resource.DeferredReasonProviderConfigUnknown}, + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, + "create-resourcewithmodifyplan-response-deferral-automatic-skip-plan-modification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {}, + ConfigureMethod: func(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + resp.Deferred = &provider.Deferred{Reason: provider.DeferredReasonProviderConfigUnknown} + }, + }, + }, + configureProviderReq: &provider.ConfigureRequest{ + ClientCapabilities: provider.ConfigureProviderClientCapabilities{ + DeferralAllowed: true, + }, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PriorState: testEmptyState, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if req.ClientCapabilities.DeferralAllowed == true { + resp.Deferred = &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq} + } + + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + data.TestComputed = types.StringValue("test-plannedstate-value") + + resp.Diagnostics.Append(resp.Plan.Set(ctx, &data)...) + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + Deferred: &resource.Deferred{Reason: resource.DeferredReasonProviderConfigUnknown}, + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, + "create-resourcewithmodifyplan-response-deferral-manual": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, @@ -6113,6 +6238,11 @@ func TestServerPlanResourceChange(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() + if testCase.configureProviderReq != nil { + configureProviderResp := &provider.ConfigureResponse{} + testCase.server.ConfigureProvider(context.Background(), testCase.configureProviderReq, configureProviderResp) + } + response := &fwserver.PlanResourceChangeResponse{} testCase.server.PlanResourceChange(context.Background(), testCase.request, response) diff --git a/internal/fwserver/server_readdatasource.go b/internal/fwserver/server_readdatasource.go index 445421216..1eda02bc7 100644 --- a/internal/fwserver/server_readdatasource.go +++ b/internal/fwserver/server_readdatasource.go @@ -6,6 +6,8 @@ package fwserver import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -38,11 +40,17 @@ func (s *Server) ReadDataSource(ctx context.Context, req *ReadDataSourceRequest, return } - //TODO: add logging for replacing deferred reason if s.deferred != nil { + logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.") + // Send an unknown value for the data source + resp.State = &tfsdk.State{ + Raw: tftypes.NewValue(req.DataSourceSchema.Type().TerraformType(ctx), tftypes.UnknownValue), + Schema: req.DataSourceSchema, + } resp.Deferred = &datasource.Deferred{ - Reason: resp.Deferred.Reason, + Reason: datasource.DeferredReason(s.deferred.Reason), } + return } if dataSourceWithConfigure, ok := req.DataSource.(datasource.DataSourceWithConfigure); ok { diff --git a/internal/fwserver/server_readdatasource_test.go b/internal/fwserver/server_readdatasource_test.go index 2a7a645e4..1c891c2a4 100644 --- a/internal/fwserver/server_readdatasource_test.go +++ b/internal/fwserver/server_readdatasource_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -41,6 +42,8 @@ func TestServerReadDataSource(t *testing.T) { "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), }) + testStateUnknownValue := tftypes.NewValue(testType, tftypes.UnknownValue) + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -94,6 +97,11 @@ func TestServerReadDataSource(t *testing.T) { Schema: testSchema, } + testStateUnknown := &tfsdk.State{ + Raw: testStateUnknownValue, + Schema: testSchema, + } + testState := &tfsdk.State{ Raw: testStateValue, Schema: testSchema, @@ -104,9 +112,10 @@ func TestServerReadDataSource(t *testing.T) { } testCases := map[string]struct { - server *fwserver.Server - request *fwserver.ReadDataSourceRequest - expectedResponse *fwserver.ReadDataSourceResponse + server *fwserver.Server + request *fwserver.ReadDataSourceRequest + expectedResponse *fwserver.ReadDataSourceResponse + configureProviderReq *provider.ConfigureRequest }{ "nil": { server: &fwserver.Server{ @@ -236,7 +245,47 @@ func TestServerReadDataSource(t *testing.T) { State: testStateUnchanged, }, }, - "response-deferral": { + "response-deferral-automatic": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {}, + ConfigureMethod: func(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + resp.Deferred = &provider.Deferred{Reason: provider.DeferredReasonProviderConfigUnknown} + }, + }, + }, + configureProviderReq: &provider.ConfigureRequest{ + ClientCapabilities: provider.ConfigureProviderClientCapabilities{ + DeferralAllowed: true, + }, + }, + request: &fwserver.ReadDataSourceRequest{ + Config: testConfig, + DataSourceSchema: testSchema, + DataSource: &testprovider.DataSource{ + ReadMethod: func(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + resp.Deferred = &datasource.Deferred{Reason: datasource.DeferredReasonAbsentPrereq} + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + }, + ClientCapabilities: testDeferralAllowed, + }, + expectedResponse: &fwserver.ReadDataSourceResponse{ + State: testStateUnknown, + Deferred: &datasource.Deferred{Reason: datasource.DeferredReasonProviderConfigUnknown}, + }, + }, + "response-deferral-manual": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, @@ -420,6 +469,11 @@ func TestServerReadDataSource(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() + if testCase.configureProviderReq != nil { + configureProviderResp := &provider.ConfigureResponse{} + testCase.server.ConfigureProvider(context.Background(), testCase.configureProviderReq, configureProviderResp) + } + response := &fwserver.ReadDataSourceResponse{} testCase.server.ReadDataSource(context.Background(), testCase.request, response) diff --git a/internal/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index a390d8fee..8b7b08fb5 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -49,11 +49,13 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res return } - //TODO: add logging for replacing deferred reason if s.deferred != nil { + logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.") + resp.NewState = req.CurrentState resp.Deferred = &resource.Deferred{ - Reason: resp.Deferred.Reason, + Reason: resource.DeferredReason(s.deferred.Reason), } + return } if resourceWithConfigure, ok := req.Resource.(resource.ResourceWithConfigure); ok { diff --git a/internal/fwserver/server_readresource_test.go b/internal/fwserver/server_readresource_test.go index 964de02dc..033a95c29 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -140,9 +141,10 @@ func TestServerReadResource(t *testing.T) { } testCases := map[string]struct { - server *fwserver.Server - request *fwserver.ReadResourceRequest - expectedResponse *fwserver.ReadResourceResponse + server *fwserver.Server + request *fwserver.ReadResourceRequest + expectedResponse *fwserver.ReadResourceResponse + configureProviderReq *provider.ConfigureRequest }{ "nil": { server: &fwserver.Server{ @@ -339,7 +341,46 @@ func TestServerReadResource(t *testing.T) { Private: testEmptyPrivate, }, }, - "response-deferral": { + "response-deferral-automatic": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {}, + ConfigureMethod: func(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + resp.Deferred = &provider.Deferred{Reason: provider.DeferredReasonProviderConfigUnknown} + }, + }, + }, + configureProviderReq: &provider.ConfigureRequest{ + ClientCapabilities: provider.ConfigureProviderClientCapabilities{ + DeferralAllowed: true, + }, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + resp.Deferred = &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq} + + if data.TestRequired.ValueString() != "test-currentstate-value" { + resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) + } + }, + }, + ClientCapabilities: testDeferralAllowed, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: testCurrentState, + Deferred: &resource.Deferred{Reason: resource.DeferredReasonProviderConfigUnknown}, + }, + }, + "response-deferral-manual": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, @@ -579,6 +620,11 @@ func TestServerReadResource(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() + if testCase.configureProviderReq != nil { + configureProviderResp := &provider.ConfigureResponse{} + testCase.server.ConfigureProvider(context.Background(), testCase.configureProviderReq, configureProviderResp) + } + response := &fwserver.ReadResourceResponse{} testCase.server.ReadResource(context.Background(), testCase.request, response) diff --git a/internal/proto5server/server_configureprovider.go b/internal/proto5server/server_configureprovider.go index 3f5d22e37..ef33f39e8 100644 --- a/internal/proto5server/server_configureprovider.go +++ b/internal/proto5server/server_configureprovider.go @@ -6,11 +6,12 @@ package proto5server import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" "github.com/hashicorp/terraform-plugin-framework/provider" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) // ConfigureProvider satisfies the tfprotov5.ProviderServer interface. diff --git a/internal/proto5server/server_planresourcechange_test.go b/internal/proto5server/server_planresourcechange_test.go index 1db515b8f..ad16683fb 100644 --- a/internal/proto5server/server_planresourcechange_test.go +++ b/internal/proto5server/server_planresourcechange_test.go @@ -8,6 +8,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/path" @@ -16,8 +19,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerPlanResourceChange(t *testing.T) { From a1e8983aa3a4551cb137f2c840b8c68939648a46 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 20 May 2024 13:46:56 -0400 Subject: [PATCH 33/44] Update resource/metadata.go Co-authored-by: Austin Valle --- resource/metadata.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/resource/metadata.go b/resource/metadata.go index 9e2bf9a1a..ac11a3d0e 100644 --- a/resource/metadata.go +++ b/resource/metadata.go @@ -35,6 +35,11 @@ type ResourceBehavior struct { ProviderDeferred ProviderDeferredBehavior } +// ProviderDeferredBehavior enables provider-defined logic to be executed +// in the case of a deferred response from provider configuration. +// +// 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. type ProviderDeferredBehavior struct { // When EnablePlanModification is true, framework will still execute // provider-defined resource plan modification logic if From 299ac681faa0a2d54d3713108c9fd99c4525d2a1 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 20 May 2024 13:47:47 -0400 Subject: [PATCH 34/44] Add experimental note --- internal/fwserver/server.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index 5a0f90722..2415f6801 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -59,6 +59,9 @@ type Server struct { // deferred indicates an automatic provider deferral. When this is set, // the provider will automatically defer the PlanResourceChange, ReadResource, // ImportResourceState, and ReadDataSource RPCs. + // + // 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. deferred *provider.Deferred // functionDefinitions is the cached Function Definitions for RPCs that need to From 4669aaa340159cbb07174d23540c207adc79251a Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 20 May 2024 17:46:07 -0400 Subject: [PATCH 35/44] Implement resource behavior in `proto6server` --- internal/fromproto6/planresourcechange.go | 3 ++- .../fromproto6/planresourcechange_test.go | 20 ++++++++++++++++++- .../proto6server/server_planresourcechange.go | 13 ++++++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/internal/fromproto6/planresourcechange.go b/internal/fromproto6/planresourcechange.go index 6a10ee180..6b95f0789 100644 --- a/internal/fromproto6/planresourcechange.go +++ b/internal/fromproto6/planresourcechange.go @@ -17,7 +17,7 @@ import ( // PlanResourceChangeRequest returns the *fwserver.PlanResourceChangeRequest // equivalent of a *tfprotov6.PlanResourceChangeRequest. -func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResourceChangeRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema) (*fwserver.PlanResourceChangeRequest, diag.Diagnostics) { +func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResourceChangeRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, resourceBehavior resource.ResourceBehavior) (*fwserver.PlanResourceChangeRequest, diag.Diagnostics) { if proto6 == nil { return nil, nil } @@ -39,6 +39,7 @@ func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResour } fw := &fwserver.PlanResourceChangeRequest{ + ResourceBehavior: resourceBehavior, ResourceSchema: resourceSchema, Resource: reqResource, ClientCapabilities: ModifyPlanClientCapabilities(proto6.ClientCapabilities), diff --git a/internal/fromproto6/planresourcechange_test.go b/internal/fromproto6/planresourcechange_test.go index e30dfe99d..81901c514 100644 --- a/internal/fromproto6/planresourcechange_test.go +++ b/internal/fromproto6/planresourcechange_test.go @@ -56,6 +56,7 @@ func TestPlanResourceChangeRequest(t *testing.T) { testCases := map[string]struct { input *tfprotov6.PlanResourceChangeRequest + resourceBehavior resource.ResourceBehavior resourceSchema fwschema.Schema resource resource.Resource providerMetaSchema fwschema.Schema @@ -241,6 +242,23 @@ func TestPlanResourceChangeRequest(t *testing.T) { ResourceSchema: testFwSchema, }, }, + "resource-behavior": { + input: &tfprotov6.PlanResourceChangeRequest{}, + resourceSchema: testFwSchema, + resourceBehavior: resource.ResourceBehavior{ + ProviderDeferred: resource.ProviderDeferredBehavior{ + EnablePlanModification: true, + }, + }, + expected: &fwserver.PlanResourceChangeRequest{ + ResourceBehavior: resource.ResourceBehavior{ + ProviderDeferred: resource.ProviderDeferredBehavior{ + EnablePlanModification: true, + }, + }, + ResourceSchema: testFwSchema, + }, + }, } for name, testCase := range testCases { @@ -249,7 +267,7 @@ func TestPlanResourceChangeRequest(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto6.PlanResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema) + got, diags := fromproto6.PlanResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.resourceBehavior) if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/proto6server/server_planresourcechange.go b/internal/proto6server/server_planresourcechange.go index 9782fc04e..32d13ddd6 100644 --- a/internal/proto6server/server_planresourcechange.go +++ b/internal/proto6server/server_planresourcechange.go @@ -6,11 +6,12 @@ package proto6server import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) // PlanResourceChange satisfies the tfprotov6.ProviderServer interface. @@ -44,7 +45,15 @@ func (s *Server) PlanResourceChange(ctx context.Context, proto6Req *tfprotov6.Pl return toproto6.PlanResourceChangeResponse(ctx, fwResp), nil } - fwReq, diags := fromproto6.PlanResourceChangeRequest(ctx, proto6Req, resource, resourceSchema, providerMetaSchema) + resourceBehavior, diags := s.FrameworkServer.ResourceBehavior(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.PlanResourceChangeResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto6.PlanResourceChangeRequest(ctx, proto6Req, resource, resourceSchema, providerMetaSchema, resourceBehavior) fwResp.Diagnostics.Append(diags...) From fe78dbb5423f207600ae090070966741ee59558f Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 20 May 2024 18:08:55 -0400 Subject: [PATCH 36/44] Add changelog entries --- .changes/unreleased/FEATURES-20240520-180458.yaml | 6 ++++++ .changes/unreleased/FEATURES-20240520-180735.yaml | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 .changes/unreleased/FEATURES-20240520-180458.yaml create mode 100644 .changes/unreleased/FEATURES-20240520-180735.yaml diff --git a/.changes/unreleased/FEATURES-20240520-180458.yaml b/.changes/unreleased/FEATURES-20240520-180458.yaml new file mode 100644 index 000000000..2127002c5 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240520-180458.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'provider: Add `Deferred` field to `ConfigureResponse` + which indicates a provider deferred action to the Terraform client' +time: 2024-05-20T18:04:58.852448-04:00 +custom: + Issue: "1002" diff --git a/.changes/unreleased/FEATURES-20240520-180735.yaml b/.changes/unreleased/FEATURES-20240520-180735.yaml new file mode 100644 index 000000000..5d150c86d --- /dev/null +++ b/.changes/unreleased/FEATURES-20240520-180735.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'provider: Add `ClientCapabilities` field to `ConfigureRequest` which specifies + optionally supported protocol features for the Terraform client' +time: 2024-05-20T18:07:35.862641-04:00 +custom: + Issue: "1002" From 277ce8277869a450952fc597c50738f5a2c76866 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 21 May 2024 13:51:57 -0400 Subject: [PATCH 37/44] Apply suggestions from code review Co-authored-by: Austin Valle --- internal/fwserver/server.go | 3 --- resource/metadata.go | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index 2415f6801..5a0f90722 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -59,9 +59,6 @@ type Server struct { // deferred indicates an automatic provider deferral. When this is set, // the provider will automatically defer the PlanResourceChange, ReadResource, // ImportResourceState, and ReadDataSource RPCs. - // - // 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. deferred *provider.Deferred // functionDefinitions is the cached Function Definitions for RPCs that need to diff --git a/resource/metadata.go b/resource/metadata.go index ac11a3d0e..289cd6ab2 100644 --- a/resource/metadata.go +++ b/resource/metadata.go @@ -32,6 +32,9 @@ type MetadataResponse struct { type ResourceBehavior struct { // ProviderDeferred enables provider-defined logic to be executed // in the case of an automatic deferred response from provider configure. + // + // 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. ProviderDeferred ProviderDeferredBehavior } From 859341237bebe82e1544832c50c11f7042d48a1e Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 21 May 2024 15:52:21 -0400 Subject: [PATCH 38/44] Log deferred reason in debug logging --- internal/fwserver/server_configureprovider.go | 2 +- internal/fwserver/server_importresourcestate.go | 6 +++++- internal/fwserver/server_planresourcechange.go | 6 +++++- internal/fwserver/server_readdatasource.go | 6 +++++- internal/fwserver/server_readresource.go | 6 +++++- internal/logging/keys.go | 3 +++ 6 files changed, 24 insertions(+), 5 deletions(-) diff --git a/internal/fwserver/server_configureprovider.go b/internal/fwserver/server_configureprovider.go index a437fbff0..2481e7fad 100644 --- a/internal/fwserver/server_configureprovider.go +++ b/internal/fwserver/server_configureprovider.go @@ -31,10 +31,10 @@ func (s *Server) ConfigureProvider(ctx context.Context, req *provider.ConfigureR } else { logging.FrameworkDebug(ctx, "Provider has configured a deferred response, "+ "all associated resources and data sources will automatically return a deferred response.") - s.deferred = resp.Deferred } } + s.deferred = resp.Deferred s.DataSourceConfigureData = resp.DataSourceData s.ResourceConfigureData = resp.ResourceData } diff --git a/internal/fwserver/server_importresourcestate.go b/internal/fwserver/server_importresourcestate.go index 7965bf7eb..7288cc3e7 100644 --- a/internal/fwserver/server_importresourcestate.go +++ b/internal/fwserver/server_importresourcestate.go @@ -56,7 +56,11 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta } if s.deferred != nil { - logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.") + logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.", + map[string]interface{}{ + logging.KeyDeferredReason: s.deferred.Reason.String(), + }, + ) // Send an unknown value for the imported object resp.ImportedResources = []ImportedResource{ { diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index 64a70856f..7a2b576ba 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -254,7 +254,11 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange // Skip resource-level ModifyPlan for automatic deferrals // unless ProviderDeferredBehavior.EnablePlanModification is true if s.deferred != nil && !req.ResourceBehavior.ProviderDeferred.EnablePlanModification { - logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.") + logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.", + map[string]interface{}{ + logging.KeyDeferredReason: s.deferred.Reason.String(), + }, + ) resp.Deferred = &resource.Deferred{ Reason: resource.DeferredReason(s.deferred.Reason), } diff --git a/internal/fwserver/server_readdatasource.go b/internal/fwserver/server_readdatasource.go index 1eda02bc7..94cb1df4d 100644 --- a/internal/fwserver/server_readdatasource.go +++ b/internal/fwserver/server_readdatasource.go @@ -41,7 +41,11 @@ func (s *Server) ReadDataSource(ctx context.Context, req *ReadDataSourceRequest, } if s.deferred != nil { - logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.") + logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.", + map[string]interface{}{ + logging.KeyDeferredReason: s.deferred.Reason.String(), + }, + ) // Send an unknown value for the data source resp.State = &tfsdk.State{ Raw: tftypes.NewValue(req.DataSourceSchema.Type().TerraformType(ctx), tftypes.UnknownValue), diff --git a/internal/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index 8b7b08fb5..628a2e445 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -50,7 +50,11 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res } if s.deferred != nil { - logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.") + logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.", + map[string]interface{}{ + logging.KeyDeferredReason: s.deferred.Reason.String(), + }, + ) resp.NewState = req.CurrentState resp.Deferred = &resource.Deferred{ Reason: resource.DeferredReason(s.deferred.Reason), diff --git a/internal/logging/keys.go b/internal/logging/keys.go index 7c68d0f13..312a839ac 100644 --- a/internal/logging/keys.go +++ b/internal/logging/keys.go @@ -18,6 +18,9 @@ const ( // The type of data source being operated on, such as "archive_file" KeyDataSourceType = "tf_data_source_type" + // The Deferred reason for an RPC response + KeyDeferredReason = "tf_deferred_reason" + // Human readable string when calling a provider defined type that must // implement the Description() method, such as validators. KeyDescription = "description" From 904a87f1c26ad25f5b821975890249335b3d6a50 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 21 May 2024 15:58:23 -0400 Subject: [PATCH 39/44] Add error diagnostics to automatic deferral tests --- .../fwserver/server_importresourcestate_test.go | 11 +---------- internal/fwserver/server_planresourcechange_test.go | 12 +----------- internal/fwserver/server_readdatasource_test.go | 13 +------------ internal/fwserver/server_readresource_test.go | 13 +------------ 4 files changed, 4 insertions(+), 45 deletions(-) diff --git a/internal/fwserver/server_importresourcestate_test.go b/internal/fwserver/server_importresourcestate_test.go index 33dcb167c..bd0275a12 100644 --- a/internal/fwserver/server_importresourcestate_test.go +++ b/internal/fwserver/server_importresourcestate_test.go @@ -309,16 +309,7 @@ func TestServerImportResourceState(t *testing.T) { Resource: &testprovider.ResourceWithImportState{ Resource: &testprovider.Resource{}, ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - if req.ID != "test-id" { - resp.Diagnostics.AddError("unexpected req.ID value: %s", req.ID) - } - - resp.Deferred = &resource.Deferred{ - Reason: resource.DeferredReasonAbsentPrereq, - } - - resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) - + resp.Diagnostics.AddError("Test assertion failed: ", "import shouldn't be called") }, }, TypeName: "test_resource", diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index bf412180f..cc800fbd9 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -3106,17 +3106,7 @@ func TestServerPlanResourceChange(t *testing.T) { ResourceSchema: testSchema, Resource: &testprovider.ResourceWithModifyPlan{ ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { - if req.ClientCapabilities.DeferralAllowed == true { - resp.Deferred = &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq} - } - - var data testSchemaData - - resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - - data.TestComputed = types.StringValue("test-plannedstate-value") - - resp.Diagnostics.Append(resp.Plan.Set(ctx, &data)...) + resp.Diagnostics.AddError("Test assertion failed: ", "modifyplan shouldn't be called") }, }, }, diff --git a/internal/fwserver/server_readdatasource_test.go b/internal/fwserver/server_readdatasource_test.go index 1c891c2a4..d2f91bd37 100644 --- a/internal/fwserver/server_readdatasource_test.go +++ b/internal/fwserver/server_readdatasource_test.go @@ -264,18 +264,7 @@ func TestServerReadDataSource(t *testing.T) { DataSourceSchema: testSchema, DataSource: &testprovider.DataSource{ ReadMethod: func(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var config struct { - TestComputed types.String `tfsdk:"test_computed"` - TestRequired types.String `tfsdk:"test_required"` - } - - resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - - resp.Deferred = &datasource.Deferred{Reason: datasource.DeferredReasonAbsentPrereq} - - if config.TestRequired.ValueString() != "test-config-value" { - resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) - } + resp.Diagnostics.AddError("Test assertion failed: ", "read shouldn't be called") }, }, ClientCapabilities: testDeferralAllowed, diff --git a/internal/fwserver/server_readresource_test.go b/internal/fwserver/server_readresource_test.go index 033a95c29..a9520edd4 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -359,18 +359,7 @@ func TestServerReadResource(t *testing.T) { CurrentState: testCurrentState, Resource: &testprovider.Resource{ ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data struct { - TestComputed types.String `tfsdk:"test_computed"` - TestRequired types.String `tfsdk:"test_required"` - } - - resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - - resp.Deferred = &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq} - - if data.TestRequired.ValueString() != "test-currentstate-value" { - resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) - } + resp.Diagnostics.AddError("Test assertion failed: ", "read shouldn't be called") }, }, ClientCapabilities: testDeferralAllowed, From 31f33f83f969b60cf83a3194165ce66965fa8491 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 23 May 2024 18:08:16 -0400 Subject: [PATCH 40/44] Refactor `PlanResourceChange` automatic deferred action implementation based on PR feedback. --- .../fwserver/server_planresourcechange.go | 59 +++++++++++-------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index 7a2b576ba..cdc124d1d 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -53,7 +53,27 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange return } - if resourceWithConfigure, ok := req.Resource.(resource.ResourceWithConfigure); ok { + var skipPlanModification bool + + // Skip ModifyPlan for automatic deferrals + // unless ProviderDeferredBehavior.EnablePlanModification is true. + if s.deferred != nil && !req.ResourceBehavior.ProviderDeferred.EnablePlanModification { + logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.", + map[string]interface{}{ + logging.KeyDeferredReason: s.deferred.Reason.String(), + }, + ) + resp.Deferred = &resource.Deferred{ + Reason: resource.DeferredReason(s.deferred.Reason), + } + + // Flag to skip schema default plan modifiers, schema plan modifiers, + // and the resource Configure method call but continue to run the logic + // to mark null computed attributes as unknown. + skipPlanModification = true + } + + if resourceWithConfigure, ok := req.Resource.(resource.ResourceWithConfigure); ok && !skipPlanModification { logging.FrameworkTrace(ctx, "Resource implements ResourceWithConfigure") configureReq := resource.ConfigureRequest{ @@ -119,7 +139,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange // identifying any attributes which are null within the configuration, and if the attribute // has a default value specified by the `Default` field on the attribute then the default // value is assigned. - if !resp.PlannedState.Raw.IsNull() { + if !resp.PlannedState.Raw.IsNull() && !skipPlanModification { data := fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, Schema: resp.PlannedState.Schema, @@ -214,6 +234,10 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange } resp.PlannedState.Raw = modifiedPlan + + if skipPlanModification { + return + } } // Execute any schema-based plan modifiers. This allows overwriting @@ -251,20 +275,6 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange } } - // Skip resource-level ModifyPlan for automatic deferrals - // unless ProviderDeferredBehavior.EnablePlanModification is true - if s.deferred != nil && !req.ResourceBehavior.ProviderDeferred.EnablePlanModification { - logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.", - map[string]interface{}{ - logging.KeyDeferredReason: s.deferred.Reason.String(), - }, - ) - resp.Deferred = &resource.Deferred{ - Reason: resource.DeferredReason(s.deferred.Reason), - } - return - } - // Execute any resource-level ModifyPlan method. This allows // overwriting any unknown values. // @@ -303,19 +313,22 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange resp.PlannedState = planToState(modifyPlanResp.Plan) resp.RequiresReplace = append(resp.RequiresReplace, modifyPlanResp.RequiresReplace...) resp.PlannedPrivate.Provider = modifyPlanResp.Private + resp.Deferred = modifyPlanResp.Deferred // Provider deferred response is present, add the deferred response alongside the provider-modified plan if s.deferred != nil { logging.FrameworkDebug(ctx, "Provider has deferred response configured, returning deferred response with modified plan.") - if modifyPlanResp.Deferred != nil { - logging.FrameworkDebug(ctx, fmt.Sprintf("Provider deferred response reason: %s replaced resource deferred response reason: %s", + // Only set the response to the provider configured deferred reason if there is no resource configured deferred reason + if resp.Deferred == nil { + resp.Deferred = &resource.Deferred{ + Reason: resource.DeferredReason(s.deferred.Reason), + } + } else { + logging.FrameworkDebug(ctx, fmt.Sprintf("Resource has deferred reason configured, "+ + "replacing provider deferred reason: %s with resource deferred reason: %s", s.deferred.Reason.String(), modifyPlanResp.Deferred.Reason.String())) } - resp.Deferred = &resource.Deferred{ - Reason: resource.DeferredReason(s.deferred.Reason), - } - } else { - resp.Deferred = modifyPlanResp.Deferred + return } } From a05621a6c89831e04d1f83a5c82518319308ce04 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 23 May 2024 18:16:41 -0400 Subject: [PATCH 41/44] Add a comment calling out intentional design of replacing configured values with `Unknown` --- internal/fwserver/server_readdatasource.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/fwserver/server_readdatasource.go b/internal/fwserver/server_readdatasource.go index 94cb1df4d..173282321 100644 --- a/internal/fwserver/server_readdatasource.go +++ b/internal/fwserver/server_readdatasource.go @@ -46,7 +46,9 @@ func (s *Server) ReadDataSource(ctx context.Context, req *ReadDataSourceRequest, logging.KeyDeferredReason: s.deferred.Reason.String(), }, ) - // Send an unknown value for the data source + // Send an unknown value for the data source. This will replace any configured values + // for ease of implementation as Terraform Core currently does not use these values for + // deferred actions, but this design could change in the future. resp.State = &tfsdk.State{ Raw: tftypes.NewValue(req.DataSourceSchema.Type().TerraformType(ctx), tftypes.UnknownValue), Schema: req.DataSourceSchema, From a3dbd79ceb72dcc7b19f51c6860ede30128bb301 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 29 May 2024 16:10:15 -0400 Subject: [PATCH 42/44] Return early in `PlanResourceChange` if `ProviderDeferredBehavior.EnablePlanModification` is false. --- .../fwserver/server_planresourcechange.go | 20 +++++++------------ .../server_planresourcechange_test.go | 3 +-- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index cdc124d1d..fc7413e38 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -53,9 +53,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange return } - var skipPlanModification bool - - // Skip ModifyPlan for automatic deferrals + // Skip ModifyPlan for automatic deferrals with proposed new state as a best effort for PlannedState // unless ProviderDeferredBehavior.EnablePlanModification is true. if s.deferred != nil && !req.ResourceBehavior.ProviderDeferred.EnablePlanModification { logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.", @@ -63,17 +61,17 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange logging.KeyDeferredReason: s.deferred.Reason.String(), }, ) + + resp.PlannedState = planToState(*req.ProposedNewState) + resp.PlannedPrivate = req.PriorPrivate resp.Deferred = &resource.Deferred{ Reason: resource.DeferredReason(s.deferred.Reason), } - // Flag to skip schema default plan modifiers, schema plan modifiers, - // and the resource Configure method call but continue to run the logic - // to mark null computed attributes as unknown. - skipPlanModification = true + return } - if resourceWithConfigure, ok := req.Resource.(resource.ResourceWithConfigure); ok && !skipPlanModification { + if resourceWithConfigure, ok := req.Resource.(resource.ResourceWithConfigure); ok { logging.FrameworkTrace(ctx, "Resource implements ResourceWithConfigure") configureReq := resource.ConfigureRequest{ @@ -139,7 +137,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange // identifying any attributes which are null within the configuration, and if the attribute // has a default value specified by the `Default` field on the attribute then the default // value is assigned. - if !resp.PlannedState.Raw.IsNull() && !skipPlanModification { + if !resp.PlannedState.Raw.IsNull() { data := fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, Schema: resp.PlannedState.Schema, @@ -234,10 +232,6 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange } resp.PlannedState.Raw = modifiedPlan - - if skipPlanModification { - return - } } // Execute any schema-based plan modifiers. This allows overwriting diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index cc800fbd9..3de894300 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -3114,12 +3114,11 @@ func TestServerPlanResourceChange(t *testing.T) { Deferred: &resource.Deferred{Reason: resource.DeferredReasonProviderConfigUnknown}, PlannedState: &tfsdk.State{ Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ - "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_computed": tftypes.NewValue(tftypes.String, nil), "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), }), Schema: testSchema, }, - PlannedPrivate: testEmptyPrivate, }, }, "create-resourcewithmodifyplan-response-deferral-manual": { From b3f4d24889699e3c06926f079dabe0e35891db44 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 30 May 2024 18:09:19 -0400 Subject: [PATCH 43/44] Update internal/fwserver/server_configureprovider.go Co-authored-by: Brian Flad --- internal/fwserver/server_configureprovider.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/fwserver/server_configureprovider.go b/internal/fwserver/server_configureprovider.go index 2481e7fad..2e04bc046 100644 --- a/internal/fwserver/server_configureprovider.go +++ b/internal/fwserver/server_configureprovider.go @@ -28,10 +28,10 @@ func (s *Server) ConfigureProvider(ctx context.Context, req *provider.ConfigureR "Provider configured a deferred response for all resources and data sources but the Terraform request "+ "did not indicate support for deferred actions. This is an issue with the provider and should be reported to the provider developers.") return - } else { - logging.FrameworkDebug(ctx, "Provider has configured a deferred response, "+ - "all associated resources and data sources will automatically return a deferred response.") } + + logging.FrameworkDebug(ctx, "Provider has configured a deferred response, "+ + "all associated resources and data sources will automatically return a deferred response.") } s.deferred = resp.Deferred From a7d72923e9635432ba90e74494b7232fa49fb2cc Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 30 May 2024 18:22:13 -0400 Subject: [PATCH 44/44] Add separate unit test for overriding provider deferred reason with resource deferred reason. --- .../server_planresourcechange_test.go | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index 3de894300..565fe5370 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -3009,7 +3009,7 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testEmptyPrivate, }, }, - "create-resourcewithmodifyplan-response-deferral-automatic-plan-modification": { + "create-resourcewithmodifyplan-response-deferral-automatic-override-provider-deferral-reason": { server: &fwserver.Server{ Provider: &testprovider.Provider{ SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {}, @@ -3024,6 +3024,7 @@ func TestServerPlanResourceChange(t *testing.T) { }, }, request: &fwserver.PlanResourceChangeRequest{ + ClientCapabilities: testDeferralAllowed, ResourceBehavior: resource.ResourceBehavior{ ProviderDeferred: resource.ProviderDeferredBehavior{ EnablePlanModification: true, @@ -3061,6 +3062,66 @@ func TestServerPlanResourceChange(t *testing.T) { }, }, }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + Deferred: &resource.Deferred{Reason: resource.DeferredReasonAbsentPrereq}, + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, + "create-resourcewithmodifyplan-response-deferral-automatic-plan-modification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {}, + ConfigureMethod: func(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + resp.Deferred = &provider.Deferred{Reason: provider.DeferredReasonProviderConfigUnknown} + }, + }, + }, + configureProviderReq: &provider.ConfigureRequest{ + ClientCapabilities: provider.ConfigureProviderClientCapabilities{ + DeferralAllowed: true, + }, + }, + request: &fwserver.PlanResourceChangeRequest{ + ResourceBehavior: resource.ResourceBehavior{ + ProviderDeferred: resource.ProviderDeferredBehavior{ + EnablePlanModification: true, + }, + }, + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PriorState: testEmptyState, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + data.TestComputed = types.StringValue("test-plannedstate-value") + + resp.Diagnostics.Append(resp.Plan.Set(ctx, &data)...) + }, + }, + }, expectedResponse: &fwserver.PlanResourceChangeResponse{ Deferred: &resource.Deferred{Reason: resource.DeferredReasonProviderConfigUnknown}, PlannedState: &tfsdk.State{