diff --git a/cmd/apply.go b/cmd/apply.go index 6b5a23f4..c47ce83d 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -47,6 +47,7 @@ func NewApplyCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.BoolVar(&applyOptions.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed on sync. By default, CRDs are installed if not already present") f.BoolVar(&applyOptions.SkipNeeds, "skip-needs", true, `do not automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when --selector/-l flag is not provided. Defaults to true when --include-needs or --include-transitive-needs is not provided`) f.BoolVar(&applyOptions.IncludeNeeds, "include-needs", false, `automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when --selector/-l flag is not provided`) + 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.IncludeTests, "include-tests", false, "enable the diffing of the helm test hooks") diff --git a/cmd/diff.go b/cmd/diff.go index c2238704..32267d3e 100644 --- a/cmd/diff.go +++ b/cmd/diff.go @@ -40,6 +40,7 @@ func NewDiffCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.BoolVar(&diffOptions.SkipNeeds, "skip-needs", true, `do not automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when --selector/-l flag is not provided. Defaults to true when --include-needs or --include-transitive-needs is not provided`) f.BoolVar(&diffOptions.IncludeTests, "include-tests", false, "enable the diffing of the helm test hooks") f.BoolVar(&diffOptions.IncludeNeeds, "include-needs", false, `automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when --selector/-l flag is not provided`) + f.BoolVar(&diffOptions.EnforceNeedsAreInstalled, "enforce-needs-are-installed", false, "enforce that all 'needs' dependencies are installable before applying changes") f.BoolVar(&diffOptions.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(&diffOptions.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(&diffOptions.ShowSecrets, "show-secrets", false, "do not redact secret values in the output. should be used for debug purpose only") diff --git a/cmd/sync.go b/cmd/sync.go index 3bd80e6f..6cc31975 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -41,6 +41,7 @@ func NewSyncCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.BoolVar(&syncOptions.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed on sync. By default, CRDs are installed if not already present") f.BoolVar(&syncOptions.IncludeNeeds, "include-needs", false, `automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when --selector/-l flag is not provided`) f.BoolVar(&syncOptions.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(&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.BoolVar(&syncOptions.SyncReleaseLabels, "sync-release-labels", false, "sync release labels to the target release") diff --git a/cmd/template.go b/cmd/template.go index 6cb81fea..8713f5b3 100644 --- a/cmd/template.go +++ b/cmd/template.go @@ -43,6 +43,7 @@ func NewTemplateCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.BoolVar(&templateOptions.SkipNeeds, "skip-needs", true, `do not automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when --selector/-l flag is not provided. Defaults to true when --include-needs or --include-transitive-needs is not provided`) f.BoolVar(&templateOptions.IncludeNeeds, "include-needs", false, `automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when --selector/-l flag is not provided`) f.BoolVar(&templateOptions.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(&templateOptions.EnforceNeedsAreInstalled, "enforce-needs-are-installed", false, "enforce that all 'needs' dependencies are installable before applying changes") f.BoolVar(&templateOptions.SkipCleanup, "skip-cleanup", false, "Stop cleaning up temporary values generated by helmfile and helm-secrets. Useful for debugging. Don't use in production for security") f.BoolVar(&templateOptions.NoHooks, "no-hooks", false, "do not template files made by hooks.") f.StringVar(&templateOptions.PostRenderer, "post-renderer", "", `pass --post-renderer to "helm template" or "helm upgrade --install"`) diff --git a/pkg/app/app.go b/pkg/app/app.go index 47717ecc..9ef25db9 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -2144,13 +2144,19 @@ func (a *App) withNeeds(r *Run, c DAGConfig, includeDisabled bool, f func(*state includeNeeds = true } - batches, err := st.PlanReleases(state.PlanOptions{Reverse: false, SelectedReleases: selectedReleases, IncludeNeeds: includeNeeds, IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(), SkipNeeds: c.SkipNeeds()}) + batches, err := st.PlanReleases(state.PlanOptions{ + Reverse: false, + SelectedReleases: selectedReleases, + IncludeNeeds: includeNeeds, + IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(), + SkipNeeds: c.SkipNeeds(), + }) + if err != nil { return false, []error{err} } var selectedReleasesWithNeeds []state.ReleaseSpec - for _, rs := range batches { for _, r := range rs { selectedReleasesWithNeeds = append(selectedReleasesWithNeeds, r.ReleaseSpec) @@ -2185,6 +2191,15 @@ func (a *App) withNeeds(r *Run, c DAGConfig, includeDisabled bool, f func(*state } } + if c.EnforceNeedsAreInstalled() { + for _, r := range toRender { + visited := make(map[string]bool) + if depErr := st.HasTransitiveDependencyWithInstalledFalse(r, visited); depErr != nil { + return false, []error{fmt.Errorf("Release %s has a transitive dependency %s marked as installed=false", depErr.Release.Name, depErr.Dependency.Name)} + } + } + } + if includeDisabled { for _, d := range releasesToUninstall { rels = append(rels, d) diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index d73f8b9e..f60af7ae 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -2195,11 +2195,12 @@ type configImpl struct { skipSchemaValidation bool skipRefresh bool - skipNeeds bool - includeNeeds bool - includeTransitiveNeeds bool - skipCharts bool - kubeVersion string + skipNeeds bool + includeNeeds bool + includeTransitiveNeeds bool + enforceNeedsAreInstalled bool + skipCharts bool + kubeVersion string } func (c configImpl) Selectors() []string { @@ -2306,51 +2307,56 @@ func (c configImpl) ShowOnly() []string { return nil } +func (c configImpl) EnforceNeedsAreInstalled() bool { + return c.enforceNeedsAreInstalled +} + type applyConfig struct { args string cascade string values []string - set []string - validate bool - skipCleanup bool - skipCRDs bool - skipDeps bool - skipRefresh bool - skipNeeds bool - includeNeeds bool - includeTransitiveNeeds bool - includeTests bool - suppress []string - suppressSecrets bool - showSecrets bool - noHooks bool - suppressDiff bool - noColor bool - color bool - context int - diffOutput string - concurrency int - detailedExitcode bool - stripTrailingCR bool - interactive bool - skipDiffOnInstall bool - syncArgs string - diffArgs string - logger *zap.SugaredLogger - wait bool - waitRetries int - waitForJobs bool - reuseValues bool - postRenderer string - postRendererArgs []string - skipSchemaValidation bool - kubeVersion string - suppressOutputLineRegex []string - showOnly []string - hideNotes bool - takeOwnership bool - syncReleaseLabels bool + set []string + validate bool + skipCleanup bool + skipCRDs bool + skipDeps bool + skipRefresh bool + skipNeeds bool + includeNeeds bool + includeTransitiveNeeds bool + includeTests bool + suppress []string + suppressSecrets bool + showSecrets bool + noHooks bool + suppressDiff bool + noColor bool + color bool + context int + diffOutput string + concurrency int + detailedExitcode bool + stripTrailingCR bool + interactive bool + skipDiffOnInstall bool + syncArgs string + diffArgs string + logger *zap.SugaredLogger + wait bool + waitRetries int + waitForJobs bool + reuseValues bool + postRenderer string + postRendererArgs []string + skipSchemaValidation bool + kubeVersion string + suppressOutputLineRegex []string + showOnly []string + hideNotes bool + takeOwnership bool + syncReleaseLabels bool + enforceNeedsAreInstalled bool // template-only options includeCRDs, skipTests bool @@ -2409,6 +2415,10 @@ func (a applyConfig) SkipNeeds() bool { return a.skipNeeds } +func (a applyConfig) EnforceNeedsAreInstalled() bool { + return a.enforceNeedsAreInstalled +} + func (a applyConfig) IncludeNeeds() bool { return a.includeNeeds || a.IncludeTransitiveNeeds() } diff --git a/pkg/app/config.go b/pkg/app/config.go index e699d86f..f7aabf85 100644 --- a/pkg/app/config.go +++ b/pkg/app/config.go @@ -242,6 +242,7 @@ type DAGConfig interface { SkipNeeds() bool IncludeNeeds() bool IncludeTransitiveNeeds() bool + EnforceNeedsAreInstalled() bool } type WriteValuesConfigProvider interface { diff --git a/pkg/app/diff_test.go b/pkg/app/diff_test.go index 18dde7fc..70b68d69 100644 --- a/pkg/app/diff_test.go +++ b/pkg/app/diff_test.go @@ -16,37 +16,38 @@ import ( ) type diffConfig struct { - args string - diffArgs string - values []string - retainValuesFiles bool - set []string - validate bool - skipCRDs bool - skipDeps bool - skipRefresh bool - includeTests bool - skipNeeds bool - includeNeeds bool - includeTransitiveNeeds bool - suppress []string - suppressSecrets bool - showSecrets bool - noHooks bool - suppressDiff bool - suppressOutputLineRegex []string - noColor bool - context int - diffOutput string - concurrency int - detailedExitcode bool - stripTrailingCR bool - interactive bool - skipDiffOnInstall bool - skipSchemaValidation bool - reuseValues bool - logger *zap.SugaredLogger - takeOwnership bool + args string + diffArgs string + values []string + retainValuesFiles bool + set []string + validate bool + skipCRDs bool + skipDeps bool + skipRefresh bool + includeTests bool + skipNeeds bool + includeNeeds bool + includeTransitiveNeeds bool + suppress []string + suppressSecrets bool + showSecrets bool + noHooks bool + suppressDiff bool + suppressOutputLineRegex []string + noColor bool + context int + diffOutput string + concurrency int + detailedExitcode bool + stripTrailingCR bool + interactive bool + skipDiffOnInstall bool + skipSchemaValidation bool + reuseValues bool + logger *zap.SugaredLogger + takeOwnership bool + enforceNeedsAreInstalled bool } func (a diffConfig) Args() string { @@ -188,6 +189,10 @@ func (a diffConfig) TakeOwnership() bool { return a.takeOwnership } +func (a diffConfig) EnforceNeedsAreInstalled() bool { + return a.enforceNeedsAreInstalled +} + func TestDiff(t *testing.T) { type flags struct { skipNeeds bool diff --git a/pkg/config/apply.go b/pkg/config/apply.go index 81a9a195..cfdd3451 100644 --- a/pkg/config/apply.go +++ b/pkg/config/apply.go @@ -28,6 +28,8 @@ type ApplyOptions struct { IncludeNeeds bool // IncludeTransitiveNeeds is true if the transitive needs should be included IncludeTransitiveNeeds bool + // EnforceNeedsAreInstalled is true if we should error if/when there are unmeetable dependencies + EnforceNeedsAreInstalled bool // SkipDiffOnInstall is true if the diff should be skipped on install SkipDiffOnInstall bool // DiffArgs is the list of arguments to pass to the helm-diff. @@ -139,6 +141,11 @@ func (a *ApplyImpl) IncludeTransitiveNeeds() bool { return a.ApplyOptions.IncludeTransitiveNeeds } +// EnforceNeedsAreInstalled is true we should error if/when there are unmeetable dependencies +func (a *ApplyImpl) EnforceNeedsAreInstalled() bool { + return a.ApplyOptions.EnforceNeedsAreInstalled +} + // ShowSecrets returns the show secrets. func (a *ApplyImpl) ShowSecrets() bool { return a.ApplyOptions.ShowSecrets diff --git a/pkg/config/diff.go b/pkg/config/diff.go index d8830fad..153d87fb 100644 --- a/pkg/config/diff.go +++ b/pkg/config/diff.go @@ -18,6 +18,8 @@ type DiffOptions struct { IncludeNeeds bool // IncludeTransitiveNeeds is the include transitive needs flag IncludeTransitiveNeeds bool + // EnforceNeedsAreInstalled is the enforce needs are installed flag + EnforceNeedsAreInstalled bool // SkipDiffOnInstall is the skip diff on install flag SkipDiffOnInstall bool // ShowSecrets is the show secrets flag @@ -87,6 +89,11 @@ func (t *DiffImpl) IncludeTransitiveNeeds() bool { return t.DiffOptions.IncludeTransitiveNeeds } +// EnforceNeedsAreInstalled errors if the transitive dependencies are not installable +func (t *DiffImpl) EnforceNeedsAreInstalled() bool { + return t.DiffOptions.EnforceNeedsAreInstalled +} + // Set returns the Set func (t *DiffImpl) Set() []string { return t.DiffOptions.Set diff --git a/pkg/config/lint.go b/pkg/config/lint.go index 46cf02f8..be4de771 100644 --- a/pkg/config/lint.go +++ b/pkg/config/lint.go @@ -14,6 +14,8 @@ type LintOptions struct { IncludeNeeds bool // IncludeTransitiveNeeds is the include transitive needs flag IncludeTransitiveNeeds bool + // EnforceNeedsAreInstalled is the enforce needs are installed flag + EnforceNeedsAreInstalled bool // SkipDeps is the skip deps flag } @@ -66,6 +68,11 @@ func (l *LintImpl) IncludeTransitiveNeeds() bool { return l.LintOptions.IncludeTransitiveNeeds } +// EnforceNeedsAreInstalled errors if the transitive dependencies are not installable +func (l *LintImpl) EnforceNeedsAreInstalled() bool { + return l.LintOptions.EnforceNeedsAreInstalled +} + // SkipNeeds returns the skip needs func (l *LintImpl) SkipNeeds() bool { if !l.IncludeNeeds() { diff --git a/pkg/config/sync.go b/pkg/config/sync.go index 8d68aba8..b59701f1 100644 --- a/pkg/config/sync.go +++ b/pkg/config/sync.go @@ -16,6 +16,8 @@ type SyncOptions struct { IncludeNeeds bool // IncludeTransitiveNeeds is the include transitive needs flag IncludeTransitiveNeeds bool + // EnforceNeedsAreInstalled indicates whether to error when there are unmeetable dependencies + EnforceNeedsAreInstalled bool // SkipCrds is the skip crds flag SkipCRDs bool // Wait is the wait flag @@ -82,6 +84,11 @@ func (t *SyncImpl) IncludeTransitiveNeeds() bool { return t.SyncOptions.IncludeTransitiveNeeds } +// EnforceNeedsAreInstalled errors if the transitive dependencies are not installable +func (t *SyncImpl) EnforceNeedsAreInstalled() bool { + return t.SyncOptions.EnforceNeedsAreInstalled +} + // Set returns the Set func (t *SyncImpl) Set() []string { return t.SyncOptions.Set diff --git a/pkg/config/template.go b/pkg/config/template.go index 90c82691..90e87ba3 100644 --- a/pkg/config/template.go +++ b/pkg/config/template.go @@ -30,6 +30,8 @@ type TemplateOptions struct { IncludeNeeds bool // IncludeTransitiveNeeds is the include transitive needs flag IncludeTransitiveNeeds bool + // EnforceNeedsAreInstalled indicates whether to error when there are unmeetable dependencies + EnforceNeedsAreInstalled bool // No-Hooks is the no hooks flag NoHooks bool // SkipCleanup is the skip cleanup flag @@ -158,3 +160,8 @@ func (t *TemplateImpl) KubeVersion() string { func (t *TemplateImpl) ShowOnly() []string { return t.TemplateOptions.ShowOnly } + +// EnforceNeedsAreInstalled errors if the transitive dependencies are not installable +func (t *TemplateImpl) EnforceNeedsAreInstalled() bool { + return t.TemplateOptions.EnforceNeedsAreInstalled +} diff --git a/pkg/state/state.go b/pkg/state/state.go index 03f22ed4..511a33f0 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -2738,6 +2738,34 @@ func (st *HelmState) UpdateDeps(helm helmexec.Interface, includeTransitiveNeeds return nil } +// DependencyError holds information about a release and its dependency that is not installed. +type DependencyError struct { + Release ReleaseSpec + Dependency ReleaseSpec +} + +// hasTransitiveDependencyWithInstallFalse checks if a release has a transitive dependency with Install set to false. +func (st *HelmState) HasTransitiveDependencyWithInstalledFalse(release ReleaseSpec, visited map[string]bool) *DependencyError { + if visited[release.Name] { + return nil + } + visited[release.Name] = true + + for _, dep := range release.Needs { + for _, r := range st.Releases { + if r.Name == dep { + if r.Installed != nil && !*r.Installed { + return &DependencyError{Release: release, Dependency: r} + } + if depErr := st.HasTransitiveDependencyWithInstalledFalse(r, visited); depErr != nil { + return depErr + } + } + } + } + return nil +} + // find "Chart.yaml" func findChartDirectory(topLevelDir string) (string, error) { var files []string