From 4b4a7f85d5dc653cd17da92f978895798db2a2cb Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 19 May 2026 15:19:17 -0400 Subject: [PATCH] validate action deferrals in grpc client --- internal/plugin/grpc_provider.go | 13 ++++++++--- internal/plugin/grpc_provider_test.go | 31 ++++++++++++++++++++++++++ internal/plugin6/grpc_provider.go | 13 ++++++++--- internal/plugin6/grpc_provider_test.go | 29 ++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 6 deletions(-) diff --git a/internal/plugin/grpc_provider.go b/internal/plugin/grpc_provider.go index 34196cb8d9..f590381ff1 100644 --- a/internal/plugin/grpc_provider.go +++ b/internal/plugin/grpc_provider.go @@ -23,6 +23,7 @@ import ( "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/plugin/convert" "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfdiags" proto "github.com/hashicorp/terraform/internal/tfplugin5" ) @@ -1507,11 +1508,17 @@ func (p *GRPCProvider) PlanAction(r providers.PlanActionRequest) (resp providers return resp } - resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) - if resp.Diagnostics.HasErrors() { - return resp + // We only allow deferral from the provider as a whole. The provider must be + // able to accept unknown configuration. + if protoResp.Deferred != nil && protoResp.Deferred.Reason != proto.Deferred_PROVIDER_CONFIG_UNKNOWN { + resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody( + tfdiags.Error, + "Invalid deferred reason", + fmt.Sprintf("An action can only be deferred due to an unknown provider configuration. Provider %s returned %s.", p.Addr.ForDisplay(), protoResp.Deferred.Reason), + )) } + resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) return resp } diff --git a/internal/plugin/grpc_provider_test.go b/internal/plugin/grpc_provider_test.go index 6b51da5079..5910751132 100644 --- a/internal/plugin/grpc_provider_test.go +++ b/internal/plugin/grpc_provider_test.go @@ -1646,6 +1646,37 @@ func TestGRPCProvider_planAction_invalid_config(t *testing.T) { checkDiagsHasError(t, resp.Diagnostics) } +func TestGRPCProvider_planAction_invalid_defer(t *testing.T) { + client := mockProviderClient(t) + + client.EXPECT().PlanAction( + gomock.Any(), + gomock.Any(), + ).Return(&proto.PlanAction_Response{ + Deferred: &proto.Deferred{ + Reason: proto.Deferred_RESOURCE_CONFIG_UNKNOWN, + }, + }, nil) + + p := &GRPCProvider{ + Addr: addrs.Provider{ + Type: "test", + Namespace: "hashicorp", + Hostname: "terraform.io", + }, + client: client, + } + + resp := p.PlanAction(providers.PlanActionRequest{ + ActionType: "action", + ProposedActionData: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("foo"), + }), + }) + + checkDiagsHasError(t, resp.Diagnostics) +} + // Mock implementation of the ListResource stream client type mockListResourceStreamClient struct { events []*proto.ListResource_Event diff --git a/internal/plugin6/grpc_provider.go b/internal/plugin6/grpc_provider.go index 2f62a34466..041e9ba996 100644 --- a/internal/plugin6/grpc_provider.go +++ b/internal/plugin6/grpc_provider.go @@ -25,6 +25,7 @@ import ( "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/plugin6/convert" "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfdiags" proto6 "github.com/hashicorp/terraform/internal/tfplugin6" ) @@ -1931,11 +1932,17 @@ func (p *GRPCProvider) PlanAction(r providers.PlanActionRequest) (resp providers return resp } - resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) - if resp.Diagnostics.HasErrors() { - return resp + // We only allow deferral from the provider as a whole. The provider must be + // able to accept unknown configuration. + if protoResp.Deferred != nil && protoResp.Deferred.Reason != proto6.Deferred_PROVIDER_CONFIG_UNKNOWN { + resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody( + tfdiags.Error, + "Invalid deferred reason", + fmt.Sprintf("An action can only be deferred due to an unknown provider configuration. Provider %s returned %s.", p.Addr.ForDisplay(), protoResp.Deferred.Reason), + )) } + resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) return resp } diff --git a/internal/plugin6/grpc_provider_test.go b/internal/plugin6/grpc_provider_test.go index c0046edddf..b51366dbf8 100644 --- a/internal/plugin6/grpc_provider_test.go +++ b/internal/plugin6/grpc_provider_test.go @@ -2138,7 +2138,36 @@ func TestGRPCProvider_invokeAction_invalid(t *testing.T) { checkDiagsHasError(t, resp.Diagnostics) } +func TestGRPCProvider_planAction_invalid_defer(t *testing.T) { + client := mockProviderClient(t) + + client.EXPECT().PlanAction( + gomock.Any(), + gomock.Any(), + ).Return(&proto.PlanAction_Response{ + Deferred: &proto.Deferred{ + Reason: proto.Deferred_RESOURCE_CONFIG_UNKNOWN, + }, + }, nil) + p := &GRPCProvider{ + Addr: addrs.Provider{ + Type: "test", + Namespace: "hashicorp", + Hostname: "terraform.io", + }, + client: client, + } + + resp := p.PlanAction(providers.PlanActionRequest{ + ActionType: "action", + ProposedActionData: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("foo"), + }), + }) + + checkDiagsHasError(t, resp.Diagnostics) +} func TestGRPCProvider_ValidateStateStoreConfig_returns_validation_errors(t *testing.T) { storeName := "mock_store" // mockProviderClient returns a mock that has this state store in its schemas