Repository: helm/helm
Pull request #30900 (2025-05-22 10:18:41)

Add timeout flag to repo add and update commands
**What this PR does / why we need it**:

Adds `--timeout` flags to `repo add` and `repo up` commands.
Fixes: #12952

**Special notes for your reviewer**:

This PR supersedes #13185. Adding an environment variable is not ideal IMO. Using CLI flag is more appropriate and consistent with what other commands do.

**If applicable**:
- [ ] this PR contains user facing changes (the `docs needed` label should be applied if so)
- [x] this PR contains unit tests
- [x] this PR has been tested for backwards compatibility


robertsirc: I am not sure this is a good PR for v3.x rather it should go to v4.x.
unguiculus: Why would you want to push this out to Helm 4? IMO this is nothing that needs to wait. It's a non-breaking change. We are really suffering from this.
unguiculus: Thanks @scottrigby, I'll rebase the PR.
unguiculus: I rebased and changed the base branch.

Patch:
diff --git a/pkg/cmd/repo_add.go b/pkg/cmd/repo_add.go
index 24c1eecab..187234486 100644
--- a/pkg/cmd/repo_add.go
+++ b/pkg/cmd/repo_add.go
@@ -52,6 +52,7 @@ type repoAddOptions struct {
 	passCredentialsAll   bool
 	forceUpdate          bool
 	allowDeprecatedRepos bool
+	timeout              time.Duration
 
 	certFile              string
 	keyFile               string
@@ -96,6 +97,7 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
 	f.BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the repository")
 	f.BoolVar(&o.allowDeprecatedRepos, "allow-deprecated-repos", false, "by default, this command will not allow adding official repos that have been permanently deleted. This disables that behavior")
 	f.BoolVar(&o.passCredentialsAll, "pass-credentials", false, "pass credentials to all domains")
+	f.DurationVar(&o.timeout, "timeout", getter.DefaultHTTPTimeout*time.Second, "time to wait for the index file download to complete")
 
 	return cmd
 }
@@ -199,7 +201,7 @@ func (o *repoAddOptions) run(out io.Writer) error {
 		return nil
 	}
 
-	r, err := repo.NewChartRepository(&c, getter.All(settings))
+	r, err := repo.NewChartRepository(&c, getter.All(settings, getter.WithTimeout(o.timeout)))
 	if err != nil {
 		return err
 	}
diff --git a/pkg/cmd/repo_update.go b/pkg/cmd/repo_update.go
index 9f4a603ae..23856d85e 100644
--- a/pkg/cmd/repo_update.go
+++ b/pkg/cmd/repo_update.go
@@ -22,6 +22,7 @@ import (
 	"io"
 	"slices"
 	"sync"
+	"time"
 
 	"github.com/spf13/cobra"
 
@@ -46,6 +47,7 @@ type repoUpdateOptions struct {
 	repoFile  string
 	repoCache string
 	names     []string
+	timeout   time.Duration
 }
 
 func newRepoUpdateCmd(out io.Writer) *cobra.Command {
@@ -68,6 +70,9 @@ func newRepoUpdateCmd(out io.Writer) *cobra.Command {
 		},
 	}
 
+	f := cmd.Flags()
+	f.DurationVar(&o.timeout, "timeout", getter.DefaultHTTPTimeout*time.Second, "time to wait for the index file download to complete")
+
 	return cmd
 }
 
@@ -94,7 +99,7 @@ func (o *repoUpdateOptions) run(out io.Writer) error {
 
 	for _, cfg := range f.Repositories {
 		if updateAllRepos || isRepoRequested(cfg.Name, o.names) {
-			r, err := repo.NewChartRepository(cfg, getter.All(settings))
+			r, err := repo.NewChartRepository(cfg, getter.All(settings, getter.WithTimeout(o.timeout)))
 			if err != nil {
 				return err
 			}
diff --git a/pkg/getter/getter.go b/pkg/getter/getter.go
index 1aa38cac1..5605e043f 100644
--- a/pkg/getter/getter.go
+++ b/pkg/getter/getter.go
@@ -191,24 +191,32 @@ const (
 
 var defaultOptions = []Option{WithTimeout(time.Second * DefaultHTTPTimeout)}
 
-var httpProvider = Provider{
-	Schemes: []string{"http", "https"},
-	New: func(options ...Option) (Getter, error) {
-		options = append(options, defaultOptions...)
-		return NewHTTPGetter(options...)
-	},
-}
-
-var ociProvider = Provider{
-	Schemes: []string{registry.OCIScheme},
-	New:     NewOCIGetter,
+func Getters(extraOpts ...Option) Providers {
+	return Providers{
+		Provider{
+			Schemes: []string{"http", "https"},
+			New: func(options ...Option) (Getter, error) {
+				options = append(options, defaultOptions...)
+				options = append(options, extraOpts...)
+				return NewHTTPGetter(options...)
+			},
+		},
+		Provider{
+			Schemes: []string{registry.OCIScheme},
+			New: func(options ...Option) (Getter, error) {
+				options = append(options, defaultOptions...)
+				options = append(options, extraOpts...)
+				return NewOCIGetter(options...)
+			},
+		},
+	}
 }
 
 // All finds all of the registered getters as a list of Provider instances.
 // Currently, the built-in getters and the discovered plugins with downloader
 // notations are collected.
-func All(settings *cli.EnvSettings) Providers {
-	result := Providers{httpProvider, ociProvider}
+func All(settings *cli.EnvSettings, opts ...Option) Providers {
+	result := Getters(opts...)
 	pluginDownloaders, _ := collectPlugins(settings)
 	result = append(result, pluginDownloaders...)
 	return result


Test patch:
diff --git a/pkg/getter/getter_test.go b/pkg/getter/getter_test.go
index a14301900..83920e809 100644
--- a/pkg/getter/getter_test.go
+++ b/pkg/getter/getter_test.go
@@ -17,6 +17,7 @@ package getter
 
 import (
 	"testing"
+	"time"
 
 	"helm.sh/helm/v4/pkg/cli"
 )
@@ -52,6 +53,23 @@ func TestProviders(t *testing.T) {
 	}
 }
 
+func TestProvidersWithTimeout(t *testing.T) {
+	want := time.Hour
+	getters := Getters(WithTimeout(want))
+	getter, err := getters.ByScheme("http")
+	if err != nil {
+		t.Error(err)
+	}
+	client, err := getter.(*HTTPGetter).httpClient()
+	if err != nil {
+		t.Error(err)
+	}
+	got := client.Timeout
+	if got != want {
+		t.Errorf("Expected %q, got %q", want, got)
+	}
+}
+
 func TestAll(t *testing.T) {
 	env := cli.New()
 	env.PluginsDirectory = pluginDir