Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions cliv2/cmd/cliv2/behavior/maperrortoexitcode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package behavior

import (
"github.com/snyk/error-catalog-golang-public/aibom"
"github.com/snyk/error-catalog-golang-public/code"
"github.com/snyk/error-catalog-golang-public/snyk"
"github.com/snyk/error-catalog-golang-public/snyk_errors"

"github.com/snyk/cli/cliv2/internal/constants"
)

var MapErrorCatalogToExitCode func(err *snyk_errors.Error, defaultValue int) int = mapErrorToExitCode

// mapErrorToExitCode maps error catalog errors to exit codes. Please extend the switch statement if new error codes need to be mapped.
func mapErrorToExitCode(err *snyk_errors.Error, defaultValue int) int {
var errorCatalogToExitCodeMap = map[string]int{
code.NewUnsupportedProjectError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS,
aibom.NewNoSupportedFilesError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS,
snyk.NewMaintenanceWindowError("").ErrorCode: constants.SNYK_EXIT_CODE_EX_TEMPFAIL,
// Add new mappings here
}

if exitCode, ok := errorCatalogToExitCodeMap[err.ErrorCode]; ok {
return exitCode
}

return defaultValue
}
32 changes: 29 additions & 3 deletions cliv2/cmd/cliv2/exitcode.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
package main

