mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-17 19:03:06 +02:00
Various U/X improvements for helmfile apply (#586)
* Various U/X improvements for `helmfile apply` This improves the U/X of `helmfile apply`, by allowing you to selectively apply sub-helmfiles. When you have two or more sub-helmfiles processed, typing `n` to cancel the first doesn't automatically stop the whole helmfile execution. Instead, it proceeds by diffing the next sub-helmfile, and asks you to apply it, which should be what the user would expect. To support this workflow, I have suppressed useless exec logs, correct exit status when diff exists in sub-helmfiles but not in the parent helmfile, and made the final error message emitted by helmfile better. More concretely, this moves more output from `helm` to STDERR and the `debug` log-level. The overall output from `helmfile` should be a bit more cleaner especially for `apply`, `sync`, `diff` and perhaps other `helmfile` sub-commands, too. For example, when one of release failed, `helmfile`'s final error message now includes the error message from the failed `helm` execution, like seen in the last line: ``` List of updated releases : RELEASE CHART VERSION envoy stable/envoy 1.5.0 List of releases in error : RELEASE envoy2 in ./helmfile.yaml: in .helmfiles[0]: in /Users/c-ykuoka/helmfile/helmfile.1.yaml: failed processing release envoy2: helm exited with status 1: Error: UPGRADE FAILED: "envoy2" has no deployed releases ``` This way you can better understand what caused helmfile to finally fail. `helmfile` has been streaminig a lot of stdout and stderr contents from the `helm` commands regardless of the helmfile's log-level. It has been suppressed by default and moved to the `debug` log-level. You will see that it helps you focus on what was the cause of a failure. While working on the above, I found an another bug that made `--detailed-exitcode` useless in some case. That is, `helmfile diff --detailed-exitcode`, when any diff existed only in sub-helmfiles, has been returning an exit code of `1`. It should return `2` when any release had diff and no release had an error, regardless of the target is a sub-helmfile or a parent helmfile. Why? Because that's what `--detailed-exitcode` meant for! After this PR gets merged, `helmfile diff --detailed-exitcode` propery return exit code `2` in such cases. Fixes #543 Resolves #540
This commit is contained in:
+92
-14
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
|
||||
"github.com/roboll/helmfile/helmexec"
|
||||
"github.com/roboll/helmfile/state"
|
||||
@@ -83,7 +84,7 @@ func (a *App) within(dir string, do func() error) error {
|
||||
func (a *App) visitStateFiles(fileOrDir string, do func(string) error) error {
|
||||
desiredStateFiles, err := a.findDesiredStateFiles(fileOrDir)
|
||||
if err != nil {
|
||||
return err
|
||||
return appError("", err)
|
||||
}
|
||||
|
||||
for _, relPath := range desiredStateFiles {
|
||||
@@ -103,7 +104,7 @@ func (a *App) visitStateFiles(fileOrDir string, do func(string) error) error {
|
||||
return do(file)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return appError(fmt.Sprintf("in %s/%s", dir, file), err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +140,8 @@ func (a *App) VisitDesiredStates(fileOrDir string, selector []string, converge f
|
||||
a.Env,
|
||||
)
|
||||
|
||||
ctx := context{a, st}
|
||||
|
||||
helm := helmexec.New(a.Logger, a.KubeContext)
|
||||
|
||||
if err != nil {
|
||||
@@ -149,17 +152,17 @@ func (a *App) VisitDesiredStates(fileOrDir string, selector []string, converge f
|
||||
case *state.UndefinedEnvError:
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
return ctx.wrapErrs(err)
|
||||
}
|
||||
default:
|
||||
return err
|
||||
return ctx.wrapErrs(err)
|
||||
}
|
||||
}
|
||||
st.Selectors = selector
|
||||
|
||||
if len(st.Helmfiles) > 0 {
|
||||
noMatchInSubHelmfiles := true
|
||||
for _, m := range st.Helmfiles {
|
||||
for i, m := range st.Helmfiles {
|
||||
//assign parent selector to sub helm selector in legacy mode or do not inherit in experimental mode
|
||||
if (m.Selectors == nil && !isExplicitSelectorInheritanceEnabled()) || m.SelectorsInherited {
|
||||
m.Selectors = selector
|
||||
@@ -169,7 +172,7 @@ func (a *App) VisitDesiredStates(fileOrDir string, selector []string, converge f
|
||||
case *NoMatchingHelmfileError:
|
||||
|
||||
default:
|
||||
return fmt.Errorf("failed processing %s: %v", m.Path, err)
|
||||
return appError(fmt.Sprintf("in .helmfiles[%d]", i), err)
|
||||
}
|
||||
} else {
|
||||
noMatchInSubHelmfiles = false
|
||||
@@ -180,11 +183,11 @@ func (a *App) VisitDesiredStates(fileOrDir string, selector []string, converge f
|
||||
|
||||
templated, tmplErr := st.ExecuteTemplates()
|
||||
if tmplErr != nil {
|
||||
return fmt.Errorf("failed executing release templates in \"%s\": %v", f, tmplErr)
|
||||
return appError(fmt.Sprintf("failed executing release templates in \"%s\"", f), tmplErr)
|
||||
}
|
||||
processed, errs := converge(templated, helm)
|
||||
noMatchInHelmfiles = noMatchInHelmfiles && !processed
|
||||
return clean(templated, errs)
|
||||
return context{a, templated}.clean(errs)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -369,7 +372,7 @@ func (a *App) loadDesiredStateFromYaml(yaml []byte, file string, namespace strin
|
||||
sig := <-sigs
|
||||
|
||||
errs := []error{fmt.Errorf("Received [%s] to shutdown ", sig)}
|
||||
_ = clean(st, errs)
|
||||
_ = context{a, st}.clean(errs)
|
||||
// See http://tldp.org/LDP/abs/html/exitcodes.html
|
||||
switch sig {
|
||||
case syscall.SIGINT:
|
||||
@@ -382,26 +385,101 @@ func (a *App) loadDesiredStateFromYaml(yaml []byte, file string, namespace strin
|
||||
return st, nil
|
||||
}
|
||||
|
||||
func clean(st *state.HelmState, errs []error) error {
|
||||
type Error struct {
|
||||
msg string
|
||||
|
||||
Errors []error
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
var cause string
|
||||
if e.Errors == nil {
|
||||
return e.msg
|
||||
}
|
||||
if len(e.Errors) == 1 {
|
||||
if e.Errors[0] == nil {
|
||||
panic(fmt.Sprintf("[bug] assertion error: unexpected state: e.Errors: %v", e.Errors))
|
||||
}
|
||||
cause = e.Errors[0].Error()
|
||||
} else {
|
||||
msgs := []string{}
|
||||
for i, err := range e.Errors {
|
||||
msgs = append(msgs, fmt.Sprintf("err %d: %v", i, err.Error()))
|
||||
}
|
||||
cause = fmt.Sprintf("%d errors:\n%s", len(e.Errors), strings.Join(msgs, "\n"))
|
||||
}
|
||||
msg := ""
|
||||
if e.msg != "" {
|
||||
msg = fmt.Sprintf("%s: %s", e.msg, cause)
|
||||
} else {
|
||||
msg = cause
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func (e *Error) Code() int {
|
||||
allDiff := false
|
||||
anyNonZero := false
|
||||
for _, err := range e.Errors {
|
||||
switch ee := err.(type) {
|
||||
case *state.ReleaseError:
|
||||
if anyNonZero {
|
||||
allDiff = allDiff && ee.Code == 2
|
||||
} else {
|
||||
allDiff = ee.Code == 2
|
||||
}
|
||||
case *Error:
|
||||
if anyNonZero {
|
||||
allDiff = allDiff && ee.Code() == 2
|
||||
} else {
|
||||
allDiff = ee.Code() == 2
|
||||
}
|
||||
}
|
||||
anyNonZero = true
|
||||
}
|
||||
|
||||
if anyNonZero {
|
||||
if allDiff {
|
||||
return 2
|
||||
}
|
||||
return 1
|
||||
}
|
||||
panic(fmt.Sprintf("[bug] assertion error: unexpected state: unable to handle errors: %v", e.Errors))
|
||||
}
|
||||
|
||||
func appError(msg string, err error) error {
|
||||
return &Error{msg, []error{err}}
|
||||
}
|
||||
|
||||
func (c context) clean(errs []error) error {
|
||||
if errs == nil {
|
||||
errs = []error{}
|
||||
}
|
||||
|
||||
cleanErrs := st.Clean()
|
||||
cleanErrs := c.st.Clean()
|
||||
if cleanErrs != nil {
|
||||
errs = append(errs, cleanErrs...)
|
||||
}
|
||||
|
||||
return c.wrapErrs(errs...)
|
||||
}
|
||||
|
||||
type context struct {
|
||||
app *App
|
||||
st *state.HelmState
|
||||
}
|
||||
|
||||
func (c context) wrapErrs(errs ...error) error {
|
||||
if errs != nil && len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
switch e := err.(type) {
|
||||
case *state.ReleaseError:
|
||||
fmt.Fprintf(os.Stderr, "err: release \"%s\" in \"%s\" failed: %v\n", e.Name, st.FilePath, e)
|
||||
c.app.Logger.Debugf("err: release \"%s\" in \"%s\" failed: %v", e.Name, c.st.FilePath, e)
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "err: %v\n", e)
|
||||
c.app.Logger.Debugf("err: %v", e)
|
||||
}
|
||||
}
|
||||
return errs[0]
|
||||
return &Error{Errors: errs}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+4
-4
@@ -282,11 +282,11 @@ releases:
|
||||
errMsg string
|
||||
}{
|
||||
{label: "name=prometheus", expectedCount: 1, expectErr: false},
|
||||
{label: "name=", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/a1.yaml: Malformed label: name=. Expected label in form k=v or k!=v"},
|
||||
{label: "name!=", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/a1.yaml: Malformed label: name!=. Expected label in form k=v or k!=v"},
|
||||
{label: "name", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/a1.yaml: Malformed label: name. Expected label in form k=v or k!=v"},
|
||||
{label: "name=", expectedCount: 0, expectErr: true, errMsg: "in ./helmfile.yaml: in .helmfiles[0]: in /path/to/helmfile.d/a1.yaml: Malformed label: name=. Expected label in form k=v or k!=v"},
|
||||
{label: "name!=", expectedCount: 0, expectErr: true, errMsg: "in ./helmfile.yaml: in .helmfiles[0]: in /path/to/helmfile.d/a1.yaml: Malformed label: name!=. Expected label in form k=v or k!=v"},
|
||||
{label: "name", expectedCount: 0, expectErr: true, errMsg: "in ./helmfile.yaml: in .helmfiles[0]: in /path/to/helmfile.d/a1.yaml: Malformed label: name. Expected label in form k=v or k!=v"},
|
||||
// See https://github.com/roboll/helmfile/issues/193
|
||||
{label: "duplicated=yes", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/b.yaml: duplicate release \"foo\" found in \"zoo\": there were 2 releases named \"foo\" matching specified selector"},
|
||||
{label: "duplicated=yes", expectedCount: 0, expectErr: true, errMsg: "in ./helmfile.yaml: in .helmfiles[2]: in /path/to/helmfile.d/b.yaml: duplicate release \"foo\" found in \"zoo\": there were 2 releases named \"foo\" matching specified selector"},
|
||||
{label: "duplicatedOK=yes", expectedCount: 2, expectErr: false},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
. "gotest.tools/assert"
|
||||
"gotest.tools/assert"
|
||||
is "gotest.tools/assert/cmp"
|
||||
"gotest.tools/env"
|
||||
"os"
|
||||
@@ -10,18 +10,18 @@ import (
|
||||
|
||||
func TestIsExplicitSelectorInheritanceEnabled(t *testing.T) {
|
||||
//env var ExperimentalEnvVar is set
|
||||
Assert(t, is.Equal(os.Getenv(ExperimentalEnvVar), ""))
|
||||
Check(t, !isExplicitSelectorInheritanceEnabled())
|
||||
assert.Assert(t, is.Equal(os.Getenv(ExperimentalEnvVar), ""))
|
||||
assert.Check(t, !isExplicitSelectorInheritanceEnabled())
|
||||
|
||||
//check for env var ExperimentalEnvVar set to true
|
||||
defer env.Patch(t, ExperimentalEnvVar, "true")()
|
||||
Check(t, isExplicitSelectorInheritanceEnabled())
|
||||
assert.Check(t, isExplicitSelectorInheritanceEnabled())
|
||||
|
||||
//check for env var ExperimentalEnvVar set to anything
|
||||
defer env.Patch(t, ExperimentalEnvVar, "foo")()
|
||||
Check(t, !isExplicitSelectorInheritanceEnabled())
|
||||
assert.Check(t, !isExplicitSelectorInheritanceEnabled())
|
||||
|
||||
//check for env var ExperimentalEnvVar set to ExperimentalSelectorExplicit
|
||||
defer env.Patch(t, ExperimentalEnvVar, ExperimentalSelectorExplicit)()
|
||||
Check(t, isExplicitSelectorInheritanceEnabled())
|
||||
assert.Check(t, isExplicitSelectorInheritanceEnabled())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user