From a58e305a6a2b95394b15d6cd2991cf31f465b120 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 3 May 2024 08:40:52 -0400 Subject: [PATCH] added logging for req/resp deferred actions --- internal/logging/keys.go | 6 + .../tf5serverlogging/client_capabilities.go | 76 ++++ .../client_capabilities_test.go | 367 ++++++++++++++++++ .../internal/tf5serverlogging/deferred.go | 24 ++ .../tf5serverlogging/deferred_test.go | 80 ++++ tfprotov5/tf5server/server.go | 10 + .../tf6serverlogging/client_capabilities.go | 76 ++++ .../client_capabilities_test.go | 367 ++++++++++++++++++ .../internal/tf6serverlogging/deferred.go | 24 ++ .../tf6serverlogging/deferred_test.go | 80 ++++ tfprotov6/tf6server/server.go | 10 + 11 files changed, 1120 insertions(+) create mode 100644 tfprotov5/internal/tf5serverlogging/client_capabilities.go create mode 100644 tfprotov5/internal/tf5serverlogging/client_capabilities_test.go create mode 100644 tfprotov5/internal/tf5serverlogging/deferred.go create mode 100644 tfprotov5/internal/tf5serverlogging/deferred_test.go create mode 100644 tfprotov6/internal/tf6serverlogging/client_capabilities.go create mode 100644 tfprotov6/internal/tf6serverlogging/client_capabilities_test.go create mode 100644 tfprotov6/internal/tf6serverlogging/deferred.go create mode 100644 tfprotov6/internal/tf6serverlogging/deferred_test.go diff --git a/internal/logging/keys.go b/internal/logging/keys.go index 7ad912714..fb8214429 100644 --- a/internal/logging/keys.go +++ b/internal/logging/keys.go @@ -63,9 +63,15 @@ const ( // The protocol version being used, as a string, such as "6" KeyProtocolVersion = "tf_proto_version" + // The Deferred reason for an RPC response + KeyDeferredReason = "tf_deferred_reason" + // Whether the GetProviderSchemaOptional server capability is enabled KeyServerCapabilityGetProviderSchemaOptional = "tf_server_capability_get_provider_schema_optional" // Whether the PlanDestroy server capability is enabled KeyServerCapabilityPlanDestroy = "tf_server_capability_plan_destroy" + + // Whether the DeferralAllowed client capability is enabled + KeyClientCapabilityDeferralAllowed = "tf_client_capability_deferral_allowed" ) diff --git a/tfprotov5/internal/tf5serverlogging/client_capabilities.go b/tfprotov5/internal/tf5serverlogging/client_capabilities.go new file mode 100644 index 000000000..8211f50f1 --- /dev/null +++ b/tfprotov5/internal/tf5serverlogging/client_capabilities.go @@ -0,0 +1,76 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf5serverlogging + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/internal/logging" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// ConfigureProviderClientCapabilities generates a TRACE "Announced client capabilities" log. +func ConfigureProviderClientCapabilities(ctx context.Context, capabilities *tfprotov5.ConfigureProviderClientCapabilities) { + responseFields := map[string]interface{}{ + logging.KeyClientCapabilityDeferralAllowed: false, + } + + if capabilities != nil { + responseFields[logging.KeyClientCapabilityDeferralAllowed] = capabilities.DeferralAllowed + } + + logging.ProtocolTrace(ctx, "Announced client capabilities", responseFields) +} + +// ReadDataSourceClientCapabilities generates a TRACE "Announced client capabilities" log. +func ReadDataSourceClientCapabilities(ctx context.Context, capabilities *tfprotov5.ReadDataSourceClientCapabilities) { + responseFields := map[string]interface{}{ + logging.KeyClientCapabilityDeferralAllowed: false, + } + + if capabilities != nil { + responseFields[logging.KeyClientCapabilityDeferralAllowed] = capabilities.DeferralAllowed + } + + logging.ProtocolTrace(ctx, "Announced client capabilities", responseFields) +} + +// ReadResourceClientCapabilities generates a TRACE "Announced client capabilities" log. +func ReadResourceClientCapabilities(ctx context.Context, capabilities *tfprotov5.ReadResourceClientCapabilities) { + responseFields := map[string]interface{}{ + logging.KeyClientCapabilityDeferralAllowed: false, + } + + if capabilities != nil { + responseFields[logging.KeyClientCapabilityDeferralAllowed] = capabilities.DeferralAllowed + } + + logging.ProtocolTrace(ctx, "Announced client capabilities", responseFields) +} + +// PlanResourceChangeClientCapabilities generates a TRACE "Announced client capabilities" log. +func PlanResourceChangeClientCapabilities(ctx context.Context, capabilities *tfprotov5.PlanResourceChangeClientCapabilities) { + responseFields := map[string]interface{}{ + logging.KeyClientCapabilityDeferralAllowed: false, + } + + if capabilities != nil { + responseFields[logging.KeyClientCapabilityDeferralAllowed] = capabilities.DeferralAllowed + } + + logging.ProtocolTrace(ctx, "Announced client capabilities", responseFields) +} + +// ImportResourceStateClientCapabilities generates a TRACE "Announced client capabilities" log. +func ImportResourceStateClientCapabilities(ctx context.Context, capabilities *tfprotov5.ImportResourceStateClientCapabilities) { + responseFields := map[string]interface{}{ + logging.KeyClientCapabilityDeferralAllowed: false, + } + + if capabilities != nil { + responseFields[logging.KeyClientCapabilityDeferralAllowed] = capabilities.DeferralAllowed + } + + logging.ProtocolTrace(ctx, "Announced client capabilities", responseFields) +} diff --git a/tfprotov5/internal/tf5serverlogging/client_capabilities_test.go b/tfprotov5/internal/tf5serverlogging/client_capabilities_test.go new file mode 100644 index 000000000..fe38f73e2 --- /dev/null +++ b/tfprotov5/internal/tf5serverlogging/client_capabilities_test.go @@ -0,0 +1,367 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf5serverlogging_test + +import ( + "bytes" + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/internal/logging" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tf5serverlogging" + "github.com/hashicorp/terraform-plugin-log/tfsdklog" + "github.com/hashicorp/terraform-plugin-log/tfsdklogtest" +) + +func TestConfigureProviderClientCapabilities(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + capabilities *tfprotov5.ConfigureProviderClientCapabilities + expected []map[string]interface{} + }{ + "nil": { + capabilities: nil, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "empty": { + capabilities: &tfprotov5.ConfigureProviderClientCapabilities{}, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "deferral_allowed": { + capabilities: &tfprotov5.ConfigureProviderClientCapabilities{ + DeferralAllowed: true, + }, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": true, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + + ctx := tfsdklogtest.RootLogger(context.Background(), &output) + ctx = logging.ProtoSubsystemContext(ctx, tfsdklog.Options{}) + + tf5serverlogging.ConfigureProviderClientCapabilities(ctx, testCase.capabilities) + + entries, err := tfsdklogtest.MultilineJSONDecode(&output) + + if err != nil { + t.Fatalf("unable to read multiple line JSON: %s", err) + } + + if diff := cmp.Diff(entries, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestReadDataSourceClientCapabilities(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + capabilities *tfprotov5.ReadDataSourceClientCapabilities + expected []map[string]interface{} + }{ + "nil": { + capabilities: nil, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "empty": { + capabilities: &tfprotov5.ReadDataSourceClientCapabilities{}, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "deferral_allowed": { + capabilities: &tfprotov5.ReadDataSourceClientCapabilities{ + DeferralAllowed: true, + }, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": true, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + + ctx := tfsdklogtest.RootLogger(context.Background(), &output) + ctx = logging.ProtoSubsystemContext(ctx, tfsdklog.Options{}) + + tf5serverlogging.ReadDataSourceClientCapabilities(ctx, testCase.capabilities) + + entries, err := tfsdklogtest.MultilineJSONDecode(&output) + + if err != nil { + t.Fatalf("unable to read multiple line JSON: %s", err) + } + + if diff := cmp.Diff(entries, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestReadResourceClientCapabilities(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + capabilities *tfprotov5.ReadResourceClientCapabilities + expected []map[string]interface{} + }{ + "nil": { + capabilities: nil, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "empty": { + capabilities: &tfprotov5.ReadResourceClientCapabilities{}, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "deferral_allowed": { + capabilities: &tfprotov5.ReadResourceClientCapabilities{ + DeferralAllowed: true, + }, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": true, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + + ctx := tfsdklogtest.RootLogger(context.Background(), &output) + ctx = logging.ProtoSubsystemContext(ctx, tfsdklog.Options{}) + + tf5serverlogging.ReadResourceClientCapabilities(ctx, testCase.capabilities) + + entries, err := tfsdklogtest.MultilineJSONDecode(&output) + + if err != nil { + t.Fatalf("unable to read multiple line JSON: %s", err) + } + + if diff := cmp.Diff(entries, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestPlanResourceChangeClientCapabilities(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + capabilities *tfprotov5.PlanResourceChangeClientCapabilities + expected []map[string]interface{} + }{ + "nil": { + capabilities: nil, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "empty": { + capabilities: &tfprotov5.PlanResourceChangeClientCapabilities{}, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "deferral_allowed": { + capabilities: &tfprotov5.PlanResourceChangeClientCapabilities{ + DeferralAllowed: true, + }, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": true, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + + ctx := tfsdklogtest.RootLogger(context.Background(), &output) + ctx = logging.ProtoSubsystemContext(ctx, tfsdklog.Options{}) + + tf5serverlogging.PlanResourceChangeClientCapabilities(ctx, testCase.capabilities) + + entries, err := tfsdklogtest.MultilineJSONDecode(&output) + + if err != nil { + t.Fatalf("unable to read multiple line JSON: %s", err) + } + + if diff := cmp.Diff(entries, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestImportResourceStateClientCapabilities(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + capabilities *tfprotov5.ImportResourceStateClientCapabilities + expected []map[string]interface{} + }{ + "nil": { + capabilities: nil, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "empty": { + capabilities: &tfprotov5.ImportResourceStateClientCapabilities{}, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "deferral_allowed": { + capabilities: &tfprotov5.ImportResourceStateClientCapabilities{ + DeferralAllowed: true, + }, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": true, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + + ctx := tfsdklogtest.RootLogger(context.Background(), &output) + ctx = logging.ProtoSubsystemContext(ctx, tfsdklog.Options{}) + + tf5serverlogging.ImportResourceStateClientCapabilities(ctx, testCase.capabilities) + + entries, err := tfsdklogtest.MultilineJSONDecode(&output) + + if err != nil { + t.Fatalf("unable to read multiple line JSON: %s", err) + } + + if diff := cmp.Diff(entries, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/tfprotov5/internal/tf5serverlogging/deferred.go b/tfprotov5/internal/tf5serverlogging/deferred.go new file mode 100644 index 000000000..fa9449ccc --- /dev/null +++ b/tfprotov5/internal/tf5serverlogging/deferred.go @@ -0,0 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf5serverlogging + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/internal/logging" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// Deferred generates a TRACE "Received downstream deferred response" log if populated. +func Deferred(ctx context.Context, deferred *tfprotov5.Deferred) { + if deferred == nil { + return + } + + responseFields := map[string]interface{}{ + logging.KeyDeferredReason: deferred.Reason.String(), + } + + logging.ProtocolTrace(ctx, "Received downstream deferred response", responseFields) +} diff --git a/tfprotov5/internal/tf5serverlogging/deferred_test.go b/tfprotov5/internal/tf5serverlogging/deferred_test.go new file mode 100644 index 000000000..33de5beab --- /dev/null +++ b/tfprotov5/internal/tf5serverlogging/deferred_test.go @@ -0,0 +1,80 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf5serverlogging_test + +import ( + "bytes" + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/internal/logging" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tf5serverlogging" + "github.com/hashicorp/terraform-plugin-log/tfsdklog" + "github.com/hashicorp/terraform-plugin-log/tfsdklogtest" +) + +func TestDeferred(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + deferred *tfprotov5.Deferred + expected []map[string]interface{} + }{ + "nil": { + deferred: nil, + expected: nil, + }, + "empty": { + deferred: &tfprotov5.Deferred{}, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Received downstream deferred response", + "@module": "sdk.proto", + "tf_deferred_reason": "UNKNOWN", + }, + }, + }, + "deferred": { + deferred: &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReasonProviderConfigUnknown, + }, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Received downstream deferred response", + "@module": "sdk.proto", + "tf_deferred_reason": "PROVIDER_CONFIG_UNKNOWN", + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + + ctx := tfsdklogtest.RootLogger(context.Background(), &output) + ctx = logging.ProtoSubsystemContext(ctx, tfsdklog.Options{}) + + tf5serverlogging.Deferred(ctx, testCase.deferred) + + entries, err := tfsdklogtest.MultilineJSONDecode(&output) + + if err != nil { + t.Fatalf("unable to read multiple line JSON: %s", err) + } + + if diff := cmp.Diff(entries, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/tfprotov5/tf5server/server.go b/tfprotov5/tf5server/server.go index 29bbc6a41..d6c0daacf 100644 --- a/tfprotov5/tf5server/server.go +++ b/tfprotov5/tf5server/server.go @@ -579,6 +579,7 @@ func (s *server) Configure(ctx context.Context, protoReq *tfplugin5.Configure_Re defer logging.ProtocolTrace(ctx, "Served request") req := fromproto.ConfigureProviderRequest(protoReq) + tf5serverlogging.ConfigureProviderClientCapabilities(ctx, req.ClientCapabilities) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "Config", req.Config) ctx = tf5serverlogging.DownstreamRequest(ctx) @@ -679,6 +680,7 @@ func (s *server) ReadDataSource(ctx context.Context, protoReq *tfplugin5.ReadDat req := fromproto.ReadDataSourceRequest(protoReq) + tf5serverlogging.ReadDataSourceClientCapabilities(ctx, req.ClientCapabilities) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "Config", req.Config) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProviderMeta", req.ProviderMeta) ctx = tf5serverlogging.DownstreamRequest(ctx) @@ -692,6 +694,7 @@ func (s *server) ReadDataSource(ctx context.Context, protoReq *tfplugin5.ReadDat tf5serverlogging.DownstreamResponse(ctx, resp.Diagnostics) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response", "State", resp.State) + tf5serverlogging.Deferred(ctx, resp.Deferred) protoResp := toproto.ReadDataSource_Response(resp) @@ -766,6 +769,7 @@ func (s *server) ReadResource(ctx context.Context, protoReq *tfplugin5.ReadResou req := fromproto.ReadResourceRequest(protoReq) + tf5serverlogging.ReadResourceClientCapabilities(ctx, req.ClientCapabilities) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "CurrentState", req.CurrentState) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProviderMeta", req.ProviderMeta) logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Request", "Private", req.Private) @@ -783,6 +787,7 @@ func (s *server) ReadResource(ctx context.Context, protoReq *tfplugin5.ReadResou logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response", "NewState", resp.NewState) logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Response", "Private", resp.Private) + tf5serverlogging.Deferred(ctx, resp.Deferred) protoResp := toproto.ReadResource_Response(resp) @@ -800,6 +805,7 @@ func (s *server) PlanResourceChange(ctx context.Context, protoReq *tfplugin5.Pla req := fromproto.PlanResourceChangeRequest(protoReq) + tf5serverlogging.PlanResourceChangeClientCapabilities(ctx, req.ClientCapabilities) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "Config", req.Config) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "PriorState", req.PriorState) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProposedNewState", req.ProposedNewState) @@ -818,6 +824,7 @@ func (s *server) PlanResourceChange(ctx context.Context, protoReq *tfplugin5.Pla tf5serverlogging.DownstreamResponse(ctx, resp.Diagnostics) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response", "PlannedState", resp.PlannedState) logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Response", "PlannedPrivate", resp.PlannedPrivate) + tf5serverlogging.Deferred(ctx, resp.Deferred) protoResp := toproto.PlanResourceChange_Response(resp) @@ -870,6 +877,8 @@ func (s *server) ImportResourceState(ctx context.Context, protoReq *tfplugin5.Im req := fromproto.ImportResourceStateRequest(protoReq) + tf5serverlogging.ImportResourceStateClientCapabilities(ctx, req.ClientCapabilities) + ctx = tf5serverlogging.DownstreamRequest(ctx) resp, err := s.downstream.ImportResourceState(ctx, req) @@ -885,6 +894,7 @@ func (s *server) ImportResourceState(ctx context.Context, protoReq *tfplugin5.Im logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response_ImportedResource", "State", importedResource.State) logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Response_ImportedResource", "Private", importedResource.Private) } + tf5serverlogging.Deferred(ctx, resp.Deferred) protoResp := toproto.ImportResourceState_Response(resp) diff --git a/tfprotov6/internal/tf6serverlogging/client_capabilities.go b/tfprotov6/internal/tf6serverlogging/client_capabilities.go new file mode 100644 index 000000000..0c5c35f72 --- /dev/null +++ b/tfprotov6/internal/tf6serverlogging/client_capabilities.go @@ -0,0 +1,76 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf6serverlogging + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/internal/logging" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ConfigureProviderClientCapabilities generates a TRACE "Announced client capabilities" log. +func ConfigureProviderClientCapabilities(ctx context.Context, capabilities *tfprotov6.ConfigureProviderClientCapabilities) { + responseFields := map[string]interface{}{ + logging.KeyClientCapabilityDeferralAllowed: false, + } + + if capabilities != nil { + responseFields[logging.KeyClientCapabilityDeferralAllowed] = capabilities.DeferralAllowed + } + + logging.ProtocolTrace(ctx, "Announced client capabilities", responseFields) +} + +// ReadDataSourceClientCapabilities generates a TRACE "Announced client capabilities" log. +func ReadDataSourceClientCapabilities(ctx context.Context, capabilities *tfprotov6.ReadDataSourceClientCapabilities) { + responseFields := map[string]interface{}{ + logging.KeyClientCapabilityDeferralAllowed: false, + } + + if capabilities != nil { + responseFields[logging.KeyClientCapabilityDeferralAllowed] = capabilities.DeferralAllowed + } + + logging.ProtocolTrace(ctx, "Announced client capabilities", responseFields) +} + +// ReadResourceClientCapabilities generates a TRACE "Announced client capabilities" log. +func ReadResourceClientCapabilities(ctx context.Context, capabilities *tfprotov6.ReadResourceClientCapabilities) { + responseFields := map[string]interface{}{ + logging.KeyClientCapabilityDeferralAllowed: false, + } + + if capabilities != nil { + responseFields[logging.KeyClientCapabilityDeferralAllowed] = capabilities.DeferralAllowed + } + + logging.ProtocolTrace(ctx, "Announced client capabilities", responseFields) +} + +// PlanResourceChangeClientCapabilities generates a TRACE "Announced client capabilities" log. +func PlanResourceChangeClientCapabilities(ctx context.Context, capabilities *tfprotov6.PlanResourceChangeClientCapabilities) { + responseFields := map[string]interface{}{ + logging.KeyClientCapabilityDeferralAllowed: false, + } + + if capabilities != nil { + responseFields[logging.KeyClientCapabilityDeferralAllowed] = capabilities.DeferralAllowed + } + + logging.ProtocolTrace(ctx, "Announced client capabilities", responseFields) +} + +// ImportResourceStateClientCapabilities generates a TRACE "Announced client capabilities" log. +func ImportResourceStateClientCapabilities(ctx context.Context, capabilities *tfprotov6.ImportResourceStateClientCapabilities) { + responseFields := map[string]interface{}{ + logging.KeyClientCapabilityDeferralAllowed: false, + } + + if capabilities != nil { + responseFields[logging.KeyClientCapabilityDeferralAllowed] = capabilities.DeferralAllowed + } + + logging.ProtocolTrace(ctx, "Announced client capabilities", responseFields) +} diff --git a/tfprotov6/internal/tf6serverlogging/client_capabilities_test.go b/tfprotov6/internal/tf6serverlogging/client_capabilities_test.go new file mode 100644 index 000000000..fe0cfe32d --- /dev/null +++ b/tfprotov6/internal/tf6serverlogging/client_capabilities_test.go @@ -0,0 +1,367 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf6serverlogging_test + +import ( + "bytes" + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/internal/logging" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6/internal/tf6serverlogging" + "github.com/hashicorp/terraform-plugin-log/tfsdklog" + "github.com/hashicorp/terraform-plugin-log/tfsdklogtest" +) + +func TestConfigureProviderClientCapabilities(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + capabilities *tfprotov6.ConfigureProviderClientCapabilities + expected []map[string]interface{} + }{ + "nil": { + capabilities: nil, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "empty": { + capabilities: &tfprotov6.ConfigureProviderClientCapabilities{}, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "deferral_allowed": { + capabilities: &tfprotov6.ConfigureProviderClientCapabilities{ + DeferralAllowed: true, + }, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": true, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + + ctx := tfsdklogtest.RootLogger(context.Background(), &output) + ctx = logging.ProtoSubsystemContext(ctx, tfsdklog.Options{}) + + tf6serverlogging.ConfigureProviderClientCapabilities(ctx, testCase.capabilities) + + entries, err := tfsdklogtest.MultilineJSONDecode(&output) + + if err != nil { + t.Fatalf("unable to read multiple line JSON: %s", err) + } + + if diff := cmp.Diff(entries, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestReadDataSourceClientCapabilities(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + capabilities *tfprotov6.ReadDataSourceClientCapabilities + expected []map[string]interface{} + }{ + "nil": { + capabilities: nil, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "empty": { + capabilities: &tfprotov6.ReadDataSourceClientCapabilities{}, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "deferral_allowed": { + capabilities: &tfprotov6.ReadDataSourceClientCapabilities{ + DeferralAllowed: true, + }, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": true, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + + ctx := tfsdklogtest.RootLogger(context.Background(), &output) + ctx = logging.ProtoSubsystemContext(ctx, tfsdklog.Options{}) + + tf6serverlogging.ReadDataSourceClientCapabilities(ctx, testCase.capabilities) + + entries, err := tfsdklogtest.MultilineJSONDecode(&output) + + if err != nil { + t.Fatalf("unable to read multiple line JSON: %s", err) + } + + if diff := cmp.Diff(entries, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestReadResourceClientCapabilities(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + capabilities *tfprotov6.ReadResourceClientCapabilities + expected []map[string]interface{} + }{ + "nil": { + capabilities: nil, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "empty": { + capabilities: &tfprotov6.ReadResourceClientCapabilities{}, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "deferral_allowed": { + capabilities: &tfprotov6.ReadResourceClientCapabilities{ + DeferralAllowed: true, + }, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": true, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + + ctx := tfsdklogtest.RootLogger(context.Background(), &output) + ctx = logging.ProtoSubsystemContext(ctx, tfsdklog.Options{}) + + tf6serverlogging.ReadResourceClientCapabilities(ctx, testCase.capabilities) + + entries, err := tfsdklogtest.MultilineJSONDecode(&output) + + if err != nil { + t.Fatalf("unable to read multiple line JSON: %s", err) + } + + if diff := cmp.Diff(entries, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestPlanResourceChangeClientCapabilities(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + capabilities *tfprotov6.PlanResourceChangeClientCapabilities + expected []map[string]interface{} + }{ + "nil": { + capabilities: nil, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "empty": { + capabilities: &tfprotov6.PlanResourceChangeClientCapabilities{}, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "deferral_allowed": { + capabilities: &tfprotov6.PlanResourceChangeClientCapabilities{ + DeferralAllowed: true, + }, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": true, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + + ctx := tfsdklogtest.RootLogger(context.Background(), &output) + ctx = logging.ProtoSubsystemContext(ctx, tfsdklog.Options{}) + + tf6serverlogging.PlanResourceChangeClientCapabilities(ctx, testCase.capabilities) + + entries, err := tfsdklogtest.MultilineJSONDecode(&output) + + if err != nil { + t.Fatalf("unable to read multiple line JSON: %s", err) + } + + if diff := cmp.Diff(entries, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestImportResourceStateClientCapabilities(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + capabilities *tfprotov6.ImportResourceStateClientCapabilities + expected []map[string]interface{} + }{ + "nil": { + capabilities: nil, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "empty": { + capabilities: &tfprotov6.ImportResourceStateClientCapabilities{}, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": false, + }, + }, + }, + "deferral_allowed": { + capabilities: &tfprotov6.ImportResourceStateClientCapabilities{ + DeferralAllowed: true, + }, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Announced client capabilities", + "@module": "sdk.proto", + "tf_client_capability_deferral_allowed": true, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + + ctx := tfsdklogtest.RootLogger(context.Background(), &output) + ctx = logging.ProtoSubsystemContext(ctx, tfsdklog.Options{}) + + tf6serverlogging.ImportResourceStateClientCapabilities(ctx, testCase.capabilities) + + entries, err := tfsdklogtest.MultilineJSONDecode(&output) + + if err != nil { + t.Fatalf("unable to read multiple line JSON: %s", err) + } + + if diff := cmp.Diff(entries, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/tfprotov6/internal/tf6serverlogging/deferred.go b/tfprotov6/internal/tf6serverlogging/deferred.go new file mode 100644 index 000000000..5822b6094 --- /dev/null +++ b/tfprotov6/internal/tf6serverlogging/deferred.go @@ -0,0 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf6serverlogging + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/internal/logging" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// Deferred generates a TRACE "Received downstream deferred response" log if populated. +func Deferred(ctx context.Context, deferred *tfprotov6.Deferred) { + if deferred == nil { + return + } + + responseFields := map[string]interface{}{ + logging.KeyDeferredReason: deferred.Reason.String(), + } + + logging.ProtocolTrace(ctx, "Received downstream deferred response", responseFields) +} diff --git a/tfprotov6/internal/tf6serverlogging/deferred_test.go b/tfprotov6/internal/tf6serverlogging/deferred_test.go new file mode 100644 index 000000000..45dc1cd99 --- /dev/null +++ b/tfprotov6/internal/tf6serverlogging/deferred_test.go @@ -0,0 +1,80 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf6serverlogging_test + +import ( + "bytes" + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/internal/logging" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6/internal/tf6serverlogging" + "github.com/hashicorp/terraform-plugin-log/tfsdklog" + "github.com/hashicorp/terraform-plugin-log/tfsdklogtest" +) + +func TestDeferred(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + deferred *tfprotov6.Deferred + expected []map[string]interface{} + }{ + "nil": { + deferred: nil, + expected: nil, + }, + "empty": { + deferred: &tfprotov6.Deferred{}, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Received downstream deferred response", + "@module": "sdk.proto", + "tf_deferred_reason": "UNKNOWN", + }, + }, + }, + "deferred": { + deferred: &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReasonProviderConfigUnknown, + }, + expected: []map[string]interface{}{ + { + "@level": "trace", + "@message": "Received downstream deferred response", + "@module": "sdk.proto", + "tf_deferred_reason": "PROVIDER_CONFIG_UNKNOWN", + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + + ctx := tfsdklogtest.RootLogger(context.Background(), &output) + ctx = logging.ProtoSubsystemContext(ctx, tfsdklog.Options{}) + + tf6serverlogging.Deferred(ctx, testCase.deferred) + + entries, err := tfsdklogtest.MultilineJSONDecode(&output) + + if err != nil { + t.Fatalf("unable to read multiple line JSON: %s", err) + } + + if diff := cmp.Diff(entries, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/tfprotov6/tf6server/server.go b/tfprotov6/tf6server/server.go index 4982c4898..ce8f6f57e 100644 --- a/tfprotov6/tf6server/server.go +++ b/tfprotov6/tf6server/server.go @@ -551,6 +551,7 @@ func (s *server) ConfigureProvider(ctx context.Context, protoReq *tfplugin6.Conf req := fromproto.ConfigureProviderRequest(protoReq) + tf6serverlogging.ConfigureProviderClientCapabilities(ctx, req.ClientCapabilities) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "Config", req.Config) ctx = tf6serverlogging.DownstreamRequest(ctx) @@ -678,6 +679,7 @@ func (s *server) ReadDataSource(ctx context.Context, protoReq *tfplugin6.ReadDat req := fromproto.ReadDataSourceRequest(protoReq) + tf6serverlogging.ReadDataSourceClientCapabilities(ctx, req.ClientCapabilities) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "Config", req.Config) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProviderMeta", req.ProviderMeta) @@ -693,6 +695,7 @@ func (s *server) ReadDataSource(ctx context.Context, protoReq *tfplugin6.ReadDat tf6serverlogging.DownstreamResponse(ctx, resp.Diagnostics) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response", "State", resp.State) + tf6serverlogging.Deferred(ctx, resp.Deferred) protoResp := toproto.ReadDataSource_Response(resp) @@ -767,6 +770,7 @@ func (s *server) ReadResource(ctx context.Context, protoReq *tfplugin6.ReadResou req := fromproto.ReadResourceRequest(protoReq) + tf6serverlogging.ReadResourceClientCapabilities(ctx, req.ClientCapabilities) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "CurrentState", req.CurrentState) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProviderMeta", req.ProviderMeta) logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Request", "Private", req.Private) @@ -783,6 +787,7 @@ func (s *server) ReadResource(ctx context.Context, protoReq *tfplugin6.ReadResou tf6serverlogging.DownstreamResponse(ctx, resp.Diagnostics) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response", "NewState", resp.NewState) logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Response", "Private", resp.Private) + tf6serverlogging.Deferred(ctx, resp.Deferred) protoResp := toproto.ReadResource_Response(resp) @@ -800,6 +805,7 @@ func (s *server) PlanResourceChange(ctx context.Context, protoReq *tfplugin6.Pla req := fromproto.PlanResourceChangeRequest(protoReq) + tf6serverlogging.PlanResourceChangeClientCapabilities(ctx, req.ClientCapabilities) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "Config", req.Config) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "PriorState", req.PriorState) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProposedNewState", req.ProposedNewState) @@ -818,6 +824,7 @@ func (s *server) PlanResourceChange(ctx context.Context, protoReq *tfplugin6.Pla tf6serverlogging.DownstreamResponse(ctx, resp.Diagnostics) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response", "PlannedState", resp.PlannedState) logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Response", "PlannedPrivate", resp.PlannedPrivate) + tf6serverlogging.Deferred(ctx, resp.Deferred) protoResp := toproto.PlanResourceChange_Response(resp) @@ -870,6 +877,8 @@ func (s *server) ImportResourceState(ctx context.Context, protoReq *tfplugin6.Im req := fromproto.ImportResourceStateRequest(protoReq) + tf6serverlogging.ImportResourceStateClientCapabilities(ctx, req.ClientCapabilities) + ctx = tf6serverlogging.DownstreamRequest(ctx) resp, err := s.downstream.ImportResourceState(ctx, req) @@ -885,6 +894,7 @@ func (s *server) ImportResourceState(ctx context.Context, protoReq *tfplugin6.Im logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response_ImportedResource", "State", importedResource.State) logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Response_ImportedResource", "Private", importedResource.Private) } + tf6serverlogging.Deferred(ctx, resp.Deferred) protoResp := toproto.ImportResourceState_Response(resp)