Skip to content

Commit

Permalink
Use a consistent user agent
Browse files Browse the repository at this point in the history
We build the user agent in multiple places, ensure that we build it consistently.
  • Loading branch information
justinsb committed Jan 16, 2025
1 parent 08b5ce7 commit 3584adc
Show file tree
Hide file tree
Showing 11 changed files with 63 additions and 39 deletions.
2 changes: 1 addition & 1 deletion pkg/cli/cmd/bulkexport/parameters/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/cmd/export/parameters/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions pkg/config/controllerconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions pkg/controller/kccmanager/kccmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pkg/dcl/clientconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion pkg/dcl/clientconfig/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 11 additions & 4 deletions pkg/gcp/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,22 @@ 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"
"google.golang.org/api/storage/v1"
)

// 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)
Expand All @@ -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
}

Expand All @@ -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
}

Expand All @@ -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
}
2 changes: 1 addition & 1 deletion pkg/krmtotf/user_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions pkg/test/controller/reconciler/testreconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down
50 changes: 23 additions & 27 deletions pkg/test/http_recorder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)"}
Expand Down Expand Up @@ -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)"}
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions tests/e2e/httplog.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 3584adc

Please sign in to comment.