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 aa73fbb2..85cbf0ea 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1979,19 +1979,34 @@ 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) } } + 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)} + } + } + } + var toRender []state.ReleaseSpec releasesToUninstall := map[string]state.ReleaseSpec{} diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index cf38aa2e..f91fe9b3 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -2104,11 +2104,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 { @@ -2318,6 +2319,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/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 d8e4013c..f5214d67 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -2657,6 +2657,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