1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-01-16 05:16:08 +02:00

Add error category parsing to cmd execution (#1703)

* Add error category parsing to cmd execution

It is now possible to define `ErrorCategoryMapping` as a `map[string][]string` on a `Command`.
The format contains the category as key which has a list of error patterns assigned.
Example:

```
cmd := Command{
  ErrorCategoryMapping: map[string][]string
    "build": {"build failed"},
    "compliance": {"vulnerabilities found", "outdated components found"},
    "test": {"some tests failed"},
  },
}
```

Setting this map triggers console log parsing when executing a command.
If a match is found the error category is stored and
it will automatically be added to the `errorDetails.json`.

* clean up go.mod

* fix test

* fix test

* Update DEVELOPMENT.md

* fix tests

* address long console content without line breaks

* scan condition update

* fix test

* add missing comment for exported function

* Update pkg/command/command.go

Co-authored-by: Stephan Aßmus <stephan.assmus@sap.com>

Co-authored-by: Stephan Aßmus <stephan.assmus@sap.com>
Co-authored-by: Christopher Fenner <26137398+CCFenner@users.noreply.github.com>
This commit is contained in:
Oliver Nocon 2020-06-24 10:04:05 +02:00 committed by GitHub
parent 1d1bf68d96
commit eafe383d54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 222 additions and 50 deletions

View File

@ -214,12 +214,12 @@ log.SetErrorCategory(log.ErrorCompliance)
Error categories are defined in [`pkg/log/ErrorCategory`](pkg/log/errors.go).
With the convenience function
With writing a fatal error
```golang
log.FatalError(err, "the error message")
log.Entry().WithError(err).Fatal("the error message")
```
the category is attached to the `fatal` error and written into the file `errorDetails.json`.
the category will be written into the file `errorDetails.json` and can be used from there in the further pipeline flow.
Writing the file is handled by [`pkg/log/FatalHook`](pkg/log/fatalHook.go).
## Testing

View File

@ -53,6 +53,7 @@ Please provide either of the following options:
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.Username)

View File

@ -55,6 +55,7 @@ Regardless of the option you chose, please make sure to provide the configuratio
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}

View File

@ -156,6 +156,7 @@ Define ` + "`" + `buildTool: custom` + "`" + `, ` + "`" + `filePath: <path to yo
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.Password)

View File

@ -191,6 +191,7 @@ thresholds instead of ` + "`" + `percentage` + "`" + ` whereas we strongly recom
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.Password)

View File

@ -47,6 +47,7 @@ func CloudFoundryCreateServiceKeyCommand() *cobra.Command {
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.Username)

View File

@ -46,6 +46,7 @@ func CloudFoundryDeleteServiceCommand() *cobra.Command {
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.Username)

View File

@ -45,6 +45,7 @@ It can be used no matter if a Docker daemon is available or not. It will also wo
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}

View File

@ -47,6 +47,7 @@ func DetectExecuteScanCommand() *cobra.Command {
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.APIToken)

View File

@ -152,6 +152,7 @@ DISCLAIMER: The step has not yet been tested on a wide variaty of projects, and
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.AuthToken)

View File

@ -44,6 +44,7 @@ func GctsCloneRepositoryCommand() *cobra.Command {
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.Username)

View File

@ -48,6 +48,7 @@ func GctsCreateRepositoryCommand() *cobra.Command {
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.Username)

View File

@ -45,6 +45,7 @@ func GctsDeployCommand() *cobra.Command {
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.Username)

View File

@ -52,6 +52,7 @@ It can for example be used for GitOps scenarios or for scenarios where you want
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.Token)

View File

@ -63,6 +63,7 @@ The result looks like
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.Token)

View File

@ -43,6 +43,7 @@ This step can, e.g., be used if there is a json schema which needs to be patched
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}

View File

@ -52,6 +52,7 @@ In the Docker network, the containers can be referenced by the values provided i
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}

View File

@ -75,6 +75,7 @@ helm upgrade <deploymentName> <chartPath> --install --force --namespace <namespa
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.ContainerRegistryPassword)

View File

@ -44,6 +44,7 @@ func MalwareExecuteScanCommand() *cobra.Command {
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.Username)

View File

