diff --git a/pkg/helmexec/exit_error.go b/pkg/helmexec/exit_error.go index 9419cf42..31eaf7cc 100644 --- a/pkg/helmexec/exit_error.go +++ b/pkg/helmexec/exit_error.go @@ -5,19 +5,55 @@ import ( "strings" ) -func newExitError(helmCmdPath string, exitStatus int, errorMessage string) ExitError { +func newExitError(path string, args []string, exitStatus int, err error, stderr, combined string) ExitError { + var out string + + out += fmt.Sprintf("PATH:\n%s", Indent(path, " ")) + + out += "\n\nARGS:" + for i, a := range args { + out += fmt.Sprintf("\n%s", Indent(fmt.Sprintf("%d: %s (%d bytes)", i, a, len(a)), " ")) + } + + out += fmt.Sprintf("\n\nERROR:\n%s", Indent(err.Error(), " ")) + + out += fmt.Sprintf("\n\nEXIT STATUS\n%s", Indent(fmt.Sprintf("%d", exitStatus), " ")) + + if len(stderr) > 0 { + out += fmt.Sprintf("\n\nSTDERR:\n%s", Indent(stderr, " ")) + } + + if len(combined) > 0 { + out += fmt.Sprintf("\n\nCOMBINED OUTPUT:\n%s", Indent(combined, " ")) + } + return ExitError{ - Message: fmt.Sprintf("the following cmd exited with status %d:\n%s\n\n%s", exitStatus, indent(strings.TrimSpace(helmCmdPath)), indent(strings.TrimSpace(errorMessage))), + Message: fmt.Sprintf("command %q exited with non-zero status:\n\n%s", path, out), Code: exitStatus, } } -func indent(text string) string { +// indents a block of text with an indent string +func Indent(text, indent string) string { + var b strings.Builder + + b.Grow(len(text) * 2) + lines := strings.Split(text, "\n") - for i := range lines { - lines[i] = " " + lines[i] + + last := len(lines) - 1 + + for i, j := range lines { + if i > 0 && i < last && j != "" { + b.WriteString("\n") + } + + if j != "" { + b.WriteString(indent + j) + } } - return strings.Join(lines, "\n") + + return b.String() } // ExitError is created whenever your shell command exits with a non-zero exit status diff --git a/pkg/helmexec/runner.go b/pkg/helmexec/runner.go index 179c0198..8762c11d 100644 --- a/pkg/helmexec/runner.go +++ b/pkg/helmexec/runner.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "go.uber.org/zap" + "io" "os" "os/exec" "strings" @@ -33,10 +34,10 @@ func (shell ShellRunner) Execute(cmd string, args []string, env map[string]strin preparedCmd := exec.Command(cmd, args...) preparedCmd.Dir = shell.Dir preparedCmd.Env = mergeEnv(os.Environ(), env) - return combinedOutput(preparedCmd, shell.Logger) + return Output(preparedCmd) } -func combinedOutput(c *exec.Cmd, logger *zap.SugaredLogger) ([]byte, error) { +func Output(c *exec.Cmd) ([]byte, error) { if c.Stdout != nil { return nil, errors.New("exec: Stdout already set") } @@ -45,13 +46,11 @@ func combinedOutput(c *exec.Cmd, logger *zap.SugaredLogger) ([]byte, error) { } var stdout bytes.Buffer var stderr bytes.Buffer - c.Stdout = &stdout - c.Stderr = &stderr + var combined bytes.Buffer + c.Stdout = io.MultiWriter(&stdout, &combined) + c.Stderr = io.MultiWriter(&stderr, &combined) err := c.Run() - o := stdout.Bytes() - e := stderr.Bytes() - if err != nil { // TrimSpace is necessary, because otherwise helmfile prints the redundant new-lines after each error like: // @@ -64,14 +63,13 @@ func combinedOutput(c *exec.Cmd, logger *zap.SugaredLogger) ([]byte, error) { // so that helmfile could return its own exit code accordingly waitStatus := ee.Sys().(syscall.WaitStatus) exitStatus := waitStatus.ExitStatus() - cmd := fmt.Sprintf("%s %s", c.Path, strings.Join(c.Args, " ")) - err = newExitError(cmd, exitStatus, string(e)) + err = newExitError(c.Path, c.Args, exitStatus, ee, stderr.String(), combined.String()) default: panic(fmt.Sprintf("unexpected error: %v", err)) } } - return o, err + return stdout.Bytes(), err } func mergeEnv(orig []string, new map[string]string) []string { diff --git a/pkg/tmpl/context_funcs.go b/pkg/tmpl/context_funcs.go index fac7c8c0..5d65e585 100644 --- a/pkg/tmpl/context_funcs.go +++ b/pkg/tmpl/context_funcs.go @@ -2,6 +2,7 @@ package tmpl import ( "fmt" + "github.com/roboll/helmfile/pkg/helmexec" "golang.org/x/sync/errgroup" "gopkg.in/yaml.v2" "io" @@ -97,25 +98,9 @@ func (c *Context) Exec(command string, args []interface{}, inputs ...string) (st g.Go(func() error { // We use CombinedOutput to produce helpful error messages // See https://github.com/roboll/helmfile/issues/1158 - bs, err := cmd.CombinedOutput() + bs, err := helmexec.Output(cmd) if err != nil { - args := strings.Join(strArgs, ", ") - shownCmd := []string{command} - if len(args) > 0 { - shownCmd = append(shownCmd, args) - } - - var out string - - out += fmt.Sprintf("\n\nCOMMAND:\n%s", Indent(strings.Join(shownCmd, " "), " ")) - - out += fmt.Sprintf("\n\nERROR:\n%s", Indent(err.Error(), " ")) - - if len(bs) > 0 { - out += fmt.Sprintf("\n\nCOMBINED OUTPUT:\n%s", Indent(string(bs), " ")) - } - - return fmt.Errorf("%v%s", err, out) + return err } bytes = bs @@ -130,29 +115,6 @@ func (c *Context) Exec(command string, args []interface{}, inputs ...string) (st return string(bytes), nil } -// indents a block of text with an indent string -func Indent(text, indent string) string { - var b strings.Builder - - b.Grow(len(text) * 2) - - lines := strings.Split(text, "\n") - - last := len(lines) - 1 - - for i, j := range lines { - if i > 0 && i < last && j != "" { - b.WriteString("\n") - } - - if j != "" { - b.WriteString(indent + j) - } - } - - return b.String() -} - func (c *Context) ReadFile(filename string) (string, error) { var path string if filepath.IsAbs(filename) {