From 51c717b20c7eb8de1d2bca48c4d78ed530890b7c Mon Sep 17 00:00:00 2001 From: Luke Watts Date: Mon, 15 Apr 2024 21:00:52 +0200 Subject: [PATCH] feat: use workflow data to determine exit code errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: parse workflow data to determine errors * fix: switch to align with finalised schema * chore(deps): bump gaf to latest * test: refactor to support integration test * fix: introduce custom error for storing exit code * chore: adjust wording on json error * test: update to match new error * chore: remove file * chore(deps): update go-application-framework to latest * chore: reorder imports * chore: remove unused code * refactor: switch to structured test data * chore: fix formatting * chore: rename to include global prefix * fix: switch to content_type ref * chore: remove unused file * refactor: switch to exported type * refactor: introduce tests for displayError We want to ensure that nothing is displayed for the new Error being generated from TestSummary payload * test: switch to NewInMemory configuration * fix: display error logic to handle ExitCode errors * fix: broken import * test: remove defunct test --------- Co-authored-by: Peter Schäfer <101886095+PeterSchafer@users.noreply.github.com> --- cliv2/cmd/cliv2/main.go | 118 +++++++++++----- cliv2/cmd/cliv2/main_test.go | 189 ++++++++++++++++++++++++-- cliv2/go.mod | 2 +- cliv2/go.sum | 4 +- cliv2/internal/cliv2/cliv2.go | 4 + cliv2/internal/cliv2/cliv2_test.go | 22 +++ cliv2/internal/constants/constants.go | 1 + cliv2/internal/errors/errors.go | 11 ++ 8 files changed, 301 insertions(+), 50 deletions(-) create mode 100644 cliv2/internal/errors/errors.go diff --git a/cliv2/cmd/cliv2/main.go b/cliv2/cmd/cliv2/main.go index 4db6869126e..cc1017c7794 100644 --- a/cliv2/cmd/cliv2/main.go +++ b/cliv2/cmd/cliv2/main.go @@ -27,6 +27,8 @@ import ( "github.com/snyk/go-application-framework/pkg/auth" "github.com/snyk/go-application-framework/pkg/configuration" localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows" + "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/networking" "github.com/snyk/go-application-framework/pkg/runtimeinfo" "github.com/snyk/go-application-framework/pkg/utils" @@ -37,11 +39,12 @@ import ( "github.com/snyk/cli/cliv2/internal/cliv2" "github.com/snyk/cli/cliv2/internal/constants" + cli_errors "github.com/snyk/cli/cliv2/internal/errors" "github.com/snyk/cli/cliv2/pkg/basic_workflows" ) var internalOS string -var engine workflow.Engine +var globalEngine workflow.Engine var globalConfiguration configuration.Configuration var helpProvided bool @@ -154,18 +157,59 @@ func runMainWorkflow(config configuration.Configuration, cmd *cobra.Command, arg name := getFullCommandString(cmd) globalLogger.Print("Running ", name) - engine.GetAnalytics().SetCommand(name) + globalEngine.GetAnalytics().SetCommand(name) + err = runWorkflowAndProcessData(globalEngine, globalLogger, name) + + return err +} + +func runWorkflowAndProcessData(engine workflow.Engine, logger *zerolog.Logger, name string) error { data, err := engine.Invoke(workflow.NewWorkflowIdentifier(name)) + if err == nil { _, err = engine.InvokeWithInput(localworkflows.WORKFLOWID_OUTPUT_WORKFLOW, data) + if err == nil { + err = getErrorFromWorkFlowData(data) + } } else { - globalLogger.Print("Failed to execute the command!", err) + logger.Print("Failed to execute the command!", err) } - return err } +func getErrorFromWorkFlowData(data []workflow.Data) error { + for i := range data { + mimeType := data[i].GetContentType() + if strings.EqualFold(mimeType, content_type.TEST_SUMMARY) { + singleData, ok := data[i].GetPayload().([]byte) + if !ok { + return fmt.Errorf("invalid payload type: %T", data[i].GetPayload()) + } + + summary := json_schemas.TestSummary{} + + err := json.Unmarshal(singleData, &summary) + if err != nil { + return fmt.Errorf("failed to parse test summary payload: %w", err) + } + + // We are missing an understanding of ignored issues here + // this should be supported in the future + for _, result := range summary.Results { + if result.Open > 0 { + return cli_errors.ErrorWithExitCode{ + ExitCode: constants.SNYK_EXIT_CODE_VULNERABILITIES_FOUND, + } + } + } + + return nil + } + } + return nil +} + func sendAnalytics(analytics analytics.Analytics, debugLogger *zerolog.Logger) { debugLogger.Print("Sending Analytics") @@ -202,7 +246,7 @@ func defaultCmd(args []string) error { // * by specifying the raw cmd args for it globalConfiguration.Set(configuration.WORKFLOW_USE_STDIO, true) globalConfiguration.Set(configuration.RAW_CMD_ARGS, args) - _, err := engine.Invoke(basic_workflows.WORKFLOWID_LEGACY_CLI) + _, err := globalEngine.Invoke(basic_workflows.WORKFLOWID_LEGACY_CLI) return err } @@ -324,25 +368,29 @@ func handleError(err error) HandleError { return resultError } -func displayError(err error) { +func displayError(err error, output io.Writer, config configuration.Configuration) { if err != nil { var exitError *exec.ExitError - if !errors.As(err, &exitError) { - if globalConfiguration.GetBool(localworkflows.OUTPUT_CONFIG_KEY_JSON) { - jsonError := JsonErrorStruct{ - Ok: false, - ErrorMsg: err.Error(), - Path: globalConfiguration.GetString(configuration.INPUT_DIRECTORY), - } + isExitError := errors.As(err, &exitError) + isErrorWithCode := errors.As(err, &cli_errors.ErrorWithExitCode{}) + if isExitError || isErrorWithCode { + return + } - jsonErrorBuffer, _ := json.MarshalIndent(jsonError, "", " ") - fmt.Println(string(jsonErrorBuffer)) + if config.GetBool(localworkflows.OUTPUT_CONFIG_KEY_JSON) { + jsonError := JsonErrorStruct{ + Ok: false, + ErrorMsg: err.Error(), + Path: globalConfiguration.GetString(configuration.INPUT_DIRECTORY), + } + + jsonErrorBuffer, _ := json.MarshalIndent(jsonError, "", " ") + fmt.Fprintln(output, string(jsonErrorBuffer)) + } else { + if errors.Is(err, context.DeadlineExceeded) { + fmt.Fprintln(output, "command timed out") } else { - if errors.Is(err, context.DeadlineExceeded) { - fmt.Println("command timed out") - } else { - fmt.Println(err) - } + fmt.Fprintln(output, err) } } } @@ -368,39 +416,39 @@ func MainWithErrorCode() int { debugEnabled := globalConfiguration.GetBool(configuration.DEBUG) globalLogger = initDebugLogger(globalConfiguration) - engine = app.CreateAppEngineWithOptions(app.WithZeroLogger(globalLogger), app.WithConfiguration(globalConfiguration), app.WithRuntimeInfo(rInfo)) + globalEngine = app.CreateAppEngineWithOptions(app.WithZeroLogger(globalLogger), app.WithConfiguration(globalConfiguration), app.WithRuntimeInfo(rInfo)) if noProxyAuth := globalConfiguration.GetBool(basic_workflows.PROXY_NOAUTH); noProxyAuth { globalConfiguration.Set(configuration.PROXY_AUTHENTICATION_MECHANISM, httpauth.StringFromAuthenticationMechanism(httpauth.NoAuth)) } // initialize the extensions -> they register themselves at the engine - engine.AddExtensionInitializer(basic_workflows.Init) - engine.AddExtensionInitializer(sbom.Init) - engine.AddExtensionInitializer(depgraph.Init) - engine.AddExtensionInitializer(capture.Init) - engine.AddExtensionInitializer(iacrules.Init) - engine.AddExtensionInitializer(snykls.Init) - engine.AddExtensionInitializer(container.Init) - engine.AddExtensionInitializer(localworkflows.InitCodeWorkflow) + globalEngine.AddExtensionInitializer(basic_workflows.Init) + globalEngine.AddExtensionInitializer(sbom.Init) + globalEngine.AddExtensionInitializer(depgraph.Init) + globalEngine.AddExtensionInitializer(capture.Init) + globalEngine.AddExtensionInitializer(iacrules.Init) + globalEngine.AddExtensionInitializer(snykls.Init) + globalEngine.AddExtensionInitializer(container.Init) + globalEngine.AddExtensionInitializer(localworkflows.InitCodeWorkflow) // init engine - err = engine.Init() + err = globalEngine.Init() if err != nil { globalLogger.Print("Failed to init Workflow Engine!", err) return constants.SNYK_EXIT_CODE_ERROR } // add output flags as persistent flags - outputWorkflow, _ := engine.GetWorkflow(localworkflows.WORKFLOWID_OUTPUT_WORKFLOW) + outputWorkflow, _ := globalEngine.GetWorkflow(localworkflows.WORKFLOWID_OUTPUT_WORKFLOW) outputFlags := workflow.FlagsetFromConfigurationOptions(outputWorkflow.GetConfigurationOptions()) rootCommand.PersistentFlags().AddFlagSet(outputFlags) // add workflows as commands - createCommandsForWorkflows(rootCommand, engine) + createCommandsForWorkflows(rootCommand, globalEngine) // init NetworkAccess - networkAccess := engine.GetNetworkAccess() + networkAccess := globalEngine.GetNetworkAccess() networkAccess.AddHeaderField("x-snyk-cli-version", cliv2.GetFullVersion()) networkAccess.AddHeaderField( "User-Agent", @@ -415,7 +463,7 @@ func MainWithErrorCode() int { } // init Analytics - cliAnalytics := engine.GetAnalytics() + cliAnalytics := globalEngine.GetAnalytics() cliAnalytics.SetVersion(cliv2.GetFullVersion()) cliAnalytics.SetCmdArguments(os.Args[1:]) cliAnalytics.SetOperatingSystem(internalOS) @@ -443,7 +491,7 @@ func MainWithErrorCode() int { cliAnalytics.AddError(err) } - displayError(err) + displayError(err, os.Stdout, globalConfiguration) exitCode := cliv2.DeriveExitCode(err) globalLogger.Printf("Exiting with %d", exitCode) diff --git a/cliv2/cmd/cliv2/main_test.go b/cliv2/cmd/cliv2/main_test.go index cca2e8f93dd..3d6d177d74f 100644 --- a/cliv2/cmd/cliv2/main_test.go +++ b/cliv2/cmd/cliv2/main_test.go @@ -1,23 +1,34 @@ package main import ( + "bytes" + "encoding/json" + "errors" + "fmt" "os" + "os/exec" "testing" "time" + "github.com/rs/zerolog" + "github.com/snyk/go-application-framework/pkg/configuration" + localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows" + "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/workflow" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" - "github.com/snyk/go-application-framework/pkg/configuration" - localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows" - "github.com/snyk/go-application-framework/pkg/workflow" + "github.com/snyk/cli/cliv2/internal/constants" + clierrors "github.com/snyk/cli/cliv2/internal/errors" ) func cleanup() { helpProvided = false globalConfiguration = nil - engine = nil + globalEngine = nil } func Test_MainWithErrorCode(t *testing.T) { @@ -93,7 +104,7 @@ func Test_CreateCommandsForWorkflowWithSubcommands(t *testing.T) { globalConfiguration = configuration.New() globalConfiguration.Set(configuration.DEBUG, true) - engine = workflow.NewWorkFlowEngine(globalConfiguration) + globalEngine = workflow.NewWorkFlowEngine(globalConfiguration) fn := func(invocation workflow.InvocationContext, input []workflow.Data) ([]workflow.Data, error) { return []workflow.Data{}, nil @@ -104,17 +115,17 @@ func Test_CreateCommandsForWorkflowWithSubcommands(t *testing.T) { for _, v := range commandList { workflowConfig := workflow.ConfigurationOptionsFromFlagset(pflag.NewFlagSet("pla", pflag.ContinueOnError)) workflowId1 := workflow.NewWorkflowIdentifier(v) - _, err := engine.Register(workflowId1, workflowConfig, fn) + _, err := globalEngine.Register(workflowId1, workflowConfig, fn) if err != nil { t.Fatal(err) } } - _ = engine.Init() + _ = globalEngine.Init() rootCommand := prepareRootCommand() // invoke method under test - createCommandsForWorkflows(rootCommand, engine) + createCommandsForWorkflows(rootCommand, globalEngine) // test that root subcmd2 has expected subcommands cmd, _, _ := rootCommand.Find([]string{"cmd"}) @@ -168,7 +179,7 @@ func Test_runMainWorkflow_unknownargs(t *testing.T) { defer cleanup() globalConfiguration = configuration.New() globalConfiguration.Set(configuration.DEBUG, true) - engine = workflow.NewWorkFlowEngine(globalConfiguration) + globalEngine = workflow.NewWorkFlowEngine(globalConfiguration) fn := func(invocation workflow.InvocationContext, input []workflow.Data) ([]workflow.Data, error) { return []workflow.Data{}, nil @@ -179,13 +190,13 @@ func Test_runMainWorkflow_unknownargs(t *testing.T) { for _, v := range commandList { workflowConfig := workflow.ConfigurationOptionsFromFlagset(pflag.NewFlagSet("pla", pflag.ContinueOnError)) workflowId1 := workflow.NewWorkflowIdentifier(v) - _, err := engine.Register(workflowId1, workflowConfig, fn) + _, err := globalEngine.Register(workflowId1, workflowConfig, fn) if err != nil { t.Fatal(err) } } - _ = engine.Init() + _ = globalEngine.Init() config := configuration.NewInMemory() cmd := &cobra.Command{ @@ -214,12 +225,130 @@ func Test_runMainWorkflow_unknownargs(t *testing.T) { } } +func Test_getErrorFromWorkFlowData(t *testing.T) { + t.Run("nil error", func(t *testing.T) { + err := getErrorFromWorkFlowData(nil) + assert.Nil(t, err) + }) + t.Run("workflow error", func(t *testing.T) { + workflowId := workflow.NewWorkflowIdentifier("output") + workflowIdentifier := workflow.NewTypeIdentifier(workflowId, "output") + data := workflow.NewData(workflowIdentifier, "application/json", []byte(`{"error": "test error"}`)) + err := getErrorFromWorkFlowData([]workflow.Data{data}) + assert.Nil(t, err) + }) + t.Run("workflow with test findings", func(t *testing.T) { + workflowId := workflow.NewWorkflowIdentifier("output") + workflowIdentifier := workflow.NewTypeIdentifier(workflowId, "output") + payload, err := json.Marshal(json_schemas.TestSummary{ + Results: []json_schemas.TestSummaryResult{{ + Severity: "critical", + Total: 99, + Open: 97, + Ignored: 2, + }}, + Type: "sast", + }) + assert.Nil(t, err) + data := workflow.NewData(workflowIdentifier, content_type.TEST_SUMMARY, payload) + err = getErrorFromWorkFlowData([]workflow.Data{data}) + require.NotNil(t, err) + assert.ErrorIs(t, err, clierrors.ErrorWithExitCode{ExitCode: constants.SNYK_EXIT_CODE_VULNERABILITIES_FOUND}) + }) + + t.Run("workflow with empty testing findings", func(t *testing.T) { + workflowId := workflow.NewWorkflowIdentifier("output") + workflowIdentifier := workflow.NewTypeIdentifier(workflowId, "output") + d, err := json.Marshal(json_schemas.TestSummary{ + Results: []json_schemas.TestSummaryResult{{ + Severity: "critical", + Total: 0, + Open: 0, + Ignored: 0, + }}, + Type: "sast", + }) + assert.Nil(t, err) + data := workflow.NewData(workflowIdentifier, content_type.TEST_SUMMARY, d) + err = getErrorFromWorkFlowData([]workflow.Data{data}) + assert.Nil(t, err) + }) +} + +func addEmptyWorkflows(t *testing.T, engine workflow.Engine, commandList []string) { + t.Helper() + for _, v := range commandList { + fn := func(invocation workflow.InvocationContext, input []workflow.Data) ([]workflow.Data, error) { + return []workflow.Data{}, nil + } + + workflowConfig := workflow.ConfigurationOptionsFromFlagset(pflag.NewFlagSet("pla", pflag.ContinueOnError)) + workflowId1 := workflow.NewWorkflowIdentifier(v) + _, err := engine.Register(workflowId1, workflowConfig, fn) + if err != nil { + t.Fatal(err) + } + } +} + +func Test_runWorkflowAndProcessData(t *testing.T) { + defer cleanup() + globalConfiguration = configuration.New() + globalConfiguration.Set(configuration.DEBUG, true) + globalEngine = workflow.NewWorkFlowEngine(globalConfiguration) + + testCmnd := "subcmd1" + addEmptyWorkflows(t, globalEngine, []string{"output"}) + + fn := func(invocation workflow.InvocationContext, input []workflow.Data) ([]workflow.Data, error) { + typeId := workflow.NewTypeIdentifier(invocation.GetWorkflowIdentifier(), "workflowData") + testSummary := json_schemas.TestSummary{ + Results: []json_schemas.TestSummaryResult{ + { + Severity: "critical", + Total: 10, + Open: 10, + Ignored: 0, + }, + }, + Type: "sast", + } + d, err := json.Marshal(testSummary) + if err != nil { + t.Fatal(err) + } + + data := workflow.NewData(typeId, content_type.TEST_SUMMARY, d) + return []workflow.Data{ + data, + }, nil + } + + // setup workflow engine to contain a workflow with subcommands + wrkflowId := workflow.NewWorkflowIdentifier(testCmnd) + workflowConfig := workflow.ConfigurationOptionsFromFlagset(pflag.NewFlagSet("pla", pflag.ContinueOnError)) + + entry, err := globalEngine.Register(wrkflowId, workflowConfig, fn) + assert.Nil(t, err) + assert.NotNil(t, entry) + + err = globalEngine.Init() + assert.NoError(t, err) + + // invoke method under test + logger := zerolog.New(os.Stderr) + err = runWorkflowAndProcessData(globalEngine, &logger, testCmnd) + assert.ErrorIs(t, err, clierrors.ErrorWithExitCode{ + ExitCode: constants.SNYK_EXIT_CODE_VULNERABILITIES_FOUND, + }) +} + func Test_setTimeout(t *testing.T) { exitedCh := make(chan struct{}) fakeExit := func() { close(exitedCh) } - config := configuration.New() + config := configuration.NewInMemory() config.Set(configuration.TIMEOUT, 1) setTimeout(config, fakeExit) select { @@ -229,3 +358,39 @@ func Test_setTimeout(t *testing.T) { t.Fatal("timeout func never executed") } } + +func Test_displayError(t *testing.T) { + t.Run("prints out generic error messages", func(t *testing.T) { + var b bytes.Buffer + config := configuration.NewInMemory() + err := errors.New("test error") + displayError(err, &b, config) + + assert.Equal(t, "test error\n", b.String()) + }) + + scenarios := []struct { + name string + err error + }{ + { + name: "exec.ExitError", + err: &exec.ExitError{}, + }, + { + name: "clierrors.ErrorWithExitCode", + err: clierrors.ErrorWithExitCode{ExitCode: 42}, + }, + } + + for _, scenario := range scenarios { + t.Run(fmt.Sprintf("%s does not display anything", scenario.name), func(t *testing.T) { + var b bytes.Buffer + config := configuration.NewInMemory() + err := scenario.err + displayError(err, &b, config) + + assert.Equal(t, "", b.String()) + }) + } +} diff --git a/cliv2/go.mod b/cliv2/go.mod index fced16e8dd5..fb8b4f23625 100644 --- a/cliv2/go.mod +++ b/cliv2/go.mod @@ -13,7 +13,7 @@ require ( github.com/snyk/cli-extension-iac-rules v0.0.0-20240404084125-0098857e0e1a github.com/snyk/cli-extension-sbom v0.0.0-20240314090036-46535b380426 github.com/snyk/container-cli v0.0.0-20240322120441-6d9b9482f9b1 - github.com/snyk/go-application-framework v0.0.0-20240404113733-1ee20e5f3ae4 + github.com/snyk/go-application-framework v0.0.0-20240412134724-124163becdd6 github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65 github.com/snyk/snyk-iac-capture v0.6.5 github.com/snyk/snyk-ls v0.0.0-20240409081112-f3ca3f397a7b diff --git a/cliv2/go.sum b/cliv2/go.sum index 3b327932511..c886449718b 100644 --- a/cliv2/go.sum +++ b/cliv2/go.sum @@ -717,8 +717,8 @@ github.com/snyk/code-client-go v1.3.0 h1:RlE92b29DCyinnwmFIfsp/7ZQ1RZ9+yw4lSxchk github.com/snyk/code-client-go v1.3.0/go.mod h1:zULRDwUaDANSvDfHIU0Eux4tTbSj7w0xWmbvQikhM9E= github.com/snyk/container-cli v0.0.0-20240322120441-6d9b9482f9b1 h1:9RKY9NdX5DrJAoVXDP0JiqrXT+4Nb9NH8pjEcA0NsLA= github.com/snyk/container-cli v0.0.0-20240322120441-6d9b9482f9b1/go.mod h1:38w+dcAQp9eG3P5t2eNS9eG0reut10AeJjLv5lJ5lpM= -github.com/snyk/go-application-framework v0.0.0-20240404113733-1ee20e5f3ae4 h1:bwk32eWWmTUr2i3Wumab+VLUy5DICIQDNbyd2BfGU70= -github.com/snyk/go-application-framework v0.0.0-20240404113733-1ee20e5f3ae4/go.mod h1:Yz/qxFyfhf0xbA+z8Vzr5IM9IDG+BS+2PiGaP1yAsEw= +github.com/snyk/go-application-framework v0.0.0-20240412134724-124163becdd6 h1:Ntg/AajKoVcDVVJ/Y+vs6uQ9Jejxm2fKh8cN9kR52V4= +github.com/snyk/go-application-framework v0.0.0-20240412134724-124163becdd6/go.mod h1:Yz/qxFyfhf0xbA+z8Vzr5IM9IDG+BS+2PiGaP1yAsEw= github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65 h1:CEQuYv0Go6MEyRCD3YjLYM2u3Oxkx8GpCpFBd4rUTUk= github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65/go.mod h1:88KbbvGYlmLgee4OcQ19yr0bNpXpOr2kciOthaSzCAg= github.com/snyk/policy-engine v0.22.0 h1:od9pduGrXyfWO791X+8M1qmnvWUxaIXh0gBzGKqeseA= diff --git a/cliv2/internal/cliv2/cliv2.go b/cliv2/internal/cliv2/cliv2.go index 71e13a9a2db..3f79b10ae53 100644 --- a/cliv2/internal/cliv2/cliv2.go +++ b/cliv2/internal/cliv2/cliv2.go @@ -18,6 +18,7 @@ import ( "time" "github.com/gofrs/flock" + cli_errors "github.com/snyk/cli/cliv2/internal/errors" "github.com/snyk/go-application-framework/pkg/configuration" "github.com/snyk/go-application-framework/pkg/utils" @@ -445,11 +446,14 @@ func DeriveExitCode(err error) int { if err != nil { var exitError *exec.ExitError + var errorWithExitCode *cli_errors.ErrorWithExitCode if errors.As(err, &exitError) { returnCode = exitError.ExitCode() } else if errors.Is(err, context.DeadlineExceeded) { returnCode = constants.SNYK_EXIT_CODE_EX_UNAVAILABLE + } else if errors.As(err, &errorWithExitCode) { + returnCode = errorWithExitCode.ExitCode } else { // got an error but it's not an ExitError returnCode = constants.SNYK_EXIT_CODE_ERROR diff --git a/cliv2/internal/cliv2/cliv2_test.go b/cliv2/internal/cliv2/cliv2_test.go index f130f41f8fe..609fa6487df 100644 --- a/cliv2/internal/cliv2/cliv2_test.go +++ b/cliv2/internal/cliv2/cliv2_test.go @@ -2,6 +2,7 @@ package cliv2_test import ( "context" + "errors" "io" "log" "os" @@ -12,6 +13,7 @@ import ( "testing" "time" + cli_errors "github.com/snyk/cli/cliv2/internal/errors" "github.com/snyk/go-application-framework/pkg/configuration" "github.com/snyk/cli/cliv2/internal/cliv2" @@ -484,3 +486,23 @@ func Test_setTimeout(t *testing.T) { // 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) { + tests := []struct { + name string + err error + expected int + }{ + {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}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + exitCode := cliv2.DeriveExitCode(tc.err) + assert.Equal(t, tc.expected, exitCode) + }) + } +} diff --git a/cliv2/internal/constants/constants.go b/cliv2/internal/constants/constants.go index d95e4af4be1..fa01a6b831a 100644 --- a/cliv2/internal/constants/constants.go +++ b/cliv2/internal/constants/constants.go @@ -1,6 +1,7 @@ package constants const SNYK_EXIT_CODE_OK = 0 +const SNYK_EXIT_CODE_VULNERABILITIES_FOUND = 1 const SNYK_EXIT_CODE_ERROR = 2 const SNYK_EXIT_CODE_EX_UNAVAILABLE = 69 const SNYK_INTEGRATION_NAME = "CLI_V1_PLUGIN" diff --git a/cliv2/internal/errors/errors.go b/cliv2/internal/errors/errors.go new file mode 100644 index 00000000000..7b7c19003b9 --- /dev/null +++ b/cliv2/internal/errors/errors.go @@ -0,0 +1,11 @@ +package cli_errors + +import "fmt" + +type ErrorWithExitCode struct { + ExitCode int +} + +func (e ErrorWithExitCode) Error() string { + return fmt.Sprintf("exit code: %d", e.ExitCode) +}