@ -48,6 +48,7 @@ supports ci friendly versioning by flattening the pom before installing.`,
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}

View File

@ -58,6 +58,7 @@ For PMD the failure priority and the max allowed violations are configurable via
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}

View File

@ -48,6 +48,7 @@ func MavenExecuteCommand() *cobra.Command {
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}

View File

@ -81,6 +81,7 @@ func MtaBuildCommand() *cobra.Command {
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}

View File

@ -66,6 +66,7 @@ If an image for mavenExecute is configured, and npm packages are to be published
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.User)

View File

@ -43,6 +43,7 @@ either use ESLint configurations present in the project or use the provided gene
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}

View File

@ -44,6 +44,7 @@ func NpmExecuteScriptsCommand() *cobra.Command {
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}

View File

@ -93,10 +93,7 @@ func Execute() {
addRootFlags(rootCmd)
if err := rootCmd.Execute(); err != nil {
// in case we end up here we know that something in the PreRunE function went wrong
// and thus this indicates a configuration issue
log.SetErrorCategory(log.ErrorConfiguration)
log.FatalError(err, "configuration error")
log.Entry().WithError(err).Fatal("configuration error")
}
}

View File

@ -106,6 +106,7 @@ func ProtecodeExecuteScanCommand() *cobra.Command {
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.Username)

View File

@ -94,6 +94,7 @@ func SonarExecuteScanCommand() *cobra.Command {
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.Token)

View File

@ -81,6 +81,7 @@ func XsDeployCommand() *cobra.Command {
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.User)

1
go.mod
View File

@ -11,6 +11,7 @@ require (
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
github.com/bmatcuk/doublestar v1.3.1
github.com/containerd/containerd v1.3.4 // indirect
github.com/docker/docker v1.4.2-0.20200114201811-16a3519d870b // indirect
github.com/evanphx/json-patch v4.5.0+incompatible
github.com/getsentry/sentry-go v0.6.1
github.com/ghodss/yaml v1.0.0

2
go.sum
View File

@ -233,6 +233,8 @@ github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r
github.com/docker/docker v0.7.3-0.20190506211059-b20a14b54661/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7 h1:Cvj7S8I4Xpx78KAl6TwTmMHuHlZ/0SM60NUneGJQ7IE=
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v1.4.2-0.20200114201811-16a3519d870b h1:CrNyKukWIBVy2bR9+Mff24Yc85oD8LUwW4f81kjSvFw=
github.com/docker/docker v1.4.2-0.20200114201811-16a3519d870b/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ=
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=

View File

@ -1,22 +1,25 @@
package command
import (
"bufio"
"bytes"
"fmt"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/pkg/errors"
"io"
"os"
"os/exec"
"strings"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/pkg/errors"
)
// Command defines the information required for executing a call to any executable
type Command struct {
dir string
stdout io.Writer
stderr io.Writer
env []string
ErrorCategoryMapping map[string][]string
dir string
stdout io.Writer
stderr io.Writer
env []string
}
// SetDir sets the working directory for the execution
@ -45,7 +48,7 @@ var ExecCommand = exec.Command
// RunShell runs the specified command on the shell
func (c *Command) RunShell(shell, script string) error {
_out, _err := prepareOut(c.stdout, c.stderr)
c.prepareOut()
cmd := ExecCommand(shell)
@ -61,7 +64,7 @@ func (c *Command) RunShell(shell, script string) error {
log.Entry().Infof("running shell script: %v %v", shell, script)
if err := runCmd(cmd, _out, _err); err != nil {
if err := c.runCmd(cmd); err != nil {
return errors.Wrapf(err, "running shell script failed with %v", shell)
}
return nil
@ -72,7 +75,7 @@ func (c *Command) RunShell(shell, script string) error {
// Thus the executable needs to be on the PATH of the current process and it is not sufficient to alter the PATH on cmd.Env.
func (c *Command) RunExecutable(executable string, params ...string) error {
_out, _err := prepareOut(c.stdout, c.stderr)
c.prepareOut()
cmd := ExecCommand(executable, params...)
@ -84,7 +87,7 @@ func (c *Command) RunExecutable(executable string, params ...string) error {
appendEnvironment(cmd, c.env)
if err := runCmd(cmd, _out, _err); err != nil {
if err := c.runCmd(cmd); err != nil {
return errors.Wrapf(err, "running command '%v' failed", executable)
}
return nil
@ -95,7 +98,7 @@ func (c *Command) RunExecutable(executable string, params ...string) error {
// Thus the executable needs to be on the PATH of the current process and it is not sufficient to alter the PATH on cmd.Env.
func (c *Command) RunExecutableInBackground(executable string, params ...string) (Execution, error) {
_out, _err := prepareOut(c.stdout, c.stderr)
c.prepareOut()
cmd := ExecCommand(executable, params...)
@ -107,7 +110,7 @@ func (c *Command) RunExecutableInBackground(executable string, params ...string)
appendEnvironment(cmd, c.env)
execution, err := startCmd(cmd, _out, _err)
execution, err := c.startCmd(cmd)
if err != nil {
return nil, errors.Wrapf(err, "starting command '%v' failed", executable)
@ -143,7 +146,7 @@ func appendEnvironment(cmd *exec.Cmd, env []string) {
}
}
func startCmd(cmd *exec.Cmd, _out, _err io.Writer) (*execution, error) {
func (c *Command) startCmd(cmd *exec.Cmd) (*execution, error) {
stdout, stderr, err := cmdPipes(cmd)
@ -159,22 +162,97 @@ func startCmd(cmd *exec.Cmd, _out, _err io.Writer) (*execution, error) {
execution := execution{cmd: cmd}
execution.wg.Add(2)
srcOut := stdout
srcErr := stderr
if c.ErrorCategoryMapping != nil {
prOut, pwOut := io.Pipe()
trOut := io.TeeReader(stdout, pwOut)
srcOut = prOut
prErr, pwErr := io.Pipe()
trErr := io.TeeReader(stderr, pwErr)
srcErr = prErr
execution.wg.Add(2)
go func() {
defer execution.wg.Done()
defer pwOut.Close()
c.scanLog(trOut)
}()
go func() {
defer execution.wg.Done()
defer pwErr.Close()
c.scanLog(trErr)
}()
}
go func() {
_, execution.errCopyStdout = io.Copy(_out, stdout)
_, execution.errCopyStdout = io.Copy(c.stdout, srcOut)
execution.wg.Done()
}()
go func() {
_, execution.errCopyStderr = io.Copy(_err, stderr)
_, execution.errCopyStderr = io.Copy(c.stderr, srcErr)
execution.wg.Done()
}()
return &execution, nil
}
func runCmd(cmd *exec.Cmd, _out, _err io.Writer) error {
func (c *Command) scanLog(in io.Reader) {
scanner := bufio.NewScanner(in)
scanner.Split(scanShortLines)
for scanner.Scan() {
line := scanner.Text()
c.parseConsoleErrors(line)
}
if err := scanner.Err(); err != nil {
log.Entry().WithError(err).Info("failed to scan log file")
}
}
execution, err := startCmd(cmd, _out, _err)
func scanShortLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
lenData := len(data)
if atEOF && lenData == 0 {
return 0, nil, nil
}
if lenData > 32767 && !bytes.Contains(data[0:lenData], []byte("\n")) {
// we will neglect long output
// no use cases known where this would be relevant
// current accepted implication: error pattern would not be found
// -> resulting in wrong error categorization
return lenData, nil, nil
}
if i := bytes.IndexByte(data, '\n'); i >= 0 && i < 32767 {
// We have a full newline-terminated line with a size limit
// Size limit is required since otherwise scanner would stall
return i + 1, data[0:i], nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), data, nil
}
// Request more data.
return 0, nil, nil
}
func (c *Command) parseConsoleErrors(logLine string) {
for category, categoryErrors := range c.ErrorCategoryMapping {
for _, errorPart := range categoryErrors {
if strings.Contains(logLine, errorPart) {
log.SetErrorCategory(log.ErrorCategoryByString(category))
return
}
}
}
}
func (c *Command) runCmd(cmd *exec.Cmd) error {
execution, err := c.startCmd(cmd)
if err != nil {
return err
}
@ -192,20 +270,18 @@ func runCmd(cmd *exec.Cmd, _out, _err io.Writer) error {
return nil
}
func prepareOut(stdout, stderr io.Writer) (io.Writer, io.Writer) {
func (c *Command) prepareOut() {
//ToDo: check use of multiwriter instead to always write into os.Stdout and os.Stdin?
//stdout := io.MultiWriter(os.Stdout, &stdoutBuf)
//stderr := io.MultiWriter(os.Stderr, &stderrBuf)
if stdout == nil {
stdout = os.Stdout
if c.stdout == nil {
c.stdout = os.Stdout
}
if stderr == nil {
stderr = os.Stderr
if c.stderr == nil {
c.stderr = os.Stderr
}
return stdout, stderr
}
func cmdPipes(cmd *exec.Cmd) (io.ReadCloser, io.ReadCloser, error) {

View File

@ -8,6 +8,9 @@ import (
"os/exec"
"strings"
"testing"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/stretchr/testify/assert"
)
//based on https://golang.org/src/os/exec/exec_test.go
@ -55,10 +58,10 @@ func TestExecutableRun(t *testing.T) {
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
ex := Command{stdout: stdout, stderr: stderr}
ex.RunExecutable("echo", []string{"foo bar", "baz"}...)
t.Run("success case", func(t *testing.T) {
ex := Command{stdout: stdout, stderr: stderr}
ex.RunExecutable("echo", []string{"foo bar", "baz"}...)
t.Run("stdin", func(t *testing.T) {
expectedOut := "foo bar baz\n"
if oStr := stdout.String(); oStr != expectedOut {
@ -72,6 +75,22 @@ func TestExecutableRun(t *testing.T) {
}
})
})
t.Run("success case - log parsing", func(t *testing.T) {
log.SetErrorCategory(log.ErrorUndefined)
ex := Command{stdout: stdout, stderr: stderr, ErrorCategoryMapping: map[string][]string{"config": {"command echo"}}}
ex.RunExecutable("echo", []string{"foo bar", "baz"}...)
assert.Equal(t, log.ErrorConfiguration, log.GetErrorCategory())
})
t.Run("success case - log parsing long line", func(t *testing.T) {
log.SetErrorCategory(log.ErrorUndefined)
ex := Command{stdout: stdout, stderr: stderr, ErrorCategoryMapping: map[string][]string{"config": {"aaaa"}}}
ex.RunExecutable("long", []string{"foo bar", "baz"}...)
assert.Equal(t, log.ErrorUndefined, log.GetErrorCategory())
})
log.SetErrorCategory(log.ErrorUndefined)
})
}
@ -103,13 +122,13 @@ func TestPrepareOut(t *testing.T) {
t.Run("os", func(t *testing.T) {
s := Command{}
_out, _err := prepareOut(s.stdout, s.stderr)
s.prepareOut()
if _out != os.Stdout {
if s.stdout != os.Stdout {
t.Errorf("expected out to be os.Stdout")
}
if _err != os.Stderr {
if s.stderr != os.Stderr {
t.Errorf("expected err to be os.Stderr")
}
})
@ -118,12 +137,12 @@ func TestPrepareOut(t *testing.T) {
o := bytes.NewBufferString("")
e := bytes.NewBufferString("")
s := Command{stdout: o, stderr: e}
_out, _err := prepareOut(s.stdout, s.stderr)
s.prepareOut()
expectOut := "Test out"
expectErr := "Test err"
_out.Write([]byte(expectOut))
_err.Write([]byte(expectErr))
s.stdout.Write([]byte(expectOut))
s.stderr.Write([]byte(expectErr))
t.Run("out", func(t *testing.T) {
if o.String() != expectOut {
@ -138,6 +157,31 @@ func TestPrepareOut(t *testing.T) {
})
}
func TestParseConsoleErrors(t *testing.T) {
cmd := Command{
ErrorCategoryMapping: map[string][]string{
"config": {"configuration error 1", "configuration error 2"},
"build": {"build failed"},
},
}
tt := []struct {
consoleLine string
expectedCategory log.ErrorCategory
}{
{consoleLine: "this is an error", expectedCategory: log.ErrorUndefined},
{consoleLine: "this is configuration error 2", expectedCategory: log.ErrorConfiguration},
{consoleLine: "the build failed", expectedCategory: log.ErrorBuild},
}
for _, test := range tt {
log.SetErrorCategory(log.ErrorUndefined)
cmd.parseConsoleErrors(test.consoleLine)
assert.Equal(t, test.expectedCategory, log.GetErrorCategory(), test.consoleLine)
}
log.SetErrorCategory(log.ErrorUndefined)
}
func TestCmdPipes(t *testing.T) {
cmd := helperCommand("echo", "foo bar", "baz")
defer func() { ExecCommand = exec.Command }()
@ -203,6 +247,12 @@ func TestHelperProcess(*testing.T) {
for _, e := range os.Environ() {
fmt.Println(e)
}
case "long":
b := []byte("a")
size := 64000
b = bytes.Repeat(b, size)
fmt.Fprint(os.Stderr, b)
default:
fmt.Fprintf(os.Stderr, "Unknown command %q\n", cmd)
os.Exit(2)

View File

@ -97,6 +97,7 @@ func {{.CobraCmdFuncName}}() *cobra.Command {
err := {{if .ExportPrefix}}{{ .ExportPrefix }}.{{end}}PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
{{- range $key, $value := .StepSecrets }}

View File

@ -116,6 +116,7 @@ func TestStepCommand() *cobra.Command {
err := piperOsCmd.PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}

View File

@ -115,6 +115,7 @@ func TestStepCommand() *cobra.Command {
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}

View File

@ -22,7 +22,7 @@ func (e ErrorCategory) String() string {
"undefined",
"build",
"compliance",
"configuration",
"config",
"custom",
"infrastructure",
"service",
@ -30,6 +30,27 @@ func (e ErrorCategory) String() string {
}[e]
}
// ErrorCategoryByString returns the error category based on the category text
func ErrorCategoryByString(category string) ErrorCategory {
switch category {
case "build":
return ErrorBuild
case "compliance":
return ErrorCompliance
case "config":
return ErrorConfiguration
case "custom":
return ErrorCustom
case "infrastructure":
return ErrorInfrastructure
case "service":
return ErrorService
case "test":
return ErrorTest
}
return ErrorUndefined
}
// SetErrorCategory sets the error category
// This can be used later by calling log.GetErrorCategory()
// In addition it will be used when exiting the program with

View File

@ -31,6 +31,7 @@ func (f *FatalHook) Fire(entry *logrus.Entry) error {
details["message"] = entry.Message
details["error"] = fmt.Sprint(details["error"])
details["category"] = fmt.Sprint(GetErrorCategory())
details["result"] = "failure"
details["correlationId"] = f.CorrelationID

View File

@ -41,7 +41,7 @@ func TestFatalHookFire(t *testing.T) {
assert.NoError(t, err)
fileContent, err := ioutil.ReadFile(filepath.Join(workspace, "testStep_errorDetails.json"))
assert.NoError(t, err)
assert.Contains(t, string(fileContent), `"category":"testCategory"`)
assert.NotContains(t, string(fileContent), `"category":"testCategory"`)
assert.Contains(t, string(fileContent), `"correlationId":"https://build.url"`)
assert.Contains(t, string(fileContent), `"message":"the error message"`)
})
@ -63,7 +63,7 @@ func TestFatalHookFire(t *testing.T) {
assert.NoError(t, err)
fileContent, err := ioutil.ReadFile(filepath.Join(workspace, "errorDetails.json"))
assert.NoError(t, err)
assert.Contains(t, string(fileContent), `"category":"testCategory"`)
assert.NotContains(t, string(fileContent), `"category":"testCategory"`)
assert.Contains(t, string(fileContent), `"correlationId":"https://build.url"`)
assert.Contains(t, string(fileContent), `"message":"the error message"`)
})

View File

@ -77,13 +77,6 @@ func Entry() *logrus.Entry {
return logger
}
// FatalError provides an alternative to Entry().WithError(err).Fatal()
// It supports error categorization and adds the error category as additional logging field
// The error category can be set anywhere by just calling log.SetErrorCategory(category)
func FatalError(err error, message string) {
Entry().WithError(err).WithField("category", GetErrorCategory()).Fatal(message)
}
// Writer returns an io.Writer into which a tool's output can be redirected.
func Writer() io.Writer {
return &logrusWriter{logger: Entry()}