From 7cf4ff9a252255892bb57dd38e5e9cdd278a3f51 Mon Sep 17 00:00:00 2001 From: henrichter-sap Date: Mon, 3 Aug 2026 15:11:45 +0200 Subject: [PATCH] feat: add --skip-diff-validation-on-install CLI flag (#2728) Signed-off-by: Richter --- cmd/apply.go | 1 + cmd/bind_diff_flags.go | 1 + cmd/sync.go | 1 + docs/cli.md | 5 ++ docs/configuration.md | 1 + pkg/app/app.go | 107 +++++++++++++++++++++------------------- pkg/app/app_test.go | 4 ++ pkg/app/config.go | 2 + pkg/app/diff_test.go | 4 ++ pkg/config/apply.go | 7 +++ pkg/config/diff.go | 7 +++ pkg/config/sync.go | 32 +++++++----- pkg/state/state.go | 13 ++--- pkg/state/state_test.go | 81 ++++++++++++++++++++++++++++++ 14 files changed, 195 insertions(+), 71 deletions(-) diff --git a/cmd/apply.go b/cmd/apply.go index 10074920..59dcc256 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -50,6 +50,7 @@ func NewApplyCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.BoolVar(&applyOptions.EnforceNeedsAreInstalled, "enforce-needs-are-installed", false, "enforce that all 'needs' dependencies are installable before applying changes") f.BoolVar(&applyOptions.IncludeTransitiveNeeds, "include-transitive-needs", false, `like --include-needs, but also includes transitive needs (needs of needs). Does nothing when --selector/-l flag is not provided. Overrides exclusions of other selectors and conditions.`) f.BoolVar(&applyOptions.SkipDiffOnInstall, "skip-diff-on-install", false, "Skips running helm-diff on releases being newly installed on this apply. Useful when the release manifests are too huge to be reviewed, or it's too time-consuming to diff at all") + f.BoolVar(&applyOptions.SkipDiffValidationOnInstall, "skip-diff-validation-on-install", false, "Disables K8s API validation (--disable-validation) when running helm-diff on releases being newly installed. Useful when charts include CRDs and CRs in the same release") f.BoolVar(&applyOptions.IncludeTests, "include-tests", false, "enable the diffing of the helm test hooks") f.StringArrayVar(&applyOptions.Suppress, "suppress", nil, "suppress specified Kubernetes objects in the diff output. Can be provided multiple times. For example: --suppress KeycloakClient --suppress VaultSecret") f.BoolVar(&applyOptions.SuppressSecrets, "suppress-secrets", false, "suppress secrets in the diff output. highly recommended to specify on CI/CD use-cases") diff --git a/cmd/bind_diff_flags.go b/cmd/bind_diff_flags.go index 90d0c697..787de995 100644 --- a/cmd/bind_diff_flags.go +++ b/cmd/bind_diff_flags.go @@ -32,6 +32,7 @@ func bindCommonDiffFlags(f *pflag.FlagSet, opts *config.DiffOptions, globalArgs f.BoolVar(&opts.EnforceNeedsAreInstalled, "enforce-needs-are-installed", false, "enforce that all 'needs' dependencies are installable before applying changes") f.BoolVar(&opts.IncludeTransitiveNeeds, "include-transitive-needs", false, `like --include-needs, but also includes transitive needs (needs of needs). Does nothing when --selector/-l flag is not provided. Overrides exclusions of other selectors and conditions.`) f.BoolVar(&opts.SkipDiffOnInstall, "skip-diff-on-install", false, "Skips running helm-diff on releases being newly installed on this apply. Useful when the release manifests are too huge to be reviewed, or it's too time-consuming to diff at all") + f.BoolVar(&opts.SkipDiffValidationOnInstall, "skip-diff-validation-on-install", false, "Disables K8s API validation (--disable-validation) when running helm-diff on releases being newly installed. Useful when charts include CRDs and CRs in the same release") f.BoolVar(&opts.NoHooks, "no-hooks", false, "do not diff changes made by hooks.") f.BoolVar(&opts.StripTrailingCR, "strip-trailing-cr", false, "strip trailing carriage return on input") f.BoolVar(&opts.SuppressSecrets, "suppress-secrets", false, "suppress secrets in the output. highly recommended to specify on CI/CD use-cases") diff --git a/cmd/sync.go b/cmd/sync.go index 26efa3ea..d406f9d5 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -74,6 +74,7 @@ func NewSyncCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.BoolVar(&syncOptions.NoHooks, "no-hooks", false, "do not diff changes made by hooks (interactive preview only)") f.BoolVar(&syncOptions.SuppressDiff, "suppress-diff", false, "suppress diff in the output (interactive preview only). Usable in new installs") f.BoolVar(&syncOptions.SkipDiffOnInstall, "skip-diff-on-install", false, "Skips running helm-diff on releases being newly installed on this sync (interactive preview only). Useful when the release manifests are too huge to be reviewed, or it's too time-consuming to diff at all") + f.BoolVar(&syncOptions.SkipDiffValidationOnInstall, "skip-diff-validation-on-install", false, "Disables K8s API validation (--disable-validation) when running helm-diff on releases being newly installed (interactive preview only). Useful when charts include CRDs and CRs in the same release") f.BoolVar(&syncOptions.IncludeTests, "include-tests", false, "enable the diffing of the helm test hooks (interactive preview only)") f.BoolVar(&syncOptions.DetailedExitcode, "detailed-exitcode", false, "return a non-zero exit code 2 instead of 0 when releases are synced (use --interactive to also see a diff preview)") f.BoolVar(&syncOptions.StripTrailingCR, "strip-trailing-cr", false, "strip trailing carriage return on input (interactive preview only)") diff --git a/docs/cli.md b/docs/cli.md index 3a942826..88bed36e 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -161,6 +161,11 @@ To supply the diff functionality Helmfile needs the [helm-diff](https://github.c you should be able to simply execute `helm plugin install https://github.com/databus23/helm-diff`. For more details please look at their [documentation](https://github.com/databus23/helm-diff#helm-diff-plugin). +#### Notable diff flags + +* `--skip-diff-on-install` — skip running `helm diff` entirely for releases that are not yet installed. The release is treated as changed and will be synced on `apply` without showing a diff. +* `--skip-diff-validation-on-install` — for releases that are not yet installed, pass `--disable-validation` to `helm diff` so the diff is shown without K8s API server validation. Useful when a chart bundles CRDs and CRs together: the CRs would fail API validation before the CRDs are installed. This is the CLI-flag equivalent of the per-release `disableValidationOnInstall` field. + ### doctor `helmfile doctor` runs `helmfile diff` and asks an OpenAI-compatible LLM to summarize the changes and flag risks diff --git a/docs/configuration.md b/docs/configuration.md index cabb7a14..7532e734 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -265,6 +265,7 @@ releases: # passes --disable-validation to helm diff plugin, this requires diff plugin >= 3.1.2 # It is useful when any release contains custom resources for CRDs that is not yet installed onto the cluster. # https://github.com/roboll/helmfile/pull/1618 + # To apply this to all releases without editing each one, use the --skip-diff-validation-on-install CLI flag. disableValidationOnInstall: false # passes --disable-openapi-validation to helm diff plugin, this requires diff plugin >= 3.1.2 # It may be helpful to deploy charts with helm api v1 CRDS diff --git a/pkg/app/app.go b/pkg/app/app.go index 3a87446d..f756f61d 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1834,24 +1834,25 @@ func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, bool, []error) { detectedKubeVersion := a.detectKubeVersion(st) diffOpts := &state.DiffOpts{ - Color: c.Color(), - NoColor: c.NoColor(), - Context: c.Context(), - Output: c.DiffOutput(), - Set: c.Set(), - SkipCleanup: c.SkipCleanup(), - SkipDiffOnInstall: c.SkipDiffOnInstall(), - ReuseValues: c.ReuseValues(), - ResetValues: c.ResetValues(), - DiffArgs: c.DiffArgs(), - TemplateArgs: c.TemplateArgs(), - PostRenderer: c.PostRenderer(), - PostRendererArgs: c.PostRendererArgs(), - SkipSchemaValidation: c.SkipSchemaValidation(), - SuppressOutputLineRegex: c.SuppressOutputLineRegex(), - TakeOwnership: c.TakeOwnership(), - ServerSide: c.ServerSide(), - DetectedKubeVersion: detectedKubeVersion, + Color: c.Color(), + NoColor: c.NoColor(), + Context: c.Context(), + Output: c.DiffOutput(), + Set: c.Set(), + SkipCleanup: c.SkipCleanup(), + SkipDiffOnInstall: c.SkipDiffOnInstall(), + SkipDiffValidationOnInstall: c.SkipDiffValidationOnInstall(), + ReuseValues: c.ReuseValues(), + ResetValues: c.ResetValues(), + DiffArgs: c.DiffArgs(), + TemplateArgs: c.TemplateArgs(), + PostRenderer: c.PostRenderer(), + PostRendererArgs: c.PostRendererArgs(), + SkipSchemaValidation: c.SkipSchemaValidation(), + SuppressOutputLineRegex: c.SuppressOutputLineRegex(), + TakeOwnership: c.TakeOwnership(), + ServerSide: c.ServerSide(), + DetectedKubeVersion: detectedKubeVersion, } infoMsg, releasesToUpdate, releasesToDelete, diffErrs := r.diff(false, detailedExitCode, c, diffOpts) @@ -2122,23 +2123,24 @@ func (a *App) diff(r *Run, c DiffConfigProvider) (*string, bool, bool, []error) detectedKubeVersion := a.detectKubeVersion(st) opts := &state.DiffOpts{ - Context: c.Context(), - Output: c.DiffOutput(), - Color: c.Color(), - NoColor: c.NoColor(), - Set: c.Set(), - DiffArgs: c.DiffArgs(), - TemplateArgs: c.TemplateArgs(), - SkipDiffOnInstall: c.SkipDiffOnInstall(), - ReuseValues: c.ReuseValues(), - ResetValues: c.ResetValues(), - PostRenderer: c.PostRenderer(), - PostRendererArgs: c.PostRendererArgs(), - SkipSchemaValidation: c.SkipSchemaValidation(), - SuppressOutputLineRegex: c.SuppressOutputLineRegex(), - TakeOwnership: c.TakeOwnership(), - ServerSide: c.ServerSide(), - DetectedKubeVersion: detectedKubeVersion, + Context: c.Context(), + Output: c.DiffOutput(), + Color: c.Color(), + NoColor: c.NoColor(), + Set: c.Set(), + DiffArgs: c.DiffArgs(), + TemplateArgs: c.TemplateArgs(), + SkipDiffOnInstall: c.SkipDiffOnInstall(), + SkipDiffValidationOnInstall: c.SkipDiffValidationOnInstall(), + ReuseValues: c.ReuseValues(), + ResetValues: c.ResetValues(), + PostRenderer: c.PostRenderer(), + PostRendererArgs: c.PostRendererArgs(), + SkipSchemaValidation: c.SkipSchemaValidation(), + SuppressOutputLineRegex: c.SuppressOutputLineRegex(), + TakeOwnership: c.TakeOwnership(), + ServerSide: c.ServerSide(), + DetectedKubeVersion: detectedKubeVersion, } filtered := &Run{ @@ -2363,23 +2365,24 @@ func (a *App) SyncState(r *Run, c SyncConfigProvider) (bool, bool, []error) { if diffC, ok := c.(DiffConfigProvider); ok { detectedKubeVersion := a.detectKubeVersion(st) diffOpts := &state.DiffOpts{ - Context: diffC.Context(), - Output: diffC.DiffOutput(), - Color: diffC.Color(), - NoColor: diffC.NoColor(), - Set: diffC.Set(), - DiffArgs: diffC.DiffArgs(), - TemplateArgs: diffC.TemplateArgs(), - SkipDiffOnInstall: diffC.SkipDiffOnInstall(), - ReuseValues: diffC.ReuseValues(), - ResetValues: diffC.ResetValues(), - PostRenderer: diffC.PostRenderer(), - PostRendererArgs: diffC.PostRendererArgs(), - SkipSchemaValidation: diffC.SkipSchemaValidation(), - SuppressOutputLineRegex: diffC.SuppressOutputLineRegex(), - TakeOwnership: diffC.TakeOwnership(), - ServerSide: diffC.ServerSide(), - DetectedKubeVersion: detectedKubeVersion, + Context: diffC.Context(), + Output: diffC.DiffOutput(), + Color: diffC.Color(), + NoColor: diffC.NoColor(), + Set: diffC.Set(), + DiffArgs: diffC.DiffArgs(), + TemplateArgs: diffC.TemplateArgs(), + SkipDiffOnInstall: diffC.SkipDiffOnInstall(), + SkipDiffValidationOnInstall: diffC.SkipDiffValidationOnInstall(), + ReuseValues: diffC.ReuseValues(), + ResetValues: diffC.ResetValues(), + PostRenderer: diffC.PostRenderer(), + PostRendererArgs: diffC.PostRendererArgs(), + SkipSchemaValidation: diffC.SkipSchemaValidation(), + SuppressOutputLineRegex: diffC.SuppressOutputLineRegex(), + TakeOwnership: diffC.TakeOwnership(), + ServerSide: diffC.ServerSide(), + DetectedKubeVersion: detectedKubeVersion, } infoMsgPtr, _, _, diffErrs := r.diff(false, diffC.DetailedExitcode(), diffC, diffOpts) if len(diffErrs) > 0 { diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index 5b4b7e8a..19233d9d 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -2697,6 +2697,10 @@ func (a applyConfig) SkipDiffOnInstall() bool { return a.skipDiffOnInstall } +func (a applyConfig) SkipDiffValidationOnInstall() bool { + return false +} + func (a applyConfig) SyncArgs() string { return a.syncArgs } diff --git a/pkg/app/config.go b/pkg/app/config.go index 0938dc70..02e4996c 100644 --- a/pkg/app/config.go +++ b/pkg/app/config.go @@ -86,6 +86,7 @@ type ApplyConfigProvider interface { Validate() bool SkipCleanup() bool SkipDiffOnInstall() bool + SkipDiffValidationOnInstall() bool DiffArgs() string SyncArgs() string @@ -182,6 +183,7 @@ type DiffConfigProvider interface { NoHooks() bool SuppressDiff() bool SkipDiffOnInstall() bool + SkipDiffValidationOnInstall() bool DiffArgs() string TemplateArgs() string diff --git a/pkg/app/diff_test.go b/pkg/app/diff_test.go index 2d9028f9..2b6a9204 100644 --- a/pkg/app/diff_test.go +++ b/pkg/app/diff_test.go @@ -159,6 +159,10 @@ func (a diffConfig) SkipDiffOnInstall() bool { return a.skipDiffOnInstall } +func (a diffConfig) SkipDiffValidationOnInstall() bool { + return false +} + func (a diffConfig) Logger() *zap.SugaredLogger { return a.logger } diff --git a/pkg/config/apply.go b/pkg/config/apply.go index 37712867..4b4431cf 100644 --- a/pkg/config/apply.go +++ b/pkg/config/apply.go @@ -37,6 +37,8 @@ type ApplyOptions struct { EnforceNeedsAreInstalled bool // SkipDiffOnInstall is true if the diff should be skipped on install SkipDiffOnInstall bool + // SkipDiffValidationOnInstall disables K8s API validation when running helm-diff on a release being newly installed + SkipDiffValidationOnInstall bool // DiffArgs is the list of arguments to pass to the helm-diff. DiffArgs string // IncludeTests is true if the tests should be included @@ -199,6 +201,11 @@ func (a *ApplyImpl) SkipDiffOnInstall() bool { return a.ApplyOptions.SkipDiffOnInstall } +// SkipDiffValidationOnInstall returns the skip diff validation on install. +func (a *ApplyImpl) SkipDiffValidationOnInstall() bool { + return a.ApplyOptions.SkipDiffValidationOnInstall +} + // DiffArgs is the list of arguments to pass to helm-diff. func (a *ApplyImpl) DiffArgs() string { return a.ApplyOptions.DiffArgs diff --git a/pkg/config/diff.go b/pkg/config/diff.go index e58b8822..66a382c4 100644 --- a/pkg/config/diff.go +++ b/pkg/config/diff.go @@ -22,6 +22,8 @@ type DiffOptions struct { EnforceNeedsAreInstalled bool // SkipDiffOnInstall is the skip diff on install flag SkipDiffOnInstall bool + // SkipDiffValidationOnInstall disables K8s API validation when running helm-diff on a release being newly installed + SkipDiffValidationOnInstall bool // ShowSecrets is the show secrets flag ShowSecrets bool // NoHooks skips hooks during diff @@ -168,6 +170,11 @@ func (t *DiffImpl) SkipDiffOnInstall() bool { return t.DiffOptions.SkipDiffOnInstall } +// SkipDiffValidationOnInstall returns the skip diff validation on install +func (t *DiffImpl) SkipDiffValidationOnInstall() bool { + return t.DiffOptions.SkipDiffValidationOnInstall +} + // DiffArgs returns the list of arguments to pass to helm-diff. func (t *DiffImpl) DiffArgs() string { return t.DiffOptions.DiffArgs diff --git a/pkg/config/sync.go b/pkg/config/sync.go index daea269f..83962269 100644 --- a/pkg/config/sync.go +++ b/pkg/config/sync.go @@ -75,19 +75,20 @@ type SyncOptions struct { TemplateArgs string // Diff-related options for --interactive mode - SuppressOutputLineRegex []string - IncludeTests bool - Suppress []string - SuppressSecrets bool - ShowSecrets bool - NoHooks bool - SuppressDiff bool - SkipDiffOnInstall bool - DiffArgs string - DetailedExitcode bool - StripTrailingCR bool - Context int - DiffOutput string + SuppressOutputLineRegex []string + IncludeTests bool + Suppress []string + SuppressSecrets bool + ShowSecrets bool + NoHooks bool + SuppressDiff bool + SkipDiffOnInstall bool + SkipDiffValidationOnInstall bool + DiffArgs string + DetailedExitcode bool + StripTrailingCR bool + Context int + DiffOutput string } // NewSyncOptions creates a new Apply @@ -308,6 +309,11 @@ func (t *SyncImpl) SkipDiffOnInstall() bool { return t.SyncOptions.SkipDiffOnInstall } +// SkipDiffValidationOnInstall returns the SkipDiffValidationOnInstall. +func (t *SyncImpl) SkipDiffValidationOnInstall() bool { + return t.SyncOptions.SkipDiffValidationOnInstall +} + // DiffArgs returns the DiffArgs. func (t *SyncImpl) DiffArgs() string { return t.SyncOptions.DiffArgs diff --git a/pkg/state/state.go b/pkg/state/state.go index e055d67d..e00f5cd9 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -2990,7 +2990,7 @@ func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValu } var disableValidation bool - if release.DisableValidationOnInstall != nil && *release.DisableValidationOnInstall { + if (release.DisableValidationOnInstall != nil && *release.DisableValidationOnInstall) || opt.SkipDiffValidationOnInstall { installed, err := isInstalled(release) if err != nil { errs = append(errs, err) @@ -3090,11 +3090,12 @@ type DiffOpts struct { Color bool // NoColor forces disabling the color output on helm-diff. // If this is true, Color has no effect. - NoColor bool - Set []string - SkipCleanup bool - SkipDiffOnInstall bool - DiffArgs string + NoColor bool + Set []string + SkipCleanup bool + SkipDiffOnInstall bool + SkipDiffValidationOnInstall bool + DiffArgs string // TemplateArgs are extra args appended to the helm template/diff rendering // (e.g. "--dry-run=server" to enable the helm lookup function during `helmfile // apply`/`diff`, which render via helm-diff). See issue #1833. diff --git a/pkg/state/state_test.go b/pkg/state/state_test.go index 552f1b8c..dc8d6b42 100644 --- a/pkg/state/state_test.go +++ b/pkg/state/state_test.go @@ -4268,6 +4268,87 @@ func TestDiffpareSyncReleases(t *testing.T) { } } +func TestPrepareDiffReleases_SkipDiffValidationOnInstall(t *testing.T) { + installedListOutput := "NAME\tNAMESPACE\tREVISION\tSTATUS\nfoo\tdefault\t1\tdeployed" + listFlags := "--uninstalling --deployed --failed --pending" + + tests := []struct { + name string + skipDiffValidationOnInstall bool + perReleaseDisableValidation bool + installed bool + wantDisableValidation bool + }{ + { + name: "flag-set-not-installed", + skipDiffValidationOnInstall: true, + installed: false, + wantDisableValidation: true, + }, + { + name: "flag-set-already-installed", + skipDiffValidationOnInstall: true, + installed: true, + wantDisableValidation: false, + }, + { + name: "flag-not-set-not-installed", + skipDiffValidationOnInstall: false, + installed: false, + wantDisableValidation: false, + }, + { + name: "per-release-flag-not-installed", + perReleaseDisableValidation: true, + installed: false, + wantDisableValidation: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + release := ReleaseSpec{ + Name: "foo", + Chart: "stable/foo", + } + if tt.perReleaseDisableValidation { + release.DisableValidationOnInstall = boolValue(true) + } + st := &HelmState{ + ReleaseSetSpec: ReleaseSetSpec{ + Releases: []ReleaseSpec{release}, + HelmDefaults: HelmSpec{}, + }, + logger: logger, + valsRuntime: valsRuntime, + } + lists := map[exectest.ListKey]string{} + if tt.installed { + lists[exectest.ListKey{Filter: "^foo$", Flags: listFlags}] = installedListOutput + } + helm := &exectest.Helm{Lists: lists} + + results, errs := st.prepareDiffReleases(helm, []string{}, 1, false, false, false, []string{}, false, false, false, &DiffOpts{ + SkipDiffValidationOnInstall: tt.skipDiffValidationOnInstall, + }) + + require.Len(t, errs, 0) + require.Len(t, results, 1) + + flags := results[0].flags + hasDisableValidation := false + for _, f := range flags { + if f == "--disable-validation" { + hasDisableValidation = true + break + } + } + require.Equal(t, tt.wantDisableValidation, hasDisableValidation, + "--disable-validation presence mismatch for case %q; flags: %v", tt.name, flags) + }) + } +} + func TestPrepareSyncReleases(t *testing.T) { tests := []struct { name string