diff --git a/go.mod b/go.mod index 81ef28b..73006c2 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module github.com/uselagoon/ssh-portal go 1.22.2 require ( - al.essio.dev/pkg/shellescape v1.5.1 github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/MicahParks/keyfunc/v2 v2.1.0 github.com/alecthomas/assert/v2 v2.11.0 github.com/alecthomas/kong v1.2.1 + github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/gliderlabs/ssh v0.3.7 github.com/go-sql-driver/mysql v1.8.1 @@ -34,7 +34,6 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/alecthomas/repr v0.4.0 // indirect - github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect diff --git a/go.sum b/go.sum index cc9811e..cf41361 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= -al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= @@ -69,8 +67,6 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.1-0.20240806143717-0e97ed3b5379 h1:9pvPp/2VCtCB2xdSUCaKe1VKCzVHMR+GGgIAVLfQxIs= github.com/google/uuid v1.6.1-0.20240806143717-0e97ed3b5379/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= diff --git a/internal/sshserver/connectionparams.go b/internal/sshserver/connectionparams.go index 2fb42e1..2b0864b 100644 --- a/internal/sshserver/connectionparams.go +++ b/internal/sshserver/connectionparams.go @@ -8,9 +8,9 @@ import ( ) var ( - serviceRegex = regexp.MustCompile(`^service=(.+)`) - containerRegex = regexp.MustCompile(`^container=(.+)`) - logsRegex = regexp.MustCompile(`^logs=(.+)`) + serviceRegex = regexp.MustCompile(`^service=(\S+)`) + containerRegex = regexp.MustCompile(`^container=(\S+)`) + logsRegex = regexp.MustCompile(`^logs=(\S+)`) tailLinesRegex = regexp.MustCompile(`^tailLines=(\d+)$`) ) @@ -26,7 +26,7 @@ var ( ErrNoServiceForLogs = errors.New("missing service argument for logs argument") ) -// parseConnectionParams takes the raw SSH command, and parses out any +// parseConnectionParams takes the split and raw SSH command, and parses out any // leading service=..., container=..., and logs=... arguments. It returns: // - If a service=... argument is given, the value of that argument. // If no such argument is given, it falls back to a default of "cli". @@ -34,8 +34,8 @@ var ( // If no such argument is given, it returns an empty string. // - If a logs=... argument is given, the value of that argument. // If no such argument is given, it returns an empty string. -// - The remaining arguments as a slice of strings, with any leading -// service=, container=, or logs= arguments removed. +// - The remaining raw SSH command, with any leading service=, container=, or +// logs= arguments removed. // // Notes about the logic implemented here: // - service=... must be given as the first argument to be recognised. @@ -48,48 +48,54 @@ var ( // // [service=... [container=...]] CMD... // service=... [container=...] logs=... -func parseConnectionParams(args []string) (string, string, string, []string) { +func parseConnectionParams( + cmd []string, + rawCmd string, +) (string, string, string, string) { // exit early if we have no args - if len(args) == 0 { - return "cli", "", "", nil + if len(cmd) == 0 { + return "cli", "", "", rawCmd } // check for service argument - serviceMatches := serviceRegex.FindStringSubmatch(args[0]) + serviceMatches := serviceRegex.FindStringSubmatch(cmd[0]) if len(serviceMatches) == 0 { // no service= match, so assume cli and return all args - return "cli", "", "", args + return "cli", "", "", rawCmd } service := serviceMatches[1] + rawCmd = strings.TrimSpace(serviceRegex.ReplaceAllString(rawCmd, "")) // exit early if we are out of arguments - if len(args) == 1 { - return service, "", "", nil + if len(cmd) == 1 { + return service, "", "", rawCmd } // check for container and/or logs argument - containerMatches := containerRegex.FindStringSubmatch(args[1]) + containerMatches := containerRegex.FindStringSubmatch(cmd[1]) if len(containerMatches) == 0 { // no container= match, so check for logs= - logsMatches := logsRegex.FindStringSubmatch(args[1]) + logsMatches := logsRegex.FindStringSubmatch(cmd[1]) if len(logsMatches) == 0 { // no container= or logs= match, so just return the args - return service, "", "", args[1:] + return service, "", "", rawCmd } - // found logs=, so return it along with the remaining args - // (which should be empty) - return service, "", logsMatches[1], args[2:] + rawCmd = strings.TrimSpace(logsRegex.ReplaceAllString(rawCmd, "")) + // found logs=, so return it along with the remaining rawCmd + return service, "", logsMatches[1], rawCmd } container := containerMatches[1] + rawCmd = strings.TrimSpace(containerRegex.ReplaceAllString(rawCmd, "")) // exit early if we are out of arguments - if len(args) == 2 { - return service, container, "", nil + if len(cmd) == 2 { + return service, container, "", rawCmd } // container= matched, so check for logs= - logsMatches := logsRegex.FindStringSubmatch(args[2]) + logsMatches := logsRegex.FindStringSubmatch(cmd[2]) if len(logsMatches) == 0 { // no logs= match, so just return the remaining args - return service, container, "", args[2:] + return service, container, "", rawCmd } + rawCmd = strings.TrimSpace(logsRegex.ReplaceAllString(rawCmd, "")) // container= and logs= matched, so return both - return service, container, logsMatches[1], args[3:] + return service, container, logsMatches[1], rawCmd } // parseLogsArg checks that: @@ -104,8 +110,8 @@ func parseConnectionParams(args []string) (string, string, string, []string) { // // Note that if multiple tailLines= values are specified, the last one will be // the value used. -func parseLogsArg(service, logs string, cmd []string) (bool, int64, error) { - if len(cmd) != 0 { +func parseLogsArg(service, logs string, rawCmd string) (bool, int64, error) { + if len(rawCmd) != 0 { return false, 0, ErrCmdArgsAfterLogs } if service == "" { diff --git a/internal/sshserver/connectionparams_test.go b/internal/sshserver/connectionparams_test.go index 81931a9..54a4190 100644 --- a/internal/sshserver/connectionparams_test.go +++ b/internal/sshserver/connectionparams_test.go @@ -1,10 +1,10 @@ package sshserver_test import ( - "errors" - "reflect" "testing" + "github.com/alecthomas/assert/v2" + "github.com/anmitsu/go-shlex" "github.com/uselagoon/ssh-portal/internal/sshserver" ) @@ -12,102 +12,186 @@ type parsedParams struct { service string container string logs string - args []string + rawCmd string } func TestParseConnectionParams(t *testing.T) { var testCases = map[string]struct { - input []string + rawCmd string + cmd []string expect parsedParams }{ "no special args": { - input: []string{"drush", "do", "something"}, + rawCmd: "drush do something", + cmd: []string{"drush", "do", "something"}, expect: parsedParams{ service: "cli", container: "", logs: "", - args: []string{"drush", "do", "something"}, + rawCmd: "drush do something", }, }, "service params": { - input: []string{"service=mongo", "drush", "do", "something"}, + rawCmd: "service=mongo drush do something", + cmd: []string{"service=mongo", "drush", "do", "something"}, expect: parsedParams{ service: "mongo", container: "", logs: "", - args: []string{"drush", "do", "something"}, + rawCmd: "drush do something", }, }, "service and container params": { - input: []string{"service=nginx", "container=php", "drush", "do", "something"}, + rawCmd: "service=nginx container=php drush do something", + cmd: []string{"service=nginx", "container=php", "drush", "do", "something"}, expect: parsedParams{ service: "nginx", container: "php", logs: "", - args: []string{"drush", "do", "something"}, + rawCmd: "drush do something", }, }, "invalid order": { - input: []string{"container=php", "service=nginx", "drush", "do", "something"}, + rawCmd: "container=php service=nginx drush do something", + cmd: []string{"container=php", "service=nginx", "drush", "do", "something"}, expect: parsedParams{ service: "cli", container: "", logs: "", - args: []string{"container=php", "service=nginx", "drush", "do", "something"}, + rawCmd: "container=php service=nginx drush do something", }, }, "service and logs params": { - input: []string{"service=nginx", "logs=follow", "drush do something"}, + rawCmd: "service=nginx logs=follow drush do something", + cmd: []string{"service=nginx", "logs=follow", "drush", "do", "something"}, expect: parsedParams{ service: "nginx", container: "", logs: "follow", - args: []string{"drush do something"}, + rawCmd: "drush do something", }, }, "service, container and logs params": { - input: []string{"service=nginx", "container=php", "logs=follow", "drush do something"}, + rawCmd: "service=nginx container=php logs=follow drush do something", + cmd: []string{"service=nginx", "container=php", "logs=follow", "drush", "do", "something"}, expect: parsedParams{ service: "nginx", container: "php", logs: "follow", - args: []string{"drush do something"}, + rawCmd: "drush do something", }, }, "service, container and logs params (wrong order)": { - input: []string{"service=nginx", "logs=follow", "container=php", "drush do something"}, + rawCmd: "service=nginx logs=follow container=php drush do something", + cmd: []string{"service=nginx", "logs=follow", "container=php", "drush", "do", "something"}, expect: parsedParams{ service: "nginx", container: "", logs: "follow", - args: []string{"container=php", "drush do something"}, + rawCmd: "container=php drush do something", }, }, "service and logs params (invalid logs value)": { - input: []string{"service=nginx", "logs=php", "drush", "do", "something"}, + rawCmd: "service=nginx logs=php drush do something", + cmd: []string{"service=nginx", "logs=php", "drush", "do", "something"}, expect: parsedParams{ service: "nginx", container: "", logs: "php", - args: []string{"drush", "do", "something"}, + rawCmd: "drush do something", + }, + }, + "subshell misquoted": { + rawCmd: "/bin/sh -c ( echo foo; echo bar; echo baz ) | tail -n2", + cmd: []string{"/bin/sh", "-c", "(", "echo", "foo;", "echo", "bar;", "echo", "baz", ")", "|", "tail", "-n2"}, + expect: parsedParams{ + service: "cli", + container: "", + logs: "", + rawCmd: "/bin/sh -c ( echo foo; echo bar; echo baz ) | tail -n2", + }, + }, + "subshell quoted": { + rawCmd: `/bin/sh -c "( echo foo; echo bar; echo baz ) | tail -n2"`, + cmd: []string{"/bin/sh", "-c", "( echo foo; echo bar; echo baz ) | tail -n2"}, + expect: parsedParams{ + service: "cli", + container: "", + logs: "", + rawCmd: `/bin/sh -c "( echo foo; echo bar; echo baz ) | tail -n2"`, + }, + }, + "process substitution misquoted": { + rawCmd: `/bin/sh -c sleep 3 & sleep 1 && pgrep sleep`, + cmd: []string{"/bin/sh", "-c", "sleep", "3", "&", "sleep", "1", "&&", "pgrep", "sleep"}, + expect: parsedParams{ + service: "cli", + container: "", + logs: "", + rawCmd: `/bin/sh -c sleep 3 & sleep 1 && pgrep sleep`, + }, + }, + "process substitution quoted": { + rawCmd: `/bin/sh -c "sleep 3 & sleep 1 && pgrep sleep"`, + cmd: []string{"/bin/sh", "-c", "sleep 3 & sleep 1 && pgrep sleep"}, + expect: parsedParams{ + service: "cli", + container: "", + logs: "", + rawCmd: `/bin/sh -c "sleep 3 & sleep 1 && pgrep sleep"`, + }, + }, + "shell variables misquoted": { + rawCmd: "/bin/sh -c echo $$ $USER", + cmd: []string{"/bin/sh", "-c", "echo", "$$", "$USER"}, + expect: parsedParams{ + service: "cli", + container: "", + logs: "", + rawCmd: "/bin/sh -c echo $$ $USER", + }, + }, + "shell variables quoted": { + rawCmd: "/bin/sh -c 'echo $$ $USER'", + cmd: []string{"/bin/sh", "-c", "echo $$ $USER"}, + expect: parsedParams{ + service: "cli", + container: "", + logs: "", + rawCmd: "/bin/sh -c 'echo $$ $USER'", + }, + }, + "shell variables and service": { + rawCmd: `service=foo echo "$(( $$ + 1 ))"`, + cmd: []string{"service=foo", "echo", "$(( $$ + 1 ))"}, + expect: parsedParams{ + service: "foo", + container: "", + logs: "", + rawCmd: `echo "$(( $$ + 1 ))"`, + }, + }, + "ansible": { + rawCmd: "/bin/sh -c '( umask 77 && mkdir -p \"` echo /tmp `\"&& mkdir \"` echo /tmp/ansible-tmp-1729564333.3484864-620266-10397749948780 `\" && echo ansible-tmp-1729564333.3484864-620266-10397749948780=\"` echo /tmp/ansible-tmp-1729564333.3484864-620266-10397749948780 `\" ) && sleep 0'", + cmd: []string{"/bin/sh", "-c", "( umask 77 && mkdir -p \"` echo /tmp `\"&& mkdir \"` echo /tmp/ansible-tmp-1729564333.3484864-620266-10397749948780 `\" && echo ansible-tmp-1729564333.3484864-620266-10397749948780=\"` echo /tmp/ansible-tmp-1729564333.3484864-620266-10397749948780 `\" ) && sleep 0"}, + expect: parsedParams{ + service: "cli", + container: "", + logs: "", + rawCmd: "/bin/sh -c '( umask 77 && mkdir -p \"` echo /tmp `\"&& mkdir \"` echo /tmp/ansible-tmp-1729564333.3484864-620266-10397749948780 `\" && echo ansible-tmp-1729564333.3484864-620266-10397749948780=\"` echo /tmp/ansible-tmp-1729564333.3484864-620266-10397749948780 `\" ) && sleep 0'", }, }, } for name, tc := range testCases { t.Run(name, func(tt *testing.T) { - service, container, logs, args := sshserver.ParseConnectionParams(tc.input) - if tc.expect.service != service { - tt.Fatalf("service: expected %v, got %v", tc.expect.service, service) - } - if tc.expect.container != container { - tt.Fatalf("container: expected %v, got %v", tc.expect.container, container) - } - if tc.expect.logs != logs { - tt.Fatalf("logs: expected %v, got %v", tc.expect.logs, logs) - } - if !reflect.DeepEqual(tc.expect.args, args) { - tt.Fatalf("args: expected %v, got %v", tc.expect.args, args) - } + service, container, logs, rawCmd := sshserver.ParseConnectionParams(tc.cmd, tc.rawCmd) + assert.Equal(tt, tc.expect.service, service, name) + assert.Equal(tt, tc.expect.container, container, name) + assert.Equal(tt, tc.expect.logs, logs, name) + assert.Equal(tt, tc.expect.rawCmd, rawCmd, name) + // and just to confirm the test data is correct, emulate ssh.Session.Command() + cmd, _ := shlex.Split(tc.rawCmd, true) + assert.Equal(tt, tc.cmd, cmd, name) }) } } @@ -210,7 +294,7 @@ func TestValidateConnectionParams(t *testing.T) { input: parsedParams{ service: "cli", logs: "php", - args: []string{"drush", "do", "something"}, + rawCmd: "drush do something", }, expect: result{ err: sshserver.ErrCmdArgsAfterLogs, @@ -229,16 +313,10 @@ func TestValidateConnectionParams(t *testing.T) { for name, tc := range testCases { t.Run(name, func(tt *testing.T) { follow, tailLines, err := sshserver.ParseLogsArg( - tc.input.service, tc.input.logs, tc.input.args) - if !errors.Is(err, tc.expect.err) { - tt.Fatalf("expected %v, got %v", tc.expect.err, err) - } - if follow != tc.expect.follow { - tt.Fatalf("expected %v, got %v", tc.expect.follow, follow) - } - if tailLines != tc.expect.tailLines { - tt.Fatalf("expected %v, got %v", tc.expect.tailLines, tailLines) - } + tc.input.service, tc.input.logs, tc.input.rawCmd) + assert.IsError(tt, err, tc.expect.err, name) + assert.Equal(tt, tc.expect.follow, follow, name) + assert.Equal(tt, tc.expect.tailLines, tailLines, name) }) } } diff --git a/internal/sshserver/sessionhandler.go b/internal/sshserver/sessionhandler.go index 4966bc3..75ec698 100644 --- a/internal/sshserver/sessionhandler.go +++ b/internal/sshserver/sessionhandler.go @@ -7,7 +7,6 @@ import ( "log/slog" "time" - "al.essio.dev/pkg/shellescape" "github.com/gliderlabs/ssh" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" @@ -74,19 +73,19 @@ func authCtxValues(ctx ssh.Context) (int, string, int, string, string, error) { // getSSHIntent analyses the SFTP flag and the raw command strings to determine // if the command should be wrapped, and returns the given cmd wrapped // appropriately. -func getSSHIntent(sftp bool, cmd []string) []string { +func getSSHIntent(sftp bool, rawCmd string) []string { // if this is an sftp session we ignore any commands if sftp { return []string{"sftp-server", "-u", "0002"} } // if there is no command, assume the user wants a shell - if len(cmd) == 0 { + if len(rawCmd) == 0 { return []string{"sh"} } // if there is a command, wrap it in a shell the way openssh does // https://github.com/openssh/openssh-portable/blob/ // 73dcca12115aa12ed0d123b914d473c384e52651/session.c#L1705-L1713 - return []string{"sh", "-c", shellescape.QuoteCommand(cmd)} + return []string{"sh", "-c", rawCmd} } // sessionHandler returns a ssh.Handler which connects the ssh session to the @@ -97,18 +96,35 @@ func getSSHIntent(sftp bool, cmd []string) []string { // handler is that the command is set to sftp-server. This implies that the // target container must have a sftp-server binary installed for sftp to work. // There is no support for a built-in sftp server. -func sessionHandler(log *slog.Logger, c K8SAPIService, - sftp, logAccessEnabled bool) ssh.Handler { +func sessionHandler( + log *slog.Logger, + c K8SAPIService, + sftp, + logAccessEnabled bool, +) ssh.Handler { return func(s ssh.Session) { sessionTotal.Inc() ctx := s.Context() log := log.With(slog.String("sessionID", ctx.SessionID())) log.Debug("starting session", - slog.Any("rawCommand", s.Command()), + slog.Any("command", s.Command()), + slog.String("rawCommand", s.RawCommand()), slog.String("subsystem", s.Subsystem()), ) // parse the command line arguments to extract any service or container args - service, container, logs, rawCmd := parseConnectionParams(s.Command()) + // + // NOTE: + // + // * s.RawCommand() returns a string containing the arguments supplied to + // the ssh client joined by a single space: + // https://github.com/openssh/openssh-portable/blob/ + // fe4305c37ffe53540a67586854e25f05cf615849/ssh.c#L1179-L1184 + // * s.Command() returns a slice of strings split on space and parsed as + // posix shell arguments: + // https://github.com/openssh/openssh-portable/blob/ + // fe4305c37ffe53540a67586854e25f05cf615849/ssh.c#L1179-L1184 + service, container, logs, rawCmd := + parseConnectionParams(s.Command(), s.RawCommand()) // validate the service and container if err := k8s.ValidateLabelValue(service); err != nil { log.Debug("invalid service name", diff --git a/internal/sshserver/sessionhandler_test.go b/internal/sshserver/sessionhandler_test.go index 7c25f66..4ed23d9 100644 --- a/internal/sshserver/sessionhandler_test.go +++ b/internal/sshserver/sessionhandler_test.go @@ -5,6 +5,7 @@ import ( "os" "testing" + "github.com/anmitsu/go-shlex" "github.com/gliderlabs/ssh" "github.com/uselagoon/ssh-portal/internal/sshserver" "go.uber.org/mock/gomock" @@ -12,69 +13,31 @@ import ( func TestExec(t *testing.T) { log := slog.New(slog.NewJSONHandler(os.Stderr, nil)) + var ( + user = "project-test" + deployment = "cli" + ) var testCases = map[string]struct { - user string - deployment string - rawCommand []string + rawCommand string command []string sftp bool logAccessEnabled bool pty bool }{ "bare interactive shell": { - user: "project-test", - deployment: "cli", - rawCommand: nil, + rawCommand: "", command: []string{"sh"}, sftp: false, logAccessEnabled: false, pty: true, }, "non-interactive id command": { - user: "project-test", - deployment: "cli", - rawCommand: []string{"id"}, + rawCommand: "id", command: []string{"sh", "-c", "id"}, sftp: false, logAccessEnabled: false, pty: false, }, - "subshell": { - user: "project-test", - deployment: "cli", - rawCommand: []string{"/bin/sh", "-c", "( echo foo; echo bar; echo baz ) | tail -n2"}, - command: []string{"sh", "-c", "/bin/sh -c '( echo foo; echo bar; echo baz ) | tail -n2'"}, - sftp: false, - logAccessEnabled: false, - pty: false, - }, - "process substitution 1": { - user: "project-test", - deployment: "cli", - rawCommand: []string{"/bin/sh", "-c", "sleep 3 & echo $(pgrep sleep)"}, - command: []string{"sh", "-c", "/bin/sh -c 'sleep 3 & echo $(pgrep sleep)'"}, - sftp: false, - logAccessEnabled: false, - pty: false, - }, - "process substitution 2": { - user: "project-test", - deployment: "cli", - rawCommand: []string{"/bin/sh", "-c", "sleep 3 & echo $( pgrep sleep )"}, - command: []string{"sh", "-c", "/bin/sh -c 'sleep 3 & echo $( pgrep sleep )'"}, - sftp: false, - logAccessEnabled: false, - pty: false, - }, - "shell variables": { - user: "project-test", - deployment: "cli", - rawCommand: []string{"/bin/sh", "-c", "echo $$ $USER"}, - command: []string{"sh", "-c", "/bin/sh -c 'echo $$ $USER'"}, - sftp: false, - logAccessEnabled: false, - pty: false, - }, } for name, tc := range testCases { t.Run(name, func(tt *testing.T) { @@ -93,14 +56,17 @@ func TestExec(t *testing.T) { // configure mocks sshSession.EXPECT().Context().Return(sshContext) sshContext.EXPECT().SessionID().Return("test_session_id") - sshSession.EXPECT().Command().Return(tc.rawCommand).AnyTimes() + sshSession.EXPECT().RawCommand().Return(tc.rawCommand).Times(2) + // emulate ssh.Session.Command() + command, _ := shlex.Split(tc.rawCommand, true) + sshSession.EXPECT().Command().Return(command).Times(2) sshSession.EXPECT().Subsystem().Return("") - sshSession.EXPECT().User().Return(tc.user).AnyTimes() + sshSession.EXPECT().User().Return(user).Times(3) k8sService.EXPECT().FindDeployment( sshContext, - tc.user, - tc.deployment, - ).Return(tc.deployment, nil) + user, + deployment, + ).Return(deployment, nil) sshContext.EXPECT().Value(sshserver.EnvironmentIDKey).Return(0) sshContext.EXPECT().Value(sshserver.EnvironmentNameKey).Return("test") sshContext.EXPECT().Value(sshserver.ProjectIDKey).Return(0) @@ -111,8 +77,8 @@ func TestExec(t *testing.T) { sshSession.EXPECT().Stderr().Return(os.Stderr) k8sService.EXPECT().Exec( sshContext, - tc.user, - tc.deployment, + user, + deployment, "", tc.command, sshSession, @@ -131,8 +97,7 @@ func TestLogs(t *testing.T) { var testCases = map[string]struct { user string deployment string - rawCommand []string - command []string + rawCommand string sftp bool logAccessEnabled bool pty bool @@ -142,8 +107,7 @@ func TestLogs(t *testing.T) { "nginx logs": { user: "project-test", deployment: "nginx", - rawCommand: []string{"service=nginx", "logs=tailLines=10"}, - command: nil, + rawCommand: "service=nginx logs=tailLines=10", sftp: false, logAccessEnabled: true, pty: false, @@ -168,9 +132,12 @@ func TestLogs(t *testing.T) { // configure mocks sshSession.EXPECT().Context().Return(sshContext) sshContext.EXPECT().SessionID().Return("test_session_id") - sshSession.EXPECT().Command().Return(tc.rawCommand).AnyTimes() + sshSession.EXPECT().RawCommand().Return(tc.rawCommand).Times(2) + // emulate ssh.Session.Command() + command, _ := shlex.Split(tc.rawCommand, true) + sshSession.EXPECT().Command().Return(command).Times(2) sshSession.EXPECT().Subsystem().Return("") - sshSession.EXPECT().User().Return(tc.user).AnyTimes() + sshSession.EXPECT().User().Return(tc.user).Times(3) k8sService.EXPECT().FindDeployment( sshContext, tc.user,