diff --git a/pkg/cli/cmd/bulkexport/parameters/parameters.go b/pkg/cli/cmd/bulkexport/parameters/parameters.go index 5d7c7d1148..b1c4823c5c 100644 --- a/pkg/cli/cmd/bulkexport/parameters/parameters.go +++ b/pkg/cli/cmd/bulkexport/parameters/parameters.go @@ -68,7 +68,7 @@ type Parameters struct { func (p *Parameters) ControllerConfig() *config.ControllerConfig { c := &config.ControllerConfig{ - UserAgent: gcp.KCCUserAgent, + UserAgent: gcp.KCCUserAgent(), } if p.OAuth2Token != "" { c.GCPTokenSource = oauth2.StaticTokenSource( diff --git a/pkg/cli/cmd/export/parameters/parameters.go b/pkg/cli/cmd/export/parameters/parameters.go index 36502c4688..6f2d34bfc8 100644 --- a/pkg/cli/cmd/export/parameters/parameters.go +++ b/pkg/cli/cmd/export/parameters/parameters.go @@ -42,7 +42,7 @@ type Parameters struct { func (p *Parameters) ControllerConfig() *config.ControllerConfig { c := &config.ControllerConfig{ HTTPClient: p.HTTPClient, - UserAgent: gcp.KCCUserAgent, + UserAgent: gcp.KCCUserAgent(), } if p.GCPAccessToken != "" { c.GCPTokenSource = oauth2.StaticTokenSource( diff --git a/pkg/config/controllerconfig.go b/pkg/config/controllerconfig.go index 221eccf4b7..d7b4f41a3c 100644 --- a/pkg/config/controllerconfig.go +++ b/pkg/config/controllerconfig.go @@ -23,6 +23,7 @@ import ( ) type ControllerConfig struct { + // UserAgent sets the User-Agent to pass in HTTP request headers UserAgent string // UserProjectOverride provides the option to use the resource project for preconditions, quota, and billing, diff --git a/pkg/controller/kccmanager/kccmanager.go b/pkg/controller/kccmanager/kccmanager.go index cf2ca5be56..5a820eac5f 100644 --- a/pkg/controller/kccmanager/kccmanager.go +++ b/pkg/controller/kccmanager/kccmanager.go @@ -133,7 +133,7 @@ func New(ctx context.Context, restConfig *rest.Config, cfg Config) (manager.Mana dclOptions.UserProjectOverride = cfg.UserProjectOverride dclOptions.BillingProject = cfg.BillingProject dclOptions.HTTPClient = cfg.HTTPClient - dclOptions.UserAgent = gcp.KCCUserAgent + dclOptions.UserAgent = gcp.KCCUserAgent() dclConfig, err := clientconfig.New(ctx, dclOptions) if err != nil { @@ -146,7 +146,7 @@ func New(ctx context.Context, restConfig *rest.Config, cfg Config) (manager.Mana BillingProject: cfg.BillingProject, HTTPClient: cfg.HTTPClient, GRPCUnaryClientInterceptor: cfg.GRPCUnaryClientInterceptor, - UserAgent: gcp.KCCUserAgent, + UserAgent: gcp.KCCUserAgent(), } // Initialize direct controllers diff --git a/pkg/dcl/clientconfig/config.go b/pkg/dcl/clientconfig/config.go index ff6e2b76ff..faef6affba 100644 --- a/pkg/dcl/clientconfig/config.go +++ b/pkg/dcl/clientconfig/config.go @@ -42,7 +42,7 @@ type Options struct { func newConfigAndClient(ctx context.Context, opt Options) (*dcl.Config, *http.Client, error) { if opt.UserAgent == "" { - opt.UserAgent = gcp.KCCUserAgent + opt.UserAgent = gcp.KCCUserAgent() } if opt.HTTPClient == nil { @@ -144,7 +144,7 @@ func SetUserAgentWithBlueprintAttribution(dclConfig *dcl.Config, resource metav1 if !found { return dclConfig } - userAgentWithBlueprintAttribution := fmt.Sprintf("%v blueprints/%v", gcp.KCCUserAgent, bp) + userAgentWithBlueprintAttribution := fmt.Sprintf("%v blueprints/%v", gcp.KCCUserAgent(), bp) newConfig := dclConfig.Clone(dcl.WithUserAgent(userAgentWithBlueprintAttribution)) return newConfig } diff --git a/pkg/dcl/clientconfig/config_test.go b/pkg/dcl/clientconfig/config_test.go index 905ec29372..f8a2fa3abe 100644 --- a/pkg/dcl/clientconfig/config_test.go +++ b/pkg/dcl/clientconfig/config_test.go @@ -30,7 +30,7 @@ func TestSetUserAgentWithBlueprintAttribution(t *testing.T) { kind := "Test1Foo" apiVersion := "test1.cnrm.cloud.google.com/v1alpha1" bp := "test-blueprint" - dclConfig := dcl.NewConfig(dcl.WithUserAgent(gcp.KCCUserAgent)) + dclConfig := dcl.NewConfig(dcl.WithUserAgent(gcp.KCCUserAgent())) tests := []struct { name string obj metav1.Object diff --git a/pkg/gcp/clients.go b/pkg/gcp/clients.go index bd3b831d8e..ce3ddb8f97 100644 --- a/pkg/gcp/clients.go +++ b/pkg/gcp/clients.go @@ -16,7 +16,9 @@ package gcp import ( "context" + "fmt" + "github.com/GoogleCloudPlatform/k8s-config-connector/version" "golang.org/x/oauth2/google" "google.golang.org/api/cloudresourcemanager/v1" "google.golang.org/api/iam/v1" @@ -24,7 +26,12 @@ import ( ) // The user agent to track KCC's attribution to GCP usages -const KCCUserAgent = "kcc/controller-manager" +func KCCUserAgent() string { + kccVersion := version.GetVersion() + // Note: try to keep in sync with third_party/github.com/hashicorp/terraform-provider-google-beta/google-beta/fwtransport/framework_utils.go + userAgent := fmt.Sprintf("kcc/%s (+https://github.com/GoogleCloudPlatform/k8s-config-connector) kcc/controller-manager/%s", kccVersion, kccVersion) + return userAgent +} func NewIAMClient(ctx context.Context) (*iam.Service, error) { httpClient, err := google.DefaultClient(ctx, iam.CloudPlatformScope) @@ -35,7 +42,7 @@ func NewIAMClient(ctx context.Context) (*iam.Service, error) { if err != nil { return nil, err } - client.UserAgent = KCCUserAgent + client.UserAgent = KCCUserAgent() return client, nil } @@ -48,7 +55,7 @@ func NewStorageClient(ctx context.Context) (*storage.Service, error) { if err != nil { return nil, err } - client.UserAgent = KCCUserAgent + client.UserAgent = KCCUserAgent() return client, nil } @@ -62,6 +69,6 @@ func NewCloudResourceManagerClient(ctx context.Context) (*cloudresourcemanager.S if err != nil { return nil, err } - client.UserAgent = KCCUserAgent + client.UserAgent = KCCUserAgent() return client, nil } diff --git a/pkg/krmtotf/user_agent.go b/pkg/krmtotf/user_agent.go index c0a92ef898..f0e1660af8 100644 --- a/pkg/krmtotf/user_agent.go +++ b/pkg/krmtotf/user_agent.go @@ -32,7 +32,7 @@ import ( // Note that SetBlueprintAttribution will be used to add the blueprint attribution part into the user agent per resource // if the resource has the 'cnrm.cloud.google.com/blueprint' annotation. func SetUserAgentForTerraformProvider() { - tfversion.ProviderVersion = gcp.KCCUserAgent + tfversion.ProviderVersion = gcp.KCCUserAgent() } // SetBlueprintAttribution sets the module name to the blueprint name on the given instance state if the resource has the 'cnrm.cloud.google.com/blueprint' annotation. diff --git a/pkg/test/controller/reconciler/testreconciler.go b/pkg/test/controller/reconciler/testreconciler.go index 3235400198..fa0989f729 100644 --- a/pkg/test/controller/reconciler/testreconciler.go +++ b/pkg/test/controller/reconciler/testreconciler.go @@ -38,6 +38,7 @@ import ( "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/conversion" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/metadata" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/schema/dclschemaloader" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/gcp" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/gvks/supportedgvks" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/kccfeatureflags" @@ -114,6 +115,7 @@ func NewTestReconciler(t *testing.T, mgr manager.Manager, provider *tfschema.Pro // Initialize direct controllers if err := registry.Init(context.TODO(), &config.ControllerConfig{ HTTPClient: httpClient, + UserAgent: gcp.KCCUserAgent(), }); err != nil { t.Fatalf("error initializing direct registry: %v", err) } diff --git a/pkg/test/http_recorder.go b/pkg/test/http_recorder.go index 7d40dfb590..9143299f3b 100644 --- a/pkg/test/http_recorder.go +++ b/pkg/test/http_recorder.go @@ -36,17 +36,25 @@ type LogEntry struct { } type Request struct { - Method string `json:"method,omitempty"` - URL string `json:"url,omitempty"` + Method string `json:"method,omitempty"` + URL string `json:"url,omitempty"` + + // The HTTP Headers for the request. + // These should be stored with canonicalized keys (using http.CanonicalHeaderKey(k)) Header http.Header `json:"header,omitempty"` - Body string `json:"body,omitempty"` + + Body string `json:"body,omitempty"` } type Response struct { - Status string `json:"status,omitempty"` - StatusCode int `json:"statusCode,omitempty"` - Header http.Header `json:"header,omitempty"` - Body string `json:"body,omitempty"` + Status string `json:"status,omitempty"` + StatusCode int `json:"statusCode,omitempty"` + + // The HTTP Headers for the response. + // These should be stored with canonicalized keys (using http.CanonicalHeaderKey(k)) + Header http.Header `json:"header,omitempty"` + + Body string `json:"body,omitempty"` } type HTTPRecorder struct { @@ -68,6 +76,7 @@ func (r *HTTPRecorder) RoundTrip(req *http.Request) (*http.Response, error) { entry.Request.Header = make(http.Header) for k, values := range req.Header { + k = http.CanonicalHeaderKey(k) switch strings.ToLower(k) { case "authorization": entry.Request.Header[k] = []string{"(removed)"} @@ -105,6 +114,7 @@ func (r *HTTPRecorder) record(entry *LogEntry, req *http.Request, resp *http.Res entry.Response.Header = make(http.Header) for k, values := range resp.Header { + k = http.CanonicalHeaderKey(k) switch strings.ToLower(k) { case "authorization": entry.Response.Header[k] = []string{"(removed)"} @@ -239,19 +249,13 @@ func prettifyJSON(s string, mutators ...JSONMutator) string { } func (r *Request) ReplaceHeader(key, value string) { - if http.CanonicalHeaderKey(key) == key { - r.Header.Set(key, value) - } else { - r.Header[key] = []string{value} - } + key = http.CanonicalHeaderKey(key) + r.Header.Set(key, value) } func (r *Response) ReplaceHeader(key, value string) { - if http.CanonicalHeaderKey(key) == key { - r.Header.Set(key, value) - } else { - r.Header[key] = []string{value} - } + key = http.CanonicalHeaderKey(key) + r.Header.Set(key, value) } func (r *Request) AddHeader(key, value string) { @@ -263,21 +267,13 @@ func (r *Response) AddHeader(key, value string) { } func (r *Response) RemoveHeader(key string) { - // The http.header `Del` converts the `key` to `CanonicalHeaderKey`, which means - // it expects the passed-in parameter `key` to be case-insensitive, but `Header` itself should - // use canonical keys. + key = http.CanonicalHeaderKey(key) r.Header.Del(key) - // Delete non canonical header keys like `x-goog-api-client`. - delete(r.Header, strings.ToLower(key)) } func (r *Request) RemoveHeader(key string) { - // The http.header `Del` converts the `key` to `CanonicalHeaderKey`, which means - // it expects the passed-in parameter `key` to be case-insensitive, but `Header` itself should - // use canonical keys. + key = http.CanonicalHeaderKey(key) r.Header.Del(key) - // Delete non canonical header keys like `x-goog-api-client`. - delete(r.Header, strings.ToLower(key)) } func (r *Response) ParseBody() map[string]any { diff --git a/tests/e2e/httplog.go b/tests/e2e/httplog.go index 223a4cf9c7..229380a53e 100644 --- a/tests/e2e/httplog.go +++ b/tests/e2e/httplog.go @@ -21,6 +21,7 @@ import ( "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test" testgcp "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test/gcp" + "github.com/GoogleCloudPlatform/k8s-config-connector/version" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/klog/v2" ) @@ -89,6 +90,22 @@ func RemoveExtraEvents(events test.LogEntries) test.LogEntries { return events } +// RewriteUserAgent removes volatile values from the user agent: +// it replaces the version with ${kccVersion}. +func RewriteUserAgent(events test.LogEntries) test.LogEntries { + // Remove operation polling requests (ones where the operation is not ready) + for _, event := range events { + userAgent := event.Request.Header.Get("User-Agent") + if userAgent != "" { + currentVersion := version.GetVersion() + userAgent = strings.ReplaceAll(userAgent, currentVersion, "${kccVersion}") + event.Request.Header.Set("User-Agent", userAgent) + } + } + + return events +} + func (x *Normalizer) Render(events test.LogEntries) string { // Replace any dynamic IDs that appear in URLs @@ -224,6 +241,7 @@ func (x *Normalizer) Render(events test.LogEntries) string { } func (x *Normalizer) Preprocess(events []*test.LogEntry) { + events = RewriteUserAgent(events) // Find "easy" operations and resources by looking for fully-qualified methods for _, event := range events {