From 3ff245382ef4ffd5b9308d949b5b92695792b867 Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 15 Nov 2024 13:21:42 -0500 Subject: [PATCH 01/20] layout implementation --- packages/core/src/awsService/ec2/model.ts | 16 ++++++++++++---- packages/core/src/shared/extensions/ssh.ts | 12 +++++++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/core/src/awsService/ec2/model.ts b/packages/core/src/awsService/ec2/model.ts index fa7bbee71b7..03ce94c22e4 100644 --- a/packages/core/src/awsService/ec2/model.ts +++ b/packages/core/src/awsService/ec2/model.ts @@ -20,7 +20,12 @@ import { } from '../../shared/remoteSession' import { DefaultIamClient } from '../../shared/clients/iamClient' import { ErrorInformation } from '../../shared/errors' -import { sshAgentSocketVariable, startSshAgent, startVscodeRemote } from '../../shared/extensions/ssh' +import { + sshAgentSocketVariable, + startSshAgent, + startVscodeRemote, + testSshConnection, +} from '../../shared/extensions/ssh' import { createBoundProcess } from '../../codecatalyst/model' import { getLogger } from '../../shared/logger/logger' import { CancellationError, Timeout } from '../../shared/utilities/timeoutUtils' @@ -195,6 +200,7 @@ export class Ec2Connecter implements vscode.Disposable { const remoteEnv = await this.prepareEc2RemoteEnvWithProgress(selection, remoteUser) try { + await testSshConnection(remoteEnv.sshPath, remoteEnv.hostname) await startVscodeRemote(remoteEnv.SessionProcess, remoteEnv.hostname, '/', remoteEnv.vscPath, remoteUser) } catch (err) { this.throwGeneralConnectionError(selection, err as Error) @@ -212,8 +218,9 @@ export class Ec2Connecter implements vscode.Disposable { const logger = this.configureRemoteConnectionLogger(selection.instanceId) const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() const keyPair = await this.configureSshKeys(selection, remoteUser) - const hostNamePrefix = 'aws-ec2-' - const sshConfig = new SshConfig(ssh, hostNamePrefix, 'ec2_connect', keyPair.getPrivateKeyPath()) + const hostnamePrefix = 'aws-ec2-' + const hostname = `${hostnamePrefix}${selection.instanceId}` + const sshConfig = new SshConfig(ssh, hostnamePrefix, 'ec2_connect', keyPair.getPrivateKeyPath()) const config = await sshConfig.ensureValid() if (config.isErr()) { @@ -222,6 +229,7 @@ export class Ec2Connecter implements vscode.Disposable { throw err } + const ssmSession = await this.ssmClient.startSession(selection.instanceId, 'AWS-StartSSHSession') await this.addActiveSession(selection.instanceId, ssmSession.SessionId!) @@ -236,7 +244,7 @@ export class Ec2Connecter implements vscode.Disposable { }) return { - hostname: `${hostNamePrefix}${selection.instanceId}`, + hostname, envProvider, sshPath: ssh, vscPath: vsc, diff --git a/packages/core/src/shared/extensions/ssh.ts b/packages/core/src/shared/extensions/ssh.ts index ff9046b3225..e6b8a77c192 100644 --- a/packages/core/src/shared/extensions/ssh.ts +++ b/packages/core/src/shared/extensions/ssh.ts @@ -8,7 +8,7 @@ import * as path from 'path' import * as nls from 'vscode-nls' import fs from '../fs/fs' import { getLogger } from '../logger' -import { ChildProcess } from '../utilities/processUtils' +import { ChildProcess, ChildProcessOptions } from '../utilities/processUtils' import { ArrayConstructor, NonNullObject } from '../utilities/typeConstructors' import { Settings } from '../settings' import { VSCODE_EXTENSION_ID } from '../extensions' @@ -119,6 +119,16 @@ export class RemoteSshSettings extends Settings.define('remote.SSH', remoteSshTy } } +export async function testSshConnection( + sshPath: string, + hostname: string, + options: ChildProcessOptions = {} +): Promise { + const process = new ChildProcess(sshPath, ['-T', hostname], options) + const result = await process.run() + console.log(result) +} + export async function startVscodeRemote( ProcessClass: typeof ChildProcess, hostname: string, From 4cd61e1367d1abe90361c8776e80ac797c32d644 Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 15 Nov 2024 16:43:34 -0500 Subject: [PATCH 02/20] implement dry run --- packages/core/src/awsService/ec2/model.ts | 13 +++++++---- packages/core/src/codecatalyst/model.ts | 25 +--------------------- packages/core/src/shared/extensions/ssh.ts | 16 ++++++++------ packages/core/src/shared/remoteSession.ts | 22 ++++++++++++++++++- 4 files changed, 41 insertions(+), 35 deletions(-) diff --git a/packages/core/src/awsService/ec2/model.ts b/packages/core/src/awsService/ec2/model.ts index 03ce94c22e4..d4ae51dd388 100644 --- a/packages/core/src/awsService/ec2/model.ts +++ b/packages/core/src/awsService/ec2/model.ts @@ -13,6 +13,7 @@ import { SsmClient } from '../../shared/clients/ssmClient' import { Ec2Client } from '../../shared/clients/ec2Client' import { VscodeRemoteConnection, + createBoundProcess, ensureDependencies, getDeniedSsmActions, openRemoteTerminal, @@ -26,7 +27,6 @@ import { startVscodeRemote, testSshConnection, } from '../../shared/extensions/ssh' -import { createBoundProcess } from '../../codecatalyst/model' import { getLogger } from '../../shared/logger/logger' import { CancellationError, Timeout } from '../../shared/utilities/timeoutUtils' import { showMessageWithCancel } from '../../shared/utilities/messages' @@ -200,7 +200,7 @@ export class Ec2Connecter implements vscode.Disposable { const remoteEnv = await this.prepareEc2RemoteEnvWithProgress(selection, remoteUser) try { - await testSshConnection(remoteEnv.sshPath, remoteEnv.hostname) + await testSshConnection(remoteEnv.SessionProcess, remoteEnv.hostname, remoteEnv.sshPath, remoteUser) await startVscodeRemote(remoteEnv.SessionProcess, remoteEnv.hostname, '/', remoteEnv.vscPath, remoteUser) } catch (err) { this.throwGeneralConnectionError(selection, err as Error) @@ -214,6 +214,12 @@ export class Ec2Connecter implements vscode.Disposable { return remoteEnv } + private async startRemoteSession(instanceId: string): Promise { + const ssmSession = await this.ssmClient.startSession(instanceId, 'AWS-StartSSHSession') + await this.addActiveSession(instanceId, ssmSession.SessionId!) + return ssmSession + } + public async prepareEc2RemoteEnv(selection: Ec2Selection, remoteUser: string): Promise { const logger = this.configureRemoteConnectionLogger(selection.instanceId) const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() @@ -230,8 +236,7 @@ export class Ec2Connecter implements vscode.Disposable { throw err } - const ssmSession = await this.ssmClient.startSession(selection.instanceId, 'AWS-StartSSHSession') - await this.addActiveSession(selection.instanceId, ssmSession.SessionId!) + const ssmSession = await this.startRemoteSession(selection.instanceId) const vars = getEc2SsmEnv(selection, ssm, ssmSession) const envProvider = async () => { diff --git a/packages/core/src/codecatalyst/model.ts b/packages/core/src/codecatalyst/model.ts index b2ba6912106..768a97890ee 100644 --- a/packages/core/src/codecatalyst/model.ts +++ b/packages/core/src/codecatalyst/model.ts @@ -19,7 +19,6 @@ import { getLogger } from '../shared/logger' import { AsyncCollection, toCollection } from '../shared/utilities/asyncCollection' import { getCodeCatalystSpaceName, getCodeCatalystProjectName, getCodeCatalystDevEnvId } from '../shared/vscode/env' import { sshAgentSocketVariable, startSshAgent, startVscodeRemote } from '../shared/extensions/ssh' -import { ChildProcess } from '../shared/utilities/processUtils' import { isDevenvVscode } from './utils' import { Timeout } from '../shared/utilities/timeoutUtils' import { Commands } from '../shared/vscode/commands2' @@ -28,7 +27,7 @@ import { fileExists } from '../shared/filesystemUtilities' import { CodeCatalystAuthenticationProvider } from './auth' import { ToolkitError } from '../shared/errors' import { Result } from '../shared/utilities/result' -import { VscodeRemoteConnection, ensureDependencies } from '../shared/remoteSession' +import { EnvProvider, VscodeRemoteConnection, createBoundProcess, ensureDependencies } from '../shared/remoteSession' import { SshConfig, sshLogFileLocation } from '../shared/sshConfig' import { fs } from '../shared' @@ -111,28 +110,6 @@ export function createCodeCatalystEnvProvider( } } -type EnvProvider = () => Promise - -/** - * Creates a new {@link ChildProcess} class bound to a specific dev environment. All instances of this - * derived class will have SSM session information injected as environment variables as-needed. - */ -export function createBoundProcess(envProvider: EnvProvider): typeof ChildProcess { - type Run = ChildProcess['run'] - return class SessionBoundProcess extends ChildProcess { - public override async run(...args: Parameters): ReturnType { - const options = args[0] - const envVars = await envProvider() - const spawnOptions = { - ...options?.spawnOptions, - env: { ...envVars, ...options?.spawnOptions?.env }, - } - - return super.run({ ...options, spawnOptions }) - } - } -} - export async function cacheBearerToken(bearerToken: string, devenvId: string): Promise { await fs.writeFile(bearerTokenCacheLocation(devenvId), `${bearerToken}`, 'utf8') } diff --git a/packages/core/src/shared/extensions/ssh.ts b/packages/core/src/shared/extensions/ssh.ts index e6b8a77c192..6fa9549f556 100644 --- a/packages/core/src/shared/extensions/ssh.ts +++ b/packages/core/src/shared/extensions/ssh.ts @@ -8,7 +8,7 @@ import * as path from 'path' import * as nls from 'vscode-nls' import fs from '../fs/fs' import { getLogger } from '../logger' -import { ChildProcess, ChildProcessOptions } from '../utilities/processUtils' +import { ChildProcess } from '../utilities/processUtils' import { ArrayConstructor, NonNullObject } from '../utilities/typeConstructors' import { Settings } from '../settings' import { VSCODE_EXTENSION_ID } from '../extensions' @@ -120,13 +120,17 @@ export class RemoteSshSettings extends Settings.define('remote.SSH', remoteSshTy } export async function testSshConnection( - sshPath: string, + ProcessClass: typeof ChildProcess, hostname: string, - options: ChildProcessOptions = {} + sshPath: string, + user: string ): Promise { - const process = new ChildProcess(sshPath, ['-T', hostname], options) - const result = await process.run() - console.log(result) + try { + await new ProcessClass(sshPath, ['-T', `${user}@${hostname}`, 'echo connected && exit']).run() + } catch (error) { + getLogger().error('SSH connection test failed: %O', error) + throw error + } } export async function startVscodeRemote( diff --git a/packages/core/src/shared/remoteSession.ts b/packages/core/src/shared/remoteSession.ts index 95c45832fa8..6b269e08734 100644 --- a/packages/core/src/shared/remoteSession.ts +++ b/packages/core/src/shared/remoteSession.ts @@ -77,7 +77,7 @@ interface DependencyPaths { readonly ssh: string } -type EnvProvider = () => Promise +export type EnvProvider = () => Promise export interface VscodeRemoteConnection { readonly sshPath: string @@ -251,3 +251,23 @@ export async function getDeniedSsmActions(client: IamClient, roleArn: string): P return deniedActions } + +/** + * Creates a new {@link ChildProcess} class bound to a specific dev environment. All instances of this + * derived class will have SSM session information injected as environment variables as-needed. + */ +export function createBoundProcess(envProvider: EnvProvider): typeof ChildProcess { + type Run = ChildProcess['run'] + return class SessionBoundProcess extends ChildProcess { + public override async run(...args: Parameters): ReturnType { + const options = args[0] + const envVars = await envProvider() + const spawnOptions = { + ...options?.spawnOptions, + env: { ...envVars, ...options?.spawnOptions?.env }, + } + + return super.run({ ...options, spawnOptions }) + } + } +} From b76bd5127e854bb7e19d090151309007010f045b Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 15 Nov 2024 17:23:42 -0500 Subject: [PATCH 03/20] create new test session for test ssh --- packages/core/src/awsService/ec2/model.ts | 13 +++- packages/core/src/shared/extensions/ssh.ts | 10 ++- packages/toolkit/package.json | 83 ++++++++++------------ 3 files changed, 56 insertions(+), 50 deletions(-) diff --git a/packages/core/src/awsService/ec2/model.ts b/packages/core/src/awsService/ec2/model.ts index d4ae51dd388..46b0c1dbf44 100644 --- a/packages/core/src/awsService/ec2/model.ts +++ b/packages/core/src/awsService/ec2/model.ts @@ -200,7 +200,14 @@ export class Ec2Connecter implements vscode.Disposable { const remoteEnv = await this.prepareEc2RemoteEnvWithProgress(selection, remoteUser) try { - await testSshConnection(remoteEnv.SessionProcess, remoteEnv.hostname, remoteEnv.sshPath, remoteUser) + const testSession = await this.startSSMSession(selection.instanceId) + await testSshConnection( + remoteEnv.SessionProcess, + remoteEnv.hostname, + remoteEnv.sshPath, + remoteUser, + testSession + ) await startVscodeRemote(remoteEnv.SessionProcess, remoteEnv.hostname, '/', remoteEnv.vscPath, remoteUser) } catch (err) { this.throwGeneralConnectionError(selection, err as Error) @@ -214,7 +221,7 @@ export class Ec2Connecter implements vscode.Disposable { return remoteEnv } - private async startRemoteSession(instanceId: string): Promise { + private async startSSMSession(instanceId: string): Promise { const ssmSession = await this.ssmClient.startSession(instanceId, 'AWS-StartSSHSession') await this.addActiveSession(instanceId, ssmSession.SessionId!) return ssmSession @@ -236,7 +243,7 @@ export class Ec2Connecter implements vscode.Disposable { throw err } - const ssmSession = await this.startRemoteSession(selection.instanceId) + const ssmSession = await this.startSSMSession(selection.instanceId) const vars = getEc2SsmEnv(selection, ssm, ssmSession) const envProvider = async () => { diff --git a/packages/core/src/shared/extensions/ssh.ts b/packages/core/src/shared/extensions/ssh.ts index 6fa9549f556..508acbdea6d 100644 --- a/packages/core/src/shared/extensions/ssh.ts +++ b/packages/core/src/shared/extensions/ssh.ts @@ -12,6 +12,7 @@ import { ChildProcess } from '../utilities/processUtils' import { ArrayConstructor, NonNullObject } from '../utilities/typeConstructors' import { Settings } from '../settings' import { VSCODE_EXTENSION_ID } from '../extensions' +import { SSM } from 'aws-sdk' const localize = nls.loadMessageBundle() @@ -123,10 +124,15 @@ export async function testSshConnection( ProcessClass: typeof ChildProcess, hostname: string, sshPath: string, - user: string + user: string, + session: SSM.StartSessionResponse ): Promise { try { - await new ProcessClass(sshPath, ['-T', `${user}@${hostname}`, 'echo connected && exit']).run() + await new ProcessClass(sshPath, ['-T', `${user}@${hostname}`, 'echo connected && exit']).run({ + spawnOptions: { + env: { SESSION_ID: session.SessionId, STREAM_URL: session.StreamUrl, TOKEN: session.TokenValue }, + }, + }) } catch (error) { getLogger().error('SSH connection test failed: %O', error) throw error diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 3c958454094..8e39296ba4b 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -4066,277 +4066,270 @@ "fontCharacter": "\\f1b4" } }, - "aws-amazonq-transform-landing-page-icon": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1b5" - } - }, "aws-amazonq-transform-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1b6" + "fontCharacter": "\\f1b5" } }, "aws-amazonq-transform-step-into-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1b7" + "fontCharacter": "\\f1b6" } }, "aws-amazonq-transform-step-into-light": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1b8" + "fontCharacter": "\\f1b7" } }, "aws-amazonq-transform-variables-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1b9" + "fontCharacter": "\\f1b8" } }, "aws-amazonq-transform-variables-light": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1ba" + "fontCharacter": "\\f1b9" } }, "aws-applicationcomposer-icon": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1bb" + "fontCharacter": "\\f1ba" } }, "aws-applicationcomposer-icon-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1bc" + "fontCharacter": "\\f1bb" } }, "aws-apprunner-service": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1bd" + "fontCharacter": "\\f1bc" } }, "aws-cdk-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1be" + "fontCharacter": "\\f1bd" } }, "aws-cloudformation-stack": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1bf" + "fontCharacter": "\\f1be" } }, "aws-cloudwatch-log-group": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1c0" + "fontCharacter": "\\f1bf" } }, "aws-codecatalyst-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1c1" + "fontCharacter": "\\f1c0" } }, "aws-codewhisperer-icon-black": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1c2" + "fontCharacter": "\\f1c1" } }, "aws-codewhisperer-icon-white": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1c3" + "fontCharacter": "\\f1c2" } }, "aws-codewhisperer-learn": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1c4" + "fontCharacter": "\\f1c3" } }, "aws-ecr-registry": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1c5" + "fontCharacter": "\\f1c4" } }, "aws-ecs-cluster": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1c6" + "fontCharacter": "\\f1c5" } }, "aws-ecs-container": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1c7" + "fontCharacter": "\\f1c6" } }, "aws-ecs-service": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1c8" + "fontCharacter": "\\f1c7" } }, "aws-generic-attach-file": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1c9" + "fontCharacter": "\\f1c8" } }, "aws-iot-certificate": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1ca" + "fontCharacter": "\\f1c9" } }, "aws-iot-policy": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1cb" + "fontCharacter": "\\f1ca" } }, "aws-iot-thing": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1cc" + "fontCharacter": "\\f1cb" } }, "aws-lambda-function": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1cd" + "fontCharacter": "\\f1cc" } }, "aws-mynah-MynahIconBlack": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1ce" + "fontCharacter": "\\f1cd" } }, "aws-mynah-MynahIconWhite": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1cf" + "fontCharacter": "\\f1ce" } }, "aws-mynah-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1d0" + "fontCharacter": "\\f1cf" } }, "aws-redshift-cluster": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1d1" + "fontCharacter": "\\f1d0" } }, "aws-redshift-cluster-connected": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1d2" + "fontCharacter": "\\f1d1" } }, "aws-redshift-database": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1d3" + "fontCharacter": "\\f1d2" } }, "aws-redshift-redshift-cluster-connected": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1d4" + "fontCharacter": "\\f1d3" } }, "aws-redshift-schema": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1d5" + "fontCharacter": "\\f1d4" } }, "aws-redshift-table": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1d6" + "fontCharacter": "\\f1d5" } }, "aws-s3-bucket": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1d7" + "fontCharacter": "\\f1d6" } }, "aws-s3-create-bucket": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1d8" + "fontCharacter": "\\f1d7" } }, "aws-schemas-registry": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1d9" + "fontCharacter": "\\f1d8" } }, "aws-schemas-schema": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1da" + "fontCharacter": "\\f1d9" } }, "aws-stepfunctions-preview": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1db" + "fontCharacter": "\\f1da" } } }, From 78454d969f3b335eb543ee06f6762499c52237fa Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 15 Nov 2024 17:39:52 -0500 Subject: [PATCH 04/20] refactor to declare env on its own line --- packages/core/src/shared/extensions/ssh.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/shared/extensions/ssh.ts b/packages/core/src/shared/extensions/ssh.ts index 508acbdea6d..e88580f6a32 100644 --- a/packages/core/src/shared/extensions/ssh.ts +++ b/packages/core/src/shared/extensions/ssh.ts @@ -128,9 +128,10 @@ export async function testSshConnection( session: SSM.StartSessionResponse ): Promise { try { + const env = { SESSION_ID: session.SessionId, STREAM_URL: session.StreamUrl, TOKEN: session.TokenValue } await new ProcessClass(sshPath, ['-T', `${user}@${hostname}`, 'echo connected && exit']).run({ spawnOptions: { - env: { SESSION_ID: session.SessionId, STREAM_URL: session.StreamUrl, TOKEN: session.TokenValue }, + env, }, }) } catch (error) { From 2f092e1c57b46b2c997fdd861529560e959d78c6 Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 15 Nov 2024 17:41:58 -0500 Subject: [PATCH 05/20] avoid adding test session to session tracker --- packages/core/src/awsService/ec2/model.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/src/awsService/ec2/model.ts b/packages/core/src/awsService/ec2/model.ts index 46b0c1dbf44..5280d951d56 100644 --- a/packages/core/src/awsService/ec2/model.ts +++ b/packages/core/src/awsService/ec2/model.ts @@ -198,9 +198,8 @@ export class Ec2Connecter implements vscode.Disposable { const remoteUser = await this.getRemoteUser(selection.instanceId) const remoteEnv = await this.prepareEc2RemoteEnvWithProgress(selection, remoteUser) - + const testSession = await this.ssmClient.startSession(selection.instanceId, 'AWS-StartSSHSession') try { - const testSession = await this.startSSMSession(selection.instanceId) await testSshConnection( remoteEnv.SessionProcess, remoteEnv.hostname, @@ -211,6 +210,8 @@ export class Ec2Connecter implements vscode.Disposable { await startVscodeRemote(remoteEnv.SessionProcess, remoteEnv.hostname, '/', remoteEnv.vscPath, remoteUser) } catch (err) { this.throwGeneralConnectionError(selection, err as Error) + } finally { + await this.ssmClient.terminateSession(testSession) } } From 823a0e3f0d0cdb416bbd8f27ecd2fe7fc1d73288 Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 15 Nov 2024 17:56:11 -0500 Subject: [PATCH 06/20] implement custom error to show more accurate error message --- packages/core/src/awsService/ec2/model.ts | 13 ++++--------- packages/core/src/shared/extensions/ssh.ts | 5 ++++- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/core/src/awsService/ec2/model.ts b/packages/core/src/awsService/ec2/model.ts index 5280d951d56..1635e6aad8c 100644 --- a/packages/core/src/awsService/ec2/model.ts +++ b/packages/core/src/awsService/ec2/model.ts @@ -23,6 +23,7 @@ import { DefaultIamClient } from '../../shared/clients/iamClient' import { ErrorInformation } from '../../shared/errors' import { sshAgentSocketVariable, + SSHError, startSshAgent, startVscodeRemote, testSshConnection, @@ -154,13 +155,6 @@ export class Ec2Connecter implements vscode.Disposable { } } - public throwGeneralConnectionError(selection: Ec2Selection, error: Error) { - this.throwConnectionError('Unable to connect to target instance. ', selection, { - code: 'EC2SSMConnect', - cause: error, - }) - } - public async checkForStartSessionError(selection: Ec2Selection): Promise { await this.checkForInstanceStatusError(selection) @@ -189,7 +183,7 @@ export class Ec2Connecter implements vscode.Disposable { const response = await this.ssmClient.startSession(selection.instanceId) await this.openSessionInTerminal(response, selection) } catch (err: unknown) { - this.throwGeneralConnectionError(selection, err as Error) + this.throwConnectionError('', selection, err as Error) } } @@ -209,7 +203,8 @@ export class Ec2Connecter implements vscode.Disposable { ) await startVscodeRemote(remoteEnv.SessionProcess, remoteEnv.hostname, '/', remoteEnv.vscPath, remoteUser) } catch (err) { - this.throwGeneralConnectionError(selection, err as Error) + const message = err instanceof SSHError ? 'Testing SSH connection to instance failed' : '' + this.throwConnectionError(message, selection, err as Error) } finally { await this.ssmClient.terminateSession(testSession) } diff --git a/packages/core/src/shared/extensions/ssh.ts b/packages/core/src/shared/extensions/ssh.ts index e88580f6a32..6f5d3ad13cf 100644 --- a/packages/core/src/shared/extensions/ssh.ts +++ b/packages/core/src/shared/extensions/ssh.ts @@ -13,11 +13,14 @@ import { ArrayConstructor, NonNullObject } from '../utilities/typeConstructors' import { Settings } from '../settings' import { VSCODE_EXTENSION_ID } from '../extensions' import { SSM } from 'aws-sdk' +import { ToolkitError } from '../errors' const localize = nls.loadMessageBundle() export const sshAgentSocketVariable = 'SSH_AUTH_SOCK' +export class SSHError extends ToolkitError {} + export function getSshConfigPath(): string { const sshConfigDir = path.join(fs.getUserHomeDir(), '.ssh') return path.join(sshConfigDir, 'config') @@ -136,7 +139,7 @@ export async function testSshConnection( }) } catch (error) { getLogger().error('SSH connection test failed: %O', error) - throw error + throw new SSHError('SSH connection test failed', { cause: error as Error }) } } From fda50015292620cb3bf9fd22b25f49fd1a44f53e Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 15 Nov 2024 18:02:44 -0500 Subject: [PATCH 07/20] undo package.json changes --- packages/toolkit/package.json | 83 +++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 8e39296ba4b..3c958454094 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -4066,271 +4066,278 @@ "fontCharacter": "\\f1b4" } }, - "aws-amazonq-transform-logo": { + "aws-amazonq-transform-landing-page-icon": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b5" } }, - "aws-amazonq-transform-step-into-dark": { + "aws-amazonq-transform-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b6" } }, - "aws-amazonq-transform-step-into-light": { + "aws-amazonq-transform-step-into-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b7" } }, - "aws-amazonq-transform-variables-dark": { + "aws-amazonq-transform-step-into-light": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b8" } }, - "aws-amazonq-transform-variables-light": { + "aws-amazonq-transform-variables-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b9" } }, - "aws-applicationcomposer-icon": { + "aws-amazonq-transform-variables-light": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1ba" } }, - "aws-applicationcomposer-icon-dark": { + "aws-applicationcomposer-icon": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1bb" } }, - "aws-apprunner-service": { + "aws-applicationcomposer-icon-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1bc" } }, - "aws-cdk-logo": { + "aws-apprunner-service": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1bd" } }, - "aws-cloudformation-stack": { + "aws-cdk-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1be" } }, - "aws-cloudwatch-log-group": { + "aws-cloudformation-stack": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1bf" } }, - "aws-codecatalyst-logo": { + "aws-cloudwatch-log-group": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c0" } }, - "aws-codewhisperer-icon-black": { + "aws-codecatalyst-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c1" } }, - "aws-codewhisperer-icon-white": { + "aws-codewhisperer-icon-black": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c2" } }, - "aws-codewhisperer-learn": { + "aws-codewhisperer-icon-white": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c3" } }, - "aws-ecr-registry": { + "aws-codewhisperer-learn": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c4" } }, - "aws-ecs-cluster": { + "aws-ecr-registry": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c5" } }, - "aws-ecs-container": { + "aws-ecs-cluster": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c6" } }, - "aws-ecs-service": { + "aws-ecs-container": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c7" } }, - "aws-generic-attach-file": { + "aws-ecs-service": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c8" } }, - "aws-iot-certificate": { + "aws-generic-attach-file": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c9" } }, - "aws-iot-policy": { + "aws-iot-certificate": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1ca" } }, - "aws-iot-thing": { + "aws-iot-policy": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1cb" } }, - "aws-lambda-function": { + "aws-iot-thing": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1cc" } }, - "aws-mynah-MynahIconBlack": { + "aws-lambda-function": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1cd" } }, - "aws-mynah-MynahIconWhite": { + "aws-mynah-MynahIconBlack": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1ce" } }, - "aws-mynah-logo": { + "aws-mynah-MynahIconWhite": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1cf" } }, - "aws-redshift-cluster": { + "aws-mynah-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d0" } }, - "aws-redshift-cluster-connected": { + "aws-redshift-cluster": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d1" } }, - "aws-redshift-database": { + "aws-redshift-cluster-connected": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d2" } }, - "aws-redshift-redshift-cluster-connected": { + "aws-redshift-database": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d3" } }, - "aws-redshift-schema": { + "aws-redshift-redshift-cluster-connected": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d4" } }, - "aws-redshift-table": { + "aws-redshift-schema": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d5" } }, - "aws-s3-bucket": { + "aws-redshift-table": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d6" } }, - "aws-s3-create-bucket": { + "aws-s3-bucket": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d7" } }, - "aws-schemas-registry": { + "aws-s3-create-bucket": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d8" } }, - "aws-schemas-schema": { + "aws-schemas-registry": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d9" } }, - "aws-stepfunctions-preview": { + "aws-schemas-schema": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1da" } + }, + "aws-stepfunctions-preview": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1db" + } } }, "notebooks": [ From 33c99d46d69277a1a3635fddd53c45494e918f97 Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Mon, 18 Nov 2024 08:56:56 -0500 Subject: [PATCH 08/20] Update packages/core/src/shared/remoteSession.ts Co-authored-by: Justin M. Keyes --- packages/core/src/shared/remoteSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/shared/remoteSession.ts b/packages/core/src/shared/remoteSession.ts index 6b269e08734..9f51c747de7 100644 --- a/packages/core/src/shared/remoteSession.ts +++ b/packages/core/src/shared/remoteSession.ts @@ -253,7 +253,7 @@ export async function getDeniedSsmActions(client: IamClient, roleArn: string): P } /** - * Creates a new {@link ChildProcess} class bound to a specific dev environment. All instances of this + * Creates a new {@link ChildProcess} class bound to a specific remote environment. All instances of this * derived class will have SSM session information injected as environment variables as-needed. */ export function createBoundProcess(envProvider: EnvProvider): typeof ChildProcess { From ead068baae553ff2906e9300c23c694338b499dc Mon Sep 17 00:00:00 2001 From: hkobew Date: Mon, 18 Nov 2024 09:29:45 -0500 Subject: [PATCH 09/20] add tests --- packages/core/src/shared/extensions/ssh.ts | 8 +-- .../src/test/shared/extensions/ssh.test.ts | 70 ++++++++++++++++++- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/packages/core/src/shared/extensions/ssh.ts b/packages/core/src/shared/extensions/ssh.ts index 6f5d3ad13cf..d785aebf127 100644 --- a/packages/core/src/shared/extensions/ssh.ts +++ b/packages/core/src/shared/extensions/ssh.ts @@ -8,7 +8,7 @@ import * as path from 'path' import * as nls from 'vscode-nls' import fs from '../fs/fs' import { getLogger } from '../logger' -import { ChildProcess } from '../utilities/processUtils' +import { ChildProcess, ChildProcessResult } from '../utilities/processUtils' import { ArrayConstructor, NonNullObject } from '../utilities/typeConstructors' import { Settings } from '../settings' import { VSCODE_EXTENSION_ID } from '../extensions' @@ -129,16 +129,16 @@ export async function testSshConnection( sshPath: string, user: string, session: SSM.StartSessionResponse -): Promise { +): Promise { try { const env = { SESSION_ID: session.SessionId, STREAM_URL: session.StreamUrl, TOKEN: session.TokenValue } - await new ProcessClass(sshPath, ['-T', `${user}@${hostname}`, 'echo connected && exit']).run({ + const result = await new ProcessClass(sshPath, ['-T', `${user}@${hostname}`, 'echo connected && exit']).run({ spawnOptions: { env, }, }) + return result } catch (error) { - getLogger().error('SSH connection test failed: %O', error) throw new SSHError('SSH connection test failed', { cause: error as Error }) } } diff --git a/packages/core/src/test/shared/extensions/ssh.test.ts b/packages/core/src/test/shared/extensions/ssh.test.ts index c7abc7095cd..c300341aacd 100644 --- a/packages/core/src/test/shared/extensions/ssh.test.ts +++ b/packages/core/src/test/shared/extensions/ssh.test.ts @@ -4,7 +4,13 @@ */ import * as assert from 'assert' import { ChildProcess } from '../../../shared/utilities/processUtils' -import { startSshAgent } from '../../../shared/extensions/ssh' +import { startSshAgent, testSshConnection } from '../../../shared/extensions/ssh' +import { createBoundProcess } from '../../../shared/remoteSession' +import { createExecutableFile, createTestWorkspaceFolder } from '../../testUtil' +import { WorkspaceFolder } from 'vscode' +import path from 'path' +import { SSM } from 'aws-sdk' +import { fs } from '../../../shared/fs/fs' describe('SSH Agent', function () { it('can start the agent on windows', async function () { @@ -29,3 +35,65 @@ describe('SSH Agent', function () { assert.strictEqual(await getStatus(), 'Running') }) }) + +describe('testSshConnection', function () { + let testWorkspace: WorkspaceFolder + let sshPath: string + + before(async function () { + testWorkspace = await createTestWorkspaceFolder() + sshPath = path.join(testWorkspace.uri.fsPath, 'fakeSSH') + }) + + after(async function () { + await fs.delete(testWorkspace.uri.fsPath, { recursive: true, force: true }) + await fs.delete(sshPath, { force: true }) + }) + + it('runs in bound process', async function () { + const envProvider = async () => ({ MY_VAR: 'yes' }) + const process = createBoundProcess(envProvider) + const session = { + SessionId: 'testSession', + StreamUrl: 'testUrl', + TokenValue: 'testToken', + } as SSM.StartSessionResponse + + await createExecutableFile(sshPath, 'echo "$MY_VAR"') + const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', session) + assert.strictEqual(r.stdout, 'yes') + await createExecutableFile(sshPath, 'echo "$UNDEFINED"') + const r2 = await testSshConnection(process, 'localhost', sshPath, 'test-user', session) + assert.strictEqual(r2.stdout, '') + }) + + it('injects new session into env', async function () { + const oldSession = { + SessionId: 'testSession1', + StreamUrl: 'testUrl1', + TokenValue: 'testToken1', + } as SSM.StartSessionResponse + const newSession = { + SessionId: 'testSession2', + StreamUrl: 'testUrl2', + TokenValue: 'testToken2', + } as SSM.StartSessionResponse + const envProvider = async () => ({ + SESSION_ID: oldSession.SessionId, + STREAM_URL: oldSession.StreamUrl, + TOKEN: oldSession.TokenValue, + }) + const process = createBoundProcess(envProvider) + + await createExecutableFile(sshPath, 'echo "$SESSION_ID, $STREAM_URL, $TOKEN"') + const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', newSession) + assert.strictEqual(r.stdout, `${newSession.SessionId}, ${newSession.StreamUrl}, ${newSession.TokenValue}`) + }) + + it('passes proper args to the ssh invoke', async function () { + const process = createBoundProcess(async () => ({})) + await createExecutableFile(sshPath, 'echo "$1,$2"') + const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', {} as SSM.StartSessionResponse) + assert.strictEqual(r.stdout, '-T,test-user@localhost') + }) +}) From 5c29dcd39078ad0c46b71bfee3c6b6dc706d7e81 Mon Sep 17 00:00:00 2001 From: hkobew Date: Mon, 18 Nov 2024 10:41:43 -0500 Subject: [PATCH 10/20] adjust tests to work on windows --- .../src/test/shared/extensions/ssh.test.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/core/src/test/shared/extensions/ssh.test.ts b/packages/core/src/test/shared/extensions/ssh.test.ts index c300341aacd..9f158e758d7 100644 --- a/packages/core/src/test/shared/extensions/ssh.test.ts +++ b/packages/core/src/test/shared/extensions/ssh.test.ts @@ -11,6 +11,7 @@ import { WorkspaceFolder } from 'vscode' import path from 'path' import { SSM } from 'aws-sdk' import { fs } from '../../../shared/fs/fs' +import { isWin } from '../../../shared/vscode/env' describe('SSH Agent', function () { it('can start the agent on windows', async function () { @@ -36,6 +37,11 @@ describe('SSH Agent', function () { }) }) +function echoEnvVarsCmd(varNames: string[]) { + const toShell = (s: string) => (isWin() ? `%${s}%` : `$${s}`) + return `echo "${varNames.map(toShell).join(' ')}"` +} + describe('testSshConnection', function () { let testWorkspace: WorkspaceFolder let sshPath: string @@ -59,10 +65,10 @@ describe('testSshConnection', function () { TokenValue: 'testToken', } as SSM.StartSessionResponse - await createExecutableFile(sshPath, 'echo "$MY_VAR"') + await createExecutableFile(sshPath, echoEnvVarsCmd(['MY_VAR'])) const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', session) assert.strictEqual(r.stdout, 'yes') - await createExecutableFile(sshPath, 'echo "$UNDEFINED"') + await createExecutableFile(sshPath, echoEnvVarsCmd(['UNDEFINED_VAR'])) const r2 = await testSshConnection(process, 'localhost', sshPath, 'test-user', session) assert.strictEqual(r2.stdout, '') }) @@ -85,15 +91,16 @@ describe('testSshConnection', function () { }) const process = createBoundProcess(envProvider) - await createExecutableFile(sshPath, 'echo "$SESSION_ID, $STREAM_URL, $TOKEN"') + await createExecutableFile(sshPath, echoEnvVarsCmd(['SESSION_ID', 'STREAM_URL', 'TOKEN'])) const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', newSession) - assert.strictEqual(r.stdout, `${newSession.SessionId}, ${newSession.StreamUrl}, ${newSession.TokenValue}`) + assert.strictEqual(r.stdout, `${newSession.SessionId} ${newSession.StreamUrl} ${newSession.TokenValue}`) }) it('passes proper args to the ssh invoke', async function () { + const executableFileContent = isWin() ? `echo "$Args[0] $Args[1]"` : `echo "$1 $2"` const process = createBoundProcess(async () => ({})) - await createExecutableFile(sshPath, 'echo "$1,$2"') + await createExecutableFile(sshPath, executableFileContent) const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', {} as SSM.StartSessionResponse) - assert.strictEqual(r.stdout, '-T,test-user@localhost') + assert.strictEqual(r.stdout, '-T test-user@localhost') }) }) From cd61f43245c88a455500a7a5446cbf91e8b04d89 Mon Sep 17 00:00:00 2001 From: hkobew Date: Mon, 18 Nov 2024 13:10:07 -0500 Subject: [PATCH 11/20] add cmd extension on windows only --- packages/core/src/test/shared/extensions/ssh.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/test/shared/extensions/ssh.test.ts b/packages/core/src/test/shared/extensions/ssh.test.ts index 9f158e758d7..7748cce4560 100644 --- a/packages/core/src/test/shared/extensions/ssh.test.ts +++ b/packages/core/src/test/shared/extensions/ssh.test.ts @@ -38,7 +38,7 @@ describe('SSH Agent', function () { }) function echoEnvVarsCmd(varNames: string[]) { - const toShell = (s: string) => (isWin() ? `%${s}%` : `$${s}`) + const toShell = (s: string) => `$${s}` return `echo "${varNames.map(toShell).join(' ')}"` } @@ -48,7 +48,7 @@ describe('testSshConnection', function () { before(async function () { testWorkspace = await createTestWorkspaceFolder() - sshPath = path.join(testWorkspace.uri.fsPath, 'fakeSSH') + sshPath = path.join(testWorkspace.uri.fsPath, `fakeSSH${isWin() ? '.cmd' : ''}`) }) after(async function () { From cc03cb0d171c7540012c8032c60cdc10a6c8931a Mon Sep 17 00:00:00 2001 From: hkobew Date: Mon, 18 Nov 2024 13:46:40 -0500 Subject: [PATCH 12/20] adjust test helper function --- packages/core/src/test/shared/extensions/ssh.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/test/shared/extensions/ssh.test.ts b/packages/core/src/test/shared/extensions/ssh.test.ts index 7748cce4560..f6ca1b1f652 100644 --- a/packages/core/src/test/shared/extensions/ssh.test.ts +++ b/packages/core/src/test/shared/extensions/ssh.test.ts @@ -38,7 +38,7 @@ describe('SSH Agent', function () { }) function echoEnvVarsCmd(varNames: string[]) { - const toShell = (s: string) => `$${s}` + const toShell = (s: string) => (isWin() ? `%${s}%` : `$${s}`) return `echo "${varNames.map(toShell).join(' ')}"` } From 43f9f2463af60c0c7979550289f28c48dcf3420e Mon Sep 17 00:00:00 2001 From: hkobew Date: Mon, 18 Nov 2024 14:10:03 -0500 Subject: [PATCH 13/20] try a more advanced parsing scheme --- .../core/src/test/shared/extensions/ssh.test.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/core/src/test/shared/extensions/ssh.test.ts b/packages/core/src/test/shared/extensions/ssh.test.ts index f6ca1b1f652..fd9b3e46827 100644 --- a/packages/core/src/test/shared/extensions/ssh.test.ts +++ b/packages/core/src/test/shared/extensions/ssh.test.ts @@ -42,6 +42,11 @@ function echoEnvVarsCmd(varNames: string[]) { return `echo "${varNames.map(toShell).join(' ')}"` } +function parseOutput(output: string) { + // On Windows the final line is the result of the script. + return isWin() ? output.split('\n').at(-1) : output +} + describe('testSshConnection', function () { let testWorkspace: WorkspaceFolder let sshPath: string @@ -70,7 +75,8 @@ describe('testSshConnection', function () { assert.strictEqual(r.stdout, 'yes') await createExecutableFile(sshPath, echoEnvVarsCmd(['UNDEFINED_VAR'])) const r2 = await testSshConnection(process, 'localhost', sshPath, 'test-user', session) - assert.strictEqual(r2.stdout, '') + + assert.strictEqual(parseOutput(r2.stdout), '') }) it('injects new session into env', async function () { @@ -93,14 +99,17 @@ describe('testSshConnection', function () { await createExecutableFile(sshPath, echoEnvVarsCmd(['SESSION_ID', 'STREAM_URL', 'TOKEN'])) const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', newSession) - assert.strictEqual(r.stdout, `${newSession.SessionId} ${newSession.StreamUrl} ${newSession.TokenValue}`) + assert.strictEqual( + parseOutput(r.stdout), + `${newSession.SessionId} ${newSession.StreamUrl} ${newSession.TokenValue}` + ) }) it('passes proper args to the ssh invoke', async function () { - const executableFileContent = isWin() ? `echo "$Args[0] $Args[1]"` : `echo "$1 $2"` + const executableFileContent = isWin() ? `echo "%1 %2"` : `echo "$1 $2"` const process = createBoundProcess(async () => ({})) await createExecutableFile(sshPath, executableFileContent) const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', {} as SSM.StartSessionResponse) - assert.strictEqual(r.stdout, '-T test-user@localhost') + assert.strictEqual(parseOutput(r.stdout), '-T test-user@localhost') }) }) From ce3dd45f62c1f02a80628673ffedf9b6a21bfe25 Mon Sep 17 00:00:00 2001 From: hkobew Date: Mon, 18 Nov 2024 14:23:51 -0500 Subject: [PATCH 14/20] remove quotes from windows output --- packages/core/src/test/shared/extensions/ssh.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/test/shared/extensions/ssh.test.ts b/packages/core/src/test/shared/extensions/ssh.test.ts index fd9b3e46827..35180aee8ea 100644 --- a/packages/core/src/test/shared/extensions/ssh.test.ts +++ b/packages/core/src/test/shared/extensions/ssh.test.ts @@ -43,8 +43,8 @@ function echoEnvVarsCmd(varNames: string[]) { } function parseOutput(output: string) { - // On Windows the final line is the result of the script. - return isWin() ? output.split('\n').at(-1) : output + // On Windows the final line is the result of the script, and it wraps result in `"` + return isWin() ? output.split('\n').at(-1)?.replace('"', '') : output } describe('testSshConnection', function () { @@ -72,7 +72,7 @@ describe('testSshConnection', function () { await createExecutableFile(sshPath, echoEnvVarsCmd(['MY_VAR'])) const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', session) - assert.strictEqual(r.stdout, 'yes') + assert.strictEqual(parseOutput(r.stdout), 'yes') await createExecutableFile(sshPath, echoEnvVarsCmd(['UNDEFINED_VAR'])) const r2 = await testSshConnection(process, 'localhost', sshPath, 'test-user', session) From c9a1b2da94959af8e167c89eb0f28aaa6fbc6488 Mon Sep 17 00:00:00 2001 From: hkobew Date: Mon, 18 Nov 2024 16:20:47 -0500 Subject: [PATCH 15/20] avoid complicated parsing, settle for includes --- .../src/test/shared/extensions/ssh.test.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/core/src/test/shared/extensions/ssh.test.ts b/packages/core/src/test/shared/extensions/ssh.test.ts index 35180aee8ea..06a34b12b27 100644 --- a/packages/core/src/test/shared/extensions/ssh.test.ts +++ b/packages/core/src/test/shared/extensions/ssh.test.ts @@ -42,9 +42,9 @@ function echoEnvVarsCmd(varNames: string[]) { return `echo "${varNames.map(toShell).join(' ')}"` } -function parseOutput(output: string) { - // On Windows the final line is the result of the script, and it wraps result in `"` - return isWin() ? output.split('\n').at(-1)?.replace('"', '') : output +// Windows gives some junk in its output. +function cleanOutput(output: string) { + return output.trim().split('\n').at(-1)?.replace('"', '') ?? '' } describe('testSshConnection', function () { @@ -72,11 +72,7 @@ describe('testSshConnection', function () { await createExecutableFile(sshPath, echoEnvVarsCmd(['MY_VAR'])) const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', session) - assert.strictEqual(parseOutput(r.stdout), 'yes') - await createExecutableFile(sshPath, echoEnvVarsCmd(['UNDEFINED_VAR'])) - const r2 = await testSshConnection(process, 'localhost', sshPath, 'test-user', session) - - assert.strictEqual(parseOutput(r2.stdout), '') + assert.ok(cleanOutput(r.stdout).includes('yes')) }) it('injects new session into env', async function () { @@ -99,9 +95,8 @@ describe('testSshConnection', function () { await createExecutableFile(sshPath, echoEnvVarsCmd(['SESSION_ID', 'STREAM_URL', 'TOKEN'])) const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', newSession) - assert.strictEqual( - parseOutput(r.stdout), - `${newSession.SessionId} ${newSession.StreamUrl} ${newSession.TokenValue}` + assert.ok( + cleanOutput(r.stdout).includes(`${newSession.SessionId} ${newSession.StreamUrl} ${newSession.TokenValue}`) ) }) @@ -110,6 +105,6 @@ describe('testSshConnection', function () { const process = createBoundProcess(async () => ({})) await createExecutableFile(sshPath, executableFileContent) const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', {} as SSM.StartSessionResponse) - assert.strictEqual(parseOutput(r.stdout), '-T test-user@localhost') + assert.ok(cleanOutput(r.stdout).includes('-T test-user@localhost')) }) }) From 0ae14858df7bdd99be749eb3445d15eddc1eb62d Mon Sep 17 00:00:00 2001 From: hkobew Date: Mon, 18 Nov 2024 16:49:48 -0500 Subject: [PATCH 16/20] factor pattern out into shared method --- .../core/src/test/shared/extensions/ssh.test.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/core/src/test/shared/extensions/ssh.test.ts b/packages/core/src/test/shared/extensions/ssh.test.ts index 06a34b12b27..1ee2f789e0e 100644 --- a/packages/core/src/test/shared/extensions/ssh.test.ts +++ b/packages/core/src/test/shared/extensions/ssh.test.ts @@ -42,9 +42,10 @@ function echoEnvVarsCmd(varNames: string[]) { return `echo "${varNames.map(toShell).join(' ')}"` } -// Windows gives some junk in its output. -function cleanOutput(output: string) { - return output.trim().split('\n').at(-1)?.replace('"', '') ?? '' +function assertOutputContains(rawOutput: string, expectedString: string): void | never { + // Windows gives some junk we want to trim + const output = rawOutput.trim().split('\n').at(-1)?.replace('"', '') ?? '' + assert.ok(output.includes(expectedString), `Expected output to contain "${expectedString}", but got "${output}"`) } describe('testSshConnection', function () { @@ -72,7 +73,7 @@ describe('testSshConnection', function () { await createExecutableFile(sshPath, echoEnvVarsCmd(['MY_VAR'])) const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', session) - assert.ok(cleanOutput(r.stdout).includes('yes')) + assertOutputContains(r.stdout, 'yes') }) it('injects new session into env', async function () { @@ -95,9 +96,7 @@ describe('testSshConnection', function () { await createExecutableFile(sshPath, echoEnvVarsCmd(['SESSION_ID', 'STREAM_URL', 'TOKEN'])) const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', newSession) - assert.ok( - cleanOutput(r.stdout).includes(`${newSession.SessionId} ${newSession.StreamUrl} ${newSession.TokenValue}`) - ) + assertOutputContains(r.stdout, `${newSession.SessionId} ${newSession.StreamUrl} ${newSession.TokenValue}`) }) it('passes proper args to the ssh invoke', async function () { @@ -105,6 +104,6 @@ describe('testSshConnection', function () { const process = createBoundProcess(async () => ({})) await createExecutableFile(sshPath, executableFileContent) const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', {} as SSM.StartSessionResponse) - assert.ok(cleanOutput(r.stdout).includes('-T test-user@localhost')) + assertOutputContains(r.stdout, '-T test-user@localhost') }) }) From da2ad80a16fbbdd92aaabeae575c27d1b607082b Mon Sep 17 00:00:00 2001 From: hkobew Date: Mon, 18 Nov 2024 18:28:44 -0500 Subject: [PATCH 17/20] split assertion --- packages/core/src/test/shared/extensions/ssh.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core/src/test/shared/extensions/ssh.test.ts b/packages/core/src/test/shared/extensions/ssh.test.ts index 1ee2f789e0e..38874e2df68 100644 --- a/packages/core/src/test/shared/extensions/ssh.test.ts +++ b/packages/core/src/test/shared/extensions/ssh.test.ts @@ -42,8 +42,10 @@ function echoEnvVarsCmd(varNames: string[]) { return `echo "${varNames.map(toShell).join(' ')}"` } +/** + * Trim noisy windows ChildProcess result to final line for easier testing. + */ function assertOutputContains(rawOutput: string, expectedString: string): void | never { - // Windows gives some junk we want to trim const output = rawOutput.trim().split('\n').at(-1)?.replace('"', '') ?? '' assert.ok(output.includes(expectedString), `Expected output to contain "${expectedString}", but got "${output}"`) } @@ -104,6 +106,7 @@ describe('testSshConnection', function () { const process = createBoundProcess(async () => ({})) await createExecutableFile(sshPath, executableFileContent) const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', {} as SSM.StartSessionResponse) - assertOutputContains(r.stdout, '-T test-user@localhost') + assertOutputContains(r.stdout, '-T') + assertOutputContains(r.stdout, 'test-user@localhost') }) }) From 17fe76c7c4d9eff0772510faa056072901928c87 Mon Sep 17 00:00:00 2001 From: hkobew Date: Wed, 20 Nov 2024 10:09:11 -0500 Subject: [PATCH 18/20] add clearer logging message --- packages/core/src/shared/extensions/ssh.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/src/shared/extensions/ssh.ts b/packages/core/src/shared/extensions/ssh.ts index d785aebf127..40e235d1ac1 100644 --- a/packages/core/src/shared/extensions/ssh.ts +++ b/packages/core/src/shared/extensions/ssh.ts @@ -132,7 +132,11 @@ export async function testSshConnection( ): Promise { try { const env = { SESSION_ID: session.SessionId, STREAM_URL: session.StreamUrl, TOKEN: session.TokenValue } - const result = await new ProcessClass(sshPath, ['-T', `${user}@${hostname}`, 'echo connected && exit']).run({ + const result = await new ProcessClass(sshPath, [ + '-T', + `${user}@${hostname}`, + 'echo "test connection succeeded" && exit', + ]).run({ spawnOptions: { env, }, From 3999851d5e3d7b2394a06e62186859c6e66f05bd Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:21:45 -0500 Subject: [PATCH 19/20] Update packages/core/src/shared/extensions/ssh.ts Co-authored-by: Justin M. Keyes --- packages/core/src/shared/extensions/ssh.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/shared/extensions/ssh.ts b/packages/core/src/shared/extensions/ssh.ts index 40e235d1ac1..dbd18c56ddf 100644 --- a/packages/core/src/shared/extensions/ssh.ts +++ b/packages/core/src/shared/extensions/ssh.ts @@ -19,7 +19,7 @@ const localize = nls.loadMessageBundle() export const sshAgentSocketVariable = 'SSH_AUTH_SOCK' -export class SSHError extends ToolkitError {} +export class SshError extends ToolkitError {} export function getSshConfigPath(): string { const sshConfigDir = path.join(fs.getUserHomeDir(), '.ssh') From b345aa55a6f52f5eafb2d2e30d195ada58d6abb4 Mon Sep 17 00:00:00 2001 From: hkobew Date: Wed, 4 Dec 2024 14:27:42 -0500 Subject: [PATCH 20/20] add error code to SshError --- packages/core/src/awsService/ec2/model.ts | 4 ++-- packages/core/src/shared/extensions/ssh.ts | 13 ++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/core/src/awsService/ec2/model.ts b/packages/core/src/awsService/ec2/model.ts index 1635e6aad8c..085bfa0674b 100644 --- a/packages/core/src/awsService/ec2/model.ts +++ b/packages/core/src/awsService/ec2/model.ts @@ -23,7 +23,7 @@ import { DefaultIamClient } from '../../shared/clients/iamClient' import { ErrorInformation } from '../../shared/errors' import { sshAgentSocketVariable, - SSHError, + SshError, startSshAgent, startVscodeRemote, testSshConnection, @@ -203,7 +203,7 @@ export class Ec2Connecter implements vscode.Disposable { ) await startVscodeRemote(remoteEnv.SessionProcess, remoteEnv.hostname, '/', remoteEnv.vscPath, remoteUser) } catch (err) { - const message = err instanceof SSHError ? 'Testing SSH connection to instance failed' : '' + const message = err instanceof SshError ? 'Testing SSH connection to instance failed' : '' this.throwConnectionError(message, selection, err as Error) } finally { await this.ssmClient.terminateSession(testSession) diff --git a/packages/core/src/shared/extensions/ssh.ts b/packages/core/src/shared/extensions/ssh.ts index dbd18c56ddf..1e75f9921aa 100644 --- a/packages/core/src/shared/extensions/ssh.ts +++ b/packages/core/src/shared/extensions/ssh.ts @@ -13,13 +13,20 @@ import { ArrayConstructor, NonNullObject } from '../utilities/typeConstructors' import { Settings } from '../settings' import { VSCODE_EXTENSION_ID } from '../extensions' import { SSM } from 'aws-sdk' -import { ToolkitError } from '../errors' +import { ErrorInformation, ToolkitError } from '../errors' const localize = nls.loadMessageBundle() export const sshAgentSocketVariable = 'SSH_AUTH_SOCK' -export class SshError extends ToolkitError {} +export class SshError extends ToolkitError { + constructor(message: string, options: ErrorInformation) { + super(message, { + ...options, + code: SshError.name, + }) + } +} export function getSshConfigPath(): string { const sshConfigDir = path.join(fs.getUserHomeDir(), '.ssh') @@ -143,7 +150,7 @@ export async function testSshConnection( }) return result } catch (error) { - throw new SSHError('SSH connection test failed', { cause: error as Error }) + throw new SshError('SSH connection test failed', { cause: error as Error }) } }