feat: add helm-legacy track mode for Helm v4 compatibility (#2466)
Add support for trackMode: helm-legacy to use Helm v4's --wait=legacy flag, which maintains compatibility with Helm v3's wait behavior during migration. Helm v4 changed the default --wait behavior from polling to a watcher-based approach. This can cause issues with charts that have broken livenessProbe configurations without startupProbe. The --wait=legacy flag preserves the Helm v3 polling behavior for smoother migration. Changes: - Add TrackModeHelmLegacy constant in pkg/kubedog/options.go - Use kubedog.TrackMode constants instead of raw strings in helmx.go - Enhance appendWaitFlags to use --wait=legacy for Helm v4 when trackMode is helm-legacy - Add nil check for logger before logging warning - Add version check with warning when helm-legacy is used with Helm v3 - Update validation in pkg/config to accept helm-legacy track mode - Update command-line flags in cmd/apply.go and cmd/sync.go - Add comprehensive documentation in docs/advanced-features.md - Add thorough test coverage including warning message verification Behavior: - Helm v4 + helm-legacy: Uses --wait=legacy - Helm v3 + helm-legacy: Falls back to --wait with warning - Helm v4 + helm: Uses --wait (watcher mode) - Any + kubedog: Skips --wait flag Fixes #2464 Signed-off-by: yxxhero <aiopsclub@163.com> Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
077a5a8dab
commit
c6e7249eb9
|
|
@ -68,7 +68,7 @@ func NewApplyCmd(globalCfg *config.GlobalImpl) *cobra.Command {
|
|||
f.BoolVar(&applyOptions.SkipSchemaValidation, "skip-schema-validation", false, `pass --skip-schema-validation to "helm template" or "helm upgrade --install"`)
|
||||
f.StringVar(&applyOptions.Cascade, "cascade", "", "pass cascade to helm exec, default: background")
|
||||
f.StringArrayVar(&applyOptions.SuppressOutputLineRegex, "suppress-output-line-regex", nil, "a list of regex patterns to suppress output lines from diff output")
|
||||
f.StringVar(&applyOptions.TrackMode, "track-mode", "", "Track mode for releases: 'helm' (default) or 'kubedog'")
|
||||
f.StringVar(&applyOptions.TrackMode, "track-mode", "", "Track mode for releases: 'helm' (default), 'helm-legacy' (Helm v4 only), or 'kubedog'")
|
||||
f.IntVar(&applyOptions.TrackTimeout, "track-timeout", 0, `Timeout in seconds for kubedog tracking (0 to use default 300s timeout)`)
|
||||
f.BoolVar(&applyOptions.TrackLogs, "track-logs", false, "Enable log streaming with kubedog tracking")
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ func NewSyncCmd(globalCfg *config.GlobalImpl) *cobra.Command {
|
|||
f.StringArrayVar(&syncOptions.PostRendererArgs, "post-renderer-args", nil, `pass --post-renderer-args to "helm template" or "helm upgrade --install"`)
|
||||
f.BoolVar(&syncOptions.SkipSchemaValidation, "skip-schema-validation", false, `pass --skip-schema-validation to "helm template" or "helm upgrade --install"`)
|
||||
f.StringVar(&syncOptions.Cascade, "cascade", "", "pass cascade to helm exec, default: background")
|
||||
f.StringVar(&syncOptions.TrackMode, "track-mode", "", "Track mode for releases: 'helm' (default) or 'kubedog'")
|
||||
f.StringVar(&syncOptions.TrackMode, "track-mode", "", "Track mode for releases: 'helm' (default), 'helm-legacy' (Helm v4 only), or 'kubedog'")
|
||||
f.IntVar(&syncOptions.TrackTimeout, "track-timeout", 0, `Timeout in seconds for kubedog tracking (0 to use default 300s timeout)`)
|
||||
f.BoolVar(&syncOptions.TrackLogs, "track-logs", false, "Enable log streaming with kubedog tracking")
|
||||
|
||||
|
|
|
|||
|
|
@ -31,10 +31,18 @@ helmfile apply --track-mode kubedog --track-timeout 300 --track-logs
|
|||
|
||||
#### Configuration Options
|
||||
|
||||
- **`trackMode`**: Set to `kubedog` to enable kubedog tracking (default: `helm`)
|
||||
- **`trackMode`**: Set to `kubedog` to enable kubedog tracking, or `helm-legacy` to use Helm v4's legacy wait mode (default: `helm`)
|
||||
- **`trackTimeout`**: Timeout in seconds for tracking resources (default: 300)
|
||||
- **`trackLogs`**: Enable real-time log streaming from tracked resources
|
||||
|
||||
#### Track Modes
|
||||
|
||||
Helmfile supports three track modes:
|
||||
|
||||
- **`helm`** (default): Uses Helm's built-in `--wait` flag for resource tracking
|
||||
- **`helm-legacy`**: Uses Helm v4's `--wait=legacy` flag. This is useful when migrating from Helm v3 to Helm v4 and you have charts that may have compatibility issues with the new watcher-based wait mechanism (e.g., charts with `livenessProbe` but no `startupProbe`). Note: This mode only works with Helm v4; with Helm v3 it falls back to regular `--wait`.
|
||||
- **`kubedog`**: Uses kubedog for advanced resource tracking with detailed feedback
|
||||
|
||||
#### Resource Filtering
|
||||
|
||||
Control which resources to track using whitelist/blacklist:
|
||||
|
|
@ -86,9 +94,31 @@ Resource filtering follows this priority (highest to lowest):
|
|||
- **Fine-grained control**: Track only the resources you care about
|
||||
- **Better debugging**: Immediate visibility into deployment issues
|
||||
|
||||
#### Helm v4 Legacy Wait Mode
|
||||
|
||||
When using Helm v4 with charts that have broken `livenessProbe` configurations without `startupProbe`, the default `--wait=watcher` mode may fail. Helm v4 introduces `--wait=legacy` which uses the simpler polling mechanism compatible with Helm v3's behavior.
|
||||
|
||||
To use this mode, set `trackMode: helm-legacy`:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: myapp
|
||||
chart: ./charts/myapp
|
||||
trackMode: helm-legacy
|
||||
```
|
||||
|
||||
Or via command-line:
|
||||
|
||||
```bash
|
||||
helmfile apply --track-mode helm-legacy
|
||||
```
|
||||
|
||||
#### Compatibility
|
||||
|
||||
- Kubedog tracking is compatible with Helm 3.x
|
||||
- **`helm`**: Default mode, uses Helm's built-in `--wait` flag
|
||||
- **`helm-legacy`**: Uses Helm v4's `--wait=legacy` flag (only available in Helm v4)
|
||||
- **`kubedog`**: Uses kubedog library for advanced resource tracking
|
||||
- Kubedog tracking is compatible with Helm 3.x and 4.x
|
||||
- Kubedog is a compiled dependency and is only used when `trackMode: kubedog` is set
|
||||
- Works with charts that deploy supported workload kinds (currently `Deployment`, `StatefulSet`, `DaemonSet`, and `Job`); other resource kinds are created by Helm/Helmfile as usual but are ignored by the kubedog tracker
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package config
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// ApplyOptoons is the options for the apply command
|
||||
type ApplyOptions struct {
|
||||
|
|
@ -305,8 +308,9 @@ func (a *ApplyImpl) TrackLogs() bool {
|
|||
}
|
||||
|
||||
func (a *ApplyImpl) ValidateConfig() error {
|
||||
if a.ApplyOptions.TrackMode != "" && a.ApplyOptions.TrackMode != "helm" && a.ApplyOptions.TrackMode != "kubedog" {
|
||||
return fmt.Errorf("--track-mode must be 'helm' or 'kubedog', got: %s", a.ApplyOptions.TrackMode)
|
||||
validTrackModes := []string{"helm", "helm-legacy", "kubedog"}
|
||||
if a.ApplyOptions.TrackMode != "" && !slices.Contains(validTrackModes, a.ApplyOptions.TrackMode) {
|
||||
return fmt.Errorf("--track-mode must be 'helm', 'helm-legacy', or 'kubedog', got: %s", a.ApplyOptions.TrackMode)
|
||||
}
|
||||
return a.GlobalImpl.ValidateConfig()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package config
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// SyncOptions is the options for the build command
|
||||
type SyncOptions struct {
|
||||
|
|
@ -212,8 +215,9 @@ func (t *SyncImpl) TrackLogs() bool {
|
|||
}
|
||||
|
||||
func (t *SyncImpl) ValidateConfig() error {
|
||||
if t.SyncOptions.TrackMode != "" && t.SyncOptions.TrackMode != "helm" && t.SyncOptions.TrackMode != "kubedog" {
|
||||
return fmt.Errorf("--track-mode must be 'helm' or 'kubedog', got: %s", t.SyncOptions.TrackMode)
|
||||
validTrackModes := []string{"helm", "helm-legacy", "kubedog"}
|
||||
if t.SyncOptions.TrackMode != "" && !slices.Contains(validTrackModes, t.SyncOptions.TrackMode) {
|
||||
return fmt.Errorf("--track-mode must be 'helm', 'helm-legacy', or 'kubedog', got: %s", t.SyncOptions.TrackMode)
|
||||
}
|
||||
return t.GlobalImpl.ValidateConfig()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ import (
|
|||
type TrackMode string
|
||||
|
||||
const (
|
||||
TrackModeHelm TrackMode = "helm"
|
||||
TrackModeKubedog TrackMode = "kubedog"
|
||||
TrackModeHelm TrackMode = "helm"
|
||||
TrackModeHelmLegacy TrackMode = "helm-legacy"
|
||||
TrackModeKubedog TrackMode = "kubedog"
|
||||
)
|
||||
|
||||
type TrackOptions struct {
|
||||
|
|
|
|||
|
|
@ -186,6 +186,10 @@ func (st *HelmState) appendWaitForJobsFlags(flags []string, release *ReleaseSpec
|
|||
}
|
||||
|
||||
func (st *HelmState) shouldUseKubedog(release *ReleaseSpec, ops *SyncOpts) bool {
|
||||
return st.getTrackMode(release, ops) == string(kubedog.TrackModeKubedog)
|
||||
}
|
||||
|
||||
func (st *HelmState) getTrackMode(release *ReleaseSpec, ops *SyncOpts) string {
|
||||
trackMode := release.TrackMode
|
||||
if trackMode == "" && ops != nil && ops.TrackMode != "" {
|
||||
trackMode = ops.TrackMode
|
||||
|
|
@ -193,24 +197,43 @@ func (st *HelmState) shouldUseKubedog(release *ReleaseSpec, ops *SyncOpts) bool
|
|||
if trackMode == "" {
|
||||
trackMode = st.HelmDefaults.TrackMode
|
||||
}
|
||||
return trackMode == "kubedog"
|
||||
if trackMode == "" {
|
||||
trackMode = string(kubedog.TrackModeHelm)
|
||||
}
|
||||
return trackMode
|
||||
}
|
||||
|
||||
func (st *HelmState) appendWaitFlags(flags []string, release *ReleaseSpec, ops *SyncOpts) []string {
|
||||
func (st *HelmState) appendWaitFlags(flags []string, helm helmexec.Interface, release *ReleaseSpec, ops *SyncOpts) []string {
|
||||
if st.shouldUseKubedog(release, ops) {
|
||||
return flags
|
||||
}
|
||||
|
||||
shouldWait := false
|
||||
switch {
|
||||
case release.Wait != nil && *release.Wait:
|
||||
flags = append(flags, "--wait")
|
||||
shouldWait = true
|
||||
case ops != nil && ops.Wait:
|
||||
flags = append(flags, "--wait")
|
||||
shouldWait = true
|
||||
case release.Wait == nil && st.HelmDefaults.Wait:
|
||||
flags = append(flags, "--wait")
|
||||
shouldWait = true
|
||||
}
|
||||
// Note: --wait-retries flag has been removed from Helm and is no longer supported
|
||||
// WaitRetries configuration is preserved for backward compatibility but ignored
|
||||
|
||||
if shouldWait {
|
||||
trackMode := st.getTrackMode(release, ops)
|
||||
if trackMode == string(kubedog.TrackModeHelmLegacy) {
|
||||
if helm != nil && helm.IsHelm4() {
|
||||
flags = append(flags, "--wait=legacy")
|
||||
} else {
|
||||
if st.logger != nil {
|
||||
st.logger.Warnf("trackMode 'helm-legacy' requires Helm v4, falling back to regular --wait for release %s", release.Name)
|
||||
}
|
||||
flags = append(flags, "--wait")
|
||||
}
|
||||
} else {
|
||||
flags = append(flags, "--wait")
|
||||
}
|
||||
}
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -77,6 +78,7 @@ func TestAppendWaitFlags(t *testing.T) {
|
|||
release *ReleaseSpec
|
||||
syncOpts *SyncOpts
|
||||
helmSpec HelmSpec
|
||||
helm helmexec.Interface
|
||||
expected []string
|
||||
}{
|
||||
// --wait
|
||||
|
|
@ -85,6 +87,7 @@ func TestAppendWaitFlags(t *testing.T) {
|
|||
release: &ReleaseSpec{Wait: &[]bool{true}[0]},
|
||||
syncOpts: nil,
|
||||
helmSpec: HelmSpec{},
|
||||
helm: testutil.NewVersionHelmExec("3.15.0"),
|
||||
expected: []string{"--wait"},
|
||||
},
|
||||
{
|
||||
|
|
@ -92,6 +95,7 @@ func TestAppendWaitFlags(t *testing.T) {
|
|||
release: &ReleaseSpec{},
|
||||
syncOpts: &SyncOpts{Wait: true},
|
||||
helmSpec: HelmSpec{},
|
||||
helm: testutil.NewVersionHelmExec("3.15.0"),
|
||||
expected: []string{"--wait"},
|
||||
},
|
||||
{
|
||||
|
|
@ -99,6 +103,7 @@ func TestAppendWaitFlags(t *testing.T) {
|
|||
release: &ReleaseSpec{},
|
||||
syncOpts: nil,
|
||||
helmSpec: HelmSpec{Wait: true},
|
||||
helm: testutil.NewVersionHelmExec("3.15.0"),
|
||||
expected: []string{"--wait"},
|
||||
},
|
||||
{
|
||||
|
|
@ -106,6 +111,7 @@ func TestAppendWaitFlags(t *testing.T) {
|
|||
release: &ReleaseSpec{Wait: &[]bool{false}[0]},
|
||||
syncOpts: nil,
|
||||
helmSpec: HelmSpec{Wait: true},
|
||||
helm: testutil.NewVersionHelmExec("3.15.0"),
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
|
|
@ -113,6 +119,7 @@ func TestAppendWaitFlags(t *testing.T) {
|
|||
release: &ReleaseSpec{},
|
||||
syncOpts: &SyncOpts{},
|
||||
helmSpec: HelmSpec{Wait: true},
|
||||
helm: testutil.NewVersionHelmExec("3.15.0"),
|
||||
expected: []string{"--wait"},
|
||||
},
|
||||
{
|
||||
|
|
@ -120,6 +127,7 @@ func TestAppendWaitFlags(t *testing.T) {
|
|||
release: &ReleaseSpec{},
|
||||
syncOpts: nil,
|
||||
helmSpec: HelmSpec{Wait: false},
|
||||
helm: testutil.NewVersionHelmExec("3.15.0"),
|
||||
expected: []string{},
|
||||
},
|
||||
// --wait-retries flag has been removed from Helm
|
||||
|
|
@ -128,6 +136,7 @@ func TestAppendWaitFlags(t *testing.T) {
|
|||
release: &ReleaseSpec{Wait: &[]bool{true}[0], WaitRetries: &[]int{1}[0]},
|
||||
syncOpts: nil,
|
||||
helmSpec: HelmSpec{},
|
||||
helm: testutil.NewVersionHelmExec("3.15.0"),
|
||||
expected: []string{"--wait"},
|
||||
},
|
||||
{
|
||||
|
|
@ -135,6 +144,7 @@ func TestAppendWaitFlags(t *testing.T) {
|
|||
release: &ReleaseSpec{Wait: &[]bool{true}[0], WaitRetries: &[]int{1}[0]},
|
||||
syncOpts: nil,
|
||||
helmSpec: HelmSpec{},
|
||||
helm: testutil.NewVersionHelmExec("3.15.0"),
|
||||
expected: []string{"--wait"},
|
||||
},
|
||||
{
|
||||
|
|
@ -142,6 +152,7 @@ func TestAppendWaitFlags(t *testing.T) {
|
|||
release: &ReleaseSpec{WaitRetries: &[]int{1}[0]},
|
||||
syncOpts: nil,
|
||||
helmSpec: HelmSpec{},
|
||||
helm: testutil.NewVersionHelmExec("3.15.0"),
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
|
|
@ -149,6 +160,7 @@ func TestAppendWaitFlags(t *testing.T) {
|
|||
release: &ReleaseSpec{},
|
||||
syncOpts: &SyncOpts{Wait: true, WaitRetries: 2},
|
||||
helmSpec: HelmSpec{},
|
||||
helm: testutil.NewVersionHelmExec("3.15.0"),
|
||||
expected: []string{"--wait"},
|
||||
},
|
||||
{
|
||||
|
|
@ -156,6 +168,7 @@ func TestAppendWaitFlags(t *testing.T) {
|
|||
release: &ReleaseSpec{},
|
||||
syncOpts: nil,
|
||||
helmSpec: HelmSpec{Wait: true, WaitRetries: 3},
|
||||
helm: testutil.NewVersionHelmExec("3.15.0"),
|
||||
expected: []string{"--wait"},
|
||||
},
|
||||
{
|
||||
|
|
@ -163,6 +176,7 @@ func TestAppendWaitFlags(t *testing.T) {
|
|||
release: &ReleaseSpec{Wait: &[]bool{true}[0]},
|
||||
syncOpts: nil,
|
||||
helmSpec: HelmSpec{WaitRetries: 4},
|
||||
helm: testutil.NewVersionHelmExec("3.15.0"),
|
||||
expected: []string{"--wait"},
|
||||
},
|
||||
{
|
||||
|
|
@ -170,16 +184,82 @@ func TestAppendWaitFlags(t *testing.T) {
|
|||
release: &ReleaseSpec{WaitRetries: &[]int{5}[0]},
|
||||
syncOpts: nil,
|
||||
helmSpec: HelmSpec{Wait: true},
|
||||
helm: testutil.NewVersionHelmExec("3.15.0"),
|
||||
expected: []string{"--wait"},
|
||||
},
|
||||
// helm-legacy track mode with Helm v4
|
||||
{
|
||||
name: "helm-legacy track mode with Helm v4",
|
||||
release: &ReleaseSpec{Wait: &[]bool{true}[0], TrackMode: "helm-legacy"},
|
||||
syncOpts: nil,
|
||||
helmSpec: HelmSpec{},
|
||||
helm: testutil.NewVersionHelmExec("4.0.0"),
|
||||
expected: []string{"--wait=legacy"},
|
||||
},
|
||||
{
|
||||
name: "helm-legacy track mode with Helm v4 from syncOpts",
|
||||
release: &ReleaseSpec{Wait: &[]bool{true}[0]},
|
||||
syncOpts: &SyncOpts{TrackMode: "helm-legacy"},
|
||||
helmSpec: HelmSpec{},
|
||||
helm: testutil.NewVersionHelmExec("4.0.0"),
|
||||
expected: []string{"--wait=legacy"},
|
||||
},
|
||||
{
|
||||
name: "helm-legacy track mode with Helm v4 from helmDefaults",
|
||||
release: &ReleaseSpec{Wait: &[]bool{true}[0]},
|
||||
syncOpts: nil,
|
||||
helmSpec: HelmSpec{TrackMode: "helm-legacy"},
|
||||
helm: testutil.NewVersionHelmExec("4.0.0"),
|
||||
expected: []string{"--wait=legacy"},
|
||||
},
|
||||
{
|
||||
name: "helm-legacy track mode with Helm v3 shows warning and uses --wait",
|
||||
release: &ReleaseSpec{Wait: &[]bool{true}[0], Name: "test-release", TrackMode: "helm-legacy"},
|
||||
syncOpts: nil,
|
||||
helmSpec: HelmSpec{},
|
||||
helm: testutil.NewVersionHelmExec("3.15.0"),
|
||||
expected: []string{"--wait"},
|
||||
},
|
||||
{
|
||||
name: "helm-legacy track mode without wait flag",
|
||||
release: &ReleaseSpec{Wait: &[]bool{false}[0], Name: "test-release", TrackMode: "helm-legacy"},
|
||||
syncOpts: nil,
|
||||
helmSpec: HelmSpec{},
|
||||
helm: testutil.NewVersionHelmExec("4.0.0"),
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "helm track mode with Helm v4 uses --wait",
|
||||
release: &ReleaseSpec{Wait: &[]bool{true}[0], TrackMode: "helm"},
|
||||
syncOpts: nil,
|
||||
helmSpec: HelmSpec{},
|
||||
helm: testutil.NewVersionHelmExec("4.0.0"),
|
||||
expected: []string{"--wait"},
|
||||
},
|
||||
{
|
||||
name: "kubedog track mode skips --wait",
|
||||
release: &ReleaseSpec{Wait: &[]bool{true}[0], TrackMode: "kubedog"},
|
||||
syncOpts: nil,
|
||||
helmSpec: HelmSpec{},
|
||||
helm: testutil.NewVersionHelmExec("4.0.0"),
|
||||
expected: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
st := &HelmState{}
|
||||
st.HelmDefaults = tt.helmSpec
|
||||
got := st.appendWaitFlags([]string{}, tt.release, tt.syncOpts)
|
||||
st.logger = helmexec.NewLogger(&buffer, "debug")
|
||||
got := st.appendWaitFlags([]string{}, tt.helm, tt.release, tt.syncOpts)
|
||||
require.Equalf(t, tt.expected, got, "appendWaitFlags() = %v, want %v", got, tt.expected)
|
||||
|
||||
// Check for warning message when helm-legacy is used with Helm v3
|
||||
if tt.name == "helm-legacy track mode with Helm v3 shows warning and uses --wait" {
|
||||
require.Contains(t, buffer.String(), "trackMode 'helm-legacy' requires Helm v4")
|
||||
require.Contains(t, buffer.String(), tt.release.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3439,7 +3439,7 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
|
|||
flags = st.appendChartVersionFlags(flags, release)
|
||||
flags = st.appendEnableDNSFlags(flags, release)
|
||||
|
||||
flags = st.appendWaitFlags(flags, release, opt)
|
||||
flags = st.appendWaitFlags(flags, helm, release, opt)
|
||||
flags = st.appendWaitForJobsFlags(flags, release, opt)
|
||||
|
||||
// non-OCI chart should be verified here
|
||||
|
|
|
|||
Loading…
Reference in New Issue