diff --git a/cmd/apply.go b/cmd/apply.go index 5f7da506..f7a603da 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -57,6 +57,7 @@ func NewApplyCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.BoolVar(&applyOptions.NoHooks, "no-hooks", false, "do not diff changes made by hooks.") f.BoolVar(&applyOptions.HideNotes, "hide-notes", false, "add --hide-notes flag to helm") f.BoolVar(&applyOptions.TakeOwnership, "take-ownership", false, "add --take-ownership flag to helm") + f.StringVar(&applyOptions.ServerSide, "server-side", "", `add --server-side flag to helm upgrade (Helm 4 only). Must be "true", "false", or "auto"`) f.BoolVar(&applyOptions.SyncReleaseLabels, "sync-release-labels", false, "sync release labels to the target release") f.BoolVar(&applyOptions.SuppressDiff, "suppress-diff", false, "suppress diff in the output. Usable in new installs") f.BoolVar(&applyOptions.Wait, "wait", false, `Override helmDefaults.wait setting "helm upgrade --install --wait"`) diff --git a/cmd/diff.go b/cmd/diff.go index 32267d3e..6c5685b6 100644 --- a/cmd/diff.go +++ b/cmd/diff.go @@ -54,6 +54,7 @@ func NewDiffCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.BoolVar(&diffOptions.ReuseValues, "reuse-values", false, `Override helmDefaults.reuseValues "helm diff upgrade --install --reuse-values"`) f.BoolVar(&diffOptions.ResetValues, "reset-values", false, `Override helmDefaults.reuseValues "helm diff upgrade --install --reset-values"`) f.BoolVar(&diffOptions.TakeOwnership, "take-ownership", false, "add --take-ownership flag to helm") + f.StringVar(&diffOptions.ServerSide, "server-side", "", `add --server-side flag to helm diff upgrade (Helm 4 only). Must be "true", "false", or "auto"`) f.StringVar(&diffOptions.PostRenderer, "post-renderer", "", `pass --post-renderer to "helm template" or "helm upgrade --install"`) f.StringArrayVar(&diffOptions.PostRendererArgs, "post-renderer-args", nil, `pass --post-renderer-args to "helm template" or "helm upgrade --install"`) f.StringArrayVar(&diffOptions.SuppressOutputLineRegex, "suppress-output-line-regex", nil, "a list of regex patterns to suppress output lines from the diff output") diff --git a/cmd/sync.go b/cmd/sync.go index cd735e3f..0f398682 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -44,6 +44,7 @@ func NewSyncCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.BoolVar(&syncOptions.EnforceNeedsAreInstalled, "enforce-needs-are-installed", false, "enforce that all 'needs' dependencies are installable before applying changes") f.BoolVar(&syncOptions.HideNotes, "hide-notes", false, "add --hide-notes flag to helm") f.BoolVar(&syncOptions.TakeOwnership, "take-ownership", false, `add --take-ownership flag to helm`) + f.StringVar(&syncOptions.ServerSide, "server-side", "", `add --server-side flag to helm upgrade (Helm 4 only). Must be "true", "false", or "auto"`) f.BoolVar(&syncOptions.SyncReleaseLabels, "sync-release-labels", false, "sync release labels to the target release") f.BoolVar(&syncOptions.Wait, "wait", false, `Override helmDefaults.wait setting "helm upgrade --install --wait"`) f.BoolVar(&syncOptions.WaitForJobs, "wait-for-jobs", false, `Override helmDefaults.waitForJobs setting "helm upgrade --install --wait-for-jobs"`) diff --git a/docs/configuration.md b/docs/configuration.md index 0ffc5bf9..ac7af78a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -433,6 +433,7 @@ The following `helmDefaults` fields are also available but not shown in the exam | `skipRefresh` | bool | false | Skip running `helm dependency up` | | `forceConflicts` | bool | false | Force server-side apply changes against conflicts (Helm 4 only) | | `takeOwnership` | bool | false | Take ownership of existing resources | +| `serverSide` | string | | Controls the helm 4 `--server-side` flag. Must be `"true"`, `"false"`, or `"auto"` (Helm 4 only) | | `trackMode` | string | `""` | Default tracking mode for resources. See [Advanced Features](advanced-features.md#resource-tracking-with-kubedog) | | `disableAutoDetectedKubeVersionForDiff` | bool | false | Disable auto-detected kubeVersion being passed to helm diff | @@ -456,6 +457,7 @@ The following per-release fields are also available: | `skipRefresh` | bool | false | Per-release skip for `helm dependency up` | | `disableAutoDetectedKubeVersionForDiff` | bool | false | Disable auto-detected kubeVersion for helm diff on this release | | `takeOwnership` | bool | false | Take ownership of existing resources for this release | +| `serverSide` | string | | Controls the helm 4 `--server-side` flag for this release. Must be `"true"`, `"false"`, or `"auto"` (Helm 4 only) | | `forceConflicts` | bool | false | Force server-side apply against conflicts (Helm 4 only) | | `description` | string | | Description of the release | | `enableDNS` | bool | false | Enable DNS lookups when rendering templates | diff --git a/pkg/app/app.go b/pkg/app/app.go index aace620f..d64eced9 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1816,6 +1816,7 @@ func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, bool, []error) { SkipSchemaValidation: c.SkipSchemaValidation(), SuppressOutputLineRegex: c.SuppressOutputLineRegex(), TakeOwnership: c.TakeOwnership(), + ServerSide: c.ServerSide(), DetectedKubeVersion: detectedKubeVersion, } @@ -1937,6 +1938,7 @@ Do you really want to apply? SyncArgs: c.SyncArgs(), HideNotes: c.HideNotes(), TakeOwnership: c.TakeOwnership(), + ServerSide: c.ServerSide(), SyncReleaseLabels: c.SyncReleaseLabels(), TrackMode: c.TrackMode(), TrackTimeout: c.TrackTimeout(), @@ -2096,6 +2098,7 @@ func (a *App) diff(r *Run, c DiffConfigProvider) (*string, bool, bool, []error) SkipSchemaValidation: c.SkipSchemaValidation(), SuppressOutputLineRegex: c.SuppressOutputLineRegex(), TakeOwnership: c.TakeOwnership(), + ServerSide: c.ServerSide(), DetectedKubeVersion: detectedKubeVersion, } @@ -2335,6 +2338,7 @@ func (a *App) SyncState(r *Run, c SyncConfigProvider) (bool, bool, []error) { SkipSchemaValidation: diffC.SkipSchemaValidation(), SuppressOutputLineRegex: diffC.SuppressOutputLineRegex(), TakeOwnership: diffC.TakeOwnership(), + ServerSide: diffC.ServerSide(), DetectedKubeVersion: detectedKubeVersion, } infoMsgPtr, _, _, diffErrs := r.diff(false, diffC.DetailedExitcode(), diffC, diffOpts) @@ -2422,6 +2426,7 @@ Do you really want to sync? SyncArgs: c.SyncArgs(), HideNotes: c.HideNotes(), TakeOwnership: c.TakeOwnership(), + ServerSide: c.ServerSide(), SkipSchemaValidation: c.SkipSchemaValidation(), SyncReleaseLabels: c.SyncReleaseLabels(), TrackMode: c.TrackMode(), diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index 132438d4..98c92bf9 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -2539,6 +2539,7 @@ type applyConfig struct { showOnly []string hideNotes bool takeOwnership bool + serverSide string syncReleaseLabels bool enforceNeedsAreInstalled bool trackMode string @@ -2755,6 +2756,10 @@ func (a applyConfig) TakeOwnership() bool { return a.takeOwnership } +func (a applyConfig) ServerSide() string { + return a.serverSide +} + func (a applyConfig) SyncReleaseLabels() bool { return a.syncReleaseLabels } diff --git a/pkg/app/config.go b/pkg/app/config.go index 560be510..61aab00b 100644 --- a/pkg/app/config.go +++ b/pkg/app/config.go @@ -49,6 +49,7 @@ type ApplyConfigProvider interface { Cascade() string HideNotes() bool TakeOwnership() bool + ServerSide() string SuppressOutputLineRegex() []string Values() []string @@ -108,6 +109,7 @@ type SyncConfigProvider interface { PostRendererArgs() []string HideNotes() bool TakeOwnership() bool + ServerSide() string Cascade() string Values() []string @@ -176,6 +178,7 @@ type DiffConfigProvider interface { Context() int DiffOutput() string TakeOwnership() bool + ServerSide() string concurrencyConfig valuesControlMode diff --git a/pkg/app/diff_test.go b/pkg/app/diff_test.go index 3693bb3c..3ab0dcc8 100644 --- a/pkg/app/diff_test.go +++ b/pkg/app/diff_test.go @@ -47,6 +47,7 @@ type diffConfig struct { reuseValues bool logger *zap.SugaredLogger takeOwnership bool + serverSide string enforceNeedsAreInstalled bool } @@ -188,6 +189,9 @@ func (a diffConfig) SuppressOutputLineRegex() []string { func (a diffConfig) TakeOwnership() bool { return a.takeOwnership } +func (a diffConfig) ServerSide() string { + return a.serverSide +} func (a diffConfig) EnforceNeedsAreInstalled() bool { return a.enforceNeedsAreInstalled diff --git a/pkg/config/apply.go b/pkg/config/apply.go index bc940b9e..b8c4ccbb 100644 --- a/pkg/config/apply.go +++ b/pkg/config/apply.go @@ -81,6 +81,9 @@ type ApplyOptions struct { // TakeOwnership is true if the ownership should be taken TakeOwnership bool + // ServerSide controls the helm 4 --server-side flag. Must be "true", "false", or "auto". + ServerSide string + SyncReleaseLabels bool // TrackMode specifies whether to use 'helm' or 'kubedog' for tracking resources TrackMode string @@ -298,6 +301,11 @@ func (a *ApplyImpl) TakeOwnership() bool { return a.ApplyOptions.TakeOwnership } +// ServerSide returns the ServerSide. +func (a *ApplyImpl) ServerSide() string { + return a.ApplyOptions.ServerSide +} + // SyncReleaseLabels returns the SyncReleaseLabels. func (a *ApplyImpl) SyncReleaseLabels() bool { return a.ApplyOptions.SyncReleaseLabels diff --git a/pkg/config/diff.go b/pkg/config/diff.go index 153d87fb..309228c2 100644 --- a/pkg/config/diff.go +++ b/pkg/config/diff.go @@ -53,6 +53,8 @@ type DiffOptions struct { SkipSchemaValidation bool // TakeOwnership is true if the ownership should be taken TakeOwnership bool + // ServerSide controls the helm 4 --server-side flag. Must be "true", "false", or "auto". + ServerSide string } // NewDiffOptions creates a new Apply @@ -219,3 +221,8 @@ func (t *DiffImpl) SkipSchemaValidation() bool { func (t *DiffImpl) TakeOwnership() bool { return t.DiffOptions.TakeOwnership } + +// ServerSide returns the ServerSide. +func (t *DiffImpl) ServerSide() string { + return t.DiffOptions.ServerSide +} diff --git a/pkg/config/sync.go b/pkg/config/sync.go index 4b9b58af..bb9a5827 100644 --- a/pkg/config/sync.go +++ b/pkg/config/sync.go @@ -51,6 +51,8 @@ type SyncOptions struct { HideNotes bool // TakeOwnership is the take ownership flag TakeOwnership bool + // ServerSide controls the helm 4 --server-side flag. Must be "true", "false", or "auto". + ServerSide string // SyncReleaseLabels is the sync release labels flag SyncReleaseLabels bool // TrackMode specifies whether to use 'helm' or 'kubedog' for tracking resources @@ -214,6 +216,11 @@ func (t *SyncImpl) TakeOwnership() bool { return t.SyncOptions.TakeOwnership } +// ServerSide returns the server-side value. +func (t *SyncImpl) ServerSide() string { + return t.SyncOptions.ServerSide +} + func (t *SyncImpl) SyncReleaseLabels() bool { return t.SyncOptions.SyncReleaseLabels } diff --git a/pkg/state/helmx.go b/pkg/state/helmx.go index 96045515..a849bdd8 100644 --- a/pkg/state/helmx.go +++ b/pkg/state/helmx.go @@ -354,6 +354,44 @@ func (st *HelmState) appendTakeOwnershipFlagsForUpgrade(flags []string, helm hel return flags } +// validServerSideValues are the allowed values for the helm 4 --server-side flag. +var validServerSideValues = map[string]struct{}{"true": {}, "false": {}, "auto": {}} + +// appendServerSideFlagsForUpgrade appends the helm 4 --server-side flag when appropriate. +// Precedence: release-level > CLI flag > helmDefaults. +func (st *HelmState) appendServerSideFlagsForUpgrade(flags []string, helm helmexec.Interface, release *ReleaseSpec, serverSide string) ([]string, error) { + if !helm.IsHelm4() { + // --server-side with a string value is helm 4 only. Guard against misconfiguration + // when the user is running helm 3. + if release.ServerSide != nil && *release.ServerSide != "" { + return nil, fmt.Errorf("serverSide requires Helm 4 or greater (set via releases[].serverSide)") + } + if st.HelmDefaults.ServerSide != nil && *st.HelmDefaults.ServerSide != "" { + return nil, fmt.Errorf("serverSide requires Helm 4 or greater (set via helmDefaults.serverSide)") + } + return flags, nil + } + + var value string + switch { + case release.ServerSide != nil && *release.ServerSide != "": + value = *release.ServerSide + case serverSide != "": + value = serverSide + case st.HelmDefaults.ServerSide != nil && *st.HelmDefaults.ServerSide != "": + value = *st.HelmDefaults.ServerSide + default: + return flags, nil + } + + if _, ok := validServerSideValues[value]; !ok { + return nil, fmt.Errorf("invalid serverSide value %q: must be \"true\", \"false\", or \"auto\"", value) + } + + flags = append(flags, "--server-side", value) + return flags, nil +} + // append show-only flags to helm flags func (st *HelmState) appendShowOnlyFlags(flags []string, showOnly []string) []string { showOnlyFlags := []string{} diff --git a/pkg/state/helmx_test.go b/pkg/state/helmx_test.go index d4cab5ff..0c08bef5 100644 --- a/pkg/state/helmx_test.go +++ b/pkg/state/helmx_test.go @@ -572,3 +572,157 @@ func TestFormatLabels(t *testing.T) { }) } } + +func strPtr(s string) *string { + return &s +} + +func TestAppendServerSideFlagsForUpgrade(t *testing.T) { + type args struct { + flags []string + helm helmexec.Interface + helmSpec HelmSpec + opt *SyncOpts + release *ReleaseSpec + expected []string + wantErr bool + } + tests := []struct { + name string + args args + }{ + { + name: "no server-side when not configured", + args: args{ + flags: []string{}, + helm: testutil.NewHelmExec(true), + release: &ReleaseSpec{}, + opt: &SyncOpts{}, + expected: []string{}, + }, + }, + { + name: "server-side from cmd flag", + args: args{ + flags: []string{}, + helm: testutil.NewHelmExec(true), + release: &ReleaseSpec{}, + opt: &SyncOpts{ServerSide: "true"}, + expected: []string{"--server-side", "true"}, + }, + }, + { + name: "server-side from release", + args: args{ + flags: []string{}, + helm: testutil.NewHelmExec(true), + release: &ReleaseSpec{ServerSide: strPtr("false")}, + opt: &SyncOpts{}, + expected: []string{"--server-side", "false"}, + }, + }, + { + name: "server-side from helmDefaults", + args: args{ + flags: []string{}, + helm: testutil.NewHelmExec(true), + helmSpec: HelmSpec{ServerSide: strPtr("auto")}, + release: &ReleaseSpec{}, + opt: &SyncOpts{}, + expected: []string{"--server-side", "auto"}, + }, + }, + { + name: "release-level overrides cmd flag", + args: args{ + flags: []string{}, + helm: testutil.NewHelmExec(true), + release: &ReleaseSpec{ServerSide: strPtr("true")}, + opt: &SyncOpts{ServerSide: "false"}, + expected: []string{"--server-side", "true"}, + }, + }, + { + name: "cmd flag overrides helmDefaults", + args: args{ + flags: []string{}, + helm: testutil.NewHelmExec(true), + helmSpec: HelmSpec{ServerSide: strPtr("false")}, + release: &ReleaseSpec{}, + opt: &SyncOpts{ServerSide: "true"}, + expected: []string{"--server-side", "true"}, + }, + }, + { + name: "helm 3 with no config is ignored", + args: args{ + flags: []string{}, + helm: testutil.NewVersionHelmExec("3.17.0"), + release: &ReleaseSpec{}, + opt: &SyncOpts{}, + expected: []string{}, + }, + }, + { + name: "helm 3 with release-level config errors", + args: args{ + flags: []string{}, + helm: testutil.NewVersionHelmExec("3.17.0"), + release: &ReleaseSpec{ServerSide: strPtr("true")}, + opt: &SyncOpts{}, + expected: []string{}, + wantErr: true, + }, + }, + { + name: "helm 3 with helmDefaults config errors", + args: args{ + flags: []string{}, + helm: testutil.NewVersionHelmExec("3.17.0"), + helmSpec: HelmSpec{ServerSide: strPtr("true")}, + release: &ReleaseSpec{}, + opt: &SyncOpts{}, + expected: []string{}, + wantErr: true, + }, + }, + { + name: "invalid value errors", + args: args{ + flags: []string{}, + helm: testutil.NewHelmExec(true), + release: &ReleaseSpec{}, + opt: &SyncOpts{ServerSide: "yes"}, + expected: []string{}, + wantErr: true, + }, + }, + { + name: "empty release-level value falls through to cmd flag", + args: args{ + flags: []string{}, + helm: testutil.NewHelmExec(true), + release: &ReleaseSpec{ServerSide: strPtr("")}, + opt: &SyncOpts{ServerSide: "auto"}, + expected: []string{"--server-side", "auto"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + st := &HelmState{} + st.HelmDefaults = tt.args.helmSpec + optServerSide := "" + if tt.args.opt != nil { + optServerSide = tt.args.opt.ServerSide + } + got, err := st.appendServerSideFlagsForUpgrade(tt.args.flags, tt.args.helm, tt.args.release, optServerSide) + if tt.args.wantErr { + require.Error(t, err, "appendServerSideFlagsForUpgrade() should return error") + } else { + require.NoError(t, err, "appendServerSideFlagsForUpgrade() should not return error") + require.Equalf(t, tt.args.expected, got, "appendServerSideFlagsForUpgrade() = %v, want %v", got, tt.args.expected) + } + }) + } +} diff --git a/pkg/state/state.go b/pkg/state/state.go index cbc907d9..acabe468 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -250,6 +250,8 @@ type HelmSpec struct { SyncReleaseLabels *bool `yaml:"syncReleaseLabels,omitempty"` // TakeOwnership is true if the helmfile should take ownership of the release TakeOwnership *bool `yaml:"takeOwnership,omitempty"` + // ServerSide controls the helm 4 --server-side flag for upgrade. Must be "true", "false", or "auto". + ServerSide *string `yaml:"serverSide,omitempty"` // TrackMode specifies whether to use 'helm' or 'kubedog' for tracking resources TrackMode string `yaml:"trackMode,omitempty"` } @@ -477,6 +479,8 @@ type ReleaseSpec struct { SyncReleaseLabels *bool `yaml:"syncReleaseLabels,omitempty"` // TakeOwnership is true if release should take ownership of resources TakeOwnership *bool `yaml:"takeOwnership,omitempty"` + // ServerSide controls the helm 4 --server-side flag for this release. Must be "true", "false", or "auto". + ServerSide *string `yaml:"serverSide,omitempty"` // TrackMode specifies whether to use 'helm' or 'kubedog' for tracking resources TrackMode string `yaml:"trackMode,omitempty"` // TrackTimeout specifies timeout for kubedog tracking (in seconds) @@ -965,6 +969,7 @@ type SyncOpts struct { SyncArgs string HideNotes bool TakeOwnership bool + ServerSide string TrackMode string TrackTimeout int TrackLogs bool @@ -2908,6 +2913,7 @@ type DiffOpts struct { SuppressOutputLineRegex []string SkipSchemaValidation bool TakeOwnership bool + ServerSide string // DetectedKubeVersion is the Kubernetes version detected from the cluster. // This is used when kubeVersion is not specified in helmfile.yaml DetectedKubeVersion string @@ -3766,10 +3772,12 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp postRenderer := "" syncReleaseLabels := false takeOwnership := false + serverSide := "" if opt != nil { postRenderer = opt.PostRenderer syncReleaseLabels = opt.SyncReleaseLabels takeOwnership = opt.TakeOwnership + serverSide = opt.ServerSide } flags = st.appendConnectionFlags(flags, release) @@ -3808,6 +3816,13 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp // append take-ownership flag flags = st.appendTakeOwnershipFlagsForUpgrade(flags, helm, release, takeOwnership) + // append server-side flag + var ssErr error + flags, ssErr = st.appendServerSideFlagsForUpgrade(flags, helm, release, serverSide) + if ssErr != nil { + return nil, nil, ssErr + } + flags = st.appendExtraSyncFlags(flags, opt) common, clean, err := st.namespaceAndValuesFlags(helm, release, workerIndex) @@ -3992,8 +4007,10 @@ func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec, } takeOwnership := false + serverSide := "" if opt != nil { takeOwnership = opt.TakeOwnership + serverSide = opt.ServerSide } flags, err = st.appendTakeOwnershipFlagsForDiff(flags, release, takeOwnership, pluginsDir) @@ -4001,6 +4018,12 @@ func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec, return nil, nil, err } + // append server-side flag + flags, err = st.appendServerSideFlagsForUpgrade(flags, helm, release, serverSide) + if err != nil { + return nil, nil, err + } + common, files, err := st.namespaceAndValuesFlags(helm, release, workerIndex) if err != nil { return nil, files, err diff --git a/pkg/state/temp_test.go b/pkg/state/temp_test.go index 9ca7339e..44f23d68 100644 --- a/pkg/state/temp_test.go +++ b/pkg/state/temp_test.go @@ -38,39 +38,39 @@ func TestGenerateID(t *testing.T) { run(testcase{ subject: "baseline", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, - want: "foo-values-7f6f8d74dd", + want: "foo-values-56c9b878cf", }) run(testcase{ subject: "different bytes content", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, data: []byte(`{"k":"v"}`), - want: "foo-values-5fc74c864c", + want: "foo-values-55d46b7dd4", }) run(testcase{ subject: "different map content", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, data: map[string]any{"k": "v"}, - want: "foo-values-77df88dd65", + want: "foo-values-8c574f646", }) run(testcase{ subject: "different chart", release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"}, - want: "foo-values-77c96457f7", + want: "foo-values-664cdfb57d", }) run(testcase{ subject: "different name", release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"}, - want: "bar-values-6695f7ff4c", + want: "bar-values-fbb5fd45b", }) run(testcase{ subject: "specific ns", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"}, - want: "myns-foo-values-9b9484d4c", + want: "myns-foo-values-7db84f99b", }) for id, n := range ids {