feat: add --skip-diff-validation-on-install CLI flag (#2728)

Signed-off-by: Richter <h.richter@sap.com>
This commit is contained in:
henrichter-sap
2026-08-03 21:11:45 +08:00
committed by GitHub
parent 1341fd8659
commit 7cf4ff9a25
14 changed files with 195 additions and 71 deletions
+1
View File
@@ -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")
+1
View File
@@ -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")
+1
View File
@@ -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)")
+5
View File
@@ -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
+1
View File
@@ -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
+55 -52
View File
@@ -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 {
+4
View File
@@ -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
}
+2
View File
@@ -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
+4
View File
@@ -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
}
+7
View File
@@ -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
+7
View File
@@ -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
+19 -13
View File
@@ -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
+7 -6
View File
@@ -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.
+81
View File
@@ -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