feat(getter): add helm-session HTTP header for traceability

Signed-off-by: vaish123-fullstck <vaishnavsreekumar301@gmail.com>
pull/31967/head
vaish123-fullstck 7 months ago
parent b3927b3900
commit 54723e0e41

@ -53,6 +53,8 @@ require (
sigs.k8s.io/yaml v1.6.0 sigs.k8s.io/yaml v1.6.0
) )
require github.com/google/uuid v1.6.0
require ( require (
dario.cat/mergo v1.0.1 // indirect dario.cat/mergo v1.0.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // 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/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.7.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp 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/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect

@ -24,20 +24,25 @@ import (
"net/url" "net/url"
"sync" "sync"
"github.com/google/uuid"
"helm.sh/helm/v4/internal/tlsutil" "helm.sh/helm/v4/internal/tlsutil"
"helm.sh/helm/v4/internal/version" "helm.sh/helm/v4/internal/version"
) )
// 🔥 Constant for session header
const helmSessionHeader = "helm-session"
// HTTPGetter is the default HTTP(/S) backend handler // HTTPGetter is the default HTTP(/S) backend handler
type HTTPGetter struct { type HTTPGetter struct {
opts getterOptions opts getterOptions
transport *http.Transport transport *http.Transport
once sync.Once once sync.Once
sessionID string // 🔥 Stores session ID for request grouping
} }
// Get performs a Get from repo.Getter and returns the body. // Get performs a Get from repo.Getter and returns the body.
func (g *HTTPGetter) Get(href string, options ...Option) (*bytes.Buffer, error) { 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 opts := g.opts
for _, opt := range options { for _, opt := range options {
opt(&opts) 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) { 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) req, err := http.NewRequest(http.MethodGet, href, nil)
if err != nil { if err != nil {
return nil, err 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 != "" { if opts.acceptHeader != "" {
req.Header.Set("Accept", 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) 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) u1, err := url.Parse(opts.url)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to parse getter URL: %w", err) 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) 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.passCredentialsAll || (u1.Scheme == u2.Scheme && u1.Host == u2.Host) {
if opts.username != "" && opts.password != "" { if opts.username != "" && opts.password != "" {
req.SetBasicAuth(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 return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch %s : %s", href, resp.Status) 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) opt(&client.opts)
} }
// 🔥 Generate session ID once per getter instance
client.sessionID = uuid.New().String()
return &client, nil return &client, nil
} }
@ -120,11 +127,9 @@ func (g *HTTPGetter) httpClient(opts getterOptions) (*http.Client, error) {
}, nil }, nil
} }
// Check if we need custom TLS configuration
needsCustomTLS := (opts.certFile != "" && opts.keyFile != "") || opts.caFile != "" || opts.insecureSkipVerifyTLS needsCustomTLS := (opts.certFile != "" && opts.keyFile != "") || opts.caFile != "" || opts.insecureSkipVerifyTLS
if needsCustomTLS { if needsCustomTLS {
// Create a new transport for custom TLS to avoid race conditions
transport := &http.Transport{ transport := &http.Transport{
DisableCompression: true, DisableCompression: true,
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
@ -147,7 +152,6 @@ func (g *HTTPGetter) httpClient(opts getterOptions) (*http.Client, error) {
}, nil }, nil
} }
// Use shared transport for default case (no custom TLS)
g.once.Do(func() { g.once.Do(func() {
g.transport = &http.Transport{ g.transport = &http.Transport{
DisableCompression: true, DisableCompression: true,

@ -678,3 +678,28 @@ func TestHTTPTransportOption(t *testing.T) {
t.Fatal("transport.TLSClientConfig should not be set") 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)
}
}

Loading…
Cancel
Save