From 54723e0e413b24c9cfcf2edc537e08d71fe63d04 Mon Sep 17 00:00:00 2001 From: vaish123-fullstck Date: Thu, 26 Mar 2026 15:16:36 +0530 Subject: [PATCH] feat(getter): add helm-session HTTP header for traceability Signed-off-by: vaish123-fullstck --- go.mod | 3 ++- pkg/getter/httpgetter.go | 26 +++++++++++++++----------- pkg/getter/httpgetter_test.go | 25 +++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 4b8bc5636..91b57cf42 100644 --- a/go.mod +++ b/go.mod @@ -53,6 +53,8 @@ require ( sigs.k8s.io/yaml v1.6.0 ) +require github.com/google/uuid v1.6.0 + require ( dario.cat/mergo v1.0.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect @@ -88,7 +90,6 @@ require ( github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect diff --git a/pkg/getter/httpgetter.go b/pkg/getter/httpgetter.go index 2bc12bdbf..1477e0a5d 100644 --- a/pkg/getter/httpgetter.go +++ b/pkg/getter/httpgetter.go @@ -24,20 +24,25 @@ import ( "net/url" "sync" + "github.com/google/uuid" + "helm.sh/helm/v4/internal/tlsutil" "helm.sh/helm/v4/internal/version" ) +// 🔥 Constant for session header +const helmSessionHeader = "helm-session" + // HTTPGetter is the default HTTP(/S) backend handler type HTTPGetter struct { opts getterOptions transport *http.Transport once sync.Once + sessionID string // 🔥 Stores session ID for request grouping } // Get performs a Get from repo.Getter and returns the body. func (g *HTTPGetter) Get(href string, options ...Option) (*bytes.Buffer, error) { - // Create a local copy of options to avoid data races when Get is called concurrently opts := g.opts for _, opt := range options { opt(&opts) @@ -46,13 +51,16 @@ func (g *HTTPGetter) Get(href string, options ...Option) (*bytes.Buffer, error) } func (g *HTTPGetter) get(href string, opts getterOptions) (*bytes.Buffer, error) { - // Set a helm specific user agent so that a repo server and metrics can - // separate helm calls from other tools interacting with repos. req, err := http.NewRequest(http.MethodGet, href, nil) if err != nil { return nil, err } + // 🔥 Add Helm session header to group requests from a single command execution + if g.sessionID != "" { + req.Header.Set(helmSessionHeader, g.sessionID) + } + if opts.acceptHeader != "" { req.Header.Set("Accept", opts.acceptHeader) } @@ -62,8 +70,6 @@ func (g *HTTPGetter) get(href string, opts getterOptions) (*bytes.Buffer, error) req.Header.Set("User-Agent", opts.userAgent) } - // Before setting the basic auth credentials, make sure the URL associated - // with the basic auth is the one being fetched. u1, err := url.Parse(opts.url) if err != nil { return nil, fmt.Errorf("unable to parse getter URL: %w", err) @@ -73,9 +79,6 @@ func (g *HTTPGetter) get(href string, opts getterOptions) (*bytes.Buffer, error) return nil, fmt.Errorf("unable to parse URL getting from: %w", err) } - // Host on URL (returned from url.Parse) contains the port if present. - // This check ensures credentials are not passed between different - // services on different ports. if opts.passCredentialsAll || (u1.Scheme == u2.Scheme && u1.Host == u2.Host) { if opts.username != "" && opts.password != "" { req.SetBasicAuth(opts.username, opts.password) @@ -92,6 +95,7 @@ func (g *HTTPGetter) get(href string, opts getterOptions) (*bytes.Buffer, error) return nil, err } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("failed to fetch %s : %s", href, resp.Status) } @@ -109,6 +113,9 @@ func NewHTTPGetter(options ...Option) (Getter, error) { opt(&client.opts) } + // 🔥 Generate session ID once per getter instance + client.sessionID = uuid.New().String() + return &client, nil } @@ -120,11 +127,9 @@ func (g *HTTPGetter) httpClient(opts getterOptions) (*http.Client, error) { }, nil } - // Check if we need custom TLS configuration needsCustomTLS := (opts.certFile != "" && opts.keyFile != "") || opts.caFile != "" || opts.insecureSkipVerifyTLS if needsCustomTLS { - // Create a new transport for custom TLS to avoid race conditions transport := &http.Transport{ DisableCompression: true, Proxy: http.ProxyFromEnvironment, @@ -147,7 +152,6 @@ func (g *HTTPGetter) httpClient(opts getterOptions) (*http.Client, error) { }, nil } - // Use shared transport for default case (no custom TLS) g.once.Do(func() { g.transport = &http.Transport{ DisableCompression: true, diff --git a/pkg/getter/httpgetter_test.go b/pkg/getter/httpgetter_test.go index 7d4581233..c42a67d2b 100644 --- a/pkg/getter/httpgetter_test.go +++ b/pkg/getter/httpgetter_test.go @@ -678,3 +678,28 @@ func TestHTTPTransportOption(t *testing.T) { t.Fatal("transport.TLSClientConfig should not be set") } } + +// 🔥 NEW TEST: Verify helm-session header is added to requests +func TestHTTPGetterSessionHeader(t *testing.T) { + var capturedHeader string + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedHeader = r.Header.Get(helmSessionHeader) + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + g, err := NewHTTPGetter(WithURL(srv.URL)) + if err != nil { + t.Fatal(err) + } + + _, err = g.Get(srv.URL) + if err != nil { + t.Fatal(err) + } + + if capturedHeader == "" { + t.Fatalf("expected %s header to be set, but it was empty", helmSessionHeader) + } +}