From 597d3e2674f7801bc1f67fbc89535b78d4b034f5 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Wed, 2 Jul 2025 14:29:35 +0100 Subject: [PATCH 1/3] chore: Adds support for header value for tf-src/{resourceOrDataSourceName} in User-Agent --- internal/config/client.go | 7 +- internal/config/transport.go | 25 ++++ internal/provider/provider.go | 2 +- internal/provider/wrapper_provider_server.go | 123 +++++++++++++++++++ 4 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 internal/provider/wrapper_provider_server.go diff --git a/internal/config/client.go b/internal/config/client.go index 260f9b9482..c300431f58 100644 --- a/internal/config/client.go +++ b/internal/config/client.go @@ -103,7 +103,12 @@ func (c *Config) NewClient(ctx context.Context) (any, error) { digestTransport := digest.NewTransportWithHTTPRoundTripper(cast.ToString(c.PublicKey), cast.ToString(c.PrivateKey), networkLoggingTransport) // Don't change logging.NewTransport to NewSubsystemLoggingHTTPTransport until all resources are in TPF. tfLoggingTransport := logging.NewTransport("Atlas", digestTransport) - client := &http.Client{Transport: tfLoggingTransport} + // Add tf-src header to User-Agent, see wrapper_provider_server.go + // Must be before tfLoggingTransport otherwise the "final" userAgent will not be logged + userAgentTransport := TFSrcUserAgentAdder{ + Transport: tfLoggingTransport, + } + client := &http.Client{Transport: &userAgentTransport} optsAtlas := []matlasClient.ClientOpt{matlasClient.SetUserAgent(userAgent(c))} if c.BaseURL != "" { diff --git a/internal/config/transport.go b/internal/config/transport.go index 766b6a6360..6049306266 100644 --- a/internal/config/transport.go +++ b/internal/config/transport.go @@ -1,12 +1,37 @@ package config import ( + "fmt" "log" "net/http" "strings" "time" ) +type ContextKey string + +const ( + ContextKeyTFSrc = ContextKey("tf-src") + UserAgentHeader = "User-Agent" +) + +type TFSrcUserAgentAdder struct { + Transport http.RoundTripper +} + +func (t *TFSrcUserAgentAdder) RoundTrip(req *http.Request) (*http.Response, error) { + ctx := req.Context() + tfSrcName := ctx.Value(ContextKeyTFSrc) + if tfSrcName != nil { + userAgent := req.Header.Get(UserAgentHeader) + tfSrcValue := tfSrcName.(string) + newVar := fmt.Sprintf("%s %s/%s", userAgent, ContextKeyTFSrc, tfSrcValue) + req.Header.Set(UserAgentHeader, newVar) + } + resp, err := t.Transport.RoundTrip(req) + return resp, err +} + // NetworkLoggingTransport wraps an http.RoundTripper to provide enhanced logging // for network operations, including timing, status codes, and error details. type NetworkLoggingTransport struct { diff --git a/internal/provider/provider.go b/internal/provider/provider.go index c05a5cec1e..64aa154d71 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -506,7 +506,7 @@ func MuxProviderFactory() func() tfprotov6.ProviderServer { if err != nil { log.Fatal(err) } - return muxServer.ProviderServer + return NewWrappedProviderServer(muxServer.ProviderServer) } func MultiEnvDefaultFunc(ks []string, def any) any { diff --git a/internal/provider/wrapper_provider_server.go b/internal/provider/wrapper_provider_server.go new file mode 100644 index 0000000000..5d420e9faa --- /dev/null +++ b/internal/provider/wrapper_provider_server.go @@ -0,0 +1,123 @@ +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/config" +) + +func NewWrappedProviderServer(old func() tfprotov6.ProviderServer) func() tfprotov6.ProviderServer { + return func() tfprotov6.ProviderServer { + return &WrappedProviderServer{ + OldServer: old(), + } + } +} + +type WrappedProviderServer struct { + OldServer tfprotov6.ProviderServer +} + +func (s *WrappedProviderServer) GetMetadata(ctx context.Context, req *tfprotov6.GetMetadataRequest) (*tfprotov6.GetMetadataResponse, error) { + return s.OldServer.GetMetadata(ctx, req) +} + +func (s *WrappedProviderServer) GetProviderSchema(ctx context.Context, req *tfprotov6.GetProviderSchemaRequest) (*tfprotov6.GetProviderSchemaResponse, error) { + return s.OldServer.GetProviderSchema(ctx, req) +} + +func (s *WrappedProviderServer) GetResourceIdentitySchemas(ctx context.Context, req *tfprotov6.GetResourceIdentitySchemasRequest) (*tfprotov6.GetResourceIdentitySchemasResponse, error) { + return s.OldServer.GetResourceIdentitySchemas(ctx, req) +} + +func (s *WrappedProviderServer) ValidateProviderConfig(ctx context.Context, req *tfprotov6.ValidateProviderConfigRequest) (*tfprotov6.ValidateProviderConfigResponse, error) { + return s.OldServer.ValidateProviderConfig(ctx, req) +} + +func (s *WrappedProviderServer) ConfigureProvider(ctx context.Context, req *tfprotov6.ConfigureProviderRequest) (*tfprotov6.ConfigureProviderResponse, error) { + return s.OldServer.ConfigureProvider(ctx, req) +} + +func (s *WrappedProviderServer) StopProvider(ctx context.Context, req *tfprotov6.StopProviderRequest) (*tfprotov6.StopProviderResponse, error) { + return s.OldServer.StopProvider(ctx, req) +} + +func (s *WrappedProviderServer) ValidateResourceConfig(ctx context.Context, req *tfprotov6.ValidateResourceConfigRequest) (*tfprotov6.ValidateResourceConfigResponse, error) { + ctx = context.WithValue(ctx, config.ContextKeyTFSrc, req.TypeName) + return s.OldServer.ValidateResourceConfig(ctx, req) +} + +func (s *WrappedProviderServer) UpgradeResourceState(ctx context.Context, req *tfprotov6.UpgradeResourceStateRequest) (*tfprotov6.UpgradeResourceStateResponse, error) { + ctx = context.WithValue(ctx, config.ContextKeyTFSrc, "upgrade."+req.TypeName) + return s.OldServer.UpgradeResourceState(ctx, req) +} + +func (s *WrappedProviderServer) ReadResource(ctx context.Context, req *tfprotov6.ReadResourceRequest) (*tfprotov6.ReadResourceResponse, error) { + ctx = context.WithValue(ctx, config.ContextKeyTFSrc, req.TypeName) + return s.OldServer.ReadResource(ctx, req) +} + +func (s *WrappedProviderServer) PlanResourceChange(ctx context.Context, req *tfprotov6.PlanResourceChangeRequest) (*tfprotov6.PlanResourceChangeResponse, error) { + ctx = context.WithValue(ctx, config.ContextKeyTFSrc, req.TypeName) + return s.OldServer.PlanResourceChange(ctx, req) +} + +func (s *WrappedProviderServer) ApplyResourceChange(ctx context.Context, req *tfprotov6.ApplyResourceChangeRequest) (*tfprotov6.ApplyResourceChangeResponse, error) { + ctx = context.WithValue(ctx, config.ContextKeyTFSrc, req.TypeName) + return s.OldServer.ApplyResourceChange(ctx, req) +} + +func (s *WrappedProviderServer) ImportResourceState(ctx context.Context, req *tfprotov6.ImportResourceStateRequest) (*tfprotov6.ImportResourceStateResponse, error) { + ctx = context.WithValue(ctx, config.ContextKeyTFSrc, "import."+req.TypeName) + return s.OldServer.ImportResourceState(ctx, req) +} + +func (s *WrappedProviderServer) MoveResourceState(ctx context.Context, req *tfprotov6.MoveResourceStateRequest) (*tfprotov6.MoveResourceStateResponse, error) { + ctx = context.WithValue(ctx, config.ContextKeyTFSrc, "move."+req.TargetTypeName) + return s.OldServer.MoveResourceState(ctx, req) +} + +func (s *WrappedProviderServer) UpgradeResourceIdentity(ctx context.Context, req *tfprotov6.UpgradeResourceIdentityRequest) (*tfprotov6.UpgradeResourceIdentityResponse, error) { + ctx = context.WithValue(ctx, config.ContextKeyTFSrc, req.TypeName) + return s.OldServer.UpgradeResourceIdentity(ctx, req) +} + +func (s *WrappedProviderServer) ValidateDataResourceConfig(ctx context.Context, req *tfprotov6.ValidateDataResourceConfigRequest) (*tfprotov6.ValidateDataResourceConfigResponse, error) { + ctx = context.WithValue(ctx, config.ContextKeyTFSrc, "data."+req.TypeName) + return s.OldServer.ValidateDataResourceConfig(ctx, req) +} + +func (s *WrappedProviderServer) ReadDataSource(ctx context.Context, req *tfprotov6.ReadDataSourceRequest) (*tfprotov6.ReadDataSourceResponse, error) { + ctx = context.WithValue(ctx, config.ContextKeyTFSrc, "data."+req.TypeName) + return s.OldServer.ReadDataSource(ctx, req) +} + +func (s *WrappedProviderServer) CallFunction(ctx context.Context, req *tfprotov6.CallFunctionRequest) (*tfprotov6.CallFunctionResponse, error) { + ctx = context.WithValue(ctx, config.ContextKeyTFSrc, "func."+req.Name) + return s.OldServer.CallFunction(ctx, req) +} + +func (s *WrappedProviderServer) GetFunctions(ctx context.Context, req *tfprotov6.GetFunctionsRequest) (*tfprotov6.GetFunctionsResponse, error) { + return s.OldServer.GetFunctions(ctx, req) +} + +func (s *WrappedProviderServer) ValidateEphemeralResourceConfig(ctx context.Context, req *tfprotov6.ValidateEphemeralResourceConfigRequest) (*tfprotov6.ValidateEphemeralResourceConfigResponse, error) { + ctx = context.WithValue(ctx, config.ContextKeyTFSrc, req.TypeName) + return s.OldServer.ValidateEphemeralResourceConfig(ctx, req) +} + +func (s *WrappedProviderServer) OpenEphemeralResource(ctx context.Context, req *tfprotov6.OpenEphemeralResourceRequest) (*tfprotov6.OpenEphemeralResourceResponse, error) { + ctx = context.WithValue(ctx, config.ContextKeyTFSrc, req.TypeName) + return s.OldServer.OpenEphemeralResource(ctx, req) +} + +func (s *WrappedProviderServer) RenewEphemeralResource(ctx context.Context, req *tfprotov6.RenewEphemeralResourceRequest) (*tfprotov6.RenewEphemeralResourceResponse, error) { + ctx = context.WithValue(ctx, config.ContextKeyTFSrc, req.TypeName) + return s.OldServer.RenewEphemeralResource(ctx, req) +} + +func (s *WrappedProviderServer) CloseEphemeralResource(ctx context.Context, req *tfprotov6.CloseEphemeralResourceRequest) (*tfprotov6.CloseEphemeralResourceResponse, error) { + ctx = context.WithValue(ctx, config.ContextKeyTFSrc, req.TypeName) + return s.OldServer.CloseEphemeralResource(ctx, req) +} From 08fb1c91dbd9f115bb14d47eb7b0cdde5d2d04a6 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Thu, 3 Jul 2025 09:24:31 +0100 Subject: [PATCH 2/3] refactor: replace simple context key with structured user agent extra info --- internal/config/transport.go | 97 ++++++++++++++++++-- internal/config/transport_test.go | 89 ++++++++++++++++++ internal/provider/wrapper_provider_server.go | 92 ++++++++++++++++--- 3 files changed, 255 insertions(+), 23 deletions(-) diff --git a/internal/config/transport.go b/internal/config/transport.go index 6049306266..da6351615b 100644 --- a/internal/config/transport.go +++ b/internal/config/transport.go @@ -1,6 +1,7 @@ package config import ( + "context" "fmt" "log" "net/http" @@ -8,24 +9,104 @@ import ( "time" ) -type ContextKey string +// UserAgentExtra holds additional metadata to be appended to the User-Agent header and context. +type UserAgentExtra struct { + Type string // Type of the operation (e.g., "Resource", "Datasource", etc.) + Name string // Full name, for example mongodbatlas_database_user + Operation string // GrpcCall for example, ReadResource, see wrapped_provider_server.go for details + ScriptLocation string // TODO: Support setting this field as opt-in on resources and datasources +} + +// Combine returns a new UserAgentExtra by merging the receiver with another. +// Non-empty fields in 'other' take precedence over the receiver's fields. +func (e UserAgentExtra) Combine(other UserAgentExtra) UserAgentExtra { + typeName := e.Type + if other.Type != "" { + typeName = other.Type + } + name := e.Name + if other.Name != "" { + name = other.Name + } + operation := e.Operation + if other.Operation != "" { + operation = other.Operation + } + scriptLocation := e.ScriptLocation + if other.ScriptLocation != "" { + scriptLocation = other.ScriptLocation + } + return UserAgentExtra{ + Type: typeName, + Name: name, + Operation: operation, + ScriptLocation: scriptLocation, + } +} + +// ToHeaderValue returns a string representation suitable for use as a User-Agent header value. +// If oldHeader is non-empty, it is prepended to the new value. +func (e UserAgentExtra) ToHeaderValue(oldHeader string) string { + parts := []string{} + addPart := func(key, part string) { + if part == "" { + return + } + parts = append(parts, fmt.Sprintf("%s/%s", key, part)) + } + addPart("Type", e.Type) + addPart("Name", e.Name) + addPart("Operation", e.Operation) + addPart("ScriptLocation", e.ScriptLocation) + newPart := strings.Join(parts, " ") + if oldHeader == "" { + return newPart + } + return fmt.Sprintf("%s %s", oldHeader, newPart) +} + +type UserAgentKey string const ( - ContextKeyTFSrc = ContextKey("tf-src") - UserAgentHeader = "User-Agent" + UserAgentExtraKey = UserAgentKey("user-agent-extra") + UserAgentHeader = "User-Agent" ) +// ReadUserAgentExtra retrieves the UserAgentExtra from the context if present. +// Returns a pointer to the UserAgentExtra, or nil if not set or of the wrong type. +// Logs a warning if the value is not of the expected type. +func ReadUserAgentExtra(ctx context.Context) *UserAgentExtra { + extra := ctx.Value(UserAgentExtraKey) + if extra == nil { + return nil + } + if userAgentExtra, ok := extra.(UserAgentExtra); ok { + return &userAgentExtra + } + log.Printf("[WARN] UserAgentExtra in context is not of type UserAgentExtra, got %v", extra) + return nil +} + +// AddUserAgentExtra returns a new context with UserAgentExtra merged into any existing value. +// If a UserAgentExtra is already present in the context, the fields of 'extra' will override non-empty fields. +func AddUserAgentExtra(ctx context.Context, extra UserAgentExtra) context.Context { + oldExtra := ReadUserAgentExtra(ctx) + if oldExtra == nil { + return context.WithValue(ctx, UserAgentExtraKey, extra) + } + newExtra := oldExtra.Combine(extra) + return context.WithValue(ctx, UserAgentExtraKey, newExtra) +} + type TFSrcUserAgentAdder struct { Transport http.RoundTripper } func (t *TFSrcUserAgentAdder) RoundTrip(req *http.Request) (*http.Response, error) { - ctx := req.Context() - tfSrcName := ctx.Value(ContextKeyTFSrc) - if tfSrcName != nil { + extra := ReadUserAgentExtra(req.Context()) + if extra != nil { userAgent := req.Header.Get(UserAgentHeader) - tfSrcValue := tfSrcName.(string) - newVar := fmt.Sprintf("%s %s/%s", userAgent, ContextKeyTFSrc, tfSrcValue) + newVar := extra.ToHeaderValue(userAgent) req.Header.Set(UserAgentHeader, newVar) } resp, err := t.Transport.RoundTrip(req) diff --git a/internal/config/transport_test.go b/internal/config/transport_test.go index ec5a859a78..44b54794f6 100644 --- a/internal/config/transport_test.go +++ b/internal/config/transport_test.go @@ -151,6 +151,95 @@ func TestNetworkLoggingTransport_Disabled(t *testing.T) { assert.Empty(t, logStr, "Expected no logs when network logging is disabled") } +func TestUserAgentExtra_ToHeaderValue(t *testing.T) { + testCases := map[string]struct { + extra config.UserAgentExtra + old string + expected string + }{ + "all fields": { + extra: config.UserAgentExtra{ + Type: "type1", + Name: "name1", + Operation: "op1", + ScriptLocation: "loc1", + }, + old: "base/1.0", + expected: "base/1.0 Type/type1 Name/name1 Operation/op1 ScriptLocation/loc1", + }, + "some fields empty": { + extra: config.UserAgentExtra{ + Type: "", + Name: "name2", + Operation: "", + }, + old: "", + expected: "Name/name2", + }, + "none": { + extra: config.UserAgentExtra{}, + old: "", + expected: "", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got := tc.extra.ToHeaderValue(tc.old) + assert.Equal(t, tc.expected, got) + }) + } +} + +func TestUserAgentExtra_Combine(t *testing.T) { + testCases := map[string]struct { + base config.UserAgentExtra + other config.UserAgentExtra + expected config.UserAgentExtra + }{ + "other overwrites non-empty": { + base: config.UserAgentExtra{Type: "A", Name: "B", Operation: "C", ScriptLocation: "D"}, + other: config.UserAgentExtra{Type: "X", Name: "Y", Operation: "Z", ScriptLocation: "Q"}, + expected: config.UserAgentExtra{Type: "X", Name: "Y", Operation: "Z", ScriptLocation: "Q"}, + }, + "other empty": { + base: config.UserAgentExtra{Type: "A", Name: "B", Operation: "C", ScriptLocation: "D"}, + other: config.UserAgentExtra{}, + expected: config.UserAgentExtra{Type: "A", Name: "B", Operation: "C", ScriptLocation: "D"}, + }, + "mixed": { + base: config.UserAgentExtra{Type: "A", Name: "B"}, + other: config.UserAgentExtra{Name: "Y", ScriptLocation: "Q"}, + expected: config.UserAgentExtra{Type: "A", Name: "Y", Operation: "", ScriptLocation: "Q"}, + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got := tc.base.Combine(tc.other) + assert.Equal(t, tc.expected, got) + }) + } +} + +func TestAddUserAgentExtra(t *testing.T) { + base := config.UserAgentExtra{Type: "A", Name: "B"} + other := config.UserAgentExtra{Name: "Y", ScriptLocation: "Q"} + ctx := config.AddUserAgentExtra(t.Context(), base) + ctx2 := config.AddUserAgentExtra(ctx, other) + // Should combine base and other + e := ctx2.Value(config.UserAgentExtraKey) + assert.NotNil(t, e) + ua := config.UserAgentExtra{} + if v, ok := e.(config.UserAgentExtra); ok { + ua = v + } + // The combined should have Type from base, Name from other, ScriptLocation from other + assert.Equal(t, "A", ua.Type) + assert.Equal(t, "Y", ua.Name) + assert.Equal(t, "Q", ua.ScriptLocation) + assert.Empty(t, ua.Operation) +} + func TestAccNetworkLogging(t *testing.T) { acc.SkipInUnitTest(t) acc.PreCheckBasic(t) diff --git a/internal/provider/wrapper_provider_server.go b/internal/provider/wrapper_provider_server.go index 5d420e9faa..0c72ca478c 100644 --- a/internal/provider/wrapper_provider_server.go +++ b/internal/provider/wrapper_provider_server.go @@ -7,6 +7,8 @@ import ( "github.com/mongodb/terraform-provider-mongodbatlas/internal/config" ) +// NewWrappedProviderServer returns a new ProviderServer that wraps the old one. +// This is used to add additional metadata to the User-Agent header and context. func NewWrappedProviderServer(old func() tfprotov6.ProviderServer) func() tfprotov6.ProviderServer { return func() tfprotov6.ProviderServer { return &WrappedProviderServer{ @@ -44,57 +46,101 @@ func (s *WrappedProviderServer) StopProvider(ctx context.Context, req *tfprotov6 } func (s *WrappedProviderServer) ValidateResourceConfig(ctx context.Context, req *tfprotov6.ValidateResourceConfigRequest) (*tfprotov6.ValidateResourceConfigResponse, error) { - ctx = context.WithValue(ctx, config.ContextKeyTFSrc, req.TypeName) + ctx = config.AddUserAgentExtra(ctx, config.UserAgentExtra{ + Type: "Resource", + Name: req.TypeName, + Operation: "ValidateResourceConfig", + }) return s.OldServer.ValidateResourceConfig(ctx, req) } func (s *WrappedProviderServer) UpgradeResourceState(ctx context.Context, req *tfprotov6.UpgradeResourceStateRequest) (*tfprotov6.UpgradeResourceStateResponse, error) { - ctx = context.WithValue(ctx, config.ContextKeyTFSrc, "upgrade."+req.TypeName) + ctx = config.AddUserAgentExtra(ctx, config.UserAgentExtra{ + Type: "Resource", + Name: req.TypeName, + Operation: "UpgradeResourceState", + }) return s.OldServer.UpgradeResourceState(ctx, req) } func (s *WrappedProviderServer) ReadResource(ctx context.Context, req *tfprotov6.ReadResourceRequest) (*tfprotov6.ReadResourceResponse, error) { - ctx = context.WithValue(ctx, config.ContextKeyTFSrc, req.TypeName) + ctx = config.AddUserAgentExtra(ctx, config.UserAgentExtra{ + Type: "Resource", + Name: req.TypeName, + Operation: "ReadResource", + }) return s.OldServer.ReadResource(ctx, req) } func (s *WrappedProviderServer) PlanResourceChange(ctx context.Context, req *tfprotov6.PlanResourceChangeRequest) (*tfprotov6.PlanResourceChangeResponse, error) { - ctx = context.WithValue(ctx, config.ContextKeyTFSrc, req.TypeName) + ctx = config.AddUserAgentExtra(ctx, config.UserAgentExtra{ + Type: "Resource", + Name: req.TypeName, + Operation: "PlanResourceChange", + }) return s.OldServer.PlanResourceChange(ctx, req) } func (s *WrappedProviderServer) ApplyResourceChange(ctx context.Context, req *tfprotov6.ApplyResourceChangeRequest) (*tfprotov6.ApplyResourceChangeResponse, error) { - ctx = context.WithValue(ctx, config.ContextKeyTFSrc, req.TypeName) + ctx = config.AddUserAgentExtra(ctx, config.UserAgentExtra{ + Type: "Resource", + Name: req.TypeName, + Operation: "ApplyResourceChange", + }) return s.OldServer.ApplyResourceChange(ctx, req) } func (s *WrappedProviderServer) ImportResourceState(ctx context.Context, req *tfprotov6.ImportResourceStateRequest) (*tfprotov6.ImportResourceStateResponse, error) { - ctx = context.WithValue(ctx, config.ContextKeyTFSrc, "import."+req.TypeName) + ctx = config.AddUserAgentExtra(ctx, config.UserAgentExtra{ + Type: "Resource", + Name: req.TypeName, + Operation: "ImportResourceState", + }) return s.OldServer.ImportResourceState(ctx, req) } func (s *WrappedProviderServer) MoveResourceState(ctx context.Context, req *tfprotov6.MoveResourceStateRequest) (*tfprotov6.MoveResourceStateResponse, error) { - ctx = context.WithValue(ctx, config.ContextKeyTFSrc, "move."+req.TargetTypeName) + ctx = config.AddUserAgentExtra(ctx, config.UserAgentExtra{ + Type: "Resource", + Name: req.TargetTypeName, + Operation: "MoveResourceState", + }) return s.OldServer.MoveResourceState(ctx, req) } func (s *WrappedProviderServer) UpgradeResourceIdentity(ctx context.Context, req *tfprotov6.UpgradeResourceIdentityRequest) (*tfprotov6.UpgradeResourceIdentityResponse, error) { - ctx = context.WithValue(ctx, config.ContextKeyTFSrc, req.TypeName) + ctx = config.AddUserAgentExtra(ctx, config.UserAgentExtra{ + Type: "Resource", + Name: req.TypeName, + Operation: "UpgradeResourceIdentity", + }) return s.OldServer.UpgradeResourceIdentity(ctx, req) } func (s *WrappedProviderServer) ValidateDataResourceConfig(ctx context.Context, req *tfprotov6.ValidateDataResourceConfigRequest) (*tfprotov6.ValidateDataResourceConfigResponse, error) { - ctx = context.WithValue(ctx, config.ContextKeyTFSrc, "data."+req.TypeName) + ctx = config.AddUserAgentExtra(ctx, config.UserAgentExtra{ + Type: "Datasource", + Name: req.TypeName, + Operation: "ValidateDataResourceConfig", + }) return s.OldServer.ValidateDataResourceConfig(ctx, req) } func (s *WrappedProviderServer) ReadDataSource(ctx context.Context, req *tfprotov6.ReadDataSourceRequest) (*tfprotov6.ReadDataSourceResponse, error) { - ctx = context.WithValue(ctx, config.ContextKeyTFSrc, "data."+req.TypeName) + ctx = config.AddUserAgentExtra(ctx, config.UserAgentExtra{ + Type: "Datasource", + Name: req.TypeName, + Operation: "ReadDataSource", + }) return s.OldServer.ReadDataSource(ctx, req) } func (s *WrappedProviderServer) CallFunction(ctx context.Context, req *tfprotov6.CallFunctionRequest) (*tfprotov6.CallFunctionResponse, error) { - ctx = context.WithValue(ctx, config.ContextKeyTFSrc, "func."+req.Name) + ctx = config.AddUserAgentExtra(ctx, config.UserAgentExtra{ + Type: "Function", + Name: req.Name, + Operation: "CallFunction", + }) return s.OldServer.CallFunction(ctx, req) } @@ -103,21 +149,37 @@ func (s *WrappedProviderServer) GetFunctions(ctx context.Context, req *tfprotov6 } func (s *WrappedProviderServer) ValidateEphemeralResourceConfig(ctx context.Context, req *tfprotov6.ValidateEphemeralResourceConfigRequest) (*tfprotov6.ValidateEphemeralResourceConfigResponse, error) { - ctx = context.WithValue(ctx, config.ContextKeyTFSrc, req.TypeName) + ctx = config.AddUserAgentExtra(ctx, config.UserAgentExtra{ + Type: "Ephemeral", + Name: req.TypeName, + Operation: "ValidateEphemeralResourceConfig", + }) return s.OldServer.ValidateEphemeralResourceConfig(ctx, req) } func (s *WrappedProviderServer) OpenEphemeralResource(ctx context.Context, req *tfprotov6.OpenEphemeralResourceRequest) (*tfprotov6.OpenEphemeralResourceResponse, error) { - ctx = context.WithValue(ctx, config.ContextKeyTFSrc, req.TypeName) + ctx = config.AddUserAgentExtra(ctx, config.UserAgentExtra{ + Type: "Ephemeral", + Name: req.TypeName, + Operation: "OpenEphemeralResource", + }) return s.OldServer.OpenEphemeralResource(ctx, req) } func (s *WrappedProviderServer) RenewEphemeralResource(ctx context.Context, req *tfprotov6.RenewEphemeralResourceRequest) (*tfprotov6.RenewEphemeralResourceResponse, error) { - ctx = context.WithValue(ctx, config.ContextKeyTFSrc, req.TypeName) + ctx = config.AddUserAgentExtra(ctx, config.UserAgentExtra{ + Type: "Ephemeral", + Name: req.TypeName, + Operation: "RenewEphemeralResource", + }) return s.OldServer.RenewEphemeralResource(ctx, req) } func (s *WrappedProviderServer) CloseEphemeralResource(ctx context.Context, req *tfprotov6.CloseEphemeralResourceRequest) (*tfprotov6.CloseEphemeralResourceResponse, error) { - ctx = context.WithValue(ctx, config.ContextKeyTFSrc, req.TypeName) + ctx = config.AddUserAgentExtra(ctx, config.UserAgentExtra{ + Type: "Ephemeral", + Name: req.TypeName, + Operation: "CloseEphemeralResource", + }) return s.OldServer.CloseEphemeralResource(ctx, req) } From 00c9bf78a19340ad4b5455f69dd07193c2d0414c Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Thu, 31 Jul 2025 06:43:41 +0100 Subject: [PATCH 3/3] refactor: Rename and document the HTTP transport chain --- internal/config/client.go | 14 +++++++++----- internal/config/transport.go | 5 +++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/internal/config/client.go b/internal/config/client.go index c300431f58..509efbbcf7 100644 --- a/internal/config/client.go +++ b/internal/config/client.go @@ -97,15 +97,19 @@ type UAMetadata struct { } func (c *Config) NewClient(ctx context.Context) (any, error) { - // Network Logging transport is before Digest transport so it can log the first Digest requests with 401 Unauthorized. - // Terraform logging transport is after Digest transport so the Unauthorized request bodies are not logged. + // Transport chain (outermost to innermost): + // userAgentTransport -> tfLoggingTransport -> digestTransport -> networkLoggingTransport -> baseTransport + // + // This ordering ensures: + // 1. networkLoggingTransport logs ALL requests including digest auth 401 challenges + // 2. tfLoggingTransport only logs final authenticated requests (not sensitive auth details) + // 3. userAgentTransport modifies User-Agent before tfLoggingTransport logs it networkLoggingTransport := NewTransportWithNetworkLogging(baseTransport, logging.IsDebugOrHigher()) digestTransport := digest.NewTransportWithHTTPRoundTripper(cast.ToString(c.PublicKey), cast.ToString(c.PrivateKey), networkLoggingTransport) // Don't change logging.NewTransport to NewSubsystemLoggingHTTPTransport until all resources are in TPF. tfLoggingTransport := logging.NewTransport("Atlas", digestTransport) - // Add tf-src header to User-Agent, see wrapper_provider_server.go - // Must be before tfLoggingTransport otherwise the "final" userAgent will not be logged - userAgentTransport := TFSrcUserAgentAdder{ + // Add UserAgentExtra fields to the User-Agent header, see wrapper_provider_server.go + userAgentTransport := UserAgentTransport{ Transport: tfLoggingTransport, } client := &http.Client{Transport: &userAgentTransport} diff --git a/internal/config/transport.go b/internal/config/transport.go index da6351615b..72b2cf98c6 100644 --- a/internal/config/transport.go +++ b/internal/config/transport.go @@ -98,11 +98,12 @@ func AddUserAgentExtra(ctx context.Context, extra UserAgentExtra) context.Contex return context.WithValue(ctx, UserAgentExtraKey, newExtra) } -type TFSrcUserAgentAdder struct { +// UserAgentTransport wraps an http.RoundTripper to add User-Agent header with additional metadata. +type UserAgentTransport struct { Transport http.RoundTripper } -func (t *TFSrcUserAgentAdder) RoundTrip(req *http.Request) (*http.Response, error) { +func (t *UserAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) { extra := ReadUserAgentExtra(req.Context()) if extra != nil { userAgent := req.Header.Get(UserAgentHeader)