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:
parent
bae842f234
commit
4f83e69bf6
12
cmd/cmd.go
12
cmd/cmd.go
|
|
@ -7,9 +7,7 @@ import (
|
||||||
"github.com/roboll/helmfile/state"
|
"github.com/roboll/helmfile/state"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"os/exec"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func VisitAllDesiredStates(c *cli.Context, converge func(*state.HelmState, helmexec.Interface, app.Context) (bool, []error)) error {
|
func VisitAllDesiredStates(c *cli.Context, converge func(*state.HelmState, helmexec.Interface, app.Context) (bool, []error)) error {
|
||||||
|
|
@ -86,14 +84,10 @@ func toCliError(err error) error {
|
||||||
switch e := err.(type) {
|
switch e := err.(type) {
|
||||||
case *app.NoMatchingHelmfileError:
|
case *app.NoMatchingHelmfileError:
|
||||||
return cli.NewExitError(e.Error(), 2)
|
return cli.NewExitError(e.Error(), 2)
|
||||||
case *exec.ExitError:
|
case *app.Error:
|
||||||
// Propagate any non-zero exit status from the external command like `helm` that is failed under the hood
|
return cli.NewExitError(e.Error(), e.Code())
|
||||||
status := e.Sys().(syscall.WaitStatus)
|
|
||||||
return cli.NewExitError(e.Error(), status.ExitStatus())
|
|
||||||
case *state.DiffError:
|
|
||||||
return cli.NewExitError(e.Error(), e.Code)
|
|
||||||
default:
|
default:
|
||||||
return cli.NewExitError(e.Error(), 1)
|
panic(fmt.Errorf("BUG: please file an github issue for this unhandled error: %T: %v", e, e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
@ -183,7 +182,26 @@ func (helm *execer) DiffRelease(context HelmContext, name, chart string, flags .
|
||||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||||
env := context.getTillerlessEnv()
|
env := context.getTillerlessEnv()
|
||||||
out, err := helm.exec(append(append(preArgs, "diff", "upgrade", "--allow-unreleased", name, chart), flags...), env)
|
out, err := helm.exec(append(append(preArgs, "diff", "upgrade", "--allow-unreleased", name, chart), flags...), env)
|
||||||
helm.write(out)
|
// Do our best to write STDOUT only when diff existed
|
||||||
|
// Unfortunately, this works only when you run helmfile with `--detailed-exitcode`
|
||||||
|
detailedExitcodeEnabled := false
|
||||||
|
for _, f := range flags {
|
||||||
|
if strings.Contains(f, "detailed-exitcode") {
|
||||||
|
detailedExitcodeEnabled = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if detailedExitcodeEnabled {
|
||||||
|
switch e := err.(type) {
|
||||||
|
case ExitError:
|
||||||
|
if e.ExitStatus() == 2 {
|
||||||
|
helm.write(out)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
helm.write(out)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,8 +245,11 @@ func (helm *execer) exec(args []string, env map[string]string) ([]byte, error) {
|
||||||
if helm.kubeContext != "" {
|
if helm.kubeContext != "" {
|
||||||
cmdargs = append(cmdargs, "--kube-context", helm.kubeContext)
|
cmdargs = append(cmdargs, "--kube-context", helm.kubeContext)
|
||||||
}
|
}
|
||||||
helm.logger.Debugf("exec: %s %s", helm.helmBinary, strings.Join(cmdargs, " "))
|
cmd := fmt.Sprintf("exec: %s %s", helm.helmBinary, strings.Join(cmdargs, " "))
|
||||||
return helm.runner.Execute(helm.helmBinary, cmdargs, env)
|
helm.logger.Debug(cmd)
|
||||||
|
bytes, err := helm.runner.Execute(helm.helmBinary, cmdargs, env)
|
||||||
|
helm.logger.Debugf("%s: %s", cmd, bytes)
|
||||||
|
return bytes, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (helm *execer) info(out []byte) {
|
func (helm *execer) info(out []byte) {
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ func Test_AddRepo(t *testing.T) {
|
||||||
helm.AddRepo("myRepo", "https://repo.example.com/", "cert.pem", "key.pem", "", "")
|
helm.AddRepo("myRepo", "https://repo.example.com/", "cert.pem", "key.pem", "", "")
|
||||||
expected := `Adding repo myRepo https://repo.example.com/
|
expected := `Adding repo myRepo https://repo.example.com/
|
||||||
exec: helm repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem --kube-context dev
|
exec: helm repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem --kube-context dev
|
||||||
|
exec: helm repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -87,6 +88,7 @@ exec: helm repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-
|
||||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "")
|
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "")
|
||||||
expected = `Adding repo myRepo https://repo.example.com/
|
expected = `Adding repo myRepo https://repo.example.com/
|
||||||
exec: helm repo add myRepo https://repo.example.com/ --kube-context dev
|
exec: helm repo add myRepo https://repo.example.com/ --kube-context dev
|
||||||
|
exec: helm repo add myRepo https://repo.example.com/ --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -96,6 +98,7 @@ exec: helm repo add myRepo https://repo.example.com/ --kube-context dev
|
||||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "example_user", "example_password")
|
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "example_user", "example_password")
|
||||||
expected = `Adding repo myRepo https://repo.example.com/
|
expected = `Adding repo myRepo https://repo.example.com/
|
||||||
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password --kube-context dev
|
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password --kube-context dev
|
||||||
|
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -109,6 +112,7 @@ func Test_UpdateRepo(t *testing.T) {
|
||||||
helm.UpdateRepo()
|
helm.UpdateRepo()
|
||||||
expected := `Updating repo
|
expected := `Updating repo
|
||||||
exec: helm repo update --kube-context dev
|
exec: helm repo update --kube-context dev
|
||||||
|
exec: helm repo update --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.UpdateRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.UpdateRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -122,6 +126,7 @@ func Test_SyncRelease(t *testing.T) {
|
||||||
helm.SyncRelease(HelmContext{}, "release", "chart", "--timeout 10", "--wait")
|
helm.SyncRelease(HelmContext{}, "release", "chart", "--timeout 10", "--wait")
|
||||||
expected := `Upgrading chart
|
expected := `Upgrading chart
|
||||||
exec: helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev
|
exec: helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev
|
||||||
|
exec: helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -131,6 +136,7 @@ exec: helm upgrade --install --reset-values release chart --timeout 10 --wait --
|
||||||
helm.SyncRelease(HelmContext{}, "release", "chart")
|
helm.SyncRelease(HelmContext{}, "release", "chart")
|
||||||
expected = `Upgrading chart
|
expected = `Upgrading chart
|
||||||
exec: helm upgrade --install --reset-values release chart --kube-context dev
|
exec: helm upgrade --install --reset-values release chart --kube-context dev
|
||||||
|
exec: helm upgrade --install --reset-values release chart --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -145,6 +151,7 @@ func Test_SyncReleaseTillerless(t *testing.T) {
|
||||||
"--timeout 10", "--wait")
|
"--timeout 10", "--wait")
|
||||||
expected := `Upgrading chart
|
expected := `Upgrading chart
|
||||||
exec: helm tiller run foo -- helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev
|
exec: helm tiller run foo -- helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev
|
||||||
|
exec: helm tiller run foo -- helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -158,6 +165,7 @@ func Test_UpdateDeps(t *testing.T) {
|
||||||
helm.UpdateDeps("./chart/foo")
|
helm.UpdateDeps("./chart/foo")
|
||||||
expected := `Updating dependency ./chart/foo
|
expected := `Updating dependency ./chart/foo
|
||||||
exec: helm dependency update ./chart/foo --kube-context dev
|
exec: helm dependency update ./chart/foo --kube-context dev
|
||||||
|
exec: helm dependency update ./chart/foo --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.UpdateDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.UpdateDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -168,6 +176,7 @@ exec: helm dependency update ./chart/foo --kube-context dev
|
||||||
helm.UpdateDeps("./chart/foo")
|
helm.UpdateDeps("./chart/foo")
|
||||||
expected = `Updating dependency ./chart/foo
|
expected = `Updating dependency ./chart/foo
|
||||||
exec: helm dependency update ./chart/foo --verify --kube-context dev
|
exec: helm dependency update ./chart/foo --verify --kube-context dev
|
||||||
|
exec: helm dependency update ./chart/foo --verify --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -181,6 +190,7 @@ func Test_BuildDeps(t *testing.T) {
|
||||||
helm.BuildDeps("./chart/foo")
|
helm.BuildDeps("./chart/foo")
|
||||||
expected := `Building dependency ./chart/foo
|
expected := `Building dependency ./chart/foo
|
||||||
exec: helm dependency build ./chart/foo --kube-context dev
|
exec: helm dependency build ./chart/foo --kube-context dev
|
||||||
|
exec: helm dependency build ./chart/foo --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -191,6 +201,7 @@ exec: helm dependency build ./chart/foo --kube-context dev
|
||||||
helm.BuildDeps("./chart/foo")
|
helm.BuildDeps("./chart/foo")
|
||||||
expected = `Building dependency ./chart/foo
|
expected = `Building dependency ./chart/foo
|
||||||
exec: helm dependency build ./chart/foo --verify --kube-context dev
|
exec: helm dependency build ./chart/foo --verify --kube-context dev
|
||||||
|
exec: helm dependency build ./chart/foo --verify --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -204,6 +215,7 @@ func Test_DecryptSecret(t *testing.T) {
|
||||||
helm.DecryptSecret(HelmContext{}, "secretName")
|
helm.DecryptSecret(HelmContext{}, "secretName")
|
||||||
expected := `Decrypting secret secretName
|
expected := `Decrypting secret secretName
|
||||||
exec: helm secrets dec secretName --kube-context dev
|
exec: helm secrets dec secretName --kube-context dev
|
||||||
|
exec: helm secrets dec secretName --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.DecryptSecret()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.DecryptSecret()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -217,6 +229,7 @@ func Test_DiffRelease(t *testing.T) {
|
||||||
helm.DiffRelease(HelmContext{}, "release", "chart", "--timeout 10", "--wait")
|
helm.DiffRelease(HelmContext{}, "release", "chart", "--timeout 10", "--wait")
|
||||||
expected := `Comparing release chart
|
expected := `Comparing release chart
|
||||||
exec: helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev
|
exec: helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev
|
||||||
|
exec: helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -226,6 +239,7 @@ exec: helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --k
|
||||||
helm.DiffRelease(HelmContext{}, "release", "chart")
|
helm.DiffRelease(HelmContext{}, "release", "chart")
|
||||||
expected = `Comparing release chart
|
expected = `Comparing release chart
|
||||||
exec: helm diff upgrade --allow-unreleased release chart --kube-context dev
|
exec: helm diff upgrade --allow-unreleased release chart --kube-context dev
|
||||||
|
exec: helm diff upgrade --allow-unreleased release chart --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -239,6 +253,7 @@ func Test_DiffReleaseTillerless(t *testing.T) {
|
||||||
helm.DiffRelease(HelmContext{Tillerless: true}, "release", "chart", "--timeout 10", "--wait")
|
helm.DiffRelease(HelmContext{Tillerless: true}, "release", "chart", "--timeout 10", "--wait")
|
||||||
expected := `Comparing release chart
|
expected := `Comparing release chart
|
||||||
exec: helm tiller run -- helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev
|
exec: helm tiller run -- helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev
|
||||||
|
exec: helm tiller run -- helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -252,6 +267,7 @@ func Test_DeleteRelease(t *testing.T) {
|
||||||
helm.DeleteRelease(HelmContext{}, "release")
|
helm.DeleteRelease(HelmContext{}, "release")
|
||||||
expected := `Deleting release
|
expected := `Deleting release
|
||||||
exec: helm delete release --kube-context dev
|
exec: helm delete release --kube-context dev
|
||||||
|
exec: helm delete release --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -264,6 +280,7 @@ func Test_DeleteRelease_Flags(t *testing.T) {
|
||||||
helm.DeleteRelease(HelmContext{}, "release", "--purge")
|
helm.DeleteRelease(HelmContext{}, "release", "--purge")
|
||||||
expected := `Deleting release
|
expected := `Deleting release
|
||||||
exec: helm delete release --purge --kube-context dev
|
exec: helm delete release --purge --kube-context dev
|
||||||
|
exec: helm delete release --purge --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -277,6 +294,7 @@ func Test_TestRelease(t *testing.T) {
|
||||||
helm.TestRelease(HelmContext{}, "release")
|
helm.TestRelease(HelmContext{}, "release")
|
||||||
expected := `Testing release
|
expected := `Testing release
|
||||||
exec: helm test release --kube-context dev
|
exec: helm test release --kube-context dev
|
||||||
|
exec: helm test release --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -289,6 +307,7 @@ func Test_TestRelease_Flags(t *testing.T) {
|
||||||
helm.TestRelease(HelmContext{}, "release", "--cleanup", "--timeout", "60")
|
helm.TestRelease(HelmContext{}, "release", "--cleanup", "--timeout", "60")
|
||||||
expected := `Testing release
|
expected := `Testing release
|
||||||
exec: helm test release --cleanup --timeout 60 --kube-context dev
|
exec: helm test release --cleanup --timeout 60 --kube-context dev
|
||||||
|
exec: helm test release --cleanup --timeout 60 --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -302,6 +321,7 @@ func Test_ReleaseStatus(t *testing.T) {
|
||||||
helm.ReleaseStatus(HelmContext{}, "myRelease")
|
helm.ReleaseStatus(HelmContext{}, "myRelease")
|
||||||
expected := `Getting status myRelease
|
expected := `Getting status myRelease
|
||||||
exec: helm status myRelease --kube-context dev
|
exec: helm status myRelease --kube-context dev
|
||||||
|
exec: helm status myRelease --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.ReleaseStatus()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.ReleaseStatus()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -314,7 +334,9 @@ func Test_exec(t *testing.T) {
|
||||||
helm := MockExecer(logger, "")
|
helm := MockExecer(logger, "")
|
||||||
env := map[string]string{}
|
env := map[string]string{}
|
||||||
helm.exec([]string{"version"}, env)
|
helm.exec([]string{"version"}, env)
|
||||||
expected := "exec: helm version\n"
|
expected := `exec: helm version
|
||||||
|
exec: helm version:
|
||||||
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
}
|
}
|
||||||
|
|
@ -328,14 +350,18 @@ func Test_exec(t *testing.T) {
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
helm = MockExecer(logger, "dev")
|
helm = MockExecer(logger, "dev")
|
||||||
helm.exec([]string{"diff", "release", "chart", "--timeout 10", "--wait"}, env)
|
helm.exec([]string{"diff", "release", "chart", "--timeout 10", "--wait"}, env)
|
||||||
expected = "exec: helm diff release chart --timeout 10 --wait --kube-context dev\n"
|
expected = `exec: helm diff release chart --timeout 10 --wait --kube-context dev
|
||||||
|
exec: helm diff release chart --timeout 10 --wait --kube-context dev:
|
||||||
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
helm.exec([]string{"version"}, env)
|
helm.exec([]string{"version"}, env)
|
||||||
expected = "exec: helm version --kube-context dev\n"
|
expected = `exec: helm version --kube-context dev
|
||||||
|
exec: helm version --kube-context dev:
|
||||||
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
}
|
}
|
||||||
|
|
@ -343,7 +369,9 @@ func Test_exec(t *testing.T) {
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
helm.SetExtraArgs("foo")
|
helm.SetExtraArgs("foo")
|
||||||
helm.exec([]string{"version"}, env)
|
helm.exec([]string{"version"}, env)
|
||||||
expected = "exec: helm version foo --kube-context dev\n"
|
expected = `exec: helm version foo --kube-context dev
|
||||||
|
exec: helm version foo --kube-context dev:
|
||||||
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
}
|
}
|
||||||
|
|
@ -352,7 +380,9 @@ func Test_exec(t *testing.T) {
|
||||||
helm = MockExecer(logger, "")
|
helm = MockExecer(logger, "")
|
||||||
helm.SetHelmBinary("overwritten")
|
helm.SetHelmBinary("overwritten")
|
||||||
helm.exec([]string{"version"}, env)
|
helm.exec([]string{"version"}, env)
|
||||||
expected = "exec: overwritten version\n"
|
expected = `exec: overwritten version
|
||||||
|
exec: overwritten version:
|
||||||
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
}
|
}
|
||||||
|
|
@ -365,6 +395,7 @@ func Test_Lint(t *testing.T) {
|
||||||
helm.Lint("path/to/chart", "--values", "file.yml")
|
helm.Lint("path/to/chart", "--values", "file.yml")
|
||||||
expected := `Linting path/to/chart
|
expected := `Linting path/to/chart
|
||||||
exec: helm lint path/to/chart --values file.yml --kube-context dev
|
exec: helm lint path/to/chart --values file.yml --kube-context dev
|
||||||
|
exec: helm lint path/to/chart --values file.yml --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.Lint()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.Lint()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -378,6 +409,7 @@ func Test_Fetch(t *testing.T) {
|
||||||
helm.Fetch("chart", "--version", "1.2.3", "--untar", "--untardir", "/tmp/dir")
|
helm.Fetch("chart", "--version", "1.2.3", "--untar", "--untardir", "/tmp/dir")
|
||||||
expected := `Fetching chart
|
expected := `Fetching chart
|
||||||
exec: helm fetch chart --version 1.2.3 --untar --untardir /tmp/dir --kube-context dev
|
exec: helm fetch chart --version 1.2.3 --untar --untardir /tmp/dir --kube-context dev
|
||||||
|
exec: helm fetch chart --version 1.2.3 --untar --untardir /tmp/dir --kube-context dev:
|
||||||
`
|
`
|
||||||
if buffer.String() != expected {
|
if buffer.String() != expected {
|
||||||
t.Errorf("helmexec.Lint()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
t.Errorf("helmexec.Lint()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||||
|
|
@ -387,6 +419,7 @@ exec: helm fetch chart --version 1.2.3 --untar --untardir /tmp/dir --kube-contex
|
||||||
var logLevelTests = map[string]string{
|
var logLevelTests = map[string]string{
|
||||||
"debug": `Adding repo myRepo https://repo.example.com/
|
"debug": `Adding repo myRepo https://repo.example.com/
|
||||||
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password
|
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password
|
||||||
|
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password:
|
||||||
`,
|
`,
|
||||||
"info": `Adding repo myRepo https://repo.example.com/
|
"info": `Adding repo myRepo https://repo.example.com/
|
||||||
`,
|
`,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package helmexec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newExitError(helmCmdPath string, exitStatus int, errorMessage string) ExitError {
|
||||||
|
return ExitError{
|
||||||
|
msg: fmt.Sprintf("%s exited with status %d:\n%s", filepath.Base(helmCmdPath), exitStatus, indent(strings.TrimSpace(errorMessage))),
|
||||||
|
exitStatus: exitStatus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func indent(text string) string {
|
||||||
|
lines := strings.Split(text, "\n")
|
||||||
|
for i := range lines {
|
||||||
|
lines[i] = " " + lines[i]
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExitError is created whenever your shell command exits with a non-zero exit status
|
||||||
|
type ExitError struct {
|
||||||
|
msg string
|
||||||
|
exitStatus int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ExitError) Error() string {
|
||||||
|
return e.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ExitError) ExitStatus() int {
|
||||||
|
return e.exitStatus
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -51,17 +52,22 @@ func combinedOutput(c *exec.Cmd, logger *zap.SugaredLogger) ([]byte, error) {
|
||||||
o := stdout.Bytes()
|
o := stdout.Bytes()
|
||||||
e := stderr.Bytes()
|
e := stderr.Bytes()
|
||||||
|
|
||||||
if len(e) > 0 {
|
|
||||||
logger.Debugf("%s\n", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TrimSpace is necessary, because otherwise helmfile prints the redundant new-lines after each error like:
|
// TrimSpace is necessary, because otherwise helmfile prints the redundant new-lines after each error like:
|
||||||
//
|
//
|
||||||
// err: release "envoy2" in "helmfile.yaml" failed: exit status 1: Error: could not find a ready tiller pod
|
// err: release "envoy2" in "helmfile.yaml" failed: exit status 1: Error: could not find a ready tiller pod
|
||||||
// <redundant new line!>
|
// <redundant new line!>
|
||||||
// err: release "envoy" in "helmfile.yaml" failed: exit status 1: Error: could not find a ready tiller pod
|
// err: release "envoy" in "helmfile.yaml" failed: exit status 1: Error: could not find a ready tiller pod
|
||||||
err = fmt.Errorf("%v: %s", err, strings.TrimSpace(string(e)))
|
switch ee := err.(type) {
|
||||||
|
case *exec.ExitError:
|
||||||
|
// Propagate any non-zero exit status from the external command, rather than throwing it away,
|
||||||
|
// so that helmfile could return its own exit code accordingly
|
||||||
|
waitStatus := ee.Sys().(syscall.WaitStatus)
|
||||||
|
exitStatus := waitStatus.ExitStatus()
|
||||||
|
err = newExitError(c.Path, exitStatus, string(e))
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unexpected error: %v", err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return o, err
|
return o, err
|
||||||
|
|
|
||||||
12
main.go
12
main.go
|
|
@ -359,13 +359,19 @@ func main() {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fatalErrs := []error{}
|
||||||
|
|
||||||
noError := true
|
noError := true
|
||||||
for _, e := range errs {
|
for _, e := range errs {
|
||||||
switch err := e.(type) {
|
switch err := e.(type) {
|
||||||
case *state.DiffError:
|
case *state.ReleaseError:
|
||||||
noError = noError && err.Code == 2
|
if err.Code != 2 {
|
||||||
|
noError = false
|
||||||
|
fatalErrs = append(fatalErrs, e)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
noError = false
|
noError = false
|
||||||
|
fatalErrs = append(fatalErrs, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -408,7 +414,7 @@ Do you really want to apply?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errs
|
return fatalErrs
|
||||||
})
|
})
|
||||||
affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger))
|
affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger))
|
||||||
return errs
|
return errs
|
||||||
|
|
|
||||||
106
pkg/app/app.go
106
pkg/app/app.go
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/roboll/helmfile/helmexec"
|
"github.com/roboll/helmfile/helmexec"
|
||||||
"github.com/roboll/helmfile/state"
|
"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 {
|
func (a *App) visitStateFiles(fileOrDir string, do func(string) error) error {
|
||||||
desiredStateFiles, err := a.findDesiredStateFiles(fileOrDir)
|
desiredStateFiles, err := a.findDesiredStateFiles(fileOrDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return appError("", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, relPath := range desiredStateFiles {
|
for _, relPath := range desiredStateFiles {
|
||||||
|
|
@ -103,7 +104,7 @@ func (a *App) visitStateFiles(fileOrDir string, do func(string) error) error {
|
||||||
return do(file)
|
return do(file)
|
||||||
})
|
})
|
||||||
if err != nil {
|
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,
|
a.Env,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ctx := context{a, st}
|
||||||
|
|
||||||
helm := helmexec.New(a.Logger, a.KubeContext)
|
helm := helmexec.New(a.Logger, a.KubeContext)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -149,17 +152,17 @@ func (a *App) VisitDesiredStates(fileOrDir string, selector []string, converge f
|
||||||
case *state.UndefinedEnvError:
|
case *state.UndefinedEnvError:
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return err
|
return ctx.wrapErrs(err)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return err
|
return ctx.wrapErrs(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
st.Selectors = selector
|
st.Selectors = selector
|
||||||
|
|
||||||
if len(st.Helmfiles) > 0 {
|
if len(st.Helmfiles) > 0 {
|
||||||
noMatchInSubHelmfiles := true
|
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
|
//assign parent selector to sub helm selector in legacy mode or do not inherit in experimental mode
|
||||||
if (m.Selectors == nil && !isExplicitSelectorInheritanceEnabled()) || m.SelectorsInherited {
|
if (m.Selectors == nil && !isExplicitSelectorInheritanceEnabled()) || m.SelectorsInherited {
|
||||||
m.Selectors = selector
|
m.Selectors = selector
|
||||||
|
|
@ -169,7 +172,7 @@ func (a *App) VisitDesiredStates(fileOrDir string, selector []string, converge f
|
||||||
case *NoMatchingHelmfileError:
|
case *NoMatchingHelmfileError:
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("failed processing %s: %v", m.Path, err)
|
return appError(fmt.Sprintf("in .helmfiles[%d]", i), err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
noMatchInSubHelmfiles = false
|
noMatchInSubHelmfiles = false
|
||||||
|
|
@ -180,11 +183,11 @@ func (a *App) VisitDesiredStates(fileOrDir string, selector []string, converge f
|
||||||
|
|
||||||
templated, tmplErr := st.ExecuteTemplates()
|
templated, tmplErr := st.ExecuteTemplates()
|
||||||
if tmplErr != nil {
|
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)
|
processed, errs := converge(templated, helm)
|
||||||
noMatchInHelmfiles = noMatchInHelmfiles && !processed
|
noMatchInHelmfiles = noMatchInHelmfiles && !processed
|
||||||
return clean(templated, errs)
|
return context{a, templated}.clean(errs)
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -369,7 +372,7 @@ func (a *App) loadDesiredStateFromYaml(yaml []byte, file string, namespace strin
|
||||||
sig := <-sigs
|
sig := <-sigs
|
||||||
|
|
||||||
errs := []error{fmt.Errorf("Received [%s] to shutdown ", sig)}
|
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
|
// See http://tldp.org/LDP/abs/html/exitcodes.html
|
||||||
switch sig {
|
switch sig {
|
||||||
case syscall.SIGINT:
|
case syscall.SIGINT:
|
||||||
|
|
@ -382,26 +385,101 @@ func (a *App) loadDesiredStateFromYaml(yaml []byte, file string, namespace strin
|
||||||
return st, nil
|
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 {
|
if errs == nil {
|
||||||
errs = []error{}
|
errs = []error{}
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanErrs := st.Clean()
|
cleanErrs := c.st.Clean()
|
||||||
if cleanErrs != nil {
|
if cleanErrs != nil {
|
||||||
errs = append(errs, cleanErrs...)
|
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 {
|
if errs != nil && len(errs) > 0 {
|
||||||
for _, err := range errs {
|
for _, err := range errs {
|
||||||
switch e := err.(type) {
|
switch e := err.(type) {
|
||||||
case *state.ReleaseError:
|
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:
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -282,11 +282,11 @@ releases:
|
||||||
errMsg string
|
errMsg string
|
||||||
}{
|
}{
|
||||||
{label: "name=prometheus", expectedCount: 1, expectErr: false},
|
{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: "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: "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: "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"},
|
||||||
// See https://github.com/roboll/helmfile/issues/193
|
// 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},
|
{label: "duplicatedOK=yes", expectedCount: 2, expectErr: false},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
. "gotest.tools/assert"
|
"gotest.tools/assert"
|
||||||
is "gotest.tools/assert/cmp"
|
is "gotest.tools/assert/cmp"
|
||||||
"gotest.tools/env"
|
"gotest.tools/env"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -10,18 +10,18 @@ import (
|
||||||
|
|
||||||
func TestIsExplicitSelectorInheritanceEnabled(t *testing.T) {
|
func TestIsExplicitSelectorInheritanceEnabled(t *testing.T) {
|
||||||
//env var ExperimentalEnvVar is set
|
//env var ExperimentalEnvVar is set
|
||||||
Assert(t, is.Equal(os.Getenv(ExperimentalEnvVar), ""))
|
assert.Assert(t, is.Equal(os.Getenv(ExperimentalEnvVar), ""))
|
||||||
Check(t, !isExplicitSelectorInheritanceEnabled())
|
assert.Check(t, !isExplicitSelectorInheritanceEnabled())
|
||||||
|
|
||||||
//check for env var ExperimentalEnvVar set to true
|
//check for env var ExperimentalEnvVar set to true
|
||||||
defer env.Patch(t, ExperimentalEnvVar, "true")()
|
defer env.Patch(t, ExperimentalEnvVar, "true")()
|
||||||
Check(t, isExplicitSelectorInheritanceEnabled())
|
assert.Check(t, isExplicitSelectorInheritanceEnabled())
|
||||||
|
|
||||||
//check for env var ExperimentalEnvVar set to anything
|
//check for env var ExperimentalEnvVar set to anything
|
||||||
defer env.Patch(t, ExperimentalEnvVar, "foo")()
|
defer env.Patch(t, ExperimentalEnvVar, "foo")()
|
||||||
Check(t, !isExplicitSelectorInheritanceEnabled())
|
assert.Check(t, !isExplicitSelectorInheritanceEnabled())
|
||||||
|
|
||||||
//check for env var ExperimentalEnvVar set to ExperimentalSelectorExplicit
|
//check for env var ExperimentalEnvVar set to ExperimentalSelectorExplicit
|
||||||
defer env.Patch(t, ExperimentalEnvVar, ExperimentalSelectorExplicit)()
|
defer env.Patch(t, ExperimentalEnvVar, ExperimentalSelectorExplicit)()
|
||||||
Check(t, isExplicitSelectorInheritanceEnabled())
|
assert.Check(t, isExplicitSelectorInheritanceEnabled())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package state
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
const ReleaseErrorCodeFailure = 1
|
||||||
|
|
||||||
|
type ReleaseError struct {
|
||||||
|
*ReleaseSpec
|
||||||
|
err error
|
||||||
|
Code int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ReleaseError) Error() string {
|
||||||
|
return fmt.Sprintf("failed processing release %s: %v", e.Name, e.err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func newReleaseError(release *ReleaseSpec, err error) *ReleaseError {
|
||||||
|
return &ReleaseError{release, err, ReleaseErrorCodeFailure}
|
||||||
|
}
|
||||||
|
|
@ -15,9 +15,6 @@ import (
|
||||||
|
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"os/exec"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/roboll/helmfile/environment"
|
"github.com/roboll/helmfile/environment"
|
||||||
|
|
@ -202,15 +199,6 @@ func (st *HelmState) SyncRepos(helm RepoUpdater) []error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReleaseError struct {
|
|
||||||
*ReleaseSpec
|
|
||||||
underlying error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ReleaseError) Error() string {
|
|
||||||
return e.underlying.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
type syncResult struct {
|
type syncResult struct {
|
||||||
errors []*ReleaseError
|
errors []*ReleaseError
|
||||||
}
|
}
|
||||||
|
|
@ -250,7 +238,7 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
|
||||||
|
|
||||||
flags, flagsErr := st.flagsForUpgrade(helm, release, workerIndex)
|
flags, flagsErr := st.flagsForUpgrade(helm, release, workerIndex)
|
||||||
if flagsErr != nil {
|
if flagsErr != nil {
|
||||||
results <- syncPrepareResult{errors: []*ReleaseError{&ReleaseError{release, flagsErr}}}
|
results <- syncPrepareResult{errors: []*ReleaseError{newReleaseError(release, flagsErr)}}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,14 +246,14 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
|
||||||
for _, value := range additionalValues {
|
for _, value := range additionalValues {
|
||||||
valfile, err := filepath.Abs(value)
|
valfile, err := filepath.Abs(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, &ReleaseError{release, err})
|
errs = append(errs, newReleaseError(release, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, err := st.fileExists(valfile)
|
ok, err := st.fileExists(valfile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, &ReleaseError{release, err})
|
errs = append(errs, newReleaseError(release, err))
|
||||||
} else if !ok {
|
} else if !ok {
|
||||||
errs = append(errs, &ReleaseError{release, fmt.Errorf("file does not exist: %s", valfile)})
|
errs = append(errs, newReleaseError(release, fmt.Errorf("file does not exist: %s", valfile)))
|
||||||
}
|
}
|
||||||
flags = append(flags, "--values", valfile)
|
flags = append(flags, "--values", valfile)
|
||||||
}
|
}
|
||||||
|
|
@ -351,22 +339,22 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
|
||||||
context := st.createHelmContext(release, workerIndex)
|
context := st.createHelmContext(release, workerIndex)
|
||||||
|
|
||||||
if _, err := st.triggerPresyncEvent(release, "sync"); err != nil {
|
if _, err := st.triggerPresyncEvent(release, "sync"); err != nil {
|
||||||
relErr = &ReleaseError{release, err}
|
relErr = newReleaseError(release, err)
|
||||||
} else if !release.Desired() {
|
} else if !release.Desired() {
|
||||||
installed, err := st.isReleaseInstalled(context, helm, *release)
|
installed, err := st.isReleaseInstalled(context, helm, *release)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
relErr = &ReleaseError{release, err}
|
relErr = newReleaseError(release, err)
|
||||||
} else if installed {
|
} else if installed {
|
||||||
if err := helm.DeleteRelease(context, release.Name, "--purge"); err != nil {
|
if err := helm.DeleteRelease(context, release.Name, "--purge"); err != nil {
|
||||||
affectedReleases.Failed = append(affectedReleases.Failed, release)
|
affectedReleases.Failed = append(affectedReleases.Failed, release)
|
||||||
relErr = &ReleaseError{release, err}
|
relErr = newReleaseError(release, err)
|
||||||
} else {
|
} else {
|
||||||
affectedReleases.Deleted = append(affectedReleases.Deleted, release)
|
affectedReleases.Deleted = append(affectedReleases.Deleted, release)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if err := helm.SyncRelease(context, release.Name, chart, flags...); err != nil {
|
} else if err := helm.SyncRelease(context, release.Name, chart, flags...); err != nil {
|
||||||
affectedReleases.Failed = append(affectedReleases.Failed, release)
|
affectedReleases.Failed = append(affectedReleases.Failed, release)
|
||||||
relErr = &ReleaseError{release, err}
|
relErr = newReleaseError(release, err)
|
||||||
} else {
|
} else {
|
||||||
affectedReleases.Upgraded = append(affectedReleases.Upgraded, release)
|
affectedReleases.Upgraded = append(affectedReleases.Upgraded, release)
|
||||||
installedVersion, err := st.getDeployedVersion(context, helm, release)
|
installedVersion, err := st.getDeployedVersion(context, helm, release)
|
||||||
|
|
@ -612,18 +600,8 @@ func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []st
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type DiffError struct {
|
|
||||||
*ReleaseSpec
|
|
||||||
err error
|
|
||||||
Code int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *DiffError) Error() string {
|
|
||||||
return e.err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
type diffResult struct {
|
type diffResult struct {
|
||||||
err *DiffError
|
err *ReleaseError
|
||||||
}
|
}
|
||||||
|
|
||||||
type diffPrepareResult struct {
|
type diffPrepareResult struct {
|
||||||
|
|
@ -691,7 +669,7 @@ func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValu
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
rsErrs := make([]*ReleaseError, len(errs))
|
rsErrs := make([]*ReleaseError, len(errs))
|
||||||
for i, e := range errs {
|
for i, e := range errs {
|
||||||
rsErrs[i] = &ReleaseError{release, e}
|
rsErrs[i] = newReleaseError(release, e)
|
||||||
}
|
}
|
||||||
results <- diffPrepareResult{errors: rsErrs}
|
results <- diffPrepareResult{errors: rsErrs}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -762,12 +740,11 @@ func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []st
|
||||||
release := prep.release
|
release := prep.release
|
||||||
if err := helm.DiffRelease(st.createHelmContext(release, workerIndex), release.Name, normalizeChart(st.basePath, release.Chart), flags...); err != nil {
|
if err := helm.DiffRelease(st.createHelmContext(release, workerIndex), release.Name, normalizeChart(st.basePath, release.Chart), flags...); err != nil {
|
||||||
switch e := err.(type) {
|
switch e := err.(type) {
|
||||||
case *exec.ExitError:
|
case helmexec.ExitError:
|
||||||
// Propagate any non-zero exit status from the external command like `helm` that is failed under the hood
|
// Propagate any non-zero exit status from the external command like `helm` that is failed under the hood
|
||||||
status := e.Sys().(syscall.WaitStatus)
|
results <- diffResult{&ReleaseError{release, err, e.ExitStatus()}}
|
||||||
results <- diffResult{&DiffError{release, err, status.ExitStatus()}}
|
|
||||||
default:
|
default:
|
||||||
results <- diffResult{&DiffError{release, err, 0}}
|
results <- diffResult{&ReleaseError{release, err, 0}}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// diff succeeded, found no changes
|
// diff succeeded, found no changes
|
||||||
|
|
@ -925,7 +902,7 @@ func (st *HelmState) PrepareRelease(helm helmexec.Interface, helmfileCommand str
|
||||||
|
|
||||||
for _, release := range st.Releases {
|
for _, release := range st.Releases {
|
||||||
if _, err := st.triggerPrepareEvent(&release, helmfileCommand); err != nil {
|
if _, err := st.triggerPrepareEvent(&release, helmfileCommand); err != nil {
|
||||||
errs = append(errs, &ReleaseError{&release, err})
|
errs = append(errs, newReleaseError(&release, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue