From 4d55f7dec5d6ac9629447f76923e1d3e8ac42ade Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 12 Aug 2022 14:51:55 -0400 Subject: [PATCH 1/2] tfprotov5/tf5server+tfprotov6/tf6server: Include private state data in protocol data output Reference: https://github.com/hashicorp/terraform-plugin-go/issues/220 This data is "private" in the sense that it is provider-owned, rather than something managed by Terraform. --- .changelog/pending.txt | 15 +++++++++ internal/logging/protocol.go | 6 ++++ internal/logging/protocol_data.go | 54 +++++++++++++++++++++++++------ tfprotov5/tf5server/server.go | 11 +++++-- tfprotov6/tf6server/server.go | 11 +++++-- 5 files changed, 84 insertions(+), 13 deletions(-) create mode 100644 .changelog/pending.txt diff --git a/.changelog/pending.txt b/.changelog/pending.txt new file mode 100644 index 000000000..f551f2b77 --- /dev/null +++ b/.changelog/pending.txt @@ -0,0 +1,15 @@ +```release-note:enhancement +tfprotov5/tf5server: Added resource private state when protocol data output is enabled +``` + +```release-note:enhancement +tfprotov6/tf6server: Added resource private state when protocol data output is enabled +``` + +```release-note:bug +tfprotov5/tf5server: Fixed `ApplyResourceChange` request RPC protocol data output to include `PriorState` and `ProviderMeta` fields +``` + +```release-note:bug +tfprotov6/tf6server: Fixed `ApplyResourceChange` request RPC protocol data output to include `PriorState` and `ProviderMeta` fields +``` diff --git a/internal/logging/protocol.go b/internal/logging/protocol.go index 21a392a3c..ee1247690 100644 --- a/internal/logging/protocol.go +++ b/internal/logging/protocol.go @@ -25,3 +25,9 @@ func ProtocolWarn(ctx context.Context, msg string, additionalFields ...map[strin func ProtocolTrace(ctx context.Context, msg string, additionalFields ...map[string]interface{}) { tfsdklog.SubsystemTrace(ctx, SubsystemProto, msg, additionalFields...) } + +// ProtocolSetField returns a context with the additional protocol subsystem +// field set. +func ProtocolSetField(ctx context.Context, key string, value any) context.Context { + return tfsdklog.SubsystemSetField(ctx, SubsystemProto, key, value) +} diff --git a/internal/logging/protocol_data.go b/internal/logging/protocol_data.go index 630358232..0e220f57c 100644 --- a/internal/logging/protocol_data.go +++ b/internal/logging/protocol_data.go @@ -55,20 +55,34 @@ func ProtocolData(ctx context.Context, dataDir string, rpc string, message strin return } - fileName := fmt.Sprintf("%d_%s_%s_%s.%s", time.Now().Unix(), rpc, message, field, fileExtension) - filePath := path.Join(dataDir, fileName) - logFields := map[string]interface{}{KeyProtocolDataFile: filePath} // should not be persisted using With() - - ProtocolTrace(ctx, "Writing protocol data file", logFields) + writeProtocolFile(ctx, dataDir, rpc, message, field, fileExtension, fileContents) +} - err := os.WriteFile(filePath, fileContents, 0644) +// ProtocolPrivateData emits raw protocol private data to a file, if given a +// directory. This data is "private" in the sense that it is provider-owned, +// rather than something managed by Terraform. +// +// The directory must exist and be writable, prior to invoking this function. +// +// File names are in the format: {TIME}_{RPC}_{MESSAGE}_{FIELD}(.empty) +func ProtocolPrivateData(ctx context.Context, dataDir string, rpc string, message string, field string, data []byte) { + if dataDir == "" { + // Write a log, only once, that explains how to enable this functionality. + protocolDataSkippedLog.Do(func() { + ProtocolTrace(ctx, "Skipping protocol data file writing because no data directory is set. "+ + fmt.Sprintf("Use the %s environment variable to enable this functionality.", EnvTfLogSdkProtoDataDir)) + }) - if err != nil { - ProtocolError(ctx, fmt.Sprintf("Unable to write protocol data file: %s", err), logFields) return } - ProtocolTrace(ctx, "Wrote protocol data file", logFields) + var fileExtension string + + if len(data) == 0 { + fileExtension = fileExtEmpty + } + + writeProtocolFile(ctx, dataDir, rpc, message, field, fileExtension, data) } func protocolDataDynamicValue5(_ context.Context, value *tfprotov5.DynamicValue) (string, []byte) { @@ -106,3 +120,25 @@ func protocolDataDynamicValue6(_ context.Context, value *tfprotov6.DynamicValue) return fileExtEmpty, nil } + +func writeProtocolFile(ctx context.Context, dataDir string, rpc string, message string, field string, fileExtension string, fileContents []byte) { + fileName := fmt.Sprintf("%d_%s_%s_%s", time.Now().Unix(), rpc, message, field) + + if fileExtension != "" { + fileName += "." + fileExtension + } + + filePath := path.Join(dataDir, fileName) + ctx = ProtocolSetField(ctx, KeyProtocolDataFile, filePath) + + ProtocolTrace(ctx, "Writing protocol data file") + + err := os.WriteFile(filePath, fileContents, 0644) + + if err != nil { + ProtocolError(ctx, "Unable to write protocol data file", map[string]any{KeyError: err.Error()}) + return + } + + ProtocolTrace(ctx, "Wrote protocol data file") +} diff --git a/tfprotov5/tf5server/server.go b/tfprotov5/tf5server/server.go index c45cfd5fa..952b789f5 100644 --- a/tfprotov5/tf5server/server.go +++ b/tfprotov5/tf5server/server.go @@ -743,6 +743,7 @@ func (s *server) ReadResource(ctx context.Context, req *tfplugin5.ReadResource_R } logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "CurrentState", r.CurrentState) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProviderMeta", r.ProviderMeta) + logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Request", "Private", r.Private) ctx = tf5serverlogging.DownstreamRequest(ctx) resp, err := s.downstream.ReadResource(ctx, r) if err != nil { @@ -751,6 +752,7 @@ func (s *server) ReadResource(ctx context.Context, req *tfplugin5.ReadResource_R } tf5serverlogging.DownstreamResponse(ctx, resp.Diagnostics) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response", "NewState", resp.NewState) + logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Response", "Private", resp.Private) ret, err := toproto.ReadResource_Response(resp) if err != nil { logging.ProtocolError(ctx, "Error converting response to protobuf", map[string]interface{}{logging.KeyError: err}) @@ -776,6 +778,7 @@ func (s *server) PlanResourceChange(ctx context.Context, req *tfplugin5.PlanReso logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "PriorState", r.PriorState) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProposedNewState", r.ProposedNewState) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProviderMeta", r.ProviderMeta) + logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Request", "PriorPrivate", r.PriorPrivate) ctx = tf5serverlogging.DownstreamRequest(ctx) resp, err := s.downstream.PlanResourceChange(ctx, r) if err != nil { @@ -784,6 +787,7 @@ func (s *server) PlanResourceChange(ctx context.Context, req *tfplugin5.PlanReso } 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) ret, err := toproto.PlanResourceChange_Response(resp) if err != nil { logging.ProtocolError(ctx, "Error converting response to protobuf", map[string]interface{}{logging.KeyError: err}) @@ -807,8 +811,9 @@ func (s *server) ApplyResourceChange(ctx context.Context, req *tfplugin5.ApplyRe } logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "Config", r.Config) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "PlannedState", r.PlannedState) - logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "Config", r.Config) - logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "Config", r.Config) + logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "PriorState", r.PriorState) + logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProviderMeta", r.ProviderMeta) + logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Request", "PlannedPrivate", r.PlannedPrivate) ctx = tf5serverlogging.DownstreamRequest(ctx) resp, err := s.downstream.ApplyResourceChange(ctx, r) if err != nil { @@ -817,6 +822,7 @@ func (s *server) ApplyResourceChange(ctx context.Context, req *tfplugin5.ApplyRe } tf5serverlogging.DownstreamResponse(ctx, resp.Diagnostics) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response", "NewState", resp.NewState) + logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Response", "Private", resp.Private) ret, err := toproto.ApplyResourceChange_Response(resp) if err != nil { logging.ProtocolError(ctx, "Error converting response to protobuf", map[string]interface{}{logging.KeyError: err}) @@ -847,6 +853,7 @@ func (s *server) ImportResourceState(ctx context.Context, req *tfplugin5.ImportR tf5serverlogging.DownstreamResponse(ctx, resp.Diagnostics) for _, importedResource := range resp.ImportedResources { logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response_ImportedResource", "State", importedResource.State) + logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Response_ImportedResource", "Private", importedResource.Private) } ret, err := toproto.ImportResourceState_Response(resp) if err != nil { diff --git a/tfprotov6/tf6server/server.go b/tfprotov6/tf6server/server.go index bdfe3d0f7..9889a93e6 100644 --- a/tfprotov6/tf6server/server.go +++ b/tfprotov6/tf6server/server.go @@ -741,6 +741,7 @@ func (s *server) ReadResource(ctx context.Context, req *tfplugin6.ReadResource_R } logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "CurrentState", r.CurrentState) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProviderMeta", r.ProviderMeta) + logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Request", "Private", r.Private) ctx = tf6serverlogging.DownstreamRequest(ctx) resp, err := s.downstream.ReadResource(ctx, r) if err != nil { @@ -749,6 +750,7 @@ func (s *server) ReadResource(ctx context.Context, req *tfplugin6.ReadResource_R } 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) ret, err := toproto.ReadResource_Response(resp) if err != nil { logging.ProtocolError(ctx, "Error converting response to protobuf", map[string]interface{}{logging.KeyError: err}) @@ -774,6 +776,7 @@ func (s *server) PlanResourceChange(ctx context.Context, req *tfplugin6.PlanReso logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "PriorState", r.PriorState) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProposedNewState", r.ProposedNewState) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProviderMeta", r.ProviderMeta) + logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Request", "PriorPrivate", r.PriorPrivate) ctx = tf6serverlogging.DownstreamRequest(ctx) resp, err := s.downstream.PlanResourceChange(ctx, r) if err != nil { @@ -782,6 +785,7 @@ func (s *server) PlanResourceChange(ctx context.Context, req *tfplugin6.PlanReso } 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) ret, err := toproto.PlanResourceChange_Response(resp) if err != nil { logging.ProtocolError(ctx, "Error converting response to protobuf", map[string]interface{}{logging.KeyError: err}) @@ -805,8 +809,9 @@ func (s *server) ApplyResourceChange(ctx context.Context, req *tfplugin6.ApplyRe } logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "Config", r.Config) logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "PlannedState", r.PlannedState) - logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "Config", r.Config) - logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "Config", r.Config) + logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "PriorState", r.PriorState) + logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProviderMeta", r.ProviderMeta) + logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Request", "PlannedPrivate", r.PlannedPrivate) ctx = tf6serverlogging.DownstreamRequest(ctx) resp, err := s.downstream.ApplyResourceChange(ctx, r) if err != nil { @@ -815,6 +820,7 @@ func (s *server) ApplyResourceChange(ctx context.Context, req *tfplugin6.ApplyRe } 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) ret, err := toproto.ApplyResourceChange_Response(resp) if err != nil { logging.ProtocolError(ctx, "Error converting response to protobuf", map[string]interface{}{logging.KeyError: err}) @@ -845,6 +851,7 @@ func (s *server) ImportResourceState(ctx context.Context, req *tfplugin6.ImportR tf6serverlogging.DownstreamResponse(ctx, resp.Diagnostics) for _, importedResource := range resp.ImportedResources { logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response_ImportedResource", "State", importedResource.State) + logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Response_ImportedResource", "Private", importedResource.Private) } ret, err := toproto.ImportResourceState_Response(resp) if err != nil { From 63f0cbccbe3ad5b1b4f41a3a9a18c4f509fe1d27 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 12 Aug 2022 14:59:37 -0400 Subject: [PATCH 2/2] Update CHANGELOG for #221 --- .changelog/{pending.txt => 221.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{pending.txt => 221.txt} (100%) diff --git a/.changelog/pending.txt b/.changelog/221.txt similarity index 100% rename from .changelog/pending.txt rename to .changelog/221.txt