mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-21 12:50:02 +02:00
feat: add --repo-retries for helm repo and registry login commands (#2683)
* feat: add --repo-retries for retrying helm repo and registry login commands Add a configurable retry mechanism for chart repository operations to handle unstable networks (corporate proxies, slow internal registries). Closes #1894 - New --repo-retries N flag and HELMFILE_REPO_RETRIES env var (default 0 = opt-in, backward compatible) - Retry applies to helm repo add, helm repo update (incl. ACR), and helm registry login with exponential backoff (1s, 2s, 4s, ..., capped 30s) - Single retryRepoOp helper; per-attempt args/buffer are local to avoid state leaking across retries - Tests cover succeed-after-retry, exhausted-retries, disabled-by-default, and regression guards for password-buffer and args-accumulation Signed-off-by: yxxhero <aiopsclub@163.com> * fix: address PR review (overflow guard, cancellable sleep, flag-override, docs) Address Copilot review feedback on #2683: - Cap backoff shift exponent at 5 to prevent time.Duration overflow on large --repo-retries values - Make retry sleep context-aware (sleepCtx) so Ctrl+C aborts the retry loop promptly via the ShellRunner context - Log a concise exit status instead of the verbose ExitError dump, and clarify the retry-counter wording ('retry N/M') - Use -1 sentinel as the CLI default so --repo-retries=0 can explicitly disable retries even when HELMFILE_REPO_RETRIES is set - Align help text and docs: retry applies 'on failure' (not just transient errors), document the 0-disables behavior - Add tests for overflow guard, cancellable sleep, and flag-zero-disables Signed-off-by: yxxhero <aiopsclub@163.com> * fix: abort retries on canceled context, hide sentinel default, align comment Address follow-up Copilot review on #2683: - Fix tight-loop bug: sleepCtx now returns whether it completed vs was interrupted by context cancellation, and retryRepoOp aborts the retry loop on interruption so Ctrl+C no longer spins into rapid helm calls - Hide the -1 sentinel from --help by overriding the displayed default to 0 (pflag DefValue), matching the documented default while keeping the flag-override semantics - Correct HelmExecOptions.RepoRetry comment: 'on failure' not 'transient network errors', matching the actual retry behavior - Add Test_Retry_AbortsOnCanceledContext covering the no-tight-loop path Signed-off-by: yxxhero <aiopsclub@163.com> * fix: copy args per retry in RegistryLogin, make cancel test deterministic Address follow-up Copilot review on #2683: - RegistryLogin: pass a per-attempt copy of args to execStdIn so its internal append (for helm.extra) can't alias the shared slice across retries - Test_Retry_AbortsOnCanceledContext: cancel the context deterministically inside the op closure after the first attempt, replacing the flaky time.Sleep(20ms) goroutine Signed-off-by: yxxhero <aiopsclub@163.com> * fix: return error on unknown managed repo type instead of silent skip Address Copilot review on #2683: AddRepo logged an error for an unknown managed type but returned nil, silently succeeding while skipping the repo add. Now returns an error so misconfigurations fail loudly. Signed-off-by: yxxhero <aiopsclub@163.com> --------- Signed-off-by: yxxhero <aiopsclub@163.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/term"
|
||||
@@ -42,6 +43,11 @@ type GlobalOptions struct {
|
||||
EnforcePluginVerification bool
|
||||
// HelmOCIPlainHTTP is true if Helm should use plain HTTP for OCI registries
|
||||
HelmOCIPlainHTTP bool
|
||||
// RepoRetry is the number of times to retry "helm repo add/update" and
|
||||
// "helm registry login" on failure with exponential backoff.
|
||||
// A negative value (the CLI default sentinel) means "unset" and falls back
|
||||
// to the HELMFILE_REPO_RETRIES env var; 0 explicitly disables retries.
|
||||
RepoRetry int
|
||||
// Quiet is true if the output should be quiet.
|
||||
Quiet bool
|
||||
// Kubeconfig is the path to the kubeconfig file to use.
|
||||
@@ -278,6 +284,21 @@ func (g *GlobalImpl) HelmOCIPlainHTTP() bool {
|
||||
return g.GlobalOptions.HelmOCIPlainHTTP
|
||||
}
|
||||
|
||||
// RepoRetry returns the number of times to retry helm repo and registry login
|
||||
// operations on failure, with exponential backoff. A negative value means the
|
||||
// flag was not specified, so the HELMFILE_REPO_RETRIES env var is consulted;
|
||||
// this lets --repo-retries=0 explicitly disable retries even when the env var
|
||||
// is set.
|
||||
func (g *GlobalImpl) RepoRetry() int {
|
||||
if g.GlobalOptions.RepoRetry >= 0 {
|
||||
return g.GlobalOptions.RepoRetry
|
||||
}
|
||||
if v, err := strconv.Atoi(os.Getenv(envvar.RepoRetry)); err == nil && v > 0 {
|
||||
return v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// SequentialHelmfiles returns whether to process helmfile.d files sequentially
|
||||
func (g *GlobalImpl) SequentialHelmfiles() bool {
|
||||
return g.GlobalOptions.SequentialHelmfiles
|
||||
|
||||
@@ -420,3 +420,31 @@ func TestColorFlagOverridesNoColorEnv(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRepoRetry tests the repo-retries flag and HELMFILE_REPO_RETRIES env var fallback
|
||||
func TestRepoRetry(t *testing.T) {
|
||||
// RepoRetry < 0 means the flag was not specified (CLI default sentinel).
|
||||
unset := GlobalOptions{RepoRetry: -1}
|
||||
tests := []struct {
|
||||
name string
|
||||
opts GlobalOptions
|
||||
env string
|
||||
expected int
|
||||
}{
|
||||
{name: "default (unset)", opts: unset, env: "", expected: 0},
|
||||
{name: "env set", opts: unset, env: "3", expected: 3},
|
||||
{name: "flag set", opts: GlobalOptions{RepoRetry: 5}, env: "", expected: 5},
|
||||
{name: "flag overrides env", opts: GlobalOptions{RepoRetry: 5}, env: "3", expected: 5},
|
||||
{name: "flag zero disables env", opts: GlobalOptions{RepoRetry: 0}, env: "3", expected: 0},
|
||||
{name: "invalid env ignored", opts: unset, env: "abc", expected: 0},
|
||||
{name: "negative env ignored", opts: unset, env: "-1", expected: 0},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Setenv(envvar.RepoRetry, test.env)
|
||||
received := NewGlobalImpl(&test.opts).RepoRetry()
|
||||
require.Equalf(t, test.expected, received, "RepoRetry expected %d, received %d", test.expected, received)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user