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:
parent
1d1bf68d96
commit
eafe383d54
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
1
go.mod
@ -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
2
go.sum
@ -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=
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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 }}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"`)
|
||||
})
|
||||
|
@ -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()}
|
||||
|
Loading…
Reference in New Issue
Block a user