From f676c6142520364553c6a574fb976eeb0c31a456 Mon Sep 17 00:00:00 2001 From: KUOKA Yusuke Date: Sun, 29 Mar 2020 10:49:02 +0900 Subject: [PATCH] feat: Better exec error reporting (#1159) Enhances Helmfile to print more helpful message on error while calling `exec` template function. Helmfile has been printing error messages like the below: ``` in ./helmfile.yaml: error during helmfile.yaml.part.0 parsing: template: stringTemplate:5:8: executing "stringTemplate" at : error calling exec: exit status 1 ``` Adding captured stdout and stderr, with some indentation to make it readable, it now produces the following message on missing executable: ``` $ make build && ./helmfile build go build in ./helmfile.yaml: error during helmfile.yaml.part.0 parsing: template: stringTemplate:5:8: executing "stringTemplate" at : error calling exec: fork/exec ./exectest.sha: no such file or directory COMMAND: ./exectest.sha ERROR: fork/exec ./exectest.sha: no such file or directory ``` On non-zero exit status without output: ``` $ make build && ./helmfile build go build in ./helmfile.yaml: error during helmfile.yaml.part.0 parsing: template: stringTemplate:5:8: executing "stringTemplate" at : error calling exec: exit status 1 COMMAND: ./exectest.sh ERROR: exit status 1 ``` On non-zero exit status with output: ``` $ make build && ./helmfile build go build in ./helmfile.yaml: error during helmfile.yaml.part.0 parsing: template: stringTemplate:5:8: executing "stringTemplate" at : error calling exec: exit status 2 COMMAND: ./exectest.sh ERROR: exit status 2 COMBINED OUTPUT: out1 err1 ``` Resolves #1158 --- pkg/tmpl/context_funcs.go | 45 +++++++++++++++++++++++++++++++++++++-- pkg/tmpl/context_tmpl.go | 4 ++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/pkg/tmpl/context_funcs.go b/pkg/tmpl/context_funcs.go index 79ddd70b..0819d9ea 100644 --- a/pkg/tmpl/context_funcs.go +++ b/pkg/tmpl/context_funcs.go @@ -92,9 +92,27 @@ func (c *Context) Exec(command string, args []interface{}, inputs ...string) (st var bytes []byte g.Go(func() error { - bs, err := cmd.Output() + // We use CombinedOutput to produce helpful error messages + // See https://github.com/roboll/helmfile/issues/1158 + bs, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("exec cmd=%s args=[%s] failed: %v", command, strings.Join(strArgs, ", "), err) + 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) } bytes = bs @@ -109,6 +127,29 @@ 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) { diff --git a/pkg/tmpl/context_tmpl.go b/pkg/tmpl/context_tmpl.go index f8dd5369..f4fae5b1 100644 --- a/pkg/tmpl/context_tmpl.go +++ b/pkg/tmpl/context_tmpl.go @@ -6,7 +6,7 @@ import ( "text/template" ) -func (c *Context) stringTemplate() *template.Template { +func (c *Context) newTemplate() *template.Template { funcMap := sprig.TxtFuncMap() for name, f := range c.createFuncMap() { funcMap[name] = f @@ -21,7 +21,7 @@ func (c *Context) stringTemplate() *template.Template { } func (c *Context) RenderTemplateToBuffer(s string, data ...interface{}) (*bytes.Buffer, error) { - var t, parseErr = c.stringTemplate().Parse(s) + var t, parseErr = c.newTemplate().Parse(s) if parseErr != nil { return nil, parseErr }