From 8fc23a4d807030be0ffbfc746102b577e40dd8bf Mon Sep 17 00:00:00 2001 From: Daniel Pinto <1619617+Cayan@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:23:47 -0300 Subject: [PATCH] fix: skip recreatePods on Helm 4, which removed --recreate-pods (#2832) * fix: skip recreatePods on Helm 4, which removed --recreate-pods - Helm 4 removed `--recreate-pods` (helm/helm#31023) and fails with "unknown flag", so a release with `recreatePods: true` broke `sync` and `apply`. On Helm 4 the flag is now left out with a warning; Helm 3 still receives it. - The upgrade flag tests gain Helm 4 cases, and the existing ones are pinned to Helm 3 so they no longer depend on the installed helm binary. - A test checks that the warning names the release on Helm 4 and is not logged on Helm 3. - The configuration docs note that recreatePods is ignored on Helm 4. Signed-off-by: Cayan <1619617+Cayan@users.noreply.github.com> * refactor: flatten recreatePods flag logic in flagsForUpgrade Drop the nested if-in-if and the st.logger nil-guard: no other log call in state.go nil-checks the logger (e.g. the --color Helm 4 skip). Compute recreatePodsEnabled and branch via a tagless switch, matching the switch style already used elsewhere in state.go: warn on Helm 4, append --recreate-pods otherwise. The flagsForUpgrade table runner now uses the shared discard logger so the Helm 4 recreate-pods cases log safely. Signed-off-by: yxxhero --------- Signed-off-by: Cayan <1619617+Cayan@users.noreply.github.com> Signed-off-by: yxxhero Co-authored-by: yxxhero --- docs/configuration.md | 5 ++- pkg/state/state.go | 9 +++- pkg/state/state_test.go | 93 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 100 insertions(+), 7 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 7c273107..1ce770f1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -116,7 +116,8 @@ helmDefaults: waitForJobs: true # time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks, and waits on pod/pvc/svc/deployment readiness) (default 300) timeout: 600 - # performs pods restart for the resource if applicable (default false) + # performs pods restart for the resource if applicable (default false). + # Ignored with a warning on Helm 4, which removed --recreate-pods. recreatePods: true # forces resource update through delete/recreate if needed (default false) force: false @@ -254,7 +255,7 @@ releases: # waitRetries: 3 waitForJobs: true timeout: 60 - recreatePods: true + recreatePods: true # ignored with a warning on Helm 4 force: false reuseValues: false # set `false` to uninstall this release on sync. (default true) diff --git a/pkg/state/state.go b/pkg/state/state.go index d100a3b5..1c50065e 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -4296,7 +4296,14 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp flags = append(flags, "--force-conflicts") } - if release.RecreatePods != nil && *release.RecreatePods || release.RecreatePods == nil && st.HelmDefaults.RecreatePods { + recreatePodsEnabled := (release.RecreatePods != nil && *release.RecreatePods) || (release.RecreatePods == nil && st.HelmDefaults.RecreatePods) + + // Helm 4 removed --recreate-pods (helm/helm#31023) and fails with "unknown flag". + // It has no replacement flag; Helm recommends a checksum annotation on the pod template. + switch { + case recreatePodsEnabled && helm.IsHelm4(): + st.logger.Warnf("recreatePods is not supported by Helm 4, ignoring it for release %s", release.Name) + case recreatePodsEnabled: flags = append(flags, "--recreate-pods") } diff --git a/pkg/state/state_test.go b/pkg/state/state_test.go index 998dcb80..f1fd54fd 100644 --- a/pkg/state/state_test.go +++ b/pkg/state/state_test.go @@ -1,12 +1,14 @@ package state import ( + "bytes" "fmt" "io" "os" "path/filepath" "reflect" "slices" + "strings" "testing" "github.com/Masterminds/semver/v3" @@ -459,10 +461,12 @@ func TestHelmState_flagsForUpgrade(t *testing.T) { wantErr: "force and forceConflicts are mutually exclusive (check both releases[].force/forceConflicts and helmDefaults.force/forceConflicts)", }, { - name: "recreate-pods", + name: "recreate-pods-helm3", defaults: HelmSpec{ - RecreatePods: false, + RecreatePods: false, + CreateNamespace: &disable, }, + version: semver.MustParse("3.10.0"), release: &ReleaseSpec{ Chart: "test/chart", Version: "0.1", @@ -477,10 +481,12 @@ func TestHelmState_flagsForUpgrade(t *testing.T) { }, }, { - name: "recreate-pods-from-default", + name: "recreate-pods-from-default-helm3", defaults: HelmSpec{ - RecreatePods: true, + RecreatePods: true, + CreateNamespace: &disable, }, + version: semver.MustParse("3.10.0"), release: &ReleaseSpec{ Chart: "test/chart", Version: "0.1", @@ -493,6 +499,43 @@ func TestHelmState_flagsForUpgrade(t *testing.T) { "--namespace", "test-namespace", }, }, + { + name: "recreate-pods-helm4", + defaults: HelmSpec{ + RecreatePods: false, + CreateNamespace: &disable, + }, + version: semver.MustParse("4.0.0"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + RecreatePods: &enable, + Name: "test-charts", + Namespace: "test-namespace", + }, + want: []string{ + "--version", "0.1", + "--namespace", "test-namespace", + }, + }, + { + name: "recreate-pods-from-default-helm4", + defaults: HelmSpec{ + RecreatePods: true, + CreateNamespace: &disable, + }, + version: semver.MustParse("4.0.0"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + Name: "test-charts", + Namespace: "test-namespace", + }, + want: []string{ + "--version", "0.1", + "--namespace", "test-namespace", + }, + }, { name: "wait", defaults: HelmSpec{ @@ -1438,6 +1481,7 @@ func TestHelmState_flagsForUpgrade(t *testing.T) { basePath: "./", Releases: []ReleaseSpec{*tt.release}, HelmDefaults: tt.defaults, + logger: logger, valsRuntime: valsRuntime, } helm := &exectest.Helm{ @@ -1458,6 +1502,47 @@ func TestHelmState_flagsForUpgrade(t *testing.T) { } } +func TestHelmState_flagsForUpgrade_RecreatePodsWarning(t *testing.T) { + enable := true + disable := false + tests := []struct { + name string + version *semver.Version + wantWarn bool + }{ + {name: "helm4", version: semver.MustParse("4.0.0"), wantWarn: true}, + {name: "helm3", version: semver.MustParse("3.10.0"), wantWarn: false}, + } + for i := range tests { + tt := tests[i] + t.Run(tt.name, func(t *testing.T) { + var logs bytes.Buffer + release := &ReleaseSpec{ + Chart: "test/chart", + RecreatePods: &enable, + Name: "test-charts", + Namespace: "test-namespace", + } + state := &HelmState{ + basePath: "./", + Releases: []ReleaseSpec{*release}, + HelmDefaults: HelmSpec{CreateNamespace: &disable}, + logger: helmexec.NewLogger(&logs, "warn"), + valsRuntime: valsRuntime, + } + + if _, _, err := state.flagsForUpgrade(&exectest.Helm{Version: tt.version}, release, 0, &SyncOpts{}); err != nil { + t.Fatalf("unexpected error flagsForUpgrade: %v", err) + } + + warned := strings.Contains(logs.String(), "recreatePods is not supported by Helm 4, ignoring it for release test-charts") + if warned != tt.wantWarn { + t.Errorf("warning logged = %v, want %v; logs: %q", warned, tt.wantWarn, logs.String()) + } + }) + } +} + func TestHelmState_flagsForTemplate(t *testing.T) { enable := true disable := false