import (
"context"
"encoding/json"
"errors"
"fmt"
"os/exec"
"strings"

"github.com/snyk/error-catalog-golang-public/code"
"github.com/snyk/error-catalog-golang-public/snyk_errors"
"github.com/snyk/go-application-framework/pkg/apiclients/testapi"
"github.com/snyk/go-application-framework/pkg/local_workflows/content_type"
"github.com/snyk/go-application-framework/pkg/local_workflows/json_schemas"
"github.com/snyk/go-application-framework/pkg/utils/ufm"
"github.com/snyk/go-application-framework/pkg/workflow"

"github.com/snyk/cli/cliv2/cmd/cliv2/behavior"
"github.com/snyk/cli/cliv2/internal/constants"
cli_errors "github.com/snyk/cli/cliv2/internal/errors"
)
Expand Down Expand Up @@ -146,8 +149,8 @@ func handleTestSummary(engine workflow.Engine, data workflow.Data) (int, error)
// handleDataErrors processes data errors and returns the appropriate exit code and error
func handleDataErrors(data workflow.Data) (int, error) {
for _, dataError := range data.GetErrorList() {
if dataError.ErrorCode == code.NewUnsupportedProjectError("").ErrorCode {
return constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, dataError
if exitCode := mapErrorToExitCode(dataError); exitCode != unsetExitCode {
return exitCode, dataError
}
}
return unsetExitCode, nil
Expand All @@ -168,3 +171,26 @@ func createErrorWithExitCode(exitCode int, err error) error {
}
return errors.Join(err, errorWithExitCode)
}

// mapErrorToExitCode maps specific errors to an exit code. Unmapped errors will return unsetExitCode.
func mapErrorToExitCode(err error) int {
// no need to map if the error already contains an exit code in some form
exitCodeError := cli_errors.ErrorWithExitCode{}
var exitError *exec.ExitError
if errors.Is(err, exitCodeError) || errors.As(err, &exitError) {
return unsetExitCode
}

// map external errors for example from golang runtime or other libraries that require a specific exit code
if errors.Is(err, context.DeadlineExceeded) {
return constants.SNYK_EXIT_CODE_EX_UNAVAILABLE
}

// map error catalog errors
errCatalogError := snyk_errors.Error{}
if errors.As(err, &errCatalogError) {
return behavior.MapErrorCatalogToExitCode(&errCatalogError, unsetExitCode)
}

return unsetExitCode
}
101 changes: 101 additions & 0 deletions cliv2/cmd/cliv2/exitcode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package main

import (
"context"
"errors"
"os"
"os/exec"
"testing"

"github.com/snyk/error-catalog-golang-public/code"
"github.com/snyk/error-catalog-golang-public/snyk_errors"

"github.com/snyk/cli/cliv2/internal/constants"
cli_errors "github.com/snyk/cli/cliv2/internal/errors"
)

func TestMapErrorToExitCode(t *testing.T) {
t.Run("nil error returns unset", func(t *testing.T) {
exitCode := mapErrorToExitCode(nil)
if exitCode != unsetExitCode {
t.Errorf("expected exit code %d, got %d", unsetExitCode, exitCode)
}
})

t.Run("ErrorWithExitCode returns unset", func(t *testing.T) {
exitCodeErr := &cli_errors.ErrorWithExitCode{ExitCode: 42}
exitCode := mapErrorToExitCode(exitCodeErr)
if exitCode != unsetExitCode {
t.Errorf("expected exit code %d, got %d", unsetExitCode, exitCode)
}
})

t.Run("wrapped ErrorWithExitCode returns unset", func(t *testing.T) {
exitCodeErr := &cli_errors.ErrorWithExitCode{ExitCode: 42}
wrappedErr := errors.Join(exitCodeErr, errors.New("additional context"))
exitCode := mapErrorToExitCode(wrappedErr)
if exitCode != unsetExitCode {
t.Errorf("expected exit code %d, got %d", unsetExitCode, exitCode)
}
})

t.Run("exec.ExitError returns unset", func(t *testing.T) {
execErr := &exec.ExitError{ProcessState: &os.ProcessState{}}
exitCode := mapErrorToExitCode(execErr)
if exitCode != unsetExitCode {
t.Errorf("expected exit code %d, got %d", unsetExitCode, exitCode)
}
})

t.Run("wrapped exec.ExitError returns unset", func(t *testing.T) {
execErr := &exec.ExitError{ProcessState: &os.ProcessState{}}
wrappedErr := errors.Join(execErr, errors.New("command failed"))
exitCode := mapErrorToExitCode(wrappedErr)
if exitCode != unsetExitCode {
t.Errorf("expected exit code %d, got %d", unsetExitCode, exitCode)
}
})

t.Run("context.DeadlineExceeded returns EX_UNAVAILABLE", func(t *testing.T) {
exitCode := mapErrorToExitCode(context.DeadlineExceeded)
if exitCode != constants.SNYK_EXIT_CODE_EX_UNAVAILABLE {
t.Errorf("expected exit code %d, got %d", constants.SNYK_EXIT_CODE_EX_UNAVAILABLE, exitCode)
}
})

t.Run("wrapped context.DeadlineExceeded returns EX_UNAVAILABLE", func(t *testing.T) {
wrappedErr := errors.Join(context.DeadlineExceeded, errors.New("timeout occurred"))
exitCode := mapErrorToExitCode(wrappedErr)
if exitCode != constants.SNYK_EXIT_CODE_EX_UNAVAILABLE {
t.Errorf("expected exit code %d, got %d", constants.SNYK_EXIT_CODE_EX_UNAVAILABLE, exitCode)
}
})

t.Run("wrapped unsupported project error returns UNSUPPORTED_PROJECTS", func(t *testing.T) {
unsupportedErr := code.NewUnsupportedProjectError("test project")
wrappedErr := errors.Join(unsupportedErr, errors.New("additional context"))
exitCode := mapErrorToExitCode(wrappedErr)
if exitCode != constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS {
t.Errorf("expected exit code %d, got %d", constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, exitCode)
}
})

t.Run("other error catalog error returns unset", func(t *testing.T) {
otherErr := snyk_errors.Error{
ErrorCode: "OTHER_ERROR_CODE",
Detail: "some other error",
}
exitCode := mapErrorToExitCode(otherErr)
if exitCode != unsetExitCode {
t.Errorf("expected exit code %d, got %d", unsetExitCode, exitCode)
}
})

t.Run("generic error returns unset", func(t *testing.T) {
genericErr := errors.New("some generic error")
exitCode := mapErrorToExitCode(genericErr)
if exitCode != unsetExitCode {
t.Errorf("expected exit code %d, got %d", unsetExitCode, exitCode)
}
})
}
48 changes: 35 additions & 13 deletions cliv2/cmd/cliv2/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,16 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"

"github.com/snyk/cli/cliv2/cmd/cliv2/behavior/legacy"
"github.com/snyk/cli/cliv2/internal/cliv2"
"github.com/snyk/cli/cliv2/internal/constants"
"github.com/snyk/go-application-framework/pkg/analytics"
"github.com/snyk/go-application-framework/pkg/app"
"github.com/snyk/go-application-framework/pkg/configuration"
"github.com/snyk/go-application-framework/pkg/instrumentation"
"github.com/snyk/go-application-framework/pkg/logging"

"github.com/snyk/cli/cliv2/cmd/cliv2/behavior/legacy"
"github.com/snyk/cli/cliv2/internal/cliv2"
"github.com/snyk/cli/cliv2/internal/constants"

cliv2utils "github.com/snyk/cli/cliv2/internal/utils"

localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows"
Expand Down Expand Up @@ -188,7 +189,7 @@ func runWorkflowAndProcessData(engine workflow.Engine, logger *zerolog.Logger, n

output, err := engine.Invoke(workflow.NewWorkflowIdentifier(name), workflow.WithInstrumentationCollector(ic))
if err != nil {
logger.Print("Failed to execute the command!", err)
logger.Print("Failed to execute the command! ", err)
return err
}

Expand Down Expand Up @@ -527,6 +528,9 @@ func MainWithErrorCode() int {
ua := networking.UserAgent(networking.UaWithConfig(globalConfiguration), networking.UaWithRuntimeInfo(rInfo), networking.UaWithOS(internalOS))
networkAccess := globalEngine.GetNetworkAccess()
networkAccess.AddErrorHandler(func(err error, ctx context.Context) error {
if err == nil {
return nil
}
errorListMutex.Lock()
defer errorListMutex.Unlock()

Expand Down Expand Up @@ -623,14 +627,13 @@ func MainWithErrorCode() int {
}

if err != nil {
err = decorateError(err)
err, errorList = processError(err, errorList)

errorList = append(errorList, err)
for _, tempError := range errorList {
cliAnalytics.AddError(tempError)
if tempError != nil {
cliAnalytics.AddError(tempError)
}
}

err = legacyCLITerminated(err, errorList)
}

displayError(err, globalEngine.GetUserInterface(), globalConfiguration, ctx)
Expand Down Expand Up @@ -659,13 +662,32 @@ func MainWithErrorCode() int {
return exitCode
}

func legacyCLITerminated(err error, errorList []error) error {
exitErr, isExitError := err.(*exec.ExitError)
if isExitError && exitErr.ExitCode() == constants.SNYK_EXIT_CODE_TS_CLI_TERMINATED {
func processError(err error, errorList []error) (error, []error) {
// ensure to use generic fallback error catalog error if no other is available
err = decorateError(err)

// filter legacycli terminate errors since it is only used for internal purposes
if exitErr, isExitError := err.(*exec.ExitError); isExitError && exitErr.ExitCode() == constants.SNYK_EXIT_CODE_TS_CLI_TERMINATED {
err = nil
}

// add all errors to analytics
if err != nil {
errorList = append([]error{err}, errorList...)
}

// create a single error from all errors
if len(errorList) == 1 {
err = errorList[0]
} else if len(errorList) > 1 {
err = errors.Join(errorList...)
}
return err

// ensure to apply exit code mapping based on errors
if exitCode := mapErrorToExitCode(err); exitCode != unsetExitCode {
err = createErrorWithExitCode(exitCode, err)
}
return err, errorList
}

func setTimeout(config configuration.Configuration, onTimeout func()) {
Expand Down
2 changes: 0 additions & 2 deletions cliv2/internal/cliv2/cliv2.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,8 +553,6 @@ func DeriveExitCode(err error) int {
if returnCode < 0 || returnCode == constants.SNYK_EXIT_CODE_TS_CLI_TERMINATED {
returnCode = constants.SNYK_EXIT_CODE_ERROR
}
} else if errors.Is(err, context.DeadlineExceeded) {
returnCode = constants.SNYK_EXIT_CODE_EX_UNAVAILABLE
} else if errors.As(err, &errorWithExitCode) {
returnCode = errorWithExitCode.ExitCode
} else {
Expand Down
4 changes: 0 additions & 4 deletions cliv2/internal/cliv2/cliv2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -561,9 +561,6 @@ func Test_setTimeout(t *testing.T) {
err = cli.Execute(getProxyInfoForTest(), []string{"2"})

assert.ErrorIs(t, err, context.DeadlineExceeded)

// ensure that -1 is correctly mapped if timeout is set
assert.Equal(t, constants.SNYK_EXIT_CODE_EX_UNAVAILABLE, cliv2.DeriveExitCode(err))
}

func TestDeriveExitCode(t *testing.T) {
Expand All @@ -574,7 +571,6 @@ func TestDeriveExitCode(t *testing.T) {
}{
{name: "no error", err: nil, expected: constants.SNYK_EXIT_CODE_OK},
{name: "error with exit code", err: &cli_errors.ErrorWithExitCode{ExitCode: 42}, expected: 42},
{name: "context.DeadlineExceeded", err: context.DeadlineExceeded, expected: constants.SNYK_EXIT_CODE_EX_UNAVAILABLE},
{name: "other error", err: errors.New("some other error"), expected: constants.SNYK_EXIT_CODE_ERROR},
}

Expand Down
1 change: 1 addition & 0 deletions cliv2/internal/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const SNYK_EXIT_CODE_OK = 0
const SNYK_EXIT_CODE_VULNERABILITIES_FOUND = 1
const SNYK_EXIT_CODE_ERROR = 2
const SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS = 3
const SNYK_EXIT_CODE_EX_TEMPFAIL = 75 // EX_TEMPFAIL, Temporary failure, indicating something that is not really an error. For example that a mailer could not create a connection, and the request should be reattempted later.
const SNYK_EXIT_CODE_EX_UNAVAILABLE = 69
const SNYK_EXIT_CODE_TS_CLI_TERMINATED = 44
const SNYK_INTEGRATION_NAME = "CLI_V1_PLUGIN"
Expand Down
1 change: 1 addition & 0 deletions src/cli/exit-codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const EXIT_CODES = {
ERROR: 2,
NO_SUPPORTED_PROJECTS_DETECTED: 3,
EX_UNAVAILABLE: 69,
EX_TEMPFAIL: 75,
EX_NOPERM: 77,
EX_TERMINATE: 44,
};
25 changes: 23 additions & 2 deletions test/acceptance/fake-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export type FakeServer = {
setSarifResponse: (next: Record<string, unknown>) => void;
setNextResponse: (r: any) => void;
setNextStatusCode: (c: number) => void;
setGlobalResponse: (response: Record<string, unknown>, code: number) => void;

setEndpointResponse: (
endpoint: string,
response: Record<string, unknown>,
Expand Down Expand Up @@ -163,6 +165,14 @@ export const fakeServer = (basePath: string, snykToken: string): FakeServer => {
statusCodes = codes;
};

const setGlobalResponse = (
response: Record<string, unknown>,
code: number,
) => {
endpointResponses.set('*', response);
endpointStatusCodes.set('*', code);
};

const setEndpointResponse = (
endpoint: string,
response: Record<string, unknown>,
Expand Down Expand Up @@ -204,8 +214,18 @@ export const fakeServer = (basePath: string, snykToken: string): FakeServer => {

app.use((req, res, next) => {
const endpoint = req.url;
const endpointResponse = endpointResponses.get(endpoint);
const endpointStatusCode = endpointStatusCodes.get(endpoint);

const wildcardEndpoint = '*';
let endpointResponse = endpointResponses.get(wildcardEndpoint);
if (!endpointResponse) {
endpointResponse = endpointResponses.get(endpoint);
}

let endpointStatusCode = endpointStatusCodes.get(wildcardEndpoint);
if (!endpointStatusCode) {
endpointStatusCode = endpointStatusCodes.get(endpoint);
}

if (endpointResponse) {
res.status(endpointStatusCode || 200);
res.send(endpointResponse);
Expand Down Expand Up @@ -1359,6 +1379,7 @@ export const fakeServer = (basePath: string, snykToken: string): FakeServer => {
setNextStatusCode,
setEndpointResponse,
setEndpointStatusCode,
setGlobalResponse,
setStatusCode,
setStatusCodes,
setFeatureFlag,
Expand Down
Loading