diff --git a/.changelog/221.txt b/.changelog/221.txt new file mode 100644 index 00000000..f551f2b7 --- /dev/null +++ b/.changelog/221.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 21a392a3..ee124769 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 63035823..0e220f57 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 c45cfd5f..952b789f 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 bdfe3d0f..9889a93e 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 {