diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index 529fd788e..48d2aafa5 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -20,6 +20,9 @@ import ( "fmt" "io" "io/fs" + "log/slog" + "net/http" + "net/http/httputil" "net/url" "os" "path/filepath" @@ -68,12 +71,51 @@ type ChartDownloader struct { // Getter collection for the operation Getters getter.Providers // Options provide parameters to be passed along to the Getter being initialized. - Options []getter.Option + Options []getter.Option + // Debug bool //Added to capture the --debug flag RegistryClient *registry.Client RepositoryConfig string RepositoryCache string } +type debugTransport struct { + *http.Transport + out io.Writer +} + +func (t *debugTransport) RoundTrip(req *http.Request) (*http.Response, error) { + debug := os.Getenv("HELM_DEBUG") == "true" + + if debug { + slog.Debug("HTTP request", "url", req.URL.String()) + // Log the request + reqDump, err := httputil.DumpRequestOut(req, false) + if err == nil { + slog.Debug("HTTP request dump", "dump", string(reqDump)) + } + } + // Perform the request + resp, err := t.Transport.RoundTrip(req) + if err != nil { + if debug { + slog.Debug("HTTP request failed", "error", err) + } + return nil, err + } + // Log the response + if debug { + respDump, err := httputil.DumpResponse(resp, false) + if err == nil { + slog.Debug("HTTP response dump", "dump", string(respDump)) + } + if resp.StatusCode >= 300 && resp.StatusCode < 400 { + location := resp.Header["Location"] + slog.Debug("HTTP redirect", "location", location) + } + } + return resp, err +} + // DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file. // // If Verify is set to VerifyNever, the verification will be nil. @@ -86,20 +128,43 @@ type ChartDownloader struct { // Returns a string path to the location where the file was downloaded and a verification // (if provenance was verified), or an error if something bad happened. func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *provenance.Verification, error) { + debug := os.Getenv("HELM_DEBUG") == "true" + + // If debug is enabled, wrap the getter's HTTP client with a debug transport + if debug { + dt := &debugTransport{ + Transport: http.DefaultTransport.(*http.Transport).Clone(), + out: c.Out, + } + dt.DisableKeepAlives = true + c.Options = append(c.Options, getter.WithClient(&http.Client{ + Transport: dt, + CheckRedirect: func(req *http.Request, _ []*http.Request) error { + slog.Debug("Following redirect", "url", req.URL.String()) + return nil + }, + })) + } + + c.Options = append(c.Options, getter.WithAcceptHeader("application/gzip,application/octet-stream")) + u, err := c.ResolveChartVersion(ref, version) if err != nil { + slog.Debug("Failed to resolve chart version", "error", err) return "", nil, err } + slog.Debug("Resolved chart URL", "url", u.String()) g, err := c.Getters.ByScheme(u.Scheme) if err != nil { + slog.Debug("Failed to get getter for scheme", "scheme", u.Scheme, "error", err) return "", nil, err } - - c.Options = append(c.Options, getter.WithAcceptHeader("application/gzip,application/octet-stream")) + slog.Debug("Using getter for scheme", "scheme", u.Scheme) data, err := g.Get(u.String(), c.Options...) if err != nil { + slog.Debug("Failed to fetch chart", "error", err) return "", nil, err } @@ -117,6 +182,21 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven // If provenance is requested, verify it. ver := &provenance.Verification{} if c.Verify > VerifyNever { + // If debug is enabled, use the same debug transport for provenance file + if debug { + dt := &debugTransport{ + Transport: http.DefaultTransport.(*http.Transport).Clone(), + out: c.Out, + } + dt.DisableKeepAlives = true + c.Options = append(c.Options, getter.WithClient(&http.Client{ + Transport: dt, + CheckRedirect: func(req *http.Request, _ []*http.Request) error { + slog.Debug("Following redirect for prov file", "url", req.URL.String()) + return nil + }, + })) + } body, err := g.Get(u.String() + ".prov") if err != nil { if c.Verify == VerifyAlways { diff --git a/pkg/getter/getter.go b/pkg/getter/getter.go index 5605e043f..b1eb1ef12 100644 --- a/pkg/getter/getter.go +++ b/pkg/getter/getter.go @@ -19,7 +19,9 @@ package getter import ( "bytes" "fmt" + "log/slog" "net/http" + "os" "slices" "time" @@ -47,6 +49,7 @@ type options struct { registryClient *registry.Client timeout time.Duration transport *http.Transport + client *http.Client } // Option allows specifying various settings configurable by the user for overriding the defaults @@ -191,6 +194,18 @@ const ( var defaultOptions = []Option{WithTimeout(time.Second * DefaultHTTPTimeout)} +func init() { + level := slog.LevelInfo + if os.Getenv("HELM_DEBUG") == "true" { + level = slog.LevelDebug + } + + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: level, + })) + slog.SetDefault(logger) +} + func Getters(extraOpts ...Option) Providers { return Providers{ Provider{ diff --git a/pkg/getter/httpgetter.go b/pkg/getter/httpgetter.go index 4cf528797..e5d152cb7 100644 --- a/pkg/getter/httpgetter.go +++ b/pkg/getter/httpgetter.go @@ -20,8 +20,10 @@ import ( "crypto/tls" "fmt" "io" + "log/slog" "net/http" "net/url" + "os" "sync" "helm.sh/helm/v4/internal/tlsutil" @@ -46,6 +48,8 @@ func (g *HTTPGetter) Get(href string, options ...Option) (*bytes.Buffer, error) func (g *HTTPGetter) get(href string) (*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. + debug := os.Getenv("HELM_DEBUG") == "true" + req, err := http.NewRequest(http.MethodGet, href, nil) if err != nil { return nil, err @@ -80,17 +84,29 @@ func (g *HTTPGetter) get(href string) (*bytes.Buffer, error) { } } - client, err := g.httpClient() - if err != nil { - return nil, err + client := g.opts.client + if client == nil { + var err error + client, err = g.httpClient() + if err != nil { + return nil, err + } + } + + if debug { + slog.Debug("HTTP GET request", "url", href) } resp, err := client.Do(req) if err != nil { + if debug { + slog.Debug("HTTP GET failed", "error", err) + } return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { + slog.Debug("HTTP GET non-OK response", "url", href, "status", resp.Status) return nil, fmt.Errorf("failed to fetch %s : %s", href, resp.Status) } @@ -158,3 +174,10 @@ func (g *HTTPGetter) httpClient() (*http.Client, error) { return client, nil } + +// WithClient sets the HTTP client for the getter +func WithClient(client *http.Client) Option { + return func(opts *options) { + opts.client = client + } +}