feat: `helmfile diff --detailed-exitcode` should also detect deletions (#1186)
Resolves #499 Resolves #1072
This commit is contained in:
parent
98886df5d2
commit
870cc03c70
|
|
@ -116,12 +116,20 @@ func (a *App) DeprecatedSyncCharts(c DeprecatedChartsConfigProvider) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Diff(c DiffConfigProvider) error {
|
func (a *App) Diff(c DiffConfigProvider) error {
|
||||||
var deferredErrs []error
|
var allDiffDetectedErrs []error
|
||||||
|
|
||||||
err := a.ForEachStateFiltered(func(run *Run) []error {
|
var affectedAny bool
|
||||||
|
|
||||||
|
err := a.ForEachState(func(run *Run) (bool, []error) {
|
||||||
var criticalErrs []error
|
var criticalErrs []error
|
||||||
|
|
||||||
errs := run.Diff(c)
|
msg, matched, affected, errs := run.Diff(c)
|
||||||
|
|
||||||
|
if msg != nil {
|
||||||
|
a.Logger.Info(*msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
affectedAny = affectedAny || affected
|
||||||
|
|
||||||
for i := range errs {
|
for i := range errs {
|
||||||
switch e := errs[i].(type) {
|
switch e := errs[i].(type) {
|
||||||
|
|
@ -129,7 +137,7 @@ func (a *App) Diff(c DiffConfigProvider) error {
|
||||||
switch e.Code {
|
switch e.Code {
|
||||||
case 2:
|
case 2:
|
||||||
// See https://github.com/roboll/helmfile/issues/874
|
// See https://github.com/roboll/helmfile/issues/874
|
||||||
deferredErrs = append(deferredErrs, e)
|
allDiffDetectedErrs = append(allDiffDetectedErrs, e)
|
||||||
default:
|
default:
|
||||||
criticalErrs = append(criticalErrs, e)
|
criticalErrs = append(criticalErrs, e)
|
||||||
}
|
}
|
||||||
|
|
@ -138,14 +146,14 @@ func (a *App) Diff(c DiffConfigProvider) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return criticalErrs
|
return matched, criticalErrs
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(deferredErrs) > 0 {
|
if c.DetailedExitcode() && (len(allDiffDetectedErrs) > 0 || affectedAny) {
|
||||||
// We take the first release error w/ exit status 2 (although all the defered errs should have exit status 2)
|
// We take the first release error w/ exit status 2 (although all the defered errs should have exit status 2)
|
||||||
// to just let helmfile itself to exit with 2
|
// to just let helmfile itself to exit with 2
|
||||||
// See https://github.com/roboll/helmfile/issues/749
|
// See https://github.com/roboll/helmfile/issues/749
|
||||||
|
|
@ -806,84 +814,28 @@ func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, bool, []error) {
|
||||||
Set: c.Set(),
|
Set: c.Set(),
|
||||||
}
|
}
|
||||||
|
|
||||||
var changedReleases []state.ReleaseSpec
|
infoMsg, releasesToBeUpdated, releasesToBeDeleted, errs := r.diff(false, detailedExitCode, c, diffOpts)
|
||||||
var deletingReleases []state.ReleaseSpec
|
if len(errs) > 0 {
|
||||||
var planningErrs []error
|
return false, false, errs
|
||||||
|
}
|
||||||
|
|
||||||
// TODO Better way to detect diff on only filtered releases
|
if releasesToBeDeleted == nil && releasesToBeUpdated == nil {
|
||||||
{
|
if infoMsg != nil {
|
||||||
changedReleases, planningErrs = st.DiffReleases(helm, c.Values(), c.Concurrency(), detailedExitCode, c.IncludeTests(), c.SuppressSecrets(), c.SuppressDiff(), false, diffOpts)
|
logger := c.Logger()
|
||||||
|
logger.Infof("")
|
||||||
var err error
|
logger.Infof(*infoMsg)
|
||||||
deletingReleases, err = st.DetectReleasesToBeDeletedForSync(helm, st.Releases)
|
|
||||||
if err != nil {
|
|
||||||
planningErrs = append(planningErrs, err)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fatalErrs := []error{}
|
|
||||||
|
|
||||||
for _, e := range planningErrs {
|
|
||||||
switch err := e.(type) {
|
|
||||||
case *state.ReleaseError:
|
|
||||||
if err.Code != 2 {
|
|
||||||
fatalErrs = append(fatalErrs, e)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
fatalErrs = append(fatalErrs, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(fatalErrs) > 0 {
|
|
||||||
return false, false, fatalErrs
|
|
||||||
}
|
|
||||||
|
|
||||||
releasesToBeDeleted := map[string]state.ReleaseSpec{}
|
|
||||||
for _, r := range deletingReleases {
|
|
||||||
id := state.ReleaseToID(&r)
|
|
||||||
releasesToBeDeleted[id] = r
|
|
||||||
}
|
|
||||||
|
|
||||||
releasesToBeUpdated := map[string]state.ReleaseSpec{}
|
|
||||||
for _, r := range changedReleases {
|
|
||||||
id := state.ReleaseToID(&r)
|
|
||||||
|
|
||||||
// If `helm-diff` detected changes but it is not being `helm delete`ed, we should run `helm upgrade`
|
|
||||||
if _, ok := releasesToBeDeleted[id]; !ok {
|
|
||||||
releasesToBeUpdated[id] = r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sync only when there are changes
|
|
||||||
if len(releasesToBeUpdated) == 0 && len(releasesToBeDeleted) == 0 {
|
|
||||||
// TODO better way to get the logger
|
|
||||||
logger := c.Logger()
|
|
||||||
logger.Infof("")
|
|
||||||
logger.Infof("No affected releases")
|
|
||||||
return true, false, nil
|
return true, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
names := []string{}
|
|
||||||
for _, r := range releasesToBeUpdated {
|
|
||||||
names = append(names, fmt.Sprintf(" %s (%s) UPDATED", r.Name, r.Chart))
|
|
||||||
}
|
|
||||||
for _, r := range releasesToBeDeleted {
|
|
||||||
names = append(names, fmt.Sprintf(" %s (%s) DELETED", r.Name, r.Chart))
|
|
||||||
}
|
|
||||||
// Make the output deterministic for testing purpose
|
|
||||||
sort.Strings(names)
|
|
||||||
|
|
||||||
infoMsg := fmt.Sprintf(`Affected releases are:
|
|
||||||
%s
|
|
||||||
`, strings.Join(names, "\n"))
|
|
||||||
confMsg := fmt.Sprintf(`%s
|
confMsg := fmt.Sprintf(`%s
|
||||||
Do you really want to apply?
|
Do you really want to apply?
|
||||||
Helmfile will apply all your changes, as shown above.
|
Helmfile will apply all your changes, as shown above.
|
||||||
|
|
||||||
`, infoMsg)
|
`, *infoMsg)
|
||||||
interactive := c.Interactive()
|
interactive := c.Interactive()
|
||||||
if !interactive {
|
if !interactive {
|
||||||
a.Logger.Debug(infoMsg)
|
a.Logger.Debug(*infoMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
syncErrs := []error{}
|
syncErrs := []error{}
|
||||||
|
|
|
||||||
|
|
@ -2524,6 +2524,8 @@ second-pass rendering result of "helmfile.yaml.part.0":
|
||||||
54:
|
54:
|
||||||
|
|
||||||
merged environment: &{default map[] map[]}
|
merged environment: &{default map[] map[]}
|
||||||
|
10 release(s) found in helmfile.yaml
|
||||||
|
|
||||||
worker 1/1 started
|
worker 1/1 started
|
||||||
worker 1/1 finished
|
worker 1/1 finished
|
||||||
worker 1/1 started
|
worker 1/1 started
|
||||||
|
|
@ -2711,6 +2713,8 @@ second-pass rendering result of "helmfile.yaml.part.0":
|
||||||
10:
|
10:
|
||||||
|
|
||||||
merged environment: &{default map[] map[]}
|
merged environment: &{default map[] map[]}
|
||||||
|
3 release(s) found in helmfile.yaml
|
||||||
|
|
||||||
worker 1/1 started
|
worker 1/1 started
|
||||||
worker 1/1 finished
|
worker 1/1 finished
|
||||||
worker 1/1 started
|
worker 1/1 started
|
||||||
|
|
@ -2989,6 +2993,8 @@ second-pass rendering result of "helmfile.yaml.part.0":
|
||||||
12:
|
12:
|
||||||
|
|
||||||
merged environment: &{default map[] map[]}
|
merged environment: &{default map[] map[]}
|
||||||
|
2 release(s) found in helmfile.yaml
|
||||||
|
|
||||||
worker 1/1 started
|
worker 1/1 started
|
||||||
worker 1/1 finished
|
worker 1/1 finished
|
||||||
worker 1/1 started
|
worker 1/1 started
|
||||||
|
|
@ -3335,6 +3341,8 @@ second-pass rendering result of "helmfile.yaml.part.0":
|
||||||
23:
|
23:
|
||||||
|
|
||||||
merged environment: &{default map[] map[]}
|
merged environment: &{default map[] map[]}
|
||||||
|
2 release(s) matching app=test found in helmfile.yaml
|
||||||
|
|
||||||
worker 1/1 started
|
worker 1/1 started
|
||||||
worker 1/1 finished
|
worker 1/1 finished
|
||||||
worker 1/1 started
|
worker 1/1 started
|
||||||
|
|
@ -3473,6 +3481,8 @@ second-pass rendering result of "helmfile.yaml.part.0":
|
||||||
23:
|
23:
|
||||||
|
|
||||||
merged environment: &{default map[] map[]}
|
merged environment: &{default map[] map[]}
|
||||||
|
0 release(s) matching app=test_non_existent found in helmfile.yaml
|
||||||
|
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
//
|
//
|
||||||
|
|
@ -3535,6 +3545,8 @@ second-pass rendering result of "helmfile.yaml.part.0":
|
||||||
9:
|
9:
|
||||||
|
|
||||||
merged environment: &{default map[] map[]}
|
merged environment: &{default map[] map[]}
|
||||||
|
2 release(s) found in helmfile.yaml
|
||||||
|
|
||||||
worker 1/1 started
|
worker 1/1 started
|
||||||
worker 1/1 finished
|
worker 1/1 finished
|
||||||
worker 1/1 started
|
worker 1/1 started
|
||||||
|
|
|
||||||
|
|
@ -284,6 +284,8 @@ second-pass rendering result of "helmfile.yaml.part.0":
|
||||||
54:
|
54:
|
||||||
|
|
||||||
merged environment: &{default map[] map[]}
|
merged environment: &{default map[] map[]}
|
||||||
|
10 release(s) found in helmfile.yaml
|
||||||
|
|
||||||
processing 5 groups of releases in this order:
|
processing 5 groups of releases in this order:
|
||||||
GROUP RELEASES
|
GROUP RELEASES
|
||||||
1 frontend-v3, frontend-v2, frontend-v1
|
1 frontend-v3, frontend-v2, frontend-v1
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
129
pkg/app/run.go
129
pkg/app/run.go
|
|
@ -1,9 +1,12 @@
|
||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/roboll/helmfile/pkg/argparser"
|
"github.com/roboll/helmfile/pkg/argparser"
|
||||||
"github.com/roboll/helmfile/pkg/helmexec"
|
"github.com/roboll/helmfile/pkg/helmexec"
|
||||||
"github.com/roboll/helmfile/pkg/state"
|
"github.com/roboll/helmfile/pkg/state"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Run struct {
|
type Run struct {
|
||||||
|
|
@ -61,21 +64,36 @@ func (r *Run) Status(c StatusesConfigProvider) []error {
|
||||||
return r.state.ReleaseStatuses(r.helm, workers)
|
return r.state.ReleaseStatuses(r.helm, workers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Run) Diff(c DiffConfigProvider) []error {
|
func (r *Run) Diff(c DiffConfigProvider) (*string, bool, bool, []error) {
|
||||||
st := r.state
|
st := r.state
|
||||||
helm := r.helm
|
helm := r.helm
|
||||||
ctx := r.ctx
|
ctx := r.ctx
|
||||||
|
|
||||||
|
allReleases := st.GetReleasesWithOverrides()
|
||||||
|
|
||||||
|
toDiff, err := st.GetSelectedReleasesWithOverrides()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, false, []error{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(toDiff) == 0 {
|
||||||
|
return nil, false, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do build deps and prepare only on selected releases so that we won't waste time
|
||||||
|
// on running various helm commands on unnecessary releases
|
||||||
|
st.Releases = toDiff
|
||||||
|
|
||||||
if !c.SkipDeps() {
|
if !c.SkipDeps() {
|
||||||
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
|
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
|
||||||
return errs
|
return nil, false, false, errs
|
||||||
}
|
}
|
||||||
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
|
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
|
||||||
return errs
|
return nil, false, false, errs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errs := st.PrepareReleases(helm, "diff"); errs != nil && len(errs) > 0 {
|
if errs := st.PrepareReleases(helm, "diff"); errs != nil && len(errs) > 0 {
|
||||||
return errs
|
return nil, false, false, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
||||||
|
|
@ -85,9 +103,28 @@ func (r *Run) Diff(c DiffConfigProvider) []error {
|
||||||
NoColor: c.NoColor(),
|
NoColor: c.NoColor(),
|
||||||
Set: c.Set(),
|
Set: c.Set(),
|
||||||
}
|
}
|
||||||
_, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), c.DetailedExitcode(), c.IncludeTests(), c.SuppressSecrets(), c.SuppressDiff(), true, opts)
|
|
||||||
|
|
||||||
return errs
|
// Validate all releases for missing `needs` targets
|
||||||
|
st.Releases = allReleases
|
||||||
|
|
||||||
|
if _, err := st.PlanReleases(false); err != nil {
|
||||||
|
return nil, false, false, []error{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diff only targeted releases
|
||||||
|
|
||||||
|
st.Releases = toDiff
|
||||||
|
|
||||||
|
filtered := &Run{
|
||||||
|
state: st,
|
||||||
|
helm: r.helm,
|
||||||
|
ctx: r.ctx,
|
||||||
|
Ask: r.Ask,
|
||||||
|
}
|
||||||
|
|
||||||
|
infoMsg, updated, deleted, errs := filtered.diff(true, c.DetailedExitcode(), c, opts)
|
||||||
|
|
||||||
|
return infoMsg, true, len(deleted) > 0 || len(updated) > 0, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Run) Test(c TestConfigProvider) []error {
|
func (r *Run) Test(c TestConfigProvider) []error {
|
||||||
|
|
@ -124,3 +161,83 @@ func (r *Run) Lint(c LintConfigProvider) []error {
|
||||||
}
|
}
|
||||||
return st.LintReleases(helm, values, args, workers, opts)
|
return st.LintReleases(helm, values, args, workers, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (run *Run) diff(triggerCleanupEvent bool, detailedExitCode bool, c DiffConfigProvider, diffOpts *state.DiffOpts) (*string, map[string]state.ReleaseSpec, map[string]state.ReleaseSpec, []error) {
|
||||||
|
st := run.state
|
||||||
|
helm := run.helm
|
||||||
|
|
||||||
|
var changedReleases []state.ReleaseSpec
|
||||||
|
var deletingReleases []state.ReleaseSpec
|
||||||
|
var planningErrs []error
|
||||||
|
|
||||||
|
// TODO Better way to detect diff on only filtered releases
|
||||||
|
{
|
||||||
|
changedReleases, planningErrs = st.DiffReleases(helm, c.Values(), c.Concurrency(), detailedExitCode, c.IncludeTests(), c.SuppressSecrets(), c.SuppressDiff(), triggerCleanupEvent, diffOpts)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
deletingReleases, err = st.DetectReleasesToBeDeletedForSync(helm, st.Releases)
|
||||||
|
if err != nil {
|
||||||
|
planningErrs = append(planningErrs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fatalErrs := []error{}
|
||||||
|
|
||||||
|
for _, e := range planningErrs {
|
||||||
|
switch err := e.(type) {
|
||||||
|
case *state.ReleaseError:
|
||||||
|
if err.Code != 2 {
|
||||||
|
fatalErrs = append(fatalErrs, e)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
fatalErrs = append(fatalErrs, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fatalErrs) > 0 {
|
||||||
|
return nil, nil, nil, fatalErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
releasesToBeDeleted := map[string]state.ReleaseSpec{}
|
||||||
|
for _, r := range deletingReleases {
|
||||||
|
id := state.ReleaseToID(&r)
|
||||||
|
releasesToBeDeleted[id] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
releasesToBeUpdated := map[string]state.ReleaseSpec{}
|
||||||
|
for _, r := range changedReleases {
|
||||||
|
id := state.ReleaseToID(&r)
|
||||||
|
|
||||||
|
// If `helm-diff` detected changes but it is not being `helm delete`ed, we should run `helm upgrade`
|
||||||
|
if _, ok := releasesToBeDeleted[id]; !ok {
|
||||||
|
releasesToBeUpdated[id] = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync only when there are changes
|
||||||
|
if len(releasesToBeUpdated) == 0 && len(releasesToBeDeleted) == 0 {
|
||||||
|
var msg *string
|
||||||
|
if c.DetailedExitcode() {
|
||||||
|
// TODO better way to get the logger
|
||||||
|
m := "No affected releases"
|
||||||
|
msg = &m
|
||||||
|
}
|
||||||
|
return msg, nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
names := []string{}
|
||||||
|
for _, r := range releasesToBeUpdated {
|
||||||
|
names = append(names, fmt.Sprintf(" %s (%s) UPDATED", r.Name, r.Chart))
|
||||||
|
}
|
||||||
|
for _, r := range releasesToBeDeleted {
|
||||||
|
names = append(names, fmt.Sprintf(" %s (%s) DELETED", r.Name, r.Chart))
|
||||||
|
}
|
||||||
|
// Make the output deterministic for testing purpose
|
||||||
|
sort.Strings(names)
|
||||||
|
|
||||||
|
infoMsg := fmt.Sprintf(`Affected releases are:
|
||||||
|
%s
|
||||||
|
`, strings.Join(names, "\n"))
|
||||||
|
|
||||||
|
return &infoMsg, releasesToBeUpdated, releasesToBeDeleted, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1327,6 +1327,15 @@ func (st *HelmState) GetSelectedReleasesWithOverrides() ([]ReleaseSpec, error) {
|
||||||
releases = append(releases, r.ReleaseSpec)
|
releases = append(releases, r.ReleaseSpec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var extra string
|
||||||
|
|
||||||
|
if len(st.Selectors) > 0 {
|
||||||
|
extra = " matching " + strings.Join(st.Selectors, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
st.logger.Debugf("%d release(s)%s found in %s\n", len(releases), extra, st.FilePath)
|
||||||
|
|
||||||
return releases, nil
|
return releases, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1337,7 +1346,6 @@ func (st *HelmState) FilterReleases() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
st.Releases = releases
|
st.Releases = releases
|
||||||
st.logger.Debugf("%d release(s) matching %s found in %s\n", len(releases), strings.Join(st.Selectors, ","), st.FilePath)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue