Skip to content

Commit

Permalink
feat: add executeCLI command (#628)
Browse files Browse the repository at this point in the history
Co-authored-by: bastiandoetsch <[email protected]>
  • Loading branch information
bastiandoetsch and bastiandoetsch authored Aug 19, 2024
1 parent da8fd45 commit e0971ab
Show file tree
Hide file tree
Showing 29 changed files with 282 additions and 120 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ LDFLAGS_DEV := "-X 'github.com/snyk/snyk-ls/application/config.Development=true'

TOOLS_BIN := $(shell pwd)/.bin

OVERRIDE_GOCI_LINT_V := v1.55.2
OVERRIDE_GOCI_LINT_V := v1.60.1
PACT_V := 2.4.2

NOCACHE := "-count=1"
Expand Down
9 changes: 1 addition & 8 deletions application/codeaction/codeaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,14 +209,7 @@ func Test_ResolveCodeAction_UnknownCommandIsReported(t *testing.T) {
testutil.UnitTest(t)
// Arrange
service := setupService(t)
command.SetService(command.NewService(
nil,
nil,
nil,
nil,
nil,
nil,
))
command.SetService(command.NewService(nil, nil, nil, nil, nil, nil, nil))

id := types.CodeActionData(uuid.New())
c := &sglsp.Command{
Expand Down
7 changes: 4 additions & 3 deletions application/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package config

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/url"
Expand Down Expand Up @@ -885,9 +886,9 @@ func (c *Config) Logger() *zerolog.Logger {
func (c *Config) TokenAsOAuthToken() (oauth2.Token, error) {
var oauthToken oauth2.Token
if _, err := uuid.Parse(c.Token()); err == nil {
msg := "creds are legacy, not oauth2"
c.Logger().Trace().Msgf(msg)
return oauthToken, fmt.Errorf(msg)
const msg = "creds are legacy, not oauth2"
c.Logger().Trace().Msg(msg)
return oauthToken, errors.New(msg)
}
err := json.Unmarshal([]byte(c.Token()), &oauthToken)
if err != nil {
Expand Down
15 changes: 5 additions & 10 deletions application/di/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@
package di

import (
"github.com/snyk/snyk-ls/domain/snyk/persistence"
"path/filepath"
"runtime"
"sync"

"github.com/snyk/snyk-ls/domain/snyk/persistence"

"github.com/adrg/xdg"

codeClient "github.com/snyk/code-client-go"
Expand Down Expand Up @@ -75,6 +76,7 @@ var notifier notification.Notifier
var codeInstrumentor codeClientObservability.Instrumentor
var codeErrorReporter codeClientObservability.ErrorReporter
var scanPersister persistence.ScanSnapshotPersister
var snykCli cli.Executor

func Init() {
initMutex.Lock()
Expand Down Expand Up @@ -128,7 +130,7 @@ func initInfrastructure(c *config.Config) {
// so that the oauth2 provider can use it for its callback
authenticationService.ConfigureProviders(c)

snykCli := cli.NewExecutor(c, errorReporter, notifier)
snykCli = cli.NewExecutor(c, errorReporter, notifier)

if gafConfiguration.GetString(cli_constants.EXECUTION_MODE_KEY) == cli_constants.EXECUTION_MODE_VALUE_EXTENSION {
snykCli = cli.NewExtensionExecutor(c)
Expand Down Expand Up @@ -173,14 +175,7 @@ func initApplication(c *config.Config) {
workspace.Set(w)
fileWatcher = watcher.NewFileWatcher()
codeActionService = codeaction.NewService(c, w, fileWatcher, notifier, snykCodeClient)
command.SetService(command.NewService(
authenticationService,
notifier,
learnService,
w,
snykCodeClient,
snykCodeScanner,
))
command.SetService(command.NewService(authenticationService, notifier, learnService, w, snykCodeClient, snykCodeScanner, snykCli))
}

/*
Expand Down
9 changes: 1 addition & 8 deletions application/server/execute_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,7 @@ func Test_loginCommand_StartsAuthentication(t *testing.T) {
loc, jsonRPCRecorder := setupServer(t)

// reset to use real service
command.SetService(command.NewService(
di.AuthenticationService(),
nil,
nil,
nil,
nil,
nil,
))
command.SetService(command.NewService(di.AuthenticationService(), nil, nil, nil, nil, nil, nil))

config.CurrentConfig().SetAutomaticAuthentication(false)
_, err := loc.Client.Call(ctx, "initialize", nil)
Expand Down
1 change: 1 addition & 0 deletions application/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ func initializeHandler(srv *jrpc2.Server) handler.Func {
types.CodeFixCommand,
types.CodeSubmitFixFeedback,
types.CodeFixDiffsCommand,
types.ExecuteCLICommand,
},
},
},
Expand Down
33 changes: 33 additions & 0 deletions application/server/server_smoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,39 @@ func Test_SmokeIssueCaching(t *testing.T) {
})
}

func Test_SmokeExecuteCLICommand(t *testing.T) {
loc, _ := setupServer(t)
c := testutil.SmokeTest(t, false)
c.EnableSnykCodeSecurity(false)
c.EnableSnykCodeQuality(false)
c.SetSnykIacEnabled(false)
c.SetSnykOssEnabled(true)
di.Init()

var cloneTargetDirGoof = setupRepoAndInitialize(t, nodejsGoof, "0336589", loc, c)
folderGoof := workspace.Get().GetFolderContaining(cloneTargetDirGoof)

// wait till the whole workspace is scanned
assert.Eventually(t, func() bool {
return folderGoof != nil && folderGoof.IsScanned()
}, maxIntegTestDuration, time.Millisecond)

// execute scan cli command
response, err := loc.Client.Call(context.Background(), "workspace/executeCommand", sglsp.ExecuteCommandParams{
Command: types.ExecuteCLICommand,
Arguments: []any{folderGoof.Path(), "test", "--json"},
})
require.NoError(t, err)

var resp map[string]any
err = response.UnmarshalResult(&resp)
require.NoError(t, err)

require.NotEmpty(t, resp)
require.Equal(t, float64(1), resp["exitCode"])
require.NotEmpty(t, resp["stdOut"])
}

func addJuiceShopAsWorkspaceFolder(t *testing.T, loc server.Local, c *config.Config) *workspace.Folder {
t.Helper()
var cloneTargetDirJuice, err = testutil.SetupCustomTestRepo(t, t.TempDir(), "https://github.com/juice-shop/juice-shop", "bc9cef127", c.Logger())
Expand Down
1 change: 1 addition & 0 deletions application/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ func Test_initialize_shouldSupportAllCommands(t *testing.T) {
assert.Contains(t, result.Capabilities.ExecuteCommandProvider.Commands, types.CodeFixCommand)
assert.Contains(t, result.Capabilities.ExecuteCommandProvider.Commands, types.CodeSubmitFixFeedback)
assert.Contains(t, result.Capabilities.ExecuteCommandProvider.Commands, types.CodeFixDiffsCommand)
assert.Contains(t, result.Capabilities.ExecuteCommandProvider.Commands, types.ExecuteCLICommand)
}

func Test_initialize_shouldSupportDocumentSaving(t *testing.T) {
Expand Down
16 changes: 15 additions & 1 deletion domain/ide/command/command_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/snyk/snyk-ls/application/config"
"github.com/snyk/snyk-ls/domain/snyk"
"github.com/snyk/snyk-ls/infrastructure/authentication"
"github.com/snyk/snyk-ls/infrastructure/cli"
"github.com/snyk/snyk-ls/infrastructure/code"
"github.com/snyk/snyk-ls/infrastructure/learn"
"github.com/snyk/snyk-ls/infrastructure/snyk_api"
Expand All @@ -31,7 +32,18 @@ import (

// CreateFromCommandData gets a command based on the given parameters that can be passed to the CommandService
// nolint: gocyclo, nolintlint // this is a factory, it's ok to have high cyclomatic complexity here
func CreateFromCommandData(c *config.Config, commandData types.CommandData, srv types.Server, authService authentication.AuthenticationService, learnService learn.Service, notifier noti.Notifier, issueProvider snyk.IssueProvider, codeApiClient SnykCodeHttpClient, codeScanner *code.Scanner) (types.Command, error) {
func CreateFromCommandData(
c *config.Config,
commandData types.CommandData,
srv types.Server,
authService authentication.AuthenticationService,
learnService learn.Service,
notifier noti.Notifier,
issueProvider snyk.IssueProvider,
codeApiClient SnykCodeHttpClient,
codeScanner *code.Scanner,
cli cli.Executor,
) (types.Command, error) {
httpClient := c.Engine().GetNetworkAccess().GetHttpClient

switch commandData.CommandId {
Expand Down Expand Up @@ -76,6 +88,8 @@ func CreateFromCommandData(c *config.Config, commandData types.CommandData, srv
issueProvider: issueProvider,
notifier: notifier,
}, nil
case types.ExecuteCLICommand:
return &executeCLICommand{command: commandData, authService: authService, notifier: notifier, logger: c.Logger(), cli: cli}, nil
}

return nil, fmt.Errorf("unknown command %v", commandData)
Expand Down
14 changes: 5 additions & 9 deletions domain/ide/command/command_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/snyk/snyk-ls/application/config"
"github.com/snyk/snyk-ls/domain/snyk"
"github.com/snyk/snyk-ls/infrastructure/authentication"
"github.com/snyk/snyk-ls/infrastructure/cli"
"github.com/snyk/snyk-ls/infrastructure/code"
"github.com/snyk/snyk-ls/infrastructure/learn"
noti "github.com/snyk/snyk-ls/internal/notification"
Expand All @@ -40,23 +41,18 @@ type serviceImpl struct {
issueProvider snyk.IssueProvider
codeApiClient SnykCodeHttpClient
codeScanner *code.Scanner
cli cli.Executor
}

func NewService(
authService authentication.AuthenticationService,
notifier noti.Notifier,
learnService learn.Service,
issueProvider snyk.IssueProvider,
codeApiClient SnykCodeHttpClient,
codeScanner *code.Scanner,
) types.CommandService {
func NewService(authService authentication.AuthenticationService, notifier noti.Notifier, learnService learn.Service, issueProvider snyk.IssueProvider, codeApiClient SnykCodeHttpClient, codeScanner *code.Scanner, cli cli.Executor) types.CommandService {
return &serviceImpl{
authService: authService,
notifier: notifier,
learnService: learnService,
issueProvider: issueProvider,
codeApiClient: codeApiClient,
codeScanner: codeScanner,
cli: cli,
}
}

Expand All @@ -77,7 +73,7 @@ func (service *serviceImpl) ExecuteCommandData(ctx context.Context, commandData

logger.Debug().Msgf("executing command %s", commandData.CommandId)

command, err := CreateFromCommandData(c, commandData, server, service.authService, service.learnService, service.notifier, service.issueProvider, service.codeApiClient, service.codeScanner)
command, err := CreateFromCommandData(c, commandData, server, service.authService, service.learnService, service.notifier, service.issueProvider, service.codeApiClient, service.codeScanner, service.cli)
if err != nil {
logger.Err(err).Msg("failed to create command")
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion domain/ide/command/command_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func Test_ExecuteCommand(t *testing.T) {
ExpectedAuthURL: "https://auth.url",
}
authenticationService := authentication.NewAuthenticationService(c, authProvider, nil, nil)
service := NewService(authenticationService, nil, nil, nil, nil, nil)
service := NewService(authenticationService, nil, nil, nil, nil, nil, nil)
cmd := types.CommandData{
CommandId: types.CopyAuthLinkCommand,
}
Expand Down
75 changes: 75 additions & 0 deletions domain/ide/command/execute_cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* © 2023-2024 Snyk Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package command

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

"github.com/rs/zerolog"

"github.com/snyk/snyk-ls/application/config"
"github.com/snyk/snyk-ls/infrastructure/authentication"
"github.com/snyk/snyk-ls/infrastructure/cli"
noti "github.com/snyk/snyk-ls/internal/notification"
"github.com/snyk/snyk-ls/internal/types"
)

type executeCLICommand struct {
command types.CommandData
authService authentication.AuthenticationService
notifier noti.Notifier
logger *zerolog.Logger
cli cli.Executor
}

type cliScanResult struct {
ExitCode int `json:"exitCode"`
StdOut string `json:"stdOut"`
}

func (cmd *executeCLICommand) Command() types.CommandData {
return cmd.command
}

func (cmd *executeCLICommand) Execute(ctx context.Context) (any, error) {
if len(cmd.command.Arguments) < 2 {
return nil, fmt.Errorf("invalid usage of executeCLICommand. First arg needs to be the workDir, then CLI arguments without binary path")
}
workDir := cmd.command.Arguments[0].(string)

args := []string{config.CurrentConfig().CliSettings().Path()}
for _, argument := range cmd.command.Arguments[1:] {
args = append(args, argument.(string))
}

args = cmd.cli.ExpandParametersFromConfig(args)
var exitCode int
resp, err := cmd.cli.Execute(ctx, args, workDir)
if err != nil {
var exitError *exec.ExitError
if errors.As(err, &exitError) {
exitCode = exitError.ExitCode()
}
}
return cliScanResult{
ExitCode: exitCode,
StdOut: string(resp),
}, nil
}
55 changes: 55 additions & 0 deletions domain/ide/command/execute_cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* © 2024 Snyk Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package command

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

cli2 "github.com/snyk/snyk-ls/infrastructure/cli"
"github.com/snyk/snyk-ls/internal/testutil"
"github.com/snyk/snyk-ls/internal/types"
)

func Test_executeCLI_callsCli(t *testing.T) {
c := testutil.UnitTest(t)
expected := `{ "outputKey": "outputValue" }`
dir := t.TempDir()

cli := cli2.NewTestExecutorWithResponse(expected)

args := []any{dir, "iac", "test", "--json"}
cut := executeCLICommand{
command: types.CommandData{
Title: "testCMD",
CommandId: types.ExecuteCLICommand,
Arguments: args,
},
logger: c.Logger(),
cli: cli,
}

response, err := cut.Execute(context.Background())
require.NoError(t, err)

assert.True(t, cli.WasExecuted())
assert.IsType(t, cliScanResult{}, response)
assert.Equal(t, expected, response.(cliScanResult).StdOut)
}
Loading

0 comments on commit e0971ab

Please sign in to comment.