From b7399c57478346716ddbedfbfc7124d7426fadc1 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 7 Jul 2023 14:03:15 -0700 Subject: [PATCH 001/172] rename existing command to openTerminal --- package.json | 8 ++++---- package.nls.json | 2 +- src/ec2/activation.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 475967786a1..b44fe830d43 100644 --- a/package.json +++ b/package.json @@ -1111,7 +1111,7 @@ "command": "aws.auth.manageConnections" }, { - "command": "aws.ec2.connectToInstance", + "command": "aws.ec2.openTerminal", "when": "aws.isDevMode" }, { @@ -1309,7 +1309,7 @@ "group": "inline@1" }, { - "command": "aws.ec2.connectToInstance", + "command": "aws.ec2.openTerminal", "group": "0@1", "when": "viewItem == awsEc2Node" }, @@ -2056,8 +2056,8 @@ } }, { - "command": "aws.ec2.connectToInstance", - "title": "%AWS.command.ec2.connectToInstance%", + "command": "aws.ec2.openTerminal", + "title": "%AWS.command.ec2.openTerminal%", "category": "%AWS.title%", "cloud9": { "cn": { diff --git a/package.nls.json b/package.nls.json index ed1412049f1..2adf8ac48ca 100644 --- a/package.nls.json +++ b/package.nls.json @@ -108,7 +108,7 @@ "AWS.command.refreshAwsExplorer": "Refresh Explorer", "AWS.command.refreshCdkExplorer": "Refresh CDK Explorer", "AWS.command.cdk.help": "View CDK Documentation", - "AWS.command.ec2.connectToInstance": "Connect to EC2 Instance...", + "AWS.command.ec2.openTerminal": "Open terminal in EC2 instance...", "AWS.command.ecr.copyTagUri": "Copy Tag URI", "AWS.command.ecr.copyRepositoryUri": "Copy Repository URI", "AWS.command.ecr.createRepository": "Create Repository...", diff --git a/src/ec2/activation.ts b/src/ec2/activation.ts index a723fe76267..66c7ef451a8 100644 --- a/src/ec2/activation.ts +++ b/src/ec2/activation.ts @@ -10,7 +10,7 @@ import { Ec2InstanceNode } from './explorer/ec2InstanceNode' export async function activate(ctx: ExtContext): Promise { ctx.extensionContext.subscriptions.push( - Commands.register('aws.ec2.connectToInstance', async (node?: Ec2InstanceNode) => { + Commands.register('aws.ec2.openTerminal', async (node?: Ec2InstanceNode) => { await telemetry.ec2_connectToInstance.run(async span => { span.record({ ec2ConnectionType: 'ssm' }) await (node ? tryConnect(node.toSelection()) : tryConnect()) From 588594d2f460af92b2aacec94d13141e1952e649 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 7 Jul 2023 14:15:39 -0700 Subject: [PATCH 002/172] add new command for ec2 instance remote-connect --- package.json | 14 ++++++++++++++ package.nls.json | 1 + 2 files changed, 15 insertions(+) diff --git a/package.json b/package.json index b44fe830d43..859acd52a07 100644 --- a/package.json +++ b/package.json @@ -1110,6 +1110,10 @@ { "command": "aws.auth.manageConnections" }, + { + "command": "aws.ec2.openRemoteConnection", + "when": "aws.isDevMode" + }, { "command": "aws.ec2.openTerminal", "when": "aws.isDevMode" @@ -2065,6 +2069,16 @@ } } }, + { + "command": "aws.ec2.openRemoteConnection", + "title": "%AWS.command.ec2.openRemoteConnection%", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, { "command": "aws.ecr.copyTagUri", "title": "%AWS.command.ecr.copyTagUri%", diff --git a/package.nls.json b/package.nls.json index 2adf8ac48ca..0185c848605 100644 --- a/package.nls.json +++ b/package.nls.json @@ -109,6 +109,7 @@ "AWS.command.refreshCdkExplorer": "Refresh CDK Explorer", "AWS.command.cdk.help": "View CDK Documentation", "AWS.command.ec2.openTerminal": "Open terminal in EC2 instance...", + "AWS.command.ec2.openRemoteConnection": "Open EC2 instance in VSCode remote...", "AWS.command.ecr.copyTagUri": "Copy Tag URI", "AWS.command.ecr.copyRepositoryUri": "Copy Repository URI", "AWS.command.ecr.createRepository": "Create Repository...", From 1c57e45a1d712a5f7c4d0898b9e820d0f5348df2 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 7 Jul 2023 14:18:31 -0700 Subject: [PATCH 003/172] register new command so that it does not throw error --- src/ec2/activation.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ec2/activation.ts b/src/ec2/activation.ts index 66c7ef451a8..b47f453295d 100644 --- a/src/ec2/activation.ts +++ b/src/ec2/activation.ts @@ -15,6 +15,10 @@ export async function activate(ctx: ExtContext): Promise { span.record({ ec2ConnectionType: 'ssm' }) await (node ? tryConnect(node.toSelection()) : tryConnect()) }) + }), + + Commands.register('aws.ec2.openRemoteConnection', async (node?: Ec2InstanceNode) => { + console.log('You have just run the aws.ec2.openRemoteConnection command!') }) ) } From 814ba9614d859977bfeb09c3d8ce72529c43dec2 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 7 Jul 2023 14:27:02 -0700 Subject: [PATCH 004/172] refactor to make terminal distinction clearer --- src/ec2/activation.ts | 8 ++++++-- src/ec2/commands.ts | 32 -------------------------------- src/ec2/model.ts | 2 +- src/ec2/prompter.ts | 18 ++++++++++++++++++ 4 files changed, 25 insertions(+), 35 deletions(-) delete mode 100644 src/ec2/commands.ts diff --git a/src/ec2/activation.ts b/src/ec2/activation.ts index b47f453295d..19452793b6a 100644 --- a/src/ec2/activation.ts +++ b/src/ec2/activation.ts @@ -4,16 +4,20 @@ */ import { ExtContext } from '../shared/extensions' import { Commands } from '../shared/vscode/commands2' -import { tryConnect } from './commands' import { telemetry } from '../shared/telemetry/telemetry' import { Ec2InstanceNode } from './explorer/ec2InstanceNode' +import { promptUserForEc2Selection } from './prompter' +import { Ec2ConnectionManager } from './model' export async function activate(ctx: ExtContext): Promise { ctx.extensionContext.subscriptions.push( Commands.register('aws.ec2.openTerminal', async (node?: Ec2InstanceNode) => { await telemetry.ec2_connectToInstance.run(async span => { span.record({ ec2ConnectionType: 'ssm' }) - await (node ? tryConnect(node.toSelection()) : tryConnect()) + const selection = node ? node.toSelection() : await promptUserForEc2Selection() + + const connectionManager = new Ec2ConnectionManager(selection.region) + await connectionManager.attemptToOpenEc2Terminal(selection) }) }), diff --git a/src/ec2/commands.ts b/src/ec2/commands.ts deleted file mode 100644 index 8cf7f75eee1..00000000000 --- a/src/ec2/commands.ts +++ /dev/null @@ -1,32 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { createEc2ConnectPrompter, handleEc2ConnectPrompterResponse } from './prompter' -import { isValidResponse } from '../shared/wizards/wizard' -import { Ec2ConnectionManager } from './model' -import { Ec2Selection } from './utils' -import { RegionSubmenuResponse } from '../shared/ui/common/regionSubmenu' -import { PromptResult } from '../shared/ui/prompter' -import { CancellationError } from '../shared/utilities/timeoutUtils' - -function getSelectionFromResponse(response: PromptResult>): Ec2Selection { - if (isValidResponse(response)) { - return handleEc2ConnectPrompterResponse(response) - } else { - throw new CancellationError('user') - } -} - -export async function tryConnect(selection?: Ec2Selection): Promise { - if (!selection) { - const prompter = createEc2ConnectPrompter() - const response = await prompter.prompt() - - selection = getSelectionFromResponse(response) - } - - const ec2Client = new Ec2ConnectionManager(selection.region) - await ec2Client.attemptEc2Connection(selection) -} diff --git a/src/ec2/model.ts b/src/ec2/model.ts index fdcb014c4bd..d32d0d9ea48 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -113,7 +113,7 @@ export class Ec2ConnectionManager { }) } - public async attemptEc2Connection(selection: Ec2Selection): Promise { + public async attemptToOpenEc2Terminal(selection: Ec2Selection): Promise { await this.checkForStartSessionError(selection) try { const response = await this.ssmClient.startSession(selection.instanceId) diff --git a/src/ec2/prompter.ts b/src/ec2/prompter.ts index 333d4056914..a77bba5e4b8 100644 --- a/src/ec2/prompter.ts +++ b/src/ec2/prompter.ts @@ -7,6 +7,9 @@ import { RegionSubmenu, RegionSubmenuResponse } from '../shared/ui/common/region import { Ec2Selection, getInstancesFromRegion } from './utils' import { DataQuickPickItem } from '../shared/ui/pickerPrompter' import { Ec2Instance } from '../shared/clients/ec2Client' +import { PromptResult } from '../shared/ui/prompter' +import { isValidResponse } from '../shared/wizards/wizard' +import { CancellationError } from '../shared/utilities/timeoutUtils' function asQuickpickItem(instance: Ec2Instance): DataQuickPickItem { return { @@ -16,6 +19,21 @@ function asQuickpickItem(instance: Ec2Instance): DataQuickPickItem { } } +function getSelectionFromResponse(response: PromptResult>): Ec2Selection { + if (isValidResponse(response)) { + return handleEc2ConnectPrompterResponse(response) + } else { + throw new CancellationError('user') + } +} + +export async function promptUserForEc2Selection(): Promise { + const prompter = createEc2ConnectPrompter() + const response = await prompter.prompt() + + return getSelectionFromResponse(response) +} + export function handleEc2ConnectPrompterResponse(response: RegionSubmenuResponse): Ec2Selection { return { instanceId: response.data, From b5d2c88394c9430867bb413473ca92e5a68f3276 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 7 Jul 2023 14:52:46 -0700 Subject: [PATCH 005/172] add prompt for new selection --- src/ec2/activation.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ec2/activation.ts b/src/ec2/activation.ts index 19452793b6a..35f732d77b9 100644 --- a/src/ec2/activation.ts +++ b/src/ec2/activation.ts @@ -22,7 +22,9 @@ export async function activate(ctx: ExtContext): Promise { }), Commands.register('aws.ec2.openRemoteConnection', async (node?: Ec2InstanceNode) => { - console.log('You have just run the aws.ec2.openRemoteConnection command!') + const selection = node ? node.toSelection() : await promptUserForEc2Selection() + //const connectionManager = new Ec2ConnectionManager(selection.region) + console.log(selection) }) ) } From 08ba1069165e9e665fcc232d0943545c84735ec2 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 10 Jul 2023 10:12:40 -0700 Subject: [PATCH 006/172] enable connection to ec2, start generalizing CodeCatalyst work --- src/codecatalyst/tools.ts | 95 +++-------------------------- src/ec2/activation.ts | 5 +- src/ec2/model.ts | 36 ++++++++++- src/shared/remoteSession.ts | 118 +++++++++++++++++++++++++++++++++++- 4 files changed, 163 insertions(+), 91 deletions(-) diff --git a/src/codecatalyst/tools.ts b/src/codecatalyst/tools.ts index ac4c8d3dc2c..72f2b972c13 100644 --- a/src/codecatalyst/tools.ts +++ b/src/codecatalyst/tools.ts @@ -11,20 +11,16 @@ const localize = nls.loadMessageBundle() import * as vscode from 'vscode' import * as fs from 'fs-extra' import * as path from 'path' -import { getOrInstallCli } from '../shared/utilities/cliUtils' import { Result } from '../shared/utilities/result' -import { isExtensionInstalled, showInstallExtensionMsg } from '../shared/utilities/vsCodeUtils' -import { SystemUtilities } from '../shared/systemUtilities' -import { pushIf } from '../shared/utilities/collectionUtils' import { ChildProcess } from '../shared/utilities/childProcess' import { CancellationError } from '../shared/utilities/timeoutUtils' import { fileExists, readFileAsString } from '../shared/filesystemUtilities' -import { ToolkitError, UnknownError } from '../shared/errors' +import { ToolkitError } from '../shared/errors' import { getLogger } from '../shared/logger' import { getIdeProperties } from '../shared/extensionUtilities' import { showConfirmationMessage } from '../shared/utilities/messages' import { getSshConfigPath } from '../shared/extensions/ssh' -import { VSCODE_EXTENSION_ID, vscodeExtensionMinVersion } from '../shared/extensions' +import { ensureRemoteSshInstalled, ensureTools, handleMissingTool } from '../shared/remoteSession' interface DependencyPaths { readonly vsc: string @@ -32,7 +28,7 @@ interface DependencyPaths { readonly ssh: string } -interface MissingTool { +export interface MissingTool { readonly name: 'code' | 'ssm' | 'ssh' readonly reason?: string } @@ -40,56 +36,15 @@ interface MissingTool { export const hostNamePrefix = 'aws-devenv-' export async function ensureDependencies(): Promise> { - if (!isExtensionInstalled(VSCODE_EXTENSION_ID.remotessh, vscodeExtensionMinVersion.remotessh)) { - showInstallExtensionMsg( - VSCODE_EXTENSION_ID.remotessh, - 'Remote SSH', - 'Connecting to Dev Environment', - vscodeExtensionMinVersion.remotessh - ) - - if (isExtensionInstalled(VSCODE_EXTENSION_ID.remotessh)) { - return Result.err( - new ToolkitError('Remote SSH extension version is too low', { - cancelled: true, - code: 'ExtensionVersionTooLow', - details: { expected: vscodeExtensionMinVersion.remotessh }, - }) - ) - } else { - return Result.err( - new ToolkitError('Remote SSH extension not installed', { - cancelled: true, - code: 'MissingExtension', - }) - ) - } + try { + await ensureRemoteSshInstalled() + } catch (e) { + return Result.err(e as Error) } const tools = await ensureTools() if (tools.isErr()) { - const missing = tools - .err() - .map(d => d.name) - .join(', ') - const msg = localize( - 'AWS.codecatalyst.missingRequiredTool', - 'Failed to connect to Dev Environment, missing required tools: {0}', - missing - ) - - tools.err().forEach(d => { - if (d.reason) { - getLogger().error(`codecatalyst: failed to get tool "${d.name}": ${d.reason}`) - } - }) - - return Result.err( - new ToolkitError(msg, { - code: 'MissingTools', - details: { missing }, - }) - ) + return await handleMissingTool(tools) } const config = await ensureCodeCatalystSshConfig(tools.ok().ssh) @@ -131,40 +86,6 @@ export async function ensureConnectScript(context = globals.context): Promise UnknownError.cast(e).message) -} - /** * Checks if the "aws-devenv-*" SSH config hostname pattern is working, else prompts user to add it. * diff --git a/src/ec2/activation.ts b/src/ec2/activation.ts index 35f732d77b9..a0c925dc1b6 100644 --- a/src/ec2/activation.ts +++ b/src/ec2/activation.ts @@ -23,8 +23,9 @@ export async function activate(ctx: ExtContext): Promise { Commands.register('aws.ec2.openRemoteConnection', async (node?: Ec2InstanceNode) => { const selection = node ? node.toSelection() : await promptUserForEc2Selection() - //const connectionManager = new Ec2ConnectionManager(selection.region) - console.log(selection) + const connectionManager = new Ec2ConnectionManager(selection.region) + + await connectionManager.attemptToOpenRemoteConnection(selection) }) ) } diff --git a/src/ec2/model.ts b/src/ec2/model.ts index d32d0d9ea48..8063186c945 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -14,9 +14,12 @@ import { Ec2Client } from '../shared/clients/ec2Client' export type Ec2ConnectErrorCode = 'EC2SSMStatus' | 'EC2SSMPermission' | 'EC2SSMConnect' | 'EC2SSMAgentStatus' -import { openRemoteTerminal } from '../shared/remoteSession' +import { ensureDependencies, openRemoteTerminal } from '../shared/remoteSession' import { DefaultIamClient } from '../shared/clients/iamClient' import { ErrorInformation } from '../shared/errors' +import { sshAgentSocketVariable, startSshAgent, startVscodeRemote } from '../shared/extensions/ssh' +import { createBoundProcess } from '../codecatalyst/model' +import { getLogger } from '../shared/logger/logger' export class Ec2ConnectionManager { private ssmClient: SsmClient @@ -126,4 +129,35 @@ export class Ec2ConnectionManager { }) } } + + public async attemptToOpenRemoteConnection(selection: Ec2Selection): Promise { + await this.checkForStartSessionError(selection) + try { + const logPrefix = `ec2 (${selection.instanceId})` + const logger = (data: string) => getLogger().verbose(`${logPrefix}: ${data}`) + const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() + const vars = getEc2SsmEnv(selection.region, ssm) + const envProvider = async () => { + return { [sshAgentSocketVariable]: await startSshAgent(), ...vars } + } + const SessionProcess = createBoundProcess(envProvider).extend({ + onStdout: logger, + onStderr: logger, + rejectOnErrorCode: true, + }) + await startVscodeRemote(SessionProcess, selection.instanceId, '/', vsc) + } catch (e) { + console.log(e as Error) + } + } +} + +function getEc2SsmEnv(region: string, ssmPath: string): NodeJS.ProcessEnv { + return Object.assign( + { + AWS_REGION: region, + AWS_SSM_CLI: ssmPath, + }, + process.env + ) } diff --git a/src/shared/remoteSession.ts b/src/shared/remoteSession.ts index bf44f8145ce..4df2135b613 100644 --- a/src/shared/remoteSession.ts +++ b/src/shared/remoteSession.ts @@ -4,9 +4,21 @@ */ import * as vscode from 'vscode' +import * as nls from 'vscode-nls' +const localize = nls.loadMessageBundle() + import { Settings } from '../shared/settings' import { showMessageWithCancel } from './utilities/messages' -import { Timeout } from './utilities/timeoutUtils' +import { CancellationError, Timeout } from './utilities/timeoutUtils' +import { isExtensionInstalled, showInstallExtensionMsg } from './utilities/vsCodeUtils' +import { VSCODE_EXTENSION_ID, vscodeExtensionMinVersion } from './extensions' +import { Err, Result } from '../shared/utilities/result' +import { ToolkitError, UnknownError } from './errors' +import { MissingTool } from '../codecatalyst/tools' +import { getLogger } from './logger/logger' +import { SystemUtilities } from './systemUtilities' +import { getOrInstallCli } from './utilities/cliUtils' +import { pushIf } from './utilities/collectionUtils' export async function openRemoteTerminal(options: vscode.TerminalOptions, onClose: () => void) { const timeout = new Timeout(60000) @@ -36,3 +48,107 @@ async function withoutShellIntegration(cb: () => T | Promise): Promise Settings.instance.update('terminal.integrated.shellIntegration.enabled', userValue) } } + +interface DependencyPaths { + readonly vsc: string + readonly ssm: string + readonly ssh: string +} + +export async function ensureDependencies(): Promise> { + try { + await ensureRemoteSshInstalled() + } catch (e) { + return Result.err(e as Error) + } + + const tools = await ensureTools() + if (tools.isErr()) { + return await handleMissingTool(tools) + } + + return tools +} + +export async function ensureRemoteSshInstalled(): Promise { + if (!isExtensionInstalled(VSCODE_EXTENSION_ID.remotessh, vscodeExtensionMinVersion.remotessh)) { + showInstallExtensionMsg( + VSCODE_EXTENSION_ID.remotessh, + 'Remote SSH', + 'Connecting to Dev Environment', + vscodeExtensionMinVersion.remotessh + ) + + if (isExtensionInstalled(VSCODE_EXTENSION_ID.remotessh)) { + throw new ToolkitError('Remote SSH extension version is too low', { + cancelled: true, + code: 'ExtensionVersionTooLow', + details: { expected: vscodeExtensionMinVersion.remotessh }, + }) + } else { + throw new ToolkitError('Remote SSH extension not installed', { + cancelled: true, + code: 'MissingExtension', + }) + } + } +} + +/** + * Checks if the SSM plugin CLI `session-manager-plugin` is available and + * working, else prompts user to install it. + * + * @returns Result object indicating whether the SSH config is working, or failure reason. + */ +async function ensureSsmCli() { + const r = await Result.promise(getOrInstallCli('session-manager-plugin', false)) + + return r.mapErr(e => UnknownError.cast(e).message) +} + +export async function ensureTools() { + const [vsc, ssh, ssm] = await Promise.all([ + SystemUtilities.getVscodeCliPath(), + SystemUtilities.findSshPath(), + ensureSsmCli(), + ]) + + const missing: MissingTool[] = [] + pushIf(missing, vsc === undefined, { name: 'code' }) + pushIf(missing, ssh === undefined, { name: 'ssh' }) + + if (ssm.isErr()) { + missing.push({ name: 'ssm', reason: ssm.err() }) + } + + if (vsc === undefined || ssh === undefined || ssm.isErr()) { + return Result.err(missing) + } + + return Result.ok({ vsc, ssh, ssm: ssm.ok() }) +} + +export async function handleMissingTool(tools: Err) { + const missing = tools + .err() + .map(d => d.name) + .join(', ') + const msg = localize( + 'AWS.codecatalyst.missingRequiredTool', + 'Failed to connect to Dev Environment, missing required tools: {0}', + missing + ) + + tools.err().forEach(d => { + if (d.reason) { + getLogger().error(`codecatalyst: failed to get tool "${d.name}": ${d.reason}`) + } + }) + + return Result.err( + new ToolkitError(msg, { + code: 'MissingTools', + details: { missing }, + }) + ) +} From 3caba57399148df5fdd0f38629b9c738b3d89fa0 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 10 Jul 2023 11:15:43 -0700 Subject: [PATCH 007/172] abstract general error msg to its own function --- src/ec2/model.ts | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 8063186c945..791e8646850 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -102,6 +102,13 @@ export class Ec2ConnectionManager { } } + public throwGeneralConnectionError(selection: Ec2Selection, error: Error) { + this.throwConnectionError('Unable to connect to target instance. ', selection, { + code: 'EC2SSMConnect', + cause: error, + }) + } + private async openSessionInTerminal(session: Session, selection: Ec2Selection) { const ssmPlugin = await getOrInstallCli('session-manager-plugin', !isCloud9) const shellArgs = [JSON.stringify(session), selection.region, 'StartSession'] @@ -122,32 +129,28 @@ export class Ec2ConnectionManager { const response = await this.ssmClient.startSession(selection.instanceId) await this.openSessionInTerminal(response, selection) } catch (err: unknown) { - // Default error if pre-check fails. - this.throwConnectionError('Unable to connect to target instance. ', selection, { - code: 'EC2SSMConnect', - cause: err as Error, - }) + this.throwGeneralConnectionError(selection, err as Error) } } public async attemptToOpenRemoteConnection(selection: Ec2Selection): Promise { await this.checkForStartSessionError(selection) + const logPrefix = `ec2 (${selection.instanceId})` + const logger = (data: string) => getLogger().verbose(`${logPrefix}: ${data}`) + const { ssm, vsc } = (await ensureDependencies()).unwrap() + const vars = getEc2SsmEnv(selection.region, ssm) + const envProvider = async () => { + return { [sshAgentSocketVariable]: await startSshAgent(), ...vars } + } + const SessionProcess = createBoundProcess(envProvider).extend({ + onStdout: logger, + onStderr: logger, + rejectOnErrorCode: true, + }) try { - const logPrefix = `ec2 (${selection.instanceId})` - const logger = (data: string) => getLogger().verbose(`${logPrefix}: ${data}`) - const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() - const vars = getEc2SsmEnv(selection.region, ssm) - const envProvider = async () => { - return { [sshAgentSocketVariable]: await startSshAgent(), ...vars } - } - const SessionProcess = createBoundProcess(envProvider).extend({ - onStdout: logger, - onStderr: logger, - rejectOnErrorCode: true, - }) await startVscodeRemote(SessionProcess, selection.instanceId, '/', vsc) - } catch (e) { - console.log(e as Error) + } catch (err) { + this.throwGeneralConnectionError(selection, err as Error) } } } From f2894bac5485d8a824fb5d86da7c431e0a2bfc39 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 10 Jul 2023 11:48:37 -0700 Subject: [PATCH 008/172] refactor more code catalyst code --- src/codecatalyst/model.ts | 11 ++++++++++- src/codecatalyst/tools.ts | 37 +------------------------------------ src/shared/remoteSession.ts | 6 +++++- 3 files changed, 16 insertions(+), 38 deletions(-) diff --git a/src/codecatalyst/model.ts b/src/codecatalyst/model.ts index afd91756355..107898996c3 100644 --- a/src/codecatalyst/model.ts +++ b/src/codecatalyst/model.ts @@ -21,7 +21,7 @@ import { getCodeCatalystSpaceName, getCodeCatalystProjectName, getCodeCatalystDe import { writeFile } from 'fs-extra' import { sshAgentSocketVariable, startSshAgent, startVscodeRemote } from '../shared/extensions/ssh' import { ChildProcess } from '../shared/utilities/childProcess' -import { ensureDependencies, hostNamePrefix } from './tools' +import { ensureCodeCatalystSshConfig, hostNamePrefix } from './tools' import { isDevenvVscode } from './utils' import { Timeout } from '../shared/utilities/timeoutUtils' import { Commands } from '../shared/vscode/commands2' @@ -30,6 +30,7 @@ import { fileExists } from '../shared/filesystemUtilities' import { CodeCatalystAuthenticationProvider } from './auth' import { ToolkitError } from '../shared/errors' import { Result } from '../shared/utilities/result' +import { ensureDependencies } from '../shared/remoteSession' export type DevEnvironmentId = Pick @@ -215,6 +216,14 @@ export async function prepareDevEnvConnection( { topic, timeout }: { topic?: string; timeout?: Timeout } = {} ): Promise { const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() + const config = await ensureCodeCatalystSshConfig(ssh) + if (config.isErr()) { + const err = config.err() + getLogger().error(`codecatalyst: failed to add ssh config section: ${err.message}`) + + throw err + } + const runningDevEnv = await client.startDevEnvironmentWithProgress({ id, spaceName: org.name, diff --git a/src/codecatalyst/tools.ts b/src/codecatalyst/tools.ts index 72f2b972c13..8045066754a 100644 --- a/src/codecatalyst/tools.ts +++ b/src/codecatalyst/tools.ts @@ -20,44 +20,9 @@ import { getLogger } from '../shared/logger' import { getIdeProperties } from '../shared/extensionUtilities' import { showConfirmationMessage } from '../shared/utilities/messages' import { getSshConfigPath } from '../shared/extensions/ssh' -import { ensureRemoteSshInstalled, ensureTools, handleMissingTool } from '../shared/remoteSession' - -interface DependencyPaths { - readonly vsc: string - readonly ssm: string - readonly ssh: string -} - -export interface MissingTool { - readonly name: 'code' | 'ssm' | 'ssh' - readonly reason?: string -} export const hostNamePrefix = 'aws-devenv-' -export async function ensureDependencies(): Promise> { - try { - await ensureRemoteSshInstalled() - } catch (e) { - return Result.err(e as Error) - } - - const tools = await ensureTools() - if (tools.isErr()) { - return await handleMissingTool(tools) - } - - const config = await ensureCodeCatalystSshConfig(tools.ok().ssh) - if (config.isErr()) { - const err = config.err() - getLogger().error(`codecatalyst: failed to add ssh config section: ${err.message}`) - - return Result.err(err) - } - - return tools -} - export async function ensureConnectScript(context = globals.context): Promise> { const scriptName = `codecatalyst_connect${process.platform === 'win32' ? '.ps1' : ''}` @@ -91,7 +56,7 @@ export async function ensureConnectScript(context = globals.context): Promise void) { const timeout = new Timeout(60000) From f0f508601ed98a419fba9df7c805e2e54adee7ec Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 10 Jul 2023 12:07:20 -0700 Subject: [PATCH 009/172] refactor code to better mirror the code catalyst implementaton --- src/codecatalyst/model.ts | 9 ++------ src/ec2/model.ts | 43 +++++++++++++++++++++++++++---------- src/shared/remoteSession.ts | 11 ++++++++++ 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/codecatalyst/model.ts b/src/codecatalyst/model.ts index 107898996c3..ba6feb52ab0 100644 --- a/src/codecatalyst/model.ts +++ b/src/codecatalyst/model.ts @@ -30,7 +30,7 @@ import { fileExists } from '../shared/filesystemUtilities' import { CodeCatalystAuthenticationProvider } from './auth' import { ToolkitError } from '../shared/errors' import { Result } from '../shared/utilities/result' -import { ensureDependencies } from '../shared/remoteSession' +import { VscodeRemoteConnection, ensureDependencies } from '../shared/remoteSession' export type DevEnvironmentId = Pick @@ -201,12 +201,7 @@ export async function getThisDevEnv(authProvider: CodeCatalystAuthenticationProv /** * Everything needed to connect to a dev environment via VS Code or `ssh` */ -interface DevEnvConnection { - readonly sshPath: string - readonly vscPath: string - readonly hostname: string - readonly envProvider: EnvProvider - readonly SessionProcess: typeof ChildProcess +interface DevEnvConnection extends VscodeRemoteConnection { readonly devenv: DevEnvironment } diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 791e8646850..6dd4b621dfe 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -11,16 +11,19 @@ import { isCloud9 } from '../shared/extensionUtilities' import { ToolkitError } from '../shared/errors' import { SsmClient } from '../shared/clients/ssmClient' import { Ec2Client } from '../shared/clients/ec2Client' - -export type Ec2ConnectErrorCode = 'EC2SSMStatus' | 'EC2SSMPermission' | 'EC2SSMConnect' | 'EC2SSMAgentStatus' - -import { ensureDependencies, openRemoteTerminal } from '../shared/remoteSession' +import { VscodeRemoteConnection, ensureDependencies, openRemoteTerminal } from '../shared/remoteSession' import { DefaultIamClient } from '../shared/clients/iamClient' import { ErrorInformation } from '../shared/errors' import { sshAgentSocketVariable, startSshAgent, startVscodeRemote } from '../shared/extensions/ssh' import { createBoundProcess } from '../codecatalyst/model' import { getLogger } from '../shared/logger/logger' +export type Ec2ConnectErrorCode = 'EC2SSMStatus' | 'EC2SSMPermission' | 'EC2SSMConnect' | 'EC2SSMAgentStatus' + +interface Ec2RemoteConnection extends VscodeRemoteConnection { + selection: Ec2Selection +} + export class Ec2ConnectionManager { private ssmClient: SsmClient private ec2Client: Ec2Client @@ -135,9 +138,17 @@ export class Ec2ConnectionManager { public async attemptToOpenRemoteConnection(selection: Ec2Selection): Promise { await this.checkForStartSessionError(selection) - const logPrefix = `ec2 (${selection.instanceId})` - const logger = (data: string) => getLogger().verbose(`${logPrefix}: ${data}`) - const { ssm, vsc } = (await ensureDependencies()).unwrap() + const remoteEnv = await this.prepareRemoteConnection(selection) + try { + await startVscodeRemote(remoteEnv.SessionProcess, selection.instanceId, '/', remoteEnv.vscPath) + } catch (err) { + this.throwGeneralConnectionError(selection, err as Error) + } + } + + public async prepareRemoteConnection(selection: Ec2Selection): Promise { + const logger = this.configureRemoteConnectionLogger(selection.instanceId) + const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() const vars = getEc2SsmEnv(selection.region, ssm) const envProvider = async () => { return { [sshAgentSocketVariable]: await startSshAgent(), ...vars } @@ -147,12 +158,22 @@ export class Ec2ConnectionManager { onStderr: logger, rejectOnErrorCode: true, }) - try { - await startVscodeRemote(SessionProcess, selection.instanceId, '/', vsc) - } catch (err) { - this.throwGeneralConnectionError(selection, err as Error) + + return { + hostname: selection.instanceId, + envProvider, + sshPath: ssh, + vscPath: vsc, + SessionProcess, + selection, } } + + private configureRemoteConnectionLogger(instanceId: string) { + const logPrefix = `ec2 (${instanceId})` + const logger = (data: string) => getLogger().verbose(`${logPrefix}: ${data}`) + return logger + } } function getEc2SsmEnv(region: string, ssmPath: string): NodeJS.ProcessEnv { diff --git a/src/shared/remoteSession.ts b/src/shared/remoteSession.ts index ad43d4a43a6..3dc121025cd 100644 --- a/src/shared/remoteSession.ts +++ b/src/shared/remoteSession.ts @@ -18,6 +18,7 @@ import { getLogger } from './logger/logger' import { SystemUtilities } from './systemUtilities' import { getOrInstallCli } from './utilities/cliUtils' import { pushIf } from './utilities/collectionUtils' +import { ChildProcess } from './utilities/childProcess' export interface MissingTool { readonly name: 'code' | 'ssm' | 'ssh' @@ -59,6 +60,16 @@ interface DependencyPaths { readonly ssh: string } +type EnvProvider = () => Promise + +export interface VscodeRemoteConnection { + readonly sshPath: string + readonly vscPath: string + readonly hostname: string + readonly envProvider: EnvProvider + readonly SessionProcess: typeof ChildProcess +} + export async function ensureDependencies(): Promise> { try { await ensureRemoteSshInstalled() From 5be1f8df08608355077397a16a9f940ce1433699 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 10 Jul 2023 13:16:48 -0700 Subject: [PATCH 010/172] add a cancellable loading bar on open --- src/ec2/model.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 6dd4b621dfe..e2977710241 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -17,6 +17,8 @@ import { ErrorInformation } from '../shared/errors' import { sshAgentSocketVariable, startSshAgent, startVscodeRemote } from '../shared/extensions/ssh' import { createBoundProcess } from '../codecatalyst/model' import { getLogger } from '../shared/logger/logger' +import { Timeout } from '../shared/utilities/timeoutUtils' +import { showMessageWithCancel } from '../shared/utilities/messages' export type Ec2ConnectErrorCode = 'EC2SSMStatus' | 'EC2SSMPermission' | 'EC2SSMConnect' | 'EC2SSMAgentStatus' @@ -138,11 +140,15 @@ export class Ec2ConnectionManager { public async attemptToOpenRemoteConnection(selection: Ec2Selection): Promise { await this.checkForStartSessionError(selection) + const timeout = new Timeout(60000) + await showMessageWithCancel('AWS: Opening remote connection...', timeout) const remoteEnv = await this.prepareRemoteConnection(selection) try { await startVscodeRemote(remoteEnv.SessionProcess, selection.instanceId, '/', remoteEnv.vscPath) } catch (err) { this.throwGeneralConnectionError(selection, err as Error) + } finally { + timeout.cancel() } } From 3c096cd2b40efcfea209207a654f17a2fa7fb986 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 10 Jul 2023 13:26:34 -0700 Subject: [PATCH 011/172] rename variable to be more explicit --- src/ec2/model.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index e2977710241..4ba06e6de9a 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -22,7 +22,7 @@ import { showMessageWithCancel } from '../shared/utilities/messages' export type Ec2ConnectErrorCode = 'EC2SSMStatus' | 'EC2SSMPermission' | 'EC2SSMConnect' | 'EC2SSMAgentStatus' -interface Ec2RemoteConnection extends VscodeRemoteConnection { +interface Ec2RemoteEnv extends VscodeRemoteConnection { selection: Ec2Selection } @@ -142,7 +142,7 @@ export class Ec2ConnectionManager { await this.checkForStartSessionError(selection) const timeout = new Timeout(60000) await showMessageWithCancel('AWS: Opening remote connection...', timeout) - const remoteEnv = await this.prepareRemoteConnection(selection) + const remoteEnv = await this.prepareEc2RemoteEnv(selection) try { await startVscodeRemote(remoteEnv.SessionProcess, selection.instanceId, '/', remoteEnv.vscPath) } catch (err) { @@ -152,7 +152,7 @@ export class Ec2ConnectionManager { } } - public async prepareRemoteConnection(selection: Ec2Selection): Promise { + public async prepareEc2RemoteEnv(selection: Ec2Selection): Promise { const logger = this.configureRemoteConnectionLogger(selection.instanceId) const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() const vars = getEc2SsmEnv(selection.region, ssm) From 13b2bc0c160b1df47d58119e1ce346b27123e514 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 10 Jul 2023 15:01:09 -0700 Subject: [PATCH 012/172] refactor to have a with-progress method layer --- src/ec2/model.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 4ba06e6de9a..7d3ec873f1f 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -140,17 +140,19 @@ export class Ec2ConnectionManager { public async attemptToOpenRemoteConnection(selection: Ec2Selection): Promise { await this.checkForStartSessionError(selection) - const timeout = new Timeout(60000) - await showMessageWithCancel('AWS: Opening remote connection...', timeout) - const remoteEnv = await this.prepareEc2RemoteEnv(selection) + const remoteEnv = await this.prepareEc2RemoteEnvWithProgress(selection) try { await startVscodeRemote(remoteEnv.SessionProcess, selection.instanceId, '/', remoteEnv.vscPath) } catch (err) { this.throwGeneralConnectionError(selection, err as Error) - } finally { - timeout.cancel() } } + public async prepareEc2RemoteEnvWithProgress(selection: Ec2Selection): Promise { + const timeout = new Timeout(60000) + await showMessageWithCancel('AWS: Opening remote connection...', timeout) + const remoteEnv = await this.prepareEc2RemoteEnv(selection).finally(() => timeout.cancel()) + return remoteEnv + } public async prepareEc2RemoteEnv(selection: Ec2Selection): Promise { const logger = this.configureRemoteConnectionLogger(selection.instanceId) From 8fbbc8d3969d4236234cd5b9b88cb52334135296 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 10 Jul 2023 16:30:35 -0700 Subject: [PATCH 013/172] refactor ssh config into the ssh file --- src/codecatalyst/model.ts | 6 +- src/codecatalyst/tools.ts | 168 +++++++---------------------------- src/ec2/model.ts | 31 +++++++ src/shared/extensions/ssh.ts | 105 ++++++++++++++++++++++ 4 files changed, 173 insertions(+), 137 deletions(-) diff --git a/src/codecatalyst/model.ts b/src/codecatalyst/model.ts index ba6feb52ab0..48d4cf8a1c0 100644 --- a/src/codecatalyst/model.ts +++ b/src/codecatalyst/model.ts @@ -21,7 +21,7 @@ import { getCodeCatalystSpaceName, getCodeCatalystProjectName, getCodeCatalystDe import { writeFile } from 'fs-extra' import { sshAgentSocketVariable, startSshAgent, startVscodeRemote } from '../shared/extensions/ssh' import { ChildProcess } from '../shared/utilities/childProcess' -import { ensureCodeCatalystSshConfig, hostNamePrefix } from './tools' +import { CodeCatalystSshConfig, hostNamePrefix } from './tools' import { isDevenvVscode } from './utils' import { Timeout } from '../shared/utilities/timeoutUtils' import { Commands } from '../shared/vscode/commands2' @@ -211,7 +211,9 @@ export async function prepareDevEnvConnection( { topic, timeout }: { topic?: string; timeout?: Timeout } = {} ): Promise { const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() - const config = await ensureCodeCatalystSshConfig(ssh) + const sshConfig = new CodeCatalystSshConfig(ssh, hostNamePrefix) + const config = await sshConfig.ensureValid() + if (config.isErr()) { const err = config.err() getLogger().error(`codecatalyst: failed to add ssh config section: ${err.message}`) diff --git a/src/codecatalyst/tools.ts b/src/codecatalyst/tools.ts index 8045066754a..8a42f88a8af 100644 --- a/src/codecatalyst/tools.ts +++ b/src/codecatalyst/tools.ts @@ -10,16 +10,11 @@ const localize = nls.loadMessageBundle() import * as vscode from 'vscode' import * as fs from 'fs-extra' -import * as path from 'path' import { Result } from '../shared/utilities/result' -import { ChildProcess } from '../shared/utilities/childProcess' -import { CancellationError } from '../shared/utilities/timeoutUtils' import { fileExists, readFileAsString } from '../shared/filesystemUtilities' import { ToolkitError } from '../shared/errors' import { getLogger } from '../shared/logger' -import { getIdeProperties } from '../shared/extensionUtilities' -import { showConfirmationMessage } from '../shared/utilities/messages' -import { getSshConfigPath } from '../shared/extensions/ssh' +import { VscodeRemoteSshConfig } from '../shared/extensions/ssh' export const hostNamePrefix = 'aws-devenv-' @@ -51,142 +46,45 @@ export async function ensureConnectScript(context = globals.context): Promise { - if (resp === openConfig) { - vscode.window.showTextDocument(vscode.Uri.file(getSshConfigPath())) - } - }) - - return Result.err(new ToolkitError(oldConfig, { code: 'OldConfig' })) +export class CodeCatalystSshConfig extends VscodeRemoteSshConfig { + /** + * Checks if the "aws-devenv-*" SSH config hostname pattern is working, else prompts user to add it. + * + * @returns Result object indicating whether the SSH config is working, or failure reason. + */ + public async ensureValid() { + const scriptResult = await ensureConnectScript() + if (scriptResult.isErr()) { + return scriptResult } - const confirmTitle = localize( - 'AWS.codecatalyst.confirm.installSshConfig.title', - '{0} Toolkit will add host {1} to ~/.ssh/config to use SSH with your Dev Environments', - getIdeProperties().company, - configHostName - ) - const confirmText = localize('AWS.codecatalyst.confirm.installSshConfig.button', 'Update SSH config') - const response = await showConfirmationMessage({ prompt: confirmTitle, confirm: confirmText }) - if (!response) { - return Result.err(new CancellationError('user')) + const connectScript = scriptResult.ok() + const proxyCommand = await this.getProxyCommand(connectScript.fsPath) + if (proxyCommand.isErr()) { + return proxyCommand } - const sshConfigPath = getSshConfigPath() - try { - await fs.ensureDir(path.dirname(path.dirname(sshConfigPath)), { mode: 0o755 }) - await fs.ensureDir(path.dirname(sshConfigPath), 0o700) - await fs.appendFile(sshConfigPath, section, { mode: 0o600 }) - } catch (e) { - const message = localize( - 'AWS.codecatalyst.error.writeFail', - 'Failed to write SSH config: {0} (permission issue?)', - sshConfigPath - ) - - return Result.err(ToolkitError.chain(e, message, { code: 'ConfigWriteFailed' })) - } - } + const section = this.createSSHConfigSection(proxyCommand.unwrap()) - return Result.ok() -} + const verifyHost = await this.verifySSHHost({ proxyCommand: proxyCommand.unwrap(), section }) + if (verifyHost.isErr()) { + return verifyHost + } -async function matchSshSection(sshPath: string, sshName: string) { - const proc = new ChildProcess(sshPath, ['-G', sshName]) - const r = await proc.run() - if (r.exitCode !== 0) { - return Result.err(r.error ?? new Error(`ssh check against host failed: ${r.exitCode}`)) + return Result.ok() } - const matches = r.stdout.match(/proxycommand.{0,1024}codecatalyst_connect(.ps1)?.{0,99}/i) - return Result.ok(matches?.[0]) -} - -async function getProxyCommand(iswin: boolean, script: string): Promise> { - if (iswin) { - // Some older versions of OpenSSH (7.8 and below) have a bug where attempting to use powershell.exe directly will fail without an absolute path - const proc = new ChildProcess('powershell.exe', ['-Command', '(get-command powershell.exe).Path']) - const r = await proc.run() - if (r.exitCode !== 0) { - return Result.err(new ToolkitError('Failed to get absolute path for powershell', { cause: r.error })) - } - return Result.ok(`"${r.stdout}" -ExecutionPolicy RemoteSigned -File "${script}" %h`) - } else { - return Result.ok(`'${script}' '%h'`) + public createSSHConfigSection(proxyCommand: string): string { + // "AddKeysToAgent" will automatically add keys used on the server to the local agent. If not set, then `ssh-add` + // must be done locally. It's mostly a convenience thing; private keys are _not_ shared with the server. + + return ` + # Created by AWS Toolkit for VSCode. https://github.com/aws/aws-toolkit-vscode + Host ${this.configHostName} + ForwardAgent yes + AddKeysToAgent yes + StrictHostKeyChecking accept-new + ProxyCommand ${proxyCommand} + ` } } diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 7d3ec873f1f..02be338528b 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -193,3 +193,34 @@ function getEc2SsmEnv(region: string, ssmPath: string): NodeJS.ProcessEnv { process.env ) } + +// function ensureEc2SshConfig(sshPath: string) { +// const iswin = process.platform === 'win32' +// const proxyCommand = "sh -c 'aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p''" +// const section = createSSHConfigSection(proxyCommand) + +// } + +// function createSSHConfigSection(proxyCommand: string): string { +// return ` +// # Created by AWS Toolkit for VSCode. https://github.com/aws/aws-toolkit-vscode +// Host i-* mi-* +// User ${hostname} +// ProxyCommand ${proxyCommand} +// ` +// } + +// async function verifySSHHost({sshPath, section, proxyCommand}: { +// sshPath: string +// section: string +// proxyCommand: string +// }) { +// const proxyCommandRegEx = new RegExp(`/proxycommand.{0,1024}${proxyCommand}.{0,99}/i`) +// const matchResult = await matchSshSection(sshPath, `i-123456`, proxyCommandRegEx) +// if (matchResult.isErr()) { +// return matchResult +// } +// const configSection = matchResult.ok() +// const hasProxyCommand = configSection?.includes(proxyCommand) + +// } diff --git a/src/shared/extensions/ssh.ts b/src/shared/extensions/ssh.ts index 9e8584dab07..9c8c6e4ee55 100644 --- a/src/shared/extensions/ssh.ts +++ b/src/shared/extensions/ssh.ts @@ -6,12 +6,18 @@ import * as vscode from 'vscode' import * as path from 'path' import * as nls from 'vscode-nls' +import * as fs from 'fs-extra' import { getLogger } from '../logger' import { ChildProcess } from '../utilities/childProcess' import { SystemUtilities } from '../systemUtilities' import { ArrayConstructor, NonNullObject } from '../utilities/typeConstructors' import { Settings } from '../settings' import { VSCODE_EXTENSION_ID } from '../extensions' +import { Result } from '../utilities/result' +import { ToolkitError } from '../errors' +import { getIdeProperties } from '../extensionUtilities' +import { showConfirmationMessage } from '../utilities/messages' +import { CancellationError } from '../utilities/timeoutUtils' const localize = nls.loadMessageBundle() @@ -143,3 +149,102 @@ export async function startVscodeRemote( await new ProcessClass(vscPath, ['--folder-uri', workspaceUri]).run() } + +export abstract class VscodeRemoteSshConfig { + private readonly iswin: boolean = process.platform === 'win32' + protected readonly configHostName: string + private readonly proxyCommandRegExp: RegExp = /proxycommand.{0,1024}codecatalyst_connect(.ps1)?.{0,99}/i + + public constructor(protected readonly sshPath: string, protected readonly hostNamePrefix: string) { + this.configHostName = `${hostNamePrefix}*` + } + + protected async getProxyCommand(script: string): Promise> { + if (this.iswin) { + // Some older versions of OpenSSH (7.8 and below) have a bug where attempting to use powershell.exe directly will fail without an absolute path + const proc = new ChildProcess('powershell.exe', ['-Command', '(get-command powershell.exe).Path']) + const r = await proc.run() + if (r.exitCode !== 0) { + return Result.err(new ToolkitError('Failed to get absolute path for powershell', { cause: r.error })) + } + return Result.ok(`"${r.stdout}" -ExecutionPolicy RemoteSigned -File "${script}" %h`) + } else { + return Result.ok(`'${script}' '%h'`) + } + } + + protected abstract createSSHConfigSection(proxyCommand: string): string + + protected async matchSshSection(proxyCommandRegExp: RegExp) { + const proc = new ChildProcess(this.sshPath, ['-G', `${this.hostNamePrefix}test`]) + const r = await proc.run() + if (r.exitCode !== 0) { + return Result.err(r.error ?? new Error(`ssh check against host failed: ${r.exitCode}`)) + } + const matches = r.stdout.match(proxyCommandRegExp) + return Result.ok(matches?.[0]) + } + + // Check if the hostname pattern is working. + protected async verifySSHHost({ section, proxyCommand }: { section: string; proxyCommand: string }) { + const matchResult = await this.matchSshSection(this.proxyCommandRegExp) + if (matchResult.isErr()) { + return matchResult + } + + const configSection = matchResult.ok() + const hasProxyCommand = configSection?.includes(proxyCommand) + + if (!hasProxyCommand) { + if (configSection !== undefined) { + getLogger().warn( + `codecatalyst: SSH config: found old/outdated "${this.configHostName}" section:\n%O`, + configSection + ) + const oldConfig = localize( + 'AWS.codecatalyst.error.oldConfig', + 'Your ~/.ssh/config has a {0} section that might be out of date. Delete it, then try again.', + this.configHostName + ) + + const openConfig = localize('AWS.ssh.openConfig', 'Open config...') + vscode.window.showWarningMessage(oldConfig, openConfig).then(resp => { + if (resp === openConfig) { + vscode.window.showTextDocument(vscode.Uri.file(getSshConfigPath())) + } + }) + + return Result.err(new ToolkitError(oldConfig, { code: 'OldConfig' })) + } + + const confirmTitle = localize( + 'AWS.codecatalyst.confirm.installSshConfig.title', + '{0} Toolkit will add host {1} to ~/.ssh/config to use SSH with your Dev Environments', + getIdeProperties().company, + this.configHostName + ) + const confirmText = localize('AWS.codecatalyst.confirm.installSshConfig.button', 'Update SSH config') + const response = await showConfirmationMessage({ prompt: confirmTitle, confirm: confirmText }) + if (!response) { + return Result.err(new CancellationError('user')) + } + + const sshConfigPath = getSshConfigPath() + try { + await fs.ensureDir(path.dirname(path.dirname(sshConfigPath)), { mode: 0o755 }) + await fs.ensureDir(path.dirname(sshConfigPath), 0o700) + await fs.appendFile(sshConfigPath, section, { mode: 0o600 }) + } catch (e) { + const message = localize( + 'AWS.codecatalyst.error.writeFail', + 'Failed to write SSH config: {0} (permission issue?)', + sshConfigPath + ) + + return Result.err(ToolkitError.chain(e, message, { code: 'ConfigWriteFailed' })) + } + } + + return Result.ok() + } +} From 57dbde6a13af53b6b7b74fa66065f094fec389ce Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 10 Jul 2023 16:42:09 -0700 Subject: [PATCH 014/172] convert regExp property to abstract --- src/codecatalyst/tools.ts | 3 ++- src/shared/extensions/ssh.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/codecatalyst/tools.ts b/src/codecatalyst/tools.ts index 8a42f88a8af..b2a956d7775 100644 --- a/src/codecatalyst/tools.ts +++ b/src/codecatalyst/tools.ts @@ -47,12 +47,13 @@ export async function ensureConnectScript(context = globals.context): Promise | Err | Ok> protected abstract createSSHConfigSection(proxyCommand: string): string protected async matchSshSection(proxyCommandRegExp: RegExp) { From fb9f1e87b32734f59237829f0db083dd38c275de Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 10 Jul 2023 17:12:19 -0700 Subject: [PATCH 015/172] reformat string in proxycommand --- src/codecatalyst/tools.ts | 12 ++++++------ src/ec2/model.ts | 10 ++++++++++ src/ec2/tools.ts | 38 ++++++++++++++++++++++++++++++++++++ src/shared/extensions/ssh.ts | 7 ++++--- 4 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 src/ec2/tools.ts diff --git a/src/codecatalyst/tools.ts b/src/codecatalyst/tools.ts index b2a956d7775..fb5d6df6b08 100644 --- a/src/codecatalyst/tools.ts +++ b/src/codecatalyst/tools.ts @@ -80,12 +80,12 @@ export class CodeCatalystSshConfig extends VscodeRemoteSshConfig { // must be done locally. It's mostly a convenience thing; private keys are _not_ shared with the server. return ` - # Created by AWS Toolkit for VSCode. https://github.com/aws/aws-toolkit-vscode - Host ${this.configHostName} - ForwardAgent yes - AddKeysToAgent yes - StrictHostKeyChecking accept-new - ProxyCommand ${proxyCommand} +# Created by AWS Toolkit for VSCode. https://github.com/aws/aws-toolkit-vscode +Host ${this.configHostName} + ForwardAgent yes + AddKeysToAgent yes + StrictHostKeyChecking accept-new + ProxyCommand ${proxyCommand} ` } } diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 02be338528b..9fe2345d605 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -19,6 +19,7 @@ import { createBoundProcess } from '../codecatalyst/model' import { getLogger } from '../shared/logger/logger' import { Timeout } from '../shared/utilities/timeoutUtils' import { showMessageWithCancel } from '../shared/utilities/messages' +import { Ec2RemoteSshConfig } from './tools' export type Ec2ConnectErrorCode = 'EC2SSMStatus' | 'EC2SSMPermission' | 'EC2SSMConnect' | 'EC2SSMAgentStatus' @@ -157,6 +158,15 @@ export class Ec2ConnectionManager { public async prepareEc2RemoteEnv(selection: Ec2Selection): Promise { const logger = this.configureRemoteConnectionLogger(selection.instanceId) const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() + const sshConfig = new Ec2RemoteSshConfig(ssh, 'ec2-user') + const config = await sshConfig.ensureValid() + if (config.isErr()) { + const err = config.err() + getLogger().error(`ec2: failed to add ssh config section: ${err.message}`) + + throw err + } + const vars = getEc2SsmEnv(selection.region, ssm) const envProvider = async () => { return { [sshAgentSocketVariable]: await startSshAgent(), ...vars } diff --git a/src/ec2/tools.ts b/src/ec2/tools.ts new file mode 100644 index 00000000000..2ee320e70b4 --- /dev/null +++ b/src/ec2/tools.ts @@ -0,0 +1,38 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VscodeRemoteSshConfig } from '../shared/extensions/ssh' +import { Result } from '../shared/utilities/result' + +export class Ec2RemoteSshConfig extends VscodeRemoteSshConfig { + private readonly command: string = + "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'" + protected override proxyCommandRegExp = new RegExp(`/proxycommand.{0,1024}${this.command}.{0,99}/i`) + + public override async ensureValid() { + const proxyCommand = await this.getProxyCommand(this.command) + if (proxyCommand.isErr()) { + return proxyCommand + } + + const section = this.createSSHConfigSection(proxyCommand.unwrap()) + + const verifyHost = await this.verifySSHHost({ proxyCommand: proxyCommand.unwrap(), section }) + if (verifyHost.isErr()) { + return verifyHost + } + + return Result.ok() + } + + protected override createSSHConfigSection(proxyCommand: string): string { + return ` +# Created by AWS Toolkit for VSCode. https://github.com/aws/aws-toolkit-vscode +Host i-* mi-* + User ${this.hostNamePrefix} + ProxyCommand sh -c \"${this.command}\" +` + } +} diff --git a/src/shared/extensions/ssh.ts b/src/shared/extensions/ssh.ts index 09b8d268cc0..1a16a712b1e 100644 --- a/src/shared/extensions/ssh.ts +++ b/src/shared/extensions/ssh.ts @@ -159,7 +159,7 @@ export abstract class VscodeRemoteSshConfig { this.configHostName = `${hostNamePrefix}*` } - protected async getProxyCommand(script: string): Promise> { + protected async getProxyCommand(command: string): Promise> { if (this.iswin) { // Some older versions of OpenSSH (7.8 and below) have a bug where attempting to use powershell.exe directly will fail without an absolute path const proc = new ChildProcess('powershell.exe', ['-Command', '(get-command powershell.exe).Path']) @@ -167,12 +167,13 @@ export abstract class VscodeRemoteSshConfig { if (r.exitCode !== 0) { return Result.err(new ToolkitError('Failed to get absolute path for powershell', { cause: r.error })) } - return Result.ok(`"${r.stdout}" -ExecutionPolicy RemoteSigned -File "${script}" %h`) + return Result.ok(`"${r.stdout}" -ExecutionPolicy RemoteSigned -File "${command}" %h`) } else { - return Result.ok(`'${script}' '%h'`) + return Result.ok(`'${command}' '%h'`) } } public abstract ensureValid(): Promise | Err | Ok> + protected abstract createSSHConfigSection(proxyCommand: string): string protected async matchSshSection(proxyCommandRegExp: RegExp) { From f94fe644d3223a2e2377452d3f8a95ac3701cedc Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 11 Jul 2023 11:28:32 -0700 Subject: [PATCH 016/172] implement basic tests on sshconfig mock object --- src/codecatalyst/model.ts | 3 +- src/codecatalyst/tools.ts | 2 - src/shared/extensions/ssh.ts | 27 +++++--- src/test/shared/extensions/ssh.test.ts | 96 ++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 src/test/shared/extensions/ssh.test.ts diff --git a/src/codecatalyst/model.ts b/src/codecatalyst/model.ts index 48d4cf8a1c0..8f81a4668f1 100644 --- a/src/codecatalyst/model.ts +++ b/src/codecatalyst/model.ts @@ -21,7 +21,7 @@ import { getCodeCatalystSpaceName, getCodeCatalystProjectName, getCodeCatalystDe import { writeFile } from 'fs-extra' import { sshAgentSocketVariable, startSshAgent, startVscodeRemote } from '../shared/extensions/ssh' import { ChildProcess } from '../shared/utilities/childProcess' -import { CodeCatalystSshConfig, hostNamePrefix } from './tools' +import { CodeCatalystSshConfig } from './tools' import { isDevenvVscode } from './utils' import { Timeout } from '../shared/utilities/timeoutUtils' import { Commands } from '../shared/vscode/commands2' @@ -33,6 +33,7 @@ import { Result } from '../shared/utilities/result' import { VscodeRemoteConnection, ensureDependencies } from '../shared/remoteSession' export type DevEnvironmentId = Pick +export const hostNamePrefix = 'aws-devenv-' export const docs = { vscode: { diff --git a/src/codecatalyst/tools.ts b/src/codecatalyst/tools.ts index fb5d6df6b08..67c6b506618 100644 --- a/src/codecatalyst/tools.ts +++ b/src/codecatalyst/tools.ts @@ -16,8 +16,6 @@ import { ToolkitError } from '../shared/errors' import { getLogger } from '../shared/logger' import { VscodeRemoteSshConfig } from '../shared/extensions/ssh' -export const hostNamePrefix = 'aws-devenv-' - export async function ensureConnectScript(context = globals.context): Promise> { const scriptName = `codecatalyst_connect${process.platform === 'win32' ? '.ps1' : ''}` diff --git a/src/shared/extensions/ssh.ts b/src/shared/extensions/ssh.ts index 1a16a712b1e..5828be10e99 100644 --- a/src/shared/extensions/ssh.ts +++ b/src/shared/extensions/ssh.ts @@ -8,7 +8,7 @@ import * as path from 'path' import * as nls from 'vscode-nls' import * as fs from 'fs-extra' import { getLogger } from '../logger' -import { ChildProcess } from '../utilities/childProcess' +import { ChildProcess, ChildProcessResult } from '../utilities/childProcess' import { SystemUtilities } from '../systemUtilities' import { ArrayConstructor, NonNullObject } from '../utilities/typeConstructors' import { Settings } from '../settings' @@ -151,7 +151,6 @@ export async function startVscodeRemote( } export abstract class VscodeRemoteSshConfig { - private readonly iswin: boolean = process.platform === 'win32' protected readonly configHostName: string protected abstract proxyCommandRegExp: RegExp @@ -159,8 +158,12 @@ export abstract class VscodeRemoteSshConfig { this.configHostName = `${hostNamePrefix}*` } + protected isWin(): boolean { + return process.platform === 'win32' + } + protected async getProxyCommand(command: string): Promise> { - if (this.iswin) { + if (this.isWin()) { // Some older versions of OpenSSH (7.8 and below) have a bug where attempting to use powershell.exe directly will fail without an absolute path const proc = new ChildProcess('powershell.exe', ['-Command', '(get-command powershell.exe).Path']) const r = await proc.run() @@ -172,23 +175,29 @@ export abstract class VscodeRemoteSshConfig { return Result.ok(`'${command}' '%h'`) } } + public abstract ensureValid(): Promise | Err | Ok> protected abstract createSSHConfigSection(proxyCommand: string): string - protected async matchSshSection(proxyCommandRegExp: RegExp) { + protected async checkSshOnHost(): Promise { const proc = new ChildProcess(this.sshPath, ['-G', `${this.hostNamePrefix}test`]) - const r = await proc.run() - if (r.exitCode !== 0) { - return Result.err(r.error ?? new Error(`ssh check against host failed: ${r.exitCode}`)) + const result = await proc.run() + return result + } + + protected async matchSshSection() { + const result = await this.checkSshOnHost() + if (result.exitCode !== 0) { + return Result.err(result.error ?? new Error(`ssh check against host failed: ${result.exitCode}`)) } - const matches = r.stdout.match(proxyCommandRegExp) + const matches = result.stdout.match(this.proxyCommandRegExp) return Result.ok(matches?.[0]) } // Check if the hostname pattern is working. protected async verifySSHHost({ section, proxyCommand }: { section: string; proxyCommand: string }) { - const matchResult = await this.matchSshSection(this.proxyCommandRegExp) + const matchResult = await this.matchSshSection() if (matchResult.isErr()) { return matchResult } diff --git a/src/test/shared/extensions/ssh.test.ts b/src/test/shared/extensions/ssh.test.ts new file mode 100644 index 00000000000..948ae627781 --- /dev/null +++ b/src/test/shared/extensions/ssh.test.ts @@ -0,0 +1,96 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as assert from 'assert' +import { VscodeRemoteSshConfig } from '../../../shared/extensions/ssh' +import { ToolkitError } from '../../../shared/errors' +import { Err, Ok, Result } from '../../../shared/utilities/result' +import { ChildProcessResult } from '../../../shared/utilities/childProcess' + +const testCommand = 'run-thing' + +class MockSshConfig extends VscodeRemoteSshConfig { + private readonly testCommand: string = testCommand + protected readonly proxyCommandRegExp: RegExp = /run-thing/ + + public testIsWin: boolean = false + public configSection: string = '' + + protected override createSSHConfigSection(proxyCommand: string): string { + return 'test-config-section' + } + + public override async ensureValid(): Promise | Err | Ok> { + const proxyCommand = await this.getProxyCommand(this.testCommand) + if (proxyCommand.isErr()) { + return proxyCommand + } + + const section = this.createSSHConfigSection(proxyCommand.unwrap()) + + const verifyHost = await this.verifySSHHost({ proxyCommand: proxyCommand.unwrap(), section }) + if (verifyHost.isErr()) { + return verifyHost + } + + return Result.ok() + } + + public async getProxyCommandWrapper(command: string): Promise> { + return await this.getProxyCommand(command) + } + + public async matchSshSectionWrapper() { + return await this.matchSshSection() + } + + protected override isWin() { + return this.testIsWin + } + + protected override async checkSshOnHost(): Promise { + return { + exitCode: 0, + error: undefined, + stdout: this.configSection, + stderr: '', + } + } +} + +describe('VscodeRemoteSshConfig', async function () { + let config: MockSshConfig + before(function () { + config = new MockSshConfig('sshPath', 'testHostNamePrefix') + config.testIsWin = false + }) + + describe('getProxyCommand', async function () { + it('returns correct proxyCommand on non-windows', async function () { + config.testIsWin = false + const result = await config.getProxyCommandWrapper(testCommand) + assert.ok(result.isOk()) + const command = result.unwrap() + assert.strictEqual(command, `'${testCommand}' '%h'`) + }) + }) + + describe('matchSshSection', async function () { + it('returns ok with match when proxycommand is present', async function () { + config.configSection = 'fdsafdsafdsarun-thing342432' + const result = await config.matchSshSectionWrapper() + assert.ok(result.isOk()) + const match = result.unwrap() + assert.ok(match) + }) + + it('returns ok with undefined when proxycommand is not present', async function () { + config.configSection = 'fdsafdsafdsa342432' + const result = await config.matchSshSectionWrapper() + assert.ok(result.isOk()) + const match = result.unwrap() + assert.strictEqual(match, undefined) + }) + }) +}) From fdec7981871b8c856d8e60e886279e8d9d9e1768 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 11 Jul 2023 11:47:00 -0700 Subject: [PATCH 017/172] split up verify ssh host into pieces --- src/shared/extensions/ssh.ts | 104 ++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 44 deletions(-) diff --git a/src/shared/extensions/ssh.ts b/src/shared/extensions/ssh.ts index 5828be10e99..2ebee12db0d 100644 --- a/src/shared/extensions/ssh.ts +++ b/src/shared/extensions/ssh.ts @@ -195,6 +195,64 @@ export abstract class VscodeRemoteSshConfig { return Result.ok(matches?.[0]) } + private async promptUserForOutdatedSection(configSection: string): Promise { + getLogger().warn( + `codecatalyst: SSH config: found old/outdated "${this.configHostName}" section:\n%O`, + configSection + ) + const oldConfig = localize( + 'AWS.codecatalyst.error.oldConfig', + 'Your ~/.ssh/config has a {0} section that might be out of date. Delete it, then try again.', + this.configHostName + ) + + const openConfig = localize('AWS.ssh.openConfig', 'Open config...') + vscode.window.showWarningMessage(oldConfig, openConfig).then(resp => { + if (resp === openConfig) { + vscode.window.showTextDocument(vscode.Uri.file(getSshConfigPath())) + } + }) + + throw new ToolkitError(oldConfig, { code: 'OldConfig' }) + } + + private async writeSectionToConfig(section: string) { + const sshConfigPath = getSshConfigPath() + try { + await fs.ensureDir(path.dirname(path.dirname(sshConfigPath)), { mode: 0o755 }) + await fs.ensureDir(path.dirname(sshConfigPath), 0o700) + await fs.appendFile(sshConfigPath, section, { mode: 0o600 }) + } catch (e) { + const message = localize( + 'AWS.codecatalyst.error.writeFail', + 'Failed to write SSH config: {0} (permission issue?)', + sshConfigPath + ) + + throw ToolkitError.chain(e, message, { code: 'ConfigWriteFailed' }) + } + } + + private async promptUserToConfigureSshConfig(configSection: string | undefined, section: string): Promise { + if (configSection !== undefined) { + await this.promptUserForOutdatedSection(configSection) + } + + const confirmTitle = localize( + 'AWS.codecatalyst.confirm.installSshConfig.title', + '{0} Toolkit will add host {1} to ~/.ssh/config to use SSH with your Dev Environments', + getIdeProperties().company, + this.configHostName + ) + const confirmText = localize('AWS.codecatalyst.confirm.installSshConfig.button', 'Update SSH config') + const response = await showConfirmationMessage({ prompt: confirmTitle, confirm: confirmText }) + if (!response) { + throw new CancellationError('user') + } + + await this.writeSectionToConfig(section) + } + // Check if the hostname pattern is working. protected async verifySSHHost({ section, proxyCommand }: { section: string; proxyCommand: string }) { const matchResult = await this.matchSshSection() @@ -206,52 +264,10 @@ export abstract class VscodeRemoteSshConfig { const hasProxyCommand = configSection?.includes(proxyCommand) if (!hasProxyCommand) { - if (configSection !== undefined) { - getLogger().warn( - `codecatalyst: SSH config: found old/outdated "${this.configHostName}" section:\n%O`, - configSection - ) - const oldConfig = localize( - 'AWS.codecatalyst.error.oldConfig', - 'Your ~/.ssh/config has a {0} section that might be out of date. Delete it, then try again.', - this.configHostName - ) - - const openConfig = localize('AWS.ssh.openConfig', 'Open config...') - vscode.window.showWarningMessage(oldConfig, openConfig).then(resp => { - if (resp === openConfig) { - vscode.window.showTextDocument(vscode.Uri.file(getSshConfigPath())) - } - }) - - return Result.err(new ToolkitError(oldConfig, { code: 'OldConfig' })) - } - - const confirmTitle = localize( - 'AWS.codecatalyst.confirm.installSshConfig.title', - '{0} Toolkit will add host {1} to ~/.ssh/config to use SSH with your Dev Environments', - getIdeProperties().company, - this.configHostName - ) - const confirmText = localize('AWS.codecatalyst.confirm.installSshConfig.button', 'Update SSH config') - const response = await showConfirmationMessage({ prompt: confirmTitle, confirm: confirmText }) - if (!response) { - return Result.err(new CancellationError('user')) - } - - const sshConfigPath = getSshConfigPath() try { - await fs.ensureDir(path.dirname(path.dirname(sshConfigPath)), { mode: 0o755 }) - await fs.ensureDir(path.dirname(sshConfigPath), 0o700) - await fs.appendFile(sshConfigPath, section, { mode: 0o600 }) + await this.promptUserToConfigureSshConfig(configSection, section) } catch (e) { - const message = localize( - 'AWS.codecatalyst.error.writeFail', - 'Failed to write SSH config: {0} (permission issue?)', - sshConfigPath - ) - - return Result.err(ToolkitError.chain(e, message, { code: 'ConfigWriteFailed' })) + return Result.err(e as Error) } } From 545b8a77469f06b775884bfa7861ed7b52311c22 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 11 Jul 2023 14:09:10 -0700 Subject: [PATCH 018/172] refactor the sshconfig to make more testable, add more tests --- src/codecatalyst/tools.ts | 4 +- src/ec2/tools.ts | 4 +- src/shared/extensions/ssh.ts | 14 +++--- src/test/shared/extensions/ssh.test.ts | 61 +++++++++++++++++++++----- 4 files changed, 61 insertions(+), 22 deletions(-) diff --git a/src/codecatalyst/tools.ts b/src/codecatalyst/tools.ts index 67c6b506618..8ca6861e38f 100644 --- a/src/codecatalyst/tools.ts +++ b/src/codecatalyst/tools.ts @@ -63,9 +63,7 @@ export class CodeCatalystSshConfig extends VscodeRemoteSshConfig { return proxyCommand } - const section = this.createSSHConfigSection(proxyCommand.unwrap()) - - const verifyHost = await this.verifySSHHost({ proxyCommand: proxyCommand.unwrap(), section }) + const verifyHost = await this.verifySSHHost(proxyCommand.unwrap()) if (verifyHost.isErr()) { return verifyHost } diff --git a/src/ec2/tools.ts b/src/ec2/tools.ts index 2ee320e70b4..1b988531329 100644 --- a/src/ec2/tools.ts +++ b/src/ec2/tools.ts @@ -17,9 +17,7 @@ export class Ec2RemoteSshConfig extends VscodeRemoteSshConfig { return proxyCommand } - const section = this.createSSHConfigSection(proxyCommand.unwrap()) - - const verifyHost = await this.verifySSHHost({ proxyCommand: proxyCommand.unwrap(), section }) + const verifyHost = await this.verifySSHHost(proxyCommand.unwrap()) if (verifyHost.isErr()) { return verifyHost } diff --git a/src/shared/extensions/ssh.ts b/src/shared/extensions/ssh.ts index 2ebee12db0d..e7deb13a0d3 100644 --- a/src/shared/extensions/ssh.ts +++ b/src/shared/extensions/ssh.ts @@ -216,8 +216,9 @@ export abstract class VscodeRemoteSshConfig { throw new ToolkitError(oldConfig, { code: 'OldConfig' }) } - private async writeSectionToConfig(section: string) { + private async writeSectionToConfig(proxyCommand: string) { const sshConfigPath = getSshConfigPath() + const section = this.createSSHConfigSection(proxyCommand) try { await fs.ensureDir(path.dirname(path.dirname(sshConfigPath)), { mode: 0o755 }) await fs.ensureDir(path.dirname(sshConfigPath), 0o700) @@ -233,7 +234,10 @@ export abstract class VscodeRemoteSshConfig { } } - private async promptUserToConfigureSshConfig(configSection: string | undefined, section: string): Promise { + protected async promptUserToConfigureSshConfig( + configSection: string | undefined, + proxyCommand: string + ): Promise { if (configSection !== undefined) { await this.promptUserForOutdatedSection(configSection) } @@ -250,11 +254,11 @@ export abstract class VscodeRemoteSshConfig { throw new CancellationError('user') } - await this.writeSectionToConfig(section) + await this.writeSectionToConfig(proxyCommand) } // Check if the hostname pattern is working. - protected async verifySSHHost({ section, proxyCommand }: { section: string; proxyCommand: string }) { + protected async verifySSHHost(proxyCommand: string) { const matchResult = await this.matchSshSection() if (matchResult.isErr()) { return matchResult @@ -265,7 +269,7 @@ export abstract class VscodeRemoteSshConfig { if (!hasProxyCommand) { try { - await this.promptUserToConfigureSshConfig(configSection, section) + await this.promptUserToConfigureSshConfig(configSection, proxyCommand) } catch (e) { return Result.err(e as Error) } diff --git a/src/test/shared/extensions/ssh.test.ts b/src/test/shared/extensions/ssh.test.ts index 948ae627781..5aa3df3118e 100644 --- a/src/test/shared/extensions/ssh.test.ts +++ b/src/test/shared/extensions/ssh.test.ts @@ -12,13 +12,15 @@ const testCommand = 'run-thing' class MockSshConfig extends VscodeRemoteSshConfig { private readonly testCommand: string = testCommand - protected readonly proxyCommandRegExp: RegExp = /run-thing/ + protected readonly proxyCommandRegExp: RegExp = new RegExp(`${testCommand}`) + // State variables to track logic flow. public testIsWin: boolean = false public configSection: string = '' + public SshConfigWritten: boolean = false protected override createSSHConfigSection(proxyCommand: string): string { - return 'test-config-section' + return this.configSection } public override async ensureValid(): Promise | Err | Ok> { @@ -27,9 +29,7 @@ class MockSshConfig extends VscodeRemoteSshConfig { return proxyCommand } - const section = this.createSSHConfigSection(proxyCommand.unwrap()) - - const verifyHost = await this.verifySSHHost({ proxyCommand: proxyCommand.unwrap(), section }) + const verifyHost = await this.verifySSHHost(proxyCommand.unwrap()) if (verifyHost.isErr()) { return verifyHost } @@ -41,14 +41,31 @@ class MockSshConfig extends VscodeRemoteSshConfig { return await this.getProxyCommand(command) } - public async matchSshSectionWrapper() { - return await this.matchSshSection() + public async testMatchSshSection(testSection: string) { + this.configSection = testSection + const result = await this.matchSshSection() + this.configSection = '' + return result + } + + public async testVerifySshHostWrapper(proxyCommand: string, testSection: string) { + this.configSection = testSection + const result = this.verifySSHHost(proxyCommand) + this.configSection = '' + return result } protected override isWin() { return this.testIsWin } + protected override async promptUserToConfigureSshConfig( + configSection: string | undefined, + section: string + ): Promise { + this.SshConfigWritten = true + } + protected override async checkSshOnHost(): Promise { return { exitCode: 0, @@ -78,19 +95,41 @@ describe('VscodeRemoteSshConfig', async function () { describe('matchSshSection', async function () { it('returns ok with match when proxycommand is present', async function () { - config.configSection = 'fdsafdsafdsarun-thing342432' - const result = await config.matchSshSectionWrapper() + const testSection = 'fdsafdsafdsarun-thing342432' + const result = await config.testMatchSshSection(testSection) assert.ok(result.isOk()) const match = result.unwrap() assert.ok(match) }) it('returns ok with undefined when proxycommand is not present', async function () { - config.configSection = 'fdsafdsafdsa342432' - const result = await config.matchSshSectionWrapper() + const testSection = 'fdsafdsafdsa342432' + const result = await config.testMatchSshSection(testSection) assert.ok(result.isOk()) const match = result.unwrap() assert.strictEqual(match, undefined) }) }) + + describe('verifySSHHost', async function () { + beforeEach(function () { + config.SshConfigWritten = false + }) + + it('writes to ssh config if command not found.', async function () { + const testSection = 'no-command-here' + const result = await config.testVerifySshHostWrapper(testCommand, testSection) + + assert.ok(result.isOk()) + assert.ok(config.SshConfigWritten) + }) + + it('does not write to ssh config if command is find', async function () { + const testSection = 'run-thing' + const result = await config.testVerifySshHostWrapper(testCommand, testSection) + + assert.ok(result.isOk()) + assert.ok(!config.SshConfigWritten) + }) + }) }) From 4f8c68088d95c102aa527dcf4bf55821ff67666a Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 11 Jul 2023 14:24:40 -0700 Subject: [PATCH 019/172] refactor tests to distinguish between command and proxyCommand --- src/test/shared/extensions/ssh.test.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/test/shared/extensions/ssh.test.ts b/src/test/shared/extensions/ssh.test.ts index 5aa3df3118e..e6b3a572745 100644 --- a/src/test/shared/extensions/ssh.test.ts +++ b/src/test/shared/extensions/ssh.test.ts @@ -9,10 +9,11 @@ import { Err, Ok, Result } from '../../../shared/utilities/result' import { ChildProcessResult } from '../../../shared/utilities/childProcess' const testCommand = 'run-thing' +const testProxyCommand = `'run-thing' '%h'` class MockSshConfig extends VscodeRemoteSshConfig { private readonly testCommand: string = testCommand - protected readonly proxyCommandRegExp: RegExp = new RegExp(`${testCommand}`) + protected readonly proxyCommandRegExp: RegExp = new RegExp(`${testProxyCommand}`) // State variables to track logic flow. public testIsWin: boolean = false @@ -95,15 +96,16 @@ describe('VscodeRemoteSshConfig', async function () { describe('matchSshSection', async function () { it('returns ok with match when proxycommand is present', async function () { - const testSection = 'fdsafdsafdsarun-thing342432' + const testSection = `fdsafdsafd${testProxyCommand}sa342432` const result = await config.testMatchSshSection(testSection) + console.log(result) assert.ok(result.isOk()) const match = result.unwrap() assert.ok(match) }) it('returns ok with undefined when proxycommand is not present', async function () { - const testSection = 'fdsafdsafdsa342432' + const testSection = `fdsafdsafdsa342432` const result = await config.testMatchSshSection(testSection) assert.ok(result.isOk()) const match = result.unwrap() @@ -118,15 +120,15 @@ describe('VscodeRemoteSshConfig', async function () { it('writes to ssh config if command not found.', async function () { const testSection = 'no-command-here' - const result = await config.testVerifySshHostWrapper(testCommand, testSection) + const result = await config.testVerifySshHostWrapper(testProxyCommand, testSection) assert.ok(result.isOk()) assert.ok(config.SshConfigWritten) }) it('does not write to ssh config if command is find', async function () { - const testSection = 'run-thing' - const result = await config.testVerifySshHostWrapper(testCommand, testSection) + const testSection = `this is some text that doesn't matter, but here ${testProxyCommand}` + const result = await config.testVerifySshHostWrapper(testProxyCommand, testSection) assert.ok(result.isOk()) assert.ok(!config.SshConfigWritten) From 222fcfd14398d84e4de5c6efd5307ec49a634701 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 11 Jul 2023 14:27:45 -0700 Subject: [PATCH 020/172] avoid hard-coding in test --- src/test/shared/extensions/ssh.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/shared/extensions/ssh.test.ts b/src/test/shared/extensions/ssh.test.ts index e6b3a572745..31ba0df8c9c 100644 --- a/src/test/shared/extensions/ssh.test.ts +++ b/src/test/shared/extensions/ssh.test.ts @@ -90,7 +90,7 @@ describe('VscodeRemoteSshConfig', async function () { const result = await config.getProxyCommandWrapper(testCommand) assert.ok(result.isOk()) const command = result.unwrap() - assert.strictEqual(command, `'${testCommand}' '%h'`) + assert.strictEqual(command, testProxyCommand) }) }) @@ -98,7 +98,6 @@ describe('VscodeRemoteSshConfig', async function () { it('returns ok with match when proxycommand is present', async function () { const testSection = `fdsafdsafd${testProxyCommand}sa342432` const result = await config.testMatchSshSection(testSection) - console.log(result) assert.ok(result.isOk()) const match = result.unwrap() assert.ok(match) From 0e5f7a209d1a129410b9f1c6c522bd65ab534825 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 11 Jul 2023 14:30:18 -0700 Subject: [PATCH 021/172] avoid other hard-coding in tests --- src/test/shared/extensions/ssh.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/shared/extensions/ssh.test.ts b/src/test/shared/extensions/ssh.test.ts index 31ba0df8c9c..d5188847a4c 100644 --- a/src/test/shared/extensions/ssh.test.ts +++ b/src/test/shared/extensions/ssh.test.ts @@ -9,7 +9,7 @@ import { Err, Ok, Result } from '../../../shared/utilities/result' import { ChildProcessResult } from '../../../shared/utilities/childProcess' const testCommand = 'run-thing' -const testProxyCommand = `'run-thing' '%h'` +const testProxyCommand = `'${testCommand}' '%h'` class MockSshConfig extends VscodeRemoteSshConfig { private readonly testCommand: string = testCommand From e87f24d5448e32ca1dd5e7cf7dcedf28825307bd Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 11 Jul 2023 15:14:04 -0700 Subject: [PATCH 022/172] change simple function to be in lined --- src/ec2/prompter.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/ec2/prompter.ts b/src/ec2/prompter.ts index a77bba5e4b8..1cf30670ac2 100644 --- a/src/ec2/prompter.ts +++ b/src/ec2/prompter.ts @@ -7,7 +7,6 @@ import { RegionSubmenu, RegionSubmenuResponse } from '../shared/ui/common/region import { Ec2Selection, getInstancesFromRegion } from './utils' import { DataQuickPickItem } from '../shared/ui/pickerPrompter' import { Ec2Instance } from '../shared/clients/ec2Client' -import { PromptResult } from '../shared/ui/prompter' import { isValidResponse } from '../shared/wizards/wizard' import { CancellationError } from '../shared/utilities/timeoutUtils' @@ -19,7 +18,10 @@ function asQuickpickItem(instance: Ec2Instance): DataQuickPickItem { } } -function getSelectionFromResponse(response: PromptResult>): Ec2Selection { +export async function promptUserForEc2Selection(): Promise { + const prompter = createEc2ConnectPrompter() + const response = await prompter.prompt() + if (isValidResponse(response)) { return handleEc2ConnectPrompterResponse(response) } else { @@ -27,13 +29,6 @@ function getSelectionFromResponse(response: PromptResult { - const prompter = createEc2ConnectPrompter() - const response = await prompter.prompt() - - return getSelectionFromResponse(response) -} - export function handleEc2ConnectPrompterResponse(response: RegionSubmenuResponse): Ec2Selection { return { instanceId: response.data, From 3764b4a184a7f8a082ff14e807812ae24f00d107 Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Tue, 11 Jul 2023 15:15:08 -0700 Subject: [PATCH 023/172] change command wording to be less clunky. Co-authored-by: JadenSimon <31319484+JadenSimon@users.noreply.github.com> --- package.nls.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.nls.json b/package.nls.json index 0185c848605..2b82ec9a377 100644 --- a/package.nls.json +++ b/package.nls.json @@ -108,7 +108,7 @@ "AWS.command.refreshAwsExplorer": "Refresh Explorer", "AWS.command.refreshCdkExplorer": "Refresh CDK Explorer", "AWS.command.cdk.help": "View CDK Documentation", - "AWS.command.ec2.openTerminal": "Open terminal in EC2 instance...", + "AWS.command.ec2.openTerminal": "Open terminal to EC2 instance...", "AWS.command.ec2.openRemoteConnection": "Open EC2 instance in VSCode remote...", "AWS.command.ecr.copyTagUri": "Copy Tag URI", "AWS.command.ecr.copyRepositoryUri": "Copy Repository URI", From 1c3be086c52ba3b60664592558b6115046b2a1d3 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 11 Jul 2023 15:16:44 -0700 Subject: [PATCH 024/172] change command phrasing to mirror ssh extension --- package.nls.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.nls.json b/package.nls.json index 2b82ec9a377..2465a2b272e 100644 --- a/package.nls.json +++ b/package.nls.json @@ -109,7 +109,7 @@ "AWS.command.refreshCdkExplorer": "Refresh CDK Explorer", "AWS.command.cdk.help": "View CDK Documentation", "AWS.command.ec2.openTerminal": "Open terminal to EC2 instance...", - "AWS.command.ec2.openRemoteConnection": "Open EC2 instance in VSCode remote...", + "AWS.command.ec2.openRemoteConnection": "Connect to EC2 instance in New Window...", "AWS.command.ecr.copyTagUri": "Copy Tag URI", "AWS.command.ecr.copyRepositoryUri": "Copy Repository URI", "AWS.command.ecr.createRepository": "Create Repository...", From 056e807b4d084ad40c3a295c74a356e3e77518eb Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Wed, 12 Jul 2023 11:59:52 -0700 Subject: [PATCH 025/172] remove commented out code --- src/ec2/model.ts | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 9fe2345d605..0b4d50a0bb8 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -203,34 +203,3 @@ function getEc2SsmEnv(region: string, ssmPath: string): NodeJS.ProcessEnv { process.env ) } - -// function ensureEc2SshConfig(sshPath: string) { -// const iswin = process.platform === 'win32' -// const proxyCommand = "sh -c 'aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p''" -// const section = createSSHConfigSection(proxyCommand) - -// } - -// function createSSHConfigSection(proxyCommand: string): string { -// return ` -// # Created by AWS Toolkit for VSCode. https://github.com/aws/aws-toolkit-vscode -// Host i-* mi-* -// User ${hostname} -// ProxyCommand ${proxyCommand} -// ` -// } - -// async function verifySSHHost({sshPath, section, proxyCommand}: { -// sshPath: string -// section: string -// proxyCommand: string -// }) { -// const proxyCommandRegEx = new RegExp(`/proxycommand.{0,1024}${proxyCommand}.{0,99}/i`) -// const matchResult = await matchSshSection(sshPath, `i-123456`, proxyCommandRegEx) -// if (matchResult.isErr()) { -// return matchResult -// } -// const configSection = matchResult.ok() -// const hasProxyCommand = configSection?.includes(proxyCommand) - -// } From 153bf629bc7aff8275271687d2a900bf8963b7ef Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 08:30:46 -0700 Subject: [PATCH 026/172] update command file with icon --- package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/package.json b/package.json index 859acd52a07..073e2bb7c98 100644 --- a/package.json +++ b/package.json @@ -1317,6 +1317,11 @@ "group": "0@1", "when": "viewItem == awsEc2Node" }, + { + "command": "aws.ec2.openTerminal", + "group": "inline@1", + "when": "viewItem == awsEc2Node" + }, { "command": "aws.ecr.createRepository", "when": "view == aws.explorer && viewItem == awsEcrNode", @@ -2062,6 +2067,7 @@ { "command": "aws.ec2.openTerminal", "title": "%AWS.command.ec2.openTerminal%", + "icon": "$(terminal-view-icon)", "category": "%AWS.title%", "cloud9": { "cn": { From bd71166fc36b86ebf26ec6ad5e04440ddd6d42f0 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 08:31:51 -0700 Subject: [PATCH 027/172] update context value for parent node --- src/ec2/explorer/ec2ParentNode.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ec2/explorer/ec2ParentNode.ts b/src/ec2/explorer/ec2ParentNode.ts index a3cac1d386c..a7edd5a108e 100644 --- a/src/ec2/explorer/ec2ParentNode.ts +++ b/src/ec2/explorer/ec2ParentNode.ts @@ -15,6 +15,7 @@ export const contextValueEc2 = 'awsEc2Node' export class Ec2ParentNode extends AWSTreeNodeBase { protected readonly placeHolderMessage = '[No EC2 Instances Found]' protected readonly ec2InstanceNodes: Map + public override readonly contextValue: string = contextValueEc2 public constructor( public override readonly regionCode: string, From e027e4053b23f916f9bbdc47d339466b409bdbce Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 08:32:36 -0700 Subject: [PATCH 028/172] handle case where parent node is passed to command --- src/ec2/activation.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ec2/activation.ts b/src/ec2/activation.ts index 35f732d77b9..f67a9630e79 100644 --- a/src/ec2/activation.ts +++ b/src/ec2/activation.ts @@ -8,21 +8,23 @@ import { telemetry } from '../shared/telemetry/telemetry' import { Ec2InstanceNode } from './explorer/ec2InstanceNode' import { promptUserForEc2Selection } from './prompter' import { Ec2ConnectionManager } from './model' +import { Ec2ParentNode } from './explorer/ec2ParentNode' export async function activate(ctx: ExtContext): Promise { ctx.extensionContext.subscriptions.push( - Commands.register('aws.ec2.openTerminal', async (node?: Ec2InstanceNode) => { + Commands.register('aws.ec2.openTerminal', async (node?: Ec2InstanceNode | Ec2ParentNode) => { await telemetry.ec2_connectToInstance.run(async span => { span.record({ ec2ConnectionType: 'ssm' }) - const selection = node ? node.toSelection() : await promptUserForEc2Selection() + const selection = + node instanceof Ec2InstanceNode ? node.toSelection() : await promptUserForEc2Selection() const connectionManager = new Ec2ConnectionManager(selection.region) await connectionManager.attemptToOpenEc2Terminal(selection) }) }), - Commands.register('aws.ec2.openRemoteConnection', async (node?: Ec2InstanceNode) => { - const selection = node ? node.toSelection() : await promptUserForEc2Selection() + Commands.register('aws.ec2.openRemoteConnection', async (node?: Ec2InstanceNode | Ec2ParentNode) => { + const selection = node instanceof Ec2InstanceNode ? node.toSelection() : await promptUserForEc2Selection() //const connectionManager = new Ec2ConnectionManager(selection.region) console.log(selection) }) From 9d6215414df8afb676e35a9d20fb7ad0d53ff698 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 08:32:55 -0700 Subject: [PATCH 029/172] change outdated prompter text --- src/ec2/prompter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ec2/prompter.ts b/src/ec2/prompter.ts index 1cf30670ac2..3452305e146 100644 --- a/src/ec2/prompter.ts +++ b/src/ec2/prompter.ts @@ -39,7 +39,7 @@ export function handleEc2ConnectPrompterResponse(response: RegionSubmenuResponse export function createEc2ConnectPrompter(): RegionSubmenu { return new RegionSubmenu( async region => (await getInstancesFromRegion(region)).map(asQuickpickItem).promise(), - { title: 'Select EC2 Instance Id', matchOnDetail: true }, + { title: 'Select EC2 Instance', matchOnDetail: true }, { title: 'Select Region for EC2 Instance' }, 'Instances' ) From f28809b4f4c47226485262a6c8670c85efa302c9 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 08:46:40 -0700 Subject: [PATCH 030/172] refactor commands to own file --- src/ec2/activation.ts | 20 ++++++-------------- src/ec2/commands.ts | 22 ++++++++++++++++++++++ src/ec2/explorer/ec2ParentNode.ts | 1 + 3 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 src/ec2/commands.ts diff --git a/src/ec2/activation.ts b/src/ec2/activation.ts index f67a9630e79..a75d7dac266 100644 --- a/src/ec2/activation.ts +++ b/src/ec2/activation.ts @@ -5,28 +5,20 @@ import { ExtContext } from '../shared/extensions' import { Commands } from '../shared/vscode/commands2' import { telemetry } from '../shared/telemetry/telemetry' -import { Ec2InstanceNode } from './explorer/ec2InstanceNode' -import { promptUserForEc2Selection } from './prompter' -import { Ec2ConnectionManager } from './model' -import { Ec2ParentNode } from './explorer/ec2ParentNode' +import { Ec2Node } from './explorer/ec2ParentNode' +import { openRemoteConnection, openTerminal } from './commands' export async function activate(ctx: ExtContext): Promise { ctx.extensionContext.subscriptions.push( - Commands.register('aws.ec2.openTerminal', async (node?: Ec2InstanceNode | Ec2ParentNode) => { + Commands.register('aws.ec2.openTerminal', async (node?: Ec2Node) => { await telemetry.ec2_connectToInstance.run(async span => { span.record({ ec2ConnectionType: 'ssm' }) - const selection = - node instanceof Ec2InstanceNode ? node.toSelection() : await promptUserForEc2Selection() - - const connectionManager = new Ec2ConnectionManager(selection.region) - await connectionManager.attemptToOpenEc2Terminal(selection) + await (node ? openTerminal(node) : openTerminal(node)) }) }), - Commands.register('aws.ec2.openRemoteConnection', async (node?: Ec2InstanceNode | Ec2ParentNode) => { - const selection = node instanceof Ec2InstanceNode ? node.toSelection() : await promptUserForEc2Selection() - //const connectionManager = new Ec2ConnectionManager(selection.region) - console.log(selection) + Commands.register('aws.ec2.openRemoteConnection', async (node?: Ec2Node) => { + await (node ? openRemoteConnection(node) : openTerminal(node)) }) ) } diff --git a/src/ec2/commands.ts b/src/ec2/commands.ts new file mode 100644 index 00000000000..41fd5e14a5b --- /dev/null +++ b/src/ec2/commands.ts @@ -0,0 +1,22 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Ec2InstanceNode } from './explorer/ec2InstanceNode' +import { Ec2Node } from './explorer/ec2ParentNode' +import { Ec2ConnectionManager } from './model' +import { promptUserForEc2Selection } from './prompter' + +export async function openTerminal(node?: Ec2Node) { + const selection = node instanceof Ec2InstanceNode ? node.toSelection() : await promptUserForEc2Selection() + + const connectionManager = new Ec2ConnectionManager(selection.region) + await connectionManager.attemptToOpenEc2Terminal(selection) +} + +export async function openRemoteConnection(node?: Ec2Node) { + const selection = node instanceof Ec2InstanceNode ? node.toSelection() : await promptUserForEc2Selection() + //const connectionManager = new Ec2ConnectionManager(selection.region) + console.log(selection) +} diff --git a/src/ec2/explorer/ec2ParentNode.ts b/src/ec2/explorer/ec2ParentNode.ts index a7edd5a108e..a0b99fc7c17 100644 --- a/src/ec2/explorer/ec2ParentNode.ts +++ b/src/ec2/explorer/ec2ParentNode.ts @@ -11,6 +11,7 @@ import { Ec2Client } from '../../shared/clients/ec2Client' import { updateInPlace } from '../../shared/utilities/collectionUtils' export const contextValueEc2 = 'awsEc2Node' +export type Ec2Node = Ec2InstanceNode | Ec2ParentNode export class Ec2ParentNode extends AWSTreeNodeBase { protected readonly placeHolderMessage = '[No EC2 Instances Found]' From 3cc9aa648a6947626a1b488c21cf604b001b642f Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 10:38:12 -0700 Subject: [PATCH 031/172] add start command to package.json files --- package.json | 14 ++++++++++++++ package.nls.json | 1 + 2 files changed, 15 insertions(+) diff --git a/package.json b/package.json index 073e2bb7c98..71bfdf1cf83 100644 --- a/package.json +++ b/package.json @@ -1118,6 +1118,10 @@ "command": "aws.ec2.openTerminal", "when": "aws.isDevMode" }, + { + "command": "aws.ec2.startInstance", + "whem": "aws.isDevMode" + }, { "command": "aws.dev.openMenu", "when": "aws.isDevMode || isCloud9" @@ -2085,6 +2089,16 @@ } } }, + { + "command": "aws.ec2.startInstance", + "title": "%AWS.command.ec2.startInstance%", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, { "command": "aws.ecr.copyTagUri", "title": "%AWS.command.ecr.copyTagUri%", diff --git a/package.nls.json b/package.nls.json index 2465a2b272e..e39cf30bef8 100644 --- a/package.nls.json +++ b/package.nls.json @@ -110,6 +110,7 @@ "AWS.command.cdk.help": "View CDK Documentation", "AWS.command.ec2.openTerminal": "Open terminal to EC2 instance...", "AWS.command.ec2.openRemoteConnection": "Connect to EC2 instance in New Window...", + "AWS.command.ec2.startInstance": "Start EC2 Instance...", "AWS.command.ecr.copyTagUri": "Copy Tag URI", "AWS.command.ecr.copyRepositoryUri": "Copy Repository URI", "AWS.command.ecr.createRepository": "Create Repository...", From 732778f8f3cf6b6a825d0982f4f650d916cd17db Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 10:42:07 -0700 Subject: [PATCH 032/172] add command to start the instance --- src/ec2/activation.ts | 6 +++++- src/ec2/commands.ts | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/ec2/activation.ts b/src/ec2/activation.ts index a75d7dac266..cccd480b6c2 100644 --- a/src/ec2/activation.ts +++ b/src/ec2/activation.ts @@ -6,7 +6,7 @@ import { ExtContext } from '../shared/extensions' import { Commands } from '../shared/vscode/commands2' import { telemetry } from '../shared/telemetry/telemetry' import { Ec2Node } from './explorer/ec2ParentNode' -import { openRemoteConnection, openTerminal } from './commands' +import { openRemoteConnection, openTerminal, startInstance } from './commands' export async function activate(ctx: ExtContext): Promise { ctx.extensionContext.subscriptions.push( @@ -19,6 +19,10 @@ export async function activate(ctx: ExtContext): Promise { Commands.register('aws.ec2.openRemoteConnection', async (node?: Ec2Node) => { await (node ? openRemoteConnection(node) : openTerminal(node)) + }), + + Commands.register('aws.ec2.startInstance', async (node?: Ec2Node) => { + await (node ? startInstance(node) : startInstance(node)) }) ) } diff --git a/src/ec2/commands.ts b/src/ec2/commands.ts index 41fd5e14a5b..14157067b0b 100644 --- a/src/ec2/commands.ts +++ b/src/ec2/commands.ts @@ -20,3 +20,8 @@ export async function openRemoteConnection(node?: Ec2Node) { //const connectionManager = new Ec2ConnectionManager(selection.region) console.log(selection) } + +export async function startInstance(node?: Ec2Node) { + const selection = node instanceof Ec2InstanceNode ? node.toSelection() : await promptUserForEc2Selection() + console.log(selection) +} From 2cbb723818207ff4d5cd36554ce012a9dfd0b359 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 10:43:03 -0700 Subject: [PATCH 033/172] fix typo in which method was being invoked --- src/ec2/activation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ec2/activation.ts b/src/ec2/activation.ts index a75d7dac266..b1bc6dea6e6 100644 --- a/src/ec2/activation.ts +++ b/src/ec2/activation.ts @@ -18,7 +18,7 @@ export async function activate(ctx: ExtContext): Promise { }), Commands.register('aws.ec2.openRemoteConnection', async (node?: Ec2Node) => { - await (node ? openRemoteConnection(node) : openTerminal(node)) + await (node ? openRemoteConnection(node) : openRemoteConnection(node)) }) ) } From ea30d8654259b4ab68988863921a0ce8179bd15f Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 10:56:42 -0700 Subject: [PATCH 034/172] refactor prompter and commands file --- src/ec2/commands.ts | 15 +++++++++--- src/ec2/prompter.ts | 59 ++++++++++++++++++++++++--------------------- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/src/ec2/commands.ts b/src/ec2/commands.ts index 14157067b0b..31e1878cc9d 100644 --- a/src/ec2/commands.ts +++ b/src/ec2/commands.ts @@ -6,22 +6,29 @@ import { Ec2InstanceNode } from './explorer/ec2InstanceNode' import { Ec2Node } from './explorer/ec2ParentNode' import { Ec2ConnectionManager } from './model' -import { promptUserForEc2Selection } from './prompter' +import { Ec2Prompter } from './prompter' +import { Ec2Selection } from './utils' export async function openTerminal(node?: Ec2Node) { - const selection = node instanceof Ec2InstanceNode ? node.toSelection() : await promptUserForEc2Selection() + const selection = await getSelection(node) const connectionManager = new Ec2ConnectionManager(selection.region) await connectionManager.attemptToOpenEc2Terminal(selection) } export async function openRemoteConnection(node?: Ec2Node) { - const selection = node instanceof Ec2InstanceNode ? node.toSelection() : await promptUserForEc2Selection() + const selection = await getSelection(node) //const connectionManager = new Ec2ConnectionManager(selection.region) console.log(selection) } export async function startInstance(node?: Ec2Node) { - const selection = node instanceof Ec2InstanceNode ? node.toSelection() : await promptUserForEc2Selection() + const selection = await getSelection(node) console.log(selection) } + +async function getSelection(node: Ec2Node | undefined): Promise { + const prompter = new Ec2Prompter() + const selection = node && node instanceof Ec2InstanceNode ? node.toSelection() : await prompter.promptUser() + return selection +} diff --git a/src/ec2/prompter.ts b/src/ec2/prompter.ts index 3452305e146..a17de417243 100644 --- a/src/ec2/prompter.ts +++ b/src/ec2/prompter.ts @@ -10,37 +10,42 @@ import { Ec2Instance } from '../shared/clients/ec2Client' import { isValidResponse } from '../shared/wizards/wizard' import { CancellationError } from '../shared/utilities/timeoutUtils' -function asQuickpickItem(instance: Ec2Instance): DataQuickPickItem { - return { - label: '$(terminal) \t' + (instance.name ?? '(no name)'), - detail: instance.InstanceId, - data: instance.InstanceId, - } -} +export class Ec2Prompter { + public constructor() {} -export async function promptUserForEc2Selection(): Promise { - const prompter = createEc2ConnectPrompter() - const response = await prompter.prompt() + private static asQuickPickItem(instance: Ec2Instance): DataQuickPickItem { + return { + label: '$(terminal) \t' + (instance.name ?? '(no name)'), + detail: instance.InstanceId, + data: instance.InstanceId, + } + } - if (isValidResponse(response)) { - return handleEc2ConnectPrompterResponse(response) - } else { - throw new CancellationError('user') + private static handleEc2ConnectPrompterResponse(response: RegionSubmenuResponse): Ec2Selection { + return { + instanceId: response.data, + region: response.region, + } } -} -export function handleEc2ConnectPrompterResponse(response: RegionSubmenuResponse): Ec2Selection { - return { - instanceId: response.data, - region: response.region, + public async promptUser(): Promise { + const prompter = this.createEc2ConnectPrompter() + const response = await prompter.prompt() + + if (isValidResponse(response)) { + return Ec2Prompter.handleEc2ConnectPrompterResponse(response) + } else { + throw new CancellationError('user') + } } -} -export function createEc2ConnectPrompter(): RegionSubmenu { - return new RegionSubmenu( - async region => (await getInstancesFromRegion(region)).map(asQuickpickItem).promise(), - { title: 'Select EC2 Instance', matchOnDetail: true }, - { title: 'Select Region for EC2 Instance' }, - 'Instances' - ) + private createEc2ConnectPrompter(): RegionSubmenu { + return new RegionSubmenu( + async region => + (await getInstancesFromRegion(region)).map(instance => Ec2Prompter.asQuickPickItem(instance)).promise(), + { title: 'Select EC2 Instance', matchOnDetail: true }, + { title: 'Select Region for EC2 Instance' }, + 'Instances' + ) + } } From 52a2fde0cf8f0128909c3735f2381ce2621c223c Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 11:42:22 -0700 Subject: [PATCH 035/172] add tests for new structure of prompter --- src/ec2/prompter.ts | 6 +-- src/test/ec2/prompter.test.ts | 86 +++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 src/test/ec2/prompter.test.ts diff --git a/src/ec2/prompter.ts b/src/ec2/prompter.ts index a17de417243..9790227edf5 100644 --- a/src/ec2/prompter.ts +++ b/src/ec2/prompter.ts @@ -13,7 +13,7 @@ import { CancellationError } from '../shared/utilities/timeoutUtils' export class Ec2Prompter { public constructor() {} - private static asQuickPickItem(instance: Ec2Instance): DataQuickPickItem { + protected static asQuickPickItem(instance: Ec2Instance): DataQuickPickItem { return { label: '$(terminal) \t' + (instance.name ?? '(no name)'), detail: instance.InstanceId, @@ -21,7 +21,7 @@ export class Ec2Prompter { } } - private static handleEc2ConnectPrompterResponse(response: RegionSubmenuResponse): Ec2Selection { + protected static getSelectionFromResponse(response: RegionSubmenuResponse): Ec2Selection { return { instanceId: response.data, region: response.region, @@ -33,7 +33,7 @@ export class Ec2Prompter { const response = await prompter.prompt() if (isValidResponse(response)) { - return Ec2Prompter.handleEc2ConnectPrompterResponse(response) + return Ec2Prompter.getSelectionFromResponse(response) } else { throw new CancellationError('user') } diff --git a/src/test/ec2/prompter.test.ts b/src/test/ec2/prompter.test.ts new file mode 100644 index 00000000000..edc2c0584ab --- /dev/null +++ b/src/test/ec2/prompter.test.ts @@ -0,0 +1,86 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as assert from 'assert' +import { Ec2Prompter } from '../../ec2/prompter' +import { Ec2Instance } from '../../shared/clients/ec2Client' +import { RegionSubmenuResponse } from '../../shared/ui/common/regionSubmenu' +import { Ec2Selection } from '../../ec2/utils' + +describe('Ec2Prompter', async function () { + class MockEc2Prompter extends Ec2Prompter { + public testAsQuickPickItem(testInstance: Ec2Instance) { + return Ec2Prompter.asQuickPickItem(testInstance) + } + + public testGetSelectionFromResponse(response: RegionSubmenuResponse): Ec2Selection { + return Ec2Prompter.getSelectionFromResponse(response) + } + } + it('initializes properly', function () { + const prompter = new Ec2Prompter() + assert.ok(prompter) + }) + + describe('asQuickPickItem', async function () { + let prompter: MockEc2Prompter + + before(function () { + prompter = new MockEc2Prompter() + }) + + it('returns QuickPickItem for named instances', function () { + const testInstance = { + name: 'testName', + InstanceId: 'testInstanceId', + } + + const result = prompter.testAsQuickPickItem(testInstance) + const expected = { + label: '$(terminal) \t' + testInstance.name, + detail: testInstance.InstanceId, + data: testInstance.InstanceId, + } + assert.deepStrictEqual(result, expected) + }) + + it('returns QuickPickItem for non-named instances', function () { + const testInstance = { + InstanceId: 'testInstanceId', + } + + const result = prompter.testAsQuickPickItem(testInstance) + const expected = { + label: '$(terminal) \t' + '(no name)', + detail: testInstance.InstanceId, + data: testInstance.InstanceId, + } + + assert.deepStrictEqual(result, expected) + }) + }) + + describe('handleEc2ConnectPrompterResponse', function () { + let prompter: MockEc2Prompter + + before(function () { + prompter = new MockEc2Prompter() + }) + + it('returns correctly formatted Ec2Selection', function () { + const testResponse: RegionSubmenuResponse = { + region: 'test-region', + data: 'testInstance', + } + + const result = prompter.testGetSelectionFromResponse(testResponse) + const expected: Ec2Selection = { + instanceId: testResponse.data, + region: testResponse.region, + } + + assert.deepStrictEqual(result, expected) + }) + }) +}) From 8fac525293b94971200d89ba1220acb3772964aa Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 11:56:49 -0700 Subject: [PATCH 036/172] refactor prompter code to be more testable --- src/ec2/prompter.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/ec2/prompter.ts b/src/ec2/prompter.ts index 9790227edf5..f21d77e9fc8 100644 --- a/src/ec2/prompter.ts +++ b/src/ec2/prompter.ts @@ -4,11 +4,12 @@ */ import { RegionSubmenu, RegionSubmenuResponse } from '../shared/ui/common/regionSubmenu' -import { Ec2Selection, getInstancesFromRegion } from './utils' +import { Ec2Selection } from './utils' import { DataQuickPickItem } from '../shared/ui/pickerPrompter' -import { Ec2Instance } from '../shared/clients/ec2Client' +import { Ec2Client, Ec2Instance } from '../shared/clients/ec2Client' import { isValidResponse } from '../shared/wizards/wizard' import { CancellationError } from '../shared/utilities/timeoutUtils' +import { AsyncCollection } from '../shared/utilities/asyncCollection' export class Ec2Prompter { public constructor() {} @@ -39,10 +40,20 @@ export class Ec2Prompter { } } + protected async getInstancesFromRegion(regionCode: string): Promise> { + const client = new Ec2Client(regionCode) + return await client.getInstances() + } + + private async getInstancesAsQuickPickItem(region: string): Promise[]> { + return (await this.getInstancesFromRegion(region)) + .map(instance => Ec2Prompter.asQuickPickItem(instance)) + .promise() + } + private createEc2ConnectPrompter(): RegionSubmenu { return new RegionSubmenu( - async region => - (await getInstancesFromRegion(region)).map(instance => Ec2Prompter.asQuickPickItem(instance)).promise(), + async region => this.getInstancesAsQuickPickItem(region), { title: 'Select EC2 Instance', matchOnDetail: true }, { title: 'Select Region for EC2 Instance' }, 'Instances' From 004ac0b635bc2a59c52a081458fbe168e0b88ecf Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 12:10:05 -0700 Subject: [PATCH 037/172] add baseline test to item provider --- src/ec2/prompter.ts | 4 +-- src/test/ec2/prompter.test.ts | 58 +++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/ec2/prompter.ts b/src/ec2/prompter.ts index f21d77e9fc8..a717b872d57 100644 --- a/src/ec2/prompter.ts +++ b/src/ec2/prompter.ts @@ -45,7 +45,7 @@ export class Ec2Prompter { return await client.getInstances() } - private async getInstancesAsQuickPickItem(region: string): Promise[]> { + protected async getInstancesAsQuickPickItems(region: string): Promise[]> { return (await this.getInstancesFromRegion(region)) .map(instance => Ec2Prompter.asQuickPickItem(instance)) .promise() @@ -53,7 +53,7 @@ export class Ec2Prompter { private createEc2ConnectPrompter(): RegionSubmenu { return new RegionSubmenu( - async region => this.getInstancesAsQuickPickItem(region), + async region => this.getInstancesAsQuickPickItems(region), { title: 'Select EC2 Instance', matchOnDetail: true }, { title: 'Select Region for EC2 Instance' }, 'Instances' diff --git a/src/test/ec2/prompter.test.ts b/src/test/ec2/prompter.test.ts index edc2c0584ab..0cd57a61a19 100644 --- a/src/test/ec2/prompter.test.ts +++ b/src/test/ec2/prompter.test.ts @@ -7,9 +7,14 @@ import { Ec2Prompter } from '../../ec2/prompter' import { Ec2Instance } from '../../shared/clients/ec2Client' import { RegionSubmenuResponse } from '../../shared/ui/common/regionSubmenu' import { Ec2Selection } from '../../ec2/utils' +import { AsyncCollection } from '../../shared/utilities/asyncCollection' +import { intoCollection } from '../../shared/utilities/collectionUtils' +import { DataQuickPickItem } from '../../shared/ui/pickerPrompter' describe('Ec2Prompter', async function () { class MockEc2Prompter extends Ec2Prompter { + public instances: Ec2Instance[] = [] + public testAsQuickPickItem(testInstance: Ec2Instance) { return Ec2Prompter.asQuickPickItem(testInstance) } @@ -17,6 +22,13 @@ describe('Ec2Prompter', async function () { public testGetSelectionFromResponse(response: RegionSubmenuResponse): Ec2Selection { return Ec2Prompter.getSelectionFromResponse(response) } + public async testGetInstancesAsQuickPickItems(region: string): Promise[]> { + return this.getInstancesAsQuickPickItems(region) + } + + protected override async getInstancesFromRegion(regionCode: string): Promise> { + return intoCollection(this.instances) + } } it('initializes properly', function () { const prompter = new Ec2Prompter() @@ -83,4 +95,50 @@ describe('Ec2Prompter', async function () { assert.deepStrictEqual(result, expected) }) }) + + describe('getInstancesAsQuickPickItem', async function () { + let prompter: MockEc2Prompter + + before(function () { + prompter = new MockEc2Prompter() + }) + + beforeEach(function () { + prompter.instances = [] + }) + + it('returns empty when no instances present', async function () { + const items = await prompter.testGetInstancesAsQuickPickItems('test-region') + assert.ok(items.length === 0) + }) + + it('returns items mapped to QuickPick items without filter', async function () { + prompter.instances = [ + { + InstanceId: 'test-id1', + name: 'test-name1', + }, + { + InstanceId: 'test-id2', + name: 'test-name2', + }, + ] + + const expected = [ + { + label: '$(terminal) \t' + prompter.instances[0].name!, + detail: prompter.instances[0].InstanceId!, + data: prompter.instances[0].InstanceId!, + }, + { + label: '$(terminal) \t' + prompter.instances[1].name!, + detail: prompter.instances[1].InstanceId!, + data: prompter.instances[1].InstanceId!, + }, + ] + + const items = await prompter.testGetInstancesAsQuickPickItems('test-region') + assert.deepStrictEqual(items, expected) + }) + }) }) From f85f0ee853359c24eedbcfb290cadcfe7a0041fa Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 12:24:11 -0700 Subject: [PATCH 038/172] add instance filter and tests for it --- src/ec2/prompter.ts | 5 +++- src/test/ec2/prompter.test.ts | 53 ++++++++++++++++++++++++++++------- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/ec2/prompter.ts b/src/ec2/prompter.ts index a717b872d57..e7b0fde1ea9 100644 --- a/src/ec2/prompter.ts +++ b/src/ec2/prompter.ts @@ -11,8 +11,10 @@ import { isValidResponse } from '../shared/wizards/wizard' import { CancellationError } from '../shared/utilities/timeoutUtils' import { AsyncCollection } from '../shared/utilities/asyncCollection' +export type instanceFilter = (instance: Ec2Instance) => boolean + export class Ec2Prompter { - public constructor() {} + public constructor(protected filter?: instanceFilter) {} protected static asQuickPickItem(instance: Ec2Instance): DataQuickPickItem { return { @@ -47,6 +49,7 @@ export class Ec2Prompter { protected async getInstancesAsQuickPickItems(region: string): Promise[]> { return (await this.getInstancesFromRegion(region)) + .filter(this.filter ?? (i => true)) .map(instance => Ec2Prompter.asQuickPickItem(instance)) .promise() } diff --git a/src/test/ec2/prompter.test.ts b/src/test/ec2/prompter.test.ts index 0cd57a61a19..d6673bc4a97 100644 --- a/src/test/ec2/prompter.test.ts +++ b/src/test/ec2/prompter.test.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as assert from 'assert' -import { Ec2Prompter } from '../../ec2/prompter' +import { Ec2Prompter, instanceFilter } from '../../ec2/prompter' import { Ec2Instance } from '../../shared/clients/ec2Client' import { RegionSubmenuResponse } from '../../shared/ui/common/regionSubmenu' import { Ec2Selection } from '../../ec2/utils' @@ -29,6 +29,10 @@ describe('Ec2Prompter', async function () { protected override async getInstancesFromRegion(regionCode: string): Promise> { return intoCollection(this.instances) } + + public setFilter(filter: instanceFilter) { + this.filter = filter + } } it('initializes properly', function () { const prompter = new Ec2Prompter() @@ -104,26 +108,52 @@ describe('Ec2Prompter', async function () { }) beforeEach(function () { - prompter.instances = [] + prompter.instances = [ + { + InstanceId: '1', + name: 'first', + }, + { + InstanceId: '2', + name: 'second', + }, + { + InstanceId: '3', + name: 'third', + }, + ] }) it('returns empty when no instances present', async function () { + prompter.instances = [] const items = await prompter.testGetInstancesAsQuickPickItems('test-region') assert.ok(items.length === 0) }) it('returns items mapped to QuickPick items without filter', async function () { - prompter.instances = [ + const expected = [ + { + label: '$(terminal) \t' + prompter.instances[0].name!, + detail: prompter.instances[0].InstanceId!, + data: prompter.instances[0].InstanceId!, + }, { - InstanceId: 'test-id1', - name: 'test-name1', + label: '$(terminal) \t' + prompter.instances[1].name!, + detail: prompter.instances[1].InstanceId!, + data: prompter.instances[1].InstanceId!, }, { - InstanceId: 'test-id2', - name: 'test-name2', + label: '$(terminal) \t' + prompter.instances[2].name!, + detail: prompter.instances[2].InstanceId!, + data: prompter.instances[2].InstanceId!, }, ] + const items = await prompter.testGetInstancesAsQuickPickItems('test-region') + assert.deepStrictEqual(items, expected) + }) + + it('filters the resulting items when filter present', async function () { const expected = [ { label: '$(terminal) \t' + prompter.instances[0].name!, @@ -131,12 +161,15 @@ describe('Ec2Prompter', async function () { data: prompter.instances[0].InstanceId!, }, { - label: '$(terminal) \t' + prompter.instances[1].name!, - detail: prompter.instances[1].InstanceId!, - data: prompter.instances[1].InstanceId!, + label: '$(terminal) \t' + prompter.instances[2].name!, + detail: prompter.instances[2].InstanceId!, + data: prompter.instances[2].InstanceId!, }, ] + const onlyOddId = (instance: Ec2Instance) => parseInt(instance.InstanceId!) % 2 === 1 + prompter.setFilter(onlyOddId) + const items = await prompter.testGetInstancesAsQuickPickItems('test-region') assert.deepStrictEqual(items, expected) }) From 29d6b62b0d851f17e4f0e72002fdfdf8151fd825 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 13:23:31 -0700 Subject: [PATCH 039/172] remove duplication of determining if instance is running --- src/ec2/model.ts | 7 +------ src/ec2/utils.ts | 9 ++++----- src/shared/clients/ec2Client.ts | 5 +++++ 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index d32d0d9ea48..a5340c2f9b4 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -58,18 +58,13 @@ export class Ec2ConnectionManager { return requiredPolicies.length !== 0 && requiredPolicies.every(policy => attachedPolicies.includes(policy)) } - public async isInstanceRunning(instanceId: string): Promise { - const instanceStatus = await this.ec2Client.getInstanceStatus(instanceId) - return instanceStatus == 'running' - } - private throwConnectionError(message: string, selection: Ec2Selection, errorInfo: ErrorInformation) { const generalErrorMessage = `Unable to connect to target instance ${selection.instanceId} on region ${selection.region}. ` throw new ToolkitError(generalErrorMessage + message, errorInfo) } public async checkForStartSessionError(selection: Ec2Selection): Promise { - const isInstanceRunning = await this.isInstanceRunning(selection.instanceId) + const isInstanceRunning = await this.ec2Client.isInstanceRunning(selection.instanceId) const hasProperPolicies = await this.hasProperPolicies(selection.instanceId) const isSsmAgentRunning = (await this.ssmClient.getInstanceAgentPingStatus(selection.instanceId)) == 'Online' diff --git a/src/ec2/utils.ts b/src/ec2/utils.ts index 25b5c5de678..4c5f474b24a 100644 --- a/src/ec2/utils.ts +++ b/src/ec2/utils.ts @@ -3,15 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { AsyncCollection } from '../shared/utilities/asyncCollection' -import { Ec2Client, Ec2Instance } from '../shared/clients/ec2Client' +import { Ec2Client } from '../shared/clients/ec2Client' export interface Ec2Selection { instanceId: string region: string } -export async function getInstancesFromRegion(regionCode: string): Promise> { - const client = new Ec2Client(regionCode) - return await client.getInstances() +export async function isEc2SelectionRunning(selection: Ec2Selection): Promise { + const client = new Ec2Client(selection.region) + return await client.isInstanceRunning(selection.instanceId) } diff --git a/src/shared/clients/ec2Client.ts b/src/shared/clients/ec2Client.ts index fd4fe837cb3..99f1c438024 100644 --- a/src/shared/clients/ec2Client.ts +++ b/src/shared/clients/ec2Client.ts @@ -67,6 +67,11 @@ export class Ec2Client { return response[0] } + public async isInstanceRunning(instanceId: string): Promise { + const status = await this.getInstanceStatus(instanceId) + return status == 'running' + } + public getInstancesFilter(instanceIds: string[]): EC2.Filter[] { return [ { From 506be9a5609336df67d09ccf23e85c3a9b4c8e50 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 14:08:53 -0700 Subject: [PATCH 040/172] backtrack filter idea because it breaks pagination --- src/ec2/prompter.ts | 3 +-- src/test/ec2/prompter.test.ts | 29 ++--------------------------- 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/src/ec2/prompter.ts b/src/ec2/prompter.ts index e7b0fde1ea9..2052b7c305c 100644 --- a/src/ec2/prompter.ts +++ b/src/ec2/prompter.ts @@ -14,7 +14,7 @@ import { AsyncCollection } from '../shared/utilities/asyncCollection' export type instanceFilter = (instance: Ec2Instance) => boolean export class Ec2Prompter { - public constructor(protected filter?: instanceFilter) {} + public constructor() {} protected static asQuickPickItem(instance: Ec2Instance): DataQuickPickItem { return { @@ -49,7 +49,6 @@ export class Ec2Prompter { protected async getInstancesAsQuickPickItems(region: string): Promise[]> { return (await this.getInstancesFromRegion(region)) - .filter(this.filter ?? (i => true)) .map(instance => Ec2Prompter.asQuickPickItem(instance)) .promise() } diff --git a/src/test/ec2/prompter.test.ts b/src/test/ec2/prompter.test.ts index d6673bc4a97..e19eb397a8e 100644 --- a/src/test/ec2/prompter.test.ts +++ b/src/test/ec2/prompter.test.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as assert from 'assert' -import { Ec2Prompter, instanceFilter } from '../../ec2/prompter' +import { Ec2Prompter } from '../../ec2/prompter' import { Ec2Instance } from '../../shared/clients/ec2Client' import { RegionSubmenuResponse } from '../../shared/ui/common/regionSubmenu' import { Ec2Selection } from '../../ec2/utils' @@ -29,10 +29,6 @@ describe('Ec2Prompter', async function () { protected override async getInstancesFromRegion(regionCode: string): Promise> { return intoCollection(this.instances) } - - public setFilter(filter: instanceFilter) { - this.filter = filter - } } it('initializes properly', function () { const prompter = new Ec2Prompter() @@ -130,7 +126,7 @@ describe('Ec2Prompter', async function () { assert.ok(items.length === 0) }) - it('returns items mapped to QuickPick items without filter', async function () { + it('returns items mapped to QuickPick items', async function () { const expected = [ { label: '$(terminal) \t' + prompter.instances[0].name!, @@ -152,26 +148,5 @@ describe('Ec2Prompter', async function () { const items = await prompter.testGetInstancesAsQuickPickItems('test-region') assert.deepStrictEqual(items, expected) }) - - it('filters the resulting items when filter present', async function () { - const expected = [ - { - label: '$(terminal) \t' + prompter.instances[0].name!, - detail: prompter.instances[0].InstanceId!, - data: prompter.instances[0].InstanceId!, - }, - { - label: '$(terminal) \t' + prompter.instances[2].name!, - detail: prompter.instances[2].InstanceId!, - data: prompter.instances[2].InstanceId!, - }, - ] - - const onlyOddId = (instance: Ec2Instance) => parseInt(instance.InstanceId!) % 2 === 1 - prompter.setFilter(onlyOddId) - - const items = await prompter.testGetInstancesAsQuickPickItems('test-region') - assert.deepStrictEqual(items, expected) - }) }) }) From 2022004553b47412a430b28daee07b4bca309b90 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 14:45:41 -0700 Subject: [PATCH 041/172] implement start command --- src/ec2/commands.ts | 25 ++++++++++++++++++++++++- src/shared/clients/ec2Client.ts | 11 ++++++++++- src/test/ec2/model.test.ts | 16 ---------------- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/ec2/commands.ts b/src/ec2/commands.ts index 31e1878cc9d..f31c731d8a7 100644 --- a/src/ec2/commands.ts +++ b/src/ec2/commands.ts @@ -3,6 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { Ec2Client } from '../shared/clients/ec2Client' +import { ToolkitError, isAwsError } from '../shared/errors' +import { showMessageWithCancel } from '../shared/utilities/messages' +import { Timeout } from '../shared/utilities/timeoutUtils' import { Ec2InstanceNode } from './explorer/ec2InstanceNode' import { Ec2Node } from './explorer/ec2ParentNode' import { Ec2ConnectionManager } from './model' @@ -24,7 +28,26 @@ export async function openRemoteConnection(node?: Ec2Node) { export async function startInstance(node?: Ec2Node) { const selection = await getSelection(node) - console.log(selection) + const timeout = new Timeout(5000) + await showMessageWithCancel(`EC2: Starting instance ${selection.instanceId}`, timeout) + const client = new Ec2Client(selection.region) + const isAlreadyRunning = await client.isInstanceRunning(selection.instanceId) + + try { + if (isAlreadyRunning) { + throw new ToolkitError(`EC2: Instance already running. Attempted to start ${selection.instanceId}.`) + } + const response = await client.startInstance(selection.instanceId) + console.log(response) + } catch (err) { + if (isAwsError(err)) { + console.log(err) + } else { + throw err + } + } finally { + timeout.cancel() + } } async function getSelection(node: Ec2Node | undefined): Promise { diff --git a/src/shared/clients/ec2Client.ts b/src/shared/clients/ec2Client.ts index 99f1c438024..24fdae48ac3 100644 --- a/src/shared/clients/ec2Client.ts +++ b/src/shared/clients/ec2Client.ts @@ -3,11 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EC2 } from 'aws-sdk' +import { AWSError, EC2 } from 'aws-sdk' import { AsyncCollection } from '../utilities/asyncCollection' import { pageableToCollection } from '../utilities/collectionUtils' import { IamInstanceProfile } from 'aws-sdk/clients/ec2' import globals from '../extensionGlobals' +import { PromiseResult } from 'aws-sdk/lib/request' export interface Ec2Instance extends EC2.Instance { name?: string @@ -81,6 +82,14 @@ export class Ec2Client { ] } + public async startInstance(instanceId: string): Promise> { + const client = await this.createSdkClient() + + const response = await client.startInstances({ InstanceIds: [instanceId] }).promise() + + return response + } + /** * Retrieve IAM Association for a given EC2 instance. * @param instanceId target EC2 instance ID diff --git a/src/test/ec2/model.test.ts b/src/test/ec2/model.test.ts index cfc15f7ac22..90e0b54c006 100644 --- a/src/test/ec2/model.test.ts +++ b/src/test/ec2/model.test.ts @@ -47,22 +47,6 @@ describe('Ec2ConnectClient', function () { } } - describe('isInstanceRunning', async function () { - let client: MockEc2ConnectClient - - before(function () { - client = new MockEc2ConnectClient() - }) - - it('only returns true with the instance is running', async function () { - const actualFirstResult = await client.isInstanceRunning('running:noPolicies') - const actualSecondResult = await client.isInstanceRunning('stopped:noPolicies') - - assert.strictEqual(true, actualFirstResult) - assert.strictEqual(false, actualSecondResult) - }) - }) - describe('handleStartSessionError', async function () { let client: MockEc2ConnectClientForError From 8560da269e065e36ca093fe1b4197321a4f283d4 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 14:55:17 -0700 Subject: [PATCH 042/172] move bulk of work to new file --- src/ec2/changeInstanceStatus.ts | 37 +++++++++++++++++++++++++++++++++ src/ec2/commands.ts | 26 ++--------------------- 2 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 src/ec2/changeInstanceStatus.ts diff --git a/src/ec2/changeInstanceStatus.ts b/src/ec2/changeInstanceStatus.ts new file mode 100644 index 00000000000..b1595073694 --- /dev/null +++ b/src/ec2/changeInstanceStatus.ts @@ -0,0 +1,37 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Ec2Client } from '../shared/clients/ec2Client' +import { ToolkitError, isAwsError } from '../shared/errors' +import { showMessageWithCancel } from '../shared/utilities/messages' +import { Timeout } from '../shared/utilities/timeoutUtils' +import { Ec2Selection } from './utils' + +async function ensureInstanceStopped(client: Ec2Client, instanceId: string) { + const isAlreadyRunning = await client.isInstanceRunning(instanceId) + if (isAlreadyRunning) { + throw new ToolkitError(`EC2: Instance already running. Attempted to start ${instanceId}.`) + } +} + +export async function startInstanceWithCancel(selection: Ec2Selection): Promise { + const client = new Ec2Client(selection.region) + const timeout = new Timeout(5000) + + await showMessageWithCancel(`EC2: Starting instance ${selection.instanceId}`, timeout) + + try { + await ensureInstanceStopped(client, selection.instanceId) + await client.startInstance(selection.instanceId) + } catch (err) { + if (isAwsError(err)) { + throw new ToolkitError(`EC2: failed to start instance ${selection.instanceId}`, { cause: err as Error }) + } else { + throw err + } + } finally { + timeout.cancel() + } +} diff --git a/src/ec2/commands.ts b/src/ec2/commands.ts index f31c731d8a7..e3a8acd7472 100644 --- a/src/ec2/commands.ts +++ b/src/ec2/commands.ts @@ -3,10 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Ec2Client } from '../shared/clients/ec2Client' -import { ToolkitError, isAwsError } from '../shared/errors' -import { showMessageWithCancel } from '../shared/utilities/messages' -import { Timeout } from '../shared/utilities/timeoutUtils' +import { startInstanceWithCancel } from './changeInstanceStatus' import { Ec2InstanceNode } from './explorer/ec2InstanceNode' import { Ec2Node } from './explorer/ec2ParentNode' import { Ec2ConnectionManager } from './model' @@ -28,26 +25,7 @@ export async function openRemoteConnection(node?: Ec2Node) { export async function startInstance(node?: Ec2Node) { const selection = await getSelection(node) - const timeout = new Timeout(5000) - await showMessageWithCancel(`EC2: Starting instance ${selection.instanceId}`, timeout) - const client = new Ec2Client(selection.region) - const isAlreadyRunning = await client.isInstanceRunning(selection.instanceId) - - try { - if (isAlreadyRunning) { - throw new ToolkitError(`EC2: Instance already running. Attempted to start ${selection.instanceId}.`) - } - const response = await client.startInstance(selection.instanceId) - console.log(response) - } catch (err) { - if (isAwsError(err)) { - console.log(err) - } else { - throw err - } - } finally { - timeout.cancel() - } + await startInstanceWithCancel(selection) } async function getSelection(node: Ec2Node | undefined): Promise { From 9022ec22c43f652880fc59f600a3b604c663d1af Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 14:58:21 -0700 Subject: [PATCH 043/172] add command for stopping instance --- package.json | 14 ++++++++++++++ package.nls.json | 1 + 2 files changed, 15 insertions(+) diff --git a/package.json b/package.json index 71bfdf1cf83..372101920eb 100644 --- a/package.json +++ b/package.json @@ -1122,6 +1122,10 @@ "command": "aws.ec2.startInstance", "whem": "aws.isDevMode" }, + { + "command": "aws.ec2.stopInstance", + "whem": "aws.isDevMode" + }, { "command": "aws.dev.openMenu", "when": "aws.isDevMode || isCloud9" @@ -2099,6 +2103,16 @@ } } }, + { + "command": "aws.ec2.stopInstance", + "title": "%AWS.command.ec2.stopInstance%", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, { "command": "aws.ecr.copyTagUri", "title": "%AWS.command.ecr.copyTagUri%", diff --git a/package.nls.json b/package.nls.json index e39cf30bef8..f4cf382f029 100644 --- a/package.nls.json +++ b/package.nls.json @@ -111,6 +111,7 @@ "AWS.command.ec2.openTerminal": "Open terminal to EC2 instance...", "AWS.command.ec2.openRemoteConnection": "Connect to EC2 instance in New Window...", "AWS.command.ec2.startInstance": "Start EC2 Instance...", + "AWS.command.ec2.stopInstance": "Stop EC2 Instance...", "AWS.command.ecr.copyTagUri": "Copy Tag URI", "AWS.command.ecr.copyRepositoryUri": "Copy Repository URI", "AWS.command.ecr.createRepository": "Create Repository...", From fd1703d3e603c6245a3e83ed78417566a55910f4 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 15:08:44 -0700 Subject: [PATCH 044/172] add functionality to stop instances --- src/ec2/activation.ts | 6 +++++- src/ec2/changeInstanceStatus.ts | 28 ++++++++++++++++++++++++---- src/ec2/commands.ts | 7 ++++++- src/shared/clients/ec2Client.ts | 8 ++++++++ 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/ec2/activation.ts b/src/ec2/activation.ts index 45e64b4a709..47c1e1f16c5 100644 --- a/src/ec2/activation.ts +++ b/src/ec2/activation.ts @@ -6,7 +6,7 @@ import { ExtContext } from '../shared/extensions' import { Commands } from '../shared/vscode/commands2' import { telemetry } from '../shared/telemetry/telemetry' import { Ec2Node } from './explorer/ec2ParentNode' -import { openRemoteConnection, openTerminal, startInstance } from './commands' +import { openRemoteConnection, openTerminal, startInstance, stopInstance } from './commands' export async function activate(ctx: ExtContext): Promise { ctx.extensionContext.subscriptions.push( @@ -23,6 +23,10 @@ export async function activate(ctx: ExtContext): Promise { Commands.register('aws.ec2.startInstance', async (node?: Ec2Node) => { await (node ? startInstance(node) : startInstance(node)) + }), + + Commands.register('aws.ec2.stopInstance', async (node?: Ec2Node) => { + await (node ? stopInstance(node) : stopInstance(node)) }) ) } diff --git a/src/ec2/changeInstanceStatus.ts b/src/ec2/changeInstanceStatus.ts index b1595073694..712b98e6b42 100644 --- a/src/ec2/changeInstanceStatus.ts +++ b/src/ec2/changeInstanceStatus.ts @@ -9,10 +9,10 @@ import { showMessageWithCancel } from '../shared/utilities/messages' import { Timeout } from '../shared/utilities/timeoutUtils' import { Ec2Selection } from './utils' -async function ensureInstanceStopped(client: Ec2Client, instanceId: string) { - const isAlreadyRunning = await client.isInstanceRunning(instanceId) +async function ensureInstanceNotInStatus(client: Ec2Client, instanceId: string, targetStatus: string) { + const isAlreadyRunning = (await client.getInstanceStatus(instanceId)) == targetStatus if (isAlreadyRunning) { - throw new ToolkitError(`EC2: Instance already running. Attempted to start ${instanceId}.`) + throw new ToolkitError(`EC2: Instance already ${targetStatus}. Unable to update status of ${instanceId}.`) } } @@ -23,7 +23,7 @@ export async function startInstanceWithCancel(selection: Ec2Selection): Promise< await showMessageWithCancel(`EC2: Starting instance ${selection.instanceId}`, timeout) try { - await ensureInstanceStopped(client, selection.instanceId) + await ensureInstanceNotInStatus(client, selection.instanceId, 'running') await client.startInstance(selection.instanceId) } catch (err) { if (isAwsError(err)) { @@ -35,3 +35,23 @@ export async function startInstanceWithCancel(selection: Ec2Selection): Promise< timeout.cancel() } } + +export async function stopInstanceWithCancel(selection: Ec2Selection): Promise { + const client = new Ec2Client(selection.region) + const timeout = new Timeout(5000) + + await showMessageWithCancel(`EC2: Stopping instance ${selection.instanceId}`, timeout) + + try { + await ensureInstanceNotInStatus(client, selection.instanceId, 'stopped') + await client.stopInstance(selection.instanceId) + } catch (err) { + if (isAwsError(err)) { + throw new ToolkitError(`EC2: failed to stop instance ${selection.instanceId}`, { cause: err as Error }) + } else { + throw err + } + } finally { + timeout.cancel() + } +} diff --git a/src/ec2/commands.ts b/src/ec2/commands.ts index e3a8acd7472..fff648679c3 100644 --- a/src/ec2/commands.ts +++ b/src/ec2/commands.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { startInstanceWithCancel } from './changeInstanceStatus' +import { startInstanceWithCancel, stopInstanceWithCancel } from './changeInstanceStatus' import { Ec2InstanceNode } from './explorer/ec2InstanceNode' import { Ec2Node } from './explorer/ec2ParentNode' import { Ec2ConnectionManager } from './model' @@ -28,6 +28,11 @@ export async function startInstance(node?: Ec2Node) { await startInstanceWithCancel(selection) } +export async function stopInstance(node?: Ec2Node) { + const selection = await getSelection(node) + await stopInstanceWithCancel(selection) +} + async function getSelection(node: Ec2Node | undefined): Promise { const prompter = new Ec2Prompter() const selection = node && node instanceof Ec2InstanceNode ? node.toSelection() : await prompter.promptUser() diff --git a/src/shared/clients/ec2Client.ts b/src/shared/clients/ec2Client.ts index 24fdae48ac3..355c2982570 100644 --- a/src/shared/clients/ec2Client.ts +++ b/src/shared/clients/ec2Client.ts @@ -90,6 +90,14 @@ export class Ec2Client { return response } + public async stopInstance(instanceId: string): Promise> { + const client = await this.createSdkClient() + + const response = await client.stopInstances({ InstanceIds: [instanceId] }).promise() + + return response + } + /** * Retrieve IAM Association for a given EC2 instance. * @param instanceId target EC2 instance ID From 9e0c16e68954132dc0cd40e94130247db294af45 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 15:36:41 -0700 Subject: [PATCH 045/172] refactor start/stop commands into a class --- src/ec2/changeInstanceStatus.ts | 57 --------------------------- src/ec2/commands.ts | 8 ++-- src/ec2/instanceStateManager.ts | 68 +++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 60 deletions(-) delete mode 100644 src/ec2/changeInstanceStatus.ts create mode 100644 src/ec2/instanceStateManager.ts diff --git a/src/ec2/changeInstanceStatus.ts b/src/ec2/changeInstanceStatus.ts deleted file mode 100644 index 712b98e6b42..00000000000 --- a/src/ec2/changeInstanceStatus.ts +++ /dev/null @@ -1,57 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { Ec2Client } from '../shared/clients/ec2Client' -import { ToolkitError, isAwsError } from '../shared/errors' -import { showMessageWithCancel } from '../shared/utilities/messages' -import { Timeout } from '../shared/utilities/timeoutUtils' -import { Ec2Selection } from './utils' - -async function ensureInstanceNotInStatus(client: Ec2Client, instanceId: string, targetStatus: string) { - const isAlreadyRunning = (await client.getInstanceStatus(instanceId)) == targetStatus - if (isAlreadyRunning) { - throw new ToolkitError(`EC2: Instance already ${targetStatus}. Unable to update status of ${instanceId}.`) - } -} - -export async function startInstanceWithCancel(selection: Ec2Selection): Promise { - const client = new Ec2Client(selection.region) - const timeout = new Timeout(5000) - - await showMessageWithCancel(`EC2: Starting instance ${selection.instanceId}`, timeout) - - try { - await ensureInstanceNotInStatus(client, selection.instanceId, 'running') - await client.startInstance(selection.instanceId) - } catch (err) { - if (isAwsError(err)) { - throw new ToolkitError(`EC2: failed to start instance ${selection.instanceId}`, { cause: err as Error }) - } else { - throw err - } - } finally { - timeout.cancel() - } -} - -export async function stopInstanceWithCancel(selection: Ec2Selection): Promise { - const client = new Ec2Client(selection.region) - const timeout = new Timeout(5000) - - await showMessageWithCancel(`EC2: Stopping instance ${selection.instanceId}`, timeout) - - try { - await ensureInstanceNotInStatus(client, selection.instanceId, 'stopped') - await client.stopInstance(selection.instanceId) - } catch (err) { - if (isAwsError(err)) { - throw new ToolkitError(`EC2: failed to stop instance ${selection.instanceId}`, { cause: err as Error }) - } else { - throw err - } - } finally { - timeout.cancel() - } -} diff --git a/src/ec2/commands.ts b/src/ec2/commands.ts index fff648679c3..b057b8336a1 100644 --- a/src/ec2/commands.ts +++ b/src/ec2/commands.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { startInstanceWithCancel, stopInstanceWithCancel } from './changeInstanceStatus' +import { InstanceStateManager } from './instanceStateManager' import { Ec2InstanceNode } from './explorer/ec2InstanceNode' import { Ec2Node } from './explorer/ec2ParentNode' import { Ec2ConnectionManager } from './model' @@ -25,12 +25,14 @@ export async function openRemoteConnection(node?: Ec2Node) { export async function startInstance(node?: Ec2Node) { const selection = await getSelection(node) - await startInstanceWithCancel(selection) + const stateManager = new InstanceStateManager(selection.instanceId, selection.region) + await stateManager.startInstanceWithCancel() } export async function stopInstance(node?: Ec2Node) { const selection = await getSelection(node) - await stopInstanceWithCancel(selection) + const stateManager = new InstanceStateManager(selection.instanceId, selection.region) + await stateManager.stopInstanceWithCancel() } async function getSelection(node: Ec2Node | undefined): Promise { diff --git a/src/ec2/instanceStateManager.ts b/src/ec2/instanceStateManager.ts new file mode 100644 index 00000000000..b8cb0b98473 --- /dev/null +++ b/src/ec2/instanceStateManager.ts @@ -0,0 +1,68 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Ec2Client } from '../shared/clients/ec2Client' +import { ToolkitError, isAwsError } from '../shared/errors' +import { showMessageWithCancel } from '../shared/utilities/messages' +import { Timeout } from '../shared/utilities/timeoutUtils' + +export class InstanceStateManager { + private readonly client: Ec2Client + + public constructor(private readonly instanceId: string, private readonly regionCode: string) { + this.client = this.getEc2Client() + } + + protected getEc2Client() { + return new Ec2Client(this.regionCode) + } + + private async ensureInstanceNotInStatus(targetStatus: string) { + const isAlreadyRunning = (await this.client.getInstanceStatus(this.instanceId)) == targetStatus + if (isAlreadyRunning) { + throw new ToolkitError( + `EC2: Instance already ${targetStatus}. Unable to update status of ${this.instanceId}.` + ) + } + } + + public async startInstanceWithCancel(): Promise { + const timeout = new Timeout(5000) + + await showMessageWithCancel(`EC2: Starting instance ${this.instanceId}`, timeout) + + try { + await this.ensureInstanceNotInStatus('running') + await this.client.startInstance(this.instanceId) + } catch (err) { + if (isAwsError(err)) { + throw new ToolkitError(`EC2: failed to start instance ${this.instanceId}`, { cause: err as Error }) + } else { + throw err + } + } finally { + timeout.cancel() + } + } + + public async stopInstanceWithCancel(): Promise { + const timeout = new Timeout(5000) + + await showMessageWithCancel(`EC2: Stopping instance ${this.instanceId}`, timeout) + + try { + await this.ensureInstanceNotInStatus('stopped') + await this.client.stopInstance(this.instanceId) + } catch (err) { + if (isAwsError(err)) { + throw new ToolkitError(`EC2: failed to stop instance ${this.instanceId}`, { cause: err as Error }) + } else { + throw err + } + } finally { + timeout.cancel() + } + } +} From 1f53a3f7400bcc8e4a777f6f172a4dd25c1f3dcc Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 15:50:48 -0700 Subject: [PATCH 046/172] add testing for checking instance status --- src/ec2/instanceStateManager.ts | 8 ++--- src/test/ec2/instanceStateManager.test.ts | 43 +++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 src/test/ec2/instanceStateManager.test.ts diff --git a/src/ec2/instanceStateManager.ts b/src/ec2/instanceStateManager.ts index b8cb0b98473..520868d1981 100644 --- a/src/ec2/instanceStateManager.ts +++ b/src/ec2/instanceStateManager.ts @@ -11,7 +11,7 @@ import { Timeout } from '../shared/utilities/timeoutUtils' export class InstanceStateManager { private readonly client: Ec2Client - public constructor(private readonly instanceId: string, private readonly regionCode: string) { + public constructor(protected readonly instanceId: string, protected readonly regionCode: string) { this.client = this.getEc2Client() } @@ -19,9 +19,9 @@ export class InstanceStateManager { return new Ec2Client(this.regionCode) } - private async ensureInstanceNotInStatus(targetStatus: string) { - const isAlreadyRunning = (await this.client.getInstanceStatus(this.instanceId)) == targetStatus - if (isAlreadyRunning) { + protected async ensureInstanceNotInStatus(targetStatus: string) { + const isAlreadyInStatus = (await this.client.getInstanceStatus(this.instanceId)) == targetStatus + if (isAlreadyInStatus) { throw new ToolkitError( `EC2: Instance already ${targetStatus}. Unable to update status of ${this.instanceId}.` ) diff --git a/src/test/ec2/instanceStateManager.test.ts b/src/test/ec2/instanceStateManager.test.ts new file mode 100644 index 00000000000..c59fc230c0f --- /dev/null +++ b/src/test/ec2/instanceStateManager.test.ts @@ -0,0 +1,43 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import { InstanceStateManager } from '../../ec2/instanceStateManager' +import { Ec2Client } from '../../shared/clients/ec2Client' + +describe('InstanceStateManager', async function () { + class MockEc2Client extends Ec2Client { + public constructor() { + super('test-region') + } + + public override async getInstanceStatus(instanceId: string): Promise { + return instanceId.split(':')[0] + } + } + + class MockInstanceStateManager extends InstanceStateManager { + protected override getEc2Client(): Ec2Client { + return new MockEc2Client() + } + + public async testEnsureInstanceNotInStatus(targetStatus: string) { + await this.ensureInstanceNotInStatus(targetStatus) + } + } + + describe('ensureInstanceNotInStatus', async function () { + it('only throws error if instance is in status', async function () { + const stateManager = new MockInstanceStateManager('running:instance', 'test-region') + + await stateManager.testEnsureInstanceNotInStatus('stopped') + + try { + await stateManager.testEnsureInstanceNotInStatus('running') + assert.ok(false) + } catch {} + }) + }) +}) From eb926092505a3610b327ce85a375dde99fc4b9f8 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 15:54:36 -0700 Subject: [PATCH 047/172] add reboot command --- package.json | 14 ++++++++++++++ package.nls.json | 1 + 2 files changed, 15 insertions(+) diff --git a/package.json b/package.json index 372101920eb..21202f6d084 100644 --- a/package.json +++ b/package.json @@ -1126,6 +1126,10 @@ "command": "aws.ec2.stopInstance", "whem": "aws.isDevMode" }, + { + "command": "aws.ec2.rebootInstance", + "whem": "aws.isDevMode" + }, { "command": "aws.dev.openMenu", "when": "aws.isDevMode || isCloud9" @@ -2113,6 +2117,16 @@ } } }, + { + "command": "aws.ec2.rebootInstance", + "title": "%AWS.command.ec2.rebootInstance%", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, { "command": "aws.ecr.copyTagUri", "title": "%AWS.command.ecr.copyTagUri%", diff --git a/package.nls.json b/package.nls.json index f4cf382f029..a6526de229f 100644 --- a/package.nls.json +++ b/package.nls.json @@ -112,6 +112,7 @@ "AWS.command.ec2.openRemoteConnection": "Connect to EC2 instance in New Window...", "AWS.command.ec2.startInstance": "Start EC2 Instance...", "AWS.command.ec2.stopInstance": "Stop EC2 Instance...", + "AWS.command.ec2.rebootInstance": "Reboot EC2 Instance...", "AWS.command.ecr.copyTagUri": "Copy Tag URI", "AWS.command.ecr.copyRepositoryUri": "Copy Repository URI", "AWS.command.ecr.createRepository": "Create Repository...", From cec3ede6b6dc4ec0482f1b407d80da26808a9d41 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 16:01:30 -0700 Subject: [PATCH 048/172] implement reboot extension --- src/ec2/activation.ts | 6 +++++- src/ec2/commands.ts | 6 ++++++ src/ec2/instanceStateManager.ts | 20 +++++++++++++++++++- src/shared/clients/ec2Client.ts | 6 ++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/ec2/activation.ts b/src/ec2/activation.ts index 47c1e1f16c5..295a1402391 100644 --- a/src/ec2/activation.ts +++ b/src/ec2/activation.ts @@ -6,7 +6,7 @@ import { ExtContext } from '../shared/extensions' import { Commands } from '../shared/vscode/commands2' import { telemetry } from '../shared/telemetry/telemetry' import { Ec2Node } from './explorer/ec2ParentNode' -import { openRemoteConnection, openTerminal, startInstance, stopInstance } from './commands' +import { openRemoteConnection, openTerminal, rebootInstance, startInstance, stopInstance } from './commands' export async function activate(ctx: ExtContext): Promise { ctx.extensionContext.subscriptions.push( @@ -27,6 +27,10 @@ export async function activate(ctx: ExtContext): Promise { Commands.register('aws.ec2.stopInstance', async (node?: Ec2Node) => { await (node ? stopInstance(node) : stopInstance(node)) + }), + + Commands.register('aws.ec2.rebootInstance', async (node?: Ec2Node) => { + await (node ? rebootInstance(node) : rebootInstance(node)) }) ) } diff --git a/src/ec2/commands.ts b/src/ec2/commands.ts index b057b8336a1..1e176248121 100644 --- a/src/ec2/commands.ts +++ b/src/ec2/commands.ts @@ -35,6 +35,12 @@ export async function stopInstance(node?: Ec2Node) { await stateManager.stopInstanceWithCancel() } +export async function rebootInstance(node?: Ec2Node) { + const selection = await getSelection(node) + const stateManager = new InstanceStateManager(selection.instanceId, selection.region) + await stateManager.rebootInstanceWithCancel() +} + async function getSelection(node: Ec2Node | undefined): Promise { const prompter = new Ec2Prompter() const selection = node && node instanceof Ec2InstanceNode ? node.toSelection() : await prompter.promptUser() diff --git a/src/ec2/instanceStateManager.ts b/src/ec2/instanceStateManager.ts index 520868d1981..b5dafbf37b6 100644 --- a/src/ec2/instanceStateManager.ts +++ b/src/ec2/instanceStateManager.ts @@ -23,7 +23,7 @@ export class InstanceStateManager { const isAlreadyInStatus = (await this.client.getInstanceStatus(this.instanceId)) == targetStatus if (isAlreadyInStatus) { throw new ToolkitError( - `EC2: Instance already ${targetStatus}. Unable to update status of ${this.instanceId}.` + `EC2: Instance is currently ${targetStatus}. Unable to update status of ${this.instanceId}.` ) } } @@ -65,4 +65,22 @@ export class InstanceStateManager { timeout.cancel() } } + + public async rebootInstanceWithCancel(): Promise { + const timeout = new Timeout(5000) + + await showMessageWithCancel(`EC2: Rebooting instance ${this.instanceId}`, timeout) + + try { + await this.client.rebootInstance(this.instanceId) + } catch (err) { + if (isAwsError(err)) { + throw new ToolkitError(`EC2: failed to reboot instance ${this.instanceId}`, { cause: err as Error }) + } else { + throw err + } + } finally { + timeout.cancel() + } + } } diff --git a/src/shared/clients/ec2Client.ts b/src/shared/clients/ec2Client.ts index 355c2982570..ae49bb9a4d7 100644 --- a/src/shared/clients/ec2Client.ts +++ b/src/shared/clients/ec2Client.ts @@ -98,6 +98,12 @@ export class Ec2Client { return response } + public async rebootInstance(instanceId: string): Promise { + const client = await this.createSdkClient() + + await client.rebootInstances({ InstanceIds: [instanceId] }).promise() + } + /** * Retrieve IAM Association for a given EC2 instance. * @param instanceId target EC2 instance ID From 024eb7b202fe3ce2e3b5130433e81cd6bea229ee Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 16:09:58 -0700 Subject: [PATCH 049/172] refactor to remove duplicate code --- src/ec2/instanceStateManager.ts | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/ec2/instanceStateManager.ts b/src/ec2/instanceStateManager.ts index b5dafbf37b6..4ab1e0264bb 100644 --- a/src/ec2/instanceStateManager.ts +++ b/src/ec2/instanceStateManager.ts @@ -28,6 +28,16 @@ export class InstanceStateManager { } } + private async handleError(err: unknown) { + if (isAwsError(err)) { + throw new ToolkitError(`EC2: failed to change status of instance ${this.instanceId}`, { + cause: err as Error, + }) + } else { + throw err + } + } + public async startInstanceWithCancel(): Promise { const timeout = new Timeout(5000) @@ -37,11 +47,7 @@ export class InstanceStateManager { await this.ensureInstanceNotInStatus('running') await this.client.startInstance(this.instanceId) } catch (err) { - if (isAwsError(err)) { - throw new ToolkitError(`EC2: failed to start instance ${this.instanceId}`, { cause: err as Error }) - } else { - throw err - } + this.handleError(err) } finally { timeout.cancel() } @@ -56,11 +62,7 @@ export class InstanceStateManager { await this.ensureInstanceNotInStatus('stopped') await this.client.stopInstance(this.instanceId) } catch (err) { - if (isAwsError(err)) { - throw new ToolkitError(`EC2: failed to stop instance ${this.instanceId}`, { cause: err as Error }) - } else { - throw err - } + this.handleError(err) } finally { timeout.cancel() } @@ -74,11 +76,7 @@ export class InstanceStateManager { try { await this.client.rebootInstance(this.instanceId) } catch (err) { - if (isAwsError(err)) { - throw new ToolkitError(`EC2: failed to reboot instance ${this.instanceId}`, { cause: err as Error }) - } else { - throw err - } + this.handleError(err) } finally { timeout.cancel() } From 38559beb0f731ad065fecfa4e8c82a4e192ea462 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 16:14:51 -0700 Subject: [PATCH 050/172] remove unnecessary async --- src/ec2/instanceStateManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ec2/instanceStateManager.ts b/src/ec2/instanceStateManager.ts index 4ab1e0264bb..73c0a5da53b 100644 --- a/src/ec2/instanceStateManager.ts +++ b/src/ec2/instanceStateManager.ts @@ -28,7 +28,7 @@ export class InstanceStateManager { } } - private async handleError(err: unknown) { + private handleError(err: unknown) { if (isAwsError(err)) { throw new ToolkitError(`EC2: failed to change status of instance ${this.instanceId}`, { cause: err as Error, From b217377fb6314f4ef121171b97f747e62172a765 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 13 Jul 2023 16:16:46 -0700 Subject: [PATCH 051/172] expose command on explorer --- package.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/package.json b/package.json index 21202f6d084..1e666191b45 100644 --- a/package.json +++ b/package.json @@ -1334,6 +1334,21 @@ "group": "inline@1", "when": "viewItem == awsEc2Node" }, + { + "command": "aws.ec2.startInstance", + "group": "0@1", + "when": "viewItem == awsEc2Node" + }, + { + "command": "aws.ec2.stopInstance", + "group": "0@1", + "when": "viewItem == awsEc2Node" + }, + { + "command": "aws.ec2.rebootInstance", + "group": "0@1", + "when": "viewItem == awsEc2Node" + }, { "command": "aws.ecr.createRepository", "when": "view == aws.explorer && viewItem == awsEcrNode", From 19ad7947a6b4f05eb2cfb8f5ead11e8da7da1775 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 14 Jul 2023 10:20:30 -0700 Subject: [PATCH 052/172] refactor to eliminate some duplicate implementation --- src/ec2/commands.ts | 17 ++++++++++------- src/ec2/instanceStateManager.ts | 5 +++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/ec2/commands.ts b/src/ec2/commands.ts index 1e176248121..32f19cdf138 100644 --- a/src/ec2/commands.ts +++ b/src/ec2/commands.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { InstanceStateManager } from './instanceStateManager' +import { InstanceStateManager, getStateManagerForSelection } from './instanceStateManager' import { Ec2InstanceNode } from './explorer/ec2InstanceNode' import { Ec2Node } from './explorer/ec2ParentNode' import { Ec2ConnectionManager } from './model' @@ -24,23 +24,26 @@ export async function openRemoteConnection(node?: Ec2Node) { } export async function startInstance(node?: Ec2Node) { - const selection = await getSelection(node) - const stateManager = new InstanceStateManager(selection.instanceId, selection.region) + const stateManager = await getStateManager(node) await stateManager.startInstanceWithCancel() } export async function stopInstance(node?: Ec2Node) { - const selection = await getSelection(node) - const stateManager = new InstanceStateManager(selection.instanceId, selection.region) + const stateManager = await getStateManager(node) await stateManager.stopInstanceWithCancel() } export async function rebootInstance(node?: Ec2Node) { - const selection = await getSelection(node) - const stateManager = new InstanceStateManager(selection.instanceId, selection.region) + const stateManager = await getStateManager(node) await stateManager.rebootInstanceWithCancel() } +async function getStateManager(node: Ec2Node | undefined): Promise { + const selection = await getSelection(node) + const stateManager = getStateManagerForSelection(selection) + return stateManager +} + async function getSelection(node: Ec2Node | undefined): Promise { const prompter = new Ec2Prompter() const selection = node && node instanceof Ec2InstanceNode ? node.toSelection() : await prompter.promptUser() diff --git a/src/ec2/instanceStateManager.ts b/src/ec2/instanceStateManager.ts index 73c0a5da53b..317fa56df71 100644 --- a/src/ec2/instanceStateManager.ts +++ b/src/ec2/instanceStateManager.ts @@ -7,6 +7,7 @@ import { Ec2Client } from '../shared/clients/ec2Client' import { ToolkitError, isAwsError } from '../shared/errors' import { showMessageWithCancel } from '../shared/utilities/messages' import { Timeout } from '../shared/utilities/timeoutUtils' +import { Ec2Selection } from './utils' export class InstanceStateManager { private readonly client: Ec2Client @@ -82,3 +83,7 @@ export class InstanceStateManager { } } } + +export function getStateManagerForSelection(selection: Ec2Selection) { + return new InstanceStateManager(selection.instanceId, selection.region) +} From 7794b67fdc71a88bc67633a5acd8285557d7a0a2 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 14 Jul 2023 10:23:57 -0700 Subject: [PATCH 053/172] handle undefined node consistently --- src/ec2/activation.ts | 10 +++++----- src/ec2/commands.ts | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ec2/activation.ts b/src/ec2/activation.ts index 295a1402391..4eea620e863 100644 --- a/src/ec2/activation.ts +++ b/src/ec2/activation.ts @@ -13,24 +13,24 @@ export async function activate(ctx: ExtContext): Promise { Commands.register('aws.ec2.openTerminal', async (node?: Ec2Node) => { await telemetry.ec2_connectToInstance.run(async span => { span.record({ ec2ConnectionType: 'ssm' }) - await (node ? openTerminal(node) : openTerminal(node)) + await openTerminal(node) }) }), Commands.register('aws.ec2.openRemoteConnection', async (node?: Ec2Node) => { - await (node ? openRemoteConnection(node) : openRemoteConnection(node)) + await openRemoteConnection(node) }), Commands.register('aws.ec2.startInstance', async (node?: Ec2Node) => { - await (node ? startInstance(node) : startInstance(node)) + await startInstance(node) }), Commands.register('aws.ec2.stopInstance', async (node?: Ec2Node) => { - await (node ? stopInstance(node) : stopInstance(node)) + await stopInstance(node) }), Commands.register('aws.ec2.rebootInstance', async (node?: Ec2Node) => { - await (node ? rebootInstance(node) : rebootInstance(node)) + await rebootInstance(node) }) ) } diff --git a/src/ec2/commands.ts b/src/ec2/commands.ts index 32f19cdf138..0756945805e 100644 --- a/src/ec2/commands.ts +++ b/src/ec2/commands.ts @@ -38,13 +38,13 @@ export async function rebootInstance(node?: Ec2Node) { await stateManager.rebootInstanceWithCancel() } -async function getStateManager(node: Ec2Node | undefined): Promise { +async function getStateManager(node?: Ec2Node): Promise { const selection = await getSelection(node) const stateManager = getStateManagerForSelection(selection) return stateManager } -async function getSelection(node: Ec2Node | undefined): Promise { +async function getSelection(node?: Ec2Node): Promise { const prompter = new Ec2Prompter() const selection = node && node instanceof Ec2InstanceNode ? node.toSelection() : await prompter.promptUser() return selection From adae37b514a1e16eec45fdce160da8844d5b884b Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 14 Jul 2023 11:01:05 -0700 Subject: [PATCH 054/172] add method to append status to instance retriever --- src/shared/clients/ec2Client.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/shared/clients/ec2Client.ts b/src/shared/clients/ec2Client.ts index ae49bb9a4d7..583abc21eb1 100644 --- a/src/shared/clients/ec2Client.ts +++ b/src/shared/clients/ec2Client.ts @@ -35,6 +35,14 @@ export class Ec2Client { return instances } + public async getInstancesWithStatus(filter?: EC2.Filter[]) { + const instances = await this.getInstances(filter) + + return instances.map(async instance => { + return { ...instance, status: await this.getInstanceStatus(instance.InstanceId!) } + }) + } + public getInstancesFromReservations( reservations: AsyncCollection ): AsyncCollection { From d3a4276fcf57236388e8adca31b9d9c951ff50e8 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 14 Jul 2023 11:09:04 -0700 Subject: [PATCH 055/172] refactor how we add fields to the ec2 instances --- src/shared/clients/ec2Client.ts | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/shared/clients/ec2Client.ts b/src/shared/clients/ec2Client.ts index 583abc21eb1..65ffd8ac30f 100644 --- a/src/shared/clients/ec2Client.ts +++ b/src/shared/clients/ec2Client.ts @@ -12,6 +12,7 @@ import { PromiseResult } from 'aws-sdk/lib/request' export interface Ec2Instance extends EC2.Instance { name?: string + status?: EC2.InstanceStateName } export class Ec2Client { @@ -31,18 +32,29 @@ export class Ec2Client { 'NextToken', 'Reservations' ) - const instances = this.getInstancesFromReservations(collection) - return instances - } + const extractedInstances = this.getInstancesFromReservations(collection) + const instancesWithStatuses = await this.addStatusesToInstances(extractedInstances) + const instancesWithNames = await this.addNamesToInstances(instancesWithStatuses) - public async getInstancesWithStatus(filter?: EC2.Filter[]) { - const instances = await this.getInstances(filter) + return instancesWithNames + } + protected async addStatusesToInstances( + instances: AsyncCollection + ): Promise> { return instances.map(async instance => { return { ...instance, status: await this.getInstanceStatus(instance.InstanceId!) } }) } + protected async addNamesToInstances( + instances: AsyncCollection + ): Promise> { + return instances.map(instance => { + return instanceHasName(instance!) ? { ...instance, name: lookupTagKey(instance!.Tags!, 'Name') } : instance! + }) + } + public getInstancesFromReservations( reservations: AsyncCollection ): AsyncCollection { @@ -51,11 +63,6 @@ export class Ec2Client { .map(instanceList => instanceList?.Instances) .flatten() .filter(instance => instance!.InstanceId !== undefined) - .map(instance => { - return instanceHasName(instance!) - ? { ...instance, name: lookupTagKey(instance!.Tags!, 'Name') } - : instance! - }) } public async getInstanceStatus(instanceId: string): Promise { From 0772dd2f5fa21909ab12d371727efdab2ecd22f4 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 14 Jul 2023 11:30:16 -0700 Subject: [PATCH 056/172] add tests for new helper functions in Ec2Client --- .../shared/clients/defaultEc2Client.test.ts | 99 +++++++++++++++++-- 1 file changed, 92 insertions(+), 7 deletions(-) diff --git a/src/test/shared/clients/defaultEc2Client.test.ts b/src/test/shared/clients/defaultEc2Client.test.ts index 4eeb027b15b..54259832859 100644 --- a/src/test/shared/clients/defaultEc2Client.test.ts +++ b/src/test/shared/clients/defaultEc2Client.test.ts @@ -10,6 +10,20 @@ import { intoCollection } from '../../../shared/utilities/collectionUtils' import { Ec2Client, instanceHasName } from '../../../shared/clients/ec2Client' import { EC2 } from 'aws-sdk' +class MockEc2Client extends Ec2Client { + public override async getInstanceStatus(instanceId: string): Promise { + return instanceId.split('-')[0] + } + + public async testAddNamesToInstances(instances: EC2.Instance[]) { + return await (await this.addNamesToInstances(intoCollection(instances))).promise() + } + + public async testAddStatusesToInstances(instances: EC2.Instance[]) { + return await (await this.addStatusesToInstances(intoCollection(instances))).promise() + } +} + describe('extractInstancesFromReservations', function () { const client = new Ec2Client('') it('returns empty when given empty collection', async function () { @@ -54,10 +68,10 @@ describe('extractInstancesFromReservations', function () { const actualResult = await client.getInstancesFromReservations(intoCollection([testReservationsList])).promise() assert.deepStrictEqual( [ - { InstanceId: 'id1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, - { InstanceId: 'id2', name: 'name2', Tags: [{ Key: 'Name', Value: 'name2' }] }, - { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, - { InstanceId: 'id4', name: 'name4', Tags: [{ Key: 'Name', Value: 'name4' }] }, + { InstanceId: 'id1', Tags: [{ Key: 'Name', Value: 'name1' }] }, + { InstanceId: 'id2', Tags: [{ Key: 'Name', Value: 'name2' }] }, + { InstanceId: 'id3', Tags: [{ Key: 'Name', Value: 'name3' }] }, + { InstanceId: 'id4', Tags: [{ Key: 'Name', Value: 'name4' }] }, ], actualResult ) @@ -89,7 +103,7 @@ describe('extractInstancesFromReservations', function () { .getInstancesFromReservations(intoCollection([testReservationsList])) .promise() assert.deepStrictEqual( - [{ InstanceId: 'id1' }, { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }], + [{ InstanceId: 'id1' }, { InstanceId: 'id3', Tags: [{ Key: 'Name', Value: 'name3' }] }], actualResult ) }) @@ -125,9 +139,9 @@ describe('extractInstancesFromReservations', function () { assert.deepStrictEqual( [ - { InstanceId: 'id1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, + { InstanceId: 'id1', Tags: [{ Key: 'Name', Value: 'name1' }] }, { InstanceId: 'id2' }, - { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, + { InstanceId: 'id3', Tags: [{ Key: 'Name', Value: 'name3' }] }, { InstanceId: 'id4', Tags: [] }, ], actualResult @@ -135,6 +149,77 @@ describe('extractInstancesFromReservations', function () { }) }) +describe('addStatusesToInstances', async function () { + let client: MockEc2Client + + before(function () { + client = new MockEc2Client('test-region') + }) + + it('adds appropriate status field to the instance', async function () { + const testInstances = [ + { InstanceId: 'running-1', Tags: [{ Key: 'Name', Value: 'name1' }] }, + { InstanceId: 'stopped-2' }, + { InstanceId: 'pending-3', Tags: [{ Key: 'Name', Value: 'name3' }] }, + { InstanceId: 'running-4', Tags: [] }, + ] + + const actualResult = await client.testAddStatusesToInstances(testInstances) + const expectedResult = [ + { InstanceId: 'running-1', status: 'running', Tags: [{ Key: 'Name', Value: 'name1' }] }, + { InstanceId: 'stopped-2', status: 'stopped' }, + { InstanceId: 'pending-3', status: 'pending', Tags: [{ Key: 'Name', Value: 'name3' }] }, + { InstanceId: 'running-4', status: 'running', Tags: [] }, + ] + + assert.deepStrictEqual(actualResult, expectedResult) + }) +}) + +describe('addNamesToInstances', async function () { + let client: MockEc2Client + + before(function () { + client = new MockEc2Client('test-region') + }) + + it('adds corresponding name to instance', async function () { + const testInstances = [ + { InstanceId: 'running-1', Tags: [{ Key: 'Name', Value: 'name1' }] }, + { InstanceId: 'pending-3', Tags: [{ Key: 'Name', Value: 'name3' }] }, + ] + + const actualResult = await client.testAddNamesToInstances(testInstances) + + const expectedResult = [ + { InstanceId: 'running-1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, + { InstanceId: 'pending-3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, + ] + + assert.deepStrictEqual(actualResult, expectedResult) + }) + + it('handles incomplete and missing tag fields', async function () { + const testInstances = [ + { InstanceId: 'running-1', Tags: [{ Key: 'Name', Value: 'name1' }] }, + { InstanceId: 'stopped-2' }, + { InstanceId: 'pending-3', Tags: [{ Key: 'Name', Value: 'name3' }] }, + { InstanceId: 'running-4', Tags: [] }, + ] + + const actualResult = await client.testAddNamesToInstances(testInstances) + + const expectedResult = [ + { InstanceId: 'running-1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, + { InstanceId: 'stopped-2' }, + { InstanceId: 'pending-3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, + { InstanceId: 'running-4', Tags: [] }, + ] + + assert.deepStrictEqual(actualResult, expectedResult) + }) +}) + describe('getInstancesFilter', function () { const client = new Ec2Client('') From b4c03c3d4d1cc215d858b00fd0f759a85dcaea4c Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 14 Jul 2023 11:47:11 -0700 Subject: [PATCH 057/172] refactor tests to use the same test-data --- .../shared/clients/defaultEc2Client.test.ts | 209 +++++++----------- 1 file changed, 79 insertions(+), 130 deletions(-) diff --git a/src/test/shared/clients/defaultEc2Client.test.ts b/src/test/shared/clients/defaultEc2Client.test.ts index 54259832859..32f96dafdf7 100644 --- a/src/test/shared/clients/defaultEc2Client.test.ts +++ b/src/test/shared/clients/defaultEc2Client.test.ts @@ -24,8 +24,72 @@ class MockEc2Client extends Ec2Client { } } +const completeReservationsList: EC2.ReservationList = [ + { + Instances: [ + { + InstanceId: 'running-1', + Tags: [{ Key: 'Name', Value: 'name1' }], + }, + { + InstanceId: 'stopped-2', + Tags: [{ Key: 'Name', Value: 'name2' }], + }, + ], + }, + { + Instances: [ + { + InstanceId: 'pending-3', + Tags: [{ Key: 'Name', Value: 'name3' }], + }, + { + InstanceId: 'running-4', + Tags: [{ Key: 'Name', Value: 'name4' }], + }, + ], + }, +] + +const completeInstanceList: EC2.InstanceList = [ + { InstanceId: 'running-1', Tags: [{ Key: 'Name', Value: 'name1' }] }, + { InstanceId: 'stopped-2', Tags: [{ Key: 'Name', Value: 'name2' }] }, + { InstanceId: 'pending-3', Tags: [{ Key: 'Name', Value: 'name3' }] }, + { InstanceId: 'running-4', Tags: [{ Key: 'Name', Value: 'name4' }] }, +] + +const incompleteReservationsList: EC2.ReservationList = [ + { + Instances: [ + { + InstanceId: 'running-1', + }, + { + InstanceId: 'stopped-2', + Tags: [], + }, + ], + }, + { + Instances: [ + { + InstanceId: 'pending-3', + Tags: [{ Key: 'Name', Value: 'name3' }], + }, + {}, + ], + }, +] + +const incomepleteInstanceList: EC2.InstanceList = [ + { InstanceId: 'running-1' }, + { InstanceId: 'stopped-2', Tags: [] }, + { InstanceId: 'pending-3', Tags: [{ Key: 'Name', Value: 'name3' }] }, +] + describe('extractInstancesFromReservations', function () { const client = new Ec2Client('') + it('returns empty when given empty collection', async function () { const actualResult = await client .getInstancesFromReservations( @@ -39,114 +103,17 @@ describe('extractInstancesFromReservations', function () { }) it('flattens the reservationList', async function () { - const testReservationsList: EC2.ReservationList = [ - { - Instances: [ - { - InstanceId: 'id1', - Tags: [{ Key: 'Name', Value: 'name1' }], - }, - { - InstanceId: 'id2', - Tags: [{ Key: 'Name', Value: 'name2' }], - }, - ], - }, - { - Instances: [ - { - InstanceId: 'id3', - Tags: [{ Key: 'Name', Value: 'name3' }], - }, - { - InstanceId: 'id4', - Tags: [{ Key: 'Name', Value: 'name4' }], - }, - ], - }, - ] - const actualResult = await client.getInstancesFromReservations(intoCollection([testReservationsList])).promise() - assert.deepStrictEqual( - [ - { InstanceId: 'id1', Tags: [{ Key: 'Name', Value: 'name1' }] }, - { InstanceId: 'id2', Tags: [{ Key: 'Name', Value: 'name2' }] }, - { InstanceId: 'id3', Tags: [{ Key: 'Name', Value: 'name3' }] }, - { InstanceId: 'id4', Tags: [{ Key: 'Name', Value: 'name4' }] }, - ], - actualResult - ) + const actualResult = await client + .getInstancesFromReservations(intoCollection([completeReservationsList])) + .promise() + assert.deepStrictEqual(actualResult, completeInstanceList) }), - // Unsure if this test case is needed, but the return type in the SDK makes it possible these are unknown/not returned. it('handles undefined and missing pieces in the ReservationList.', async function () { - const testReservationsList: EC2.ReservationList = [ - { - Instances: [ - { - InstanceId: 'id1', - }, - { - InstanceId: undefined, - }, - ], - }, - { - Instances: [ - { - InstanceId: 'id3', - Tags: [{ Key: 'Name', Value: 'name3' }], - }, - {}, - ], - }, - ] const actualResult = await client - .getInstancesFromReservations(intoCollection([testReservationsList])) + .getInstancesFromReservations(intoCollection([incompleteReservationsList])) .promise() - assert.deepStrictEqual( - [{ InstanceId: 'id1' }, { InstanceId: 'id3', Tags: [{ Key: 'Name', Value: 'name3' }] }], - actualResult - ) + assert.deepStrictEqual(actualResult, incomepleteInstanceList) }) - - it('can process results without complete Tag field.', async function () { - const testReservationsList: EC2.ReservationList = [ - { - Instances: [ - { - InstanceId: 'id1', - Tags: [{ Key: 'Name', Value: 'name1' }], - }, - { - InstanceId: 'id2', - }, - ], - }, - { - Instances: [ - { - InstanceId: 'id3', - Tags: [{ Key: 'Name', Value: 'name3' }], - }, - { - InstanceId: 'id4', - Tags: [], - }, - ], - }, - ] - - const actualResult = await client.getInstancesFromReservations(intoCollection([testReservationsList])).promise() - - assert.deepStrictEqual( - [ - { InstanceId: 'id1', Tags: [{ Key: 'Name', Value: 'name1' }] }, - { InstanceId: 'id2' }, - { InstanceId: 'id3', Tags: [{ Key: 'Name', Value: 'name3' }] }, - { InstanceId: 'id4', Tags: [] }, - ], - actualResult - ) - }) }) describe('addStatusesToInstances', async function () { @@ -157,19 +124,12 @@ describe('addStatusesToInstances', async function () { }) it('adds appropriate status field to the instance', async function () { - const testInstances = [ - { InstanceId: 'running-1', Tags: [{ Key: 'Name', Value: 'name1' }] }, - { InstanceId: 'stopped-2' }, - { InstanceId: 'pending-3', Tags: [{ Key: 'Name', Value: 'name3' }] }, - { InstanceId: 'running-4', Tags: [] }, - ] - - const actualResult = await client.testAddStatusesToInstances(testInstances) + const actualResult = await client.testAddStatusesToInstances(completeInstanceList) const expectedResult = [ { InstanceId: 'running-1', status: 'running', Tags: [{ Key: 'Name', Value: 'name1' }] }, - { InstanceId: 'stopped-2', status: 'stopped' }, + { InstanceId: 'stopped-2', status: 'stopped', Tags: [{ Key: 'Name', Value: 'name2' }] }, { InstanceId: 'pending-3', status: 'pending', Tags: [{ Key: 'Name', Value: 'name3' }] }, - { InstanceId: 'running-4', status: 'running', Tags: [] }, + { InstanceId: 'running-4', status: 'running', Tags: [{ Key: 'Name', Value: 'name4' }] }, ] assert.deepStrictEqual(actualResult, expectedResult) @@ -184,36 +144,25 @@ describe('addNamesToInstances', async function () { }) it('adds corresponding name to instance', async function () { - const testInstances = [ - { InstanceId: 'running-1', Tags: [{ Key: 'Name', Value: 'name1' }] }, - { InstanceId: 'pending-3', Tags: [{ Key: 'Name', Value: 'name3' }] }, - ] - - const actualResult = await client.testAddNamesToInstances(testInstances) + const actualResult = await client.testAddNamesToInstances(completeInstanceList) const expectedResult = [ { InstanceId: 'running-1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, + { InstanceId: 'stopped-2', name: 'name2', Tags: [{ Key: 'Name', Value: 'name2' }] }, { InstanceId: 'pending-3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, + { InstanceId: 'running-4', name: 'name4', Tags: [{ Key: 'Name', Value: 'name4' }] }, ] assert.deepStrictEqual(actualResult, expectedResult) }) it('handles incomplete and missing tag fields', async function () { - const testInstances = [ - { InstanceId: 'running-1', Tags: [{ Key: 'Name', Value: 'name1' }] }, - { InstanceId: 'stopped-2' }, - { InstanceId: 'pending-3', Tags: [{ Key: 'Name', Value: 'name3' }] }, - { InstanceId: 'running-4', Tags: [] }, - ] - - const actualResult = await client.testAddNamesToInstances(testInstances) + const actualResult = await client.testAddNamesToInstances(incomepleteInstanceList) const expectedResult = [ - { InstanceId: 'running-1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, - { InstanceId: 'stopped-2' }, + { InstanceId: 'running-1' }, + { InstanceId: 'stopped-2', Tags: [] }, { InstanceId: 'pending-3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, - { InstanceId: 'running-4', Tags: [] }, ] assert.deepStrictEqual(actualResult, expectedResult) From b76e22fbcc157d1831eaa61453d5ab60ef489ef6 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 14 Jul 2023 11:57:09 -0700 Subject: [PATCH 058/172] add test for filter functionality --- src/ec2/prompter.ts | 3 ++- src/test/ec2/prompter.test.ts | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/ec2/prompter.ts b/src/ec2/prompter.ts index 2052b7c305c..ba7ab2a4194 100644 --- a/src/ec2/prompter.ts +++ b/src/ec2/prompter.ts @@ -14,7 +14,7 @@ import { AsyncCollection } from '../shared/utilities/asyncCollection' export type instanceFilter = (instance: Ec2Instance) => boolean export class Ec2Prompter { - public constructor() {} + public constructor(protected filter?: instanceFilter) {} protected static asQuickPickItem(instance: Ec2Instance): DataQuickPickItem { return { @@ -49,6 +49,7 @@ export class Ec2Prompter { protected async getInstancesAsQuickPickItems(region: string): Promise[]> { return (await this.getInstancesFromRegion(region)) + .filter(this.filter ? instance => this.filter!(instance) : instance => true) .map(instance => Ec2Prompter.asQuickPickItem(instance)) .promise() } diff --git a/src/test/ec2/prompter.test.ts b/src/test/ec2/prompter.test.ts index e19eb397a8e..aa57b08f954 100644 --- a/src/test/ec2/prompter.test.ts +++ b/src/test/ec2/prompter.test.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as assert from 'assert' -import { Ec2Prompter } from '../../ec2/prompter' +import { Ec2Prompter, instanceFilter } from '../../ec2/prompter' import { Ec2Instance } from '../../shared/clients/ec2Client' import { RegionSubmenuResponse } from '../../shared/ui/common/regionSubmenu' import { Ec2Selection } from '../../ec2/utils' @@ -29,6 +29,14 @@ describe('Ec2Prompter', async function () { protected override async getInstancesFromRegion(regionCode: string): Promise> { return intoCollection(this.instances) } + + public setFilter(filter: instanceFilter) { + this.filter = filter + } + + public unsetFilter() { + this.filter = undefined + } } it('initializes properly', function () { const prompter = new Ec2Prompter() @@ -118,6 +126,7 @@ describe('Ec2Prompter', async function () { name: 'third', }, ] + prompter.unsetFilter() }) it('returns empty when no instances present', async function () { @@ -126,7 +135,7 @@ describe('Ec2Prompter', async function () { assert.ok(items.length === 0) }) - it('returns items mapped to QuickPick items', async function () { + it('returns items mapped to QuickPick items without filter', async function () { const expected = [ { label: '$(terminal) \t' + prompter.instances[0].name!, @@ -148,5 +157,25 @@ describe('Ec2Prompter', async function () { const items = await prompter.testGetInstancesAsQuickPickItems('test-region') assert.deepStrictEqual(items, expected) }) + + it('applies filter when given', async function () { + prompter.setFilter(i => parseInt(i.InstanceId!) % 2 == 1) + + const expected = [ + { + label: '$(terminal) \t' + prompter.instances[0].name!, + detail: prompter.instances[0].InstanceId!, + data: prompter.instances[0].InstanceId!, + }, + { + label: '$(terminal) \t' + prompter.instances[2].name!, + detail: prompter.instances[2].InstanceId!, + data: prompter.instances[2].InstanceId!, + }, + ] + + const items = await prompter.testGetInstancesAsQuickPickItems('test-region') + assert.deepStrictEqual(items, expected) + }) }) }) From 98f03fbb514a63aef6452cc9ef59c86582d41ab3 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 14 Jul 2023 12:03:12 -0700 Subject: [PATCH 059/172] change prompter to filter based on instance status --- src/ec2/commands.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/ec2/commands.ts b/src/ec2/commands.ts index 0756945805e..206a5256b5c 100644 --- a/src/ec2/commands.ts +++ b/src/ec2/commands.ts @@ -7,8 +7,9 @@ import { InstanceStateManager, getStateManagerForSelection } from './instanceSta import { Ec2InstanceNode } from './explorer/ec2InstanceNode' import { Ec2Node } from './explorer/ec2ParentNode' import { Ec2ConnectionManager } from './model' -import { Ec2Prompter } from './prompter' +import { Ec2Prompter, instanceFilter } from './prompter' import { Ec2Selection } from './utils' +import { Ec2Instance } from '../shared/clients/ec2Client' export async function openTerminal(node?: Ec2Node) { const selection = await getSelection(node) @@ -24,12 +25,14 @@ export async function openRemoteConnection(node?: Ec2Node) { } export async function startInstance(node?: Ec2Node) { - const stateManager = await getStateManager(node) + const prompterFilter = (instance: Ec2Instance) => instance.status !== 'running' + const stateManager = await getStateManager(node, prompterFilter) await stateManager.startInstanceWithCancel() } export async function stopInstance(node?: Ec2Node) { - const stateManager = await getStateManager(node) + const prompterFilter = (instance: Ec2Instance) => instance.status !== 'stopped' + const stateManager = await getStateManager(node, prompterFilter) await stateManager.stopInstanceWithCancel() } @@ -38,14 +41,14 @@ export async function rebootInstance(node?: Ec2Node) { await stateManager.rebootInstanceWithCancel() } -async function getStateManager(node?: Ec2Node): Promise { - const selection = await getSelection(node) +async function getStateManager(node?: Ec2Node, prompterFilter?: instanceFilter): Promise { + const selection = await getSelection(node, prompterFilter) const stateManager = getStateManagerForSelection(selection) return stateManager } -async function getSelection(node?: Ec2Node): Promise { - const prompter = new Ec2Prompter() +async function getSelection(node?: Ec2Node, filter?: instanceFilter): Promise { + const prompter = new Ec2Prompter(filter) const selection = node && node instanceof Ec2InstanceNode ? node.toSelection() : await prompter.promptUser() return selection } From 6cc4cc5c32aa5867d9986852eca8567c958da063 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 14 Jul 2023 12:13:39 -0700 Subject: [PATCH 060/172] add icons to QuickPick --- src/ec2/prompter.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/ec2/prompter.ts b/src/ec2/prompter.ts index ba7ab2a4194..9b61eaf039f 100644 --- a/src/ec2/prompter.ts +++ b/src/ec2/prompter.ts @@ -16,9 +16,22 @@ export type instanceFilter = (instance: Ec2Instance) => boolean export class Ec2Prompter { public constructor(protected filter?: instanceFilter) {} + protected static getIconForInstance(instance: Ec2Instance) { + if (instance.status === 'running') { + return '$(check)' + } + + if (instance.status === 'stopped') { + return '$(stop)' + } + + return '$(loading~spin)' + } + protected static asQuickPickItem(instance: Ec2Instance): DataQuickPickItem { + const icon = Ec2Prompter.getIconForInstance(instance) return { - label: '$(terminal) \t' + (instance.name ?? '(no name)'), + label: `${icon} \t ${instance.name ?? '(no name)'}`, detail: instance.InstanceId, data: instance.InstanceId, } From d147b2288d323507d12df9d7f2bff1e294338dd6 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 14 Jul 2023 12:16:50 -0700 Subject: [PATCH 061/172] disallow rebooting stopped instance --- src/ec2/commands.ts | 3 ++- src/ec2/instanceStateManager.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ec2/commands.ts b/src/ec2/commands.ts index 206a5256b5c..8243c1da029 100644 --- a/src/ec2/commands.ts +++ b/src/ec2/commands.ts @@ -37,7 +37,8 @@ export async function stopInstance(node?: Ec2Node) { } export async function rebootInstance(node?: Ec2Node) { - const stateManager = await getStateManager(node) + const prompterFilter = (instance: Ec2Instance) => instance.status !== 'stopped' + const stateManager = await getStateManager(node, prompterFilter) await stateManager.rebootInstanceWithCancel() } diff --git a/src/ec2/instanceStateManager.ts b/src/ec2/instanceStateManager.ts index 317fa56df71..58ebe7206d6 100644 --- a/src/ec2/instanceStateManager.ts +++ b/src/ec2/instanceStateManager.ts @@ -75,6 +75,7 @@ export class InstanceStateManager { await showMessageWithCancel(`EC2: Rebooting instance ${this.instanceId}`, timeout) try { + await this.ensureInstanceNotInStatus('stopped') await this.client.rebootInstance(this.instanceId) } catch (err) { this.handleError(err) From d72f9c969dc050e2c3543d73e441aba968ea08c2 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 14 Jul 2023 13:08:28 -0700 Subject: [PATCH 062/172] add icon entrypoints to start/stop/restart commands --- package.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/package.json b/package.json index 1e666191b45..acf863db1be 100644 --- a/package.json +++ b/package.json @@ -1339,16 +1339,31 @@ "group": "0@1", "when": "viewItem == awsEc2Node" }, + { + "command": "aws.ec2.startInstance", + "group": "inline@1", + "when": "viewItem == awsEc2Node" + }, { "command": "aws.ec2.stopInstance", "group": "0@1", "when": "viewItem == awsEc2Node" }, + { + "command": "aws.ec2.stopInstance", + "group": "inline@1", + "when": "viewItem == awsEc2Node" + }, { "command": "aws.ec2.rebootInstance", "group": "0@1", "when": "viewItem == awsEc2Node" }, + { + "command": "aws.ec2.rebootInstance", + "group": "inline@1", + "when": "viewItem == awsEc2Node" + }, { "command": "aws.ecr.createRepository", "when": "view == aws.explorer && viewItem == awsEcrNode", @@ -2115,6 +2130,7 @@ { "command": "aws.ec2.startInstance", "title": "%AWS.command.ec2.startInstance%", + "icon": "$(debug-start)", "category": "%AWS.title%", "cloud9": { "cn": { @@ -2125,6 +2141,7 @@ { "command": "aws.ec2.stopInstance", "title": "%AWS.command.ec2.stopInstance%", + "icon": "$(debug-stop)", "category": "%AWS.title%", "cloud9": { "cn": { @@ -2135,6 +2152,7 @@ { "command": "aws.ec2.rebootInstance", "title": "%AWS.command.ec2.rebootInstance%", + "icon": "$(debug-restart)", "category": "%AWS.title%", "cloud9": { "cn": { From bd709da495aa95bf1659d39e5596e944a712c2bc Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 14 Jul 2023 13:09:49 -0700 Subject: [PATCH 063/172] move icon determiner function to seperate file --- src/ec2/prompter.ts | 16 ++-------------- src/ec2/utils.ts | 14 +++++++++++++- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/ec2/prompter.ts b/src/ec2/prompter.ts index 9b61eaf039f..620124ea4e8 100644 --- a/src/ec2/prompter.ts +++ b/src/ec2/prompter.ts @@ -4,7 +4,7 @@ */ import { RegionSubmenu, RegionSubmenuResponse } from '../shared/ui/common/regionSubmenu' -import { Ec2Selection } from './utils' +import { Ec2Selection, getIconForInstance } from './utils' import { DataQuickPickItem } from '../shared/ui/pickerPrompter' import { Ec2Client, Ec2Instance } from '../shared/clients/ec2Client' import { isValidResponse } from '../shared/wizards/wizard' @@ -16,20 +16,8 @@ export type instanceFilter = (instance: Ec2Instance) => boolean export class Ec2Prompter { public constructor(protected filter?: instanceFilter) {} - protected static getIconForInstance(instance: Ec2Instance) { - if (instance.status === 'running') { - return '$(check)' - } - - if (instance.status === 'stopped') { - return '$(stop)' - } - - return '$(loading~spin)' - } - protected static asQuickPickItem(instance: Ec2Instance): DataQuickPickItem { - const icon = Ec2Prompter.getIconForInstance(instance) + const icon = getIconForInstance(instance) return { label: `${icon} \t ${instance.name ?? '(no name)'}`, detail: instance.InstanceId, diff --git a/src/ec2/utils.ts b/src/ec2/utils.ts index 4c5f474b24a..9c08cbe12cf 100644 --- a/src/ec2/utils.ts +++ b/src/ec2/utils.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Ec2Client } from '../shared/clients/ec2Client' +import { Ec2Client, Ec2Instance } from '../shared/clients/ec2Client' export interface Ec2Selection { instanceId: string @@ -14,3 +14,15 @@ export async function isEc2SelectionRunning(selection: Ec2Selection): Promise Date: Fri, 14 Jul 2023 13:30:02 -0700 Subject: [PATCH 064/172] add icons to show if its running in explorer --- src/ec2/explorer/ec2InstanceNode.ts | 7 ++++--- src/ec2/prompter.ts | 4 ++-- src/ec2/utils.ts | 12 ++++++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/ec2/explorer/ec2InstanceNode.ts b/src/ec2/explorer/ec2InstanceNode.ts index 38c664631aa..ab55bbcace9 100644 --- a/src/ec2/explorer/ec2InstanceNode.ts +++ b/src/ec2/explorer/ec2InstanceNode.ts @@ -2,13 +2,13 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ - +import * as vscode from 'vscode' import { getNameOfInstance } from '../../shared/clients/ec2Client' import { AWSResourceNode } from '../../shared/treeview/nodes/awsResourceNode' import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase' import { Ec2Instance } from '../../shared/clients/ec2Client' import globals from '../../shared/extensionGlobals' -import { Ec2Selection } from '../utils' +import { Ec2Selection, getIconCodeForInstanceStatus } from '../utils' export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode { public constructor( @@ -24,7 +24,8 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode public update(newInstance: Ec2Instance) { this.setInstance(newInstance) this.label = `${this.name} (${this.InstanceId})` - this.tooltip = `${this.name}\n${this.InstanceId}\n${this.arn}` + this.iconPath = new vscode.ThemeIcon(getIconCodeForInstanceStatus(this.instance)) + this.tooltip = `${this.name}\n${this.InstanceId}\n${this.instance.status}\n${this.arn}` } public setInstance(newInstance: Ec2Instance) { diff --git a/src/ec2/prompter.ts b/src/ec2/prompter.ts index 620124ea4e8..826c15a93fb 100644 --- a/src/ec2/prompter.ts +++ b/src/ec2/prompter.ts @@ -4,7 +4,7 @@ */ import { RegionSubmenu, RegionSubmenuResponse } from '../shared/ui/common/regionSubmenu' -import { Ec2Selection, getIconForInstance } from './utils' +import { Ec2Selection, getIconForInstanceStatus } from './utils' import { DataQuickPickItem } from '../shared/ui/pickerPrompter' import { Ec2Client, Ec2Instance } from '../shared/clients/ec2Client' import { isValidResponse } from '../shared/wizards/wizard' @@ -17,7 +17,7 @@ export class Ec2Prompter { public constructor(protected filter?: instanceFilter) {} protected static asQuickPickItem(instance: Ec2Instance): DataQuickPickItem { - const icon = getIconForInstance(instance) + const icon = getIconForInstanceStatus(instance) return { label: `${icon} \t ${instance.name ?? '(no name)'}`, detail: instance.InstanceId, diff --git a/src/ec2/utils.ts b/src/ec2/utils.ts index 9c08cbe12cf..9b65de5fa45 100644 --- a/src/ec2/utils.ts +++ b/src/ec2/utils.ts @@ -15,14 +15,18 @@ export async function isEc2SelectionRunning(selection: Ec2Selection): Promise Date: Fri, 14 Jul 2023 13:47:05 -0700 Subject: [PATCH 065/172] limit commands to cases where they are dont throw error --- package.json | 16 ++++++++-------- src/ec2/explorer/ec2InstanceNode.ts | 18 ++++++++++++++++-- src/ec2/explorer/ec2ParentNode.ts | 6 +++--- src/test/ec2/explorer/ec2InstanceNode.test.ts | 4 ++-- src/test/ec2/explorer/ec2ParentNode.test.ts | 4 ++-- 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index acf863db1be..67fcb4772d7 100644 --- a/package.json +++ b/package.json @@ -1327,42 +1327,42 @@ { "command": "aws.ec2.openTerminal", "group": "0@1", - "when": "viewItem == awsEc2Node" + "when": "viewItem =~ /^(awsEc2(Parent|Running|Stopped|Pending)Node)$/" }, { "command": "aws.ec2.openTerminal", "group": "inline@1", - "when": "viewItem == awsEc2Node" + "when": "viewItem =~ /^(awsEc2(Parent|Running|Stopped|Pending)Node)$/" }, { "command": "aws.ec2.startInstance", "group": "0@1", - "when": "viewItem == awsEc2Node" + "when": "viewItem =~ /^(awsEc2(Stopped|Pending)Node)$/" }, { "command": "aws.ec2.startInstance", "group": "inline@1", - "when": "viewItem == awsEc2Node" + "when": "viewItem =~ /^(awsEc2(Stopped|Pending)Node)$/" }, { "command": "aws.ec2.stopInstance", "group": "0@1", - "when": "viewItem == awsEc2Node" + "when": "viewItem =~ /^(awsEc2(Running|Pending)Node)$/" }, { "command": "aws.ec2.stopInstance", "group": "inline@1", - "when": "viewItem == awsEc2Node" + "when": "viewItem =~ /^(awsEc2(Running|Pending)Node)$/" }, { "command": "aws.ec2.rebootInstance", "group": "0@1", - "when": "viewItem == awsEc2Node" + "when": "viewItem =~ /^(awsEc2(Running|Pending)Node)$/" }, { "command": "aws.ec2.rebootInstance", "group": "inline@1", - "when": "viewItem == awsEc2Node" + "when": "viewItem =~ /^(awsEc2(Running|Pending)Node)$/" }, { "command": "aws.ecr.createRepository", diff --git a/src/ec2/explorer/ec2InstanceNode.ts b/src/ec2/explorer/ec2InstanceNode.ts index ab55bbcace9..e035507331a 100644 --- a/src/ec2/explorer/ec2InstanceNode.ts +++ b/src/ec2/explorer/ec2InstanceNode.ts @@ -10,12 +10,13 @@ import { Ec2Instance } from '../../shared/clients/ec2Client' import globals from '../../shared/extensionGlobals' import { Ec2Selection, getIconCodeForInstanceStatus } from '../utils' +type Ec2InstanceNodeContext = 'awsEc2RunningNode' | 'awsEc2StoppedNode' | 'awsEc2PendingNode' + export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode { public constructor( public override readonly regionCode: string, private readonly partitionId: string, - private instance: Ec2Instance, - public override readonly contextValue: string + private instance: Ec2Instance ) { super('') this.update(instance) @@ -24,10 +25,23 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode public update(newInstance: Ec2Instance) { this.setInstance(newInstance) this.label = `${this.name} (${this.InstanceId})` + this.contextValue = this.getContext() this.iconPath = new vscode.ThemeIcon(getIconCodeForInstanceStatus(this.instance)) this.tooltip = `${this.name}\n${this.InstanceId}\n${this.instance.status}\n${this.arn}` } + private getContext(): Ec2InstanceNodeContext { + if (this.instance.status == 'running') { + return 'awsEc2RunningNode' + } + + if (this.instance.status == 'stopped') { + return 'awsEc2StoppedNode' + } + + return 'awsEc2PendingNode' + } + public setInstance(newInstance: Ec2Instance) { this.instance = newInstance } diff --git a/src/ec2/explorer/ec2ParentNode.ts b/src/ec2/explorer/ec2ParentNode.ts index a0b99fc7c17..d3614db5b16 100644 --- a/src/ec2/explorer/ec2ParentNode.ts +++ b/src/ec2/explorer/ec2ParentNode.ts @@ -10,13 +10,13 @@ import { Ec2InstanceNode } from './ec2InstanceNode' import { Ec2Client } from '../../shared/clients/ec2Client' import { updateInPlace } from '../../shared/utilities/collectionUtils' -export const contextValueEc2 = 'awsEc2Node' +export const parentContextValue = 'awsEc2ParentNode' export type Ec2Node = Ec2InstanceNode | Ec2ParentNode export class Ec2ParentNode extends AWSTreeNodeBase { protected readonly placeHolderMessage = '[No EC2 Instances Found]' protected readonly ec2InstanceNodes: Map - public override readonly contextValue: string = contextValueEc2 + public override readonly contextValue: string = parentContextValue public constructor( public override readonly regionCode: string, @@ -45,7 +45,7 @@ export class Ec2ParentNode extends AWSTreeNodeBase { this.ec2InstanceNodes, ec2Instances.keys(), key => this.ec2InstanceNodes.get(key)!.update(ec2Instances.get(key)!), - key => new Ec2InstanceNode(this.regionCode, this.partitionId, ec2Instances.get(key)!, contextValueEc2) + key => new Ec2InstanceNode(this.regionCode, this.partitionId, ec2Instances.get(key)!) ) } } diff --git a/src/test/ec2/explorer/ec2InstanceNode.test.ts b/src/test/ec2/explorer/ec2InstanceNode.test.ts index ceeff98986b..f425f979aff 100644 --- a/src/test/ec2/explorer/ec2InstanceNode.test.ts +++ b/src/test/ec2/explorer/ec2InstanceNode.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert' import { Ec2InstanceNode } from '../../../ec2/explorer/ec2InstanceNode' import { Ec2Instance, getNameOfInstance } from '../../../shared/clients/ec2Client' -import { contextValueEc2 } from '../../../ec2/explorer/ec2ParentNode' +import { parentContextValue } from '../../../ec2/explorer/ec2ParentNode' describe('ec2InstanceNode', function () { let testNode: Ec2InstanceNode @@ -23,7 +23,7 @@ describe('ec2InstanceNode', function () { ], } - testNode = new Ec2InstanceNode('testRegion', 'testPartition', testInstance, contextValueEc2) + testNode = new Ec2InstanceNode('testRegion', 'testPartition', testInstance, parentContextValue) }) it('instantiates without issue', async function () { diff --git a/src/test/ec2/explorer/ec2ParentNode.test.ts b/src/test/ec2/explorer/ec2ParentNode.test.ts index 168fb3c62cb..3b578d095e4 100644 --- a/src/test/ec2/explorer/ec2ParentNode.test.ts +++ b/src/test/ec2/explorer/ec2ParentNode.test.ts @@ -4,7 +4,7 @@ */ import * as assert from 'assert' -import { Ec2ParentNode, contextValueEc2 } from '../../../ec2/explorer/ec2ParentNode' +import { Ec2ParentNode, parentContextValue } from '../../../ec2/explorer/ec2ParentNode' import { stub } from '../../utilities/stubber' import { Ec2Client, Ec2Instance } from '../../../shared/clients/ec2Client' import { intoCollection } from '../../../shared/utilities/collectionUtils' @@ -65,7 +65,7 @@ describe('ec2ParentNode', function () { const childNodes = await testNode.getChildren() childNodes.forEach(node => - assert.strictEqual(node.contextValue, contextValueEc2, 'expected the node to have a ec2 contextValue') + assert.strictEqual(node.contextValue, parentContextValue, 'expected the node to have a ec2 contextValue') ) }) From f7999b1ba01221c6b6b097d714902c68836a04ab Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 14 Jul 2023 14:05:42 -0700 Subject: [PATCH 066/172] limit explorer icons and refresh after --- package.json | 8 ++++---- src/ec2/activation.ts | 13 ++++++++++++- src/ec2/commands.ts | 10 ++++++++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 67fcb4772d7..2e876ecc9aa 100644 --- a/package.json +++ b/package.json @@ -1327,12 +1327,12 @@ { "command": "aws.ec2.openTerminal", "group": "0@1", - "when": "viewItem =~ /^(awsEc2(Parent|Running|Stopped|Pending)Node)$/" + "when": "viewItem =~ /^(awsEc2(Parent|Running)Node)$/" }, { "command": "aws.ec2.openTerminal", "group": "inline@1", - "when": "viewItem =~ /^(awsEc2(Parent|Running|Stopped|Pending)Node)$/" + "when": "viewItem =~ /^(awsEc2(Parent|Running)Node)$/" }, { "command": "aws.ec2.startInstance", @@ -1357,12 +1357,12 @@ { "command": "aws.ec2.rebootInstance", "group": "0@1", - "when": "viewItem =~ /^(awsEc2(Running|Pending)Node)$/" + "when": "viewItem =~ /^(awsEc2RunningNode)$/" }, { "command": "aws.ec2.rebootInstance", "group": "inline@1", - "when": "viewItem =~ /^(awsEc2(Running|Pending)Node)$/" + "when": "viewItem =~ /^(awsEc2RunningNode)$/" }, { "command": "aws.ecr.createRepository", diff --git a/src/ec2/activation.ts b/src/ec2/activation.ts index 4eea620e863..7914746dbb2 100644 --- a/src/ec2/activation.ts +++ b/src/ec2/activation.ts @@ -6,7 +6,14 @@ import { ExtContext } from '../shared/extensions' import { Commands } from '../shared/vscode/commands2' import { telemetry } from '../shared/telemetry/telemetry' import { Ec2Node } from './explorer/ec2ParentNode' -import { openRemoteConnection, openTerminal, rebootInstance, startInstance, stopInstance } from './commands' +import { + openRemoteConnection, + openTerminal, + rebootInstance, + refreshExplorer, + startInstance, + stopInstance, +} from './commands' export async function activate(ctx: ExtContext): Promise { ctx.extensionContext.subscriptions.push( @@ -23,14 +30,18 @@ export async function activate(ctx: ExtContext): Promise { Commands.register('aws.ec2.startInstance', async (node?: Ec2Node) => { await startInstance(node) + + refreshExplorer() }), Commands.register('aws.ec2.stopInstance', async (node?: Ec2Node) => { await stopInstance(node) + refreshExplorer() }), Commands.register('aws.ec2.rebootInstance', async (node?: Ec2Node) => { await rebootInstance(node) + refreshExplorer() }) ) } diff --git a/src/ec2/commands.ts b/src/ec2/commands.ts index 8243c1da029..8574453da76 100644 --- a/src/ec2/commands.ts +++ b/src/ec2/commands.ts @@ -10,6 +10,16 @@ import { Ec2ConnectionManager } from './model' import { Ec2Prompter, instanceFilter } from './prompter' import { Ec2Selection } from './utils' import { Ec2Instance } from '../shared/clients/ec2Client' +import { isCloud9 } from '../shared/extensionUtilities' +import { commands } from 'vscode' + +export function refreshExplorer(node?: Ec2Node) { + if (isCloud9()) { + commands.executeCommand('aws.refreshAwsExplorer', true) + } else { + commands.executeCommand('aws.refreshAwsExplorerNode', node) + } +} export async function openTerminal(node?: Ec2Node) { const selection = await getSelection(node) From f2a778628c7b19d0d6f1fde4e2abb3b9be493b3e Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 17 Jul 2023 10:22:16 -0700 Subject: [PATCH 067/172] fix outdated test --- src/test/ec2/explorer/ec2InstanceNode.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/ec2/explorer/ec2InstanceNode.test.ts b/src/test/ec2/explorer/ec2InstanceNode.test.ts index f425f979aff..007f0574fd3 100644 --- a/src/test/ec2/explorer/ec2InstanceNode.test.ts +++ b/src/test/ec2/explorer/ec2InstanceNode.test.ts @@ -6,7 +6,6 @@ import * as assert from 'assert' import { Ec2InstanceNode } from '../../../ec2/explorer/ec2InstanceNode' import { Ec2Instance, getNameOfInstance } from '../../../shared/clients/ec2Client' -import { parentContextValue } from '../../../ec2/explorer/ec2ParentNode' describe('ec2InstanceNode', function () { let testNode: Ec2InstanceNode @@ -23,7 +22,7 @@ describe('ec2InstanceNode', function () { ], } - testNode = new Ec2InstanceNode('testRegion', 'testPartition', testInstance, parentContextValue) + testNode = new Ec2InstanceNode('testRegion', 'testPartition', testInstance) }) it('instantiates without issue', async function () { From cf05c8ed4616ae8d0308f28dfeec642b7d7522b6 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 17 Jul 2023 10:50:38 -0700 Subject: [PATCH 068/172] remove commands from command palette --- package.json | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/package.json b/package.json index 2e876ecc9aa..9b419fcefe0 100644 --- a/package.json +++ b/package.json @@ -1118,18 +1118,6 @@ "command": "aws.ec2.openTerminal", "when": "aws.isDevMode" }, - { - "command": "aws.ec2.startInstance", - "whem": "aws.isDevMode" - }, - { - "command": "aws.ec2.stopInstance", - "whem": "aws.isDevMode" - }, - { - "command": "aws.ec2.rebootInstance", - "whem": "aws.isDevMode" - }, { "command": "aws.dev.openMenu", "when": "aws.isDevMode || isCloud9" From f0900d783f317de4963368b9c67d1f22bbdcc12d Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 17 Jul 2023 11:40:45 -0700 Subject: [PATCH 069/172] refactor such that it only updates ec2 parent node --- src/ec2/activation.ts | 7 +++---- src/ec2/commands.ts | 8 ++------ src/ec2/explorer/ec2InstanceNode.ts | 14 +++++++++++--- src/ec2/explorer/ec2ParentNode.ts | 18 ++++++++++++++---- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/ec2/activation.ts b/src/ec2/activation.ts index 7914746dbb2..5f10ea9701d 100644 --- a/src/ec2/activation.ts +++ b/src/ec2/activation.ts @@ -30,18 +30,17 @@ export async function activate(ctx: ExtContext): Promise { Commands.register('aws.ec2.startInstance', async (node?: Ec2Node) => { await startInstance(node) - - refreshExplorer() + refreshExplorer(node) }), Commands.register('aws.ec2.stopInstance', async (node?: Ec2Node) => { await stopInstance(node) - refreshExplorer() + refreshExplorer(node) }), Commands.register('aws.ec2.rebootInstance', async (node?: Ec2Node) => { await rebootInstance(node) - refreshExplorer() + refreshExplorer(node) }) ) } diff --git a/src/ec2/commands.ts b/src/ec2/commands.ts index 8574453da76..0ba9bec259c 100644 --- a/src/ec2/commands.ts +++ b/src/ec2/commands.ts @@ -10,14 +10,10 @@ import { Ec2ConnectionManager } from './model' import { Ec2Prompter, instanceFilter } from './prompter' import { Ec2Selection } from './utils' import { Ec2Instance } from '../shared/clients/ec2Client' -import { isCloud9 } from '../shared/extensionUtilities' -import { commands } from 'vscode' export function refreshExplorer(node?: Ec2Node) { - if (isCloud9()) { - commands.executeCommand('aws.refreshAwsExplorer', true) - } else { - commands.executeCommand('aws.refreshAwsExplorerNode', node) + if (node) { + node instanceof Ec2InstanceNode ? node.parent.refreshNode() : node.refreshNode() } } diff --git a/src/ec2/explorer/ec2InstanceNode.ts b/src/ec2/explorer/ec2InstanceNode.ts index e035507331a..3809fa5c9f6 100644 --- a/src/ec2/explorer/ec2InstanceNode.ts +++ b/src/ec2/explorer/ec2InstanceNode.ts @@ -3,26 +3,29 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' -import { getNameOfInstance } from '../../shared/clients/ec2Client' +import { Ec2Client, getNameOfInstance } from '../../shared/clients/ec2Client' import { AWSResourceNode } from '../../shared/treeview/nodes/awsResourceNode' import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase' import { Ec2Instance } from '../../shared/clients/ec2Client' import globals from '../../shared/extensionGlobals' import { Ec2Selection, getIconCodeForInstanceStatus } from '../utils' +import { Ec2ParentNode } from './ec2ParentNode' type Ec2InstanceNodeContext = 'awsEc2RunningNode' | 'awsEc2StoppedNode' | 'awsEc2PendingNode' export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode { public constructor( + public readonly parent: Ec2ParentNode, + public readonly client: Ec2Client, public override readonly regionCode: string, private readonly partitionId: string, private instance: Ec2Instance ) { super('') - this.update(instance) + this.updateInstance(instance) } - public update(newInstance: Ec2Instance) { + public updateInstance(newInstance: Ec2Instance) { this.setInstance(newInstance) this.label = `${this.name} (${this.InstanceId})` this.contextValue = this.getContext() @@ -30,6 +33,11 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode this.tooltip = `${this.name}\n${this.InstanceId}\n${this.instance.status}\n${this.arn}` } + public async updateStatus() { + const newStatus = await this.client.getInstanceStatus(this.InstanceId) + this.updateInstance({ ...this.instance, status: newStatus }) + } + private getContext(): Ec2InstanceNodeContext { if (this.instance.status == 'running') { return 'awsEc2RunningNode' diff --git a/src/ec2/explorer/ec2ParentNode.ts b/src/ec2/explorer/ec2ParentNode.ts index d3614db5b16..e55bac938c7 100644 --- a/src/ec2/explorer/ec2ParentNode.ts +++ b/src/ec2/explorer/ec2ParentNode.ts @@ -9,18 +9,19 @@ import { PlaceholderNode } from '../../shared/treeview/nodes/placeholderNode' import { Ec2InstanceNode } from './ec2InstanceNode' import { Ec2Client } from '../../shared/clients/ec2Client' import { updateInPlace } from '../../shared/utilities/collectionUtils' +import { Commands } from '../../shared/vscode/commands' export const parentContextValue = 'awsEc2ParentNode' export type Ec2Node = Ec2InstanceNode | Ec2ParentNode export class Ec2ParentNode extends AWSTreeNodeBase { protected readonly placeHolderMessage = '[No EC2 Instances Found]' - protected readonly ec2InstanceNodes: Map + protected ec2InstanceNodes: Map public override readonly contextValue: string = parentContextValue public constructor( public override readonly regionCode: string, - private readonly partitionId: string, + public readonly partitionId: string, protected readonly ec2Client: Ec2Client ) { super('EC2', vscode.TreeItemCollapsibleState.Collapsed) @@ -44,8 +45,17 @@ export class Ec2ParentNode extends AWSTreeNodeBase { updateInPlace( this.ec2InstanceNodes, ec2Instances.keys(), - key => this.ec2InstanceNodes.get(key)!.update(ec2Instances.get(key)!), - key => new Ec2InstanceNode(this.regionCode, this.partitionId, ec2Instances.get(key)!) + key => this.ec2InstanceNodes.get(key)!.updateInstance(ec2Instances.get(key)!), + key => new Ec2InstanceNode(this, this.ec2Client, this.regionCode, this.partitionId, ec2Instances.get(key)!) ) } + + public async clearChildren() { + this.ec2InstanceNodes = new Map() + } + + public async refreshNode(): Promise { + this.clearChildren() + Commands.vscode().execute('aws.refreshAwsExplorerNode', this) + } } From 10566c19598465e558c52056ac602e8130885080 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 17 Jul 2023 11:53:29 -0700 Subject: [PATCH 070/172] update outdated test --- src/test/ec2/explorer/ec2InstanceNode.test.ts | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/test/ec2/explorer/ec2InstanceNode.test.ts b/src/test/ec2/explorer/ec2InstanceNode.test.ts index 007f0574fd3..7ae1d9e3ab2 100644 --- a/src/test/ec2/explorer/ec2InstanceNode.test.ts +++ b/src/test/ec2/explorer/ec2InstanceNode.test.ts @@ -5,11 +5,14 @@ import * as assert from 'assert' import { Ec2InstanceNode } from '../../../ec2/explorer/ec2InstanceNode' -import { Ec2Instance, getNameOfInstance } from '../../../shared/clients/ec2Client' +import { Ec2Client, Ec2Instance, getNameOfInstance } from '../../../shared/clients/ec2Client' +import { Ec2ParentNode } from '../../../ec2/explorer/ec2ParentNode' describe('ec2InstanceNode', function () { let testNode: Ec2InstanceNode let testInstance: Ec2Instance + const testRegion = 'testRegion' + const testPartition = 'testPartition' before(function () { testInstance = { @@ -20,9 +23,11 @@ describe('ec2InstanceNode', function () { Value: 'testName', }, ], + status: 'testing', } - - testNode = new Ec2InstanceNode('testRegion', 'testPartition', testInstance) + const testClient = new Ec2Client('') + const testParentNode = new Ec2ParentNode(testRegion, testPartition, testClient) + testNode = new Ec2InstanceNode(testParentNode, testClient, 'testRegion', 'testPartition', testInstance) }) it('instantiates without issue', async function () { @@ -41,13 +46,17 @@ describe('ec2InstanceNode', function () { assert.strictEqual(testNode.name, getNameOfInstance(testInstance)) }) - it('initializes the tooltip', async function () { - assert.strictEqual(testNode.tooltip, `${testNode.name}\n${testNode.InstanceId}\n${testNode.arn}`) - }) - it('has no children', async function () { const childNodes = await testNode.getChildren() assert.ok(childNodes) assert.strictEqual(childNodes.length, 0, 'Expected node to have no children') }) + + it('has an EC2ParentNode as parent', async function () { + assert.ok(testNode.parent instanceof Ec2ParentNode) + }) + + it('intializes the client', async function () { + assert.ok(testNode.client instanceof Ec2Client) + }) }) From 2b749a3e5a3cd30c6fa87687795a756c5b5d47d6 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 17 Jul 2023 12:13:30 -0700 Subject: [PATCH 071/172] add testing to ec2InstanceNode --- src/ec2/explorer/ec2InstanceNode.ts | 12 ++++++--- src/test/ec2/explorer/ec2InstanceNode.test.ts | 25 ++++++++++++++++--- src/test/ec2/explorer/ec2ParentNode.test.ts | 14 +++-------- 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/ec2/explorer/ec2InstanceNode.ts b/src/ec2/explorer/ec2InstanceNode.ts index 3809fa5c9f6..969735f22ab 100644 --- a/src/ec2/explorer/ec2InstanceNode.ts +++ b/src/ec2/explorer/ec2InstanceNode.ts @@ -11,6 +11,10 @@ import globals from '../../shared/extensionGlobals' import { Ec2Selection, getIconCodeForInstanceStatus } from '../utils' import { Ec2ParentNode } from './ec2ParentNode' +export const Ec2InstanceRunningContext = 'awsEc2RunningNode' +export const Ec2InstanceStoppedContext = 'awsEc2StoppedNode' +export const Ec2InstancePendingContext = 'awsEc2PendingNode' + type Ec2InstanceNodeContext = 'awsEc2RunningNode' | 'awsEc2StoppedNode' | 'awsEc2PendingNode' export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode { @@ -19,7 +23,7 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode public readonly client: Ec2Client, public override readonly regionCode: string, private readonly partitionId: string, - private instance: Ec2Instance + protected instance: Ec2Instance ) { super('') this.updateInstance(instance) @@ -40,14 +44,14 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode private getContext(): Ec2InstanceNodeContext { if (this.instance.status == 'running') { - return 'awsEc2RunningNode' + return Ec2InstanceRunningContext } if (this.instance.status == 'stopped') { - return 'awsEc2StoppedNode' + return Ec2InstanceStoppedContext } - return 'awsEc2PendingNode' + return Ec2InstancePendingContext } public setInstance(newInstance: Ec2Instance) { diff --git a/src/test/ec2/explorer/ec2InstanceNode.test.ts b/src/test/ec2/explorer/ec2InstanceNode.test.ts index 7ae1d9e3ab2..c78d14c4a44 100644 --- a/src/test/ec2/explorer/ec2InstanceNode.test.ts +++ b/src/test/ec2/explorer/ec2InstanceNode.test.ts @@ -4,7 +4,12 @@ */ import * as assert from 'assert' -import { Ec2InstanceNode } from '../../../ec2/explorer/ec2InstanceNode' +import { + Ec2InstanceNode, + Ec2InstancePendingContext, + Ec2InstanceRunningContext, + Ec2InstanceStoppedContext, +} from '../../../ec2/explorer/ec2InstanceNode' import { Ec2Client, Ec2Instance, getNameOfInstance } from '../../../shared/clients/ec2Client' import { Ec2ParentNode } from '../../../ec2/explorer/ec2ParentNode' @@ -16,14 +21,14 @@ describe('ec2InstanceNode', function () { before(function () { testInstance = { - InstanceId: 'testId', + InstanceId: 'running-testId', Tags: [ { Key: 'Name', Value: 'testName', }, ], - status: 'testing', + status: 'running', } const testClient = new Ec2Client('') const testParentNode = new Ec2ParentNode(testRegion, testPartition, testClient) @@ -59,4 +64,18 @@ describe('ec2InstanceNode', function () { it('intializes the client', async function () { assert.ok(testNode.client instanceof Ec2Client) }) + + it('sets context value based on status', async function () { + const stoppedInstance = { ...testInstance, status: 'stopped' } + testNode.updateInstance(stoppedInstance) + assert.strictEqual(testNode.contextValue, Ec2InstanceStoppedContext) + + const runningInstance = { ...testInstance, status: 'running' } + testNode.updateInstance(runningInstance) + assert.strictEqual(testNode.contextValue, Ec2InstanceRunningContext) + + const pendingInstance = { ...testInstance, status: 'pending' } + testNode.updateInstance(pendingInstance) + assert.strictEqual(testNode.contextValue, Ec2InstancePendingContext) + }) }) diff --git a/src/test/ec2/explorer/ec2ParentNode.test.ts b/src/test/ec2/explorer/ec2ParentNode.test.ts index 3b578d095e4..98e0298b08f 100644 --- a/src/test/ec2/explorer/ec2ParentNode.test.ts +++ b/src/test/ec2/explorer/ec2ParentNode.test.ts @@ -4,7 +4,7 @@ */ import * as assert from 'assert' -import { Ec2ParentNode, parentContextValue } from '../../../ec2/explorer/ec2ParentNode' +import { Ec2ParentNode } from '../../../ec2/explorer/ec2ParentNode' import { stub } from '../../utilities/stubber' import { Ec2Client, Ec2Instance } from '../../../shared/clients/ec2Client' import { intoCollection } from '../../../shared/utilities/collectionUtils' @@ -36,8 +36,8 @@ describe('ec2ParentNode', function () { beforeEach(function () { instances = [ - { name: 'firstOne', InstanceId: '0' }, - { name: 'secondOne', InstanceId: '1' }, + { name: 'firstOne', InstanceId: '0', status: 'running' }, + { name: 'secondOne', InstanceId: '1', status: 'stopped' }, ] testNode = new Ec2ParentNode(testRegion, testPartition, createClient()) @@ -61,14 +61,6 @@ describe('ec2ParentNode', function () { ) }) - it('has child nodes with ec2 contextValuue', async function () { - const childNodes = await testNode.getChildren() - - childNodes.forEach(node => - assert.strictEqual(node.contextValue, parentContextValue, 'expected the node to have a ec2 contextValue') - ) - }) - it('sorts child nodes', async function () { const sortedText = ['aa', 'ab', 'bb', 'bc', 'cc', 'cd'] instances = [ From bf25145367e4a52bf4b28572ecc7ee421c1c1272 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 17 Jul 2023 12:16:25 -0700 Subject: [PATCH 072/172] add another test for the update functionality --- src/test/ec2/explorer/ec2InstanceNode.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/test/ec2/explorer/ec2InstanceNode.test.ts b/src/test/ec2/explorer/ec2InstanceNode.test.ts index c78d14c4a44..b45a17b0b0e 100644 --- a/src/test/ec2/explorer/ec2InstanceNode.test.ts +++ b/src/test/ec2/explorer/ec2InstanceNode.test.ts @@ -21,7 +21,7 @@ describe('ec2InstanceNode', function () { before(function () { testInstance = { - InstanceId: 'running-testId', + InstanceId: 'testId', Tags: [ { Key: 'Name', @@ -35,6 +35,10 @@ describe('ec2InstanceNode', function () { testNode = new Ec2InstanceNode(testParentNode, testClient, 'testRegion', 'testPartition', testInstance) }) + this.beforeEach(function () { + testNode.updateInstance(testInstance) + }) + it('instantiates without issue', async function () { assert.ok(testNode) }) @@ -78,4 +82,10 @@ describe('ec2InstanceNode', function () { testNode.updateInstance(pendingInstance) assert.strictEqual(testNode.contextValue, Ec2InstancePendingContext) }) + + it('updates label with new instance', async function () { + const newIdInstance = { ...testInstance, InstanceId: 'testId2' } + testNode.updateInstance(newIdInstance) + assert.strictEqual(testNode.label, `${getNameOfInstance(newIdInstance)} (${newIdInstance.InstanceId})`) + }) }) From de97b43d865c0cb5b9bc3de65b72d28e80c8f7a5 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 17 Jul 2023 12:24:20 -0700 Subject: [PATCH 073/172] update tests to use icons --- src/test/ec2/prompter.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/test/ec2/prompter.test.ts b/src/test/ec2/prompter.test.ts index aa57b08f954..fe7470ad10b 100644 --- a/src/test/ec2/prompter.test.ts +++ b/src/test/ec2/prompter.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert' import { Ec2Prompter, instanceFilter } from '../../ec2/prompter' import { Ec2Instance } from '../../shared/clients/ec2Client' import { RegionSubmenuResponse } from '../../shared/ui/common/regionSubmenu' -import { Ec2Selection } from '../../ec2/utils' +import { Ec2Selection, getIconForInstanceStatus } from '../../ec2/utils' import { AsyncCollection } from '../../shared/utilities/asyncCollection' import { intoCollection } from '../../shared/utilities/collectionUtils' import { DataQuickPickItem } from '../../shared/ui/pickerPrompter' @@ -58,7 +58,7 @@ describe('Ec2Prompter', async function () { const result = prompter.testAsQuickPickItem(testInstance) const expected = { - label: '$(terminal) \t' + testInstance.name, + label: `${getIconForInstanceStatus(testInstance)} \t ${testInstance.name}`, detail: testInstance.InstanceId, data: testInstance.InstanceId, } @@ -72,7 +72,7 @@ describe('Ec2Prompter', async function () { const result = prompter.testAsQuickPickItem(testInstance) const expected = { - label: '$(terminal) \t' + '(no name)', + label: `${getIconForInstanceStatus(testInstance)} \t (no name)`, detail: testInstance.InstanceId, data: testInstance.InstanceId, } @@ -138,17 +138,17 @@ describe('Ec2Prompter', async function () { it('returns items mapped to QuickPick items without filter', async function () { const expected = [ { - label: '$(terminal) \t' + prompter.instances[0].name!, + label: `${getIconForInstanceStatus(prompter.instances[0])} \t ${prompter.instances[0].name!}`, detail: prompter.instances[0].InstanceId!, data: prompter.instances[0].InstanceId!, }, { - label: '$(terminal) \t' + prompter.instances[1].name!, + label: `${getIconForInstanceStatus(prompter.instances[1])} \t ${prompter.instances[1].name!}`, detail: prompter.instances[1].InstanceId!, data: prompter.instances[1].InstanceId!, }, { - label: '$(terminal) \t' + prompter.instances[2].name!, + label: `${getIconForInstanceStatus(prompter.instances[2])} \t ${prompter.instances[2].name!}`, detail: prompter.instances[2].InstanceId!, data: prompter.instances[2].InstanceId!, }, @@ -163,12 +163,12 @@ describe('Ec2Prompter', async function () { const expected = [ { - label: '$(terminal) \t' + prompter.instances[0].name!, + label: `${getIconForInstanceStatus(prompter.instances[0])} \t ${prompter.instances[0].name!}`, detail: prompter.instances[0].InstanceId!, data: prompter.instances[0].InstanceId!, }, { - label: '$(terminal) \t' + prompter.instances[2].name!, + label: `${getIconForInstanceStatus(prompter.instances[2])} \t ${prompter.instances[2].name!}`, detail: prompter.instances[2].InstanceId!, data: prompter.instances[2].InstanceId!, }, From 1914fe822563d707399e49b7f78234781aa4a630 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 17 Jul 2023 16:02:35 -0700 Subject: [PATCH 074/172] implement core logic with basic tests --- src/ec2/explorer/ec2InstanceNode.ts | 6 +++- src/ec2/explorer/ec2ParentNode.ts | 38 +++++++++++++++++++++ src/test/ec2/explorer/ec2ParentNode.test.ts | 19 ++++++++++- 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/ec2/explorer/ec2InstanceNode.ts b/src/ec2/explorer/ec2InstanceNode.ts index 969735f22ab..fa40cae88bf 100644 --- a/src/ec2/explorer/ec2InstanceNode.ts +++ b/src/ec2/explorer/ec2InstanceNode.ts @@ -23,7 +23,7 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode public readonly client: Ec2Client, public override readonly regionCode: string, private readonly partitionId: string, - protected instance: Ec2Instance + public instance: Ec2Instance ) { super('') this.updateInstance(instance) @@ -65,6 +65,10 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode } } + public get status(): string { + return this.instance.status! + } + public get name(): string { return getNameOfInstance(this.instance) ?? `(no name)` } diff --git a/src/ec2/explorer/ec2ParentNode.ts b/src/ec2/explorer/ec2ParentNode.ts index e55bac938c7..8e983005be8 100644 --- a/src/ec2/explorer/ec2ParentNode.ts +++ b/src/ec2/explorer/ec2ParentNode.ts @@ -10,14 +10,20 @@ import { Ec2InstanceNode } from './ec2InstanceNode' import { Ec2Client } from '../../shared/clients/ec2Client' import { updateInPlace } from '../../shared/utilities/collectionUtils' import { Commands } from '../../shared/vscode/commands' +import globals from '../../shared/extensionGlobals' +import { ToolkitError } from '../../shared/errors' export const parentContextValue = 'awsEc2ParentNode' export type Ec2Node = Ec2InstanceNode | Ec2ParentNode +const pollingInterval = 3000 + export class Ec2ParentNode extends AWSTreeNodeBase { protected readonly placeHolderMessage = '[No EC2 Instances Found]' protected ec2InstanceNodes: Map public override readonly contextValue: string = parentContextValue + protected pollingNodes: Set = new Set() + private pollTimer?: NodeJS.Timeout public constructor( public override readonly regionCode: string, @@ -50,6 +56,38 @@ export class Ec2ParentNode extends AWSTreeNodeBase { ) } + public isPolling(): boolean { + return this.pollingNodes.size !== 0 + } + + public startPolling(instanceId: string) { + this.pollingNodes.add(instanceId) + this.pollTimer = + this.pollTimer ?? globals.clock.setInterval(this.updatePollingNodes.bind(this), pollingInterval) + } + + public updatePollingNodes() { + console.log('here') + this.pollingNodes.forEach(async value => { + const instanceNode = this.getChild(value) + await instanceNode.updateStatus() + + if (instanceNode.status != 'pending') { + this.pollingNodes.delete(value) + } + }) + this.refreshNode() + } + + public getChild(instanceId: string): Ec2InstanceNode { + if (!this.ec2InstanceNodes.has(instanceId)) { + throw new ToolkitError(`Unable to retrieve child instance requested with id: ${instanceId}`, { + code: 'MissingChild', + }) + } + return this.ec2InstanceNodes.get(instanceId)! + } + public async clearChildren() { this.ec2InstanceNodes = new Map() } diff --git a/src/test/ec2/explorer/ec2ParentNode.test.ts b/src/test/ec2/explorer/ec2ParentNode.test.ts index 98e0298b08f..8e7d6df18c5 100644 --- a/src/test/ec2/explorer/ec2ParentNode.test.ts +++ b/src/test/ec2/explorer/ec2ParentNode.test.ts @@ -4,6 +4,9 @@ */ import * as assert from 'assert' +import * as FakeTimers from '@sinonjs/fake-timers' +import * as sinon from 'sinon' +import { verify, anything, instance, mock, when } from 'ts-mockito' import { Ec2ParentNode } from '../../../ec2/explorer/ec2ParentNode' import { stub } from '../../utilities/stubber' import { Ec2Client, Ec2Instance } from '../../../shared/clients/ec2Client' @@ -13,10 +16,15 @@ import { assertNodeListOnlyHasPlaceholderNode, } from '../../utilities/explorerNodeAssertions' import { Ec2InstanceNode } from '../../../ec2/explorer/ec2InstanceNode' +import { installFakeClock } from '../../testUtil' +import { AWSTreeNodeBase } from '../../../shared/treeview/nodes/awsTreeNodeBase' describe('ec2ParentNode', function () { let testNode: Ec2ParentNode let instances: Ec2Instance[] + let clock: FakeTimers.InstalledClock + let refreshStub: sinon.SinonStub<[], Promise> + const testRegion = 'testRegion' const testPartition = 'testPartition' @@ -34,9 +42,14 @@ describe('ec2ParentNode', function () { return client } + before(function () { + clock = installFakeClock() + refreshStub = sinon.stub(Ec2ParentNode.prototype, 'refreshNode') + }) + beforeEach(function () { instances = [ - { name: 'firstOne', InstanceId: '0', status: 'running' }, + { name: 'firstOne', InstanceId: '0', status: 'pending' }, { name: 'secondOne', InstanceId: '1', status: 'stopped' }, ] @@ -96,4 +109,8 @@ describe('ec2ParentNode', function () { const childNodes = await testNode.getChildren() assert.strictEqual(childNodes.length, instances.length, 'Unexpected child count') }) + + it('is not polling on initialization', async function () { + assert.strictEqual(testNode.isPolling(), false) + }) }) From ce7beb9d85e200a39c3bbf2084aa9990d2df0bae Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 17 Jul 2023 16:10:28 -0700 Subject: [PATCH 075/172] refactor to key by node, rather than string --- src/ec2/explorer/ec2InstanceNode.ts | 2 +- src/ec2/explorer/ec2ParentNode.ts | 26 ++++++--------------- src/test/ec2/explorer/ec2ParentNode.test.ts | 18 ++++++++++++-- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/ec2/explorer/ec2InstanceNode.ts b/src/ec2/explorer/ec2InstanceNode.ts index fa40cae88bf..aaef9f06243 100644 --- a/src/ec2/explorer/ec2InstanceNode.ts +++ b/src/ec2/explorer/ec2InstanceNode.ts @@ -65,7 +65,7 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode } } - public get status(): string { + public getStatus(): string { return this.instance.status! } diff --git a/src/ec2/explorer/ec2ParentNode.ts b/src/ec2/explorer/ec2ParentNode.ts index 8e983005be8..359bb0cf407 100644 --- a/src/ec2/explorer/ec2ParentNode.ts +++ b/src/ec2/explorer/ec2ParentNode.ts @@ -11,7 +11,6 @@ import { Ec2Client } from '../../shared/clients/ec2Client' import { updateInPlace } from '../../shared/utilities/collectionUtils' import { Commands } from '../../shared/vscode/commands' import globals from '../../shared/extensionGlobals' -import { ToolkitError } from '../../shared/errors' export const parentContextValue = 'awsEc2ParentNode' export type Ec2Node = Ec2InstanceNode | Ec2ParentNode @@ -22,7 +21,7 @@ export class Ec2ParentNode extends AWSTreeNodeBase { protected readonly placeHolderMessage = '[No EC2 Instances Found]' protected ec2InstanceNodes: Map public override readonly contextValue: string = parentContextValue - protected pollingNodes: Set = new Set() + protected pollingNodes: Set = new Set() private pollTimer?: NodeJS.Timeout public constructor( @@ -60,34 +59,23 @@ export class Ec2ParentNode extends AWSTreeNodeBase { return this.pollingNodes.size !== 0 } - public startPolling(instanceId: string) { - this.pollingNodes.add(instanceId) + public startPolling(childNode: Ec2InstanceNode) { + this.pollingNodes.add(childNode) this.pollTimer = this.pollTimer ?? globals.clock.setInterval(this.updatePollingNodes.bind(this), pollingInterval) } public updatePollingNodes() { - console.log('here') - this.pollingNodes.forEach(async value => { - const instanceNode = this.getChild(value) - await instanceNode.updateStatus() + this.pollingNodes.forEach(async childNode => { + await childNode.updateStatus() - if (instanceNode.status != 'pending') { - this.pollingNodes.delete(value) + if (childNode.getStatus() != 'pending') { + this.pollingNodes.delete(childNode) } }) this.refreshNode() } - public getChild(instanceId: string): Ec2InstanceNode { - if (!this.ec2InstanceNodes.has(instanceId)) { - throw new ToolkitError(`Unable to retrieve child instance requested with id: ${instanceId}`, { - code: 'MissingChild', - }) - } - return this.ec2InstanceNodes.get(instanceId)! - } - public async clearChildren() { this.ec2InstanceNodes = new Map() } diff --git a/src/test/ec2/explorer/ec2ParentNode.test.ts b/src/test/ec2/explorer/ec2ParentNode.test.ts index 8e7d6df18c5..4de07605571 100644 --- a/src/test/ec2/explorer/ec2ParentNode.test.ts +++ b/src/test/ec2/explorer/ec2ParentNode.test.ts @@ -6,7 +6,6 @@ import * as assert from 'assert' import * as FakeTimers from '@sinonjs/fake-timers' import * as sinon from 'sinon' -import { verify, anything, instance, mock, when } from 'ts-mockito' import { Ec2ParentNode } from '../../../ec2/explorer/ec2ParentNode' import { stub } from '../../utilities/stubber' import { Ec2Client, Ec2Instance } from '../../../shared/clients/ec2Client' @@ -17,7 +16,7 @@ import { } from '../../utilities/explorerNodeAssertions' import { Ec2InstanceNode } from '../../../ec2/explorer/ec2InstanceNode' import { installFakeClock } from '../../testUtil' -import { AWSTreeNodeBase } from '../../../shared/treeview/nodes/awsTreeNodeBase' +import { mock, when } from 'ts-mockito' describe('ec2ParentNode', function () { let testNode: Ec2ParentNode @@ -113,4 +112,19 @@ describe('ec2ParentNode', function () { it('is not polling on initialization', async function () { assert.strictEqual(testNode.isPolling(), false) }) + + it('updates the nodes when the timer is up', async function () { + const mockChildNode: Ec2InstanceNode = mock() + testNode.startPolling(mockChildNode) + await clock.tickAsync(4000) + sinon.assert.calledOn(refreshStub, testNode) + }) + + it('deletes node from polling set when state changes', async function () { + const mockChildNode: Ec2InstanceNode = mock() + when(mockChildNode.getStatus()).thenReturn('running') + testNode.startPolling(mockChildNode) + await clock.tickAsync(4000) + assert.strictEqual(testNode.isPolling(), false) + }) }) From a5cf7f3fa2c7770787a8cff81e8c3685a99b1b41 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 17 Jul 2023 16:58:46 -0700 Subject: [PATCH 076/172] switch back to using instanceids to add more testing --- src/ec2/explorer/ec2InstanceNode.ts | 8 +++++ src/ec2/explorer/ec2ParentNode.ts | 15 ++++---- src/test/ec2/explorer/ec2ParentNode.test.ts | 38 ++++++++++++++------- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/ec2/explorer/ec2InstanceNode.ts b/src/ec2/explorer/ec2InstanceNode.ts index aaef9f06243..15f0a56d85a 100644 --- a/src/ec2/explorer/ec2InstanceNode.ts +++ b/src/ec2/explorer/ec2InstanceNode.ts @@ -35,6 +35,14 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode this.contextValue = this.getContext() this.iconPath = new vscode.ThemeIcon(getIconCodeForInstanceStatus(this.instance)) this.tooltip = `${this.name}\n${this.InstanceId}\n${this.instance.status}\n${this.arn}` + + if (this.isPending()) { + this.parent.startPolling(this.InstanceId) + } + } + + public isPending(): boolean { + return this.getStatus() != 'running' && this.getStatus() != 'stopped' } public async updateStatus() { diff --git a/src/ec2/explorer/ec2ParentNode.ts b/src/ec2/explorer/ec2ParentNode.ts index 359bb0cf407..f8a503f45a3 100644 --- a/src/ec2/explorer/ec2ParentNode.ts +++ b/src/ec2/explorer/ec2ParentNode.ts @@ -15,13 +15,13 @@ import globals from '../../shared/extensionGlobals' export const parentContextValue = 'awsEc2ParentNode' export type Ec2Node = Ec2InstanceNode | Ec2ParentNode -const pollingInterval = 3000 +const pollingInterval = 5000 export class Ec2ParentNode extends AWSTreeNodeBase { protected readonly placeHolderMessage = '[No EC2 Instances Found]' protected ec2InstanceNodes: Map public override readonly contextValue: string = parentContextValue - protected pollingNodes: Set = new Set() + public pollingNodes: Set = new Set() private pollTimer?: NodeJS.Timeout public constructor( @@ -59,18 +59,19 @@ export class Ec2ParentNode extends AWSTreeNodeBase { return this.pollingNodes.size !== 0 } - public startPolling(childNode: Ec2InstanceNode) { - this.pollingNodes.add(childNode) + public startPolling(newNode: string) { + this.pollingNodes.add(newNode) this.pollTimer = this.pollTimer ?? globals.clock.setInterval(this.updatePollingNodes.bind(this), pollingInterval) } public updatePollingNodes() { - this.pollingNodes.forEach(async childNode => { + this.pollingNodes.forEach(async instanceId => { + const childNode = this.ec2InstanceNodes.get(instanceId)! await childNode.updateStatus() - if (childNode.getStatus() != 'pending') { - this.pollingNodes.delete(childNode) + if (!childNode.isPending()) { + this.pollingNodes.delete(instanceId) } }) this.refreshNode() diff --git a/src/test/ec2/explorer/ec2ParentNode.test.ts b/src/test/ec2/explorer/ec2ParentNode.test.ts index 4de07605571..ec022a1c327 100644 --- a/src/test/ec2/explorer/ec2ParentNode.test.ts +++ b/src/test/ec2/explorer/ec2ParentNode.test.ts @@ -16,7 +16,6 @@ import { } from '../../utilities/explorerNodeAssertions' import { Ec2InstanceNode } from '../../../ec2/explorer/ec2InstanceNode' import { installFakeClock } from '../../testUtil' -import { mock, when } from 'ts-mockito' describe('ec2ParentNode', function () { let testNode: Ec2ParentNode @@ -33,6 +32,7 @@ describe('ec2ParentNode', function () { intoCollection( instances.map(instance => ({ InstanceId: instance.InstanceId, + status: instance.status, Tags: [{ Key: 'Name', Value: instance.name }], })) ) @@ -113,18 +113,32 @@ describe('ec2ParentNode', function () { assert.strictEqual(testNode.isPolling(), false) }) - it('updates the nodes when the timer is up', async function () { - const mockChildNode: Ec2InstanceNode = mock() - testNode.startPolling(mockChildNode) - await clock.tickAsync(4000) - sinon.assert.calledOn(refreshStub, testNode) + it('adds pending nodes to the polling nodes set', async function () { + await testNode.updateChildren() + assert.strictEqual(testNode.pollingNodes.size, 1) }) - it('deletes node from polling set when state changes', async function () { - const mockChildNode: Ec2InstanceNode = mock() - when(mockChildNode.getStatus()).thenReturn('running') - testNode.startPolling(mockChildNode) - await clock.tickAsync(4000) - assert.strictEqual(testNode.isPolling(), false) + it('refreshes explorer when timer goes off', async function () { + await testNode.updateChildren() + await clock.tickAsync(6000) + sinon.assert.calledOn(refreshStub, testNode) }) + + // it('deletes node from polling set when state changes', async function () { + // const mockChildNode: Ec2InstanceNode = mock() + // when(mockChildNode.getStatus()).thenReturn('running') + // testNode.startPolling(mockChildNode) + // await clock.tickAsync(4000) + // assert.strictEqual(testNode.isPolling(), false) + // }) + + // it('stops polling once node status has been updated', async function () { + // const mockChildNode: Ec2InstanceNode = mock() + // when(mockChildNode.getStatus()).thenReturn('running') + // testNode.startPolling(mockChildNode) + // await clock.tickAsync(4000) + // sinon.assert.calledOn(refreshStub, testNode) + // await clock.tickAsync(4000) + // sinon.assert.calledOnce(refreshStub) + // }) }) From 177aa07e761cc8d7464193e89d530247120f0fb8 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 17 Jul 2023 17:07:14 -0700 Subject: [PATCH 077/172] ensure timer stops with tests --- src/ec2/explorer/ec2ParentNode.ts | 8 ++++++ src/test/ec2/explorer/ec2ParentNode.test.ts | 27 ++++++++------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/ec2/explorer/ec2ParentNode.ts b/src/ec2/explorer/ec2ParentNode.ts index f8a503f45a3..d249dfff26b 100644 --- a/src/ec2/explorer/ec2ParentNode.ts +++ b/src/ec2/explorer/ec2ParentNode.ts @@ -75,6 +75,14 @@ export class Ec2ParentNode extends AWSTreeNodeBase { } }) this.refreshNode() + if (!this.isPolling()) { + this.clearPollTimer() + } + } + + public clearPollTimer() { + globals.clock.clearInterval(this.pollTimer!) + this.pollTimer = undefined } public async clearChildren() { diff --git a/src/test/ec2/explorer/ec2ParentNode.test.ts b/src/test/ec2/explorer/ec2ParentNode.test.ts index ec022a1c327..b9dd926fb97 100644 --- a/src/test/ec2/explorer/ec2ParentNode.test.ts +++ b/src/test/ec2/explorer/ec2ParentNode.test.ts @@ -22,6 +22,7 @@ describe('ec2ParentNode', function () { let instances: Ec2Instance[] let clock: FakeTimers.InstalledClock let refreshStub: sinon.SinonStub<[], Promise> + let clearTimerStub: sinon.SinonStub<[], void> const testRegion = 'testRegion' const testPartition = 'testPartition' @@ -44,6 +45,7 @@ describe('ec2ParentNode', function () { before(function () { clock = installFakeClock() refreshStub = sinon.stub(Ec2ParentNode.prototype, 'refreshNode') + clearTimerStub = sinon.stub(Ec2ParentNode.prototype, 'clearPollTimer') }) beforeEach(function () { @@ -124,21 +126,12 @@ describe('ec2ParentNode', function () { sinon.assert.calledOn(refreshStub, testNode) }) - // it('deletes node from polling set when state changes', async function () { - // const mockChildNode: Ec2InstanceNode = mock() - // when(mockChildNode.getStatus()).thenReturn('running') - // testNode.startPolling(mockChildNode) - // await clock.tickAsync(4000) - // assert.strictEqual(testNode.isPolling(), false) - // }) - - // it('stops polling once node status has been updated', async function () { - // const mockChildNode: Ec2InstanceNode = mock() - // when(mockChildNode.getStatus()).thenReturn('running') - // testNode.startPolling(mockChildNode) - // await clock.tickAsync(4000) - // sinon.assert.calledOn(refreshStub, testNode) - // await clock.tickAsync(4000) - // sinon.assert.calledOnce(refreshStub) - // }) + it('stops timer once polling nodes are empty', async function () { + await testNode.updateChildren() + assert.strictEqual(testNode.isPolling(), true) + testNode.pollingNodes.delete('0') + await clock.tickAsync(6000) + assert.strictEqual(testNode.isPolling(), false) + sinon.assert.calledOn(clearTimerStub, testNode) + }) }) From dd82ff92e2ead29b7cda7678fdaf7496780daef5 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 18 Jul 2023 08:29:00 -0700 Subject: [PATCH 078/172] refactor tests to avoid polling before tests start --- src/test/ec2/explorer/ec2ParentNode.test.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/test/ec2/explorer/ec2ParentNode.test.ts b/src/test/ec2/explorer/ec2ParentNode.test.ts index b9dd926fb97..1ea426a53c1 100644 --- a/src/test/ec2/explorer/ec2ParentNode.test.ts +++ b/src/test/ec2/explorer/ec2ParentNode.test.ts @@ -50,11 +50,12 @@ describe('ec2ParentNode', function () { beforeEach(function () { instances = [ - { name: 'firstOne', InstanceId: '0', status: 'pending' }, + { name: 'firstOne', InstanceId: '0', status: 'running' }, { name: 'secondOne', InstanceId: '1', status: 'stopped' }, ] testNode = new Ec2ParentNode(testRegion, testPartition, createClient()) + refreshStub.resetHistory() }) it('returns placeholder node if no children are present', async function () { @@ -116,18 +117,34 @@ describe('ec2ParentNode', function () { }) it('adds pending nodes to the polling nodes set', async function () { + instances = [ + { name: 'firstOne', InstanceId: '0', status: 'pending' }, + { name: 'secondOne', InstanceId: '1', status: 'stopped' }, + { name: 'thirdOne', InstanceId: '2', status: 'running' }, + ] await testNode.updateChildren() assert.strictEqual(testNode.pollingNodes.size, 1) }) it('refreshes explorer when timer goes off', async function () { + instances = [ + { name: 'firstOne', InstanceId: '0', status: 'pending' }, + { name: 'secondOne', InstanceId: '1', status: 'stopped' }, + { name: 'thirdOne', InstanceId: '2', status: 'running' }, + ] await testNode.updateChildren() await clock.tickAsync(6000) sinon.assert.calledOn(refreshStub, testNode) }) it('stops timer once polling nodes are empty', async function () { + instances = [ + { name: 'firstOne', InstanceId: '0', status: 'pending' }, + { name: 'secondOne', InstanceId: '1', status: 'stopped' }, + { name: 'thirdOne', InstanceId: '2', status: 'running' }, + ] await testNode.updateChildren() + assert.strictEqual(testNode.isPolling(), true) testNode.pollingNodes.delete('0') await clock.tickAsync(6000) From 76eb0fd56664db7447797ea7c82683363ff8a8ed Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 18 Jul 2023 10:28:24 -0700 Subject: [PATCH 079/172] refactor such that only instance node is refreshed --- src/ec2/activation.ts | 6 +++--- src/ec2/commands.ts | 6 ++---- src/ec2/explorer/ec2InstanceNode.ts | 6 ++++++ src/ec2/explorer/ec2ParentNode.ts | 9 ++++++--- src/test/ec2/explorer/ec2ParentNode.test.ts | 17 ++++++++++++++--- 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/ec2/activation.ts b/src/ec2/activation.ts index 5f10ea9701d..100231751f7 100644 --- a/src/ec2/activation.ts +++ b/src/ec2/activation.ts @@ -30,17 +30,17 @@ export async function activate(ctx: ExtContext): Promise { Commands.register('aws.ec2.startInstance', async (node?: Ec2Node) => { await startInstance(node) - refreshExplorer(node) + await refreshExplorer(node) }), Commands.register('aws.ec2.stopInstance', async (node?: Ec2Node) => { await stopInstance(node) - refreshExplorer(node) + await refreshExplorer(node) }), Commands.register('aws.ec2.rebootInstance', async (node?: Ec2Node) => { await rebootInstance(node) - refreshExplorer(node) + await refreshExplorer(node) }) ) } diff --git a/src/ec2/commands.ts b/src/ec2/commands.ts index 0ba9bec259c..c6cc9b3ba74 100644 --- a/src/ec2/commands.ts +++ b/src/ec2/commands.ts @@ -11,10 +11,8 @@ import { Ec2Prompter, instanceFilter } from './prompter' import { Ec2Selection } from './utils' import { Ec2Instance } from '../shared/clients/ec2Client' -export function refreshExplorer(node?: Ec2Node) { - if (node) { - node instanceof Ec2InstanceNode ? node.parent.refreshNode() : node.refreshNode() - } +export async function refreshExplorer(node?: Ec2Node) { + await node?.refreshNode() } export async function openTerminal(node?: Ec2Node) { diff --git a/src/ec2/explorer/ec2InstanceNode.ts b/src/ec2/explorer/ec2InstanceNode.ts index 15f0a56d85a..5f610552b4f 100644 --- a/src/ec2/explorer/ec2InstanceNode.ts +++ b/src/ec2/explorer/ec2InstanceNode.ts @@ -10,6 +10,7 @@ import { Ec2Instance } from '../../shared/clients/ec2Client' import globals from '../../shared/extensionGlobals' import { Ec2Selection, getIconCodeForInstanceStatus } from '../utils' import { Ec2ParentNode } from './ec2ParentNode' +import { Commands } from '../../shared/vscode/commands' export const Ec2InstanceRunningContext = 'awsEc2RunningNode' export const Ec2InstanceStoppedContext = 'awsEc2StoppedNode' @@ -90,4 +91,9 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode this.regionCode }:${globals.awsContext.getCredentialAccountId()}:instance/${this.InstanceId}` } + + public async refreshNode(): Promise { + await this.updateStatus() + Commands.vscode().execute('aws.refreshAwsExplorerNode', this) + } } diff --git a/src/ec2/explorer/ec2ParentNode.ts b/src/ec2/explorer/ec2ParentNode.ts index d249dfff26b..6286eb60359 100644 --- a/src/ec2/explorer/ec2ParentNode.ts +++ b/src/ec2/explorer/ec2ParentNode.ts @@ -65,16 +65,19 @@ export class Ec2ParentNode extends AWSTreeNodeBase { this.pollTimer ?? globals.clock.setInterval(this.updatePollingNodes.bind(this), pollingInterval) } - public updatePollingNodes() { + private checkForPendingNodes() { this.pollingNodes.forEach(async instanceId => { const childNode = this.ec2InstanceNodes.get(instanceId)! await childNode.updateStatus() - if (!childNode.isPending()) { this.pollingNodes.delete(instanceId) + childNode.refreshNode() } }) - this.refreshNode() + } + + public updatePollingNodes() { + this.checkForPendingNodes() if (!this.isPolling()) { this.clearPollTimer() } diff --git a/src/test/ec2/explorer/ec2ParentNode.test.ts b/src/test/ec2/explorer/ec2ParentNode.test.ts index 1ea426a53c1..c50e203b9e7 100644 --- a/src/test/ec2/explorer/ec2ParentNode.test.ts +++ b/src/test/ec2/explorer/ec2ParentNode.test.ts @@ -23,6 +23,7 @@ describe('ec2ParentNode', function () { let clock: FakeTimers.InstalledClock let refreshStub: sinon.SinonStub<[], Promise> let clearTimerStub: sinon.SinonStub<[], void> + let statusUpdateFromClient: string const testRegion = 'testRegion' const testPartition = 'testPartition' @@ -38,13 +39,14 @@ describe('ec2ParentNode', function () { })) ) ) + client.getInstanceStatus.callsFake(async () => statusUpdateFromClient) return client } before(function () { clock = installFakeClock() - refreshStub = sinon.stub(Ec2ParentNode.prototype, 'refreshNode') + refreshStub = sinon.stub(Ec2InstanceNode.prototype, 'refreshNode') clearTimerStub = sinon.stub(Ec2ParentNode.prototype, 'clearPollTimer') }) @@ -126,7 +128,8 @@ describe('ec2ParentNode', function () { assert.strictEqual(testNode.pollingNodes.size, 1) }) - it('refreshes explorer when timer goes off', async function () { + it('does not refresh explorer when timer goes off if status unchanged', async function () { + statusUpdateFromClient = 'pending' instances = [ { name: 'firstOne', InstanceId: '0', status: 'pending' }, { name: 'secondOne', InstanceId: '1', status: 'stopped' }, @@ -134,7 +137,15 @@ describe('ec2ParentNode', function () { ] await testNode.updateChildren() await clock.tickAsync(6000) - sinon.assert.calledOn(refreshStub, testNode) + sinon.assert.notCalled(refreshStub) + }) + + it('does refresh explorer when timer goes and status changed', async function () { + sinon.assert.notCalled(refreshStub) + statusUpdateFromClient = 'running' + testNode.pollingNodes.add('0') + await clock.tickAsync(6000) + sinon.assert.called(refreshStub) }) it('stops timer once polling nodes are empty', async function () { From 5ad533f4969bdecd92ed63cbf555a076ccebbbc7 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 18 Jul 2023 10:35:02 -0700 Subject: [PATCH 080/172] no stopping/starting on pending instances --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 9b419fcefe0..66eda712737 100644 --- a/package.json +++ b/package.json @@ -1325,22 +1325,22 @@ { "command": "aws.ec2.startInstance", "group": "0@1", - "when": "viewItem =~ /^(awsEc2(Stopped|Pending)Node)$/" + "when": "viewItem =~ /^(awsEc2StoppedNode)$/" }, { "command": "aws.ec2.startInstance", "group": "inline@1", - "when": "viewItem =~ /^(awsEc2(Stopped|Pending)Node)$/" + "when": "viewItem =~ /^(awsEc2StoppedNode)$/" }, { "command": "aws.ec2.stopInstance", "group": "0@1", - "when": "viewItem =~ /^(awsEc2(Running|Pending)Node)$/" + "when": "viewItem =~ /^(awsEc2RunningNode)$/" }, { "command": "aws.ec2.stopInstance", "group": "inline@1", - "when": "viewItem =~ /^(awsEc2(Running|Pending)Node)$/" + "when": "viewItem =~ /^(awsEc2RunningNode)$/" }, { "command": "aws.ec2.rebootInstance", From 6efd4fd7f3659ad1e6c0c48c7ad2724c6c6e4b61 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 18 Jul 2023 12:04:09 -0700 Subject: [PATCH 081/172] refactor sshConfig changes to their own new file --- src/codecatalyst/tools.ts | 2 +- src/ec2/tools.ts | 2 +- src/shared/extensions/ssh.ts | 137 +--------------- src/shared/sshConfig.ts | 148 ++++++++++++++++++ .../ssh.test.ts => sshConfig.test.ts} | 8 +- 5 files changed, 155 insertions(+), 142 deletions(-) create mode 100644 src/shared/sshConfig.ts rename src/test/shared/{extensions/ssh.test.ts => sshConfig.test.ts} (94%) diff --git a/src/codecatalyst/tools.ts b/src/codecatalyst/tools.ts index 8ca6861e38f..f66826b9fe2 100644 --- a/src/codecatalyst/tools.ts +++ b/src/codecatalyst/tools.ts @@ -14,7 +14,7 @@ import { Result } from '../shared/utilities/result' import { fileExists, readFileAsString } from '../shared/filesystemUtilities' import { ToolkitError } from '../shared/errors' import { getLogger } from '../shared/logger' -import { VscodeRemoteSshConfig } from '../shared/extensions/ssh' +import { VscodeRemoteSshConfig } from '../shared/sshConfig' export async function ensureConnectScript(context = globals.context): Promise> { const scriptName = `codecatalyst_connect${process.platform === 'win32' ? '.ps1' : ''}` diff --git a/src/ec2/tools.ts b/src/ec2/tools.ts index 1b988531329..63c978486a2 100644 --- a/src/ec2/tools.ts +++ b/src/ec2/tools.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { VscodeRemoteSshConfig } from '../shared/extensions/ssh' +import { VscodeRemoteSshConfig } from '../shared/sshConfig' import { Result } from '../shared/utilities/result' export class Ec2RemoteSshConfig extends VscodeRemoteSshConfig { diff --git a/src/shared/extensions/ssh.ts b/src/shared/extensions/ssh.ts index e7deb13a0d3..9e8584dab07 100644 --- a/src/shared/extensions/ssh.ts +++ b/src/shared/extensions/ssh.ts @@ -6,18 +6,12 @@ import * as vscode from 'vscode' import * as path from 'path' import * as nls from 'vscode-nls' -import * as fs from 'fs-extra' import { getLogger } from '../logger' -import { ChildProcess, ChildProcessResult } from '../utilities/childProcess' +import { ChildProcess } from '../utilities/childProcess' import { SystemUtilities } from '../systemUtilities' import { ArrayConstructor, NonNullObject } from '../utilities/typeConstructors' import { Settings } from '../settings' import { VSCODE_EXTENSION_ID } from '../extensions' -import { Err, Ok, Result } from '../utilities/result' -import { ToolkitError } from '../errors' -import { getIdeProperties } from '../extensionUtilities' -import { showConfirmationMessage } from '../utilities/messages' -import { CancellationError } from '../utilities/timeoutUtils' const localize = nls.loadMessageBundle() @@ -149,132 +143,3 @@ export async function startVscodeRemote( await new ProcessClass(vscPath, ['--folder-uri', workspaceUri]).run() } - -export abstract class VscodeRemoteSshConfig { - protected readonly configHostName: string - protected abstract proxyCommandRegExp: RegExp - - public constructor(protected readonly sshPath: string, protected readonly hostNamePrefix: string) { - this.configHostName = `${hostNamePrefix}*` - } - - protected isWin(): boolean { - return process.platform === 'win32' - } - - protected async getProxyCommand(command: string): Promise> { - if (this.isWin()) { - // Some older versions of OpenSSH (7.8 and below) have a bug where attempting to use powershell.exe directly will fail without an absolute path - const proc = new ChildProcess('powershell.exe', ['-Command', '(get-command powershell.exe).Path']) - const r = await proc.run() - if (r.exitCode !== 0) { - return Result.err(new ToolkitError('Failed to get absolute path for powershell', { cause: r.error })) - } - return Result.ok(`"${r.stdout}" -ExecutionPolicy RemoteSigned -File "${command}" %h`) - } else { - return Result.ok(`'${command}' '%h'`) - } - } - - public abstract ensureValid(): Promise | Err | Ok> - - protected abstract createSSHConfigSection(proxyCommand: string): string - - protected async checkSshOnHost(): Promise { - const proc = new ChildProcess(this.sshPath, ['-G', `${this.hostNamePrefix}test`]) - const result = await proc.run() - return result - } - - protected async matchSshSection() { - const result = await this.checkSshOnHost() - if (result.exitCode !== 0) { - return Result.err(result.error ?? new Error(`ssh check against host failed: ${result.exitCode}`)) - } - const matches = result.stdout.match(this.proxyCommandRegExp) - return Result.ok(matches?.[0]) - } - - private async promptUserForOutdatedSection(configSection: string): Promise { - getLogger().warn( - `codecatalyst: SSH config: found old/outdated "${this.configHostName}" section:\n%O`, - configSection - ) - const oldConfig = localize( - 'AWS.codecatalyst.error.oldConfig', - 'Your ~/.ssh/config has a {0} section that might be out of date. Delete it, then try again.', - this.configHostName - ) - - const openConfig = localize('AWS.ssh.openConfig', 'Open config...') - vscode.window.showWarningMessage(oldConfig, openConfig).then(resp => { - if (resp === openConfig) { - vscode.window.showTextDocument(vscode.Uri.file(getSshConfigPath())) - } - }) - - throw new ToolkitError(oldConfig, { code: 'OldConfig' }) - } - - private async writeSectionToConfig(proxyCommand: string) { - const sshConfigPath = getSshConfigPath() - const section = this.createSSHConfigSection(proxyCommand) - try { - await fs.ensureDir(path.dirname(path.dirname(sshConfigPath)), { mode: 0o755 }) - await fs.ensureDir(path.dirname(sshConfigPath), 0o700) - await fs.appendFile(sshConfigPath, section, { mode: 0o600 }) - } catch (e) { - const message = localize( - 'AWS.codecatalyst.error.writeFail', - 'Failed to write SSH config: {0} (permission issue?)', - sshConfigPath - ) - - throw ToolkitError.chain(e, message, { code: 'ConfigWriteFailed' }) - } - } - - protected async promptUserToConfigureSshConfig( - configSection: string | undefined, - proxyCommand: string - ): Promise { - if (configSection !== undefined) { - await this.promptUserForOutdatedSection(configSection) - } - - const confirmTitle = localize( - 'AWS.codecatalyst.confirm.installSshConfig.title', - '{0} Toolkit will add host {1} to ~/.ssh/config to use SSH with your Dev Environments', - getIdeProperties().company, - this.configHostName - ) - const confirmText = localize('AWS.codecatalyst.confirm.installSshConfig.button', 'Update SSH config') - const response = await showConfirmationMessage({ prompt: confirmTitle, confirm: confirmText }) - if (!response) { - throw new CancellationError('user') - } - - await this.writeSectionToConfig(proxyCommand) - } - - // Check if the hostname pattern is working. - protected async verifySSHHost(proxyCommand: string) { - const matchResult = await this.matchSshSection() - if (matchResult.isErr()) { - return matchResult - } - - const configSection = matchResult.ok() - const hasProxyCommand = configSection?.includes(proxyCommand) - - if (!hasProxyCommand) { - try { - await this.promptUserToConfigureSshConfig(configSection, proxyCommand) - } catch (e) { - return Result.err(e as Error) - } - } - - return Result.ok() - } -} diff --git a/src/shared/sshConfig.ts b/src/shared/sshConfig.ts new file mode 100644 index 00000000000..9558de25442 --- /dev/null +++ b/src/shared/sshConfig.ts @@ -0,0 +1,148 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as path from 'path' +import * as fs from 'fs-extra' +import * as nls from 'vscode-nls' +import { getLogger } from './logger' +import { ChildProcess, ChildProcessResult } from './utilities/childProcess' +import { Err, Ok, Result } from './utilities/result' +import { ToolkitError } from './errors' +import { getIdeProperties } from './extensionUtilities' +import { showConfirmationMessage } from './utilities/messages' +import { CancellationError } from './utilities/timeoutUtils' +import { getSshConfigPath } from './extensions/ssh' + +const localize = nls.loadMessageBundle() + +export abstract class VscodeRemoteSshConfig { + protected readonly configHostName: string + protected abstract proxyCommandRegExp: RegExp + + public constructor(protected readonly sshPath: string, protected readonly hostNamePrefix: string) { + this.configHostName = `${hostNamePrefix}*` + } + + protected isWin(): boolean { + return process.platform === 'win32' + } + + protected async getProxyCommand(command: string): Promise> { + if (this.isWin()) { + // Some older versions of OpenSSH (7.8 and below) have a bug where attempting to use powershell.exe directly will fail without an absolute path + const proc = new ChildProcess('powershell.exe', ['-Command', '(get-command powershell.exe).Path']) + const r = await proc.run() + if (r.exitCode !== 0) { + return Result.err(new ToolkitError('Failed to get absolute path for powershell', { cause: r.error })) + } + return Result.ok(`"${r.stdout}" -ExecutionPolicy RemoteSigned -File "${command}" %h`) + } else { + return Result.ok(`'${command}' '%h'`) + } + } + + public abstract ensureValid(): Promise | Err | Ok> + + protected abstract createSSHConfigSection(proxyCommand: string): string + + protected async checkSshOnHost(): Promise { + const proc = new ChildProcess(this.sshPath, ['-G', `${this.hostNamePrefix}test`]) + const result = await proc.run() + return result + } + + protected async matchSshSection() { + const result = await this.checkSshOnHost() + if (result.exitCode !== 0) { + return Result.err(result.error ?? new Error(`ssh check against host failed: ${result.exitCode}`)) + } + const matches = result.stdout.match(this.proxyCommandRegExp) + return Result.ok(matches?.[0]) + } + + private async promptUserForOutdatedSection(configSection: string): Promise { + getLogger().warn( + `codecatalyst: SSH config: found old/outdated "${this.configHostName}" section:\n%O`, + configSection + ) + const oldConfig = localize( + 'AWS.codecatalyst.error.oldConfig', + 'Your ~/.ssh/config has a {0} section that might be out of date. Delete it, then try again.', + this.configHostName + ) + + const openConfig = localize('AWS.ssh.openConfig', 'Open config...') + vscode.window.showWarningMessage(oldConfig, openConfig).then(resp => { + if (resp === openConfig) { + vscode.window.showTextDocument(vscode.Uri.file(getSshConfigPath())) + } + }) + + throw new ToolkitError(oldConfig, { code: 'OldConfig' }) + } + + private async writeSectionToConfig(proxyCommand: string) { + const sshConfigPath = getSshConfigPath() + const section = this.createSSHConfigSection(proxyCommand) + try { + await fs.ensureDir(path.dirname(path.dirname(sshConfigPath)), { mode: 0o755 }) + await fs.ensureDir(path.dirname(sshConfigPath), 0o700) + await fs.appendFile(sshConfigPath, section, { mode: 0o600 }) + } catch (e) { + const message = localize( + 'AWS.codecatalyst.error.writeFail', + 'Failed to write SSH config: {0} (permission issue?)', + sshConfigPath + ) + + throw ToolkitError.chain(e, message, { code: 'ConfigWriteFailed' }) + } + } + + protected async promptUserToConfigureSshConfig( + configSection: string | undefined, + proxyCommand: string + ): Promise { + if (configSection !== undefined) { + await this.promptUserForOutdatedSection(configSection) + } + + const confirmTitle = localize( + 'AWS.codecatalyst.confirm.installSshConfig.title', + '{0} Toolkit will add host {1} to ~/.ssh/config to use SSH with your Dev Environments', + getIdeProperties().company, + this.configHostName + ) + const confirmText = localize('AWS.codecatalyst.confirm.installSshConfig.button', 'Update SSH config') + const response = await showConfirmationMessage({ prompt: confirmTitle, confirm: confirmText }) + if (!response) { + throw new CancellationError('user') + } + + await this.writeSectionToConfig(proxyCommand) + } + + // Check if the hostname pattern is working. + protected async verifySSHHost(proxyCommand: string) { + const matchResult = await this.matchSshSection() + if (matchResult.isErr()) { + return matchResult + } + + const configSection = matchResult.ok() + const hasProxyCommand = configSection?.includes(proxyCommand) + + if (!hasProxyCommand) { + try { + await this.promptUserToConfigureSshConfig(configSection, proxyCommand) + } catch (e) { + return Result.err(e as Error) + } + } + + return Result.ok() + } +} diff --git a/src/test/shared/extensions/ssh.test.ts b/src/test/shared/sshConfig.test.ts similarity index 94% rename from src/test/shared/extensions/ssh.test.ts rename to src/test/shared/sshConfig.test.ts index d5188847a4c..eb7112c4df4 100644 --- a/src/test/shared/extensions/ssh.test.ts +++ b/src/test/shared/sshConfig.test.ts @@ -3,10 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as assert from 'assert' -import { VscodeRemoteSshConfig } from '../../../shared/extensions/ssh' -import { ToolkitError } from '../../../shared/errors' -import { Err, Ok, Result } from '../../../shared/utilities/result' -import { ChildProcessResult } from '../../../shared/utilities/childProcess' +import { ToolkitError } from '../../shared/errors' +import { Err, Ok, Result } from '../../shared/utilities/result' +import { ChildProcessResult } from '../../shared/utilities/childProcess' +import { VscodeRemoteSshConfig } from '../../shared/sshConfig' const testCommand = 'run-thing' const testProxyCommand = `'${testCommand}' '%h'` From 4a68497f5824b1ee9fc8d06a71644aa4d9d2f7ed Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 18 Jul 2023 12:13:15 -0700 Subject: [PATCH 082/172] move the creation of ssh section to parent abstract class --- resources/ec2_connect | 118 ++++++++++++++++++++++++++++++++++++++ src/codecatalyst/tools.ts | 14 ----- src/ec2/tools.ts | 9 --- src/shared/sshConfig.ts | 16 +++++- 4 files changed, 132 insertions(+), 25 deletions(-) create mode 100755 resources/ec2_connect diff --git a/resources/ec2_connect b/resources/ec2_connect new file mode 100755 index 00000000000..7df88be4576 --- /dev/null +++ b/resources/ec2_connect @@ -0,0 +1,118 @@ +#!/usr/bin/env bash + +# Usage: +# When connecting to a dev environment +# AWS_REGION=… AWS_SSM_CLI=… CODECATALYST_ENDPOINT=… BEARER_TOKEN_LOCATION=… SPACE_NAME=… PROJECT_NAME=… DEVENV_ID=… ./codecatalyst_connect + +set -e +set -u + +_DATE_CMD=true + +if command > /dev/null 2>&1 -v date; then + _DATE_CMD=date +elif command > /dev/null 2>&1 -v /bin/date; then + _DATE_CMD=/bin/date +fi + +_log() { + echo "$("$_DATE_CMD" '+%Y/%m/%d %H:%M:%S')" "$@" >> "${LOG_FILE_LOCATION}" 2>&1 +} + +_require_nolog() { + if [ -z "${1:-}" ] || [ -z "${2:-}" ]; then + _log "error: missing required arg: $1" + exit 1 + fi +} + +_require() { + _require_nolog "$@" + _log "$1=$2" +} + +_parse_json_for_value() { + key=$1 + json=$2 + echo "$json" | grep -o "\"$key\":\"[^\"]*" | grep -o '[^"]*$' +} + +# Note: dev environment must have been previously started by VSCode Extension/CodeCatalyst +_start_dev_environment_session() { + # Function inputs + local CODECATALYST_ENDPOINT=$1 + local BEARER_TOKEN=$2 + local SPACE_NAME=$3 + local PROJECT_NAME=$4 + local DEVENV_ID=$5 + + # Local variables + local START_SESSION_PATH="/v1/spaces/$SPACE_NAME/projects/$PROJECT_NAME/devEnvironments/$DEVENV_ID/session" + local START_SESSION_QUERY=$( + cat << EOF +{ + "sessionConfiguration": { + "sessionType": "SSH" + } +} +EOF + ) + + curl -s -X PUT \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $BEARER_TOKEN" \ + -d "$START_SESSION_QUERY" \ + "$CODECATALYST_ENDPOINT$START_SESSION_PATH" +} + +_codecatalyst() { + # Function inputs + local AWS_SSM_CLI=$1 + local CODECATALYST_ENDPOINT=$2 + local BEARER_TOKEN=$3 + local SPACE_NAME=$4 + local PROJECT_NAME=$5 + local DEVENV_ID=$6 + local AWS_REGION=$7 + + # Local variables + local START_SESSION_RESPONSE + local STREAM_URL + local TOKEN + local SESSION + + START_SESSION_RESPONSE=$(_start_dev_environment_session "$CODECATALYST_ENDPOINT" "$BEARER_TOKEN" "$SPACE_NAME" "$PROJECT_NAME" "$DEVENV_ID") + + # Errors happen when you have invalid token etc + # ValidationExceptions happen when the devenv is not running + if [[ "$START_SESSION_RESPONSE" == *"errors"* || "$START_SESSION_RESPONSE" == *"ValidationException"* || "$START_SESSION_RESPONSE" == *"NotValidException"* ]]; then + _log "Failed to start the session with error:" "$START_SESSION_RESPONSE" + exit 1 + fi + + STREAM_URL=$(_parse_json_for_value "streamUrl" "$START_SESSION_RESPONSE") + TOKEN=$(_parse_json_for_value "tokenValue" "$START_SESSION_RESPONSE") + SESSION=$(_parse_json_for_value "sessionId" "$START_SESSION_RESPONSE") + + exec "$AWS_SSM_CLI" "{\"streamUrl\":\"$STREAM_URL\",\"tokenValue\":\"$TOKEN\",\"sessionId\":\"$SESSION\"}" "$AWS_REGION" "StartSession" +} + +_main() { + _log "==============================================================================" + + _require LOG_FILE_LOCATION "${LOG_FILE_LOCATION:-}" + _require AWS_REGION "${AWS_REGION:-}" + _require AWS_SSM_CLI "${AWS_SSM_CLI:-}" + + _require CODECATALYST_ENDPOINT "${CODECATALYST_ENDPOINT:-}" + _require BEARER_TOKEN_LOCATION "${BEARER_TOKEN_LOCATION:-}" + _require SPACE_NAME "${SPACE_NAME:-}" + _require PROJECT_NAME "${PROJECT_NAME:-}" + _require DEVENV_ID "${DEVENV_ID:-}" + + CACHED_BEARER_TOKEN=$(cat "$BEARER_TOKEN_LOCATION") + + _codecatalyst "$AWS_SSM_CLI" "$CODECATALYST_ENDPOINT" "$CACHED_BEARER_TOKEN" "$SPACE_NAME" "$PROJECT_NAME" "$DEVENV_ID" "$AWS_REGION" +} + +_main diff --git a/src/codecatalyst/tools.ts b/src/codecatalyst/tools.ts index f66826b9fe2..f70fea45d2d 100644 --- a/src/codecatalyst/tools.ts +++ b/src/codecatalyst/tools.ts @@ -70,18 +70,4 @@ export class CodeCatalystSshConfig extends VscodeRemoteSshConfig { return Result.ok() } - - public createSSHConfigSection(proxyCommand: string): string { - // "AddKeysToAgent" will automatically add keys used on the server to the local agent. If not set, then `ssh-add` - // must be done locally. It's mostly a convenience thing; private keys are _not_ shared with the server. - - return ` -# Created by AWS Toolkit for VSCode. https://github.com/aws/aws-toolkit-vscode -Host ${this.configHostName} - ForwardAgent yes - AddKeysToAgent yes - StrictHostKeyChecking accept-new - ProxyCommand ${proxyCommand} - ` - } } diff --git a/src/ec2/tools.ts b/src/ec2/tools.ts index 63c978486a2..5c7e6b7c85b 100644 --- a/src/ec2/tools.ts +++ b/src/ec2/tools.ts @@ -24,13 +24,4 @@ export class Ec2RemoteSshConfig extends VscodeRemoteSshConfig { return Result.ok() } - - protected override createSSHConfigSection(proxyCommand: string): string { - return ` -# Created by AWS Toolkit for VSCode. https://github.com/aws/aws-toolkit-vscode -Host i-* mi-* - User ${this.hostNamePrefix} - ProxyCommand sh -c \"${this.command}\" -` - } } diff --git a/src/shared/sshConfig.ts b/src/shared/sshConfig.ts index 9558de25442..9237d11f00a 100644 --- a/src/shared/sshConfig.ts +++ b/src/shared/sshConfig.ts @@ -46,8 +46,6 @@ export abstract class VscodeRemoteSshConfig { public abstract ensureValid(): Promise | Err | Ok> - protected abstract createSSHConfigSection(proxyCommand: string): string - protected async checkSshOnHost(): Promise { const proc = new ChildProcess(this.sshPath, ['-G', `${this.hostNamePrefix}test`]) const result = await proc.run() @@ -145,4 +143,18 @@ export abstract class VscodeRemoteSshConfig { return Result.ok() } + + protected createSSHConfigSection(proxyCommand: string): string { + // "AddKeysToAgent" will automatically add keys used on the server to the local agent. If not set, then `ssh-add` + // must be done locally. It's mostly a convenience thing; private keys are _not_ shared with the server. + + return ` +# Created by AWS Toolkit for VSCode. https://github.com/aws/aws-toolkit-vscode +Host ${this.configHostName} + ForwardAgent yes + AddKeysToAgent yes + StrictHostKeyChecking accept-new + ProxyCommand ${proxyCommand} + ` + } } From ba214118d71fa6591b2c0fa4c753d947c8d512de Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 18 Jul 2023 12:31:00 -0700 Subject: [PATCH 083/172] move ensureConnect to shared file --- src/codecatalyst/tools.ts | 40 +---------------------------- src/shared/sshConfig.ts | 30 ++++++++++++++++++++++ src/test/codecatalyst/tools.test.ts | 2 +- 3 files changed, 32 insertions(+), 40 deletions(-) diff --git a/src/codecatalyst/tools.ts b/src/codecatalyst/tools.ts index f70fea45d2d..e8c3a635ce8 100644 --- a/src/codecatalyst/tools.ts +++ b/src/codecatalyst/tools.ts @@ -3,46 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import globals from '../shared/extensionGlobals' - -import * as nls from 'vscode-nls' -const localize = nls.loadMessageBundle() - -import * as vscode from 'vscode' -import * as fs from 'fs-extra' import { Result } from '../shared/utilities/result' -import { fileExists, readFileAsString } from '../shared/filesystemUtilities' -import { ToolkitError } from '../shared/errors' -import { getLogger } from '../shared/logger' -import { VscodeRemoteSshConfig } from '../shared/sshConfig' - -export async function ensureConnectScript(context = globals.context): Promise> { - const scriptName = `codecatalyst_connect${process.platform === 'win32' ? '.ps1' : ''}` - - // Script resource path. Includes the Toolkit version string so it changes with each release. - const versionedScript = vscode.Uri.joinPath(context.extensionUri, 'resources', scriptName) - - // Copy to globalStorage to ensure a "stable" path (not influenced by Toolkit version string.) - const connectScript = vscode.Uri.joinPath(context.globalStorageUri, scriptName) - - try { - const exists = await fileExists(connectScript.fsPath) - const contents1 = await readFileAsString(versionedScript.fsPath) - const contents2 = exists ? await readFileAsString(connectScript.fsPath) : '' - const isOutdated = contents1 !== contents2 - - if (isOutdated) { - await fs.copyFile(versionedScript.fsPath, connectScript.fsPath) - getLogger().info('ssh: updated connect script') - } - - return Result.ok(connectScript) - } catch (e) { - const message = localize('AWS.codecatalyst.error.copyScript', 'Failed to update connect script') - - return Result.err(ToolkitError.chain(e, message, { code: 'ConnectScriptUpdateFailed' })) - } -} +import { VscodeRemoteSshConfig, ensureConnectScript } from '../shared/sshConfig' export class CodeCatalystSshConfig extends VscodeRemoteSshConfig { protected override readonly proxyCommandRegExp: RegExp = /proxycommand.{0,1024}codecatalyst_connect(.ps1)?.{0,99}/i diff --git a/src/shared/sshConfig.ts b/src/shared/sshConfig.ts index 9237d11f00a..7d825f57b5e 100644 --- a/src/shared/sshConfig.ts +++ b/src/shared/sshConfig.ts @@ -15,6 +15,8 @@ import { getIdeProperties } from './extensionUtilities' import { showConfirmationMessage } from './utilities/messages' import { CancellationError } from './utilities/timeoutUtils' import { getSshConfigPath } from './extensions/ssh' +import globals from './extensionGlobals' +import { fileExists, readFileAsString } from './filesystemUtilities' const localize = nls.loadMessageBundle() @@ -158,3 +160,31 @@ Host ${this.configHostName} ` } } + +export async function ensureConnectScript(context = globals.context): Promise> { + const scriptName = `codecatalyst_connect${process.platform === 'win32' ? '.ps1' : ''}` + + // Script resource path. Includes the Toolkit version string so it changes with each release. + const versionedScript = vscode.Uri.joinPath(context.extensionUri, 'resources', scriptName) + + // Copy to globalStorage to ensure a "stable" path (not influenced by Toolkit version string.) + const connectScript = vscode.Uri.joinPath(context.globalStorageUri, scriptName) + + try { + const exists = await fileExists(connectScript.fsPath) + const contents1 = await readFileAsString(versionedScript.fsPath) + const contents2 = exists ? await readFileAsString(connectScript.fsPath) : '' + const isOutdated = contents1 !== contents2 + + if (isOutdated) { + await fs.copyFile(versionedScript.fsPath, connectScript.fsPath) + getLogger().info('ssh: updated connect script') + } + + return Result.ok(connectScript) + } catch (e) { + const message = localize('AWS.codecatalyst.error.copyScript', 'Failed to update connect script') + + return Result.err(ToolkitError.chain(e, message, { code: 'ConnectScriptUpdateFailed' })) + } +} diff --git a/src/test/codecatalyst/tools.test.ts b/src/test/codecatalyst/tools.test.ts index a99f4183f34..76214d4b9ab 100644 --- a/src/test/codecatalyst/tools.test.ts +++ b/src/test/codecatalyst/tools.test.ts @@ -12,7 +12,6 @@ import { fileExists, makeTemporaryToolkitFolder } from '../../shared/filesystemU import { ChildProcess } from '../../shared/utilities/childProcess' import { FakeExtensionContext } from '../fakeExtensionContext' import { startSshAgent } from '../../shared/extensions/ssh' -import { ensureConnectScript } from '../../codecatalyst/tools' import { bearerTokenCacheLocation, DevEnvironmentId, @@ -22,6 +21,7 @@ import { import { mkdir, readFile, writeFile } from 'fs-extra' import { StartDevEnvironmentSessionRequest } from 'aws-sdk/clients/codecatalyst' import { SystemUtilities } from '../../shared/systemUtilities' +import { ensureConnectScript } from '../../shared/sshConfig' describe('SSH Agent', function () { it('can start the agent on windows', async function () { From 5ec5496c78997ebc63ea4721aa8cad3988ef7393 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 18 Jul 2023 12:39:34 -0700 Subject: [PATCH 084/172] move ensure valid method to shared file --- src/codecatalyst/tools.ts | 27 +-------------------------- src/shared/sshConfig.ts | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/src/codecatalyst/tools.ts b/src/codecatalyst/tools.ts index e8c3a635ce8..2931bb62c3f 100644 --- a/src/codecatalyst/tools.ts +++ b/src/codecatalyst/tools.ts @@ -3,33 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Result } from '../shared/utilities/result' -import { VscodeRemoteSshConfig, ensureConnectScript } from '../shared/sshConfig' +import { VscodeRemoteSshConfig } from '../shared/sshConfig' export class CodeCatalystSshConfig extends VscodeRemoteSshConfig { protected override readonly proxyCommandRegExp: RegExp = /proxycommand.{0,1024}codecatalyst_connect(.ps1)?.{0,99}/i - /** - * Checks if the "aws-devenv-*" SSH config hostname pattern is working, else prompts user to add it. - * - * @returns Result object indicating whether the SSH config is working, or failure reason. - */ - public override async ensureValid() { - const scriptResult = await ensureConnectScript() - if (scriptResult.isErr()) { - return scriptResult - } - - const connectScript = scriptResult.ok() - const proxyCommand = await this.getProxyCommand(connectScript.fsPath) - if (proxyCommand.isErr()) { - return proxyCommand - } - - const verifyHost = await this.verifySSHHost(proxyCommand.unwrap()) - if (verifyHost.isErr()) { - return verifyHost - } - - return Result.ok() - } } diff --git a/src/shared/sshConfig.ts b/src/shared/sshConfig.ts index 7d825f57b5e..84d29778bed 100644 --- a/src/shared/sshConfig.ts +++ b/src/shared/sshConfig.ts @@ -9,7 +9,7 @@ import * as fs from 'fs-extra' import * as nls from 'vscode-nls' import { getLogger } from './logger' import { ChildProcess, ChildProcessResult } from './utilities/childProcess' -import { Err, Ok, Result } from './utilities/result' +import { Result } from './utilities/result' import { ToolkitError } from './errors' import { getIdeProperties } from './extensionUtilities' import { showConfirmationMessage } from './utilities/messages' @@ -46,7 +46,25 @@ export abstract class VscodeRemoteSshConfig { } } - public abstract ensureValid(): Promise | Err | Ok> + public async ensureValid() { + const scriptResult = await ensureConnectScript() + if (scriptResult.isErr()) { + return scriptResult + } + + const connectScript = scriptResult.ok() + const proxyCommand = await this.getProxyCommand(connectScript.fsPath) + if (proxyCommand.isErr()) { + return proxyCommand + } + + const verifyHost = await this.verifySSHHost(proxyCommand.unwrap()) + if (verifyHost.isErr()) { + return verifyHost + } + + return Result.ok() + } protected async checkSshOnHost(): Promise { const proc = new ChildProcess(this.sshPath, ['-G', `${this.hostNamePrefix}test`]) From 6c03515e8b3cff6f096bc9f5877d1163148fff92 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 18 Jul 2023 12:42:33 -0700 Subject: [PATCH 085/172] remove sub-classes --- src/codecatalyst/model.ts | 4 ++-- src/codecatalyst/tools.ts | 10 ---------- src/ec2/model.ts | 4 ++-- src/ec2/tools.ts | 27 --------------------------- src/shared/sshConfig.ts | 4 ++-- src/test/shared/sshConfig.test.ts | 2 +- 6 files changed, 7 insertions(+), 44 deletions(-) delete mode 100644 src/codecatalyst/tools.ts delete mode 100644 src/ec2/tools.ts diff --git a/src/codecatalyst/model.ts b/src/codecatalyst/model.ts index 8f81a4668f1..95ec0f381c5 100644 --- a/src/codecatalyst/model.ts +++ b/src/codecatalyst/model.ts @@ -21,7 +21,6 @@ import { getCodeCatalystSpaceName, getCodeCatalystProjectName, getCodeCatalystDe import { writeFile } from 'fs-extra' import { sshAgentSocketVariable, startSshAgent, startVscodeRemote } from '../shared/extensions/ssh' import { ChildProcess } from '../shared/utilities/childProcess' -import { CodeCatalystSshConfig } from './tools' import { isDevenvVscode } from './utils' import { Timeout } from '../shared/utilities/timeoutUtils' import { Commands } from '../shared/vscode/commands2' @@ -31,6 +30,7 @@ import { CodeCatalystAuthenticationProvider } from './auth' import { ToolkitError } from '../shared/errors' import { Result } from '../shared/utilities/result' import { VscodeRemoteConnection, ensureDependencies } from '../shared/remoteSession' +import { VscodeRemoteSshConfig } from '../shared/sshConfig' export type DevEnvironmentId = Pick export const hostNamePrefix = 'aws-devenv-' @@ -212,7 +212,7 @@ export async function prepareDevEnvConnection( { topic, timeout }: { topic?: string; timeout?: Timeout } = {} ): Promise { const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() - const sshConfig = new CodeCatalystSshConfig(ssh, hostNamePrefix) + const sshConfig = new VscodeRemoteSshConfig(ssh, hostNamePrefix) const config = await sshConfig.ensureValid() if (config.isErr()) { diff --git a/src/codecatalyst/tools.ts b/src/codecatalyst/tools.ts deleted file mode 100644 index 2931bb62c3f..00000000000 --- a/src/codecatalyst/tools.ts +++ /dev/null @@ -1,10 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { VscodeRemoteSshConfig } from '../shared/sshConfig' - -export class CodeCatalystSshConfig extends VscodeRemoteSshConfig { - protected override readonly proxyCommandRegExp: RegExp = /proxycommand.{0,1024}codecatalyst_connect(.ps1)?.{0,99}/i -} diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 0b4d50a0bb8..ed1ea46891f 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -19,7 +19,7 @@ import { createBoundProcess } from '../codecatalyst/model' import { getLogger } from '../shared/logger/logger' import { Timeout } from '../shared/utilities/timeoutUtils' import { showMessageWithCancel } from '../shared/utilities/messages' -import { Ec2RemoteSshConfig } from './tools' +import { VscodeRemoteSshConfig } from '../shared/sshConfig' export type Ec2ConnectErrorCode = 'EC2SSMStatus' | 'EC2SSMPermission' | 'EC2SSMConnect' | 'EC2SSMAgentStatus' @@ -158,7 +158,7 @@ export class Ec2ConnectionManager { public async prepareEc2RemoteEnv(selection: Ec2Selection): Promise { const logger = this.configureRemoteConnectionLogger(selection.instanceId) const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() - const sshConfig = new Ec2RemoteSshConfig(ssh, 'ec2-user') + const sshConfig = new VscodeRemoteSshConfig(ssh, 'ec2-user') const config = await sshConfig.ensureValid() if (config.isErr()) { const err = config.err() diff --git a/src/ec2/tools.ts b/src/ec2/tools.ts deleted file mode 100644 index 5c7e6b7c85b..00000000000 --- a/src/ec2/tools.ts +++ /dev/null @@ -1,27 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { VscodeRemoteSshConfig } from '../shared/sshConfig' -import { Result } from '../shared/utilities/result' - -export class Ec2RemoteSshConfig extends VscodeRemoteSshConfig { - private readonly command: string = - "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'" - protected override proxyCommandRegExp = new RegExp(`/proxycommand.{0,1024}${this.command}.{0,99}/i`) - - public override async ensureValid() { - const proxyCommand = await this.getProxyCommand(this.command) - if (proxyCommand.isErr()) { - return proxyCommand - } - - const verifyHost = await this.verifySSHHost(proxyCommand.unwrap()) - if (verifyHost.isErr()) { - return verifyHost - } - - return Result.ok() - } -} diff --git a/src/shared/sshConfig.ts b/src/shared/sshConfig.ts index 84d29778bed..55d3a40e4a6 100644 --- a/src/shared/sshConfig.ts +++ b/src/shared/sshConfig.ts @@ -20,9 +20,9 @@ import { fileExists, readFileAsString } from './filesystemUtilities' const localize = nls.loadMessageBundle() -export abstract class VscodeRemoteSshConfig { +export class VscodeRemoteSshConfig { protected readonly configHostName: string - protected abstract proxyCommandRegExp: RegExp + protected readonly proxyCommandRegExp: RegExp = /proxycommand.{0,1024}codecatalyst_connect(.ps1)?.{0,99}/i public constructor(protected readonly sshPath: string, protected readonly hostNamePrefix: string) { this.configHostName = `${hostNamePrefix}*` diff --git a/src/test/shared/sshConfig.test.ts b/src/test/shared/sshConfig.test.ts index eb7112c4df4..12e2cb4bfdf 100644 --- a/src/test/shared/sshConfig.test.ts +++ b/src/test/shared/sshConfig.test.ts @@ -13,7 +13,7 @@ const testProxyCommand = `'${testCommand}' '%h'` class MockSshConfig extends VscodeRemoteSshConfig { private readonly testCommand: string = testCommand - protected readonly proxyCommandRegExp: RegExp = new RegExp(`${testProxyCommand}`) + protected override readonly proxyCommandRegExp: RegExp = new RegExp(`${testProxyCommand}`) // State variables to track logic flow. public testIsWin: boolean = false From 7e6c56e8745f44d6f19cb41dfa52b775bd48d5d9 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 18 Jul 2023 13:07:06 -0700 Subject: [PATCH 086/172] switch scriptPrefix to a parameter --- src/codecatalyst/model.ts | 3 ++- src/ec2/model.ts | 2 +- src/shared/sshConfig.ts | 19 +++++++++++++++---- src/test/codecatalyst/tools.test.ts | 9 +++++---- src/test/shared/sshConfig.test.ts | 2 +- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/codecatalyst/model.ts b/src/codecatalyst/model.ts index 95ec0f381c5..c64283160cd 100644 --- a/src/codecatalyst/model.ts +++ b/src/codecatalyst/model.ts @@ -34,6 +34,7 @@ import { VscodeRemoteSshConfig } from '../shared/sshConfig' export type DevEnvironmentId = Pick export const hostNamePrefix = 'aws-devenv-' +export const connectScriptPrefix = 'codecatalyst_connect' export const docs = { vscode: { @@ -212,7 +213,7 @@ export async function prepareDevEnvConnection( { topic, timeout }: { topic?: string; timeout?: Timeout } = {} ): Promise { const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() - const sshConfig = new VscodeRemoteSshConfig(ssh, hostNamePrefix) + const sshConfig = new VscodeRemoteSshConfig(ssh, hostNamePrefix, connectScriptPrefix) const config = await sshConfig.ensureValid() if (config.isErr()) { diff --git a/src/ec2/model.ts b/src/ec2/model.ts index ed1ea46891f..feaa6c3d405 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -158,7 +158,7 @@ export class Ec2ConnectionManager { public async prepareEc2RemoteEnv(selection: Ec2Selection): Promise { const logger = this.configureRemoteConnectionLogger(selection.instanceId) const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() - const sshConfig = new VscodeRemoteSshConfig(ssh, 'ec2-user') + const sshConfig = new VscodeRemoteSshConfig(ssh, 'ec2-user', 'ec2_connect') const config = await sshConfig.ensureValid() if (config.isErr()) { const err = config.err() diff --git a/src/shared/sshConfig.ts b/src/shared/sshConfig.ts index 55d3a40e4a6..7158011c56d 100644 --- a/src/shared/sshConfig.ts +++ b/src/shared/sshConfig.ts @@ -24,7 +24,11 @@ export class VscodeRemoteSshConfig { protected readonly configHostName: string protected readonly proxyCommandRegExp: RegExp = /proxycommand.{0,1024}codecatalyst_connect(.ps1)?.{0,99}/i - public constructor(protected readonly sshPath: string, protected readonly hostNamePrefix: string) { + public constructor( + protected readonly sshPath: string, + protected readonly hostNamePrefix: string, + protected readonly scriptPrefix: string + ) { this.configHostName = `${hostNamePrefix}*` } @@ -47,7 +51,7 @@ export class VscodeRemoteSshConfig { } public async ensureValid() { - const scriptResult = await ensureConnectScript() + const scriptResult = await ensureConnectScript(this.scriptPrefix) if (scriptResult.isErr()) { return scriptResult } @@ -179,8 +183,15 @@ Host ${this.configHostName} } } -export async function ensureConnectScript(context = globals.context): Promise> { - const scriptName = `codecatalyst_connect${process.platform === 'win32' ? '.ps1' : ''}` +export function constructScriptName(scriptPrefix: string) { + return `${scriptPrefix}${process.platform === 'win32' ? '.ps1' : ''}` +} + +export async function ensureConnectScript( + scriptPrefix: string, + context = globals.context +): Promise> { + const scriptName = constructScriptName(scriptPrefix) // Script resource path. Includes the Toolkit version string so it changes with each release. const versionedScript = vscode.Uri.joinPath(context.extensionUri, 'resources', scriptName) diff --git a/src/test/codecatalyst/tools.test.ts b/src/test/codecatalyst/tools.test.ts index 76214d4b9ab..e3e8ab3195b 100644 --- a/src/test/codecatalyst/tools.test.ts +++ b/src/test/codecatalyst/tools.test.ts @@ -14,6 +14,7 @@ import { FakeExtensionContext } from '../fakeExtensionContext' import { startSshAgent } from '../../shared/extensions/ssh' import { bearerTokenCacheLocation, + connectScriptPrefix, DevEnvironmentId, getCodeCatalystSsmEnv, sshLogFileLocation, @@ -61,7 +62,7 @@ describe('Connect Script', function () { }) it('can get a connect script path, adding a copy to global storage', async function () { - const script = (await ensureConnectScript(context)).unwrap().fsPath + const script = (await ensureConnectScript(connectScriptPrefix, context)).unwrap().fsPath assert.ok(await fileExists(script)) assert.ok(isWithin(context.globalStorageUri.fsPath, script)) }) @@ -115,7 +116,7 @@ describe('Connect Script', function () { }) await writeFile(bearerTokenCacheLocation(testDevEnv.id), 'token') - const script = (await ensureConnectScript(context)).unwrap().fsPath + const script = (await ensureConnectScript(connectScriptPrefix, context)).unwrap().fsPath const env = getCodeCatalystSsmEnv('us-weast-1', 'echo', testDevEnv) env.CODECATALYST_ENDPOINT = address @@ -147,12 +148,12 @@ describe('Connect Script', function () { }) it('works if the .ssh directory is missing', async function () { - ;(await ensureConnectScript(context)).unwrap() + ;(await ensureConnectScript(connectScriptPrefix, context)).unwrap() }) it('works if the .ssh directory exists but has different perms', async function () { await mkdir(path.join(tmpDir, '.ssh'), 0o777) - ;(await ensureConnectScript(context)).unwrap() + ;(await ensureConnectScript(connectScriptPrefix, context)).unwrap() }) }) }) diff --git a/src/test/shared/sshConfig.test.ts b/src/test/shared/sshConfig.test.ts index 12e2cb4bfdf..4dbfa603a40 100644 --- a/src/test/shared/sshConfig.test.ts +++ b/src/test/shared/sshConfig.test.ts @@ -80,7 +80,7 @@ class MockSshConfig extends VscodeRemoteSshConfig { describe('VscodeRemoteSshConfig', async function () { let config: MockSshConfig before(function () { - config = new MockSshConfig('sshPath', 'testHostNamePrefix') + config = new MockSshConfig('sshPath', 'testHostNamePrefix', 'scirpt') config.testIsWin = false }) From 19d3e2cd5d720a60d5b65c4703a00ad5fca29eb0 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 18 Jul 2023 13:10:57 -0700 Subject: [PATCH 087/172] use variable to name prefix of script --- src/ec2/model.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index feaa6c3d405..a9899da7835 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -27,6 +27,8 @@ interface Ec2RemoteEnv extends VscodeRemoteConnection { selection: Ec2Selection } +const ec2ConnectScriptPrefix = 'ec2_connect' + export class Ec2ConnectionManager { private ssmClient: SsmClient private ec2Client: Ec2Client @@ -158,7 +160,7 @@ export class Ec2ConnectionManager { public async prepareEc2RemoteEnv(selection: Ec2Selection): Promise { const logger = this.configureRemoteConnectionLogger(selection.instanceId) const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() - const sshConfig = new VscodeRemoteSshConfig(ssh, 'ec2-user', 'ec2_connect') + const sshConfig = new VscodeRemoteSshConfig(ssh, 'ec2-user', ec2ConnectScriptPrefix) const config = await sshConfig.ensureValid() if (config.isErr()) { const err = config.err() From 886a41ed5e2450e2dd51d1e1f717bbf2aca714f0 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 18 Jul 2023 13:17:04 -0700 Subject: [PATCH 088/172] add test for section created by sshConfig --- src/test/shared/sshConfig.test.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/test/shared/sshConfig.test.ts b/src/test/shared/sshConfig.test.ts index 4dbfa603a40..da1ce7c0a01 100644 --- a/src/test/shared/sshConfig.test.ts +++ b/src/test/shared/sshConfig.test.ts @@ -20,10 +20,6 @@ class MockSshConfig extends VscodeRemoteSshConfig { public configSection: string = '' public SshConfigWritten: boolean = false - protected override createSSHConfigSection(proxyCommand: string): string { - return this.configSection - } - public override async ensureValid(): Promise | Err | Ok> { const proxyCommand = await this.getProxyCommand(this.testCommand) if (proxyCommand.isErr()) { @@ -75,6 +71,10 @@ class MockSshConfig extends VscodeRemoteSshConfig { stderr: '', } } + + public createSSHConfigSectionWrapper(proxyCommand: string): string { + return this.createSSHConfigSection(proxyCommand) + } } describe('VscodeRemoteSshConfig', async function () { @@ -133,4 +133,12 @@ describe('VscodeRemoteSshConfig', async function () { assert.ok(!config.SshConfigWritten) }) }) + + describe('createSSHConfigSection', async function () { + it('section includes relevant script prefix', function () { + const testScriptName = 'testScript' + const section = config.createSSHConfigSectionWrapper(testScriptName) + assert.ok(section.includes(testScriptName)) + }) + }) }) From a4a4626759619a4ed0a61a102bc695afa1813f2e Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 18 Jul 2023 13:38:28 -0700 Subject: [PATCH 089/172] construct regexp from script name --- src/shared/sshConfig.ts | 3 ++- src/test/shared/sshConfig.test.ts | 20 ++++++++------------ 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/shared/sshConfig.ts b/src/shared/sshConfig.ts index 7158011c56d..8921673b38a 100644 --- a/src/shared/sshConfig.ts +++ b/src/shared/sshConfig.ts @@ -22,7 +22,7 @@ const localize = nls.loadMessageBundle() export class VscodeRemoteSshConfig { protected readonly configHostName: string - protected readonly proxyCommandRegExp: RegExp = /proxycommand.{0,1024}codecatalyst_connect(.ps1)?.{0,99}/i + protected readonly proxyCommandRegExp: RegExp public constructor( protected readonly sshPath: string, @@ -30,6 +30,7 @@ export class VscodeRemoteSshConfig { protected readonly scriptPrefix: string ) { this.configHostName = `${hostNamePrefix}*` + this.proxyCommandRegExp = new RegExp(`proxycommand.{0,1024}${scriptPrefix}(.ps1)?.{0,99}`) } protected isWin(): boolean { diff --git a/src/test/shared/sshConfig.test.ts b/src/test/shared/sshConfig.test.ts index da1ce7c0a01..fbae803e499 100644 --- a/src/test/shared/sshConfig.test.ts +++ b/src/test/shared/sshConfig.test.ts @@ -8,20 +8,14 @@ import { Err, Ok, Result } from '../../shared/utilities/result' import { ChildProcessResult } from '../../shared/utilities/childProcess' import { VscodeRemoteSshConfig } from '../../shared/sshConfig' -const testCommand = 'run-thing' -const testProxyCommand = `'${testCommand}' '%h'` - class MockSshConfig extends VscodeRemoteSshConfig { - private readonly testCommand: string = testCommand - protected override readonly proxyCommandRegExp: RegExp = new RegExp(`${testProxyCommand}`) - // State variables to track logic flow. public testIsWin: boolean = false public configSection: string = '' public SshConfigWritten: boolean = false public override async ensureValid(): Promise | Err | Ok> { - const proxyCommand = await this.getProxyCommand(this.testCommand) + const proxyCommand = await this.getProxyCommand(this.scriptPrefix) if (proxyCommand.isErr()) { return proxyCommand } @@ -79,8 +73,10 @@ class MockSshConfig extends VscodeRemoteSshConfig { describe('VscodeRemoteSshConfig', async function () { let config: MockSshConfig + const testCommand = 'test_connect' + const testProxyCommand = `'${testCommand}' '%h'` before(function () { - config = new MockSshConfig('sshPath', 'testHostNamePrefix', 'scirpt') + config = new MockSshConfig('sshPath', 'testHostNamePrefix', testCommand) config.testIsWin = false }) @@ -96,7 +92,7 @@ describe('VscodeRemoteSshConfig', async function () { describe('matchSshSection', async function () { it('returns ok with match when proxycommand is present', async function () { - const testSection = `fdsafdsafd${testProxyCommand}sa342432` + const testSection = `proxycommandfdsafdsafd${testProxyCommand}sa342432` const result = await config.testMatchSshSection(testSection) assert.ok(result.isOk()) const match = result.unwrap() @@ -119,15 +115,15 @@ describe('VscodeRemoteSshConfig', async function () { it('writes to ssh config if command not found.', async function () { const testSection = 'no-command-here' - const result = await config.testVerifySshHostWrapper(testProxyCommand, testSection) + const result = await config.testVerifySshHostWrapper(testCommand, testSection) assert.ok(result.isOk()) assert.ok(config.SshConfigWritten) }) it('does not write to ssh config if command is find', async function () { - const testSection = `this is some text that doesn't matter, but here ${testProxyCommand}` - const result = await config.testVerifySshHostWrapper(testProxyCommand, testSection) + const testSection = `this is some text that doesn't matter, but here proxycommand ${testProxyCommand}` + const result = await config.testVerifySshHostWrapper(testCommand, testSection) assert.ok(result.isOk()) assert.ok(!config.SshConfigWritten) From 8b36799acf1981bc50583e0682d16232bb1f499d Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 18 Jul 2023 13:50:20 -0700 Subject: [PATCH 090/172] move logFile location generator to general file --- src/codecatalyst/model.ts | 8 ++------ src/ec2/model.ts | 9 +++++---- src/shared/sshConfig.ts | 4 ++++ src/test/codecatalyst/tools.test.ts | 5 ++--- src/test/shared/sshConfig.test.ts | 14 +++++++++++++- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/codecatalyst/model.ts b/src/codecatalyst/model.ts index c64283160cd..1f8268c4569 100644 --- a/src/codecatalyst/model.ts +++ b/src/codecatalyst/model.ts @@ -30,7 +30,7 @@ import { CodeCatalystAuthenticationProvider } from './auth' import { ToolkitError } from '../shared/errors' import { Result } from '../shared/utilities/result' import { VscodeRemoteConnection, ensureDependencies } from '../shared/remoteSession' -import { VscodeRemoteSshConfig } from '../shared/sshConfig' +import { VscodeRemoteSshConfig, sshLogFileLocation } from '../shared/sshConfig' export type DevEnvironmentId = Pick export const hostNamePrefix = 'aws-devenv-' @@ -89,7 +89,7 @@ export function getCodeCatalystSsmEnv(region: string, ssmPath: string, devenv: D AWS_SSM_CLI: ssmPath, CODECATALYST_ENDPOINT: getCodeCatalystConfig().endpoint, BEARER_TOKEN_LOCATION: bearerTokenCacheLocation(devenv.id), - LOG_FILE_LOCATION: sshLogFileLocation(devenv.id), + LOG_FILE_LOCATION: sshLogFileLocation('codecatalyst', devenv.id), SPACE_NAME: devenv.org.name, PROJECT_NAME: devenv.project.name, DEVENV_ID: devenv.id, @@ -142,10 +142,6 @@ export function bearerTokenCacheLocation(devenvId: string): string { return path.join(globals.context.globalStorageUri.fsPath, `codecatalyst.${devenvId}.token`) } -export function sshLogFileLocation(devenvId: string): string { - return path.join(globals.context.globalStorageUri.fsPath, `codecatalyst.${devenvId}.log`) -} - export function getHostNameFromEnv(env: DevEnvironmentId): string { return `${hostNamePrefix}${env.id}` } diff --git a/src/ec2/model.ts b/src/ec2/model.ts index a9899da7835..2dd00074aa9 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -19,7 +19,7 @@ import { createBoundProcess } from '../codecatalyst/model' import { getLogger } from '../shared/logger/logger' import { Timeout } from '../shared/utilities/timeoutUtils' import { showMessageWithCancel } from '../shared/utilities/messages' -import { VscodeRemoteSshConfig } from '../shared/sshConfig' +import { VscodeRemoteSshConfig, sshLogFileLocation } from '../shared/sshConfig' export type Ec2ConnectErrorCode = 'EC2SSMStatus' | 'EC2SSMPermission' | 'EC2SSMConnect' | 'EC2SSMAgentStatus' @@ -169,7 +169,7 @@ export class Ec2ConnectionManager { throw err } - const vars = getEc2SsmEnv(selection.region, ssm) + const vars = getEc2SsmEnv(selection, ssm) const envProvider = async () => { return { [sshAgentSocketVariable]: await startSshAgent(), ...vars } } @@ -196,11 +196,12 @@ export class Ec2ConnectionManager { } } -function getEc2SsmEnv(region: string, ssmPath: string): NodeJS.ProcessEnv { +function getEc2SsmEnv(selection: Ec2Selection, ssmPath: string): NodeJS.ProcessEnv { return Object.assign( { - AWS_REGION: region, + AWS_REGION: selection.region, AWS_SSM_CLI: ssmPath, + LOG_FILE_LOCATION: sshLogFileLocation('ec2', selection.instanceId), }, process.env ) diff --git a/src/shared/sshConfig.ts b/src/shared/sshConfig.ts index 8921673b38a..60592a70195 100644 --- a/src/shared/sshConfig.ts +++ b/src/shared/sshConfig.ts @@ -184,6 +184,10 @@ Host ${this.configHostName} } } +export function sshLogFileLocation(service: string, id: string): string { + return path.join(globals.context.globalStorageUri.fsPath, `${service}.${id}.log`) +} + export function constructScriptName(scriptPrefix: string) { return `${scriptPrefix}${process.platform === 'win32' ? '.ps1' : ''}` } diff --git a/src/test/codecatalyst/tools.test.ts b/src/test/codecatalyst/tools.test.ts index e3e8ab3195b..2f9d36cb311 100644 --- a/src/test/codecatalyst/tools.test.ts +++ b/src/test/codecatalyst/tools.test.ts @@ -17,12 +17,11 @@ import { connectScriptPrefix, DevEnvironmentId, getCodeCatalystSsmEnv, - sshLogFileLocation, } from '../../codecatalyst/model' import { mkdir, readFile, writeFile } from 'fs-extra' import { StartDevEnvironmentSessionRequest } from 'aws-sdk/clients/codecatalyst' import { SystemUtilities } from '../../shared/systemUtilities' -import { ensureConnectScript } from '../../shared/sshConfig' +import { ensureConnectScript, sshLogFileLocation } from '../../shared/sshConfig' describe('SSH Agent', function () { it('can start the agent on windows', async function () { @@ -127,7 +126,7 @@ describe('Connect Script', function () { const output = await new ChildProcess(cmd, args).run({ spawnOptions: { env } }) if (output.exitCode !== 0) { - const logOutput = sshLogFileLocation(testDevEnv.id) + const logOutput = sshLogFileLocation('codecatalyst', testDevEnv.id) const message = `stderr:\n${output.stderr}\n\nlogs:\n${await readFile(logOutput)}` assert.fail(`Connect script should exit with a zero status:\n${message}`) diff --git a/src/test/shared/sshConfig.test.ts b/src/test/shared/sshConfig.test.ts index fbae803e499..bef06177dde 100644 --- a/src/test/shared/sshConfig.test.ts +++ b/src/test/shared/sshConfig.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert' import { ToolkitError } from '../../shared/errors' import { Err, Ok, Result } from '../../shared/utilities/result' import { ChildProcessResult } from '../../shared/utilities/childProcess' -import { VscodeRemoteSshConfig } from '../../shared/sshConfig' +import { VscodeRemoteSshConfig, sshLogFileLocation } from '../../shared/sshConfig' class MockSshConfig extends VscodeRemoteSshConfig { // State variables to track logic flow. @@ -137,4 +137,16 @@ describe('VscodeRemoteSshConfig', async function () { assert.ok(section.includes(testScriptName)) }) }) + + describe('sshLogFileLocation', async function () { + it('combines service and id into proper log file', function () { + const testService = 'testScript' + const testId = 'id' + const result = sshLogFileLocation(testService, testId) + + assert.ok(result.includes(testService)) + assert.ok(result.includes(testId)) + assert.ok(result.endsWith('.log')) + }) + }) }) From 72ae902c5987441c2add1d0baf9c1112234e4bf7 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 18 Jul 2023 13:59:49 -0700 Subject: [PATCH 091/172] refactor the ec2_connect script --- resources/ec2_connect | 75 ++++++------------------------------------- src/ec2/model.ts | 2 ++ 2 files changed, 12 insertions(+), 65 deletions(-) diff --git a/resources/ec2_connect b/resources/ec2_connect index 7df88be4576..261632c1847 100755 --- a/resources/ec2_connect +++ b/resources/ec2_connect @@ -37,62 +37,13 @@ _parse_json_for_value() { echo "$json" | grep -o "\"$key\":\"[^\"]*" | grep -o '[^"]*$' } -# Note: dev environment must have been previously started by VSCode Extension/CodeCatalyst -_start_dev_environment_session() { - # Function inputs - local CODECATALYST_ENDPOINT=$1 - local BEARER_TOKEN=$2 - local SPACE_NAME=$3 - local PROJECT_NAME=$4 - local DEVENV_ID=$5 - - # Local variables - local START_SESSION_PATH="/v1/spaces/$SPACE_NAME/projects/$PROJECT_NAME/devEnvironments/$DEVENV_ID/session" - local START_SESSION_QUERY=$( - cat << EOF -{ - "sessionConfiguration": { - "sessionType": "SSH" - } -} -EOF - ) - - curl -s -X PUT \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $BEARER_TOKEN" \ - -d "$START_SESSION_QUERY" \ - "$CODECATALYST_ENDPOINT$START_SESSION_PATH" -} - -_codecatalyst() { +_ec2() { # Function inputs local AWS_SSM_CLI=$1 - local CODECATALYST_ENDPOINT=$2 - local BEARER_TOKEN=$3 - local SPACE_NAME=$4 - local PROJECT_NAME=$5 - local DEVENV_ID=$6 - local AWS_REGION=$7 - - # Local variables - local START_SESSION_RESPONSE - local STREAM_URL - local TOKEN - local SESSION - - START_SESSION_RESPONSE=$(_start_dev_environment_session "$CODECATALYST_ENDPOINT" "$BEARER_TOKEN" "$SPACE_NAME" "$PROJECT_NAME" "$DEVENV_ID") - - # Errors happen when you have invalid token etc - # ValidationExceptions happen when the devenv is not running - if [[ "$START_SESSION_RESPONSE" == *"errors"* || "$START_SESSION_RESPONSE" == *"ValidationException"* || "$START_SESSION_RESPONSE" == *"NotValidException"* ]]; then - _log "Failed to start the session with error:" "$START_SESSION_RESPONSE" - exit 1 - fi - - STREAM_URL=$(_parse_json_for_value "streamUrl" "$START_SESSION_RESPONSE") - TOKEN=$(_parse_json_for_value "tokenValue" "$START_SESSION_RESPONSE") - SESSION=$(_parse_json_for_value "sessionId" "$START_SESSION_RESPONSE") + local AWS_REGION=$2 + local STREAM_URL=$3 + local SESSION_ID=$4 + local LOG_FILE_LOCATION=$5 exec "$AWS_SSM_CLI" "{\"streamUrl\":\"$STREAM_URL\",\"tokenValue\":\"$TOKEN\",\"sessionId\":\"$SESSION\"}" "$AWS_REGION" "StartSession" } @@ -100,19 +51,13 @@ _codecatalyst() { _main() { _log "==============================================================================" - _require LOG_FILE_LOCATION "${LOG_FILE_LOCATION:-}" - _require AWS_REGION "${AWS_REGION:-}" _require AWS_SSM_CLI "${AWS_SSM_CLI:-}" + _require AWS_REGION "${AWS_REGION:-}" + _require STREAM_URL "${STREAM_URL:-}" + _require SESSION_ID "${SESSION_ID:-}" + _require LOG_FILE_LOCATION "${LOG_FILE_LOCATION:-}" - _require CODECATALYST_ENDPOINT "${CODECATALYST_ENDPOINT:-}" - _require BEARER_TOKEN_LOCATION "${BEARER_TOKEN_LOCATION:-}" - _require SPACE_NAME "${SPACE_NAME:-}" - _require PROJECT_NAME "${PROJECT_NAME:-}" - _require DEVENV_ID "${DEVENV_ID:-}" - - CACHED_BEARER_TOKEN=$(cat "$BEARER_TOKEN_LOCATION") - - _codecatalyst "$AWS_SSM_CLI" "$CODECATALYST_ENDPOINT" "$CACHED_BEARER_TOKEN" "$SPACE_NAME" "$PROJECT_NAME" "$DEVENV_ID" "$AWS_REGION" + _ec2 "$AWS_SSM_CLI" "$AWS_REGION" "$STREAM_URL" "$SESSION_ID" "$LOG_FILE_LOCATION" } _main diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 2dd00074aa9..3ca74fdd148 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -202,6 +202,8 @@ function getEc2SsmEnv(selection: Ec2Selection, ssmPath: string): NodeJS.ProcessE AWS_REGION: selection.region, AWS_SSM_CLI: ssmPath, LOG_FILE_LOCATION: sshLogFileLocation('ec2', selection.instanceId), + STREAM_URL: 'testStream', + SESSION_ID: 'session_id', }, process.env ) From b08bbdb8fa389dbc1c79fdec859944e3ca557e72 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 18 Jul 2023 14:10:56 -0700 Subject: [PATCH 092/172] refactor script to include token for session --- resources/ec2_connect | 4 +++- src/ec2/model.ts | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/resources/ec2_connect b/resources/ec2_connect index 261632c1847..99f3b2d8961 100755 --- a/resources/ec2_connect +++ b/resources/ec2_connect @@ -42,6 +42,7 @@ _ec2() { local AWS_SSM_CLI=$1 local AWS_REGION=$2 local STREAM_URL=$3 + local TOKEN=$4 local SESSION_ID=$4 local LOG_FILE_LOCATION=$5 @@ -54,10 +55,11 @@ _main() { _require AWS_SSM_CLI "${AWS_SSM_CLI:-}" _require AWS_REGION "${AWS_REGION:-}" _require STREAM_URL "${STREAM_URL:-}" + _require TOKEN "${TOKEN:-}" _require SESSION_ID "${SESSION_ID:-}" _require LOG_FILE_LOCATION "${LOG_FILE_LOCATION:-}" - _ec2 "$AWS_SSM_CLI" "$AWS_REGION" "$STREAM_URL" "$SESSION_ID" "$LOG_FILE_LOCATION" + _ec2 "$AWS_SSM_CLI" "$AWS_REGION" "$STREAM_URL" "$TOKEN" "$SESSION_ID" "$LOG_FILE_LOCATION" } _main diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 3ca74fdd148..241de421fb7 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -28,6 +28,7 @@ interface Ec2RemoteEnv extends VscodeRemoteConnection { } const ec2ConnectScriptPrefix = 'ec2_connect' +const hostNamePrefix = 'ec2-' export class Ec2ConnectionManager { private ssmClient: SsmClient @@ -144,8 +145,9 @@ export class Ec2ConnectionManager { public async attemptToOpenRemoteConnection(selection: Ec2Selection): Promise { await this.checkForStartSessionError(selection) const remoteEnv = await this.prepareEc2RemoteEnvWithProgress(selection) + const fullHostName = `${hostNamePrefix}${selection.instanceId}` try { - await startVscodeRemote(remoteEnv.SessionProcess, selection.instanceId, '/', remoteEnv.vscPath) + await startVscodeRemote(remoteEnv.SessionProcess, fullHostName, '/', remoteEnv.vscPath) } catch (err) { this.throwGeneralConnectionError(selection, err as Error) } @@ -160,7 +162,7 @@ export class Ec2ConnectionManager { public async prepareEc2RemoteEnv(selection: Ec2Selection): Promise { const logger = this.configureRemoteConnectionLogger(selection.instanceId) const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() - const sshConfig = new VscodeRemoteSshConfig(ssh, 'ec2-user', ec2ConnectScriptPrefix) + const sshConfig = new VscodeRemoteSshConfig(ssh, hostNamePrefix, ec2ConnectScriptPrefix) const config = await sshConfig.ensureValid() if (config.isErr()) { const err = config.err() @@ -204,6 +206,7 @@ function getEc2SsmEnv(selection: Ec2Selection, ssmPath: string): NodeJS.ProcessE LOG_FILE_LOCATION: sshLogFileLocation('ec2', selection.instanceId), STREAM_URL: 'testStream', SESSION_ID: 'session_id', + TOKEN: 'token', }, process.env ) From c4348b4aaad730c37bb914ba9b9eaf184b590c3f Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 18 Jul 2023 14:16:19 -0700 Subject: [PATCH 093/172] include session tokens in script env --- resources/ec2_connect | 2 +- src/ec2/model.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/resources/ec2_connect b/resources/ec2_connect index 99f3b2d8961..a688207ba9d 100755 --- a/resources/ec2_connect +++ b/resources/ec2_connect @@ -46,7 +46,7 @@ _ec2() { local SESSION_ID=$4 local LOG_FILE_LOCATION=$5 - exec "$AWS_SSM_CLI" "{\"streamUrl\":\"$STREAM_URL\",\"tokenValue\":\"$TOKEN\",\"sessionId\":\"$SESSION\"}" "$AWS_REGION" "StartSession" + exec "$AWS_SSM_CLI" "{\"streamUrl\":\"$STREAM_URL\",\"tokenValue\":\"$TOKEN\",\"sessionId\":\"$SESSION_ID\"}" "$AWS_REGION" "StartSession" } _main() { diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 241de421fb7..687d757695f 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -4,7 +4,7 @@ */ import * as vscode from 'vscode' import { Session } from 'aws-sdk/clients/ssm' -import { IAM } from 'aws-sdk' +import { IAM, SSM } from 'aws-sdk' import { Ec2Selection } from './utils' import { getOrInstallCli } from '../shared/utilities/cliUtils' import { isCloud9 } from '../shared/extensionUtilities' @@ -170,8 +170,8 @@ export class Ec2ConnectionManager { throw err } - - const vars = getEc2SsmEnv(selection, ssm) + const session = await this.ssmClient.startSession(selection.instanceId) + const vars = getEc2SsmEnv(selection, ssm, session) const envProvider = async () => { return { [sshAgentSocketVariable]: await startSshAgent(), ...vars } } @@ -198,15 +198,15 @@ export class Ec2ConnectionManager { } } -function getEc2SsmEnv(selection: Ec2Selection, ssmPath: string): NodeJS.ProcessEnv { +function getEc2SsmEnv(selection: Ec2Selection, ssmPath: string, session: SSM.StartSessionResponse): NodeJS.ProcessEnv { return Object.assign( { AWS_REGION: selection.region, AWS_SSM_CLI: ssmPath, LOG_FILE_LOCATION: sshLogFileLocation('ec2', selection.instanceId), - STREAM_URL: 'testStream', - SESSION_ID: 'session_id', - TOKEN: 'token', + STREAM_URL: session.StreamUrl, + SESSION_ID: session.SessionId, + TOKEN: session.TokenValue, }, process.env ) From 02b2a2b3d19df14830389c275060645632e3e68f Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 18 Jul 2023 15:39:32 -0700 Subject: [PATCH 094/172] adding suport for documents in start session --- resources/ec2_connect | 6 ------ src/ec2/model.ts | 5 ++++- src/shared/clients/ssmClient.ts | 10 ++++++++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/resources/ec2_connect b/resources/ec2_connect index a688207ba9d..6a9285d5f45 100755 --- a/resources/ec2_connect +++ b/resources/ec2_connect @@ -31,12 +31,6 @@ _require() { _log "$1=$2" } -_parse_json_for_value() { - key=$1 - json=$2 - echo "$json" | grep -o "\"$key\":\"[^\"]*" | grep -o '[^"]*$' -} - _ec2() { # Function inputs local AWS_SSM_CLI=$1 diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 687d757695f..b48893c2e49 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -170,7 +170,10 @@ export class Ec2ConnectionManager { throw err } - const session = await this.ssmClient.startSession(selection.instanceId) + const session = await this.ssmClient.startSession(selection.instanceId, 'AWS-StartSSHSession', { + portNumber: ['22'], + }) + console.log(session) const vars = getEc2SsmEnv(selection, ssm, session) const envProvider = async () => { return { [sshAgentSocketVariable]: await startSshAgent(), ...vars } diff --git a/src/shared/clients/ssmClient.ts b/src/shared/clients/ssmClient.ts index 7374aeb25b9..648ccbc809f 100644 --- a/src/shared/clients/ssmClient.ts +++ b/src/shared/clients/ssmClient.ts @@ -28,9 +28,15 @@ export class SsmClient { return termination! } - public async startSession(target: string): Promise { + public async startSession( + target: string, + document?: string, + parameters?: SSM.SessionManagerParameters + ): Promise { const client = await this.createSdkClient() - const response = await client.startSession({ Target: target }).promise() + const response = await client + .startSession({ Target: target, DocumentName: document, Parameters: parameters }) + .promise() return response } From e373be8396f97a75ffdc55b5ebcbf2e0ce5b723a Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 18 Jul 2023 15:59:31 -0700 Subject: [PATCH 095/172] remove port number --- src/ec2/model.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index b48893c2e49..1cc00872684 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -170,9 +170,7 @@ export class Ec2ConnectionManager { throw err } - const session = await this.ssmClient.startSession(selection.instanceId, 'AWS-StartSSHSession', { - portNumber: ['22'], - }) + const session = await this.ssmClient.startSession(selection.instanceId, 'AWS-StartSSHSession') console.log(session) const vars = getEc2SsmEnv(selection, ssm, session) const envProvider = async () => { From 0f2c01d0c84d103c6f2b3df3e4fd4da98d69e90e Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Wed, 19 Jul 2023 10:13:50 -0700 Subject: [PATCH 096/172] remove log file location where unused --- resources/ec2_connect | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/ec2_connect b/resources/ec2_connect index 6a9285d5f45..df22ef18101 100755 --- a/resources/ec2_connect +++ b/resources/ec2_connect @@ -38,7 +38,6 @@ _ec2() { local STREAM_URL=$3 local TOKEN=$4 local SESSION_ID=$4 - local LOG_FILE_LOCATION=$5 exec "$AWS_SSM_CLI" "{\"streamUrl\":\"$STREAM_URL\",\"tokenValue\":\"$TOKEN\",\"sessionId\":\"$SESSION_ID\"}" "$AWS_REGION" "StartSession" } @@ -53,7 +52,7 @@ _main() { _require SESSION_ID "${SESSION_ID:-}" _require LOG_FILE_LOCATION "${LOG_FILE_LOCATION:-}" - _ec2 "$AWS_SSM_CLI" "$AWS_REGION" "$STREAM_URL" "$TOKEN" "$SESSION_ID" "$LOG_FILE_LOCATION" + _ec2 "$AWS_SSM_CLI" "$AWS_REGION" "$STREAM_URL" "$TOKEN" "$SESSION_ID" } _main From 718c402966f0d87998afac7f6fc30936cd0805d4 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Wed, 19 Jul 2023 10:15:55 -0700 Subject: [PATCH 097/172] clean up state after tests --- src/test/ec2/explorer/ec2ParentNode.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/ec2/explorer/ec2ParentNode.test.ts b/src/test/ec2/explorer/ec2ParentNode.test.ts index c50e203b9e7..b79da5bb4ae 100644 --- a/src/test/ec2/explorer/ec2ParentNode.test.ts +++ b/src/test/ec2/explorer/ec2ParentNode.test.ts @@ -60,6 +60,11 @@ describe('ec2ParentNode', function () { refreshStub.resetHistory() }) + after(function () { + clock.uninstall() + sinon.restore() + }) + it('returns placeholder node if no children are present', async function () { instances = [] From a21d6f85c5915bc8836e84b6d42304485e34bd70 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Wed, 19 Jul 2023 13:43:58 -0700 Subject: [PATCH 098/172] hide start/stop/reboot from command palette --- package.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/package.json b/package.json index 66eda712737..677d58a84ce 100644 --- a/package.json +++ b/package.json @@ -1118,6 +1118,18 @@ "command": "aws.ec2.openTerminal", "when": "aws.isDevMode" }, + { + "command": "aws.ec2.startInstance", + "when": "false" + }, + { + "command": "aws.ec2.stopInstance", + "when": "false" + }, + { + "command": "aws.ec2.rebootInstance", + "when": "false" + }, { "command": "aws.dev.openMenu", "when": "aws.isDevMode || isCloud9" From 60566f5876ea2ffe33a68a042e5f5d811e0ea36f Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Wed, 19 Jul 2023 15:01:46 -0700 Subject: [PATCH 099/172] add functionality to generate keys --- src/ec2/sendKeysToInstance.ts | 15 ++++++++++++++ src/test/ec2/sendKeysToInstance.test.ts | 27 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/ec2/sendKeysToInstance.ts create mode 100644 src/test/ec2/sendKeysToInstance.test.ts diff --git a/src/ec2/sendKeysToInstance.ts b/src/ec2/sendKeysToInstance.ts new file mode 100644 index 00000000000..ae90764190b --- /dev/null +++ b/src/ec2/sendKeysToInstance.ts @@ -0,0 +1,15 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ToolkitError } from '../shared/errors' +import { ChildProcess } from '../shared/utilities/childProcess' + +export async function generateSshKeys(keyPath: string) { + const process = new ChildProcess('ssh-keygen', ['-t', 'rsa', '-N', "''", '-q', '-f', keyPath]) + const result = await process.run() + if (result.exitCode !== 0) { + throw new ToolkitError('ec2: Failed to generate ssh key') + } +} diff --git a/src/test/ec2/sendKeysToInstance.test.ts b/src/test/ec2/sendKeysToInstance.test.ts new file mode 100644 index 00000000000..43029fb0dc7 --- /dev/null +++ b/src/test/ec2/sendKeysToInstance.test.ts @@ -0,0 +1,27 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as fs from 'fs-extra' +import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../shared/filesystemUtilities' +import { generateSshKeys } from '../../ec2/sendKeysToInstance' + +describe('generateSshKeys', async function () { + let temporaryDirectory: string + before(async function () { + temporaryDirectory = await makeTemporaryToolkitFolder() + }) + + after(async function () { + await tryRemoveFolder(temporaryDirectory) + }) + + it('generates key in target file', async function () { + const keyPath = `${temporaryDirectory}testKey` + await generateSshKeys(keyPath) + const contents = await fs.readFile(keyPath, 'utf-8') + assert.notStrictEqual(contents.length, 0) + }) +}) From e15c0a2744281e6fba0547b373588d4aba0cdddd Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Wed, 19 Jul 2023 16:09:04 -0700 Subject: [PATCH 100/172] implement sending keys to target instance, with hard coded dest file --- src/ec2/model.ts | 15 ++++++-- src/ec2/sendKeysToInstance.ts | 15 -------- src/ec2/sshKeyPair.ts | 25 ++++++++++++++ src/shared/clients/ssmClient.ts | 34 ++++++++++++++++++- src/test/ec2/model.test.ts | 34 ++++++++++++++++++- ...sToInstance.test.ts => sshKeyPair.test.ts} | 18 ++++++---- 6 files changed, 114 insertions(+), 27 deletions(-) delete mode 100644 src/ec2/sendKeysToInstance.ts create mode 100644 src/ec2/sshKeyPair.ts rename src/test/ec2/{sendKeysToInstance.test.ts => sshKeyPair.test.ts} (50%) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 1cc00872684..9f474b743b6 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -20,6 +20,7 @@ import { getLogger } from '../shared/logger/logger' import { Timeout } from '../shared/utilities/timeoutUtils' import { showMessageWithCancel } from '../shared/utilities/messages' import { VscodeRemoteSshConfig, sshLogFileLocation } from '../shared/sshConfig' +import { SshKeyPair } from './sshKeyPair' export type Ec2ConnectErrorCode = 'EC2SSMStatus' | 'EC2SSMPermission' | 'EC2SSMConnect' | 'EC2SSMAgentStatus' @@ -31,9 +32,9 @@ const ec2ConnectScriptPrefix = 'ec2_connect' const hostNamePrefix = 'ec2-' export class Ec2ConnectionManager { - private ssmClient: SsmClient - private ec2Client: Ec2Client - private iamClient: DefaultIamClient + protected ssmClient: SsmClient + protected ec2Client: Ec2Client + protected iamClient: DefaultIamClient public constructor(readonly regionCode: string) { this.ssmClient = this.createSsmSdkClient() @@ -197,6 +198,14 @@ export class Ec2ConnectionManager { const logger = (data: string) => getLogger().verbose(`${logPrefix}: ${data}`) return logger } + + public async sendSshKeyToInstance(selection: Ec2Selection, sshKeyPair: SshKeyPair): Promise { + const sshKey = sshKeyPair.getPublicKey() + const remoteAuthorizedKeysPaths = '/home/ec2-user/.ssh/authorized_keys' + const command = `echo ${sshKey} > ${remoteAuthorizedKeysPaths}` + const documentName = 'AWS-RunShellScript' + await this.ssmClient.sendCommandAndWait(selection.instanceId, documentName, { commands: [command] }) + } } function getEc2SsmEnv(selection: Ec2Selection, ssmPath: string, session: SSM.StartSessionResponse): NodeJS.ProcessEnv { diff --git a/src/ec2/sendKeysToInstance.ts b/src/ec2/sendKeysToInstance.ts deleted file mode 100644 index ae90764190b..00000000000 --- a/src/ec2/sendKeysToInstance.ts +++ /dev/null @@ -1,15 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ToolkitError } from '../shared/errors' -import { ChildProcess } from '../shared/utilities/childProcess' - -export async function generateSshKeys(keyPath: string) { - const process = new ChildProcess('ssh-keygen', ['-t', 'rsa', '-N', "''", '-q', '-f', keyPath]) - const result = await process.run() - if (result.exitCode !== 0) { - throw new ToolkitError('ec2: Failed to generate ssh key') - } -} diff --git a/src/ec2/sshKeyPair.ts b/src/ec2/sshKeyPair.ts new file mode 100644 index 00000000000..2c489582ef6 --- /dev/null +++ b/src/ec2/sshKeyPair.ts @@ -0,0 +1,25 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ToolkitError } from '../shared/errors' +import { ChildProcess } from '../shared/utilities/childProcess' + +export class SshKeyPair { + private constructor(protected keyPath: string) {} + + public static async generateSshKeys(keyPath: string) { + const process = new ChildProcess('ssh-keygen', ['-t', 'rsa', '-N', "''", '-q', '-f', keyPath]) + const result = await process.run() + if (result.exitCode !== 0) { + throw new ToolkitError('ec2: Failed to generate ssh key') + } + + return new SshKeyPair(keyPath) + } + + public getPublicKey(): string { + return '' + } +} diff --git a/src/shared/clients/ssmClient.ts b/src/shared/clients/ssmClient.ts index 648ccbc809f..ca4d2d19f56 100644 --- a/src/shared/clients/ssmClient.ts +++ b/src/shared/clients/ssmClient.ts @@ -3,10 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SSM } from 'aws-sdk' +import { AWSError, SSM } from 'aws-sdk' import { getLogger } from '../logger/logger' import globals from '../extensionGlobals' import { pageableToCollection } from '../utilities/collectionUtils' +import { PromiseResult } from 'aws-sdk/lib/request' +import { ToolkitError } from '../errors' export class SsmClient { public constructor(public readonly regionCode: string) {} @@ -61,6 +63,36 @@ export class SsmClient { return response[0]! } + public async sendCommand( + target: string, + documentName: string, + parameters: SSM.Parameters + ): Promise { + const client = await this.createSdkClient() + const response = await client + .sendCommand({ InstanceIds: [target], DocumentName: documentName, Parameters: parameters }) + .promise() + return response + } + + public async sendCommandAndWait( + target: string, + documentName: string, + parameters: SSM.Parameters + ): Promise> { + const response = await this.sendCommand(target, documentName, parameters) + const client = await this.createSdkClient() + try { + const commandId = response.Command!.CommandId! + const result = await client + .waitFor('commandExecuted', { CommandId: commandId, InstanceId: target }) + .promise() + return result + } catch (err) { + throw new ToolkitError(`Failed in sending command to target ${target}`, { cause: err as Error }) + } + } + public async getInstanceAgentPingStatus(target: string): Promise { const instanceInformation = await this.describeInstance(target) return instanceInformation ? instanceInformation.PingStatus! : 'Inactive' diff --git a/src/test/ec2/model.test.ts b/src/test/ec2/model.test.ts index cfc15f7ac22..27720003a82 100644 --- a/src/test/ec2/model.test.ts +++ b/src/test/ec2/model.test.ts @@ -4,13 +4,18 @@ */ import * as assert from 'assert' +import * as sinon from 'sinon' import { Ec2ConnectErrorCode, Ec2ConnectionManager } from '../../ec2/model' import { SsmClient } from '../../shared/clients/ssmClient' import { Ec2Client } from '../../shared/clients/ec2Client' import { attachedPoliciesListType } from 'aws-sdk/clients/iam' import { Ec2Selection } from '../../ec2/utils' import { ToolkitError } from '../../shared/errors' -import { EC2 } from 'aws-sdk' +import { AWSError, EC2, SSM } from 'aws-sdk' +import { PromiseResult } from 'aws-sdk/lib/request' +import { GetCommandInvocationResult } from 'aws-sdk/clients/ssm' +import { mock } from 'ts-mockito' +import { SshKeyPair } from '../../ec2/sshKeyPair' describe('Ec2ConnectClient', function () { class MockSsmClient extends SsmClient { @@ -189,4 +194,31 @@ describe('Ec2ConnectClient', function () { assert.strictEqual(false, result) }) }) + + describe('sendSshKeysToInstance', async function () { + let client: MockEc2ConnectClient + let sendCommandStub: sinon.SinonStub< + [target: string, documentName: string, parameters: SSM.Parameters], + Promise> + > + + before(function () { + client = new MockEc2ConnectClient() + sendCommandStub = sinon.stub(MockSsmClient.prototype, 'sendCommandAndWait') + }) + + after(function () { + sinon.restore() + }) + + it('calls the sdk with the proper parameters', async function () { + const testSelection = { + instanceId: 'test-id', + region: 'test-region', + } + const mockKeys = mock() as SshKeyPair + await client.sendSshKeyToInstance(testSelection, mockKeys) + sinon.assert.calledWith(sendCommandStub, testSelection.instanceId, 'AWS-RunShellScript') + }) + }) }) diff --git a/src/test/ec2/sendKeysToInstance.test.ts b/src/test/ec2/sshKeyPair.test.ts similarity index 50% rename from src/test/ec2/sendKeysToInstance.test.ts rename to src/test/ec2/sshKeyPair.test.ts index 43029fb0dc7..32cc19efd1b 100644 --- a/src/test/ec2/sendKeysToInstance.test.ts +++ b/src/test/ec2/sshKeyPair.test.ts @@ -6,22 +6,26 @@ import * as assert from 'assert' import * as fs from 'fs-extra' import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../shared/filesystemUtilities' -import { generateSshKeys } from '../../ec2/sendKeysToInstance' +import { SshKeyPair } from '../../ec2/sshKeyPair' -describe('generateSshKeys', async function () { +describe('SshKeyUtility', async function () { let temporaryDirectory: string + let keyPath: string + let keyPair: SshKeyPair before(async function () { temporaryDirectory = await makeTemporaryToolkitFolder() + keyPath = `${temporaryDirectory}/test-key` + keyPair = await SshKeyPair.generateSshKeys(keyPath) }) after(async function () { await tryRemoveFolder(temporaryDirectory) }) - it('generates key in target file', async function () { - const keyPath = `${temporaryDirectory}testKey` - await generateSshKeys(keyPath) - const contents = await fs.readFile(keyPath, 'utf-8') - assert.notStrictEqual(contents.length, 0) + describe('generateSshKeys', async function () { + it('generates key in target file', async function () { + const contents = await fs.readFile(keyPath, 'utf-8') + assert.notStrictEqual(contents.length, 0) + }) }) }) From 41eade412d371f9decce063871a91bc59a05c87a Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 20 Jul 2023 13:09:36 -0700 Subject: [PATCH 101/172] implement reading public key from KeyPair --- src/ec2/model.ts | 1 - src/ec2/sshKeyPair.ts | 15 +++++++++++---- src/test/ec2/sshKeyPair.test.ts | 9 +++++++++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 9f474b743b6..fad21e24dda 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -172,7 +172,6 @@ export class Ec2ConnectionManager { throw err } const session = await this.ssmClient.startSession(selection.instanceId, 'AWS-StartSSHSession') - console.log(session) const vars = getEc2SsmEnv(selection, ssm, session) const envProvider = async () => { return { [sshAgentSocketVariable]: await startSshAgent(), ...vars } diff --git a/src/ec2/sshKeyPair.ts b/src/ec2/sshKeyPair.ts index 2c489582ef6..7ab0c61ed0a 100644 --- a/src/ec2/sshKeyPair.ts +++ b/src/ec2/sshKeyPair.ts @@ -2,12 +2,15 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ - +import * as fs from 'fs-extra' import { ToolkitError } from '../shared/errors' import { ChildProcess } from '../shared/utilities/childProcess' export class SshKeyPair { - private constructor(protected keyPath: string) {} + private publicKeyPath: string + private constructor(keyPath: string) { + this.publicKeyPath = `${keyPath}.pub` + } public static async generateSshKeys(keyPath: string) { const process = new ChildProcess('ssh-keygen', ['-t', 'rsa', '-N', "''", '-q', '-f', keyPath]) @@ -19,7 +22,11 @@ export class SshKeyPair { return new SshKeyPair(keyPath) } - public getPublicKey(): string { - return '' + public getPublicKeyPath(): string { + return this.publicKeyPath + } + public async getPublicKey(): Promise { + const contents = await fs.readFile(this.publicKeyPath, 'utf-8') + return contents } } diff --git a/src/test/ec2/sshKeyPair.test.ts b/src/test/ec2/sshKeyPair.test.ts index 32cc19efd1b..367c5f9529d 100644 --- a/src/test/ec2/sshKeyPair.test.ts +++ b/src/test/ec2/sshKeyPair.test.ts @@ -28,4 +28,13 @@ describe('SshKeyUtility', async function () { assert.notStrictEqual(contents.length, 0) }) }) + + it('properly names the public key', function () { + assert.strictEqual(keyPair.getPublicKeyPath(), `${keyPath}.pub`) + }) + + it('reads in public ssh key that is non-empty', async function () { + const key = await keyPair.getPublicKey() + assert.notStrictEqual(key.length, 0) + }) }) From 33fbd79005aa65b18b794f9dcd851a55c7b55505 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 20 Jul 2023 13:10:50 -0700 Subject: [PATCH 102/172] add comment about hard coded path --- src/ec2/model.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index fad21e24dda..0e71b687ebb 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -200,6 +200,7 @@ export class Ec2ConnectionManager { public async sendSshKeyToInstance(selection: Ec2Selection, sshKeyPair: SshKeyPair): Promise { const sshKey = sshKeyPair.getPublicKey() + // TODO: this path is hard-coded from amazon linux instances. const remoteAuthorizedKeysPaths = '/home/ec2-user/.ssh/authorized_keys' const command = `echo ${sshKey} > ${remoteAuthorizedKeysPaths}` const documentName = 'AWS-RunShellScript' From 8cb386d0019b002f1fcfc1653dffe798a8480dff Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 20 Jul 2023 13:46:41 -0700 Subject: [PATCH 103/172] fix some spacing --- src/ec2/sshKeyPair.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ec2/sshKeyPair.ts b/src/ec2/sshKeyPair.ts index 7ab0c61ed0a..15c06a756a0 100644 --- a/src/ec2/sshKeyPair.ts +++ b/src/ec2/sshKeyPair.ts @@ -25,6 +25,7 @@ export class SshKeyPair { public getPublicKeyPath(): string { return this.publicKeyPath } + public async getPublicKey(): Promise { const contents = await fs.readFile(this.publicKeyPath, 'utf-8') return contents From 84fa81547091550482e6a964e19320bf4353398d Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 20 Jul 2023 15:41:50 -0700 Subject: [PATCH 104/172] update config to get it working --- src/ec2/model.ts | 27 +++++++++++++++++++++++---- src/ec2/sshKeyPair.ts | 13 ++++++++----- src/shared/sshConfig.ts | 13 +++++++++++-- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 0e71b687ebb..5f235d4f43e 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -21,6 +21,8 @@ import { Timeout } from '../shared/utilities/timeoutUtils' import { showMessageWithCancel } from '../shared/utilities/messages' import { VscodeRemoteSshConfig, sshLogFileLocation } from '../shared/sshConfig' import { SshKeyPair } from './sshKeyPair' +import path = require('path') +import globals from '../shared/extensionGlobals' export type Ec2ConnectErrorCode = 'EC2SSMStatus' | 'EC2SSMPermission' | 'EC2SSMConnect' | 'EC2SSMAgentStatus' @@ -161,9 +163,14 @@ export class Ec2ConnectionManager { } public async prepareEc2RemoteEnv(selection: Ec2Selection): Promise { + console.log('running function') const logger = this.configureRemoteConnectionLogger(selection.instanceId) const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() - const sshConfig = new VscodeRemoteSshConfig(ssh, hostNamePrefix, ec2ConnectScriptPrefix) + const keyPath = await this.configureSshKeys(selection) + const sshConfig = new VscodeRemoteSshConfig(ssh, hostNamePrefix, ec2ConnectScriptPrefix, 'ec2-user', keyPath) + console.log('configure ssh keys') + await this.configureSshKeys(selection) + const config = await sshConfig.ensureValid() if (config.isErr()) { const err = config.err() @@ -198,13 +205,25 @@ export class Ec2ConnectionManager { return logger } + public async configureSshKeys(selection: Ec2Selection): Promise { + const keyPath = path.join(globals.context.globalStorageUri.fsPath, `aws-ec2-key`) + console.log(keyPath) + const keyPair = await SshKeyPair.generateSshKeys(keyPath) + await this.sendSshKeyToInstance(selection, keyPair) + return keyPath + } + public async sendSshKeyToInstance(selection: Ec2Selection, sshKeyPair: SshKeyPair): Promise { - const sshKey = sshKeyPair.getPublicKey() + const sshKey = await sshKeyPair.getPublicKey() // TODO: this path is hard-coded from amazon linux instances. const remoteAuthorizedKeysPaths = '/home/ec2-user/.ssh/authorized_keys' - const command = `echo ${sshKey} > ${remoteAuthorizedKeysPaths}` + const command = `echo "${sshKey}" > ${remoteAuthorizedKeysPaths}` const documentName = 'AWS-RunShellScript' - await this.ssmClient.sendCommandAndWait(selection.instanceId, documentName, { commands: [command] }) + console.log(sshKey) + const resp = await this.ssmClient.sendCommandAndWait(selection.instanceId, documentName, { + commands: [command], + }) + console.log(resp) } } diff --git a/src/ec2/sshKeyPair.ts b/src/ec2/sshKeyPair.ts index 15c06a756a0..17104b64818 100644 --- a/src/ec2/sshKeyPair.ts +++ b/src/ec2/sshKeyPair.ts @@ -13,12 +13,15 @@ export class SshKeyPair { } public static async generateSshKeys(keyPath: string) { - const process = new ChildProcess('ssh-keygen', ['-t', 'rsa', '-N', "''", '-q', '-f', keyPath]) - const result = await process.run() - if (result.exitCode !== 0) { - throw new ToolkitError('ec2: Failed to generate ssh key') + const keyExists = await fs.pathExists(keyPath) + if (!keyExists) { + const process = new ChildProcess(`ssh-keygen`, ['-t', 'rsa', '-N', '', '-q', '-f', keyPath]) + const result = await process.run() + if (result.exitCode !== 0) { + throw new ToolkitError('ec2: Failed to generate ssh key') + } + console.log(result) } - return new SshKeyPair(keyPath) } diff --git a/src/shared/sshConfig.ts b/src/shared/sshConfig.ts index 60592a70195..68dc071b9fb 100644 --- a/src/shared/sshConfig.ts +++ b/src/shared/sshConfig.ts @@ -27,7 +27,9 @@ export class VscodeRemoteSshConfig { public constructor( protected readonly sshPath: string, protected readonly hostNamePrefix: string, - protected readonly scriptPrefix: string + protected readonly scriptPrefix: string, + protected readonly user?: string, + protected readonly identityFile?: string ) { this.configHostName = `${hostNamePrefix}*` this.proxyCommandRegExp = new RegExp(`proxycommand.{0,1024}${scriptPrefix}(.ps1)?.{0,99}`) @@ -169,7 +171,7 @@ export class VscodeRemoteSshConfig { return Result.ok() } - protected createSSHConfigSection(proxyCommand: string): string { + private getBaseSSHConfig(proxyCommand: string): string { // "AddKeysToAgent" will automatically add keys used on the server to the local agent. If not set, then `ssh-add` // must be done locally. It's mostly a convenience thing; private keys are _not_ shared with the server. @@ -182,6 +184,13 @@ Host ${this.configHostName} ProxyCommand ${proxyCommand} ` } + + protected createSSHConfigSection(proxyCommand: string): string { + if (this.user && this.identityFile) { + return `${this.getBaseSSHConfig(proxyCommand)}IdentityFile '${this.identityFile}'\n User ${this.user}\n` + } + return this.getBaseSSHConfig(proxyCommand) + } } export function sshLogFileLocation(service: string, id: string): string { From dca607e0d91722919691536a7b6fab8faa607de3 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 20 Jul 2023 16:02:18 -0700 Subject: [PATCH 105/172] cleanup some small things --- src/ec2/model.ts | 9 ++------- src/ec2/sshKeyPair.ts | 17 ++++++++++------- src/test/ec2/sshKeyPair.test.ts | 2 +- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 5f235d4f43e..780f6780a31 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -163,12 +163,10 @@ export class Ec2ConnectionManager { } public async prepareEc2RemoteEnv(selection: Ec2Selection): Promise { - console.log('running function') const logger = this.configureRemoteConnectionLogger(selection.instanceId) const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() const keyPath = await this.configureSshKeys(selection) const sshConfig = new VscodeRemoteSshConfig(ssh, hostNamePrefix, ec2ConnectScriptPrefix, 'ec2-user', keyPath) - console.log('configure ssh keys') await this.configureSshKeys(selection) const config = await sshConfig.ensureValid() @@ -207,8 +205,7 @@ export class Ec2ConnectionManager { public async configureSshKeys(selection: Ec2Selection): Promise { const keyPath = path.join(globals.context.globalStorageUri.fsPath, `aws-ec2-key`) - console.log(keyPath) - const keyPair = await SshKeyPair.generateSshKeys(keyPath) + const keyPair = await SshKeyPair.getSshKeyPair(keyPath) await this.sendSshKeyToInstance(selection, keyPair) return keyPath } @@ -219,11 +216,9 @@ export class Ec2ConnectionManager { const remoteAuthorizedKeysPaths = '/home/ec2-user/.ssh/authorized_keys' const command = `echo "${sshKey}" > ${remoteAuthorizedKeysPaths}` const documentName = 'AWS-RunShellScript' - console.log(sshKey) - const resp = await this.ssmClient.sendCommandAndWait(selection.instanceId, documentName, { + await this.ssmClient.sendCommandAndWait(selection.instanceId, documentName, { commands: [command], }) - console.log(resp) } } diff --git a/src/ec2/sshKeyPair.ts b/src/ec2/sshKeyPair.ts index 17104b64818..969c67d2cd1 100644 --- a/src/ec2/sshKeyPair.ts +++ b/src/ec2/sshKeyPair.ts @@ -12,19 +12,22 @@ export class SshKeyPair { this.publicKeyPath = `${keyPath}.pub` } - public static async generateSshKeys(keyPath: string) { + public static async getSshKeyPair(keyPath: string) { const keyExists = await fs.pathExists(keyPath) if (!keyExists) { - const process = new ChildProcess(`ssh-keygen`, ['-t', 'rsa', '-N', '', '-q', '-f', keyPath]) - const result = await process.run() - if (result.exitCode !== 0) { - throw new ToolkitError('ec2: Failed to generate ssh key') - } - console.log(result) + await SshKeyPair.generateSshKeyPair(keyPath) } return new SshKeyPair(keyPath) } + public static async generateSshKeyPair(keyPath: string) { + const process = new ChildProcess(`ssh-keygen`, ['-t', 'rsa', '-N', '', '-q', '-f', keyPath]) + const result = await process.run() + if (result.exitCode !== 0) { + throw new ToolkitError('ec2: Failed to generate ssh key') + } + } + public getPublicKeyPath(): string { return this.publicKeyPath } diff --git a/src/test/ec2/sshKeyPair.test.ts b/src/test/ec2/sshKeyPair.test.ts index 367c5f9529d..8d70bd24da3 100644 --- a/src/test/ec2/sshKeyPair.test.ts +++ b/src/test/ec2/sshKeyPair.test.ts @@ -15,7 +15,7 @@ describe('SshKeyUtility', async function () { before(async function () { temporaryDirectory = await makeTemporaryToolkitFolder() keyPath = `${temporaryDirectory}/test-key` - keyPair = await SshKeyPair.generateSshKeys(keyPath) + keyPair = await SshKeyPair.getSshKeyPair(keyPath) }) after(async function () { From f6ac8e3463ffdea5cdb6be2952a1f708e8ea976b Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 20 Jul 2023 16:16:08 -0700 Subject: [PATCH 106/172] add some testing --- src/ec2/sshKeyPair.ts | 2 +- src/test/ec2/sshKeyPair.test.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/ec2/sshKeyPair.ts b/src/ec2/sshKeyPair.ts index 969c67d2cd1..d3889cbcd81 100644 --- a/src/ec2/sshKeyPair.ts +++ b/src/ec2/sshKeyPair.ts @@ -20,7 +20,7 @@ export class SshKeyPair { return new SshKeyPair(keyPath) } - public static async generateSshKeyPair(keyPath: string) { + public static async generateSshKeyPair(keyPath: string): Promise { const process = new ChildProcess(`ssh-keygen`, ['-t', 'rsa', '-N', '', '-q', '-f', keyPath]) const result = await process.run() if (result.exitCode !== 0) { diff --git a/src/test/ec2/sshKeyPair.test.ts b/src/test/ec2/sshKeyPair.test.ts index 8d70bd24da3..ad6f6f5ca5f 100644 --- a/src/test/ec2/sshKeyPair.test.ts +++ b/src/test/ec2/sshKeyPair.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert' import * as fs from 'fs-extra' +import * as sinon from 'sinon' import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../shared/filesystemUtilities' import { SshKeyPair } from '../../ec2/sshKeyPair' @@ -12,6 +13,7 @@ describe('SshKeyUtility', async function () { let temporaryDirectory: string let keyPath: string let keyPair: SshKeyPair + before(async function () { temporaryDirectory = await makeTemporaryToolkitFolder() keyPath = `${temporaryDirectory}/test-key` @@ -20,6 +22,7 @@ describe('SshKeyUtility', async function () { after(async function () { await tryRemoveFolder(temporaryDirectory) + sinon.restore() }) describe('generateSshKeys', async function () { @@ -37,4 +40,11 @@ describe('SshKeyUtility', async function () { const key = await keyPair.getPublicKey() assert.notStrictEqual(key.length, 0) }) + + it('does not overwrite existing keys', async function () { + const generateStub = sinon.stub(SshKeyPair, 'generateSshKeyPair') + await SshKeyPair.getSshKeyPair(keyPath) + sinon.assert.notCalled(generateStub) + sinon.restore() + }) }) From e15e1841b7baafee567f6fb836e76acdfa86890a Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 21 Jul 2023 08:10:22 -0700 Subject: [PATCH 107/172] clean up handling of key parameters to ssh config --- src/ec2/model.ts | 5 ++++- src/shared/sshConfig.ts | 14 ++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 780f6780a31..c2be7a27290 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -166,7 +166,10 @@ export class Ec2ConnectionManager { const logger = this.configureRemoteConnectionLogger(selection.instanceId) const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() const keyPath = await this.configureSshKeys(selection) - const sshConfig = new VscodeRemoteSshConfig(ssh, hostNamePrefix, ec2ConnectScriptPrefix, 'ec2-user', keyPath) + const sshConfig = new VscodeRemoteSshConfig(ssh, hostNamePrefix, ec2ConnectScriptPrefix, { + identityFile: keyPath, + user: 'ec2-user', + }) await this.configureSshKeys(selection) const config = await sshConfig.ensureValid() diff --git a/src/shared/sshConfig.ts b/src/shared/sshConfig.ts index 68dc071b9fb..c7572bd2599 100644 --- a/src/shared/sshConfig.ts +++ b/src/shared/sshConfig.ts @@ -20,6 +20,11 @@ import { fileExists, readFileAsString } from './filesystemUtilities' const localize = nls.loadMessageBundle() +interface KeyParameters { + user: string + identityFile: string +} + export class VscodeRemoteSshConfig { protected readonly configHostName: string protected readonly proxyCommandRegExp: RegExp @@ -28,8 +33,7 @@ export class VscodeRemoteSshConfig { protected readonly sshPath: string, protected readonly hostNamePrefix: string, protected readonly scriptPrefix: string, - protected readonly user?: string, - protected readonly identityFile?: string + protected readonly keyParameters?: KeyParameters ) { this.configHostName = `${hostNamePrefix}*` this.proxyCommandRegExp = new RegExp(`proxycommand.{0,1024}${scriptPrefix}(.ps1)?.{0,99}`) @@ -186,8 +190,10 @@ Host ${this.configHostName} } protected createSSHConfigSection(proxyCommand: string): string { - if (this.user && this.identityFile) { - return `${this.getBaseSSHConfig(proxyCommand)}IdentityFile '${this.identityFile}'\n User ${this.user}\n` + if (this.keyParameters) { + return `${this.getBaseSSHConfig(proxyCommand)}IdentityFile '${this.keyParameters.identityFile}'\n User ${ + this.keyParameters.user + }\n` } return this.getBaseSSHConfig(proxyCommand) } From 6950712ee0391ceb865aabff440762b9cff5384e Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 21 Jul 2023 10:31:04 -0700 Subject: [PATCH 108/172] updates tests to utilize sinon stub --- src/shared/sshConfig.ts | 4 +-- src/test/shared/sshConfig.test.ts | 47 +++++++++++++------------------ 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/src/shared/sshConfig.ts b/src/shared/sshConfig.ts index c7572bd2599..eb7536b0833 100644 --- a/src/shared/sshConfig.ts +++ b/src/shared/sshConfig.ts @@ -113,7 +113,7 @@ export class VscodeRemoteSshConfig { throw new ToolkitError(oldConfig, { code: 'OldConfig' }) } - private async writeSectionToConfig(proxyCommand: string) { + protected async writeSectionToConfig(proxyCommand: string) { const sshConfigPath = getSshConfigPath() const section = this.createSSHConfigSection(proxyCommand) try { @@ -131,7 +131,7 @@ export class VscodeRemoteSshConfig { } } - protected async promptUserToConfigureSshConfig( + public async promptUserToConfigureSshConfig( configSection: string | undefined, proxyCommand: string ): Promise { diff --git a/src/test/shared/sshConfig.test.ts b/src/test/shared/sshConfig.test.ts index bef06177dde..6dac6178e29 100644 --- a/src/test/shared/sshConfig.test.ts +++ b/src/test/shared/sshConfig.test.ts @@ -3,8 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as assert from 'assert' +import * as sinon from 'sinon' import { ToolkitError } from '../../shared/errors' -import { Err, Ok, Result } from '../../shared/utilities/result' +import { Result } from '../../shared/utilities/result' import { ChildProcessResult } from '../../shared/utilities/childProcess' import { VscodeRemoteSshConfig, sshLogFileLocation } from '../../shared/sshConfig' @@ -12,21 +13,6 @@ class MockSshConfig extends VscodeRemoteSshConfig { // State variables to track logic flow. public testIsWin: boolean = false public configSection: string = '' - public SshConfigWritten: boolean = false - - public override async ensureValid(): Promise | Err | Ok> { - const proxyCommand = await this.getProxyCommand(this.scriptPrefix) - if (proxyCommand.isErr()) { - return proxyCommand - } - - const verifyHost = await this.verifySSHHost(proxyCommand.unwrap()) - if (verifyHost.isErr()) { - return verifyHost - } - - return Result.ok() - } public async getProxyCommandWrapper(command: string): Promise> { return await this.getProxyCommand(command) @@ -50,13 +36,6 @@ class MockSshConfig extends VscodeRemoteSshConfig { return this.testIsWin } - protected override async promptUserToConfigureSshConfig( - configSection: string | undefined, - section: string - ): Promise { - this.SshConfigWritten = true - } - protected override async checkSshOnHost(): Promise { return { exitCode: 0, @@ -73,11 +52,24 @@ class MockSshConfig extends VscodeRemoteSshConfig { describe('VscodeRemoteSshConfig', async function () { let config: MockSshConfig + let promptUserToConfigureSshConfigStub: sinon.SinonStub< + [configSection: string | undefined, proxyCommand: string], + Promise + > + const testCommand = 'test_connect' const testProxyCommand = `'${testCommand}' '%h'` before(function () { config = new MockSshConfig('sshPath', 'testHostNamePrefix', testCommand) config.testIsWin = false + promptUserToConfigureSshConfigStub = sinon.stub( + VscodeRemoteSshConfig.prototype, + 'promptUserToConfigureSshConfig' + ) + }) + + after(function () { + sinon.restore() }) describe('getProxyCommand', async function () { @@ -99,7 +91,7 @@ describe('VscodeRemoteSshConfig', async function () { assert.ok(match) }) - it('returns ok with undefined when proxycommand is not present', async function () { + it('returns ok result with undefined inside when proxycommand is not present', async function () { const testSection = `fdsafdsafdsa342432` const result = await config.testMatchSshSection(testSection) assert.ok(result.isOk()) @@ -110,7 +102,7 @@ describe('VscodeRemoteSshConfig', async function () { describe('verifySSHHost', async function () { beforeEach(function () { - config.SshConfigWritten = false + promptUserToConfigureSshConfigStub.resetHistory() }) it('writes to ssh config if command not found.', async function () { @@ -118,7 +110,8 @@ describe('VscodeRemoteSshConfig', async function () { const result = await config.testVerifySshHostWrapper(testCommand, testSection) assert.ok(result.isOk()) - assert.ok(config.SshConfigWritten) + sinon.assert.calledOn(promptUserToConfigureSshConfigStub, config) + sinon.assert.calledOnce(promptUserToConfigureSshConfigStub) }) it('does not write to ssh config if command is find', async function () { @@ -126,7 +119,7 @@ describe('VscodeRemoteSshConfig', async function () { const result = await config.testVerifySshHostWrapper(testCommand, testSection) assert.ok(result.isOk()) - assert.ok(!config.SshConfigWritten) + sinon.assert.notCalled(promptUserToConfigureSshConfigStub) }) }) From d951675deb9d274b100ad17eb2757695561b66aa Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 21 Jul 2023 10:42:15 -0700 Subject: [PATCH 109/172] remove references to code catatlyst in error/log messages --- src/shared/sshConfig.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/shared/sshConfig.ts b/src/shared/sshConfig.ts index eb7536b0833..fa939ede341 100644 --- a/src/shared/sshConfig.ts +++ b/src/shared/sshConfig.ts @@ -93,12 +93,9 @@ export class VscodeRemoteSshConfig { } private async promptUserForOutdatedSection(configSection: string): Promise { - getLogger().warn( - `codecatalyst: SSH config: found old/outdated "${this.configHostName}" section:\n%O`, - configSection - ) + getLogger().warn(`SSH config: found old/outdated "${this.configHostName}" section:\n%O`, configSection) const oldConfig = localize( - 'AWS.codecatalyst.error.oldConfig', + 'AWS.sshConfig.error.oldConfig', 'Your ~/.ssh/config has a {0} section that might be out of date. Delete it, then try again.', this.configHostName ) @@ -122,7 +119,7 @@ export class VscodeRemoteSshConfig { await fs.appendFile(sshConfigPath, section, { mode: 0o600 }) } catch (e) { const message = localize( - 'AWS.codecatalyst.error.writeFail', + 'AWS.sshConfig.error.writeFail', 'Failed to write SSH config: {0} (permission issue?)', sshConfigPath ) @@ -140,12 +137,12 @@ export class VscodeRemoteSshConfig { } const confirmTitle = localize( - 'AWS.codecatalyst.confirm.installSshConfig.title', + 'AWS.sshConfig.confirm.installSshConfig.title', '{0} Toolkit will add host {1} to ~/.ssh/config to use SSH with your Dev Environments', getIdeProperties().company, this.configHostName ) - const confirmText = localize('AWS.codecatalyst.confirm.installSshConfig.button', 'Update SSH config') + const confirmText = localize('AWS.sshConfig.confirm.installSshConfig.button', 'Update SSH config') const response = await showConfirmationMessage({ prompt: confirmTitle, confirm: confirmText }) if (!response) { throw new CancellationError('user') @@ -232,7 +229,7 @@ export async function ensureConnectScript( return Result.ok(connectScript) } catch (e) { - const message = localize('AWS.codecatalyst.error.copyScript', 'Failed to update connect script') + const message = localize('AWS.sshConfig.error.copyScript', 'Failed to update connect script') return Result.err(ToolkitError.chain(e, message, { code: 'ConnectScriptUpdateFailed' })) } From 2a63ae37645484eab98b82de16670d8835700920 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 21 Jul 2023 12:12:33 -0700 Subject: [PATCH 110/172] pass remote username down from above --- src/ec2/model.ts | 26 +++++++++++++++----------- src/shared/extensions/ssh.ts | 5 +++-- src/shared/sshConfig.ts | 6 +++--- src/test/shared/sshConfig.test.ts | 8 ++++++++ 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index c2be7a27290..03fb4eee00d 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -146,31 +146,31 @@ export class Ec2ConnectionManager { } public async attemptToOpenRemoteConnection(selection: Ec2Selection): Promise { + const remoteUser = 'ec2-user' await this.checkForStartSessionError(selection) - const remoteEnv = await this.prepareEc2RemoteEnvWithProgress(selection) + const remoteEnv = await this.prepareEc2RemoteEnvWithProgress(selection, remoteUser) const fullHostName = `${hostNamePrefix}${selection.instanceId}` try { - await startVscodeRemote(remoteEnv.SessionProcess, fullHostName, '/', remoteEnv.vscPath) + await startVscodeRemote(remoteEnv.SessionProcess, fullHostName, '/', remoteEnv.vscPath, 'ec2-user') } catch (err) { this.throwGeneralConnectionError(selection, err as Error) } } - public async prepareEc2RemoteEnvWithProgress(selection: Ec2Selection): Promise { + public async prepareEc2RemoteEnvWithProgress(selection: Ec2Selection, remoteUser: string): Promise { const timeout = new Timeout(60000) await showMessageWithCancel('AWS: Opening remote connection...', timeout) - const remoteEnv = await this.prepareEc2RemoteEnv(selection).finally(() => timeout.cancel()) + const remoteEnv = await this.prepareEc2RemoteEnv(selection, remoteUser).finally(() => timeout.cancel()) return remoteEnv } - public async prepareEc2RemoteEnv(selection: Ec2Selection): Promise { + public async prepareEc2RemoteEnv(selection: Ec2Selection, remoteUser: string): Promise { const logger = this.configureRemoteConnectionLogger(selection.instanceId) const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() - const keyPath = await this.configureSshKeys(selection) + const keyPath = await this.configureSshKeys(selection, remoteUser) const sshConfig = new VscodeRemoteSshConfig(ssh, hostNamePrefix, ec2ConnectScriptPrefix, { identityFile: keyPath, user: 'ec2-user', }) - await this.configureSshKeys(selection) const config = await sshConfig.ensureValid() if (config.isErr()) { @@ -206,17 +206,21 @@ export class Ec2ConnectionManager { return logger } - public async configureSshKeys(selection: Ec2Selection): Promise { + public async configureSshKeys(selection: Ec2Selection, remoteUser: string): Promise { const keyPath = path.join(globals.context.globalStorageUri.fsPath, `aws-ec2-key`) const keyPair = await SshKeyPair.getSshKeyPair(keyPath) - await this.sendSshKeyToInstance(selection, keyPair) + await this.sendSshKeyToInstance(selection, keyPair, remoteUser) return keyPath } - public async sendSshKeyToInstance(selection: Ec2Selection, sshKeyPair: SshKeyPair): Promise { + public async sendSshKeyToInstance( + selection: Ec2Selection, + sshKeyPair: SshKeyPair, + remoteUser: string + ): Promise { const sshKey = await sshKeyPair.getPublicKey() // TODO: this path is hard-coded from amazon linux instances. - const remoteAuthorizedKeysPaths = '/home/ec2-user/.ssh/authorized_keys' + const remoteAuthorizedKeysPaths = `/home/${remoteUser}/.ssh/authorized_keys` const command = `echo "${sshKey}" > ${remoteAuthorizedKeysPaths}` const documentName = 'AWS-RunShellScript' await this.ssmClient.sendCommandAndWait(selection.instanceId, documentName, { diff --git a/src/shared/extensions/ssh.ts b/src/shared/extensions/ssh.ts index 9e8584dab07..04997aadf35 100644 --- a/src/shared/extensions/ssh.ts +++ b/src/shared/extensions/ssh.ts @@ -123,9 +123,10 @@ export async function startVscodeRemote( ProcessClass: typeof ChildProcess, hostname: string, targetDirectory: string, - vscPath: string + vscPath: string, + user?: string ): Promise { - const workspaceUri = `vscode-remote://ssh-remote+${hostname}${targetDirectory}` + const workspaceUri = `vscode-remote://ssh-remote+${`${user}@` ?? ''}${hostname}${targetDirectory}` const settings = new RemoteSshSettings() settings.ensureDefaultExtension(VSCODE_EXTENSION_ID.awstoolkit) diff --git a/src/shared/sshConfig.ts b/src/shared/sshConfig.ts index fa939ede341..89181fb899e 100644 --- a/src/shared/sshConfig.ts +++ b/src/shared/sshConfig.ts @@ -188,9 +188,9 @@ Host ${this.configHostName} protected createSSHConfigSection(proxyCommand: string): string { if (this.keyParameters) { - return `${this.getBaseSSHConfig(proxyCommand)}IdentityFile '${this.keyParameters.identityFile}'\n User ${ - this.keyParameters.user - }\n` + return `${this.getBaseSSHConfig(proxyCommand)}IdentityFile '${ + this.keyParameters.identityFile + }'\n User '%r'\n` } return this.getBaseSSHConfig(proxyCommand) } diff --git a/src/test/shared/sshConfig.test.ts b/src/test/shared/sshConfig.test.ts index 6dac6178e29..81ca3e8cc2e 100644 --- a/src/test/shared/sshConfig.test.ts +++ b/src/test/shared/sshConfig.test.ts @@ -129,6 +129,14 @@ describe('VscodeRemoteSshConfig', async function () { const section = config.createSSHConfigSectionWrapper(testScriptName) assert.ok(section.includes(testScriptName)) }) + + // it('includes keyParameters if included in the class', function () { + // const keyParameters = { + // identityFile: 'path/to/identity/file', + // user: + // } + // const newConfig = new VscodeRemoteSshConfig('sshPath', 'testHostNamePrefix', 'someScript', ) + // }) }) describe('sshLogFileLocation', async function () { From 1087678b81c272d7c540168d31c6283eddb92bfd Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 21 Jul 2023 12:27:45 -0700 Subject: [PATCH 111/172] increase testing coverage for sshConfigSection --- src/ec2/model.ts | 8 +++----- src/shared/sshConfig.ts | 13 +++---------- src/test/shared/sshConfig.test.ts | 27 ++++++++++++++++++++------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 03fb4eee00d..1f16739015d 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -167,10 +167,7 @@ export class Ec2ConnectionManager { const logger = this.configureRemoteConnectionLogger(selection.instanceId) const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() const keyPath = await this.configureSshKeys(selection, remoteUser) - const sshConfig = new VscodeRemoteSshConfig(ssh, hostNamePrefix, ec2ConnectScriptPrefix, { - identityFile: keyPath, - user: 'ec2-user', - }) + const sshConfig = new VscodeRemoteSshConfig(ssh, hostNamePrefix, ec2ConnectScriptPrefix, keyPath) const config = await sshConfig.ensureValid() if (config.isErr()) { @@ -219,10 +216,11 @@ export class Ec2ConnectionManager { remoteUser: string ): Promise { const sshKey = await sshKeyPair.getPublicKey() - // TODO: this path is hard-coded from amazon linux instances. + const remoteAuthorizedKeysPaths = `/home/${remoteUser}/.ssh/authorized_keys` const command = `echo "${sshKey}" > ${remoteAuthorizedKeysPaths}` const documentName = 'AWS-RunShellScript' + await this.ssmClient.sendCommandAndWait(selection.instanceId, documentName, { commands: [command], }) diff --git a/src/shared/sshConfig.ts b/src/shared/sshConfig.ts index 89181fb899e..5783cc621a2 100644 --- a/src/shared/sshConfig.ts +++ b/src/shared/sshConfig.ts @@ -20,11 +20,6 @@ import { fileExists, readFileAsString } from './filesystemUtilities' const localize = nls.loadMessageBundle() -interface KeyParameters { - user: string - identityFile: string -} - export class VscodeRemoteSshConfig { protected readonly configHostName: string protected readonly proxyCommandRegExp: RegExp @@ -33,7 +28,7 @@ export class VscodeRemoteSshConfig { protected readonly sshPath: string, protected readonly hostNamePrefix: string, protected readonly scriptPrefix: string, - protected readonly keyParameters?: KeyParameters + protected readonly keyPath?: string ) { this.configHostName = `${hostNamePrefix}*` this.proxyCommandRegExp = new RegExp(`proxycommand.{0,1024}${scriptPrefix}(.ps1)?.{0,99}`) @@ -187,10 +182,8 @@ Host ${this.configHostName} } protected createSSHConfigSection(proxyCommand: string): string { - if (this.keyParameters) { - return `${this.getBaseSSHConfig(proxyCommand)}IdentityFile '${ - this.keyParameters.identityFile - }'\n User '%r'\n` + if (this.keyPath) { + return `${this.getBaseSSHConfig(proxyCommand)}IdentityFile '${this.keyPath}'\n User '%r'\n` } return this.getBaseSSHConfig(proxyCommand) } diff --git a/src/test/shared/sshConfig.test.ts b/src/test/shared/sshConfig.test.ts index 81ca3e8cc2e..65b92794f54 100644 --- a/src/test/shared/sshConfig.test.ts +++ b/src/test/shared/sshConfig.test.ts @@ -124,19 +124,32 @@ describe('VscodeRemoteSshConfig', async function () { }) describe('createSSHConfigSection', async function () { + const testKeyPath = 'path/to/keys' + const newConfig = new MockSshConfig('sshPath', 'testHostNamePrefix', 'someScript', testKeyPath) + const expectedUserString = `User '%r'` + const expectedIdentityFileString = `IdentityFile '${testKeyPath}'` + it('section includes relevant script prefix', function () { const testScriptName = 'testScript' const section = config.createSSHConfigSectionWrapper(testScriptName) assert.ok(section.includes(testScriptName)) }) - // it('includes keyParameters if included in the class', function () { - // const keyParameters = { - // identityFile: 'path/to/identity/file', - // user: - // } - // const newConfig = new VscodeRemoteSshConfig('sshPath', 'testHostNamePrefix', 'someScript', ) - // }) + it('includes keyPath if included in the class', function () { + const section = newConfig.createSSHConfigSectionWrapper('proxyCommand') + assert.ok(section.match(expectedIdentityFileString)) + }) + + it('parses the remote username from the ssh execution', function () { + const section = newConfig.createSSHConfigSectionWrapper('proxyCommand') + assert.ok(section.match(expectedUserString)) + }) + + it('omits User and IdentityFile fields when keyPath not given', function () { + const section = config.createSSHConfigSectionWrapper('proxyCommand') + assert.ok(!section.match(expectedUserString)) + assert.ok(!section.match(expectedIdentityFileString)) + }) }) describe('sshLogFileLocation', async function () { From 84ea20af91a135086b1423257834d3365426d5f4 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 21 Jul 2023 13:29:41 -0700 Subject: [PATCH 112/172] implement method to determine remote user --- src/ec2/model.ts | 9 ++++++++ src/shared/clients/ec2Client.ts | 40 +++++++++++++++++++++++++++++++++ src/test/ec2/model.test.ts | 31 ++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 1f16739015d..c34bfb3347f 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -225,6 +225,15 @@ export class Ec2ConnectionManager { commands: [command], }) } + + protected async getRemoteUser(instanceId: string): Promise { + const osName = await this.ec2Client.guessInstanceOsName(instanceId) + if (osName == 'ubuntu') { + return 'ubuntu' + } + + return 'ec2-user' + } } function getEc2SsmEnv(selection: Ec2Selection, ssmPath: string, session: SSM.StartSessionResponse): NodeJS.ProcessEnv { diff --git a/src/shared/clients/ec2Client.ts b/src/shared/clients/ec2Client.ts index fd4fe837cb3..25906d221fb 100644 --- a/src/shared/clients/ec2Client.ts +++ b/src/shared/clients/ec2Client.ts @@ -8,6 +8,7 @@ import { AsyncCollection } from '../utilities/asyncCollection' import { pageableToCollection } from '../utilities/collectionUtils' import { IamInstanceProfile } from 'aws-sdk/clients/ec2' import globals from '../extensionGlobals' +import { ToolkitError } from '../errors' export interface Ec2Instance extends EC2.Instance { name?: string @@ -108,6 +109,45 @@ export class Ec2Client { const association = await this.getIamInstanceProfileAssociation(instanceId) return association ? association.IamInstanceProfile : undefined } + + public async getImageFromInstance(instanceId: string): Promise { + const imageId = await this.getInstanceImageId(instanceId) + const image = await this.describeImage(imageId) + return image + } + + public async guessInstanceOsName(instanceId: string): Promise { + const image = await this.getImageFromInstance(instanceId) + const imageName = image.Name! + + if (imageName.match('al{0-9}{4}')) { + return 'amazon-linux' + } + + if (imageName.match('ubuntu')) { + return 'ubuntu' + } + + return '' + } + + public async getInstanceImageId(instanceId: string): Promise { + const instanceFilter = this.getInstancesFilter([instanceId]) + const instance = (await (await this.getInstances(instanceFilter)).promise())[0] + return instance.ImageId! + } + + public async describeImage(imageId: string): Promise { + const client = await this.createSdkClient() + const requester = async (request: EC2.DescribeImagesRequest) => client.describeImages(request).promise() + const response = ( + await pageableToCollection(requester, { ImageIds: [imageId] }, 'NextToken', 'Images') + .flatten() + .promise() + )[0]! + + return response + } } export function getNameOfInstance(instance: EC2.Instance): string | undefined { diff --git a/src/test/ec2/model.test.ts b/src/test/ec2/model.test.ts index 27720003a82..e8b1923e462 100644 --- a/src/test/ec2/model.test.ts +++ b/src/test/ec2/model.test.ts @@ -18,6 +18,7 @@ import { mock } from 'ts-mockito' import { SshKeyPair } from '../../ec2/sshKeyPair' describe('Ec2ConnectClient', function () { + let currentInstanceOs: string class MockSsmClient extends SsmClient { public constructor() { super('test-region') @@ -36,6 +37,10 @@ describe('Ec2ConnectClient', function () { public override async getInstanceStatus(instanceId: string): Promise { return instanceId.split(':')[0] as EC2.InstanceStateName } + + public override async guessInstanceOsName(instanceId: string): Promise { + return currentInstanceOs + } } class MockEc2ConnectClient extends Ec2ConnectionManager { @@ -50,6 +55,12 @@ describe('Ec2ConnectClient', function () { protected override createEc2SdkClient(): Ec2Client { return new MockEc2Client() } + + public async testGetRemoteUser(instanceId: string, osName: string) { + currentInstanceOs = osName + const remoteUser = await this.getRemoteUser(instanceId) + return remoteUser + } } describe('isInstanceRunning', async function () { @@ -217,8 +228,26 @@ describe('Ec2ConnectClient', function () { region: 'test-region', } const mockKeys = mock() as SshKeyPair - await client.sendSshKeyToInstance(testSelection, mockKeys) + await client.sendSshKeyToInstance(testSelection, mockKeys, '') sinon.assert.calledWith(sendCommandStub, testSelection.instanceId, 'AWS-RunShellScript') }) }) + + describe('determineRemoteUser', async function () { + let client: MockEc2ConnectClient + + before(function () { + client = new MockEc2ConnectClient() + }) + + it('identifies the user for ubuntu as ubuntu', async function () { + const remoteUser = await client.testGetRemoteUser('testInstance', 'ubuntu') + assert.strictEqual(remoteUser, 'ubuntu') + }) + + it('identifies the user for amazon linux as ec2-user', async function () { + const remoteUser = await client.testGetRemoteUser('testInstance', 'linux') + assert.strictEqual(remoteUser, 'ec2-user') + }) + }) }) From eed3a09f3aa0647d053ef452cc18c63c12a24bb0 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 21 Jul 2023 14:43:13 -0700 Subject: [PATCH 113/172] implement method for guessing the os --- src/shared/clients/ec2Client.ts | 8 +- .../shared/clients/defaultEc2Client.test.ts | 313 ++++++++++-------- 2 files changed, 182 insertions(+), 139 deletions(-) diff --git a/src/shared/clients/ec2Client.ts b/src/shared/clients/ec2Client.ts index 25906d221fb..004ffbea610 100644 --- a/src/shared/clients/ec2Client.ts +++ b/src/shared/clients/ec2Client.ts @@ -8,7 +8,6 @@ import { AsyncCollection } from '../utilities/asyncCollection' import { pageableToCollection } from '../utilities/collectionUtils' import { IamInstanceProfile } from 'aws-sdk/clients/ec2' import globals from '../extensionGlobals' -import { ToolkitError } from '../errors' export interface Ec2Instance extends EC2.Instance { name?: string @@ -119,16 +118,15 @@ export class Ec2Client { public async guessInstanceOsName(instanceId: string): Promise { const image = await this.getImageFromInstance(instanceId) const imageName = image.Name! - - if (imageName.match('al{0-9}{4}')) { + if (imageName.match(/al[0-9]{4}-/) || imageName.match('AmazonLinux')) { return 'amazon-linux' } if (imageName.match('ubuntu')) { return 'ubuntu' } - - return '' + // Currently defaults to amazon-linux because this is default in console. + return 'amazon-linux' } public async getInstanceImageId(instanceId: string): Promise { diff --git a/src/test/shared/clients/defaultEc2Client.test.ts b/src/test/shared/clients/defaultEc2Client.test.ts index 4eeb027b15b..53767745452 100644 --- a/src/test/shared/clients/defaultEc2Client.test.ts +++ b/src/test/shared/clients/defaultEc2Client.test.ts @@ -4,74 +4,113 @@ */ import * as assert from 'assert' +import * as sinon from 'sinon' import { AsyncCollection } from '../../../shared/utilities/asyncCollection' import { toCollection } from '../../../shared/utilities/asyncCollection' import { intoCollection } from '../../../shared/utilities/collectionUtils' import { Ec2Client, instanceHasName } from '../../../shared/clients/ec2Client' import { EC2 } from 'aws-sdk' -describe('extractInstancesFromReservations', function () { - const client = new Ec2Client('') - it('returns empty when given empty collection', async function () { - const actualResult = await client - .getInstancesFromReservations( - toCollection(async function* () { - yield [] - }) as AsyncCollection - ) - .promise() +describe('EC2Client', async function () { + describe('extractInstancesFromReservations', function () { + const client = new Ec2Client('') + it('returns empty when given empty collection', async function () { + const actualResult = await client + .getInstancesFromReservations( + toCollection(async function* () { + yield [] + }) as AsyncCollection + ) + .promise() - assert.strictEqual(0, actualResult.length) - }) + assert.strictEqual(0, actualResult.length) + }) - it('flattens the reservationList', async function () { - const testReservationsList: EC2.ReservationList = [ - { - Instances: [ - { - InstanceId: 'id1', - Tags: [{ Key: 'Name', Value: 'name1' }], - }, - { - InstanceId: 'id2', - Tags: [{ Key: 'Name', Value: 'name2' }], - }, + it('flattens the reservationList', async function () { + const testReservationsList: EC2.ReservationList = [ + { + Instances: [ + { + InstanceId: 'id1', + Tags: [{ Key: 'Name', Value: 'name1' }], + }, + { + InstanceId: 'id2', + Tags: [{ Key: 'Name', Value: 'name2' }], + }, + ], + }, + { + Instances: [ + { + InstanceId: 'id3', + Tags: [{ Key: 'Name', Value: 'name3' }], + }, + { + InstanceId: 'id4', + Tags: [{ Key: 'Name', Value: 'name4' }], + }, + ], + }, + ] + const actualResult = await client + .getInstancesFromReservations(intoCollection([testReservationsList])) + .promise() + assert.deepStrictEqual( + [ + { InstanceId: 'id1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, + { InstanceId: 'id2', name: 'name2', Tags: [{ Key: 'Name', Value: 'name2' }] }, + { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, + { InstanceId: 'id4', name: 'name4', Tags: [{ Key: 'Name', Value: 'name4' }] }, ], - }, - { - Instances: [ + actualResult + ) + }), + // Unsure if this test case is needed, but the return type in the SDK makes it possible these are unknown/not returned. + it('handles undefined and missing pieces in the ReservationList.', async function () { + const testReservationsList: EC2.ReservationList = [ { - InstanceId: 'id3', - Tags: [{ Key: 'Name', Value: 'name3' }], + Instances: [ + { + InstanceId: 'id1', + }, + { + InstanceId: undefined, + }, + ], }, { - InstanceId: 'id4', - Tags: [{ Key: 'Name', Value: 'name4' }], + Instances: [ + { + InstanceId: 'id3', + Tags: [{ Key: 'Name', Value: 'name3' }], + }, + {}, + ], }, - ], - }, - ] - const actualResult = await client.getInstancesFromReservations(intoCollection([testReservationsList])).promise() - assert.deepStrictEqual( - [ - { InstanceId: 'id1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, - { InstanceId: 'id2', name: 'name2', Tags: [{ Key: 'Name', Value: 'name2' }] }, - { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, - { InstanceId: 'id4', name: 'name4', Tags: [{ Key: 'Name', Value: 'name4' }] }, - ], - actualResult - ) - }), - // Unsure if this test case is needed, but the return type in the SDK makes it possible these are unknown/not returned. - it('handles undefined and missing pieces in the ReservationList.', async function () { + ] + const actualResult = await client + .getInstancesFromReservations(intoCollection([testReservationsList])) + .promise() + assert.deepStrictEqual( + [ + { InstanceId: 'id1' }, + { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, + ], + actualResult + ) + }) + + it('can process results without complete Tag field.', async function () { const testReservationsList: EC2.ReservationList = [ { Instances: [ { InstanceId: 'id1', + Tags: [{ Key: 'Name', Value: 'name1' }], }, { - InstanceId: undefined, + InstanceId: 'id2', }, ], }, @@ -81,107 +120,113 @@ describe('extractInstancesFromReservations', function () { InstanceId: 'id3', Tags: [{ Key: 'Name', Value: 'name3' }], }, - {}, + { + InstanceId: 'id4', + Tags: [], + }, ], }, ] + const actualResult = await client .getInstancesFromReservations(intoCollection([testReservationsList])) .promise() + assert.deepStrictEqual( - [{ InstanceId: 'id1' }, { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }], + [ + { InstanceId: 'id1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, + { InstanceId: 'id2' }, + { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, + { InstanceId: 'id4', Tags: [] }, + ], actualResult ) }) + }) - it('can process results without complete Tag field.', async function () { - const testReservationsList: EC2.ReservationList = [ - { - Instances: [ - { - InstanceId: 'id1', - Tags: [{ Key: 'Name', Value: 'name1' }], - }, - { - InstanceId: 'id2', - }, - ], - }, - { - Instances: [ - { - InstanceId: 'id3', - Tags: [{ Key: 'Name', Value: 'name3' }], - }, - { - InstanceId: 'id4', - Tags: [], - }, - ], - }, - ] - - const actualResult = await client.getInstancesFromReservations(intoCollection([testReservationsList])).promise() - - assert.deepStrictEqual( - [ - { InstanceId: 'id1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, - { InstanceId: 'id2' }, - { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, - { InstanceId: 'id4', Tags: [] }, - ], - actualResult - ) + describe('getInstancesFilter', function () { + const client = new Ec2Client('') + + it('returns proper filter when given instanceId', function () { + const testInstanceId1 = 'test' + const actualFilters1 = client.getInstancesFilter([testInstanceId1]) + const expectedFilters1: EC2.Filter[] = [ + { + Name: 'instance-id', + Values: [testInstanceId1], + }, + ] + + assert.deepStrictEqual(expectedFilters1, actualFilters1) + + const testInstanceId2 = 'test2' + const actualFilters2 = client.getInstancesFilter([testInstanceId1, testInstanceId2]) + const expectedFilters2: EC2.Filter[] = [ + { + Name: 'instance-id', + Values: [testInstanceId1, testInstanceId2], + }, + ] + + assert.deepStrictEqual(expectedFilters2, actualFilters2) + }) }) -}) -describe('getInstancesFilter', function () { - const client = new Ec2Client('') - - it('returns proper filter when given instanceId', function () { - const testInstanceId1 = 'test' - const actualFilters1 = client.getInstancesFilter([testInstanceId1]) - const expectedFilters1: EC2.Filter[] = [ - { - Name: 'instance-id', - Values: [testInstanceId1], - }, - ] - - assert.deepStrictEqual(expectedFilters1, actualFilters1) - - const testInstanceId2 = 'test2' - const actualFilters2 = client.getInstancesFilter([testInstanceId1, testInstanceId2]) - const expectedFilters2: EC2.Filter[] = [ - { - Name: 'instance-id', - Values: [testInstanceId1, testInstanceId2], - }, - ] - - assert.deepStrictEqual(expectedFilters2, actualFilters2) + describe('instanceHasName', function () { + it('returns whether or not there is name attached to instance', function () { + const instances = [ + { InstanceId: 'id1', Tags: [] }, + { InstanceId: 'id2', name: 'name2', Tags: [{ Key: 'Name', Value: 'name2' }] }, + { InstanceId: 'id3', Tags: [{ Key: 'NotName', Value: 'notAName' }] }, + { + InstanceId: 'id4', + name: 'name4', + Tags: [ + { Key: 'Name', Value: 'name4' }, + { Key: 'anotherKey', Value: 'Another Key' }, + ], + }, + ] + + assert.deepStrictEqual(false, instanceHasName(instances[0])) + assert.deepStrictEqual(true, instanceHasName(instances[1])) + assert.deepStrictEqual(false, instanceHasName(instances[2])) + assert.deepStrictEqual(true, instanceHasName(instances[3])) + }) }) -}) -describe('instanceHasName', function () { - it('returns whether or not there is name attached to instance', function () { - const instances = [ - { InstanceId: 'id1', Tags: [] }, - { InstanceId: 'id2', name: 'name2', Tags: [{ Key: 'Name', Value: 'name2' }] }, - { InstanceId: 'id3', Tags: [{ Key: 'NotName', Value: 'notAName' }] }, - { - InstanceId: 'id4', - name: 'name4', - Tags: [ - { Key: 'Name', Value: 'name4' }, - { Key: 'anotherKey', Value: 'Another Key' }, - ], - }, - ] + describe('guessInstanceOsName', async function () { + let client: Ec2Client + let getOsStub: sinon.SinonStub<[instanceId: string], Promise> + + async function assertRecognizesAmiName(amiName: string, expectedOS: string) { + getOsStub = sinon.stub(Ec2Client.prototype, 'getImageFromInstance') + getOsStub.callsFake(async i => { + return { Name: amiName } as EC2.Image + }) + const guess = await client.guessInstanceOsName('') + assert.deepStrictEqual(guess, expectedOS) + sinon.restore() + } + + before(function () { + client = new Ec2Client('') + }) - assert.deepStrictEqual(false, instanceHasName(instances[0])) - assert.deepStrictEqual(true, instanceHasName(instances[1])) - assert.deepStrictEqual(false, instanceHasName(instances[2])) - assert.deepStrictEqual(true, instanceHasName(instances[3])) + after(function () { + sinon.restore() + }) + it('recognizes variants of amazon linux', async function () { + const amiName1 = `al2023-ami-2023.1.20230719.0-kernel-6.1-x86_64` + await assertRecognizesAmiName(amiName1, 'amazon-linux') + + const amiName2 = `Cloud9AmazonLinux2-2022-08-01T09-18` + await assertRecognizesAmiName(amiName2, 'amazon-linux') + }) + + it('recognizes variants of ubuntu', async function () { + const amiName1 = `ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-20230516` + await assertRecognizesAmiName(amiName1, 'ubuntu') + }) }) }) From 4b94c83bfd56bc2bb1ffbebf62cdfb120c3643b3 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 21 Jul 2023 14:59:02 -0700 Subject: [PATCH 114/172] refactor to use ssm sdk to determine os --- src/ec2/model.ts | 13 +++++-- src/shared/clients/ec2Client.ts | 38 ------------------- src/shared/clients/ssmClient.ts | 7 +++- src/test/ec2/model.test.ts | 21 +++++++--- .../shared/clients/defaultEc2Client.test.ts | 36 ------------------ 5 files changed, 30 insertions(+), 85 deletions(-) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index c34bfb3347f..8f40373f091 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -147,6 +147,7 @@ export class Ec2ConnectionManager { public async attemptToOpenRemoteConnection(selection: Ec2Selection): Promise { const remoteUser = 'ec2-user' + console.log(await this.getRemoteUser(selection.instanceId)) await this.checkForStartSessionError(selection) const remoteEnv = await this.prepareEc2RemoteEnvWithProgress(selection, remoteUser) const fullHostName = `${hostNamePrefix}${selection.instanceId}` @@ -226,13 +227,17 @@ export class Ec2ConnectionManager { }) } - protected async getRemoteUser(instanceId: string): Promise { - const osName = await this.ec2Client.guessInstanceOsName(instanceId) - if (osName == 'ubuntu') { + protected async getRemoteUser(instanceId: string) { + const osName = await this.ssmClient.getTargetPlatformName(instanceId) + if (osName === 'Amazon Linux') { + return 'ec2-user' + } + + if (osName === 'Ubuntu') { return 'ubuntu' } - return 'ec2-user' + throw new ToolkitError(`Unrecognized OS name ${osName} on instance ${instanceId}`, { code: 'UnknownEc2OS' }) } } diff --git a/src/shared/clients/ec2Client.ts b/src/shared/clients/ec2Client.ts index 004ffbea610..fd4fe837cb3 100644 --- a/src/shared/clients/ec2Client.ts +++ b/src/shared/clients/ec2Client.ts @@ -108,44 +108,6 @@ export class Ec2Client { const association = await this.getIamInstanceProfileAssociation(instanceId) return association ? association.IamInstanceProfile : undefined } - - public async getImageFromInstance(instanceId: string): Promise { - const imageId = await this.getInstanceImageId(instanceId) - const image = await this.describeImage(imageId) - return image - } - - public async guessInstanceOsName(instanceId: string): Promise { - const image = await this.getImageFromInstance(instanceId) - const imageName = image.Name! - if (imageName.match(/al[0-9]{4}-/) || imageName.match('AmazonLinux')) { - return 'amazon-linux' - } - - if (imageName.match('ubuntu')) { - return 'ubuntu' - } - // Currently defaults to amazon-linux because this is default in console. - return 'amazon-linux' - } - - public async getInstanceImageId(instanceId: string): Promise { - const instanceFilter = this.getInstancesFilter([instanceId]) - const instance = (await (await this.getInstances(instanceFilter)).promise())[0] - return instance.ImageId! - } - - public async describeImage(imageId: string): Promise { - const client = await this.createSdkClient() - const requester = async (request: EC2.DescribeImagesRequest) => client.describeImages(request).promise() - const response = ( - await pageableToCollection(requester, { ImageIds: [imageId] }, 'NextToken', 'Images') - .flatten() - .promise() - )[0]! - - return response - } } export function getNameOfInstance(instance: EC2.Instance): string | undefined { diff --git a/src/shared/clients/ssmClient.ts b/src/shared/clients/ssmClient.ts index ca4d2d19f56..7a226597ced 100644 --- a/src/shared/clients/ssmClient.ts +++ b/src/shared/clients/ssmClient.ts @@ -59,10 +59,15 @@ export class SsmClient { .flatten() .flatten() .promise() - + console.log(response) return response[0]! } + public async getTargetPlatformName(target: string): Promise { + const instanceInformation = await this.describeInstance(target) + return instanceInformation.PlatformName! + } + public async sendCommand( target: string, documentName: string, diff --git a/src/test/ec2/model.test.ts b/src/test/ec2/model.test.ts index e8b1923e462..f8013ee24e9 100644 --- a/src/test/ec2/model.test.ts +++ b/src/test/ec2/model.test.ts @@ -27,6 +27,10 @@ describe('Ec2ConnectClient', function () { public override async getInstanceAgentPingStatus(target: string): Promise { return target.split(':')[2] } + + public override async getTargetPlatformName(target: string): Promise { + return currentInstanceOs + } } class MockEc2Client extends Ec2Client { @@ -37,10 +41,6 @@ describe('Ec2ConnectClient', function () { public override async getInstanceStatus(instanceId: string): Promise { return instanceId.split(':')[0] as EC2.InstanceStateName } - - public override async guessInstanceOsName(instanceId: string): Promise { - return currentInstanceOs - } } class MockEc2ConnectClient extends Ec2ConnectionManager { @@ -241,13 +241,22 @@ describe('Ec2ConnectClient', function () { }) it('identifies the user for ubuntu as ubuntu', async function () { - const remoteUser = await client.testGetRemoteUser('testInstance', 'ubuntu') + const remoteUser = await client.testGetRemoteUser('testInstance', 'Ubuntu') assert.strictEqual(remoteUser, 'ubuntu') }) it('identifies the user for amazon linux as ec2-user', async function () { - const remoteUser = await client.testGetRemoteUser('testInstance', 'linux') + const remoteUser = await client.testGetRemoteUser('testInstance', 'Amazon Linux') assert.strictEqual(remoteUser, 'ec2-user') }) + + it('throws error when not given known OS', async function () { + try { + await client.testGetRemoteUser('testInstance', 'ThisIsNotARealOs!') + assert.ok(false) + } catch (exception) { + assert.ok(true) + } + }) }) }) diff --git a/src/test/shared/clients/defaultEc2Client.test.ts b/src/test/shared/clients/defaultEc2Client.test.ts index 53767745452..825eb620194 100644 --- a/src/test/shared/clients/defaultEc2Client.test.ts +++ b/src/test/shared/clients/defaultEc2Client.test.ts @@ -4,7 +4,6 @@ */ import * as assert from 'assert' -import * as sinon from 'sinon' import { AsyncCollection } from '../../../shared/utilities/asyncCollection' import { toCollection } from '../../../shared/utilities/asyncCollection' import { intoCollection } from '../../../shared/utilities/collectionUtils' @@ -194,39 +193,4 @@ describe('EC2Client', async function () { assert.deepStrictEqual(true, instanceHasName(instances[3])) }) }) - - describe('guessInstanceOsName', async function () { - let client: Ec2Client - let getOsStub: sinon.SinonStub<[instanceId: string], Promise> - - async function assertRecognizesAmiName(amiName: string, expectedOS: string) { - getOsStub = sinon.stub(Ec2Client.prototype, 'getImageFromInstance') - getOsStub.callsFake(async i => { - return { Name: amiName } as EC2.Image - }) - const guess = await client.guessInstanceOsName('') - assert.deepStrictEqual(guess, expectedOS) - sinon.restore() - } - - before(function () { - client = new Ec2Client('') - }) - - after(function () { - sinon.restore() - }) - it('recognizes variants of amazon linux', async function () { - const amiName1 = `al2023-ami-2023.1.20230719.0-kernel-6.1-x86_64` - await assertRecognizesAmiName(amiName1, 'amazon-linux') - - const amiName2 = `Cloud9AmazonLinux2-2022-08-01T09-18` - await assertRecognizesAmiName(amiName2, 'amazon-linux') - }) - - it('recognizes variants of ubuntu', async function () { - const amiName1 = `ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-20230516` - await assertRecognizesAmiName(amiName1, 'ubuntu') - }) - }) }) From 65ad6ac359d7af0b2d379d6b50dc8d3ca8892b75 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 21 Jul 2023 15:00:53 -0700 Subject: [PATCH 115/172] check for permissions before trying the ssm sdk --- src/ec2/model.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 8f40373f091..b7df61251ad 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -146,10 +146,11 @@ export class Ec2ConnectionManager { } public async attemptToOpenRemoteConnection(selection: Ec2Selection): Promise { - const remoteUser = 'ec2-user' - console.log(await this.getRemoteUser(selection.instanceId)) await this.checkForStartSessionError(selection) + + const remoteUser = await this.getRemoteUser(selection.instanceId) const remoteEnv = await this.prepareEc2RemoteEnvWithProgress(selection, remoteUser) + const fullHostName = `${hostNamePrefix}${selection.instanceId}` try { await startVscodeRemote(remoteEnv.SessionProcess, fullHostName, '/', remoteEnv.vscPath, 'ec2-user') From febac9510ea8f9ff9653bc6d32cbefb4d9f9dac4 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 21 Jul 2023 15:12:20 -0700 Subject: [PATCH 116/172] fix to work on ubuntu --- src/ec2/model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index b7df61251ad..508f797db52 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -153,7 +153,7 @@ export class Ec2ConnectionManager { const fullHostName = `${hostNamePrefix}${selection.instanceId}` try { - await startVscodeRemote(remoteEnv.SessionProcess, fullHostName, '/', remoteEnv.vscPath, 'ec2-user') + await startVscodeRemote(remoteEnv.SessionProcess, fullHostName, '/', remoteEnv.vscPath, remoteUser) } catch (err) { this.throwGeneralConnectionError(selection, err as Error) } From ee149b7c26e36d26aa98f98f3c353fee25513135 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 21 Jul 2023 15:24:14 -0700 Subject: [PATCH 117/172] remove leftover log statement --- src/shared/clients/ssmClient.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/shared/clients/ssmClient.ts b/src/shared/clients/ssmClient.ts index 7a226597ced..cbc8db53364 100644 --- a/src/shared/clients/ssmClient.ts +++ b/src/shared/clients/ssmClient.ts @@ -59,7 +59,6 @@ export class SsmClient { .flatten() .flatten() .promise() - console.log(response) return response[0]! } From aa56c9370147b355bd41dca40489e3df995701af Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 24 Jul 2023 09:28:39 -0700 Subject: [PATCH 118/172] refactor tests to use stub only where needed --- src/test/shared/sshConfig.test.ts | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/test/shared/sshConfig.test.ts b/src/test/shared/sshConfig.test.ts index 65b92794f54..41d054a7944 100644 --- a/src/test/shared/sshConfig.test.ts +++ b/src/test/shared/sshConfig.test.ts @@ -52,24 +52,13 @@ class MockSshConfig extends VscodeRemoteSshConfig { describe('VscodeRemoteSshConfig', async function () { let config: MockSshConfig - let promptUserToConfigureSshConfigStub: sinon.SinonStub< - [configSection: string | undefined, proxyCommand: string], - Promise - > const testCommand = 'test_connect' const testProxyCommand = `'${testCommand}' '%h'` + before(function () { config = new MockSshConfig('sshPath', 'testHostNamePrefix', testCommand) config.testIsWin = false - promptUserToConfigureSshConfigStub = sinon.stub( - VscodeRemoteSshConfig.prototype, - 'promptUserToConfigureSshConfig' - ) - }) - - after(function () { - sinon.restore() }) describe('getProxyCommand', async function () { @@ -101,10 +90,25 @@ describe('VscodeRemoteSshConfig', async function () { }) describe('verifySSHHost', async function () { + let promptUserToConfigureSshConfigStub: sinon.SinonStub< + [configSection: string | undefined, proxyCommand: string], + Promise + > + before(function () { + promptUserToConfigureSshConfigStub = sinon.stub( + VscodeRemoteSshConfig.prototype, + 'promptUserToConfigureSshConfig' + ) + }) + beforeEach(function () { promptUserToConfigureSshConfigStub.resetHistory() }) + after(function () { + sinon.restore() + }) + it('writes to ssh config if command not found.', async function () { const testSection = 'no-command-here' const result = await config.testVerifySshHostWrapper(testCommand, testSection) From 90b248b756936e25fb6ef461a67c506020ef6b53 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 24 Jul 2023 09:40:13 -0700 Subject: [PATCH 119/172] remove double restore in test file --- src/test/ec2/sshKeyPair.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/ec2/sshKeyPair.test.ts b/src/test/ec2/sshKeyPair.test.ts index ad6f6f5ca5f..44a9e38a2d5 100644 --- a/src/test/ec2/sshKeyPair.test.ts +++ b/src/test/ec2/sshKeyPair.test.ts @@ -22,7 +22,6 @@ describe('SshKeyUtility', async function () { after(async function () { await tryRemoveFolder(temporaryDirectory) - sinon.restore() }) describe('generateSshKeys', async function () { From 950ca3c0caea22876a72ef30e51fb0c474f25757 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 24 Jul 2023 09:46:11 -0700 Subject: [PATCH 120/172] test commenting out sshConfig tests --- src/test/shared/sshConfig.test.ts | 74 +++++++++++++++---------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/test/shared/sshConfig.test.ts b/src/test/shared/sshConfig.test.ts index 41d054a7944..b39f8d7339b 100644 --- a/src/test/shared/sshConfig.test.ts +++ b/src/test/shared/sshConfig.test.ts @@ -89,43 +89,43 @@ describe('VscodeRemoteSshConfig', async function () { }) }) - describe('verifySSHHost', async function () { - let promptUserToConfigureSshConfigStub: sinon.SinonStub< - [configSection: string | undefined, proxyCommand: string], - Promise - > - before(function () { - promptUserToConfigureSshConfigStub = sinon.stub( - VscodeRemoteSshConfig.prototype, - 'promptUserToConfigureSshConfig' - ) - }) - - beforeEach(function () { - promptUserToConfigureSshConfigStub.resetHistory() - }) - - after(function () { - sinon.restore() - }) - - it('writes to ssh config if command not found.', async function () { - const testSection = 'no-command-here' - const result = await config.testVerifySshHostWrapper(testCommand, testSection) - - assert.ok(result.isOk()) - sinon.assert.calledOn(promptUserToConfigureSshConfigStub, config) - sinon.assert.calledOnce(promptUserToConfigureSshConfigStub) - }) - - it('does not write to ssh config if command is find', async function () { - const testSection = `this is some text that doesn't matter, but here proxycommand ${testProxyCommand}` - const result = await config.testVerifySshHostWrapper(testCommand, testSection) - - assert.ok(result.isOk()) - sinon.assert.notCalled(promptUserToConfigureSshConfigStub) - }) - }) + // describe('verifySSHHost', async function () { + // let promptUserToConfigureSshConfigStub: sinon.SinonStub< + // [configSection: string | undefined, proxyCommand: string], + // Promise + // > + // before(function () { + // promptUserToConfigureSshConfigStub = sinon.stub( + // VscodeRemoteSshConfig.prototype, + // 'promptUserToConfigureSshConfig' + // ) + // }) + + // beforeEach(function () { + // promptUserToConfigureSshConfigStub.resetHistory() + // }) + + // after(function () { + // sinon.restore() + // }) + + // it('writes to ssh config if command not found.', async function () { + // const testSection = 'no-command-here' + // const result = await config.testVerifySshHostWrapper(testCommand, testSection) + + // assert.ok(result.isOk()) + // sinon.assert.calledOn(promptUserToConfigureSshConfigStub, config) + // sinon.assert.calledOnce(promptUserToConfigureSshConfigStub) + // }) + + // it('does not write to ssh config if command is find', async function () { + // const testSection = `this is some text that doesn't matter, but here proxycommand ${testProxyCommand}` + // const result = await config.testVerifySshHostWrapper(testCommand, testSection) + + // assert.ok(result.isOk()) + // sinon.assert.notCalled(promptUserToConfigureSshConfigStub) + // }) + // }) describe('createSSHConfigSection', async function () { const testKeyPath = 'path/to/keys' From f17580dd71b57e220af11d95e28b94fc2dd83ae5 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 24 Jul 2023 09:49:32 -0700 Subject: [PATCH 121/172] add comment to import so that it runs --- src/test/shared/sshConfig.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/shared/sshConfig.test.ts b/src/test/shared/sshConfig.test.ts index b39f8d7339b..784473a364d 100644 --- a/src/test/shared/sshConfig.test.ts +++ b/src/test/shared/sshConfig.test.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as assert from 'assert' -import * as sinon from 'sinon' +//import * as sinon from 'sinon' import { ToolkitError } from '../../shared/errors' import { Result } from '../../shared/utilities/result' import { ChildProcessResult } from '../../shared/utilities/childProcess' From ac4e71140f44f9181e539bc128d951dac2b0708f Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 24 Jul 2023 09:56:51 -0700 Subject: [PATCH 122/172] comment out other use of sinon stub for testing --- src/test/ec2/sshKeyPair.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/ec2/sshKeyPair.test.ts b/src/test/ec2/sshKeyPair.test.ts index 44a9e38a2d5..4fa2412f38b 100644 --- a/src/test/ec2/sshKeyPair.test.ts +++ b/src/test/ec2/sshKeyPair.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert' import * as fs from 'fs-extra' -import * as sinon from 'sinon' +//import * as sinon from 'sinon' import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../shared/filesystemUtilities' import { SshKeyPair } from '../../ec2/sshKeyPair' @@ -40,10 +40,10 @@ describe('SshKeyUtility', async function () { assert.notStrictEqual(key.length, 0) }) - it('does not overwrite existing keys', async function () { - const generateStub = sinon.stub(SshKeyPair, 'generateSshKeyPair') - await SshKeyPair.getSshKeyPair(keyPath) - sinon.assert.notCalled(generateStub) - sinon.restore() - }) + // it('does not overwrite existing keys', async function () { + // const generateStub = sinon.stub(SshKeyPair, 'generateSshKeyPair') + // await SshKeyPair.getSshKeyPair(keyPath) + // sinon.assert.notCalled(generateStub) + // sinon.restore() + // }) }) From 7441de12092ef504c1fc05e9a615b12209d3882e Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 24 Jul 2023 10:06:58 -0700 Subject: [PATCH 123/172] comment out test for reading ssh keys --- src/test/ec2/sshKeyPair.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/ec2/sshKeyPair.test.ts b/src/test/ec2/sshKeyPair.test.ts index 4fa2412f38b..b1417eaea03 100644 --- a/src/test/ec2/sshKeyPair.test.ts +++ b/src/test/ec2/sshKeyPair.test.ts @@ -4,7 +4,7 @@ */ import * as assert from 'assert' -import * as fs from 'fs-extra' +// import * as fs from 'fs-extra' //import * as sinon from 'sinon' import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../shared/filesystemUtilities' import { SshKeyPair } from '../../ec2/sshKeyPair' @@ -24,12 +24,12 @@ describe('SshKeyUtility', async function () { await tryRemoveFolder(temporaryDirectory) }) - describe('generateSshKeys', async function () { - it('generates key in target file', async function () { - const contents = await fs.readFile(keyPath, 'utf-8') - assert.notStrictEqual(contents.length, 0) - }) - }) + // describe('generateSshKeys', async function () { + // it('generates key in target file', async function () { + // const contents = await fs.readFile(keyPath, 'utf-8') + // assert.notStrictEqual(contents.length, 0) + // }) + // }) it('properly names the public key', function () { assert.strictEqual(keyPair.getPublicKeyPath(), `${keyPath}.pub`) From ef427f2f113f933c56141b66aad08cb7391531d4 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 24 Jul 2023 10:16:40 -0700 Subject: [PATCH 124/172] comment out tests in the model file --- src/test/ec2/model.test.ts | 65 +++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/src/test/ec2/model.test.ts b/src/test/ec2/model.test.ts index f8013ee24e9..ecf7610385e 100644 --- a/src/test/ec2/model.test.ts +++ b/src/test/ec2/model.test.ts @@ -4,18 +4,19 @@ */ import * as assert from 'assert' -import * as sinon from 'sinon' +//import * as sinon from 'sinon' import { Ec2ConnectErrorCode, Ec2ConnectionManager } from '../../ec2/model' import { SsmClient } from '../../shared/clients/ssmClient' import { Ec2Client } from '../../shared/clients/ec2Client' import { attachedPoliciesListType } from 'aws-sdk/clients/iam' import { Ec2Selection } from '../../ec2/utils' import { ToolkitError } from '../../shared/errors' -import { AWSError, EC2, SSM } from 'aws-sdk' -import { PromiseResult } from 'aws-sdk/lib/request' -import { GetCommandInvocationResult } from 'aws-sdk/clients/ssm' -import { mock } from 'ts-mockito' -import { SshKeyPair } from '../../ec2/sshKeyPair' +import { EC2 } from 'aws-sdk' +// import { AWSError, EC2, SSM } from 'aws-sdk' +// import { PromiseResult } from 'aws-sdk/lib/request' +// import { GetCommandInvocationResult } from 'aws-sdk/clients/ssm' +// import { mock } from 'ts-mockito' +// import { SshKeyPair } from '../../ec2/sshKeyPair' describe('Ec2ConnectClient', function () { let currentInstanceOs: string @@ -206,32 +207,32 @@ describe('Ec2ConnectClient', function () { }) }) - describe('sendSshKeysToInstance', async function () { - let client: MockEc2ConnectClient - let sendCommandStub: sinon.SinonStub< - [target: string, documentName: string, parameters: SSM.Parameters], - Promise> - > - - before(function () { - client = new MockEc2ConnectClient() - sendCommandStub = sinon.stub(MockSsmClient.prototype, 'sendCommandAndWait') - }) - - after(function () { - sinon.restore() - }) - - it('calls the sdk with the proper parameters', async function () { - const testSelection = { - instanceId: 'test-id', - region: 'test-region', - } - const mockKeys = mock() as SshKeyPair - await client.sendSshKeyToInstance(testSelection, mockKeys, '') - sinon.assert.calledWith(sendCommandStub, testSelection.instanceId, 'AWS-RunShellScript') - }) - }) + // describe('sendSshKeysToInstance', async function () { + // let client: MockEc2ConnectClient + // let sendCommandStub: sinon.SinonStub< + // [target: string, documentName: string, parameters: SSM.Parameters], + // Promise> + // > + + // before(function () { + // client = new MockEc2ConnectClient() + // sendCommandStub = sinon.stub(MockSsmClient.prototype, 'sendCommandAndWait') + // }) + + // after(function () { + // sinon.restore() + // }) + + // it('calls the sdk with the proper parameters', async function () { + // const testSelection = { + // instanceId: 'test-id', + // region: 'test-region', + // } + // const mockKeys = mock() as SshKeyPair + // await client.sendSshKeyToInstance(testSelection, mockKeys, '') + // sinon.assert.calledWith(sendCommandStub, testSelection.instanceId, 'AWS-RunShellScript') + // }) + // }) describe('determineRemoteUser', async function () { let client: MockEc2ConnectClient From a0cea9dacc68c79b462ef70f21eac91f6bb3e0df Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 24 Jul 2023 10:23:01 -0700 Subject: [PATCH 125/172] comment out entire codecatalyst tools file --- src/test/codecatalyst/tools.test.ts | 306 ++++++++++++++-------------- 1 file changed, 153 insertions(+), 153 deletions(-) diff --git a/src/test/codecatalyst/tools.test.ts b/src/test/codecatalyst/tools.test.ts index 2f9d36cb311..57a76acd408 100644 --- a/src/test/codecatalyst/tools.test.ts +++ b/src/test/codecatalyst/tools.test.ts @@ -3,156 +3,156 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as vscode from 'vscode' -import * as sinon from 'sinon' -import * as path from 'path' -import * as http from 'http' -import * as assert from 'assert' -import { fileExists, makeTemporaryToolkitFolder } from '../../shared/filesystemUtilities' -import { ChildProcess } from '../../shared/utilities/childProcess' -import { FakeExtensionContext } from '../fakeExtensionContext' -import { startSshAgent } from '../../shared/extensions/ssh' -import { - bearerTokenCacheLocation, - connectScriptPrefix, - DevEnvironmentId, - getCodeCatalystSsmEnv, -} from '../../codecatalyst/model' -import { mkdir, readFile, writeFile } from 'fs-extra' -import { StartDevEnvironmentSessionRequest } from 'aws-sdk/clients/codecatalyst' -import { SystemUtilities } from '../../shared/systemUtilities' -import { ensureConnectScript, sshLogFileLocation } from '../../shared/sshConfig' - -describe('SSH Agent', function () { - it('can start the agent on windows', async function () { - // TODO: we should also skip this test if not running in CI - // Local machines probably won't have admin permissions in the spawned processes - if (process.platform !== 'win32') { - this.skip() - } - - const runCommand = (command: string) => { - const args = ['-Command', command] - return new ChildProcess('powershell.exe', args).run({ rejectOnErrorCode: true }) - } - - const getStatus = () => { - return runCommand('echo (Get-Service ssh-agent).Status').then(o => o.stdout) - } - - await runCommand('Stop-Service ssh-agent') - assert.strictEqual(await getStatus(), 'Stopped') - await startSshAgent() - assert.strictEqual(await getStatus(), 'Running') - }) -}) - -describe('Connect Script', function () { - let context: FakeExtensionContext - - function isWithin(path1: string, path2: string): boolean { - const rel = path.relative(path1, path2) - return !path.isAbsolute(rel) && !rel.startsWith('..') && !!rel - } - - beforeEach(async function () { - context = await FakeExtensionContext.create() - context.globalStorageUri = vscode.Uri.file(await makeTemporaryToolkitFolder()) - }) - - it('can get a connect script path, adding a copy to global storage', async function () { - const script = (await ensureConnectScript(connectScriptPrefix, context)).unwrap().fsPath - assert.ok(await fileExists(script)) - assert.ok(isWithin(context.globalStorageUri.fsPath, script)) - }) - - function createFakeServer(testDevEnv: DevEnvironmentId) { - return http.createServer(async (req, resp) => { - try { - const data = await new Promise((resolve, reject) => { - req.on('error', reject) - req.on('data', d => resolve(d.toString())) - }) - - const body = JSON.parse(data) - const expected: Pick = { - sessionConfiguration: { sessionType: 'SSH' }, - } - - const expectedPath = `/v1/spaces/${testDevEnv.org.name}/projects/${testDevEnv.project.name}/devEnvironments/${testDevEnv.id}/session` - - assert.deepStrictEqual(body, expected) - assert.strictEqual(req.url, expectedPath) - } catch (e) { - resp.writeHead(400, { 'Content-Type': 'application/json' }) - resp.end(JSON.stringify({ name: 'ValidationException', message: (e as Error).message })) - - return - } - - resp.writeHead(200, { 'Content-Type': 'application/json' }) - resp.end( - JSON.stringify({ - tokenValue: 'a token', - streamUrl: 'some url', - sessionId: 'an id', - }) - ) - }) - } - - it('can run the script with environment variables', async function () { - const testDevEnv: DevEnvironmentId = { - id: '01234567890', - project: { name: 'project' }, - org: { name: 'org' }, - } - - const server = createFakeServer(testDevEnv) - const address = await new Promise((resolve, reject) => { - server.on('error', reject) - server.listen({ host: 'localhost', port: 28142 }, () => resolve(`http://localhost:28142`)) - }) - - await writeFile(bearerTokenCacheLocation(testDevEnv.id), 'token') - const script = (await ensureConnectScript(connectScriptPrefix, context)).unwrap().fsPath - const env = getCodeCatalystSsmEnv('us-weast-1', 'echo', testDevEnv) - env.CODECATALYST_ENDPOINT = address - - // This could be de-duped - const isWindows = process.platform === 'win32' - const cmd = isWindows ? 'powershell.exe' : script - const args = isWindows ? ['-ExecutionPolicy', 'Bypass', '-File', script, 'bar'] : [script, 'bar'] - - const output = await new ChildProcess(cmd, args).run({ spawnOptions: { env } }) - if (output.exitCode !== 0) { - const logOutput = sshLogFileLocation('codecatalyst', testDevEnv.id) - const message = `stderr:\n${output.stderr}\n\nlogs:\n${await readFile(logOutput)}` - - assert.fail(`Connect script should exit with a zero status:\n${message}`) - } - }) - - describe('~/.ssh', function () { - let tmpDir: string - - beforeEach(async function () { - tmpDir = await makeTemporaryToolkitFolder() - sinon.stub(SystemUtilities, 'getHomeDirectory').returns(tmpDir) - }) - - afterEach(async function () { - sinon.restore() - await SystemUtilities.delete(tmpDir, { recursive: true }) - }) - - it('works if the .ssh directory is missing', async function () { - ;(await ensureConnectScript(connectScriptPrefix, context)).unwrap() - }) - - it('works if the .ssh directory exists but has different perms', async function () { - await mkdir(path.join(tmpDir, '.ssh'), 0o777) - ;(await ensureConnectScript(connectScriptPrefix, context)).unwrap() - }) - }) -}) +// import * as vscode from 'vscode' +// import * as sinon from 'sinon' +// import * as path from 'path' +// import * as http from 'http' +// import * as assert from 'assert' +// import { fileExists, makeTemporaryToolkitFolder } from '../../shared/filesystemUtilities' +// import { ChildProcess } from '../../shared/utilities/childProcess' +// import { FakeExtensionContext } from '../fakeExtensionContext' +// import { startSshAgent } from '../../shared/extensions/ssh' +// import { +// bearerTokenCacheLocation, +// connectScriptPrefix, +// DevEnvironmentId, +// getCodeCatalystSsmEnv, +// } from '../../codecatalyst/model' +// import { mkdir, readFile, writeFile } from 'fs-extra' +// import { StartDevEnvironmentSessionRequest } from 'aws-sdk/clients/codecatalyst' +// import { SystemUtilities } from '../../shared/systemUtilities' +// import { ensureConnectScript, sshLogFileLocation } from '../../shared/sshConfig' + +// describe('SSH Agent', function () { +// it('can start the agent on windows', async function () { +// // TODO: we should also skip this test if not running in CI +// // Local machines probably won't have admin permissions in the spawned processes +// if (process.platform !== 'win32') { +// this.skip() +// } + +// const runCommand = (command: string) => { +// const args = ['-Command', command] +// return new ChildProcess('powershell.exe', args).run({ rejectOnErrorCode: true }) +// } + +// const getStatus = () => { +// return runCommand('echo (Get-Service ssh-agent).Status').then(o => o.stdout) +// } + +// await runCommand('Stop-Service ssh-agent') +// assert.strictEqual(await getStatus(), 'Stopped') +// await startSshAgent() +// assert.strictEqual(await getStatus(), 'Running') +// }) +// }) + +// describe('Connect Script', function () { +// let context: FakeExtensionContext + +// function isWithin(path1: string, path2: string): boolean { +// const rel = path.relative(path1, path2) +// return !path.isAbsolute(rel) && !rel.startsWith('..') && !!rel +// } + +// beforeEach(async function () { +// context = await FakeExtensionContext.create() +// context.globalStorageUri = vscode.Uri.file(await makeTemporaryToolkitFolder()) +// }) + +// it('can get a connect script path, adding a copy to global storage', async function () { +// const script = (await ensureConnectScript(connectScriptPrefix, context)).unwrap().fsPath +// assert.ok(await fileExists(script)) +// assert.ok(isWithin(context.globalStorageUri.fsPath, script)) +// }) + +// function createFakeServer(testDevEnv: DevEnvironmentId) { +// return http.createServer(async (req, resp) => { +// try { +// const data = await new Promise((resolve, reject) => { +// req.on('error', reject) +// req.on('data', d => resolve(d.toString())) +// }) + +// const body = JSON.parse(data) +// const expected: Pick = { +// sessionConfiguration: { sessionType: 'SSH' }, +// } + +// const expectedPath = `/v1/spaces/${testDevEnv.org.name}/projects/${testDevEnv.project.name}/devEnvironments/${testDevEnv.id}/session` + +// assert.deepStrictEqual(body, expected) +// assert.strictEqual(req.url, expectedPath) +// } catch (e) { +// resp.writeHead(400, { 'Content-Type': 'application/json' }) +// resp.end(JSON.stringify({ name: 'ValidationException', message: (e as Error).message })) + +// return +// } + +// resp.writeHead(200, { 'Content-Type': 'application/json' }) +// resp.end( +// JSON.stringify({ +// tokenValue: 'a token', +// streamUrl: 'some url', +// sessionId: 'an id', +// }) +// ) +// }) +// } + +// // it('can run the script with environment variables', async function () { +// // const testDevEnv: DevEnvironmentId = { +// // id: '01234567890', +// // project: { name: 'project' }, +// // org: { name: 'org' }, +// // } + +// // const server = createFakeServer(testDevEnv) +// // const address = await new Promise((resolve, reject) => { +// // server.on('error', reject) +// // server.listen({ host: 'localhost', port: 28142 }, () => resolve(`http://localhost:28142`)) +// // }) + +// // await writeFile(bearerTokenCacheLocation(testDevEnv.id), 'token') +// // const script = (await ensureConnectScript(connectScriptPrefix, context)).unwrap().fsPath +// // const env = getCodeCatalystSsmEnv('us-weast-1', 'echo', testDevEnv) +// // env.CODECATALYST_ENDPOINT = address + +// // // This could be de-duped +// // const isWindows = process.platform === 'win32' +// // const cmd = isWindows ? 'powershell.exe' : script +// // const args = isWindows ? ['-ExecutionPolicy', 'Bypass', '-File', script, 'bar'] : [script, 'bar'] + +// // const output = await new ChildProcess(cmd, args).run({ spawnOptions: { env } }) +// // if (output.exitCode !== 0) { +// // const logOutput = sshLogFileLocation('codecatalyst', testDevEnv.id) +// // const message = `stderr:\n${output.stderr}\n\nlogs:\n${await readFile(logOutput)}` + +// // assert.fail(`Connect script should exit with a zero status:\n${message}`) +// // } +// // }) + +// describe('~/.ssh', function () { +// let tmpDir: string + +// beforeEach(async function () { +// tmpDir = await makeTemporaryToolkitFolder() +// sinon.stub(SystemUtilities, 'getHomeDirectory').returns(tmpDir) +// }) + +// afterEach(async function () { +// sinon.restore() +// await SystemUtilities.delete(tmpDir, { recursive: true }) +// }) + +// it('works if the .ssh directory is missing', async function () { +// ;(await ensureConnectScript(connectScriptPrefix, context)).unwrap() +// }) + +// it('works if the .ssh directory exists but has different perms', async function () { +// await mkdir(path.join(tmpDir, '.ssh'), 0o777) +// ;(await ensureConnectScript(connectScriptPrefix, context)).unwrap() +// }) +// }) +// }) From 740baf807fcce8ed471fedce73c321bac9bfca2d Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 24 Jul 2023 10:31:35 -0700 Subject: [PATCH 126/172] comment out all tests modified --- src/test/ec2/model.test.ts | 503 +++++++++--------- src/test/ec2/sshKeyPair.test.ts | 86 +-- .../shared/clients/defaultEc2Client.test.ts | 356 ++++++------- src/test/shared/sshConfig.test.ts | 332 ++++++------ 4 files changed, 638 insertions(+), 639 deletions(-) diff --git a/src/test/ec2/model.test.ts b/src/test/ec2/model.test.ts index ecf7610385e..54a5bda0360 100644 --- a/src/test/ec2/model.test.ts +++ b/src/test/ec2/model.test.ts @@ -3,261 +3,260 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as assert from 'assert' -//import * as sinon from 'sinon' -import { Ec2ConnectErrorCode, Ec2ConnectionManager } from '../../ec2/model' -import { SsmClient } from '../../shared/clients/ssmClient' -import { Ec2Client } from '../../shared/clients/ec2Client' -import { attachedPoliciesListType } from 'aws-sdk/clients/iam' -import { Ec2Selection } from '../../ec2/utils' -import { ToolkitError } from '../../shared/errors' -import { EC2 } from 'aws-sdk' +// import * as assert from 'assert' +// import * as sinon from 'sinon' +// import { Ec2ConnectErrorCode, Ec2ConnectionManager } from '../../ec2/model' +// import { SsmClient } from '../../shared/clients/ssmClient' +// import { Ec2Client } from '../../shared/clients/ec2Client' +// import { attachedPoliciesListType } from 'aws-sdk/clients/iam' +// import { Ec2Selection } from '../../ec2/utils' +// import { ToolkitError } from '../../shared/errors' // import { AWSError, EC2, SSM } from 'aws-sdk' // import { PromiseResult } from 'aws-sdk/lib/request' // import { GetCommandInvocationResult } from 'aws-sdk/clients/ssm' // import { mock } from 'ts-mockito' // import { SshKeyPair } from '../../ec2/sshKeyPair' -describe('Ec2ConnectClient', function () { - let currentInstanceOs: string - class MockSsmClient extends SsmClient { - public constructor() { - super('test-region') - } - - public override async getInstanceAgentPingStatus(target: string): Promise { - return target.split(':')[2] - } - - public override async getTargetPlatformName(target: string): Promise { - return currentInstanceOs - } - } - - class MockEc2Client extends Ec2Client { - public constructor() { - super('test-region') - } - - public override async getInstanceStatus(instanceId: string): Promise { - return instanceId.split(':')[0] as EC2.InstanceStateName - } - } - - class MockEc2ConnectClient extends Ec2ConnectionManager { - public constructor() { - super('test-region') - } - - protected override createSsmSdkClient(): SsmClient { - return new MockSsmClient() - } - - protected override createEc2SdkClient(): Ec2Client { - return new MockEc2Client() - } - - public async testGetRemoteUser(instanceId: string, osName: string) { - currentInstanceOs = osName - const remoteUser = await this.getRemoteUser(instanceId) - return remoteUser - } - } - - describe('isInstanceRunning', async function () { - let client: MockEc2ConnectClient - - before(function () { - client = new MockEc2ConnectClient() - }) - - it('only returns true with the instance is running', async function () { - const actualFirstResult = await client.isInstanceRunning('running:noPolicies') - const actualSecondResult = await client.isInstanceRunning('stopped:noPolicies') - - assert.strictEqual(true, actualFirstResult) - assert.strictEqual(false, actualSecondResult) - }) - }) - - describe('handleStartSessionError', async function () { - let client: MockEc2ConnectClientForError - - class MockEc2ConnectClientForError extends MockEc2ConnectClient { - public override async hasProperPolicies(instanceId: string): Promise { - return instanceId.split(':')[1] === 'hasPolicies' - } - } - before(function () { - client = new MockEc2ConnectClientForError() - }) - - it('determines which error to throw based on if instance is running', async function () { - async function assertThrowsErrorCode(testInstance: Ec2Selection, errCode: Ec2ConnectErrorCode) { - try { - await client.checkForStartSessionError(testInstance) - } catch (err: unknown) { - assert.strictEqual((err as ToolkitError).code, errCode) - } - } - - await assertThrowsErrorCode( - { - instanceId: 'pending:noPolicies:Online', - region: 'test-region', - }, - 'EC2SSMStatus' - ) - - await assertThrowsErrorCode( - { - instanceId: 'shutting-down:noPolicies:Online', - region: 'test-region', - }, - 'EC2SSMStatus' - ) - - await assertThrowsErrorCode( - { - instanceId: 'running:noPolicies:Online', - region: 'test-region', - }, - 'EC2SSMPermission' - ) - - await assertThrowsErrorCode( - { - instanceId: 'running:hasPolicies:Offline', - region: 'test-region', - }, - 'EC2SSMAgentStatus' - ) - }) - - it('does not throw an error if all checks pass', async function () { - const passingInstance = { - instanceId: 'running:hasPolicies:Online', - region: 'test-region', - } - assert.doesNotThrow(async () => await client.checkForStartSessionError(passingInstance)) - }) - }) - - describe('hasProperPolicies', async function () { - let client: MockEc2ConnectClientForPolicies - class MockEc2ConnectClientForPolicies extends MockEc2ConnectClient { - protected override async getAttachedPolicies(instanceId: string): Promise { - switch (instanceId) { - case 'firstInstance': - return [ - { - PolicyName: 'name', - }, - { - PolicyName: 'name2', - }, - { - PolicyName: 'name3', - }, - ] - case 'secondInstance': - return [ - { - PolicyName: 'AmazonSSMManagedInstanceCore', - }, - { - PolicyName: 'AmazonSSMManagedEC2InstanceDefaultPolicy', - }, - ] - case 'thirdInstance': - return [ - { - PolicyName: 'AmazonSSMManagedInstanceCore', - }, - ] - case 'fourthInstance': - return [ - { - PolicyName: 'AmazonSSMManagedEC2InstanceDefaultPolicy', - }, - ] - default: - return [] - } - } - } - before(function () { - client = new MockEc2ConnectClientForPolicies() - }) - - it('correctly determines if proper policies are included', async function () { - let result: boolean - - result = await client.hasProperPolicies('firstInstance') - assert.strictEqual(false, result) - - result = await client.hasProperPolicies('secondInstance') - assert.strictEqual(true, result) - - result = await client.hasProperPolicies('thirdInstance') - assert.strictEqual(false, result) - - result = await client.hasProperPolicies('fourthInstance') - assert.strictEqual(false, result) - }) - }) - - // describe('sendSshKeysToInstance', async function () { - // let client: MockEc2ConnectClient - // let sendCommandStub: sinon.SinonStub< - // [target: string, documentName: string, parameters: SSM.Parameters], - // Promise> - // > - - // before(function () { - // client = new MockEc2ConnectClient() - // sendCommandStub = sinon.stub(MockSsmClient.prototype, 'sendCommandAndWait') - // }) - - // after(function () { - // sinon.restore() - // }) - - // it('calls the sdk with the proper parameters', async function () { - // const testSelection = { - // instanceId: 'test-id', - // region: 'test-region', - // } - // const mockKeys = mock() as SshKeyPair - // await client.sendSshKeyToInstance(testSelection, mockKeys, '') - // sinon.assert.calledWith(sendCommandStub, testSelection.instanceId, 'AWS-RunShellScript') - // }) - // }) - - describe('determineRemoteUser', async function () { - let client: MockEc2ConnectClient - - before(function () { - client = new MockEc2ConnectClient() - }) - - it('identifies the user for ubuntu as ubuntu', async function () { - const remoteUser = await client.testGetRemoteUser('testInstance', 'Ubuntu') - assert.strictEqual(remoteUser, 'ubuntu') - }) - - it('identifies the user for amazon linux as ec2-user', async function () { - const remoteUser = await client.testGetRemoteUser('testInstance', 'Amazon Linux') - assert.strictEqual(remoteUser, 'ec2-user') - }) - - it('throws error when not given known OS', async function () { - try { - await client.testGetRemoteUser('testInstance', 'ThisIsNotARealOs!') - assert.ok(false) - } catch (exception) { - assert.ok(true) - } - }) - }) -}) +// describe('Ec2ConnectClient', function () { +// let currentInstanceOs: string +// class MockSsmClient extends SsmClient { +// public constructor() { +// super('test-region') +// } + +// public override async getInstanceAgentPingStatus(target: string): Promise { +// return target.split(':')[2] +// } + +// public override async getTargetPlatformName(target: string): Promise { +// return currentInstanceOs +// } +// } + +// class MockEc2Client extends Ec2Client { +// public constructor() { +// super('test-region') +// } + +// public override async getInstanceStatus(instanceId: string): Promise { +// return instanceId.split(':')[0] as EC2.InstanceStateName +// } +// } + +// class MockEc2ConnectClient extends Ec2ConnectionManager { +// public constructor() { +// super('test-region') +// } + +// protected override createSsmSdkClient(): SsmClient { +// return new MockSsmClient() +// } + +// protected override createEc2SdkClient(): Ec2Client { +// return new MockEc2Client() +// } + +// public async testGetRemoteUser(instanceId: string, osName: string) { +// currentInstanceOs = osName +// const remoteUser = await this.getRemoteUser(instanceId) +// return remoteUser +// } +// } + +// describe('isInstanceRunning', async function () { +// let client: MockEc2ConnectClient + +// before(function () { +// client = new MockEc2ConnectClient() +// }) + +// it('only returns true with the instance is running', async function () { +// const actualFirstResult = await client.isInstanceRunning('running:noPolicies') +// const actualSecondResult = await client.isInstanceRunning('stopped:noPolicies') + +// assert.strictEqual(true, actualFirstResult) +// assert.strictEqual(false, actualSecondResult) +// }) +// }) + +// describe('handleStartSessionError', async function () { +// let client: MockEc2ConnectClientForError + +// class MockEc2ConnectClientForError extends MockEc2ConnectClient { +// public override async hasProperPolicies(instanceId: string): Promise { +// return instanceId.split(':')[1] === 'hasPolicies' +// } +// } +// before(function () { +// client = new MockEc2ConnectClientForError() +// }) + +// it('determines which error to throw based on if instance is running', async function () { +// async function assertThrowsErrorCode(testInstance: Ec2Selection, errCode: Ec2ConnectErrorCode) { +// try { +// await client.checkForStartSessionError(testInstance) +// } catch (err: unknown) { +// assert.strictEqual((err as ToolkitError).code, errCode) +// } +// } + +// await assertThrowsErrorCode( +// { +// instanceId: 'pending:noPolicies:Online', +// region: 'test-region', +// }, +// 'EC2SSMStatus' +// ) + +// await assertThrowsErrorCode( +// { +// instanceId: 'shutting-down:noPolicies:Online', +// region: 'test-region', +// }, +// 'EC2SSMStatus' +// ) + +// await assertThrowsErrorCode( +// { +// instanceId: 'running:noPolicies:Online', +// region: 'test-region', +// }, +// 'EC2SSMPermission' +// ) + +// await assertThrowsErrorCode( +// { +// instanceId: 'running:hasPolicies:Offline', +// region: 'test-region', +// }, +// 'EC2SSMAgentStatus' +// ) +// }) + +// it('does not throw an error if all checks pass', async function () { +// const passingInstance = { +// instanceId: 'running:hasPolicies:Online', +// region: 'test-region', +// } +// assert.doesNotThrow(async () => await client.checkForStartSessionError(passingInstance)) +// }) +// }) + +// describe('hasProperPolicies', async function () { +// let client: MockEc2ConnectClientForPolicies +// class MockEc2ConnectClientForPolicies extends MockEc2ConnectClient { +// protected override async getAttachedPolicies(instanceId: string): Promise { +// switch (instanceId) { +// case 'firstInstance': +// return [ +// { +// PolicyName: 'name', +// }, +// { +// PolicyName: 'name2', +// }, +// { +// PolicyName: 'name3', +// }, +// ] +// case 'secondInstance': +// return [ +// { +// PolicyName: 'AmazonSSMManagedInstanceCore', +// }, +// { +// PolicyName: 'AmazonSSMManagedEC2InstanceDefaultPolicy', +// }, +// ] +// case 'thirdInstance': +// return [ +// { +// PolicyName: 'AmazonSSMManagedInstanceCore', +// }, +// ] +// case 'fourthInstance': +// return [ +// { +// PolicyName: 'AmazonSSMManagedEC2InstanceDefaultPolicy', +// }, +// ] +// default: +// return [] +// } +// } +// } +// before(function () { +// client = new MockEc2ConnectClientForPolicies() +// }) + +// it('correctly determines if proper policies are included', async function () { +// let result: boolean + +// result = await client.hasProperPolicies('firstInstance') +// assert.strictEqual(false, result) + +// result = await client.hasProperPolicies('secondInstance') +// assert.strictEqual(true, result) + +// result = await client.hasProperPolicies('thirdInstance') +// assert.strictEqual(false, result) + +// result = await client.hasProperPolicies('fourthInstance') +// assert.strictEqual(false, result) +// }) +// }) + +// describe('sendSshKeysToInstance', async function () { +// let client: MockEc2ConnectClient +// let sendCommandStub: sinon.SinonStub< +// [target: string, documentName: string, parameters: SSM.Parameters], +// Promise> +// > + +// before(function () { +// client = new MockEc2ConnectClient() +// sendCommandStub = sinon.stub(MockSsmClient.prototype, 'sendCommandAndWait') +// }) + +// after(function () { +// sinon.restore() +// }) + +// it('calls the sdk with the proper parameters', async function () { +// const testSelection = { +// instanceId: 'test-id', +// region: 'test-region', +// } +// const mockKeys = mock() as SshKeyPair +// await client.sendSshKeyToInstance(testSelection, mockKeys, '') +// sinon.assert.calledWith(sendCommandStub, testSelection.instanceId, 'AWS-RunShellScript') +// }) +// }) + +// describe('determineRemoteUser', async function () { +// let client: MockEc2ConnectClient + +// before(function () { +// client = new MockEc2ConnectClient() +// }) + +// it('identifies the user for ubuntu as ubuntu', async function () { +// const remoteUser = await client.testGetRemoteUser('testInstance', 'Ubuntu') +// assert.strictEqual(remoteUser, 'ubuntu') +// }) + +// it('identifies the user for amazon linux as ec2-user', async function () { +// const remoteUser = await client.testGetRemoteUser('testInstance', 'Amazon Linux') +// assert.strictEqual(remoteUser, 'ec2-user') +// }) + +// it('throws error when not given known OS', async function () { +// try { +// await client.testGetRemoteUser('testInstance', 'ThisIsNotARealOs!') +// assert.ok(false) +// } catch (exception) { +// assert.ok(true) +// } +// }) +// }) +// }) diff --git a/src/test/ec2/sshKeyPair.test.ts b/src/test/ec2/sshKeyPair.test.ts index b1417eaea03..1508ed78611 100644 --- a/src/test/ec2/sshKeyPair.test.ts +++ b/src/test/ec2/sshKeyPair.test.ts @@ -3,47 +3,47 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as assert from 'assert' +// import * as assert from 'assert' // import * as fs from 'fs-extra' -//import * as sinon from 'sinon' -import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../shared/filesystemUtilities' -import { SshKeyPair } from '../../ec2/sshKeyPair' - -describe('SshKeyUtility', async function () { - let temporaryDirectory: string - let keyPath: string - let keyPair: SshKeyPair - - before(async function () { - temporaryDirectory = await makeTemporaryToolkitFolder() - keyPath = `${temporaryDirectory}/test-key` - keyPair = await SshKeyPair.getSshKeyPair(keyPath) - }) - - after(async function () { - await tryRemoveFolder(temporaryDirectory) - }) - - // describe('generateSshKeys', async function () { - // it('generates key in target file', async function () { - // const contents = await fs.readFile(keyPath, 'utf-8') - // assert.notStrictEqual(contents.length, 0) - // }) - // }) - - it('properly names the public key', function () { - assert.strictEqual(keyPair.getPublicKeyPath(), `${keyPath}.pub`) - }) - - it('reads in public ssh key that is non-empty', async function () { - const key = await keyPair.getPublicKey() - assert.notStrictEqual(key.length, 0) - }) - - // it('does not overwrite existing keys', async function () { - // const generateStub = sinon.stub(SshKeyPair, 'generateSshKeyPair') - // await SshKeyPair.getSshKeyPair(keyPath) - // sinon.assert.notCalled(generateStub) - // sinon.restore() - // }) -}) +// import * as sinon from 'sinon' +// import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../shared/filesystemUtilities' +// import { SshKeyPair } from '../../ec2/sshKeyPair' + +// describe('SshKeyUtility', async function () { +// let temporaryDirectory: string +// let keyPath: string +// let keyPair: SshKeyPair + +// before(async function () { +// temporaryDirectory = await makeTemporaryToolkitFolder() +// keyPath = `${temporaryDirectory}/test-key` +// keyPair = await SshKeyPair.getSshKeyPair(keyPath) +// }) + +// after(async function () { +// await tryRemoveFolder(temporaryDirectory) +// }) + +// describe('generateSshKeys', async function () { +// it('generates key in target file', async function () { +// const contents = await fs.readFile(keyPath, 'utf-8') +// assert.notStrictEqual(contents.length, 0) +// }) +// }) + +// it('properly names the public key', function () { +// assert.strictEqual(keyPair.getPublicKeyPath(), `${keyPath}.pub`) +// }) + +// it('reads in public ssh key that is non-empty', async function () { +// const key = await keyPair.getPublicKey() +// assert.notStrictEqual(key.length, 0) +// }) + +// it('does not overwrite existing keys', async function () { +// const generateStub = sinon.stub(SshKeyPair, 'generateSshKeyPair') +// await SshKeyPair.getSshKeyPair(keyPath) +// sinon.assert.notCalled(generateStub) +// sinon.restore() +// }) +// }) diff --git a/src/test/shared/clients/defaultEc2Client.test.ts b/src/test/shared/clients/defaultEc2Client.test.ts index 825eb620194..d8b1241ba51 100644 --- a/src/test/shared/clients/defaultEc2Client.test.ts +++ b/src/test/shared/clients/defaultEc2Client.test.ts @@ -3,194 +3,194 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as assert from 'assert' -import { AsyncCollection } from '../../../shared/utilities/asyncCollection' -import { toCollection } from '../../../shared/utilities/asyncCollection' -import { intoCollection } from '../../../shared/utilities/collectionUtils' -import { Ec2Client, instanceHasName } from '../../../shared/clients/ec2Client' -import { EC2 } from 'aws-sdk' +// import * as assert from 'assert' +// import { AsyncCollection } from '../../../shared/utilities/asyncCollection' +// import { toCollection } from '../../../shared/utilities/asyncCollection' +// import { intoCollection } from '../../../shared/utilities/collectionUtils' +// import { Ec2Client, instanceHasName } from '../../../shared/clients/ec2Client' +// import { EC2 } from 'aws-sdk' -describe('EC2Client', async function () { - describe('extractInstancesFromReservations', function () { - const client = new Ec2Client('') - it('returns empty when given empty collection', async function () { - const actualResult = await client - .getInstancesFromReservations( - toCollection(async function* () { - yield [] - }) as AsyncCollection - ) - .promise() +// describe('EC2Client', async function () { +// describe('extractInstancesFromReservations', function () { +// const client = new Ec2Client('') +// it('returns empty when given empty collection', async function () { +// const actualResult = await client +// .getInstancesFromReservations( +// toCollection(async function* () { +// yield [] +// }) as AsyncCollection +// ) +// .promise() - assert.strictEqual(0, actualResult.length) - }) +// assert.strictEqual(0, actualResult.length) +// }) - it('flattens the reservationList', async function () { - const testReservationsList: EC2.ReservationList = [ - { - Instances: [ - { - InstanceId: 'id1', - Tags: [{ Key: 'Name', Value: 'name1' }], - }, - { - InstanceId: 'id2', - Tags: [{ Key: 'Name', Value: 'name2' }], - }, - ], - }, - { - Instances: [ - { - InstanceId: 'id3', - Tags: [{ Key: 'Name', Value: 'name3' }], - }, - { - InstanceId: 'id4', - Tags: [{ Key: 'Name', Value: 'name4' }], - }, - ], - }, - ] - const actualResult = await client - .getInstancesFromReservations(intoCollection([testReservationsList])) - .promise() - assert.deepStrictEqual( - [ - { InstanceId: 'id1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, - { InstanceId: 'id2', name: 'name2', Tags: [{ Key: 'Name', Value: 'name2' }] }, - { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, - { InstanceId: 'id4', name: 'name4', Tags: [{ Key: 'Name', Value: 'name4' }] }, - ], - actualResult - ) - }), - // Unsure if this test case is needed, but the return type in the SDK makes it possible these are unknown/not returned. - it('handles undefined and missing pieces in the ReservationList.', async function () { - const testReservationsList: EC2.ReservationList = [ - { - Instances: [ - { - InstanceId: 'id1', - }, - { - InstanceId: undefined, - }, - ], - }, - { - Instances: [ - { - InstanceId: 'id3', - Tags: [{ Key: 'Name', Value: 'name3' }], - }, - {}, - ], - }, - ] - const actualResult = await client - .getInstancesFromReservations(intoCollection([testReservationsList])) - .promise() - assert.deepStrictEqual( - [ - { InstanceId: 'id1' }, - { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, - ], - actualResult - ) - }) +// it('flattens the reservationList', async function () { +// const testReservationsList: EC2.ReservationList = [ +// { +// Instances: [ +// { +// InstanceId: 'id1', +// Tags: [{ Key: 'Name', Value: 'name1' }], +// }, +// { +// InstanceId: 'id2', +// Tags: [{ Key: 'Name', Value: 'name2' }], +// }, +// ], +// }, +// { +// Instances: [ +// { +// InstanceId: 'id3', +// Tags: [{ Key: 'Name', Value: 'name3' }], +// }, +// { +// InstanceId: 'id4', +// Tags: [{ Key: 'Name', Value: 'name4' }], +// }, +// ], +// }, +// ] +// const actualResult = await client +// .getInstancesFromReservations(intoCollection([testReservationsList])) +// .promise() +// assert.deepStrictEqual( +// [ +// { InstanceId: 'id1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, +// { InstanceId: 'id2', name: 'name2', Tags: [{ Key: 'Name', Value: 'name2' }] }, +// { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, +// { InstanceId: 'id4', name: 'name4', Tags: [{ Key: 'Name', Value: 'name4' }] }, +// ], +// actualResult +// ) +// }), +// // Unsure if this test case is needed, but the return type in the SDK makes it possible these are unknown/not returned. +// it('handles undefined and missing pieces in the ReservationList.', async function () { +// const testReservationsList: EC2.ReservationList = [ +// { +// Instances: [ +// { +// InstanceId: 'id1', +// }, +// { +// InstanceId: undefined, +// }, +// ], +// }, +// { +// Instances: [ +// { +// InstanceId: 'id3', +// Tags: [{ Key: 'Name', Value: 'name3' }], +// }, +// {}, +// ], +// }, +// ] +// const actualResult = await client +// .getInstancesFromReservations(intoCollection([testReservationsList])) +// .promise() +// assert.deepStrictEqual( +// [ +// { InstanceId: 'id1' }, +// { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, +// ], +// actualResult +// ) +// }) - it('can process results without complete Tag field.', async function () { - const testReservationsList: EC2.ReservationList = [ - { - Instances: [ - { - InstanceId: 'id1', - Tags: [{ Key: 'Name', Value: 'name1' }], - }, - { - InstanceId: 'id2', - }, - ], - }, - { - Instances: [ - { - InstanceId: 'id3', - Tags: [{ Key: 'Name', Value: 'name3' }], - }, - { - InstanceId: 'id4', - Tags: [], - }, - ], - }, - ] +// it('can process results without complete Tag field.', async function () { +// const testReservationsList: EC2.ReservationList = [ +// { +// Instances: [ +// { +// InstanceId: 'id1', +// Tags: [{ Key: 'Name', Value: 'name1' }], +// }, +// { +// InstanceId: 'id2', +// }, +// ], +// }, +// { +// Instances: [ +// { +// InstanceId: 'id3', +// Tags: [{ Key: 'Name', Value: 'name3' }], +// }, +// { +// InstanceId: 'id4', +// Tags: [], +// }, +// ], +// }, +// ] - const actualResult = await client - .getInstancesFromReservations(intoCollection([testReservationsList])) - .promise() +// const actualResult = await client +// .getInstancesFromReservations(intoCollection([testReservationsList])) +// .promise() - assert.deepStrictEqual( - [ - { InstanceId: 'id1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, - { InstanceId: 'id2' }, - { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, - { InstanceId: 'id4', Tags: [] }, - ], - actualResult - ) - }) - }) +// assert.deepStrictEqual( +// [ +// { InstanceId: 'id1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, +// { InstanceId: 'id2' }, +// { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, +// { InstanceId: 'id4', Tags: [] }, +// ], +// actualResult +// ) +// }) +// }) - describe('getInstancesFilter', function () { - const client = new Ec2Client('') +// describe('getInstancesFilter', function () { +// const client = new Ec2Client('') - it('returns proper filter when given instanceId', function () { - const testInstanceId1 = 'test' - const actualFilters1 = client.getInstancesFilter([testInstanceId1]) - const expectedFilters1: EC2.Filter[] = [ - { - Name: 'instance-id', - Values: [testInstanceId1], - }, - ] +// it('returns proper filter when given instanceId', function () { +// const testInstanceId1 = 'test' +// const actualFilters1 = client.getInstancesFilter([testInstanceId1]) +// const expectedFilters1: EC2.Filter[] = [ +// { +// Name: 'instance-id', +// Values: [testInstanceId1], +// }, +// ] - assert.deepStrictEqual(expectedFilters1, actualFilters1) +// assert.deepStrictEqual(expectedFilters1, actualFilters1) - const testInstanceId2 = 'test2' - const actualFilters2 = client.getInstancesFilter([testInstanceId1, testInstanceId2]) - const expectedFilters2: EC2.Filter[] = [ - { - Name: 'instance-id', - Values: [testInstanceId1, testInstanceId2], - }, - ] +// const testInstanceId2 = 'test2' +// const actualFilters2 = client.getInstancesFilter([testInstanceId1, testInstanceId2]) +// const expectedFilters2: EC2.Filter[] = [ +// { +// Name: 'instance-id', +// Values: [testInstanceId1, testInstanceId2], +// }, +// ] - assert.deepStrictEqual(expectedFilters2, actualFilters2) - }) - }) +// assert.deepStrictEqual(expectedFilters2, actualFilters2) +// }) +// }) - describe('instanceHasName', function () { - it('returns whether or not there is name attached to instance', function () { - const instances = [ - { InstanceId: 'id1', Tags: [] }, - { InstanceId: 'id2', name: 'name2', Tags: [{ Key: 'Name', Value: 'name2' }] }, - { InstanceId: 'id3', Tags: [{ Key: 'NotName', Value: 'notAName' }] }, - { - InstanceId: 'id4', - name: 'name4', - Tags: [ - { Key: 'Name', Value: 'name4' }, - { Key: 'anotherKey', Value: 'Another Key' }, - ], - }, - ] +// describe('instanceHasName', function () { +// it('returns whether or not there is name attached to instance', function () { +// const instances = [ +// { InstanceId: 'id1', Tags: [] }, +// { InstanceId: 'id2', name: 'name2', Tags: [{ Key: 'Name', Value: 'name2' }] }, +// { InstanceId: 'id3', Tags: [{ Key: 'NotName', Value: 'notAName' }] }, +// { +// InstanceId: 'id4', +// name: 'name4', +// Tags: [ +// { Key: 'Name', Value: 'name4' }, +// { Key: 'anotherKey', Value: 'Another Key' }, +// ], +// }, +// ] - assert.deepStrictEqual(false, instanceHasName(instances[0])) - assert.deepStrictEqual(true, instanceHasName(instances[1])) - assert.deepStrictEqual(false, instanceHasName(instances[2])) - assert.deepStrictEqual(true, instanceHasName(instances[3])) - }) - }) -}) +// assert.deepStrictEqual(false, instanceHasName(instances[0])) +// assert.deepStrictEqual(true, instanceHasName(instances[1])) +// assert.deepStrictEqual(false, instanceHasName(instances[2])) +// assert.deepStrictEqual(true, instanceHasName(instances[3])) +// }) +// }) +// }) diff --git a/src/test/shared/sshConfig.test.ts b/src/test/shared/sshConfig.test.ts index 784473a364d..318f4d1e252 100644 --- a/src/test/shared/sshConfig.test.ts +++ b/src/test/shared/sshConfig.test.ts @@ -2,169 +2,169 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ -import * as assert from 'assert' -//import * as sinon from 'sinon' -import { ToolkitError } from '../../shared/errors' -import { Result } from '../../shared/utilities/result' -import { ChildProcessResult } from '../../shared/utilities/childProcess' -import { VscodeRemoteSshConfig, sshLogFileLocation } from '../../shared/sshConfig' - -class MockSshConfig extends VscodeRemoteSshConfig { - // State variables to track logic flow. - public testIsWin: boolean = false - public configSection: string = '' - - public async getProxyCommandWrapper(command: string): Promise> { - return await this.getProxyCommand(command) - } - - public async testMatchSshSection(testSection: string) { - this.configSection = testSection - const result = await this.matchSshSection() - this.configSection = '' - return result - } - - public async testVerifySshHostWrapper(proxyCommand: string, testSection: string) { - this.configSection = testSection - const result = this.verifySSHHost(proxyCommand) - this.configSection = '' - return result - } - - protected override isWin() { - return this.testIsWin - } - - protected override async checkSshOnHost(): Promise { - return { - exitCode: 0, - error: undefined, - stdout: this.configSection, - stderr: '', - } - } - - public createSSHConfigSectionWrapper(proxyCommand: string): string { - return this.createSSHConfigSection(proxyCommand) - } -} - -describe('VscodeRemoteSshConfig', async function () { - let config: MockSshConfig - - const testCommand = 'test_connect' - const testProxyCommand = `'${testCommand}' '%h'` - - before(function () { - config = new MockSshConfig('sshPath', 'testHostNamePrefix', testCommand) - config.testIsWin = false - }) - - describe('getProxyCommand', async function () { - it('returns correct proxyCommand on non-windows', async function () { - config.testIsWin = false - const result = await config.getProxyCommandWrapper(testCommand) - assert.ok(result.isOk()) - const command = result.unwrap() - assert.strictEqual(command, testProxyCommand) - }) - }) - - describe('matchSshSection', async function () { - it('returns ok with match when proxycommand is present', async function () { - const testSection = `proxycommandfdsafdsafd${testProxyCommand}sa342432` - const result = await config.testMatchSshSection(testSection) - assert.ok(result.isOk()) - const match = result.unwrap() - assert.ok(match) - }) - - it('returns ok result with undefined inside when proxycommand is not present', async function () { - const testSection = `fdsafdsafdsa342432` - const result = await config.testMatchSshSection(testSection) - assert.ok(result.isOk()) - const match = result.unwrap() - assert.strictEqual(match, undefined) - }) - }) - - // describe('verifySSHHost', async function () { - // let promptUserToConfigureSshConfigStub: sinon.SinonStub< - // [configSection: string | undefined, proxyCommand: string], - // Promise - // > - // before(function () { - // promptUserToConfigureSshConfigStub = sinon.stub( - // VscodeRemoteSshConfig.prototype, - // 'promptUserToConfigureSshConfig' - // ) - // }) - - // beforeEach(function () { - // promptUserToConfigureSshConfigStub.resetHistory() - // }) - - // after(function () { - // sinon.restore() - // }) - - // it('writes to ssh config if command not found.', async function () { - // const testSection = 'no-command-here' - // const result = await config.testVerifySshHostWrapper(testCommand, testSection) - - // assert.ok(result.isOk()) - // sinon.assert.calledOn(promptUserToConfigureSshConfigStub, config) - // sinon.assert.calledOnce(promptUserToConfigureSshConfigStub) - // }) - - // it('does not write to ssh config if command is find', async function () { - // const testSection = `this is some text that doesn't matter, but here proxycommand ${testProxyCommand}` - // const result = await config.testVerifySshHostWrapper(testCommand, testSection) - - // assert.ok(result.isOk()) - // sinon.assert.notCalled(promptUserToConfigureSshConfigStub) - // }) - // }) - - describe('createSSHConfigSection', async function () { - const testKeyPath = 'path/to/keys' - const newConfig = new MockSshConfig('sshPath', 'testHostNamePrefix', 'someScript', testKeyPath) - const expectedUserString = `User '%r'` - const expectedIdentityFileString = `IdentityFile '${testKeyPath}'` - - it('section includes relevant script prefix', function () { - const testScriptName = 'testScript' - const section = config.createSSHConfigSectionWrapper(testScriptName) - assert.ok(section.includes(testScriptName)) - }) - - it('includes keyPath if included in the class', function () { - const section = newConfig.createSSHConfigSectionWrapper('proxyCommand') - assert.ok(section.match(expectedIdentityFileString)) - }) - - it('parses the remote username from the ssh execution', function () { - const section = newConfig.createSSHConfigSectionWrapper('proxyCommand') - assert.ok(section.match(expectedUserString)) - }) - - it('omits User and IdentityFile fields when keyPath not given', function () { - const section = config.createSSHConfigSectionWrapper('proxyCommand') - assert.ok(!section.match(expectedUserString)) - assert.ok(!section.match(expectedIdentityFileString)) - }) - }) - - describe('sshLogFileLocation', async function () { - it('combines service and id into proper log file', function () { - const testService = 'testScript' - const testId = 'id' - const result = sshLogFileLocation(testService, testId) - - assert.ok(result.includes(testService)) - assert.ok(result.includes(testId)) - assert.ok(result.endsWith('.log')) - }) - }) -}) +// import * as assert from 'assert' +// import * as sinon from 'sinon' +// import { ToolkitError } from '../../shared/errors' +// import { Result } from '../../shared/utilities/result' +// import { ChildProcessResult } from '../../shared/utilities/childProcess' +// import { VscodeRemoteSshConfig, sshLogFileLocation } from '../../shared/sshConfig' + +// class MockSshConfig extends VscodeRemoteSshConfig { +// // State variables to track logic flow. +// public testIsWin: boolean = false +// public configSection: string = '' + +// public async getProxyCommandWrapper(command: string): Promise> { +// return await this.getProxyCommand(command) +// } + +// public async testMatchSshSection(testSection: string) { +// this.configSection = testSection +// const result = await this.matchSshSection() +// this.configSection = '' +// return result +// } + +// public async testVerifySshHostWrapper(proxyCommand: string, testSection: string) { +// this.configSection = testSection +// const result = this.verifySSHHost(proxyCommand) +// this.configSection = '' +// return result +// } + +// protected override isWin() { +// return this.testIsWin +// } + +// protected override async checkSshOnHost(): Promise { +// return { +// exitCode: 0, +// error: undefined, +// stdout: this.configSection, +// stderr: '', +// } +// } + +// public createSSHConfigSectionWrapper(proxyCommand: string): string { +// return this.createSSHConfigSection(proxyCommand) +// } +// } + +// describe('VscodeRemoteSshConfig', async function () { +// let config: MockSshConfig + +// const testCommand = 'test_connect' +// const testProxyCommand = `'${testCommand}' '%h'` + +// before(function () { +// config = new MockSshConfig('sshPath', 'testHostNamePrefix', testCommand) +// config.testIsWin = false +// }) + +// describe('getProxyCommand', async function () { +// it('returns correct proxyCommand on non-windows', async function () { +// config.testIsWin = false +// const result = await config.getProxyCommandWrapper(testCommand) +// assert.ok(result.isOk()) +// const command = result.unwrap() +// assert.strictEqual(command, testProxyCommand) +// }) +// }) + +// describe('matchSshSection', async function () { +// it('returns ok with match when proxycommand is present', async function () { +// const testSection = `proxycommandfdsafdsafd${testProxyCommand}sa342432` +// const result = await config.testMatchSshSection(testSection) +// assert.ok(result.isOk()) +// const match = result.unwrap() +// assert.ok(match) +// }) + +// it('returns ok result with undefined inside when proxycommand is not present', async function () { +// const testSection = `fdsafdsafdsa342432` +// const result = await config.testMatchSshSection(testSection) +// assert.ok(result.isOk()) +// const match = result.unwrap() +// assert.strictEqual(match, undefined) +// }) +// }) + +// describe('verifySSHHost', async function () { +// let promptUserToConfigureSshConfigStub: sinon.SinonStub< +// [configSection: string | undefined, proxyCommand: string], +// Promise +// > +// before(function () { +// promptUserToConfigureSshConfigStub = sinon.stub( +// VscodeRemoteSshConfig.prototype, +// 'promptUserToConfigureSshConfig' +// ) +// }) + +// beforeEach(function () { +// promptUserToConfigureSshConfigStub.resetHistory() +// }) + +// after(function () { +// sinon.restore() +// }) + +// it('writes to ssh config if command not found.', async function () { +// const testSection = 'no-command-here' +// const result = await config.testVerifySshHostWrapper(testCommand, testSection) + +// assert.ok(result.isOk()) +// sinon.assert.calledOn(promptUserToConfigureSshConfigStub, config) +// sinon.assert.calledOnce(promptUserToConfigureSshConfigStub) +// }) + +// it('does not write to ssh config if command is find', async function () { +// const testSection = `this is some text that doesn't matter, but here proxycommand ${testProxyCommand}` +// const result = await config.testVerifySshHostWrapper(testCommand, testSection) + +// assert.ok(result.isOk()) +// sinon.assert.notCalled(promptUserToConfigureSshConfigStub) +// }) +// }) + +// describe('createSSHConfigSection', async function () { +// const testKeyPath = 'path/to/keys' +// const newConfig = new MockSshConfig('sshPath', 'testHostNamePrefix', 'someScript', testKeyPath) +// const expectedUserString = `User '%r'` +// const expectedIdentityFileString = `IdentityFile '${testKeyPath}'` + +// it('section includes relevant script prefix', function () { +// const testScriptName = 'testScript' +// const section = config.createSSHConfigSectionWrapper(testScriptName) +// assert.ok(section.includes(testScriptName)) +// }) + +// it('includes keyPath if included in the class', function () { +// const section = newConfig.createSSHConfigSectionWrapper('proxyCommand') +// assert.ok(section.match(expectedIdentityFileString)) +// }) + +// it('parses the remote username from the ssh execution', function () { +// const section = newConfig.createSSHConfigSectionWrapper('proxyCommand') +// assert.ok(section.match(expectedUserString)) +// }) + +// it('omits User and IdentityFile fields when keyPath not given', function () { +// const section = config.createSSHConfigSectionWrapper('proxyCommand') +// assert.ok(!section.match(expectedUserString)) +// assert.ok(!section.match(expectedIdentityFileString)) +// }) +// }) + +// describe('sshLogFileLocation', async function () { +// it('combines service and id into proper log file', function () { +// const testService = 'testScript' +// const testId = 'id' +// const result = sshLogFileLocation(testService, testId) + +// assert.ok(result.includes(testService)) +// assert.ok(result.includes(testId)) +// assert.ok(result.endsWith('.log')) +// }) +// }) +// }) From c60d540803acb972c5ab1773031bd791d147a7cf Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 24 Jul 2023 11:57:13 -0700 Subject: [PATCH 127/172] uncomment test files --- src/test/codecatalyst/tools.test.ts | 306 +++++++-------- src/test/ec2/sshKeyPair.test.ts | 88 ++--- .../shared/clients/defaultEc2Client.test.ts | 356 +++++++++--------- src/test/shared/sshConfig.test.ts | 332 ++++++++-------- 4 files changed, 541 insertions(+), 541 deletions(-) diff --git a/src/test/codecatalyst/tools.test.ts b/src/test/codecatalyst/tools.test.ts index 57a76acd408..2f9d36cb311 100644 --- a/src/test/codecatalyst/tools.test.ts +++ b/src/test/codecatalyst/tools.test.ts @@ -3,156 +3,156 @@ * SPDX-License-Identifier: Apache-2.0 */ -// import * as vscode from 'vscode' -// import * as sinon from 'sinon' -// import * as path from 'path' -// import * as http from 'http' -// import * as assert from 'assert' -// import { fileExists, makeTemporaryToolkitFolder } from '../../shared/filesystemUtilities' -// import { ChildProcess } from '../../shared/utilities/childProcess' -// import { FakeExtensionContext } from '../fakeExtensionContext' -// import { startSshAgent } from '../../shared/extensions/ssh' -// import { -// bearerTokenCacheLocation, -// connectScriptPrefix, -// DevEnvironmentId, -// getCodeCatalystSsmEnv, -// } from '../../codecatalyst/model' -// import { mkdir, readFile, writeFile } from 'fs-extra' -// import { StartDevEnvironmentSessionRequest } from 'aws-sdk/clients/codecatalyst' -// import { SystemUtilities } from '../../shared/systemUtilities' -// import { ensureConnectScript, sshLogFileLocation } from '../../shared/sshConfig' - -// describe('SSH Agent', function () { -// it('can start the agent on windows', async function () { -// // TODO: we should also skip this test if not running in CI -// // Local machines probably won't have admin permissions in the spawned processes -// if (process.platform !== 'win32') { -// this.skip() -// } - -// const runCommand = (command: string) => { -// const args = ['-Command', command] -// return new ChildProcess('powershell.exe', args).run({ rejectOnErrorCode: true }) -// } - -// const getStatus = () => { -// return runCommand('echo (Get-Service ssh-agent).Status').then(o => o.stdout) -// } - -// await runCommand('Stop-Service ssh-agent') -// assert.strictEqual(await getStatus(), 'Stopped') -// await startSshAgent() -// assert.strictEqual(await getStatus(), 'Running') -// }) -// }) - -// describe('Connect Script', function () { -// let context: FakeExtensionContext - -// function isWithin(path1: string, path2: string): boolean { -// const rel = path.relative(path1, path2) -// return !path.isAbsolute(rel) && !rel.startsWith('..') && !!rel -// } - -// beforeEach(async function () { -// context = await FakeExtensionContext.create() -// context.globalStorageUri = vscode.Uri.file(await makeTemporaryToolkitFolder()) -// }) - -// it('can get a connect script path, adding a copy to global storage', async function () { -// const script = (await ensureConnectScript(connectScriptPrefix, context)).unwrap().fsPath -// assert.ok(await fileExists(script)) -// assert.ok(isWithin(context.globalStorageUri.fsPath, script)) -// }) - -// function createFakeServer(testDevEnv: DevEnvironmentId) { -// return http.createServer(async (req, resp) => { -// try { -// const data = await new Promise((resolve, reject) => { -// req.on('error', reject) -// req.on('data', d => resolve(d.toString())) -// }) - -// const body = JSON.parse(data) -// const expected: Pick = { -// sessionConfiguration: { sessionType: 'SSH' }, -// } - -// const expectedPath = `/v1/spaces/${testDevEnv.org.name}/projects/${testDevEnv.project.name}/devEnvironments/${testDevEnv.id}/session` - -// assert.deepStrictEqual(body, expected) -// assert.strictEqual(req.url, expectedPath) -// } catch (e) { -// resp.writeHead(400, { 'Content-Type': 'application/json' }) -// resp.end(JSON.stringify({ name: 'ValidationException', message: (e as Error).message })) - -// return -// } - -// resp.writeHead(200, { 'Content-Type': 'application/json' }) -// resp.end( -// JSON.stringify({ -// tokenValue: 'a token', -// streamUrl: 'some url', -// sessionId: 'an id', -// }) -// ) -// }) -// } - -// // it('can run the script with environment variables', async function () { -// // const testDevEnv: DevEnvironmentId = { -// // id: '01234567890', -// // project: { name: 'project' }, -// // org: { name: 'org' }, -// // } - -// // const server = createFakeServer(testDevEnv) -// // const address = await new Promise((resolve, reject) => { -// // server.on('error', reject) -// // server.listen({ host: 'localhost', port: 28142 }, () => resolve(`http://localhost:28142`)) -// // }) - -// // await writeFile(bearerTokenCacheLocation(testDevEnv.id), 'token') -// // const script = (await ensureConnectScript(connectScriptPrefix, context)).unwrap().fsPath -// // const env = getCodeCatalystSsmEnv('us-weast-1', 'echo', testDevEnv) -// // env.CODECATALYST_ENDPOINT = address - -// // // This could be de-duped -// // const isWindows = process.platform === 'win32' -// // const cmd = isWindows ? 'powershell.exe' : script -// // const args = isWindows ? ['-ExecutionPolicy', 'Bypass', '-File', script, 'bar'] : [script, 'bar'] - -// // const output = await new ChildProcess(cmd, args).run({ spawnOptions: { env } }) -// // if (output.exitCode !== 0) { -// // const logOutput = sshLogFileLocation('codecatalyst', testDevEnv.id) -// // const message = `stderr:\n${output.stderr}\n\nlogs:\n${await readFile(logOutput)}` - -// // assert.fail(`Connect script should exit with a zero status:\n${message}`) -// // } -// // }) - -// describe('~/.ssh', function () { -// let tmpDir: string - -// beforeEach(async function () { -// tmpDir = await makeTemporaryToolkitFolder() -// sinon.stub(SystemUtilities, 'getHomeDirectory').returns(tmpDir) -// }) - -// afterEach(async function () { -// sinon.restore() -// await SystemUtilities.delete(tmpDir, { recursive: true }) -// }) - -// it('works if the .ssh directory is missing', async function () { -// ;(await ensureConnectScript(connectScriptPrefix, context)).unwrap() -// }) - -// it('works if the .ssh directory exists but has different perms', async function () { -// await mkdir(path.join(tmpDir, '.ssh'), 0o777) -// ;(await ensureConnectScript(connectScriptPrefix, context)).unwrap() -// }) -// }) -// }) +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import * as path from 'path' +import * as http from 'http' +import * as assert from 'assert' +import { fileExists, makeTemporaryToolkitFolder } from '../../shared/filesystemUtilities' +import { ChildProcess } from '../../shared/utilities/childProcess' +import { FakeExtensionContext } from '../fakeExtensionContext' +import { startSshAgent } from '../../shared/extensions/ssh' +import { + bearerTokenCacheLocation, + connectScriptPrefix, + DevEnvironmentId, + getCodeCatalystSsmEnv, +} from '../../codecatalyst/model' +import { mkdir, readFile, writeFile } from 'fs-extra' +import { StartDevEnvironmentSessionRequest } from 'aws-sdk/clients/codecatalyst' +import { SystemUtilities } from '../../shared/systemUtilities' +import { ensureConnectScript, sshLogFileLocation } from '../../shared/sshConfig' + +describe('SSH Agent', function () { + it('can start the agent on windows', async function () { + // TODO: we should also skip this test if not running in CI + // Local machines probably won't have admin permissions in the spawned processes + if (process.platform !== 'win32') { + this.skip() + } + + const runCommand = (command: string) => { + const args = ['-Command', command] + return new ChildProcess('powershell.exe', args).run({ rejectOnErrorCode: true }) + } + + const getStatus = () => { + return runCommand('echo (Get-Service ssh-agent).Status').then(o => o.stdout) + } + + await runCommand('Stop-Service ssh-agent') + assert.strictEqual(await getStatus(), 'Stopped') + await startSshAgent() + assert.strictEqual(await getStatus(), 'Running') + }) +}) + +describe('Connect Script', function () { + let context: FakeExtensionContext + + function isWithin(path1: string, path2: string): boolean { + const rel = path.relative(path1, path2) + return !path.isAbsolute(rel) && !rel.startsWith('..') && !!rel + } + + beforeEach(async function () { + context = await FakeExtensionContext.create() + context.globalStorageUri = vscode.Uri.file(await makeTemporaryToolkitFolder()) + }) + + it('can get a connect script path, adding a copy to global storage', async function () { + const script = (await ensureConnectScript(connectScriptPrefix, context)).unwrap().fsPath + assert.ok(await fileExists(script)) + assert.ok(isWithin(context.globalStorageUri.fsPath, script)) + }) + + function createFakeServer(testDevEnv: DevEnvironmentId) { + return http.createServer(async (req, resp) => { + try { + const data = await new Promise((resolve, reject) => { + req.on('error', reject) + req.on('data', d => resolve(d.toString())) + }) + + const body = JSON.parse(data) + const expected: Pick = { + sessionConfiguration: { sessionType: 'SSH' }, + } + + const expectedPath = `/v1/spaces/${testDevEnv.org.name}/projects/${testDevEnv.project.name}/devEnvironments/${testDevEnv.id}/session` + + assert.deepStrictEqual(body, expected) + assert.strictEqual(req.url, expectedPath) + } catch (e) { + resp.writeHead(400, { 'Content-Type': 'application/json' }) + resp.end(JSON.stringify({ name: 'ValidationException', message: (e as Error).message })) + + return + } + + resp.writeHead(200, { 'Content-Type': 'application/json' }) + resp.end( + JSON.stringify({ + tokenValue: 'a token', + streamUrl: 'some url', + sessionId: 'an id', + }) + ) + }) + } + + it('can run the script with environment variables', async function () { + const testDevEnv: DevEnvironmentId = { + id: '01234567890', + project: { name: 'project' }, + org: { name: 'org' }, + } + + const server = createFakeServer(testDevEnv) + const address = await new Promise((resolve, reject) => { + server.on('error', reject) + server.listen({ host: 'localhost', port: 28142 }, () => resolve(`http://localhost:28142`)) + }) + + await writeFile(bearerTokenCacheLocation(testDevEnv.id), 'token') + const script = (await ensureConnectScript(connectScriptPrefix, context)).unwrap().fsPath + const env = getCodeCatalystSsmEnv('us-weast-1', 'echo', testDevEnv) + env.CODECATALYST_ENDPOINT = address + + // This could be de-duped + const isWindows = process.platform === 'win32' + const cmd = isWindows ? 'powershell.exe' : script + const args = isWindows ? ['-ExecutionPolicy', 'Bypass', '-File', script, 'bar'] : [script, 'bar'] + + const output = await new ChildProcess(cmd, args).run({ spawnOptions: { env } }) + if (output.exitCode !== 0) { + const logOutput = sshLogFileLocation('codecatalyst', testDevEnv.id) + const message = `stderr:\n${output.stderr}\n\nlogs:\n${await readFile(logOutput)}` + + assert.fail(`Connect script should exit with a zero status:\n${message}`) + } + }) + + describe('~/.ssh', function () { + let tmpDir: string + + beforeEach(async function () { + tmpDir = await makeTemporaryToolkitFolder() + sinon.stub(SystemUtilities, 'getHomeDirectory').returns(tmpDir) + }) + + afterEach(async function () { + sinon.restore() + await SystemUtilities.delete(tmpDir, { recursive: true }) + }) + + it('works if the .ssh directory is missing', async function () { + ;(await ensureConnectScript(connectScriptPrefix, context)).unwrap() + }) + + it('works if the .ssh directory exists but has different perms', async function () { + await mkdir(path.join(tmpDir, '.ssh'), 0o777) + ;(await ensureConnectScript(connectScriptPrefix, context)).unwrap() + }) + }) +}) diff --git a/src/test/ec2/sshKeyPair.test.ts b/src/test/ec2/sshKeyPair.test.ts index 1508ed78611..44a9e38a2d5 100644 --- a/src/test/ec2/sshKeyPair.test.ts +++ b/src/test/ec2/sshKeyPair.test.ts @@ -3,47 +3,47 @@ * SPDX-License-Identifier: Apache-2.0 */ -// import * as assert from 'assert' -// import * as fs from 'fs-extra' -// import * as sinon from 'sinon' -// import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../shared/filesystemUtilities' -// import { SshKeyPair } from '../../ec2/sshKeyPair' - -// describe('SshKeyUtility', async function () { -// let temporaryDirectory: string -// let keyPath: string -// let keyPair: SshKeyPair - -// before(async function () { -// temporaryDirectory = await makeTemporaryToolkitFolder() -// keyPath = `${temporaryDirectory}/test-key` -// keyPair = await SshKeyPair.getSshKeyPair(keyPath) -// }) - -// after(async function () { -// await tryRemoveFolder(temporaryDirectory) -// }) - -// describe('generateSshKeys', async function () { -// it('generates key in target file', async function () { -// const contents = await fs.readFile(keyPath, 'utf-8') -// assert.notStrictEqual(contents.length, 0) -// }) -// }) - -// it('properly names the public key', function () { -// assert.strictEqual(keyPair.getPublicKeyPath(), `${keyPath}.pub`) -// }) - -// it('reads in public ssh key that is non-empty', async function () { -// const key = await keyPair.getPublicKey() -// assert.notStrictEqual(key.length, 0) -// }) - -// it('does not overwrite existing keys', async function () { -// const generateStub = sinon.stub(SshKeyPair, 'generateSshKeyPair') -// await SshKeyPair.getSshKeyPair(keyPath) -// sinon.assert.notCalled(generateStub) -// sinon.restore() -// }) -// }) +import * as assert from 'assert' +import * as fs from 'fs-extra' +import * as sinon from 'sinon' +import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../shared/filesystemUtilities' +import { SshKeyPair } from '../../ec2/sshKeyPair' + +describe('SshKeyUtility', async function () { + let temporaryDirectory: string + let keyPath: string + let keyPair: SshKeyPair + + before(async function () { + temporaryDirectory = await makeTemporaryToolkitFolder() + keyPath = `${temporaryDirectory}/test-key` + keyPair = await SshKeyPair.getSshKeyPair(keyPath) + }) + + after(async function () { + await tryRemoveFolder(temporaryDirectory) + }) + + describe('generateSshKeys', async function () { + it('generates key in target file', async function () { + const contents = await fs.readFile(keyPath, 'utf-8') + assert.notStrictEqual(contents.length, 0) + }) + }) + + it('properly names the public key', function () { + assert.strictEqual(keyPair.getPublicKeyPath(), `${keyPath}.pub`) + }) + + it('reads in public ssh key that is non-empty', async function () { + const key = await keyPair.getPublicKey() + assert.notStrictEqual(key.length, 0) + }) + + it('does not overwrite existing keys', async function () { + const generateStub = sinon.stub(SshKeyPair, 'generateSshKeyPair') + await SshKeyPair.getSshKeyPair(keyPath) + sinon.assert.notCalled(generateStub) + sinon.restore() + }) +}) diff --git a/src/test/shared/clients/defaultEc2Client.test.ts b/src/test/shared/clients/defaultEc2Client.test.ts index d8b1241ba51..825eb620194 100644 --- a/src/test/shared/clients/defaultEc2Client.test.ts +++ b/src/test/shared/clients/defaultEc2Client.test.ts @@ -3,194 +3,194 @@ * SPDX-License-Identifier: Apache-2.0 */ -// import * as assert from 'assert' -// import { AsyncCollection } from '../../../shared/utilities/asyncCollection' -// import { toCollection } from '../../../shared/utilities/asyncCollection' -// import { intoCollection } from '../../../shared/utilities/collectionUtils' -// import { Ec2Client, instanceHasName } from '../../../shared/clients/ec2Client' -// import { EC2 } from 'aws-sdk' +import * as assert from 'assert' +import { AsyncCollection } from '../../../shared/utilities/asyncCollection' +import { toCollection } from '../../../shared/utilities/asyncCollection' +import { intoCollection } from '../../../shared/utilities/collectionUtils' +import { Ec2Client, instanceHasName } from '../../../shared/clients/ec2Client' +import { EC2 } from 'aws-sdk' -// describe('EC2Client', async function () { -// describe('extractInstancesFromReservations', function () { -// const client = new Ec2Client('') -// it('returns empty when given empty collection', async function () { -// const actualResult = await client -// .getInstancesFromReservations( -// toCollection(async function* () { -// yield [] -// }) as AsyncCollection -// ) -// .promise() +describe('EC2Client', async function () { + describe('extractInstancesFromReservations', function () { + const client = new Ec2Client('') + it('returns empty when given empty collection', async function () { + const actualResult = await client + .getInstancesFromReservations( + toCollection(async function* () { + yield [] + }) as AsyncCollection + ) + .promise() -// assert.strictEqual(0, actualResult.length) -// }) + assert.strictEqual(0, actualResult.length) + }) -// it('flattens the reservationList', async function () { -// const testReservationsList: EC2.ReservationList = [ -// { -// Instances: [ -// { -// InstanceId: 'id1', -// Tags: [{ Key: 'Name', Value: 'name1' }], -// }, -// { -// InstanceId: 'id2', -// Tags: [{ Key: 'Name', Value: 'name2' }], -// }, -// ], -// }, -// { -// Instances: [ -// { -// InstanceId: 'id3', -// Tags: [{ Key: 'Name', Value: 'name3' }], -// }, -// { -// InstanceId: 'id4', -// Tags: [{ Key: 'Name', Value: 'name4' }], -// }, -// ], -// }, -// ] -// const actualResult = await client -// .getInstancesFromReservations(intoCollection([testReservationsList])) -// .promise() -// assert.deepStrictEqual( -// [ -// { InstanceId: 'id1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, -// { InstanceId: 'id2', name: 'name2', Tags: [{ Key: 'Name', Value: 'name2' }] }, -// { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, -// { InstanceId: 'id4', name: 'name4', Tags: [{ Key: 'Name', Value: 'name4' }] }, -// ], -// actualResult -// ) -// }), -// // Unsure if this test case is needed, but the return type in the SDK makes it possible these are unknown/not returned. -// it('handles undefined and missing pieces in the ReservationList.', async function () { -// const testReservationsList: EC2.ReservationList = [ -// { -// Instances: [ -// { -// InstanceId: 'id1', -// }, -// { -// InstanceId: undefined, -// }, -// ], -// }, -// { -// Instances: [ -// { -// InstanceId: 'id3', -// Tags: [{ Key: 'Name', Value: 'name3' }], -// }, -// {}, -// ], -// }, -// ] -// const actualResult = await client -// .getInstancesFromReservations(intoCollection([testReservationsList])) -// .promise() -// assert.deepStrictEqual( -// [ -// { InstanceId: 'id1' }, -// { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, -// ], -// actualResult -// ) -// }) + it('flattens the reservationList', async function () { + const testReservationsList: EC2.ReservationList = [ + { + Instances: [ + { + InstanceId: 'id1', + Tags: [{ Key: 'Name', Value: 'name1' }], + }, + { + InstanceId: 'id2', + Tags: [{ Key: 'Name', Value: 'name2' }], + }, + ], + }, + { + Instances: [ + { + InstanceId: 'id3', + Tags: [{ Key: 'Name', Value: 'name3' }], + }, + { + InstanceId: 'id4', + Tags: [{ Key: 'Name', Value: 'name4' }], + }, + ], + }, + ] + const actualResult = await client + .getInstancesFromReservations(intoCollection([testReservationsList])) + .promise() + assert.deepStrictEqual( + [ + { InstanceId: 'id1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, + { InstanceId: 'id2', name: 'name2', Tags: [{ Key: 'Name', Value: 'name2' }] }, + { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, + { InstanceId: 'id4', name: 'name4', Tags: [{ Key: 'Name', Value: 'name4' }] }, + ], + actualResult + ) + }), + // Unsure if this test case is needed, but the return type in the SDK makes it possible these are unknown/not returned. + it('handles undefined and missing pieces in the ReservationList.', async function () { + const testReservationsList: EC2.ReservationList = [ + { + Instances: [ + { + InstanceId: 'id1', + }, + { + InstanceId: undefined, + }, + ], + }, + { + Instances: [ + { + InstanceId: 'id3', + Tags: [{ Key: 'Name', Value: 'name3' }], + }, + {}, + ], + }, + ] + const actualResult = await client + .getInstancesFromReservations(intoCollection([testReservationsList])) + .promise() + assert.deepStrictEqual( + [ + { InstanceId: 'id1' }, + { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, + ], + actualResult + ) + }) -// it('can process results without complete Tag field.', async function () { -// const testReservationsList: EC2.ReservationList = [ -// { -// Instances: [ -// { -// InstanceId: 'id1', -// Tags: [{ Key: 'Name', Value: 'name1' }], -// }, -// { -// InstanceId: 'id2', -// }, -// ], -// }, -// { -// Instances: [ -// { -// InstanceId: 'id3', -// Tags: [{ Key: 'Name', Value: 'name3' }], -// }, -// { -// InstanceId: 'id4', -// Tags: [], -// }, -// ], -// }, -// ] + it('can process results without complete Tag field.', async function () { + const testReservationsList: EC2.ReservationList = [ + { + Instances: [ + { + InstanceId: 'id1', + Tags: [{ Key: 'Name', Value: 'name1' }], + }, + { + InstanceId: 'id2', + }, + ], + }, + { + Instances: [ + { + InstanceId: 'id3', + Tags: [{ Key: 'Name', Value: 'name3' }], + }, + { + InstanceId: 'id4', + Tags: [], + }, + ], + }, + ] -// const actualResult = await client -// .getInstancesFromReservations(intoCollection([testReservationsList])) -// .promise() + const actualResult = await client + .getInstancesFromReservations(intoCollection([testReservationsList])) + .promise() -// assert.deepStrictEqual( -// [ -// { InstanceId: 'id1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, -// { InstanceId: 'id2' }, -// { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, -// { InstanceId: 'id4', Tags: [] }, -// ], -// actualResult -// ) -// }) -// }) + assert.deepStrictEqual( + [ + { InstanceId: 'id1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, + { InstanceId: 'id2' }, + { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, + { InstanceId: 'id4', Tags: [] }, + ], + actualResult + ) + }) + }) -// describe('getInstancesFilter', function () { -// const client = new Ec2Client('') + describe('getInstancesFilter', function () { + const client = new Ec2Client('') -// it('returns proper filter when given instanceId', function () { -// const testInstanceId1 = 'test' -// const actualFilters1 = client.getInstancesFilter([testInstanceId1]) -// const expectedFilters1: EC2.Filter[] = [ -// { -// Name: 'instance-id', -// Values: [testInstanceId1], -// }, -// ] + it('returns proper filter when given instanceId', function () { + const testInstanceId1 = 'test' + const actualFilters1 = client.getInstancesFilter([testInstanceId1]) + const expectedFilters1: EC2.Filter[] = [ + { + Name: 'instance-id', + Values: [testInstanceId1], + }, + ] -// assert.deepStrictEqual(expectedFilters1, actualFilters1) + assert.deepStrictEqual(expectedFilters1, actualFilters1) -// const testInstanceId2 = 'test2' -// const actualFilters2 = client.getInstancesFilter([testInstanceId1, testInstanceId2]) -// const expectedFilters2: EC2.Filter[] = [ -// { -// Name: 'instance-id', -// Values: [testInstanceId1, testInstanceId2], -// }, -// ] + const testInstanceId2 = 'test2' + const actualFilters2 = client.getInstancesFilter([testInstanceId1, testInstanceId2]) + const expectedFilters2: EC2.Filter[] = [ + { + Name: 'instance-id', + Values: [testInstanceId1, testInstanceId2], + }, + ] -// assert.deepStrictEqual(expectedFilters2, actualFilters2) -// }) -// }) + assert.deepStrictEqual(expectedFilters2, actualFilters2) + }) + }) -// describe('instanceHasName', function () { -// it('returns whether or not there is name attached to instance', function () { -// const instances = [ -// { InstanceId: 'id1', Tags: [] }, -// { InstanceId: 'id2', name: 'name2', Tags: [{ Key: 'Name', Value: 'name2' }] }, -// { InstanceId: 'id3', Tags: [{ Key: 'NotName', Value: 'notAName' }] }, -// { -// InstanceId: 'id4', -// name: 'name4', -// Tags: [ -// { Key: 'Name', Value: 'name4' }, -// { Key: 'anotherKey', Value: 'Another Key' }, -// ], -// }, -// ] + describe('instanceHasName', function () { + it('returns whether or not there is name attached to instance', function () { + const instances = [ + { InstanceId: 'id1', Tags: [] }, + { InstanceId: 'id2', name: 'name2', Tags: [{ Key: 'Name', Value: 'name2' }] }, + { InstanceId: 'id3', Tags: [{ Key: 'NotName', Value: 'notAName' }] }, + { + InstanceId: 'id4', + name: 'name4', + Tags: [ + { Key: 'Name', Value: 'name4' }, + { Key: 'anotherKey', Value: 'Another Key' }, + ], + }, + ] -// assert.deepStrictEqual(false, instanceHasName(instances[0])) -// assert.deepStrictEqual(true, instanceHasName(instances[1])) -// assert.deepStrictEqual(false, instanceHasName(instances[2])) -// assert.deepStrictEqual(true, instanceHasName(instances[3])) -// }) -// }) -// }) + assert.deepStrictEqual(false, instanceHasName(instances[0])) + assert.deepStrictEqual(true, instanceHasName(instances[1])) + assert.deepStrictEqual(false, instanceHasName(instances[2])) + assert.deepStrictEqual(true, instanceHasName(instances[3])) + }) + }) +}) diff --git a/src/test/shared/sshConfig.test.ts b/src/test/shared/sshConfig.test.ts index 318f4d1e252..41d054a7944 100644 --- a/src/test/shared/sshConfig.test.ts +++ b/src/test/shared/sshConfig.test.ts @@ -2,169 +2,169 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ -// import * as assert from 'assert' -// import * as sinon from 'sinon' -// import { ToolkitError } from '../../shared/errors' -// import { Result } from '../../shared/utilities/result' -// import { ChildProcessResult } from '../../shared/utilities/childProcess' -// import { VscodeRemoteSshConfig, sshLogFileLocation } from '../../shared/sshConfig' - -// class MockSshConfig extends VscodeRemoteSshConfig { -// // State variables to track logic flow. -// public testIsWin: boolean = false -// public configSection: string = '' - -// public async getProxyCommandWrapper(command: string): Promise> { -// return await this.getProxyCommand(command) -// } - -// public async testMatchSshSection(testSection: string) { -// this.configSection = testSection -// const result = await this.matchSshSection() -// this.configSection = '' -// return result -// } - -// public async testVerifySshHostWrapper(proxyCommand: string, testSection: string) { -// this.configSection = testSection -// const result = this.verifySSHHost(proxyCommand) -// this.configSection = '' -// return result -// } - -// protected override isWin() { -// return this.testIsWin -// } - -// protected override async checkSshOnHost(): Promise { -// return { -// exitCode: 0, -// error: undefined, -// stdout: this.configSection, -// stderr: '', -// } -// } - -// public createSSHConfigSectionWrapper(proxyCommand: string): string { -// return this.createSSHConfigSection(proxyCommand) -// } -// } - -// describe('VscodeRemoteSshConfig', async function () { -// let config: MockSshConfig - -// const testCommand = 'test_connect' -// const testProxyCommand = `'${testCommand}' '%h'` - -// before(function () { -// config = new MockSshConfig('sshPath', 'testHostNamePrefix', testCommand) -// config.testIsWin = false -// }) - -// describe('getProxyCommand', async function () { -// it('returns correct proxyCommand on non-windows', async function () { -// config.testIsWin = false -// const result = await config.getProxyCommandWrapper(testCommand) -// assert.ok(result.isOk()) -// const command = result.unwrap() -// assert.strictEqual(command, testProxyCommand) -// }) -// }) - -// describe('matchSshSection', async function () { -// it('returns ok with match when proxycommand is present', async function () { -// const testSection = `proxycommandfdsafdsafd${testProxyCommand}sa342432` -// const result = await config.testMatchSshSection(testSection) -// assert.ok(result.isOk()) -// const match = result.unwrap() -// assert.ok(match) -// }) - -// it('returns ok result with undefined inside when proxycommand is not present', async function () { -// const testSection = `fdsafdsafdsa342432` -// const result = await config.testMatchSshSection(testSection) -// assert.ok(result.isOk()) -// const match = result.unwrap() -// assert.strictEqual(match, undefined) -// }) -// }) - -// describe('verifySSHHost', async function () { -// let promptUserToConfigureSshConfigStub: sinon.SinonStub< -// [configSection: string | undefined, proxyCommand: string], -// Promise -// > -// before(function () { -// promptUserToConfigureSshConfigStub = sinon.stub( -// VscodeRemoteSshConfig.prototype, -// 'promptUserToConfigureSshConfig' -// ) -// }) - -// beforeEach(function () { -// promptUserToConfigureSshConfigStub.resetHistory() -// }) - -// after(function () { -// sinon.restore() -// }) - -// it('writes to ssh config if command not found.', async function () { -// const testSection = 'no-command-here' -// const result = await config.testVerifySshHostWrapper(testCommand, testSection) - -// assert.ok(result.isOk()) -// sinon.assert.calledOn(promptUserToConfigureSshConfigStub, config) -// sinon.assert.calledOnce(promptUserToConfigureSshConfigStub) -// }) - -// it('does not write to ssh config if command is find', async function () { -// const testSection = `this is some text that doesn't matter, but here proxycommand ${testProxyCommand}` -// const result = await config.testVerifySshHostWrapper(testCommand, testSection) - -// assert.ok(result.isOk()) -// sinon.assert.notCalled(promptUserToConfigureSshConfigStub) -// }) -// }) - -// describe('createSSHConfigSection', async function () { -// const testKeyPath = 'path/to/keys' -// const newConfig = new MockSshConfig('sshPath', 'testHostNamePrefix', 'someScript', testKeyPath) -// const expectedUserString = `User '%r'` -// const expectedIdentityFileString = `IdentityFile '${testKeyPath}'` - -// it('section includes relevant script prefix', function () { -// const testScriptName = 'testScript' -// const section = config.createSSHConfigSectionWrapper(testScriptName) -// assert.ok(section.includes(testScriptName)) -// }) - -// it('includes keyPath if included in the class', function () { -// const section = newConfig.createSSHConfigSectionWrapper('proxyCommand') -// assert.ok(section.match(expectedIdentityFileString)) -// }) - -// it('parses the remote username from the ssh execution', function () { -// const section = newConfig.createSSHConfigSectionWrapper('proxyCommand') -// assert.ok(section.match(expectedUserString)) -// }) - -// it('omits User and IdentityFile fields when keyPath not given', function () { -// const section = config.createSSHConfigSectionWrapper('proxyCommand') -// assert.ok(!section.match(expectedUserString)) -// assert.ok(!section.match(expectedIdentityFileString)) -// }) -// }) - -// describe('sshLogFileLocation', async function () { -// it('combines service and id into proper log file', function () { -// const testService = 'testScript' -// const testId = 'id' -// const result = sshLogFileLocation(testService, testId) - -// assert.ok(result.includes(testService)) -// assert.ok(result.includes(testId)) -// assert.ok(result.endsWith('.log')) -// }) -// }) -// }) +import * as assert from 'assert' +import * as sinon from 'sinon' +import { ToolkitError } from '../../shared/errors' +import { Result } from '../../shared/utilities/result' +import { ChildProcessResult } from '../../shared/utilities/childProcess' +import { VscodeRemoteSshConfig, sshLogFileLocation } from '../../shared/sshConfig' + +class MockSshConfig extends VscodeRemoteSshConfig { + // State variables to track logic flow. + public testIsWin: boolean = false + public configSection: string = '' + + public async getProxyCommandWrapper(command: string): Promise> { + return await this.getProxyCommand(command) + } + + public async testMatchSshSection(testSection: string) { + this.configSection = testSection + const result = await this.matchSshSection() + this.configSection = '' + return result + } + + public async testVerifySshHostWrapper(proxyCommand: string, testSection: string) { + this.configSection = testSection + const result = this.verifySSHHost(proxyCommand) + this.configSection = '' + return result + } + + protected override isWin() { + return this.testIsWin + } + + protected override async checkSshOnHost(): Promise { + return { + exitCode: 0, + error: undefined, + stdout: this.configSection, + stderr: '', + } + } + + public createSSHConfigSectionWrapper(proxyCommand: string): string { + return this.createSSHConfigSection(proxyCommand) + } +} + +describe('VscodeRemoteSshConfig', async function () { + let config: MockSshConfig + + const testCommand = 'test_connect' + const testProxyCommand = `'${testCommand}' '%h'` + + before(function () { + config = new MockSshConfig('sshPath', 'testHostNamePrefix', testCommand) + config.testIsWin = false + }) + + describe('getProxyCommand', async function () { + it('returns correct proxyCommand on non-windows', async function () { + config.testIsWin = false + const result = await config.getProxyCommandWrapper(testCommand) + assert.ok(result.isOk()) + const command = result.unwrap() + assert.strictEqual(command, testProxyCommand) + }) + }) + + describe('matchSshSection', async function () { + it('returns ok with match when proxycommand is present', async function () { + const testSection = `proxycommandfdsafdsafd${testProxyCommand}sa342432` + const result = await config.testMatchSshSection(testSection) + assert.ok(result.isOk()) + const match = result.unwrap() + assert.ok(match) + }) + + it('returns ok result with undefined inside when proxycommand is not present', async function () { + const testSection = `fdsafdsafdsa342432` + const result = await config.testMatchSshSection(testSection) + assert.ok(result.isOk()) + const match = result.unwrap() + assert.strictEqual(match, undefined) + }) + }) + + describe('verifySSHHost', async function () { + let promptUserToConfigureSshConfigStub: sinon.SinonStub< + [configSection: string | undefined, proxyCommand: string], + Promise + > + before(function () { + promptUserToConfigureSshConfigStub = sinon.stub( + VscodeRemoteSshConfig.prototype, + 'promptUserToConfigureSshConfig' + ) + }) + + beforeEach(function () { + promptUserToConfigureSshConfigStub.resetHistory() + }) + + after(function () { + sinon.restore() + }) + + it('writes to ssh config if command not found.', async function () { + const testSection = 'no-command-here' + const result = await config.testVerifySshHostWrapper(testCommand, testSection) + + assert.ok(result.isOk()) + sinon.assert.calledOn(promptUserToConfigureSshConfigStub, config) + sinon.assert.calledOnce(promptUserToConfigureSshConfigStub) + }) + + it('does not write to ssh config if command is find', async function () { + const testSection = `this is some text that doesn't matter, but here proxycommand ${testProxyCommand}` + const result = await config.testVerifySshHostWrapper(testCommand, testSection) + + assert.ok(result.isOk()) + sinon.assert.notCalled(promptUserToConfigureSshConfigStub) + }) + }) + + describe('createSSHConfigSection', async function () { + const testKeyPath = 'path/to/keys' + const newConfig = new MockSshConfig('sshPath', 'testHostNamePrefix', 'someScript', testKeyPath) + const expectedUserString = `User '%r'` + const expectedIdentityFileString = `IdentityFile '${testKeyPath}'` + + it('section includes relevant script prefix', function () { + const testScriptName = 'testScript' + const section = config.createSSHConfigSectionWrapper(testScriptName) + assert.ok(section.includes(testScriptName)) + }) + + it('includes keyPath if included in the class', function () { + const section = newConfig.createSSHConfigSectionWrapper('proxyCommand') + assert.ok(section.match(expectedIdentityFileString)) + }) + + it('parses the remote username from the ssh execution', function () { + const section = newConfig.createSSHConfigSectionWrapper('proxyCommand') + assert.ok(section.match(expectedUserString)) + }) + + it('omits User and IdentityFile fields when keyPath not given', function () { + const section = config.createSSHConfigSectionWrapper('proxyCommand') + assert.ok(!section.match(expectedUserString)) + assert.ok(!section.match(expectedIdentityFileString)) + }) + }) + + describe('sshLogFileLocation', async function () { + it('combines service and id into proper log file', function () { + const testService = 'testScript' + const testId = 'id' + const result = sshLogFileLocation(testService, testId) + + assert.ok(result.includes(testService)) + assert.ok(result.includes(testId)) + assert.ok(result.endsWith('.log')) + }) + }) +}) From 2a08bb7dbfb10169043d52e512587280f83ea7ad Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 24 Jul 2023 12:02:21 -0700 Subject: [PATCH 128/172] move ssh tests to their own file --- src/test/codecatalyst/tools.test.ts | 25 --------------------- src/test/shared/extensions/ssh.test.ts | 31 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 25 deletions(-) create mode 100644 src/test/shared/extensions/ssh.test.ts diff --git a/src/test/codecatalyst/tools.test.ts b/src/test/codecatalyst/tools.test.ts index 2f9d36cb311..2d69405c252 100644 --- a/src/test/codecatalyst/tools.test.ts +++ b/src/test/codecatalyst/tools.test.ts @@ -11,7 +11,6 @@ import * as assert from 'assert' import { fileExists, makeTemporaryToolkitFolder } from '../../shared/filesystemUtilities' import { ChildProcess } from '../../shared/utilities/childProcess' import { FakeExtensionContext } from '../fakeExtensionContext' -import { startSshAgent } from '../../shared/extensions/ssh' import { bearerTokenCacheLocation, connectScriptPrefix, @@ -23,30 +22,6 @@ import { StartDevEnvironmentSessionRequest } from 'aws-sdk/clients/codecatalyst' import { SystemUtilities } from '../../shared/systemUtilities' import { ensureConnectScript, sshLogFileLocation } from '../../shared/sshConfig' -describe('SSH Agent', function () { - it('can start the agent on windows', async function () { - // TODO: we should also skip this test if not running in CI - // Local machines probably won't have admin permissions in the spawned processes - if (process.platform !== 'win32') { - this.skip() - } - - const runCommand = (command: string) => { - const args = ['-Command', command] - return new ChildProcess('powershell.exe', args).run({ rejectOnErrorCode: true }) - } - - const getStatus = () => { - return runCommand('echo (Get-Service ssh-agent).Status').then(o => o.stdout) - } - - await runCommand('Stop-Service ssh-agent') - assert.strictEqual(await getStatus(), 'Stopped') - await startSshAgent() - assert.strictEqual(await getStatus(), 'Running') - }) -}) - describe('Connect Script', function () { let context: FakeExtensionContext diff --git a/src/test/shared/extensions/ssh.test.ts b/src/test/shared/extensions/ssh.test.ts new file mode 100644 index 00000000000..56feba3e571 --- /dev/null +++ b/src/test/shared/extensions/ssh.test.ts @@ -0,0 +1,31 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as assert from 'assert' +import { ChildProcess } from '../../../shared/utilities/childProcess' +import { startSshAgent } from '../../../shared/extensions/ssh' + +describe('SSH Agent', function () { + it('can start the agent on windows', async function () { + // TODO: we should also skip this test if not running in CI + // Local machines probably won't have admin permissions in the spawned processes + if (process.platform !== 'win32') { + this.skip() + } + + const runCommand = (command: string) => { + const args = ['-Command', command] + return new ChildProcess('powershell.exe', args).run({ rejectOnErrorCode: true }) + } + + const getStatus = () => { + return runCommand('echo (Get-Service ssh-agent).Status').then(o => o.stdout) + } + + await runCommand('Stop-Service ssh-agent') + assert.strictEqual(await getStatus(), 'Stopped') + await startSshAgent() + assert.strictEqual(await getStatus(), 'Running') + }) +}) From 24a25d0128ca44842d311b4f01b9410dd0e7af93 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 24 Jul 2023 12:03:08 -0700 Subject: [PATCH 129/172] uncomment tests related to the model --- src/test/ec2/model.test.ts | 514 ++++++++++++++++++------------------- 1 file changed, 257 insertions(+), 257 deletions(-) diff --git a/src/test/ec2/model.test.ts b/src/test/ec2/model.test.ts index 54a5bda0360..f8013ee24e9 100644 --- a/src/test/ec2/model.test.ts +++ b/src/test/ec2/model.test.ts @@ -3,260 +3,260 @@ * SPDX-License-Identifier: Apache-2.0 */ -// import * as assert from 'assert' -// import * as sinon from 'sinon' -// import { Ec2ConnectErrorCode, Ec2ConnectionManager } from '../../ec2/model' -// import { SsmClient } from '../../shared/clients/ssmClient' -// import { Ec2Client } from '../../shared/clients/ec2Client' -// import { attachedPoliciesListType } from 'aws-sdk/clients/iam' -// import { Ec2Selection } from '../../ec2/utils' -// import { ToolkitError } from '../../shared/errors' -// import { AWSError, EC2, SSM } from 'aws-sdk' -// import { PromiseResult } from 'aws-sdk/lib/request' -// import { GetCommandInvocationResult } from 'aws-sdk/clients/ssm' -// import { mock } from 'ts-mockito' -// import { SshKeyPair } from '../../ec2/sshKeyPair' - -// describe('Ec2ConnectClient', function () { -// let currentInstanceOs: string -// class MockSsmClient extends SsmClient { -// public constructor() { -// super('test-region') -// } - -// public override async getInstanceAgentPingStatus(target: string): Promise { -// return target.split(':')[2] -// } - -// public override async getTargetPlatformName(target: string): Promise { -// return currentInstanceOs -// } -// } - -// class MockEc2Client extends Ec2Client { -// public constructor() { -// super('test-region') -// } - -// public override async getInstanceStatus(instanceId: string): Promise { -// return instanceId.split(':')[0] as EC2.InstanceStateName -// } -// } - -// class MockEc2ConnectClient extends Ec2ConnectionManager { -// public constructor() { -// super('test-region') -// } - -// protected override createSsmSdkClient(): SsmClient { -// return new MockSsmClient() -// } - -// protected override createEc2SdkClient(): Ec2Client { -// return new MockEc2Client() -// } - -// public async testGetRemoteUser(instanceId: string, osName: string) { -// currentInstanceOs = osName -// const remoteUser = await this.getRemoteUser(instanceId) -// return remoteUser -// } -// } - -// describe('isInstanceRunning', async function () { -// let client: MockEc2ConnectClient - -// before(function () { -// client = new MockEc2ConnectClient() -// }) - -// it('only returns true with the instance is running', async function () { -// const actualFirstResult = await client.isInstanceRunning('running:noPolicies') -// const actualSecondResult = await client.isInstanceRunning('stopped:noPolicies') - -// assert.strictEqual(true, actualFirstResult) -// assert.strictEqual(false, actualSecondResult) -// }) -// }) - -// describe('handleStartSessionError', async function () { -// let client: MockEc2ConnectClientForError - -// class MockEc2ConnectClientForError extends MockEc2ConnectClient { -// public override async hasProperPolicies(instanceId: string): Promise { -// return instanceId.split(':')[1] === 'hasPolicies' -// } -// } -// before(function () { -// client = new MockEc2ConnectClientForError() -// }) - -// it('determines which error to throw based on if instance is running', async function () { -// async function assertThrowsErrorCode(testInstance: Ec2Selection, errCode: Ec2ConnectErrorCode) { -// try { -// await client.checkForStartSessionError(testInstance) -// } catch (err: unknown) { -// assert.strictEqual((err as ToolkitError).code, errCode) -// } -// } - -// await assertThrowsErrorCode( -// { -// instanceId: 'pending:noPolicies:Online', -// region: 'test-region', -// }, -// 'EC2SSMStatus' -// ) - -// await assertThrowsErrorCode( -// { -// instanceId: 'shutting-down:noPolicies:Online', -// region: 'test-region', -// }, -// 'EC2SSMStatus' -// ) - -// await assertThrowsErrorCode( -// { -// instanceId: 'running:noPolicies:Online', -// region: 'test-region', -// }, -// 'EC2SSMPermission' -// ) - -// await assertThrowsErrorCode( -// { -// instanceId: 'running:hasPolicies:Offline', -// region: 'test-region', -// }, -// 'EC2SSMAgentStatus' -// ) -// }) - -// it('does not throw an error if all checks pass', async function () { -// const passingInstance = { -// instanceId: 'running:hasPolicies:Online', -// region: 'test-region', -// } -// assert.doesNotThrow(async () => await client.checkForStartSessionError(passingInstance)) -// }) -// }) - -// describe('hasProperPolicies', async function () { -// let client: MockEc2ConnectClientForPolicies -// class MockEc2ConnectClientForPolicies extends MockEc2ConnectClient { -// protected override async getAttachedPolicies(instanceId: string): Promise { -// switch (instanceId) { -// case 'firstInstance': -// return [ -// { -// PolicyName: 'name', -// }, -// { -// PolicyName: 'name2', -// }, -// { -// PolicyName: 'name3', -// }, -// ] -// case 'secondInstance': -// return [ -// { -// PolicyName: 'AmazonSSMManagedInstanceCore', -// }, -// { -// PolicyName: 'AmazonSSMManagedEC2InstanceDefaultPolicy', -// }, -// ] -// case 'thirdInstance': -// return [ -// { -// PolicyName: 'AmazonSSMManagedInstanceCore', -// }, -// ] -// case 'fourthInstance': -// return [ -// { -// PolicyName: 'AmazonSSMManagedEC2InstanceDefaultPolicy', -// }, -// ] -// default: -// return [] -// } -// } -// } -// before(function () { -// client = new MockEc2ConnectClientForPolicies() -// }) - -// it('correctly determines if proper policies are included', async function () { -// let result: boolean - -// result = await client.hasProperPolicies('firstInstance') -// assert.strictEqual(false, result) - -// result = await client.hasProperPolicies('secondInstance') -// assert.strictEqual(true, result) - -// result = await client.hasProperPolicies('thirdInstance') -// assert.strictEqual(false, result) - -// result = await client.hasProperPolicies('fourthInstance') -// assert.strictEqual(false, result) -// }) -// }) - -// describe('sendSshKeysToInstance', async function () { -// let client: MockEc2ConnectClient -// let sendCommandStub: sinon.SinonStub< -// [target: string, documentName: string, parameters: SSM.Parameters], -// Promise> -// > - -// before(function () { -// client = new MockEc2ConnectClient() -// sendCommandStub = sinon.stub(MockSsmClient.prototype, 'sendCommandAndWait') -// }) - -// after(function () { -// sinon.restore() -// }) - -// it('calls the sdk with the proper parameters', async function () { -// const testSelection = { -// instanceId: 'test-id', -// region: 'test-region', -// } -// const mockKeys = mock() as SshKeyPair -// await client.sendSshKeyToInstance(testSelection, mockKeys, '') -// sinon.assert.calledWith(sendCommandStub, testSelection.instanceId, 'AWS-RunShellScript') -// }) -// }) - -// describe('determineRemoteUser', async function () { -// let client: MockEc2ConnectClient - -// before(function () { -// client = new MockEc2ConnectClient() -// }) - -// it('identifies the user for ubuntu as ubuntu', async function () { -// const remoteUser = await client.testGetRemoteUser('testInstance', 'Ubuntu') -// assert.strictEqual(remoteUser, 'ubuntu') -// }) - -// it('identifies the user for amazon linux as ec2-user', async function () { -// const remoteUser = await client.testGetRemoteUser('testInstance', 'Amazon Linux') -// assert.strictEqual(remoteUser, 'ec2-user') -// }) - -// it('throws error when not given known OS', async function () { -// try { -// await client.testGetRemoteUser('testInstance', 'ThisIsNotARealOs!') -// assert.ok(false) -// } catch (exception) { -// assert.ok(true) -// } -// }) -// }) -// }) +import * as assert from 'assert' +import * as sinon from 'sinon' +import { Ec2ConnectErrorCode, Ec2ConnectionManager } from '../../ec2/model' +import { SsmClient } from '../../shared/clients/ssmClient' +import { Ec2Client } from '../../shared/clients/ec2Client' +import { attachedPoliciesListType } from 'aws-sdk/clients/iam' +import { Ec2Selection } from '../../ec2/utils' +import { ToolkitError } from '../../shared/errors' +import { AWSError, EC2, SSM } from 'aws-sdk' +import { PromiseResult } from 'aws-sdk/lib/request' +import { GetCommandInvocationResult } from 'aws-sdk/clients/ssm' +import { mock } from 'ts-mockito' +import { SshKeyPair } from '../../ec2/sshKeyPair' + +describe('Ec2ConnectClient', function () { + let currentInstanceOs: string + class MockSsmClient extends SsmClient { + public constructor() { + super('test-region') + } + + public override async getInstanceAgentPingStatus(target: string): Promise { + return target.split(':')[2] + } + + public override async getTargetPlatformName(target: string): Promise { + return currentInstanceOs + } + } + + class MockEc2Client extends Ec2Client { + public constructor() { + super('test-region') + } + + public override async getInstanceStatus(instanceId: string): Promise { + return instanceId.split(':')[0] as EC2.InstanceStateName + } + } + + class MockEc2ConnectClient extends Ec2ConnectionManager { + public constructor() { + super('test-region') + } + + protected override createSsmSdkClient(): SsmClient { + return new MockSsmClient() + } + + protected override createEc2SdkClient(): Ec2Client { + return new MockEc2Client() + } + + public async testGetRemoteUser(instanceId: string, osName: string) { + currentInstanceOs = osName + const remoteUser = await this.getRemoteUser(instanceId) + return remoteUser + } + } + + describe('isInstanceRunning', async function () { + let client: MockEc2ConnectClient + + before(function () { + client = new MockEc2ConnectClient() + }) + + it('only returns true with the instance is running', async function () { + const actualFirstResult = await client.isInstanceRunning('running:noPolicies') + const actualSecondResult = await client.isInstanceRunning('stopped:noPolicies') + + assert.strictEqual(true, actualFirstResult) + assert.strictEqual(false, actualSecondResult) + }) + }) + + describe('handleStartSessionError', async function () { + let client: MockEc2ConnectClientForError + + class MockEc2ConnectClientForError extends MockEc2ConnectClient { + public override async hasProperPolicies(instanceId: string): Promise { + return instanceId.split(':')[1] === 'hasPolicies' + } + } + before(function () { + client = new MockEc2ConnectClientForError() + }) + + it('determines which error to throw based on if instance is running', async function () { + async function assertThrowsErrorCode(testInstance: Ec2Selection, errCode: Ec2ConnectErrorCode) { + try { + await client.checkForStartSessionError(testInstance) + } catch (err: unknown) { + assert.strictEqual((err as ToolkitError).code, errCode) + } + } + + await assertThrowsErrorCode( + { + instanceId: 'pending:noPolicies:Online', + region: 'test-region', + }, + 'EC2SSMStatus' + ) + + await assertThrowsErrorCode( + { + instanceId: 'shutting-down:noPolicies:Online', + region: 'test-region', + }, + 'EC2SSMStatus' + ) + + await assertThrowsErrorCode( + { + instanceId: 'running:noPolicies:Online', + region: 'test-region', + }, + 'EC2SSMPermission' + ) + + await assertThrowsErrorCode( + { + instanceId: 'running:hasPolicies:Offline', + region: 'test-region', + }, + 'EC2SSMAgentStatus' + ) + }) + + it('does not throw an error if all checks pass', async function () { + const passingInstance = { + instanceId: 'running:hasPolicies:Online', + region: 'test-region', + } + assert.doesNotThrow(async () => await client.checkForStartSessionError(passingInstance)) + }) + }) + + describe('hasProperPolicies', async function () { + let client: MockEc2ConnectClientForPolicies + class MockEc2ConnectClientForPolicies extends MockEc2ConnectClient { + protected override async getAttachedPolicies(instanceId: string): Promise { + switch (instanceId) { + case 'firstInstance': + return [ + { + PolicyName: 'name', + }, + { + PolicyName: 'name2', + }, + { + PolicyName: 'name3', + }, + ] + case 'secondInstance': + return [ + { + PolicyName: 'AmazonSSMManagedInstanceCore', + }, + { + PolicyName: 'AmazonSSMManagedEC2InstanceDefaultPolicy', + }, + ] + case 'thirdInstance': + return [ + { + PolicyName: 'AmazonSSMManagedInstanceCore', + }, + ] + case 'fourthInstance': + return [ + { + PolicyName: 'AmazonSSMManagedEC2InstanceDefaultPolicy', + }, + ] + default: + return [] + } + } + } + before(function () { + client = new MockEc2ConnectClientForPolicies() + }) + + it('correctly determines if proper policies are included', async function () { + let result: boolean + + result = await client.hasProperPolicies('firstInstance') + assert.strictEqual(false, result) + + result = await client.hasProperPolicies('secondInstance') + assert.strictEqual(true, result) + + result = await client.hasProperPolicies('thirdInstance') + assert.strictEqual(false, result) + + result = await client.hasProperPolicies('fourthInstance') + assert.strictEqual(false, result) + }) + }) + + describe('sendSshKeysToInstance', async function () { + let client: MockEc2ConnectClient + let sendCommandStub: sinon.SinonStub< + [target: string, documentName: string, parameters: SSM.Parameters], + Promise> + > + + before(function () { + client = new MockEc2ConnectClient() + sendCommandStub = sinon.stub(MockSsmClient.prototype, 'sendCommandAndWait') + }) + + after(function () { + sinon.restore() + }) + + it('calls the sdk with the proper parameters', async function () { + const testSelection = { + instanceId: 'test-id', + region: 'test-region', + } + const mockKeys = mock() as SshKeyPair + await client.sendSshKeyToInstance(testSelection, mockKeys, '') + sinon.assert.calledWith(sendCommandStub, testSelection.instanceId, 'AWS-RunShellScript') + }) + }) + + describe('determineRemoteUser', async function () { + let client: MockEc2ConnectClient + + before(function () { + client = new MockEc2ConnectClient() + }) + + it('identifies the user for ubuntu as ubuntu', async function () { + const remoteUser = await client.testGetRemoteUser('testInstance', 'Ubuntu') + assert.strictEqual(remoteUser, 'ubuntu') + }) + + it('identifies the user for amazon linux as ec2-user', async function () { + const remoteUser = await client.testGetRemoteUser('testInstance', 'Amazon Linux') + assert.strictEqual(remoteUser, 'ec2-user') + }) + + it('throws error when not given known OS', async function () { + try { + await client.testGetRemoteUser('testInstance', 'ThisIsNotARealOs!') + assert.ok(false) + } catch (exception) { + assert.ok(true) + } + }) + }) +}) From 27179ab2454f916d4600498529f65cf8c813e771 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 24 Jul 2023 12:11:53 -0700 Subject: [PATCH 130/172] move the rest of the the tests to the sshConfig file --- src/test/codecatalyst/tools.test.ts | 133 ---------------------------- src/test/shared/sshConfig.test.ts | 128 +++++++++++++++++++++++++- 2 files changed, 126 insertions(+), 135 deletions(-) delete mode 100644 src/test/codecatalyst/tools.test.ts diff --git a/src/test/codecatalyst/tools.test.ts b/src/test/codecatalyst/tools.test.ts deleted file mode 100644 index 2d69405c252..00000000000 --- a/src/test/codecatalyst/tools.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as sinon from 'sinon' -import * as path from 'path' -import * as http from 'http' -import * as assert from 'assert' -import { fileExists, makeTemporaryToolkitFolder } from '../../shared/filesystemUtilities' -import { ChildProcess } from '../../shared/utilities/childProcess' -import { FakeExtensionContext } from '../fakeExtensionContext' -import { - bearerTokenCacheLocation, - connectScriptPrefix, - DevEnvironmentId, - getCodeCatalystSsmEnv, -} from '../../codecatalyst/model' -import { mkdir, readFile, writeFile } from 'fs-extra' -import { StartDevEnvironmentSessionRequest } from 'aws-sdk/clients/codecatalyst' -import { SystemUtilities } from '../../shared/systemUtilities' -import { ensureConnectScript, sshLogFileLocation } from '../../shared/sshConfig' - -describe('Connect Script', function () { - let context: FakeExtensionContext - - function isWithin(path1: string, path2: string): boolean { - const rel = path.relative(path1, path2) - return !path.isAbsolute(rel) && !rel.startsWith('..') && !!rel - } - - beforeEach(async function () { - context = await FakeExtensionContext.create() - context.globalStorageUri = vscode.Uri.file(await makeTemporaryToolkitFolder()) - }) - - it('can get a connect script path, adding a copy to global storage', async function () { - const script = (await ensureConnectScript(connectScriptPrefix, context)).unwrap().fsPath - assert.ok(await fileExists(script)) - assert.ok(isWithin(context.globalStorageUri.fsPath, script)) - }) - - function createFakeServer(testDevEnv: DevEnvironmentId) { - return http.createServer(async (req, resp) => { - try { - const data = await new Promise((resolve, reject) => { - req.on('error', reject) - req.on('data', d => resolve(d.toString())) - }) - - const body = JSON.parse(data) - const expected: Pick = { - sessionConfiguration: { sessionType: 'SSH' }, - } - - const expectedPath = `/v1/spaces/${testDevEnv.org.name}/projects/${testDevEnv.project.name}/devEnvironments/${testDevEnv.id}/session` - - assert.deepStrictEqual(body, expected) - assert.strictEqual(req.url, expectedPath) - } catch (e) { - resp.writeHead(400, { 'Content-Type': 'application/json' }) - resp.end(JSON.stringify({ name: 'ValidationException', message: (e as Error).message })) - - return - } - - resp.writeHead(200, { 'Content-Type': 'application/json' }) - resp.end( - JSON.stringify({ - tokenValue: 'a token', - streamUrl: 'some url', - sessionId: 'an id', - }) - ) - }) - } - - it('can run the script with environment variables', async function () { - const testDevEnv: DevEnvironmentId = { - id: '01234567890', - project: { name: 'project' }, - org: { name: 'org' }, - } - - const server = createFakeServer(testDevEnv) - const address = await new Promise((resolve, reject) => { - server.on('error', reject) - server.listen({ host: 'localhost', port: 28142 }, () => resolve(`http://localhost:28142`)) - }) - - await writeFile(bearerTokenCacheLocation(testDevEnv.id), 'token') - const script = (await ensureConnectScript(connectScriptPrefix, context)).unwrap().fsPath - const env = getCodeCatalystSsmEnv('us-weast-1', 'echo', testDevEnv) - env.CODECATALYST_ENDPOINT = address - - // This could be de-duped - const isWindows = process.platform === 'win32' - const cmd = isWindows ? 'powershell.exe' : script - const args = isWindows ? ['-ExecutionPolicy', 'Bypass', '-File', script, 'bar'] : [script, 'bar'] - - const output = await new ChildProcess(cmd, args).run({ spawnOptions: { env } }) - if (output.exitCode !== 0) { - const logOutput = sshLogFileLocation('codecatalyst', testDevEnv.id) - const message = `stderr:\n${output.stderr}\n\nlogs:\n${await readFile(logOutput)}` - - assert.fail(`Connect script should exit with a zero status:\n${message}`) - } - }) - - describe('~/.ssh', function () { - let tmpDir: string - - beforeEach(async function () { - tmpDir = await makeTemporaryToolkitFolder() - sinon.stub(SystemUtilities, 'getHomeDirectory').returns(tmpDir) - }) - - afterEach(async function () { - sinon.restore() - await SystemUtilities.delete(tmpDir, { recursive: true }) - }) - - it('works if the .ssh directory is missing', async function () { - ;(await ensureConnectScript(connectScriptPrefix, context)).unwrap() - }) - - it('works if the .ssh directory exists but has different perms', async function () { - await mkdir(path.join(tmpDir, '.ssh'), 0o777) - ;(await ensureConnectScript(connectScriptPrefix, context)).unwrap() - }) - }) -}) diff --git a/src/test/shared/sshConfig.test.ts b/src/test/shared/sshConfig.test.ts index 41d054a7944..e65891fa088 100644 --- a/src/test/shared/sshConfig.test.ts +++ b/src/test/shared/sshConfig.test.ts @@ -4,10 +4,24 @@ */ import * as assert from 'assert' import * as sinon from 'sinon' +import * as path from 'path' +import * as vscode from 'vscode' +import * as http from 'http' import { ToolkitError } from '../../shared/errors' import { Result } from '../../shared/utilities/result' -import { ChildProcessResult } from '../../shared/utilities/childProcess' -import { VscodeRemoteSshConfig, sshLogFileLocation } from '../../shared/sshConfig' +import { ChildProcess, ChildProcessResult } from '../../shared/utilities/childProcess' +import { VscodeRemoteSshConfig, ensureConnectScript, sshLogFileLocation } from '../../shared/sshConfig' +import { FakeExtensionContext } from '../fakeExtensionContext' +import { fileExists, makeTemporaryToolkitFolder } from '../../shared/filesystemUtilities' +import { + DevEnvironmentId, + bearerTokenCacheLocation, + connectScriptPrefix, + getCodeCatalystSsmEnv, +} from '../../codecatalyst/model' +import { StartDevEnvironmentSessionRequest } from 'aws-sdk/clients/codecatalyst' +import { mkdir, readFile, writeFile } from 'fs-extra' +import { SystemUtilities } from '../../shared/systemUtilities' class MockSshConfig extends VscodeRemoteSshConfig { // State variables to track logic flow. @@ -168,3 +182,113 @@ describe('VscodeRemoteSshConfig', async function () { }) }) }) + +describe('Connect Script', function () { + let context: FakeExtensionContext + + function isWithin(path1: string, path2: string): boolean { + const rel = path.relative(path1, path2) + return !path.isAbsolute(rel) && !rel.startsWith('..') && !!rel + } + + beforeEach(async function () { + context = await FakeExtensionContext.create() + context.globalStorageUri = vscode.Uri.file(await makeTemporaryToolkitFolder()) + }) + + it('can get a connect script path, adding a copy to global storage', async function () { + const script = (await ensureConnectScript(connectScriptPrefix, context)).unwrap().fsPath + assert.ok(await fileExists(script)) + assert.ok(isWithin(context.globalStorageUri.fsPath, script)) + }) + + function createFakeServer(testDevEnv: DevEnvironmentId) { + return http.createServer(async (req, resp) => { + try { + const data = await new Promise((resolve, reject) => { + req.on('error', reject) + req.on('data', d => resolve(d.toString())) + }) + + const body = JSON.parse(data) + const expected: Pick = { + sessionConfiguration: { sessionType: 'SSH' }, + } + + const expectedPath = `/v1/spaces/${testDevEnv.org.name}/projects/${testDevEnv.project.name}/devEnvironments/${testDevEnv.id}/session` + + assert.deepStrictEqual(body, expected) + assert.strictEqual(req.url, expectedPath) + } catch (e) { + resp.writeHead(400, { 'Content-Type': 'application/json' }) + resp.end(JSON.stringify({ name: 'ValidationException', message: (e as Error).message })) + + return + } + + resp.writeHead(200, { 'Content-Type': 'application/json' }) + resp.end( + JSON.stringify({ + tokenValue: 'a token', + streamUrl: 'some url', + sessionId: 'an id', + }) + ) + }) + } + + it('can run the script with environment variables', async function () { + const testDevEnv: DevEnvironmentId = { + id: '01234567890', + project: { name: 'project' }, + org: { name: 'org' }, + } + + const server = createFakeServer(testDevEnv) + const address = await new Promise((resolve, reject) => { + server.on('error', reject) + server.listen({ host: 'localhost', port: 28142 }, () => resolve(`http://localhost:28142`)) + }) + + await writeFile(bearerTokenCacheLocation(testDevEnv.id), 'token') + const script = (await ensureConnectScript(connectScriptPrefix, context)).unwrap().fsPath + const env = getCodeCatalystSsmEnv('us-weast-1', 'echo', testDevEnv) + env.CODECATALYST_ENDPOINT = address + + // This could be de-duped + const isWindows = process.platform === 'win32' + const cmd = isWindows ? 'powershell.exe' : script + const args = isWindows ? ['-ExecutionPolicy', 'Bypass', '-File', script, 'bar'] : [script, 'bar'] + + const output = await new ChildProcess(cmd, args).run({ spawnOptions: { env } }) + if (output.exitCode !== 0) { + const logOutput = sshLogFileLocation('codecatalyst', testDevEnv.id) + const message = `stderr:\n${output.stderr}\n\nlogs:\n${await readFile(logOutput)}` + + assert.fail(`Connect script should exit with a zero status:\n${message}`) + } + }) + + describe('~/.ssh', function () { + let tmpDir: string + + beforeEach(async function () { + tmpDir = await makeTemporaryToolkitFolder() + sinon.stub(SystemUtilities, 'getHomeDirectory').returns(tmpDir) + }) + + afterEach(async function () { + sinon.restore() + await SystemUtilities.delete(tmpDir, { recursive: true }) + }) + + it('works if the .ssh directory is missing', async function () { + ;(await ensureConnectScript(connectScriptPrefix, context)).unwrap() + }) + + it('works if the .ssh directory exists but has different perms', async function () { + await mkdir(path.join(tmpDir, '.ssh'), 0o777) + ;(await ensureConnectScript(connectScriptPrefix, context)).unwrap() + }) + }) +}) From a8a92ae0b8674c534143cf1c3b39e35604857e6e Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 24 Jul 2023 12:23:21 -0700 Subject: [PATCH 131/172] add codecatalyst label to relevant tests --- src/test/shared/sshConfig.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/shared/sshConfig.test.ts b/src/test/shared/sshConfig.test.ts index e65891fa088..9392a539e8c 100644 --- a/src/test/shared/sshConfig.test.ts +++ b/src/test/shared/sshConfig.test.ts @@ -183,7 +183,7 @@ describe('VscodeRemoteSshConfig', async function () { }) }) -describe('Connect Script', function () { +describe('CodeCatalyst Connect Script', function () { let context: FakeExtensionContext function isWithin(path1: string, path2: string): boolean { From d6896c1bd2f28831bdd310bd4e3d540ae2b1059b Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 24 Jul 2023 12:26:06 -0700 Subject: [PATCH 132/172] rename file to mirror main class name in file --- src/codecatalyst/model.ts | 2 +- src/ec2/model.ts | 2 +- src/shared/{sshConfig.ts => vscodeRemoteSshConfig.ts} | 0 src/test/shared/{sshConfig.test.ts => vscodeRemoteSshConfig.ts} | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/shared/{sshConfig.ts => vscodeRemoteSshConfig.ts} (100%) rename src/test/shared/{sshConfig.test.ts => vscodeRemoteSshConfig.ts} (99%) diff --git a/src/codecatalyst/model.ts b/src/codecatalyst/model.ts index 1f8268c4569..564f5f1101c 100644 --- a/src/codecatalyst/model.ts +++ b/src/codecatalyst/model.ts @@ -30,7 +30,7 @@ import { CodeCatalystAuthenticationProvider } from './auth' import { ToolkitError } from '../shared/errors' import { Result } from '../shared/utilities/result' import { VscodeRemoteConnection, ensureDependencies } from '../shared/remoteSession' -import { VscodeRemoteSshConfig, sshLogFileLocation } from '../shared/sshConfig' +import { VscodeRemoteSshConfig, sshLogFileLocation } from '../shared/vscodeRemoteSshConfig' export type DevEnvironmentId = Pick export const hostNamePrefix = 'aws-devenv-' diff --git a/src/ec2/model.ts b/src/ec2/model.ts index 508f797db52..ca168445f87 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -19,7 +19,7 @@ import { createBoundProcess } from '../codecatalyst/model' import { getLogger } from '../shared/logger/logger' import { Timeout } from '../shared/utilities/timeoutUtils' import { showMessageWithCancel } from '../shared/utilities/messages' -import { VscodeRemoteSshConfig, sshLogFileLocation } from '../shared/sshConfig' +import { VscodeRemoteSshConfig, sshLogFileLocation } from '../shared/vscodeRemoteSshConfig' import { SshKeyPair } from './sshKeyPair' import path = require('path') import globals from '../shared/extensionGlobals' diff --git a/src/shared/sshConfig.ts b/src/shared/vscodeRemoteSshConfig.ts similarity index 100% rename from src/shared/sshConfig.ts rename to src/shared/vscodeRemoteSshConfig.ts diff --git a/src/test/shared/sshConfig.test.ts b/src/test/shared/vscodeRemoteSshConfig.ts similarity index 99% rename from src/test/shared/sshConfig.test.ts rename to src/test/shared/vscodeRemoteSshConfig.ts index 9392a539e8c..e748c814bad 100644 --- a/src/test/shared/sshConfig.test.ts +++ b/src/test/shared/vscodeRemoteSshConfig.ts @@ -10,7 +10,7 @@ import * as http from 'http' import { ToolkitError } from '../../shared/errors' import { Result } from '../../shared/utilities/result' import { ChildProcess, ChildProcessResult } from '../../shared/utilities/childProcess' -import { VscodeRemoteSshConfig, ensureConnectScript, sshLogFileLocation } from '../../shared/sshConfig' +import { VscodeRemoteSshConfig, ensureConnectScript, sshLogFileLocation } from '../../shared/vscodeRemoteSshConfig' import { FakeExtensionContext } from '../fakeExtensionContext' import { fileExists, makeTemporaryToolkitFolder } from '../../shared/filesystemUtilities' import { From e201693927fcdf013c5344f72e2d3491b7257c5e Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 24 Jul 2023 12:32:25 -0700 Subject: [PATCH 133/172] rename test file --- .../{vscodeRemoteSshConfig.ts => vscodeRemoteSshConfig.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/test/shared/{vscodeRemoteSshConfig.ts => vscodeRemoteSshConfig.test.ts} (100%) diff --git a/src/test/shared/vscodeRemoteSshConfig.ts b/src/test/shared/vscodeRemoteSshConfig.test.ts similarity index 100% rename from src/test/shared/vscodeRemoteSshConfig.ts rename to src/test/shared/vscodeRemoteSshConfig.test.ts From 3bbabd1057cfb10110bfbb1bf16e90683f57df63 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 24 Jul 2023 12:39:54 -0700 Subject: [PATCH 134/172] change path import structure --- src/ec2/model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ec2/model.ts b/src/ec2/model.ts index ca168445f87..7610206335b 100644 --- a/src/ec2/model.ts +++ b/src/ec2/model.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' +import * as path from 'path' import { Session } from 'aws-sdk/clients/ssm' import { IAM, SSM } from 'aws-sdk' import { Ec2Selection } from './utils' @@ -21,7 +22,6 @@ import { Timeout } from '../shared/utilities/timeoutUtils' import { showMessageWithCancel } from '../shared/utilities/messages' import { VscodeRemoteSshConfig, sshLogFileLocation } from '../shared/vscodeRemoteSshConfig' import { SshKeyPair } from './sshKeyPair' -import path = require('path') import globals from '../shared/extensionGlobals' export type Ec2ConnectErrorCode = 'EC2SSMStatus' | 'EC2SSMPermission' | 'EC2SSMConnect' | 'EC2SSMAgentStatus' From 8e281cd8113ac08df7f2f681f88a0ba2c55091b6 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 31 Jul 2023 16:15:49 -0700 Subject: [PATCH 135/172] fix duplicate declaration error --- src/ec2/activation.ts | 9 +-------- src/ec2/commands.ts | 4 ++-- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/ec2/activation.ts b/src/ec2/activation.ts index 401ac5204d2..0bd2bab5b47 100644 --- a/src/ec2/activation.ts +++ b/src/ec2/activation.ts @@ -24,19 +24,12 @@ export async function activate(ctx: ExtContext): Promise { }) }), - Commands.register('aws.ec2.openRemoteConnection', async (node?: Ec2InstanceNode) => { - const selection = node ? node.toSelection() : await promptUserForEc2Selection() - const connectionManager = new Ec2ConnectionManager(selection.region) - - await connectionManager.attemptToOpenRemoteConnection(selection) - }), - Commands.register('aws.ec2.copyInstanceId', async (node: Ec2InstanceNode) => { await copyTextCommand(node, 'id') }), Commands.register('aws.ec2.openRemoteConnection', async (node?: Ec2Node) => { - await (node ? openRemoteConnection(node) : openRemoteConnection(node)) + await openRemoteConnection(node) }) ) } diff --git a/src/ec2/commands.ts b/src/ec2/commands.ts index 164852df2e7..2e8cbb259ab 100644 --- a/src/ec2/commands.ts +++ b/src/ec2/commands.ts @@ -18,8 +18,8 @@ export async function openTerminal(node?: Ec2Node) { export async function openRemoteConnection(node?: Ec2Node) { const selection = node instanceof Ec2InstanceNode ? node.toSelection() : await promptUserForEc2Selection() - //const connectionManager = new Ec2ConnectionManager(selection.region) - console.log(selection) + const connectionManager = new Ec2ConnectionManager(selection.region) + await connectionManager.attemptToOpenRemoteConnection(selection) } export async function copyInstanceId(instanceId: string): Promise { From 54ed4686483c004d92884f438f237a99c891beb5 Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Thu, 3 Aug 2023 08:46:08 -0700 Subject: [PATCH 136/172] separate return statement from body of return. Co-authored-by: JadenSimon <31319484+JadenSimon@users.noreply.github.com> --- src/shared/clients/ssmClient.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/clients/ssmClient.ts b/src/shared/clients/ssmClient.ts index cbc8db53364..172f4c8e270 100644 --- a/src/shared/clients/ssmClient.ts +++ b/src/shared/clients/ssmClient.ts @@ -64,6 +64,7 @@ export class SsmClient { public async getTargetPlatformName(target: string): Promise { const instanceInformation = await this.describeInstance(target) + return instanceInformation.PlatformName! } From c8f5cd3d364e293077cf907b85320dda22e40b92 Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Thu, 3 Aug 2023 14:36:43 -0700 Subject: [PATCH 137/172] attach sdout to error Co-authored-by: JadenSimon <31319484+JadenSimon@users.noreply.github.com> --- src/ec2/sshKeyPair.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ec2/sshKeyPair.ts b/src/ec2/sshKeyPair.ts index d3889cbcd81..b6a9e8c8f8e 100644 --- a/src/ec2/sshKeyPair.ts +++ b/src/ec2/sshKeyPair.ts @@ -24,7 +24,7 @@ export class SshKeyPair { const process = new ChildProcess(`ssh-keygen`, ['-t', 'rsa', '-N', '', '-q', '-f', keyPath]) const result = await process.run() if (result.exitCode !== 0) { - throw new ToolkitError('ec2: Failed to generate ssh key') + throw new ToolkitError('ec2: Failed to generate ssh key', { details: { stdout: result.stdout } }) } } From ba6afbfba5fdb209a3ae82c7029707293aef3e43 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Sat, 5 Aug 2023 10:18:23 -0700 Subject: [PATCH 138/172] fix failing tests --- src/test/ec2/explorer/ec2ParentNode.test.ts | 45 ++-- .../shared/clients/defaultEc2Client.test.ts | 196 ------------------ src/test/shared/clients/ec2Client.test.ts | 15 +- 3 files changed, 37 insertions(+), 219 deletions(-) delete mode 100644 src/test/shared/clients/defaultEc2Client.test.ts diff --git a/src/test/ec2/explorer/ec2ParentNode.test.ts b/src/test/ec2/explorer/ec2ParentNode.test.ts index 168fb3c62cb..c593c416c89 100644 --- a/src/test/ec2/explorer/ec2ParentNode.test.ts +++ b/src/test/ec2/explorer/ec2ParentNode.test.ts @@ -4,8 +4,8 @@ */ import * as assert from 'assert' +import * as sinon from 'sinon' import { Ec2ParentNode, contextValueEc2 } from '../../../ec2/explorer/ec2ParentNode' -import { stub } from '../../utilities/stubber' import { Ec2Client, Ec2Instance } from '../../../shared/clients/ec2Client' import { intoCollection } from '../../../shared/utilities/collectionUtils' import { @@ -13,16 +13,35 @@ import { assertNodeListOnlyHasPlaceholderNode, } from '../../utilities/explorerNodeAssertions' import { Ec2InstanceNode } from '../../../ec2/explorer/ec2InstanceNode' +import { EC2 } from 'aws-sdk' +import { AsyncCollection } from '../../../shared/utilities/asyncCollection' describe('ec2ParentNode', function () { let testNode: Ec2ParentNode let instances: Ec2Instance[] + let client: Ec2Client + let getInstanceStub: sinon.SinonStub<[filters?: EC2.Filter[] | undefined], Promise>> + const testRegion = 'testRegion' const testPartition = 'testPartition' - function createClient() { - const client = stub(Ec2Client, { regionCode: testRegion }) - client.getInstances.callsFake(async () => + before(function () { + //getInstanceStub = sinon.stub(Ec2Client.prototype, 'getInstances') + client = new Ec2Client(testRegion) + }) + + after(function () { + sinon.restore() + }) + + beforeEach(function () { + getInstanceStub = sinon.stub(Ec2Client.prototype, 'getInstances') + instances = [ + { name: 'firstOne', InstanceId: '0' }, + { name: 'secondOne', InstanceId: '1' }, + ] + + getInstanceStub.callsFake(async () => intoCollection( instances.map(instance => ({ InstanceId: instance.InstanceId, @@ -31,16 +50,11 @@ describe('ec2ParentNode', function () { ) ) - return client - } - - beforeEach(function () { - instances = [ - { name: 'firstOne', InstanceId: '0' }, - { name: 'secondOne', InstanceId: '1' }, - ] + testNode = new Ec2ParentNode(testRegion, testPartition, client) + }) - testNode = new Ec2ParentNode(testRegion, testPartition, createClient()) + afterEach(function () { + getInstanceStub.restore() }) it('returns placeholder node if no children are present', async function () { @@ -87,11 +101,10 @@ describe('ec2ParentNode', function () { }) it('has an error node for a child if an error happens during loading', async function () { - const client = createClient() - client.getInstances.throws(new Error()) - + getInstanceStub.throws(new Error()) const node = new Ec2ParentNode(testRegion, testPartition, client) assertNodeListOnlyHasErrorNode(await node.getChildren()) + getInstanceStub.restore() }) it('is able to handle children with duplicate names', async function () { diff --git a/src/test/shared/clients/defaultEc2Client.test.ts b/src/test/shared/clients/defaultEc2Client.test.ts deleted file mode 100644 index 825eb620194..00000000000 --- a/src/test/shared/clients/defaultEc2Client.test.ts +++ /dev/null @@ -1,196 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as assert from 'assert' -import { AsyncCollection } from '../../../shared/utilities/asyncCollection' -import { toCollection } from '../../../shared/utilities/asyncCollection' -import { intoCollection } from '../../../shared/utilities/collectionUtils' -import { Ec2Client, instanceHasName } from '../../../shared/clients/ec2Client' -import { EC2 } from 'aws-sdk' - -describe('EC2Client', async function () { - describe('extractInstancesFromReservations', function () { - const client = new Ec2Client('') - it('returns empty when given empty collection', async function () { - const actualResult = await client - .getInstancesFromReservations( - toCollection(async function* () { - yield [] - }) as AsyncCollection - ) - .promise() - - assert.strictEqual(0, actualResult.length) - }) - - it('flattens the reservationList', async function () { - const testReservationsList: EC2.ReservationList = [ - { - Instances: [ - { - InstanceId: 'id1', - Tags: [{ Key: 'Name', Value: 'name1' }], - }, - { - InstanceId: 'id2', - Tags: [{ Key: 'Name', Value: 'name2' }], - }, - ], - }, - { - Instances: [ - { - InstanceId: 'id3', - Tags: [{ Key: 'Name', Value: 'name3' }], - }, - { - InstanceId: 'id4', - Tags: [{ Key: 'Name', Value: 'name4' }], - }, - ], - }, - ] - const actualResult = await client - .getInstancesFromReservations(intoCollection([testReservationsList])) - .promise() - assert.deepStrictEqual( - [ - { InstanceId: 'id1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, - { InstanceId: 'id2', name: 'name2', Tags: [{ Key: 'Name', Value: 'name2' }] }, - { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, - { InstanceId: 'id4', name: 'name4', Tags: [{ Key: 'Name', Value: 'name4' }] }, - ], - actualResult - ) - }), - // Unsure if this test case is needed, but the return type in the SDK makes it possible these are unknown/not returned. - it('handles undefined and missing pieces in the ReservationList.', async function () { - const testReservationsList: EC2.ReservationList = [ - { - Instances: [ - { - InstanceId: 'id1', - }, - { - InstanceId: undefined, - }, - ], - }, - { - Instances: [ - { - InstanceId: 'id3', - Tags: [{ Key: 'Name', Value: 'name3' }], - }, - {}, - ], - }, - ] - const actualResult = await client - .getInstancesFromReservations(intoCollection([testReservationsList])) - .promise() - assert.deepStrictEqual( - [ - { InstanceId: 'id1' }, - { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, - ], - actualResult - ) - }) - - it('can process results without complete Tag field.', async function () { - const testReservationsList: EC2.ReservationList = [ - { - Instances: [ - { - InstanceId: 'id1', - Tags: [{ Key: 'Name', Value: 'name1' }], - }, - { - InstanceId: 'id2', - }, - ], - }, - { - Instances: [ - { - InstanceId: 'id3', - Tags: [{ Key: 'Name', Value: 'name3' }], - }, - { - InstanceId: 'id4', - Tags: [], - }, - ], - }, - ] - - const actualResult = await client - .getInstancesFromReservations(intoCollection([testReservationsList])) - .promise() - - assert.deepStrictEqual( - [ - { InstanceId: 'id1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] }, - { InstanceId: 'id2' }, - { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }, - { InstanceId: 'id4', Tags: [] }, - ], - actualResult - ) - }) - }) - - describe('getInstancesFilter', function () { - const client = new Ec2Client('') - - it('returns proper filter when given instanceId', function () { - const testInstanceId1 = 'test' - const actualFilters1 = client.getInstancesFilter([testInstanceId1]) - const expectedFilters1: EC2.Filter[] = [ - { - Name: 'instance-id', - Values: [testInstanceId1], - }, - ] - - assert.deepStrictEqual(expectedFilters1, actualFilters1) - - const testInstanceId2 = 'test2' - const actualFilters2 = client.getInstancesFilter([testInstanceId1, testInstanceId2]) - const expectedFilters2: EC2.Filter[] = [ - { - Name: 'instance-id', - Values: [testInstanceId1, testInstanceId2], - }, - ] - - assert.deepStrictEqual(expectedFilters2, actualFilters2) - }) - }) - - describe('instanceHasName', function () { - it('returns whether or not there is name attached to instance', function () { - const instances = [ - { InstanceId: 'id1', Tags: [] }, - { InstanceId: 'id2', name: 'name2', Tags: [{ Key: 'Name', Value: 'name2' }] }, - { InstanceId: 'id3', Tags: [{ Key: 'NotName', Value: 'notAName' }] }, - { - InstanceId: 'id4', - name: 'name4', - Tags: [ - { Key: 'Name', Value: 'name4' }, - { Key: 'anotherKey', Value: 'Another Key' }, - ], - }, - ] - - assert.deepStrictEqual(false, instanceHasName(instances[0])) - assert.deepStrictEqual(true, instanceHasName(instances[1])) - assert.deepStrictEqual(false, instanceHasName(instances[2])) - assert.deepStrictEqual(true, instanceHasName(instances[3])) - }) - }) -}) diff --git a/src/test/shared/clients/ec2Client.test.ts b/src/test/shared/clients/ec2Client.test.ts index 1b40f13f279..fb713e01296 100644 --- a/src/test/shared/clients/ec2Client.test.ts +++ b/src/test/shared/clients/ec2Client.test.ts @@ -103,13 +103,14 @@ describe('extractInstancesFromReservations', function () { .getInstancesFromReservations(intoCollection([completeReservationsList])) .promise() assert.deepStrictEqual(actualResult, completeInstanceList) - }), - it('handles undefined and missing pieces in the ReservationList.', async function () { - const actualResult = await client - .getInstancesFromReservations(intoCollection([incompleteReservationsList])) - .promise() - assert.deepStrictEqual(actualResult, incomepleteInstanceList) - }) + }) + + it('handles undefined and missing pieces in the ReservationList.', async function () { + const actualResult = await client + .getInstancesFromReservations(intoCollection([incompleteReservationsList])) + .promise() + assert.deepStrictEqual(actualResult, incomepleteInstanceList) + }) }) describe('updateInstancesDetail', async function () { From dd05a0d1d40ff60d66823a8acce69903452d8750 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Sat, 5 Aug 2023 10:44:52 -0700 Subject: [PATCH 139/172] remove duplicate commands --- package.json | 40 ---------------------------------------- 1 file changed, 40 deletions(-) diff --git a/package.json b/package.json index 7dc5942da3b..c46bf6e0f2d 100644 --- a/package.json +++ b/package.json @@ -2191,46 +2191,6 @@ } } }, - { - "command": "aws.ec2.openRemoteConnection", - "title": "%AWS.command.ec2.openRemoteConnection%", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ec2.startInstance", - "title": "%AWS.command.ec2.startInstance%", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ec2.stopInstance", - "title": "%AWS.command.ec2.stopInstance%", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ec2.rebootInstance", - "title": "%AWS.command.ec2.rebootInstance%", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, { "command": "aws.ec2.copyInstanceId", "title": "%AWS.command.ec2.copyInstanceId%", From ce8db343fe0cc0ec927cbb715a23ec134981eed4 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 7 Aug 2023 10:19:10 -0700 Subject: [PATCH 140/172] add icon back for terminal on explorer --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 3924f667dba..231dcb52add 100644 --- a/package.json +++ b/package.json @@ -2101,6 +2101,7 @@ { "command": "aws.ec2.openTerminal", "title": "%AWS.command.ec2.openTerminal%", + "icon": "$(terminal-view-icon)", "category": "%AWS.title%", "cloud9": { "cn": { From 7c0e57d5cbd4c434012fd84814ada5422ba6ab09 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Mon, 7 Aug 2023 10:23:29 -0700 Subject: [PATCH 141/172] expose command on explorer --- package.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/package.json b/package.json index 231dcb52add..fef19d2f085 100644 --- a/package.json +++ b/package.json @@ -1336,6 +1336,16 @@ "group": "inline@1", "when": "viewItem == awsEc2Node" }, + { + "command": "aws.ec2.openRemoteConnection", + "group": "0@1", + "when": "viewItem == awsEc2Node" + }, + { + "command": "aws.ec2.openRemoteConnection", + "group": "inline@1", + "when": "viewItem == awsEc2Node" + }, { "command": "aws.ec2.startInstance", "group": "0@1", @@ -2112,6 +2122,7 @@ { "command": "aws.ec2.openRemoteConnection", "title": "%AWS.command.ec2.openRemoteConnection%", + "icon": "$(remote-explorer)", "category": "%AWS.title%", "cloud9": { "cn": { From 9cbcb4842c84ca48162ff77091aec1d10b4eb4bc Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 8 Aug 2023 08:32:26 -0700 Subject: [PATCH 142/172] remove duplicate commands in package.json --- package.json | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 3f60369b072..6fa9a440139 100644 --- a/package.json +++ b/package.json @@ -1360,34 +1360,19 @@ "group": "inline@1", "when": "viewItem =~ /^(awsEc2(Parent|Running)Node)$/" }, - { - "command": "aws.ec2.startInstance", - "group": "0@1", - "when": "viewItem =~ /^(awsEc2(Stopped|Pending)Node)$/" - }, { "command": "aws.ec2.startInstance", "group": "inline@1", - "when": "viewItem =~ /^(awsEc2(Stopped|Pending)Node)$/" - }, - { - "command": "aws.ec2.stopInstance", - "group": "0@1", - "when": "viewItem =~ /^(awsEc2(Running|Pending)Node)$/" + "when": "viewItem =~ /^(awsEc2StoppedNode)$/" }, { "command": "aws.ec2.stopInstance", "group": "inline@1", - "when": "viewItem =~ /^(awsEc2(Running|Pending)Node)$/" - }, - { - "command": "aws.ec2.rebootInstance", - "group": "0@1", "when": "viewItem =~ /^(awsEc2RunningNode)$/" }, { "command": "aws.ec2.rebootInstance", - "group": "inline@1", + "group": "0@1", "when": "viewItem =~ /^(awsEc2RunningNode)$/" }, { From 4d6cd419c383da97347f9a359b2f3805d0c77b72 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Tue, 8 Aug 2023 08:34:13 -0700 Subject: [PATCH 143/172] remove duplicate terminal command --- package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package.json b/package.json index 6fa9a440139..d6fc58580ab 100644 --- a/package.json +++ b/package.json @@ -1120,10 +1120,6 @@ "command": "aws.ec2.openTerminal", "when": "aws.isDevMode" }, - { - "command": "aws.ec2.openTerminal", - "when": "aws.isDevMode" - }, { "command": "aws.ec2.startInstance", "when": "false" From ac6d47352b087cdf059db1afa8390c4ac5839647 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 10 Aug 2023 08:43:27 -0700 Subject: [PATCH 144/172] make instance readonly on instance node --- src/ec2/explorer/ec2InstanceNode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ec2/explorer/ec2InstanceNode.ts b/src/ec2/explorer/ec2InstanceNode.ts index 989ba6cf943..775b8992694 100644 --- a/src/ec2/explorer/ec2InstanceNode.ts +++ b/src/ec2/explorer/ec2InstanceNode.ts @@ -25,7 +25,7 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode public readonly client: Ec2Client, public override readonly regionCode: string, private readonly partitionId: string, - public instance: Ec2Instance + public readonly instance: Ec2Instance ) { super('') this.updateInstance(instance) From 292bc566697554910ef079ea4e6217a4dc5cd4b5 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 10 Aug 2023 10:06:24 -0700 Subject: [PATCH 145/172] fix merge errors in duplicate commands --- package.json | 44 ++++++++++++-------------------------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 72c4c08cf0b..5b9504b1b3b 100644 --- a/package.json +++ b/package.json @@ -1329,72 +1329,52 @@ { "command": "aws.ec2.openTerminal", "group": "0@1", - "when": "viewItem == awsEc2Node" + "when": "viewItem =~ /^(awsEc2(Parent|Running)Node)$/" }, { "command": "aws.ec2.openTerminal", "group": "inline@1", - "when": "viewItem == awsEc2Node" - }, - { - "command": "aws.ec2.openRemoteConnection", - "group": "0@1", - "when": "viewItem == awsEc2Node" + "when": "viewItem =~ /^(awsEc2(Parent|Running)Node)$/" }, { "command": "aws.ec2.openRemoteConnection", - "group": "inline@1", - "when": "viewItem == awsEc2Node" - }, - { - "command": "aws.ec2.startInstance", - "group": "0@1", - "when": "viewItem == awsEc2Node" - }, - { - "command": "aws.ec2.stopInstance", - "group": "0@1", - "when": "viewItem == awsEc2Node" - }, - { - "command": "aws.ec2.rebootInstance", "group": "0@1", "when": "viewItem =~ /^(awsEc2(Parent|Running)Node)$/" }, { - "command": "aws.ec2.openTerminal", + "command": "aws.ec2.openRemoteConnection", "group": "inline@1", "when": "viewItem =~ /^(awsEc2(Parent|Running)Node)$/" }, { "command": "aws.ec2.startInstance", "group": "0@1", - "when": "viewItem =~ /^(awsEc2(Stopped|Pending)Node)$/" + "when": "viewItem == awsEc2StoppedNode" }, { "command": "aws.ec2.startInstance", "group": "inline@1", - "when": "viewItem =~ /^(awsEc2(Stopped|Pending)Node)$/" + "when": "viewItem == awsEc2StoppedNode" }, { "command": "aws.ec2.stopInstance", "group": "0@1", - "when": "viewItem =~ /^(awsEc2(Running|Pending)Node)$/" + "when": "viewItem == awsEc2RunningNode" }, { "command": "aws.ec2.stopInstance", "group": "inline@1", - "when": "viewItem =~ /^(awsEc2(Running|Pending)Node)$/" + "when": "viewItem == awsEc2RunningNode" }, { "command": "aws.ec2.rebootInstance", "group": "0@1", - "when": "viewItem =~ /^(awsEc2RunningNode)$/" + "when": "viewItem == awsEc2RunningNode" }, { "command": "aws.ec2.rebootInstance", "group": "inline@1", - "when": "viewItem =~ /^(awsEc2RunningNode)$/" + "when": "viewItem == awsEc2RunningNode" }, { "command": "aws.ecr.createRepository", @@ -1488,7 +1468,7 @@ }, { "command": "aws.ec2.copyInstanceId", - "when": "view == aws.explorer && viewItem == awsEc2Node", + "when": "view == aws.explorer && viewItem =~ /^(awsEc2(Parent|Running|Stopped)Node)$/", "group": "2@0" }, { @@ -1678,12 +1658,12 @@ }, { "command": "aws.copyName", - "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode|awsStateMachineNode|awsCloudFormationNode|awsS3BucketNode|awsS3FolderNode|awsS3FileNode|awsApiGatewayNode|awsIotThingNode)$|^(awsAppRunnerServiceNode|awsIotCertificateNode|awsIotPolicyNode|awsIotPolicyVersionNode|awsEc2Node)/", + "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode|awsStateMachineNode|awsCloudFormationNode|awsS3BucketNode|awsS3FolderNode|awsS3FileNode|awsApiGatewayNode|awsIotThingNode)$|^(awsAppRunnerServiceNode|awsIotCertificateNode|awsIotPolicyNode|awsIotPolicyVersionNode|(awsEc2(Running|Stopped)Node))/", "group": "2@1" }, { "command": "aws.copyArn", - "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode|awsStateMachineNode|awsCloudFormationNode|awsCloudWatchLogNode|awsS3BucketNode|awsS3FolderNode|awsS3FileNode|awsApiGatewayNode|awsEcrRepositoryNode|awsIotThingNode)$|^(awsAppRunnerServiceNode|awsEcsServiceNode|awsIotCertificateNode|awsIotPolicyNode|awsIotPolicyVersionNode|awsMdeInstanceNode|awsEc2Node)/", + "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode|awsStateMachineNode|awsCloudFormationNode|awsCloudWatchLogNode|awsS3BucketNode|awsS3FolderNode|awsS3FileNode|awsApiGatewayNode|awsEcrRepositoryNode|awsIotThingNode)$|^(awsAppRunnerServiceNode|awsEcsServiceNode|awsIotCertificateNode|awsIotPolicyNode|awsIotPolicyVersionNode|awsMdeInstanceNode|(awsEc2(Running|Stopped)Node))/", "group": "2@2" }, { From 3eeba07930dfe0e1d31ed9db545556e244f6abf8 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 10 Aug 2023 10:08:17 -0700 Subject: [PATCH 146/172] enable utility functions on pending nodes --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5b9504b1b3b..64259b81519 100644 --- a/package.json +++ b/package.json @@ -1658,12 +1658,12 @@ }, { "command": "aws.copyName", - "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode|awsStateMachineNode|awsCloudFormationNode|awsS3BucketNode|awsS3FolderNode|awsS3FileNode|awsApiGatewayNode|awsIotThingNode)$|^(awsAppRunnerServiceNode|awsIotCertificateNode|awsIotPolicyNode|awsIotPolicyVersionNode|(awsEc2(Running|Stopped)Node))/", + "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode|awsStateMachineNode|awsCloudFormationNode|awsS3BucketNode|awsS3FolderNode|awsS3FileNode|awsApiGatewayNode|awsIotThingNode)$|^(awsAppRunnerServiceNode|awsIotCertificateNode|awsIotPolicyNode|awsIotPolicyVersionNode|(awsEc2(Running|Pending|Stopped)Node))/", "group": "2@1" }, { "command": "aws.copyArn", - "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode|awsStateMachineNode|awsCloudFormationNode|awsCloudWatchLogNode|awsS3BucketNode|awsS3FolderNode|awsS3FileNode|awsApiGatewayNode|awsEcrRepositoryNode|awsIotThingNode)$|^(awsAppRunnerServiceNode|awsEcsServiceNode|awsIotCertificateNode|awsIotPolicyNode|awsIotPolicyVersionNode|awsMdeInstanceNode|(awsEc2(Running|Stopped)Node))/", + "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode|awsStateMachineNode|awsCloudFormationNode|awsCloudWatchLogNode|awsS3BucketNode|awsS3FolderNode|awsS3FileNode|awsApiGatewayNode|awsEcrRepositoryNode|awsIotThingNode)$|^(awsAppRunnerServiceNode|awsEcsServiceNode|awsIotCertificateNode|awsIotPolicyNode|awsIotPolicyVersionNode|awsMdeInstanceNode|(awsEc2(Running|Pending|Stopped)Node))/", "group": "2@2" }, { From 3e73b23f21c8121a090b9520f9efe9091c2692c0 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 10 Aug 2023 10:15:29 -0700 Subject: [PATCH 147/172] remove dead files --- src/shared/vscodeRemoteSshConfig.ts | 229 -------------- src/test/shared/vscodeRemoteSshConfig.test.ts | 294 ------------------ 2 files changed, 523 deletions(-) delete mode 100644 src/shared/vscodeRemoteSshConfig.ts delete mode 100644 src/test/shared/vscodeRemoteSshConfig.test.ts diff --git a/src/shared/vscodeRemoteSshConfig.ts b/src/shared/vscodeRemoteSshConfig.ts deleted file mode 100644 index 5783cc621a2..00000000000 --- a/src/shared/vscodeRemoteSshConfig.ts +++ /dev/null @@ -1,229 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as path from 'path' -import * as fs from 'fs-extra' -import * as nls from 'vscode-nls' -import { getLogger } from './logger' -import { ChildProcess, ChildProcessResult } from './utilities/childProcess' -import { Result } from './utilities/result' -import { ToolkitError } from './errors' -import { getIdeProperties } from './extensionUtilities' -import { showConfirmationMessage } from './utilities/messages' -import { CancellationError } from './utilities/timeoutUtils' -import { getSshConfigPath } from './extensions/ssh' -import globals from './extensionGlobals' -import { fileExists, readFileAsString } from './filesystemUtilities' - -const localize = nls.loadMessageBundle() - -export class VscodeRemoteSshConfig { - protected readonly configHostName: string - protected readonly proxyCommandRegExp: RegExp - - public constructor( - protected readonly sshPath: string, - protected readonly hostNamePrefix: string, - protected readonly scriptPrefix: string, - protected readonly keyPath?: string - ) { - this.configHostName = `${hostNamePrefix}*` - this.proxyCommandRegExp = new RegExp(`proxycommand.{0,1024}${scriptPrefix}(.ps1)?.{0,99}`) - } - - protected isWin(): boolean { - return process.platform === 'win32' - } - - protected async getProxyCommand(command: string): Promise> { - if (this.isWin()) { - // Some older versions of OpenSSH (7.8 and below) have a bug where attempting to use powershell.exe directly will fail without an absolute path - const proc = new ChildProcess('powershell.exe', ['-Command', '(get-command powershell.exe).Path']) - const r = await proc.run() - if (r.exitCode !== 0) { - return Result.err(new ToolkitError('Failed to get absolute path for powershell', { cause: r.error })) - } - return Result.ok(`"${r.stdout}" -ExecutionPolicy RemoteSigned -File "${command}" %h`) - } else { - return Result.ok(`'${command}' '%h'`) - } - } - - public async ensureValid() { - const scriptResult = await ensureConnectScript(this.scriptPrefix) - if (scriptResult.isErr()) { - return scriptResult - } - - const connectScript = scriptResult.ok() - const proxyCommand = await this.getProxyCommand(connectScript.fsPath) - if (proxyCommand.isErr()) { - return proxyCommand - } - - const verifyHost = await this.verifySSHHost(proxyCommand.unwrap()) - if (verifyHost.isErr()) { - return verifyHost - } - - return Result.ok() - } - - protected async checkSshOnHost(): Promise { - const proc = new ChildProcess(this.sshPath, ['-G', `${this.hostNamePrefix}test`]) - const result = await proc.run() - return result - } - - protected async matchSshSection() { - const result = await this.checkSshOnHost() - if (result.exitCode !== 0) { - return Result.err(result.error ?? new Error(`ssh check against host failed: ${result.exitCode}`)) - } - const matches = result.stdout.match(this.proxyCommandRegExp) - return Result.ok(matches?.[0]) - } - - private async promptUserForOutdatedSection(configSection: string): Promise { - getLogger().warn(`SSH config: found old/outdated "${this.configHostName}" section:\n%O`, configSection) - const oldConfig = localize( - 'AWS.sshConfig.error.oldConfig', - 'Your ~/.ssh/config has a {0} section that might be out of date. Delete it, then try again.', - this.configHostName - ) - - const openConfig = localize('AWS.ssh.openConfig', 'Open config...') - vscode.window.showWarningMessage(oldConfig, openConfig).then(resp => { - if (resp === openConfig) { - vscode.window.showTextDocument(vscode.Uri.file(getSshConfigPath())) - } - }) - - throw new ToolkitError(oldConfig, { code: 'OldConfig' }) - } - - protected async writeSectionToConfig(proxyCommand: string) { - const sshConfigPath = getSshConfigPath() - const section = this.createSSHConfigSection(proxyCommand) - try { - await fs.ensureDir(path.dirname(path.dirname(sshConfigPath)), { mode: 0o755 }) - await fs.ensureDir(path.dirname(sshConfigPath), 0o700) - await fs.appendFile(sshConfigPath, section, { mode: 0o600 }) - } catch (e) { - const message = localize( - 'AWS.sshConfig.error.writeFail', - 'Failed to write SSH config: {0} (permission issue?)', - sshConfigPath - ) - - throw ToolkitError.chain(e, message, { code: 'ConfigWriteFailed' }) - } - } - - public async promptUserToConfigureSshConfig( - configSection: string | undefined, - proxyCommand: string - ): Promise { - if (configSection !== undefined) { - await this.promptUserForOutdatedSection(configSection) - } - - const confirmTitle = localize( - 'AWS.sshConfig.confirm.installSshConfig.title', - '{0} Toolkit will add host {1} to ~/.ssh/config to use SSH with your Dev Environments', - getIdeProperties().company, - this.configHostName - ) - const confirmText = localize('AWS.sshConfig.confirm.installSshConfig.button', 'Update SSH config') - const response = await showConfirmationMessage({ prompt: confirmTitle, confirm: confirmText }) - if (!response) { - throw new CancellationError('user') - } - - await this.writeSectionToConfig(proxyCommand) - } - - // Check if the hostname pattern is working. - protected async verifySSHHost(proxyCommand: string) { - const matchResult = await this.matchSshSection() - if (matchResult.isErr()) { - return matchResult - } - - const configSection = matchResult.ok() - const hasProxyCommand = configSection?.includes(proxyCommand) - - if (!hasProxyCommand) { - try { - await this.promptUserToConfigureSshConfig(configSection, proxyCommand) - } catch (e) { - return Result.err(e as Error) - } - } - - return Result.ok() - } - - private getBaseSSHConfig(proxyCommand: string): string { - // "AddKeysToAgent" will automatically add keys used on the server to the local agent. If not set, then `ssh-add` - // must be done locally. It's mostly a convenience thing; private keys are _not_ shared with the server. - - return ` -# Created by AWS Toolkit for VSCode. https://github.com/aws/aws-toolkit-vscode -Host ${this.configHostName} - ForwardAgent yes - AddKeysToAgent yes - StrictHostKeyChecking accept-new - ProxyCommand ${proxyCommand} - ` - } - - protected createSSHConfigSection(proxyCommand: string): string { - if (this.keyPath) { - return `${this.getBaseSSHConfig(proxyCommand)}IdentityFile '${this.keyPath}'\n User '%r'\n` - } - return this.getBaseSSHConfig(proxyCommand) - } -} - -export function sshLogFileLocation(service: string, id: string): string { - return path.join(globals.context.globalStorageUri.fsPath, `${service}.${id}.log`) -} - -export function constructScriptName(scriptPrefix: string) { - return `${scriptPrefix}${process.platform === 'win32' ? '.ps1' : ''}` -} - -export async function ensureConnectScript( - scriptPrefix: string, - context = globals.context -): Promise> { - const scriptName = constructScriptName(scriptPrefix) - - // Script resource path. Includes the Toolkit version string so it changes with each release. - const versionedScript = vscode.Uri.joinPath(context.extensionUri, 'resources', scriptName) - - // Copy to globalStorage to ensure a "stable" path (not influenced by Toolkit version string.) - const connectScript = vscode.Uri.joinPath(context.globalStorageUri, scriptName) - - try { - const exists = await fileExists(connectScript.fsPath) - const contents1 = await readFileAsString(versionedScript.fsPath) - const contents2 = exists ? await readFileAsString(connectScript.fsPath) : '' - const isOutdated = contents1 !== contents2 - - if (isOutdated) { - await fs.copyFile(versionedScript.fsPath, connectScript.fsPath) - getLogger().info('ssh: updated connect script') - } - - return Result.ok(connectScript) - } catch (e) { - const message = localize('AWS.sshConfig.error.copyScript', 'Failed to update connect script') - - return Result.err(ToolkitError.chain(e, message, { code: 'ConnectScriptUpdateFailed' })) - } -} diff --git a/src/test/shared/vscodeRemoteSshConfig.test.ts b/src/test/shared/vscodeRemoteSshConfig.test.ts deleted file mode 100644 index e748c814bad..00000000000 --- a/src/test/shared/vscodeRemoteSshConfig.test.ts +++ /dev/null @@ -1,294 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import * as assert from 'assert' -import * as sinon from 'sinon' -import * as path from 'path' -import * as vscode from 'vscode' -import * as http from 'http' -import { ToolkitError } from '../../shared/errors' -import { Result } from '../../shared/utilities/result' -import { ChildProcess, ChildProcessResult } from '../../shared/utilities/childProcess' -import { VscodeRemoteSshConfig, ensureConnectScript, sshLogFileLocation } from '../../shared/vscodeRemoteSshConfig' -import { FakeExtensionContext } from '../fakeExtensionContext' -import { fileExists, makeTemporaryToolkitFolder } from '../../shared/filesystemUtilities' -import { - DevEnvironmentId, - bearerTokenCacheLocation, - connectScriptPrefix, - getCodeCatalystSsmEnv, -} from '../../codecatalyst/model' -import { StartDevEnvironmentSessionRequest } from 'aws-sdk/clients/codecatalyst' -import { mkdir, readFile, writeFile } from 'fs-extra' -import { SystemUtilities } from '../../shared/systemUtilities' - -class MockSshConfig extends VscodeRemoteSshConfig { - // State variables to track logic flow. - public testIsWin: boolean = false - public configSection: string = '' - - public async getProxyCommandWrapper(command: string): Promise> { - return await this.getProxyCommand(command) - } - - public async testMatchSshSection(testSection: string) { - this.configSection = testSection - const result = await this.matchSshSection() - this.configSection = '' - return result - } - - public async testVerifySshHostWrapper(proxyCommand: string, testSection: string) { - this.configSection = testSection - const result = this.verifySSHHost(proxyCommand) - this.configSection = '' - return result - } - - protected override isWin() { - return this.testIsWin - } - - protected override async checkSshOnHost(): Promise { - return { - exitCode: 0, - error: undefined, - stdout: this.configSection, - stderr: '', - } - } - - public createSSHConfigSectionWrapper(proxyCommand: string): string { - return this.createSSHConfigSection(proxyCommand) - } -} - -describe('VscodeRemoteSshConfig', async function () { - let config: MockSshConfig - - const testCommand = 'test_connect' - const testProxyCommand = `'${testCommand}' '%h'` - - before(function () { - config = new MockSshConfig('sshPath', 'testHostNamePrefix', testCommand) - config.testIsWin = false - }) - - describe('getProxyCommand', async function () { - it('returns correct proxyCommand on non-windows', async function () { - config.testIsWin = false - const result = await config.getProxyCommandWrapper(testCommand) - assert.ok(result.isOk()) - const command = result.unwrap() - assert.strictEqual(command, testProxyCommand) - }) - }) - - describe('matchSshSection', async function () { - it('returns ok with match when proxycommand is present', async function () { - const testSection = `proxycommandfdsafdsafd${testProxyCommand}sa342432` - const result = await config.testMatchSshSection(testSection) - assert.ok(result.isOk()) - const match = result.unwrap() - assert.ok(match) - }) - - it('returns ok result with undefined inside when proxycommand is not present', async function () { - const testSection = `fdsafdsafdsa342432` - const result = await config.testMatchSshSection(testSection) - assert.ok(result.isOk()) - const match = result.unwrap() - assert.strictEqual(match, undefined) - }) - }) - - describe('verifySSHHost', async function () { - let promptUserToConfigureSshConfigStub: sinon.SinonStub< - [configSection: string | undefined, proxyCommand: string], - Promise - > - before(function () { - promptUserToConfigureSshConfigStub = sinon.stub( - VscodeRemoteSshConfig.prototype, - 'promptUserToConfigureSshConfig' - ) - }) - - beforeEach(function () { - promptUserToConfigureSshConfigStub.resetHistory() - }) - - after(function () { - sinon.restore() - }) - - it('writes to ssh config if command not found.', async function () { - const testSection = 'no-command-here' - const result = await config.testVerifySshHostWrapper(testCommand, testSection) - - assert.ok(result.isOk()) - sinon.assert.calledOn(promptUserToConfigureSshConfigStub, config) - sinon.assert.calledOnce(promptUserToConfigureSshConfigStub) - }) - - it('does not write to ssh config if command is find', async function () { - const testSection = `this is some text that doesn't matter, but here proxycommand ${testProxyCommand}` - const result = await config.testVerifySshHostWrapper(testCommand, testSection) - - assert.ok(result.isOk()) - sinon.assert.notCalled(promptUserToConfigureSshConfigStub) - }) - }) - - describe('createSSHConfigSection', async function () { - const testKeyPath = 'path/to/keys' - const newConfig = new MockSshConfig('sshPath', 'testHostNamePrefix', 'someScript', testKeyPath) - const expectedUserString = `User '%r'` - const expectedIdentityFileString = `IdentityFile '${testKeyPath}'` - - it('section includes relevant script prefix', function () { - const testScriptName = 'testScript' - const section = config.createSSHConfigSectionWrapper(testScriptName) - assert.ok(section.includes(testScriptName)) - }) - - it('includes keyPath if included in the class', function () { - const section = newConfig.createSSHConfigSectionWrapper('proxyCommand') - assert.ok(section.match(expectedIdentityFileString)) - }) - - it('parses the remote username from the ssh execution', function () { - const section = newConfig.createSSHConfigSectionWrapper('proxyCommand') - assert.ok(section.match(expectedUserString)) - }) - - it('omits User and IdentityFile fields when keyPath not given', function () { - const section = config.createSSHConfigSectionWrapper('proxyCommand') - assert.ok(!section.match(expectedUserString)) - assert.ok(!section.match(expectedIdentityFileString)) - }) - }) - - describe('sshLogFileLocation', async function () { - it('combines service and id into proper log file', function () { - const testService = 'testScript' - const testId = 'id' - const result = sshLogFileLocation(testService, testId) - - assert.ok(result.includes(testService)) - assert.ok(result.includes(testId)) - assert.ok(result.endsWith('.log')) - }) - }) -}) - -describe('CodeCatalyst Connect Script', function () { - let context: FakeExtensionContext - - function isWithin(path1: string, path2: string): boolean { - const rel = path.relative(path1, path2) - return !path.isAbsolute(rel) && !rel.startsWith('..') && !!rel - } - - beforeEach(async function () { - context = await FakeExtensionContext.create() - context.globalStorageUri = vscode.Uri.file(await makeTemporaryToolkitFolder()) - }) - - it('can get a connect script path, adding a copy to global storage', async function () { - const script = (await ensureConnectScript(connectScriptPrefix, context)).unwrap().fsPath - assert.ok(await fileExists(script)) - assert.ok(isWithin(context.globalStorageUri.fsPath, script)) - }) - - function createFakeServer(testDevEnv: DevEnvironmentId) { - return http.createServer(async (req, resp) => { - try { - const data = await new Promise((resolve, reject) => { - req.on('error', reject) - req.on('data', d => resolve(d.toString())) - }) - - const body = JSON.parse(data) - const expected: Pick = { - sessionConfiguration: { sessionType: 'SSH' }, - } - - const expectedPath = `/v1/spaces/${testDevEnv.org.name}/projects/${testDevEnv.project.name}/devEnvironments/${testDevEnv.id}/session` - - assert.deepStrictEqual(body, expected) - assert.strictEqual(req.url, expectedPath) - } catch (e) { - resp.writeHead(400, { 'Content-Type': 'application/json' }) - resp.end(JSON.stringify({ name: 'ValidationException', message: (e as Error).message })) - - return - } - - resp.writeHead(200, { 'Content-Type': 'application/json' }) - resp.end( - JSON.stringify({ - tokenValue: 'a token', - streamUrl: 'some url', - sessionId: 'an id', - }) - ) - }) - } - - it('can run the script with environment variables', async function () { - const testDevEnv: DevEnvironmentId = { - id: '01234567890', - project: { name: 'project' }, - org: { name: 'org' }, - } - - const server = createFakeServer(testDevEnv) - const address = await new Promise((resolve, reject) => { - server.on('error', reject) - server.listen({ host: 'localhost', port: 28142 }, () => resolve(`http://localhost:28142`)) - }) - - await writeFile(bearerTokenCacheLocation(testDevEnv.id), 'token') - const script = (await ensureConnectScript(connectScriptPrefix, context)).unwrap().fsPath - const env = getCodeCatalystSsmEnv('us-weast-1', 'echo', testDevEnv) - env.CODECATALYST_ENDPOINT = address - - // This could be de-duped - const isWindows = process.platform === 'win32' - const cmd = isWindows ? 'powershell.exe' : script - const args = isWindows ? ['-ExecutionPolicy', 'Bypass', '-File', script, 'bar'] : [script, 'bar'] - - const output = await new ChildProcess(cmd, args).run({ spawnOptions: { env } }) - if (output.exitCode !== 0) { - const logOutput = sshLogFileLocation('codecatalyst', testDevEnv.id) - const message = `stderr:\n${output.stderr}\n\nlogs:\n${await readFile(logOutput)}` - - assert.fail(`Connect script should exit with a zero status:\n${message}`) - } - }) - - describe('~/.ssh', function () { - let tmpDir: string - - beforeEach(async function () { - tmpDir = await makeTemporaryToolkitFolder() - sinon.stub(SystemUtilities, 'getHomeDirectory').returns(tmpDir) - }) - - afterEach(async function () { - sinon.restore() - await SystemUtilities.delete(tmpDir, { recursive: true }) - }) - - it('works if the .ssh directory is missing', async function () { - ;(await ensureConnectScript(connectScriptPrefix, context)).unwrap() - }) - - it('works if the .ssh directory exists but has different perms', async function () { - await mkdir(path.join(tmpDir, '.ssh'), 0o777) - ;(await ensureConnectScript(connectScriptPrefix, context)).unwrap() - }) - }) -}) From 6cab5775d2c9189621ae1d5b3ee978cbd36ede0e Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 10 Aug 2023 11:32:59 -0700 Subject: [PATCH 148/172] add comment about not-readonly --- src/ec2/explorer/ec2InstanceNode.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ec2/explorer/ec2InstanceNode.ts b/src/ec2/explorer/ec2InstanceNode.ts index 775b8992694..b55f623fe01 100644 --- a/src/ec2/explorer/ec2InstanceNode.ts +++ b/src/ec2/explorer/ec2InstanceNode.ts @@ -25,7 +25,8 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode public readonly client: Ec2Client, public override readonly regionCode: string, private readonly partitionId: string, - public readonly instance: Ec2Instance + /* the instance attribute is not read-only because we update its state when polling for updates */ + public instance: Ec2Instance ) { super('') this.updateInstance(instance) From a034dc3b9808827802886a5d6391b0471df27006 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 10 Aug 2023 11:52:48 -0700 Subject: [PATCH 149/172] refactor to allow for readonly instance with comment --- src/ec2/explorer/ec2InstanceNode.ts | 10 +++++----- src/test/ec2/explorer/ec2InstanceNode.test.ts | 7 ++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/ec2/explorer/ec2InstanceNode.ts b/src/ec2/explorer/ec2InstanceNode.ts index b55f623fe01..e8f10ec537d 100644 --- a/src/ec2/explorer/ec2InstanceNode.ts +++ b/src/ec2/explorer/ec2InstanceNode.ts @@ -25,8 +25,8 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode public readonly client: Ec2Client, public override readonly regionCode: string, private readonly partitionId: string, - /* the instance attribute is not read-only because we update its state when polling for updates */ - public instance: Ec2Instance + // XXX: this variable is marked as readonly, but the 'status' attribute is updated when polling the nodes. + public readonly instance: Ec2Instance ) { super('') this.updateInstance(instance) @@ -34,7 +34,7 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode } public updateInstance(newInstance: Ec2Instance) { - this.setInstance(newInstance) + this.setInstanceStatus(newInstance.status!) this.label = `${this.name} (${this.InstanceId})` this.contextValue = this.getContext() this.iconPath = new vscode.ThemeIcon(getIconCode(this.instance)) @@ -66,8 +66,8 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode return Ec2InstancePendingContext } - public setInstance(newInstance: Ec2Instance) { - this.instance = newInstance + public setInstanceStatus(instanceStatus: string) { + this.instance.status = instanceStatus } public toSelection(): Ec2Selection { diff --git a/src/test/ec2/explorer/ec2InstanceNode.test.ts b/src/test/ec2/explorer/ec2InstanceNode.test.ts index b45a17b0b0e..9fab8619724 100644 --- a/src/test/ec2/explorer/ec2InstanceNode.test.ts +++ b/src/test/ec2/explorer/ec2InstanceNode.test.ts @@ -83,9 +83,10 @@ describe('ec2InstanceNode', function () { assert.strictEqual(testNode.contextValue, Ec2InstancePendingContext) }) - it('updates label with new instance', async function () { - const newIdInstance = { ...testInstance, InstanceId: 'testId2' } + it('updates status with new instance', async function () { + const newStatus = 'pending' + const newIdInstance = { ...testInstance, InstanceId: 'testId2', status: newStatus } testNode.updateInstance(newIdInstance) - assert.strictEqual(testNode.label, `${getNameOfInstance(newIdInstance)} (${newIdInstance.InstanceId})`) + assert.strictEqual(testNode.getStatus(), newStatus) }) }) From 60e1d41d7d0dccc335a54ce533f4c27f9959c06b Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Thu, 10 Aug 2023 12:08:30 -0700 Subject: [PATCH 150/172] add telemetry for remote connection through vscode --- src/ec2/activation.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ec2/activation.ts b/src/ec2/activation.ts index 261bcbe425d..3d24aa68614 100644 --- a/src/ec2/activation.ts +++ b/src/ec2/activation.ts @@ -31,6 +31,10 @@ export async function activate(ctx: ExtContext): Promise { }), Commands.register('aws.ec2.openRemoteConnection', async (node?: Ec2Node) => { + await telemetry.ec2_connectToInstance.run(async span => { + span.record({ ec2ConnectionType: 'remoteDesktop' }) + await openRemoteConnection(node) + }) await openRemoteConnection(node) }), From f58867d5965b9c1339934748fa1c47d11dc41348 Mon Sep 17 00:00:00 2001 From: Harry Weinstock Date: Fri, 11 Aug 2023 13:57:10 -0700 Subject: [PATCH 151/172] add new type on ec2connect metric to track interface --- src/ec2/activation.ts | 4 +++- src/shared/telemetry/vscodeTelemetry.json | 13 ++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/ec2/activation.ts b/src/ec2/activation.ts index 3d24aa68614..66e2f1a16de 100644 --- a/src/ec2/activation.ts +++ b/src/ec2/activation.ts @@ -22,6 +22,7 @@ export async function activate(ctx: ExtContext): Promise { Commands.register('aws.ec2.openTerminal', async (node?: Ec2InstanceNode) => { await telemetry.ec2_connectToInstance.run(async span => { span.record({ ec2ConnectionType: 'ssm' }) + span.record({ ec2ConnectionInterface: 'terminal' }) await openTerminal(node) }) }), @@ -32,7 +33,8 @@ export async function activate(ctx: ExtContext): Promise { Commands.register('aws.ec2.openRemoteConnection', async (node?: Ec2Node) => { await telemetry.ec2_connectToInstance.run(async span => { - span.record({ ec2ConnectionType: 'remoteDesktop' }) + span.record({ ec2ConnectionType: 'ssm' }) + span.record({ ec2ConnectionInterface: 'remote-dev' }) await openRemoteConnection(node) }) await openRemoteConnection(node) diff --git a/src/shared/telemetry/vscodeTelemetry.json b/src/shared/telemetry/vscodeTelemetry.json index 15cf573a427..a9cdb07db43 100644 --- a/src/shared/telemetry/vscodeTelemetry.json +++ b/src/shared/telemetry/vscodeTelemetry.json @@ -4,7 +4,13 @@ "name": "ec2ConnectionType", "type": "string", "allowedValues": ["remoteDesktop", "ssh", "scp", "ssm"], - "description": "Ec2 Connection Methods" + "description": "EC2 Connection Methods" + }, + { + "name": "ec2ConnectionInterface", + "type": "string", + "allowedValues": ["terminal", "remote-dev"], + "description": "EC2 connection Interface" }, { "name": "documentFormat", @@ -235,6 +241,11 @@ { "name": "sam_openConfigUi", "description": "Called after opening the SAM Config UI" + }, + { + "name": "ec2_connectToInstance", + "description": "EC2 Connection Methods", + "metadata": [{ "type": "ec2ConnectionType" }, { "type": "ec2ConnectionInterface" }] } ] } From c9d6eff312f245634078e3cb0a16c35c7667994d Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 5 Sep 2024 14:05:53 -0400 Subject: [PATCH 152/172] update vscode.command usage --- .../ec2/explorer/ec2InstanceNode.ts | 2 +- packages/toolkit/package.json | 3168 ++++++++++++++++- 2 files changed, 3064 insertions(+), 106 deletions(-) diff --git a/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts b/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts index bc671d72d62..72b0b4bb305 100644 --- a/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts +++ b/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts @@ -96,6 +96,6 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode public async refreshNode(): Promise { await this.updateStatus() - Commands.vscode().execute('aws.refreshAwsExplorerNode', this) + vscode.commands.executeCommand('aws.refreshAwsExplorerNode', this) } } diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index f6ca970581b..8c0e23d0e15 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -51,7 +51,10 @@ ], "main": "./dist/src/extensionNode", "browser": "./dist/src/extensionWeb", - "engines": "This field will be autopopulated from the core module during debugging and packaging.", + "engines": { + "npm": "^10.1.0", + "vscode": "^1.68.0" + }, "scripts": { "vscode:prepublish": "npm run clean && npm run buildScripts && webpack --mode production", "buildScripts": "npm run generateNonCodeFiles && npm run copyFiles && npm run generateIcons && npm run generateSettings && npm run generateConfigurationAttributes && tsc -p ./ --noEmit", @@ -670,120 +673,3048 @@ ] } ], - "icons": { - "aws-amazonq-q-gradient": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1aa" - } - }, - "aws-amazonq-q-squid-ink": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1ab" - } - }, - "aws-amazonq-q-white": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1ac" - } - }, - "aws-amazonq-transform-arrow-dark": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1ad" - } - }, - "aws-amazonq-transform-arrow-light": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1ae" - } - }, - "aws-amazonq-transform-default-dark": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1af" - } - }, - "aws-amazonq-transform-default-light": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1b0" - } - }, - "aws-amazonq-transform-dependencies-dark": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1b1" + "viewsContainers": { + "activitybar": [ + { + "id": "aws-explorer", + "title": "%AWS.title%", + "icon": "resources/aws-logo.svg", + "cloud9": { + "cn": { + "title": "%AWS.title.cn%", + "icon": "resources/aws-cn-logo.svg" + } + } } - }, - "aws-amazonq-transform-dependencies-light": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1b2" + ] + }, + "views": { + "aws-explorer": [ + { + "id": "aws.amazonq.codewhisperer", + "name": "%AWS.amazonq.codewhisperer.title%", + "when": "!isCloud9 && !aws.isSageMaker && !aws.toolkit.amazonq.dismissed && !aws.explorer.showAuthView" + }, + { + "id": "aws.explorer", + "name": "%AWS.lambda.explorerTitle%", + "when": "(isCloud9 || !aws.isWebExtHost) && !aws.explorer.showAuthView" + }, + { + "id": "aws.cdk", + "name": "%AWS.cdk.explorerTitle%", + "when": "!aws.explorer.showAuthView" + }, + { + "id": "aws.codecatalyst", + "name": "%AWS.codecatalyst.explorerTitle%", + "when": "(!isCloud9 && !aws.isSageMaker || isCloud9CodeCatalyst) && !aws.explorer.showAuthView" + }, + { + "type": "webview", + "id": "aws.toolkit.AmazonCommonAuth", + "name": "%AWS.amazonq.login%", + "when": "!isCloud9 && !aws.isSageMaker && aws.explorer.showAuthView" } + ] + }, + "submenus": [ + { + "id": "aws.toolkit.auth", + "label": "%AWS.submenu.auth.title%", + "icon": "$(ellipsis)", + "when": "isCloud9 || !aws.isWebExtHost" }, - "aws-amazonq-transform-file-dark": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1b3" - } + { + "id": "aws.codecatalyst.submenu", + "label": "%AWS.codecatalyst.submenu.title%", + "icon": "$(ellipsis)", + "when": "isCloud9 || !aws.isWebExtHost" }, - "aws-amazonq-transform-file-light": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1b4" - } + { + "label": "%AWS.generic.feedback%", + "id": "aws.toolkit.submenu.feedback" }, - "aws-amazonq-transform-logo": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1b5" + { + "label": "%AWS.generic.help%", + "id": "aws.toolkit.submenu.help" + } + ], + "menus": { + "commandPalette": [ + { + "command": "aws.apig.copyUrl", + "when": "false" + }, + { + "command": "aws.apig.invokeRemoteRestApi", + "when": "false" + }, + { + "command": "aws.deleteCloudFormation", + "when": "false" + }, + { + "command": "aws.downloadStateMachineDefinition", + "when": "false" + }, + { + "command": "aws.ecr.createRepository", + "when": "false" + }, + { + "command": "aws.executeStateMachine", + "when": "false" + }, + { + "command": "aws.copyArn", + "when": "false" + }, + { + "command": "aws.copyName", + "when": "false" + }, + { + "command": "aws.listCommands", + "when": "false" + }, + { + "command": "aws.codecatalyst.listCommands", + "when": "false" + }, + { + "command": "aws.codecatalyst.manageConnections", + "when": "false" + }, + { + "command": "aws.codecatalyst.openDevEnv", + "when": "!isCloud9" + }, + { + "command": "aws.codecatalyst.createDevEnv", + "when": "!isCloud9" + }, + { + "command": "aws.downloadSchemaItemCode", + "when": "false" + }, + { + "command": "aws.deleteLambda", + "when": "false" + }, + { + "command": "aws.downloadLambda", + "when": "false" + }, + { + "command": "aws.invokeLambda", + "when": "false" + }, + { + "command": "aws.copyLambdaUrl", + "when": "false" + }, + { + "command": "aws.viewSchemaItem", + "when": "false" + }, + { + "command": "aws.searchSchema", + "when": "false" + }, + { + "command": "aws.searchSchemaPerRegistry", + "when": "false" + }, + { + "command": "aws.refreshAwsExplorer", + "when": "false" + }, + { + "command": "aws.cdk.refresh", + "when": "false" + }, + { + "command": "aws.cdk.viewDocs", + "when": "false" + }, + { + "command": "aws.ssmDocument.openLocalDocument", + "when": "false" + }, + { + "command": "aws.ssmDocument.openLocalDocumentJson", + "when": "false" + }, + { + "command": "aws.ssmDocument.openLocalDocumentYaml", + "when": "false" + }, + { + "command": "aws.ssmDocument.deleteDocument", + "when": "false" + }, + { + "command": "aws.ssmDocument.updateDocumentVersion", + "when": "false" + }, + { + "command": "aws.copyLogResource", + "when": "resourceScheme == aws-cwl" + }, + { + "command": "aws.saveCurrentLogDataContent", + "when": "resourceScheme == aws-cwl" + }, + { + "command": "aws.s3.editFile", + "when": "resourceScheme == s3-readonly" + }, + { + "command": "aws.cwl.viewLogStream", + "when": "false" + }, + { + "command": "aws.cwl.changeFilterPattern", + "when": "false" + }, + { + "command": "aws.cwl.changeTimeFilter", + "when": "false" + }, + { + "command": "aws.ecr.deleteRepository", + "when": "false" + }, + { + "command": "aws.ecr.copyTagUri", + "when": "false" + }, + { + "command": "aws.ecr.copyRepositoryUri", + "when": "false" + }, + { + "command": "aws.ecr.deleteTag", + "when": "false" + }, + { + "command": "aws.iot.createThing", + "when": "false" + }, + { + "command": "aws.iot.deleteThing", + "when": "false" + }, + { + "command": "aws.iot.createCert", + "when": "false" + }, + { + "command": "aws.iot.deleteCert", + "when": "false" + }, + { + "command": "aws.iot.attachCert", + "when": "false" + }, + { + "command": "aws.iot.attachPolicy", + "when": "false" + }, + { + "command": "aws.iot.activateCert", + "when": "false" + }, + { + "command": "aws.iot.deactivateCert", + "when": "false" + }, + { + "command": "aws.iot.revokeCert", + "when": "false" + }, + { + "command": "aws.iot.createPolicy", + "when": "false" + }, + { + "command": "aws.iot.deletePolicy", + "when": "false" + }, + { + "command": "aws.iot.createPolicyVersion", + "when": "false" + }, + { + "command": "aws.iot.deletePolicyVersion", + "when": "false" + }, + { + "command": "aws.iot.detachCert", + "when": "false" + }, + { + "command": "aws.iot.detachPolicy", + "when": "false" + }, + { + "command": "aws.iot.viewPolicyVersion", + "when": "false" + }, + { + "command": "aws.iot.setDefaultPolicy", + "when": "false" + }, + { + "command": "aws.iot.copyEndpoint", + "when": "false" + }, + { + "command": "aws.deploySamApplication", + "when": "config.aws.samcli.legacyDeploy" + }, + { + "command": "aws.redshift.editConnection", + "when": "false" + }, + { + "command": "aws.redshift.deleteConnection", + "when": "false" + }, + { + "command": "aws.samcli.sync", + "when": "!config.aws.samcli.legacyDeploy" + }, + { + "command": "aws.s3.copyPath", + "when": "false" + }, + { + "command": "aws.s3.createBucket", + "when": "false" + }, + { + "command": "aws.s3.createFolder", + "when": "false" + }, + { + "command": "aws.s3.deleteBucket", + "when": "false" + }, + { + "command": "aws.s3.deleteFile", + "when": "false" + }, + { + "command": "aws.s3.downloadFileAs", + "when": "false" + }, + { + "command": "aws.s3.openFile", + "when": "false" + }, + { + "command": "aws.s3.editFile", + "when": "false" + }, + { + "command": "aws.s3.uploadFileToParent", + "when": "false" + }, + { + "command": "aws.apprunner.startDeployment", + "when": "false" + }, + { + "command": "aws.apprunner.createService", + "when": "false" + }, + { + "command": "aws.apprunner.pauseService", + "when": "false" + }, + { + "command": "aws.apprunner.resumeService", + "when": "false" + }, + { + "command": "aws.apprunner.copyServiceUrl", + "when": "false" + }, + { + "command": "aws.apprunner.open", + "when": "false" + }, + { + "command": "aws.apprunner.deleteService", + "when": "false" + }, + { + "command": "aws.apprunner.createServiceFromEcr", + "when": "false" + }, + { + "command": "aws.resources.copyIdentifier", + "when": "false" + }, + { + "command": "aws.resources.openResourcePreview", + "when": "false" + }, + { + "command": "aws.resources.createResource", + "when": "false" + }, + { + "command": "aws.resources.deleteResource", + "when": "false" + }, + { + "command": "aws.resources.updateResource", + "when": "false" + }, + { + "command": "aws.resources.updateResourceInline", + "when": "false" + }, + { + "command": "aws.resources.saveResource", + "when": "false" + }, + { + "command": "aws.resources.closeResource", + "when": "false" + }, + { + "command": "aws.resources.viewDocs", + "when": "false" + }, + { + "command": "aws.ecs.runCommandInContainer", + "when": "false" + }, + { + "command": "aws.ecs.openTaskInTerminal", + "when": "false" + }, + { + "command": "aws.ecs.enableEcsExec", + "when": "false" + }, + { + "command": "aws.ecs.disableEcsExec", + "when": "false" + }, + { + "command": "aws.ecs.viewDocumentation", + "when": "false" + }, + { + "command": "aws.renderStateMachineGraph", + "when": "false" + }, + { + "command": "aws.toolkit.auth.addConnection", + "when": "false" + }, + { + "command": "aws.toolkit.auth.switchConnections", + "when": "false" + }, + { + "command": "aws.toolkit.auth.help", + "when": "false" + }, + { + "command": "aws.toolkit.auth.manageConnections" + }, + { + "command": "aws.ec2.openRemoteConnection", + "when": "aws.isDevMode" + }, + { + "command": "aws.ec2.openTerminal", + "when": "aws.isDevMode" + }, + { + "command": "aws.ec2.startInstance", + "when": "aws.isDevMode" + }, + { + "command": "aws.ec2.stopInstance", + "when": "aws.isDevMode" + }, + { + "command": "aws.ec2.rebootInstance", + "when": "aws.isDevMode" + }, + { + "command": "aws.dev.openMenu", + "when": "aws.isDevMode || isCloud9" + }, + { + "command": "aws.openInApplicationComposer", + "when": "false" + }, + { + "command": "aws.toolkit.amazonq.learnMore", + "when": "false" + }, + { + "command": "aws.toolkit.amazonq.extensionpage", + "when": "false" + }, + { + "command": "aws.newThreatComposerFile", + "when": "false" } - }, - "aws-amazonq-transform-step-into-dark": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1b6" + ], + "editor/title": [ + { + "command": "aws.previewStateMachine", + "when": "editorLangId == asl || editorLangId == asl-yaml", + "group": "navigation" + }, + { + "command": "aws.saveCurrentLogDataContent", + "when": "resourceScheme == aws-cwl", + "group": "navigation" + }, + { + "command": "aws.cwl.changeFilterPattern", + "when": "resourceScheme == aws-cwl", + "group": "navigation" + }, + { + "command": "aws.cwl.changeTimeFilter", + "when": "resourceScheme == aws-cwl", + "group": "navigation" + }, + { + "command": "aws.s3.editFile", + "when": "resourceScheme == s3-readonly", + "group": "navigation" + }, + { + "command": "aws.ssmDocument.publishDocument", + "when": "editorLangId =~ /^(ssm-yaml|ssm-json)$/", + "group": "navigation" + }, + { + "command": "aws.resources.updateResourceInline", + "when": "resourceScheme == awsResource && !isCloud9 && config.aws.experiments.jsonResourceModification", + "group": "navigation" + }, + { + "command": "aws.resources.closeResource", + "when": "resourcePath =~ /^.+(awsResource.json)$/", + "group": "navigation" + }, + { + "command": "aws.resources.saveResource", + "when": "resourcePath =~ /^.+(awsResource.json)$/", + "group": "navigation" + }, + { + "command": "aws.openInApplicationComposer", + "when": "(editorLangId == json && !(resourceFilename =~ /^.*\\.tc\\.json$/)) || editorLangId == yaml || resourceFilename =~ /^.*\\.(template)$/", + "group": "navigation" } - }, - "aws-amazonq-transform-step-into-light": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1b7" + ], + "editor/title/context": [ + { + "command": "aws.copyLogResource", + "when": "resourceScheme == aws-cwl", + "group": "1_cutcopypaste@1" } - }, - "aws-amazonq-transform-variables-dark": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1b8" + ], + "view/title": [ + { + "command": "aws.toolkit.submitFeedback", + "when": "view == aws.explorer && !aws.isWebExtHost", + "group": "navigation@6" + }, + { + "command": "aws.refreshAwsExplorer", + "when": "view == aws.explorer", + "group": "navigation@5" + }, + { + "command": "aws.cdk.refresh", + "when": "view == aws.cdk", + "group": "navigation@1" + }, + { + "command": "aws.toolkit.login", + "when": "view == aws.explorer", + "group": "1_account@1" + }, + { + "command": "aws.showRegion", + "when": "view == aws.explorer", + "group": "1_account@2" + }, + { + "command": "aws.listCommands", + "when": "view == aws.explorer && !isCloud9", + "group": "1_account@3" + }, + { + "command": "aws.lambda.createNewSamApp", + "when": "view == aws.explorer", + "group": "3_lambda@1" + }, + { + "command": "aws.launchConfigForm", + "when": "view == aws.explorer", + "group": "3_lambda@2" + }, + { + "command": "aws.deploySamApplication", + "when": "config.aws.samcli.legacyDeploy && view == aws.explorer", + "group": "3_lambda@3" + }, + { + "command": "aws.samcli.sync", + "when": "!config.aws.samcli.legacyDeploy && view == aws.explorer", + "group": "3_lambda@3" + }, + { + "submenu": "aws.toolkit.submenu.feedback", + "when": "view =~ /^aws\\./ && view != aws.AmazonQChatView && view != aws.amazonq.AmazonCommonAuth", + "group": "y_toolkitMeta@1" + }, + { + "submenu": "aws.toolkit.submenu.help", + "when": "view =~ /^aws\\./ && view != aws.AmazonQChatView && view != aws.amazonq.AmazonCommonAuth", + "group": "y_toolkitMeta@2" + }, + { + "command": "aws.codecatalyst.cloneRepo", + "when": "view == aws.codecatalyst && !isCloud9 && aws.codecatalyst.connected", + "group": "1_codeCatalyst@1" + }, + { + "command": "aws.codecatalyst.createDevEnv", + "when": "view == aws.codecatalyst && !isCloud9 && aws.codecatalyst.connected", + "group": "1_codeCatalyst@1" + }, + { + "command": "aws.codecatalyst.listCommands", + "when": "view == aws.codecatalyst && !isCloud9 && aws.codecatalyst.connected", + "group": "1_codeCatalyst@1" + }, + { + "command": "aws.codecatalyst.openDevEnv", + "when": "view == aws.codecatalyst && !isCloud9 && aws.codecatalyst.connected", + "group": "1_codeCatalyst@1" + }, + { + "command": "aws.codecatalyst.manageConnections", + "when": "view == aws.codecatalyst && !isCloud9 && !aws.codecatalyst.connected", + "group": "2_codeCatalyst@1" + }, + { + "command": "aws.codecatalyst.signout", + "when": "view == aws.codecatalyst && !isCloud9 && aws.codecatalyst.connected", + "group": "2_codeCatalyst@1" } - }, - "aws-amazonq-transform-variables-light": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1b9" + ], + "explorer/context": [ + { + "command": "aws.deploySamApplication", + "when": "config.aws.samcli.legacyDeploy && isFileSystemResource && resourceFilename =~ /^template\\.(json|yml|yaml)$/", + "group": "z_aws@1" + }, + { + "command": "aws.samcli.sync", + "when": "!config.aws.samcli.legacyDeploy && isFileSystemResource && resourceFilename =~ /^(template\\.(json|yml|yaml))|(samconfig\\.toml)$/", + "group": "z_aws@1" + }, + { + "command": "aws.uploadLambda", + "when": "explorerResourceIsFolder || isFileSystemResource && resourceFilename =~ /^template\\.(json|yml|yaml)$/", + "group": "z_aws@3" + }, + { + "command": "aws.openInApplicationComposer", + "when": "isFileSystemResource && !(resourceFilename =~ /^.*\\.tc\\.json$/) && resourceFilename =~ /^.*\\.(json|yml|yaml|template)$/", + "group": "z_aws@1" } - }, - "aws-applicationcomposer-icon": { + ], + "view/item/context": [ + { + "command": "aws.apig.invokeRemoteRestApi", + "when": "view == aws.explorer && viewItem =~ /^(awsApiGatewayNode)$/", + "group": "0@1" + }, + { + "command": "aws.ec2.openTerminal", + "group": "0@1", + "when": "viewItem =~ /^(awsEc2(Parent|Running)Node)$/" + }, + { + "command": "aws.ec2.openTerminal", + "group": "inline@1", + "when": "viewItem =~ /^(awsEc2(Parent|Running)Node)$/" + }, + { + "command": "aws.ec2.openRemoteConnection", + "group": "0@1", + "when": "viewItem =~ /^(awsEc2(Parent|Running)Node)$/" + }, + { + "command": "aws.ec2.openRemoteConnection", + "group": "inline@1", + "when": "viewItem =~ /^(awsEc2(Parent|Running)Node)$/" + }, + { + "command": "aws.ec2.startInstance", + "group": "0@1", + "when": "viewItem == awsEc2StoppedNode" + }, + { + "command": "aws.ec2.startInstance", + "group": "inline@1", + "when": "viewItem == awsEc2StoppedNode" + }, + { + "command": "aws.ec2.stopInstance", + "group": "0@1", + "when": "viewItem == awsEc2RunningNode" + }, + { + "command": "aws.ec2.stopInstance", + "group": "inline@1", + "when": "viewItem == awsEc2RunningNode" + }, + { + "command": "aws.ec2.rebootInstance", + "group": "0@1", + "when": "viewItem == awsEc2RunningNode" + }, + { + "command": "aws.ec2.rebootInstance", + "group": "inline@1", + "when": "viewItem == awsEc2RunningNode" + }, + { + "command": "aws.ecr.createRepository", + "when": "view == aws.explorer && viewItem == awsEcrNode", + "group": "inline@1" + }, + { + "command": "aws.iot.createThing", + "when": "view == aws.explorer && viewItem == awsIotThingsNode", + "group": "inline@1" + }, + { + "command": "aws.iot.createCert", + "when": "view == aws.explorer && viewItem == awsIotCertsNode", + "group": "inline@1" + }, + { + "command": "aws.iot.createPolicy", + "when": "view == aws.explorer && viewItem == awsIotPoliciesNode", + "group": "inline@1" + }, + { + "command": "aws.iot.attachCert", + "when": "view == aws.explorer && viewItem == awsIotThingNode", + "group": "inline@1" + }, + { + "command": "aws.iot.attachPolicy", + "when": "view == aws.explorer && viewItem =~ /^awsIotCertificateNode.(Things|Policies)/", + "group": "inline@1" + }, + { + "command": "aws.redshift.editConnection", + "when": "view == aws.explorer && viewItem == awsRedshiftWarehouseNode", + "group": "0@1" + }, + { + "command": "aws.redshift.deleteConnection", + "when": "view == aws.explorer && viewItem == awsRedshiftWarehouseNode", + "group": "0@2" + }, + { + "command": "aws.s3.openFile", + "when": "view == aws.explorer && viewItem == awsS3FileNode && !isCloud9", + "group": "0@1" + }, + { + "command": "aws.s3.editFile", + "when": "view == aws.explorer && viewItem == awsS3FileNode && !isCloud9", + "group": "inline@1" + }, + { + "command": "aws.s3.downloadFileAs", + "when": "view == aws.explorer && viewItem == awsS3FileNode", + "group": "inline@2" + }, + { + "command": "aws.s3.createBucket", + "when": "view == aws.explorer && viewItem == awsS3Node", + "group": "inline@1" + }, + { + "command": "aws.s3.createFolder", + "when": "view == aws.explorer && viewItem =~ /^(awsS3BucketNode|awsS3FolderNode)$/", + "group": "inline@1" + }, + { + "command": "aws.ssmDocument.openLocalDocument", + "when": "view == aws.explorer && viewItem =~ /^(awsDocumentItemNode|awsDocumentItemNodeWriteable)$/", + "group": "inline@1" + }, + { + "command": "aws.s3.uploadFile", + "when": "view == aws.explorer && viewItem =~ /^(awsS3BucketNode|awsS3FolderNode)$/", + "group": "inline@2" + }, + { + "command": "aws.showRegion", + "when": "view == aws.explorer && viewItem == awsRegionNode", + "group": "0@1" + }, + { + "command": "aws.lambda.createNewSamApp", + "when": "view == aws.explorer && viewItem == awsLambdaNode || viewItem == awsRegionNode", + "group": "1@1" + }, + { + "command": "aws.launchConfigForm", + "when": "view == aws.explorer && viewItem == awsLambdaNode || viewItem == awsRegionNode || viewItem == awsCloudFormationRootNode", + "group": "1@1" + }, + { + "command": "aws.deploySamApplication", + "when": "config.aws.samcli.legacyDeploy && view == aws.explorer && viewItem =~ /^(awsLambdaNode|awsRegionNode|awsCloudFormationRootNode)$/", + "group": "1@2" + }, + { + "command": "aws.samcli.sync", + "when": "!config.aws.samcli.legacyDeploy && view == aws.explorer && viewItem =~ /^(awsLambdaNode|awsRegionNode|awsCloudFormationRootNode)$/", + "group": "1@2" + }, + { + "command": "aws.ec2.copyInstanceId", + "when": "view == aws.explorer && viewItem =~ /^(awsEc2(Parent|Running|Stopped)Node)$/", + "group": "2@0" + }, + { + "command": "aws.ecr.copyTagUri", + "when": "view == aws.explorer && viewItem == awsEcrTagNode", + "group": "2@1" + }, + { + "command": "aws.ecr.deleteTag", + "when": "view == aws.explorer && viewItem == awsEcrTagNode", + "group": "3@1" + }, + { + "command": "aws.ecr.copyRepositoryUri", + "when": "view == aws.explorer && viewItem == awsEcrRepositoryNode", + "group": "2@1" + }, + { + "command": "aws.ecr.createRepository", + "when": "view == aws.explorer && viewItem == awsEcrNode", + "group": "0@1" + }, + { + "command": "aws.ecr.deleteRepository", + "when": "view == aws.explorer && viewItem == awsEcrRepositoryNode", + "group": "3@1" + }, + { + "command": "aws.invokeLambda", + "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode)$/", + "group": "0@1" + }, + { + "command": "aws.downloadLambda", + "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable)$/", + "group": "0@2" + }, + { + "command": "aws.uploadLambda", + "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable)$/", + "group": "1@1" + }, + { + "command": "aws.deleteLambda", + "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable)$/", + "group": "4@1" + }, + { + "command": "aws.copyLambdaUrl", + "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable)$/", + "group": "2@0" + }, + { + "command": "aws.deleteCloudFormation", + "when": "view == aws.explorer && viewItem == awsCloudFormationNode", + "group": "3@5" + }, + { + "command": "aws.searchSchema", + "when": "view == aws.explorer && viewItem == awsSchemasNode", + "group": "0@1" + }, + { + "command": "aws.searchSchemaPerRegistry", + "when": "view == aws.explorer && viewItem == awsRegistryItemNode", + "group": "0@1" + }, + { + "command": "aws.viewSchemaItem", + "when": "view == aws.explorer && viewItem == awsSchemaItemNode", + "group": "0@1" + }, + { + "command": "aws.stepfunctions.createStateMachineFromTemplate", + "when": "view == aws.explorer && viewItem == awsStepFunctionsNode", + "group": "0@1" + }, + { + "command": "aws.downloadStateMachineDefinition", + "when": "view == aws.explorer && viewItem == awsStateMachineNode", + "group": "0@1" + }, + { + "command": "aws.renderStateMachineGraph", + "when": "view == aws.explorer && viewItem == awsStateMachineNode", + "group": "0@2" + }, + { + "command": "aws.cdk.renderStateMachineGraph", + "when": "viewItem == awsCdkStateMachineNode", + "group": "inline@1" + }, + { + "command": "aws.cdk.renderStateMachineGraph", + "when": "viewItem == awsCdkStateMachineNode", + "group": "0@1" + }, + { + "command": "aws.executeStateMachine", + "when": "view == aws.explorer && viewItem == awsStateMachineNode", + "group": "0@3" + }, + { + "command": "aws.iot.createThing", + "when": "view == aws.explorer && viewItem == awsIotThingsNode", + "group": "0@1" + }, + { + "command": "aws.iot.createCert", + "when": "view == aws.explorer && viewItem == awsIotCertsNode", + "group": "0@1" + }, + { + "command": "aws.iot.createPolicy", + "when": "view == aws.explorer && viewItem == awsIotPoliciesNode", + "group": "0@1" + }, + { + "command": "aws.iot.createPolicyVersion", + "when": "view == aws.explorer && viewItem == awsIotPolicyNode.WithVersions", + "group": "0@1" + }, + { + "command": "aws.iot.viewPolicyVersion", + "when": "view == aws.explorer && viewItem =~ /^awsIotPolicyVersionNode./", + "group": "0@1" + }, + { + "command": "aws.iot.attachCert", + "when": "view == aws.explorer && viewItem == awsIotThingNode", + "group": "0@1" + }, + { + "command": "aws.iot.attachPolicy", + "when": "view == aws.explorer && viewItem =~ /^awsIotCertificateNode.(Things|Policies)/", + "group": "0@1" + }, + { + "command": "aws.s3.createBucket", + "when": "view == aws.explorer && viewItem == awsS3Node", + "group": "0@1" + }, + { + "command": "aws.s3.downloadFileAs", + "when": "view == aws.explorer && viewItem == awsS3FileNode", + "group": "0@1" + }, + { + "command": "aws.s3.uploadFile", + "when": "view == aws.explorer && viewItem =~ /^(awsS3BucketNode|awsS3FolderNode)$/", + "group": "0@1" + }, + { + "command": "aws.s3.uploadFileToParent", + "when": "view == aws.explorer && viewItem == awsS3FileNode", + "group": "1@1" + }, + { + "command": "aws.s3.createFolder", + "when": "view == aws.explorer && viewItem =~ /^(awsS3BucketNode|awsS3FolderNode)$/", + "group": "1@1" + }, + { + "command": "aws.iot.deactivateCert", + "when": "view == aws.explorer && viewItem =~ /^awsIotCertificateNode.(Things|Policies).ACTIVE$/", + "group": "1@1" + }, + { + "command": "aws.iot.activateCert", + "when": "view == aws.explorer && viewItem =~ /^awsIotCertificateNode.(Things|Policies).INACTIVE$/", + "group": "1@1" + }, + { + "command": "aws.iot.revokeCert", + "when": "view == aws.explorer && viewItem =~ /^awsIotCertificateNode.(Things|Policies).(ACTIVE|INACTIVE)$/", + "group": "1@2" + }, + { + "command": "aws.iot.setDefaultPolicy", + "when": "view == aws.explorer && viewItem == awsIotPolicyVersionNode.NONDEFAULT", + "group": "1@1" + }, + { + "command": "aws.iot.copyEndpoint", + "when": "view == aws.explorer && viewItem == awsIotNode", + "group": "2@1" + }, + { + "command": "aws.copyName", + "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode|awsStateMachineNode|awsCloudFormationNode|awsS3BucketNode|awsS3FolderNode|awsS3FileNode|awsApiGatewayNode|awsIotThingNode)$|^(awsAppRunnerServiceNode|awsIotCertificateNode|awsIotPolicyNode|awsIotPolicyVersionNode|(awsEc2(Running|Pending|Stopped)Node))/", + "group": "2@1" + }, + { + "command": "aws.copyArn", + "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode|awsStateMachineNode|awsCloudFormationNode|awsCloudWatchLogNode|awsS3BucketNode|awsS3FolderNode|awsS3FileNode|awsApiGatewayNode|awsEcrRepositoryNode|awsIotThingNode)$|^(awsAppRunnerServiceNode|awsEcsServiceNode|awsIotCertificateNode|awsIotPolicyNode|awsIotPolicyVersionNode|awsMdeInstanceNode|(awsEc2(Running|Pending|Stopped)Node))/", + "group": "2@2" + }, + { + "command": "aws.cwl.searchLogGroup", + "group": "0@1", + "when": "view == aws.explorer && viewItem =~ /^awsCloudWatchLogNode|awsCloudWatchLogParentNode$/" + }, + { + "command": "aws.cwl.searchLogGroup", + "group": "inline@1", + "when": "view == aws.explorer && viewItem =~ /^awsCloudWatchLogNode|awsCloudWatchLogParentNode$/" + }, + { + "command": "aws.apig.copyUrl", + "when": "view == aws.explorer && viewItem =~ /^(awsApiGatewayNode)$/", + "group": "2@0" + }, + { + "command": "aws.s3.copyPath", + "when": "view == aws.explorer && viewItem =~ /^(awsS3FolderNode|awsS3FileNode)$/", + "group": "2@3" + }, + { + "command": "aws.s3.presignedURL", + "when": "view == aws.explorer && viewItem =~ /^(awsS3FileNode)$/", + "group": "2@4" + }, + { + "command": "aws.iot.detachCert", + "when": "view == aws.explorer && viewItem =~ /^(awsIotCertificateNode.Things)/", + "group": "3@1" + }, + { + "command": "aws.iot.detachPolicy", + "when": "view == aws.explorer && viewItem == awsIotPolicyNode.Certificates", + "group": "3@1" + }, + { + "command": "aws.iot.deleteThing", + "when": "view == aws.explorer && viewItem == awsIotThingNode", + "group": "3@1" + }, + { + "command": "aws.iot.deleteCert", + "when": "view == aws.explorer && viewItem =~ /^awsIotCertificateNode.Policies/", + "group": "3@1" + }, + { + "command": "aws.iot.deletePolicy", + "when": "view == aws.explorer && viewItem == awsIotPolicyNode.WithVersions", + "group": "3@1" + }, + { + "command": "aws.iot.deletePolicyVersion", + "when": "view == aws.explorer && viewItem == awsIotPolicyVersionNode.NONDEFAULT", + "group": "3@1" + }, + { + "command": "aws.s3.deleteBucket", + "when": "view == aws.explorer && viewItem == awsS3BucketNode", + "group": "3@1" + }, + { + "command": "aws.s3.deleteFile", + "when": "view == aws.explorer && viewItem == awsS3FileNode", + "group": "3@1" + }, + { + "command": "aws.downloadSchemaItemCode", + "when": "view == aws.explorer && viewItem == awsSchemaItemNode", + "group": "1@1" + }, + { + "command": "aws.cwl.viewLogStream", + "group": "0@1", + "when": "view == aws.explorer && viewItem == awsCloudWatchLogNode" + }, + { + "command": "aws.ssmDocument.openLocalDocumentYaml", + "group": "0@1", + "when": "view == aws.explorer && viewItem =~ /^(awsDocumentItemNode|awsDocumentItemNodeWriteable)$/" + }, + { + "command": "aws.ssmDocument.openLocalDocumentJson", + "group": "0@2", + "when": "view == aws.explorer && viewItem =~ /^(awsDocumentItemNode|awsDocumentItemNodeWriteable)$/" + }, + { + "command": "aws.ssmDocument.updateDocumentVersion", + "group": "2@1", + "when": "view == aws.explorer && viewItem == awsDocumentItemNodeWriteable" + }, + { + "command": "aws.ssmDocument.deleteDocument", + "group": "3@2", + "when": "view == aws.explorer && viewItem == awsDocumentItemNodeWriteable" + }, + { + "command": "aws.ecs.runCommandInContainer", + "group": "0@1", + "when": "view == aws.explorer && viewItem =~ /^(awsEcsContainerNodeExec)(.*)$/" + }, + { + "command": "aws.ecs.openTaskInTerminal", + "group": "0@2", + "when": "view == aws.explorer && viewItem =~ /^(awsEcsContainerNodeExec)(.*)$/ && !isCloud9" + }, + { + "command": "aws.ecs.enableEcsExec", + "group": "0@2", + "when": "view == aws.explorer && viewItem == awsEcsServiceNode.DISABLED" + }, + { + "command": "aws.ecs.disableEcsExec", + "group": "0@2", + "when": "view == aws.explorer && viewItem == awsEcsServiceNode.ENABLED" + }, + { + "command": "aws.ecs.viewDocumentation", + "group": "1@3", + "when": "view == aws.explorer && viewItem =~ /^(awsEcsClusterNode|awsEcsContainerNode)$|^awsEcsServiceNode/" + }, + { + "command": "aws.resources.configure", + "when": "view == aws.explorer && viewItem == resourcesRootNode", + "group": "1@1" + }, + { + "command": "aws.resources.configure", + "when": "view == aws.explorer && viewItem == resourcesRootNode", + "group": "inline@1" + }, + { + "command": "aws.resources.openResourcePreview", + "when": "view == aws.explorer && viewItem =~ /^(.*)(ResourceNode)$/", + "group": "1@1" + }, + { + "command": "aws.resources.copyIdentifier", + "when": "view == aws.explorer && viewItem =~ /^(.*)(ResourceNode)$/", + "group": "1@1" + }, + { + "command": "aws.resources.viewDocs", + "when": "view == aws.explorer && viewItem =~ /^(.*)(Documented)(.*)(ResourceTypeNode)$/", + "group": "1@1" + }, + { + "command": "aws.resources.createResource", + "when": "view == aws.explorer && viewItem =~ /^(.*)(Creatable)(.*)(ResourceTypeNode)$/ && !isCloud9 && config.aws.experiments.jsonResourceModification", + "group": "2@1" + }, + { + "command": "aws.resources.createResource", + "when": "view == aws.explorer && viewItem =~ /^(.*)(Creatable)(.*)(ResourceTypeNode)$/ && !isCloud9 && config.aws.experiments.jsonResourceModification", + "group": "inline@1" + }, + { + "command": "aws.resources.updateResource", + "when": "view == aws.explorer && viewItem =~ /^(.*)(Updatable)(.*)(ResourceNode)$/ && !isCloud9 && config.aws.experiments.jsonResourceModification", + "group": "2@1" + }, + { + "command": "aws.resources.deleteResource", + "when": "view == aws.explorer && viewItem =~ /^(.*)(Deletable)(.*)(ResourceNode)$/ && !isCloud9 && config.aws.experiments.jsonResourceModification", + "group": "2@2" + }, + { + "command": "aws.apprunner.createServiceFromEcr", + "group": "0@2", + "when": "view == aws.explorer && viewItem =~ /awsEcrTagNode|awsEcrRepositoryNode/" + }, + { + "command": "aws.apprunner.startDeployment", + "group": "0@1", + "when": "view == aws.explorer && viewItem == awsAppRunnerServiceNode.RUNNING" + }, + { + "command": "aws.apprunner.createService", + "group": "0@2", + "when": "view == aws.explorer && viewItem == awsAppRunnerNode" + }, + { + "command": "aws.apprunner.pauseService", + "group": "0@3", + "when": "view == aws.explorer && viewItem == awsAppRunnerServiceNode.RUNNING" + }, + { + "command": "aws.apprunner.resumeService", + "group": "0@3", + "when": "view == aws.explorer && viewItem == awsAppRunnerServiceNode.PAUSED" + }, + { + "command": "aws.apprunner.copyServiceUrl", + "group": "1@1", + "when": "view == aws.explorer && viewItem == awsAppRunnerServiceNode.RUNNING" + }, + { + "command": "aws.apprunner.open", + "group": "1@2", + "when": "view == aws.explorer && viewItem == awsAppRunnerServiceNode.RUNNING" + }, + { + "command": "aws.apprunner.deleteService", + "group": "3@1", + "when": "view == aws.explorer && viewItem =~ /awsAppRunnerServiceNode.[RUNNING|PAUSED|CREATE_FAILED]/" + }, + { + "command": "aws.cloudFormation.newTemplate", + "group": "0@1", + "when": "view == aws.explorer && viewItem == awsCloudFormationRootNode" + }, + { + "command": "aws.sam.newTemplate", + "group": "0@2", + "when": "view == aws.explorer && viewItem == awsCloudFormationRootNode" + }, + { + "command": "aws.cdk.viewDocs", + "when": "viewItem == awsCdkRootNode", + "group": "0@2" + }, + { + "command": "aws.toolkit.auth.addConnection", + "when": "viewItem == awsAuthNode", + "group": "0@1" + }, + { + "command": "aws.toolkit.auth.switchConnections", + "when": "viewItem == awsAuthNode", + "group": "0@2" + }, + { + "command": "aws.toolkit.auth.signout", + "when": "viewItem == awsAuthNode && !isCloud9", + "group": "0@3" + }, + { + "command": "aws.toolkit.auth.help", + "when": "viewItem == awsAuthNode", + "group": "inline@1" + }, + { + "submenu": "aws.toolkit.auth", + "when": "viewItem == awsAuthNode", + "group": "inline@2" + }, + { + "submenu": "aws.codecatalyst.submenu", + "when": "viewItem =~ /^awsCodeCatalystNode/", + "group": "inline@1" + }, + { + "command": "aws.codecatalyst.manageConnections", + "when": "viewItem =~ /^awsCodeCatalystNode/", + "group": "0@1" + }, + { + "command": "aws.codecatalyst.signout", + "when": "viewItem =~ /^awsCodeCatalystNode/&& !isCloud9 && aws.codecatalyst.connected", + "group": "0@2" + } + ], + "aws.toolkit.auth": [ + { + "command": "aws.toolkit.auth.manageConnections", + "group": "0@1" + }, + { + "command": "aws.toolkit.auth.switchConnections", + "group": "0@2" + }, + { + "command": "aws.toolkit.auth.signout", + "enablement": "!isCloud9", + "group": "0@3" + } + ], + "aws.toolkit.submenu.feedback": [ + { + "command": "aws.toolkit.submitFeedback", + "when": "!aws.isWebExtHost", + "group": "1_feedback@1" + }, + { + "command": "aws.toolkit.createIssueOnGitHub", + "group": "1_feedback@2" + } + ], + "aws.toolkit.submenu.help": [ + { + "command": "aws.quickStart", + "when": "isCloud9", + "group": "1_help@1" + }, + { + "command": "aws.toolkit.help", + "group": "1_help@2" + }, + { + "command": "aws.toolkit.github", + "group": "1_help@3" + }, + { + "command": "aws.toolkit.aboutExtension", + "group": "1_help@4" + }, + { + "command": "aws.toolkit.viewLogs", + "group": "1_help@5" + } + ], + "file/newFile": [ + { + "command": "aws.newThreatComposerFile" + } + ] + }, + "commands": [ + { + "command": "aws.accessanalyzer.iamPolicyChecks", + "title": "%AWS.command.accessanalyzer.iamPolicyChecks%", + "category": "%AWS.title%" + }, + { + "command": "aws.launchConfigForm", + "title": "%AWS.command.launchConfigForm.title%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.apig.copyUrl", + "title": "%AWS.command.apig.copyUrl%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.apig.invokeRemoteRestApi", + "title": "%AWS.command.apig.invokeRemoteRestApi%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%", + "title": "%AWS.command.apig.invokeRemoteRestApi.cn%" + } + } + }, + { + "command": "aws.lambda.createNewSamApp", + "title": "%AWS.command.createNewSamApp%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.toolkit.login", + "title": "%AWS.command.login%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "title": "%AWS.command.login.cn%", + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.toolkit.credentials.profile.create", + "title": "%AWS.command.credentials.profile.create%", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.toolkit.credentials.edit", + "title": "%AWS.command.credentials.edit%", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.codecatalyst.openOrg", + "title": "%AWS.command.codecatalyst.openOrg%", + "category": "AWS", + "enablement": "isCloud9 || !aws.isWebExtHost" + }, + { + "command": "aws.codecatalyst.openProject", + "title": "%AWS.command.codecatalyst.openProject%", + "category": "AWS", + "enablement": "isCloud9 || !aws.isWebExtHost" + }, + { + "command": "aws.codecatalyst.openRepo", + "title": "%AWS.command.codecatalyst.openRepo%", + "category": "AWS", + "enablement": "isCloud9 || !aws.isWebExtHost" + }, + { + "command": "aws.codecatalyst.openDevEnv", + "title": "%AWS.command.codecatalyst.openDevEnv%", + "category": "AWS", + "enablement": "!isCloud9 && !aws.isWebExtHost" + }, + { + "command": "aws.codecatalyst.listCommands", + "title": "%AWS.command.codecatalyst.listCommands%", + "category": "AWS", + "enablement": "!isCloud9 && !aws.isWebExtHost" + }, + { + "command": "aws.codecatalyst.cloneRepo", + "title": "%AWS.command.codecatalyst.cloneRepo%", + "category": "AWS", + "enablement": "!isCloud9 && !aws.isWebExtHost" + }, + { + "command": "aws.codecatalyst.createDevEnv", + "title": "%AWS.command.codecatalyst.createDevEnv%", + "category": "AWS", + "enablement": "!isCloud9 && !aws.isWebExtHost" + }, + { + "command": "aws.codecatalyst.signout", + "title": "%AWS.command.codecatalyst.signout%", + "category": "AWS", + "icon": "$(debug-disconnect)", + "enablement": "isCloud9 || !aws.isWebExtHost" + }, + { + "command": "aws.toolkit.auth.addConnection", + "title": "%AWS.command.auth.addConnection%", + "category": "%AWS.title%" + }, + { + "command": "aws.toolkit.auth.manageConnections", + "title": "%AWS.command.auth.showConnectionsPage%", + "category": "%AWS.title%" + }, + { + "command": "aws.codecatalyst.manageConnections", + "title": "%AWS.command.auth.showConnectionsPage%", + "category": "%AWS.title%" + }, + { + "command": "aws.toolkit.auth.switchConnections", + "title": "%AWS.command.auth.switchConnections%", + "category": "%AWS.title%" + }, + { + "command": "aws.toolkit.auth.signout", + "title": "%AWS.command.auth.signout%", + "category": "%AWS.title%", + "enablement": "!isCloud9" + }, + { + "command": "aws.toolkit.auth.help", + "title": "%AWS.generic.viewDocs%", + "category": "%AWS.title%", + "icon": "$(question)" + }, + { + "command": "aws.toolkit.createIssueOnGitHub", + "title": "%AWS.command.createIssueOnGitHub%", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ec2.openTerminal", + "title": "%AWS.command.ec2.openTerminal%", + "icon": "$(terminal-view-icon)", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ec2.openRemoteConnection", + "title": "%AWS.command.ec2.openRemoteConnection%", + "icon": "$(remote-explorer)", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ec2.startInstance", + "title": "%AWS.command.ec2.startInstance%", + "icon": "$(debug-start)", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ec2.stopInstance", + "title": "%AWS.command.ec2.stopInstance%", + "icon": "$(debug-stop)", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ec2.rebootInstance", + "title": "%AWS.command.ec2.rebootInstance%", + "icon": "$(debug-restart)", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ec2.copyInstanceId", + "title": "%AWS.command.ec2.copyInstanceId%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ecr.copyTagUri", + "title": "%AWS.command.ecr.copyTagUri%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ecr.deleteTag", + "title": "%AWS.command.ecr.deleteTag%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ecr.copyRepositoryUri", + "title": "%AWS.command.ecr.copyRepositoryUri%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ecr.createRepository", + "title": "%AWS.command.ecr.createRepository%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(add)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ecr.deleteRepository", + "title": "%AWS.command.ecr.deleteRepository%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.showRegion", + "title": "%AWS.command.showRegion%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.iot.createThing", + "title": "%AWS.command.iot.createThing%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(add)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.iot.deleteThing", + "title": "%AWS.generic.promptDelete%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.iot.createCert", + "title": "%AWS.command.iot.createCert%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(add)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.iot.deleteCert", + "title": "%AWS.generic.promptDelete%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.iot.attachCert", + "title": "%AWS.command.iot.attachCert%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(aws-generic-attach-file)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.iot.attachPolicy", + "title": "%AWS.command.iot.attachPolicy%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(aws-generic-attach-file)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.iot.activateCert", + "title": "%AWS.command.iot.activateCert%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.iot.deactivateCert", + "title": "%AWS.command.iot.deactivateCert%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.iot.revokeCert", + "title": "%AWS.command.iot.revokeCert%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.iot.createPolicy", + "title": "%AWS.command.iot.createPolicy%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(add)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.iot.deletePolicy", + "title": "%AWS.generic.promptDelete%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.iot.createPolicyVersion", + "title": "%AWS.command.iot.createPolicyVersion%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.iot.deletePolicyVersion", + "title": "%AWS.generic.promptDelete%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.iot.detachCert", + "title": "%AWS.command.iot.detachCert%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.iot.detachPolicy", + "title": "%AWS.command.iot.detachCert%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.iot.viewPolicyVersion", + "title": "%AWS.command.iot.viewPolicyVersion%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.iot.setDefaultPolicy", + "title": "%AWS.command.iot.setDefaultPolicy%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.iot.copyEndpoint", + "title": "%AWS.command.iot.copyEndpoint%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.redshift.editConnection", + "title": "Edit connection", + "category": "%AWS.title%" + }, + { + "command": "aws.redshift.deleteConnection", + "title": "Delete connection", + "category": "%AWS.title%" + }, + { + "command": "aws.s3.presignedURL", + "title": "%AWS.command.s3.presignedURL%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost" + }, + { + "command": "aws.s3.copyPath", + "title": "%AWS.command.s3.copyPath%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.s3.downloadFileAs", + "title": "%AWS.command.s3.downloadFileAs%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(cloud-download)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.s3.openFile", + "title": "%AWS.command.s3.openFile%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(open-preview)" + }, + { + "command": "aws.s3.editFile", + "title": "%AWS.command.s3.editFile%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(edit)" + }, + { + "command": "aws.s3.uploadFile", + "title": "%AWS.command.s3.uploadFile%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(cloud-upload)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.s3.uploadFileToParent", + "title": "%AWS.command.s3.uploadFileToParent%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.s3.createFolder", + "title": "%AWS.command.s3.createFolder%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(new-folder)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.s3.createBucket", + "title": "%AWS.command.s3.createBucket%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(aws-s3-create-bucket)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.s3.deleteBucket", + "title": "%AWS.generic.promptDelete%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.s3.deleteFile", + "title": "%AWS.generic.promptDelete%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.invokeLambda", + "title": "%AWS.command.invokeLambda%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "title": "%AWS.command.invokeLambda.cn%", + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.downloadLambda", + "title": "%AWS.command.downloadLambda%", + "category": "%AWS.title%", + "enablement": "viewItem == awsRegionFunctionNodeDownloadable", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.uploadLambda", + "title": "%AWS.command.uploadLambda%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.deleteLambda", + "title": "%AWS.generic.promptDelete%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.copyLambdaUrl", + "title": "%AWS.generic.copyUrl%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.deploySamApplication", + "title": "%AWS.command.deploySamApplication%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.toolkit.submitFeedback", + "title": "%AWS.command.submitFeedback%", + "enablement": "!aws.isWebExtHost", + "category": "%AWS.title%", + "icon": "$(comment)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.refreshAwsExplorer", + "title": "%AWS.command.refreshAwsExplorer%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "category": "%AWS.title%", + "icon": { + "dark": "resources/icons/vscode/dark/refresh.svg", + "light": "resources/icons/vscode/light/refresh.svg" + } + }, + { + "command": "aws.samcli.detect", + "title": "%AWS.command.samcli.detect%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.deleteCloudFormation", + "title": "%AWS.command.deleteCloudFormation%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.downloadStateMachineDefinition", + "title": "%AWS.command.downloadStateMachineDefinition%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.executeStateMachine", + "title": "%AWS.command.executeStateMachine%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.renderStateMachineGraph", + "title": "%AWS.command.renderStateMachineGraph%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.copyArn", + "title": "%AWS.command.copyArn%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.copyName", + "title": "%AWS.command.copyName%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.listCommands", + "title": "%AWS.command.listCommands%", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "title": "%AWS.command.listCommands.cn%", + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.viewSchemaItem", + "title": "%AWS.command.viewSchemaItem%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.searchSchema", + "title": "%AWS.command.searchSchema%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.searchSchemaPerRegistry", + "title": "%AWS.command.searchSchemaPerRegistry%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.downloadSchemaItemCode", + "title": "%AWS.command.downloadSchemaItemCode%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.toolkit.viewLogs", + "title": "%AWS.command.viewLogs%", + "category": "%AWS.title%" + }, + { + "command": "aws.toolkit.help", + "title": "%AWS.command.help%", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.toolkit.github", + "title": "%AWS.command.github%", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.quickStart", + "title": "%AWS.command.quickStart%", + "category": "%AWS.title%", + "enablement": "isCloud9", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.cdk.refresh", + "title": "%AWS.command.refreshCdkExplorer%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": { + "dark": "resources/icons/vscode/dark/refresh.svg", + "light": "resources/icons/vscode/light/refresh.svg" + }, + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.cdk.viewDocs", + "title": "%AWS.generic.viewDocs%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost" + }, + { + "command": "aws.stepfunctions.createStateMachineFromTemplate", + "title": "%AWS.command.stepFunctions.createStateMachineFromTemplate%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.stepfunctions.publishStateMachine", + "title": "%AWS.command.stepFunctions.publishStateMachine%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.previewStateMachine", + "title": "%AWS.command.stepFunctions.previewStateMachine%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(aws-stepfunctions-preview)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.cdk.renderStateMachineGraph", + "title": "%AWS.command.cdk.previewStateMachine%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "category": "AWS", + "icon": "$(aws-stepfunctions-preview)" + }, + { + "command": "aws.toolkit.aboutExtension", + "title": "%AWS.command.aboutToolkit%", + "category": "%AWS.title%" + }, + { + "command": "aws.cwl.viewLogStream", + "title": "%AWS.command.viewLogStream%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ssmDocument.createLocalDocument", + "title": "%AWS.command.ssmDocument.createLocalDocument%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ssmDocument.openLocalDocument", + "title": "%AWS.command.ssmDocument.openLocalDocument%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(cloud-download)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ssmDocument.openLocalDocumentJson", + "title": "%AWS.command.ssmDocument.openLocalDocumentJson%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ssmDocument.openLocalDocumentYaml", + "title": "%AWS.command.ssmDocument.openLocalDocumentYaml%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ssmDocument.deleteDocument", + "title": "%AWS.command.ssmDocument.deleteDocument%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ssmDocument.publishDocument", + "title": "%AWS.command.ssmDocument.publishDocument%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(cloud-upload)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ssmDocument.updateDocumentVersion", + "title": "%AWS.command.ssmDocument.updateDocumentVersion%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.copyLogResource", + "title": "%AWS.command.copyLogResource%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(files)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.cwl.searchLogGroup", + "title": "%AWS.command.cloudWatchLogs.searchLogGroup%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(search-view-icon)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.saveCurrentLogDataContent", + "title": "%AWS.command.saveCurrentLogDataContent%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(save)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.cwl.changeFilterPattern", + "title": "%AWS.command.cwl.changeFilterPattern%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(search-view-icon)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.cwl.changeTimeFilter", + "title": "%AWS.command.cwl.changeTimeFilter%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(calendar)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.addSamDebugConfig", + "title": "%AWS.command.addSamDebugConfig%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.toggleSamCodeLenses", + "title": "%AWS.command.toggleSamCodeLenses%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ecs.runCommandInContainer", + "title": "%AWS.ecs.runCommandInContainer%", + "category": "%AWS.title%", + "enablement": "viewItem == awsEcsContainerNodeExecEnabled", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ecs.openTaskInTerminal", + "title": "%AWS.ecs.openTaskInTerminal%", + "category": "%AWS.title%", + "enablement": "viewItem == awsEcsContainerNodeExecEnabled", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ecs.enableEcsExec", + "title": "%AWS.ecs.enableEcsExec%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ecs.viewDocumentation", + "title": "%AWS.generic.viewDocs%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.resources.copyIdentifier", + "title": "%AWS.command.resources.copyIdentifier%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.resources.openResourcePreview", + "title": "%AWS.generic.preview%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(open-preview)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.resources.createResource", + "title": "%AWS.generic.create%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(add)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.resources.deleteResource", + "title": "%AWS.generic.promptDelete%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.resources.updateResource", + "title": "%AWS.generic.promptUpdate%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(pencil)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.resources.updateResourceInline", + "title": "%AWS.generic.promptUpdate%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(pencil)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.resources.saveResource", + "title": "%AWS.generic.save%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(save)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.resources.closeResource", + "title": "%AWS.generic.close%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(close)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.resources.viewDocs", + "title": "%AWS.generic.viewDocs%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(book)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.resources.configure", + "title": "%AWS.command.resources.configure%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(gear)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.apprunner.createService", + "title": "%AWS.command.apprunner.createService%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.ecs.disableEcsExec", + "title": "%AWS.ecs.disableEcsExec%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.apprunner.createServiceFromEcr", + "title": "%AWS.command.apprunner.createServiceFromEcr%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.apprunner.pauseService", + "title": "%AWS.command.apprunner.pauseService%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.apprunner.resumeService", + "title": "%AWS.command.apprunner.resumeService%", + "category": "AWS", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.apprunner.copyServiceUrl", + "title": "%AWS.command.apprunner.copyServiceUrl%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.apprunner.open", + "title": "%AWS.command.apprunner.open%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.apprunner.deleteService", + "title": "%AWS.generic.promptDelete%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.apprunner.startDeployment", + "title": "%AWS.command.apprunner.startDeployment%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.cloudFormation.newTemplate", + "title": "%AWS.command.cloudFormation.newTemplate%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.sam.newTemplate", + "title": "%AWS.command.sam.newTemplate%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.samcli.sync", + "title": "%AWS.command.samcli.sync%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost" + }, + { + "command": "aws.toolkit.amazonq.learnMore", + "title": "%AWS.amazonq.learnMore%", + "category": "%AWS.title%" + }, + { + "command": "aws.toolkit.amazonq.extensionpage", + "title": "Open Amazon Q Extension", + "category": "%AWS.title%" + }, + { + "command": "aws.dev.openMenu", + "title": "Open Developer Menu", + "category": "AWS (Developer)", + "enablement": "aws.isDevMode" + }, + { + "command": "aws.dev.viewLogs", + "title": "Watch Logs", + "category": "AWS (Developer)" + }, + { + "command": "aws.openInApplicationComposerDialog", + "title": "%AWS.command.applicationComposer.openDialog%", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.openInApplicationComposer", + "title": "%AWS.command.applicationComposer.open%", + "category": "%AWS.title%", + "icon": { + "dark": "resources/icons/aws/applicationcomposer/icon-dark.svg", + "light": "resources/icons/aws/applicationcomposer/icon.svg" + }, + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.createNewThreatComposer", + "title": "%AWS.command.threatComposer.createNew%", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.newThreatComposerFile", + "title": "%AWS.command.threatComposer.newFile%", + "category": "%AWS.title%", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + } + ], + "jsonValidation": [ + { + "fileMatch": ".aws/templates.json", + "url": "./dist/src/templates/templates.json" + }, + { + "fileMatch": "*ecs-task-def.json", + "url": "https://ecs-intellisense.s3-us-west-2.amazonaws.com/task-definition/schema.json" + } + ], + "languages": [ + { + "id": "asl", + "extensions": [ + ".asl.json", + ".asl" + ], + "aliases": [ + "Amazon States Language" + ] + }, + { + "id": "asl-yaml", + "aliases": [ + "Amazon States Language (YAML)" + ], + "extensions": [ + ".asl.yaml", + ".asl.yml" + ] + }, + { + "id": "ssm-json", + "extensions": [ + ".ssm.json" + ], + "aliases": [ + "AWS Systems Manager Document (JSON)" + ] + }, + { + "id": "ssm-yaml", + "extensions": [ + ".ssm.yaml", + ".ssm.yml" + ], + "aliases": [ + "AWS Systems Manager Document (YAML)" + ] + } + ], + "keybindings": [ + { + "command": "aws.previewStateMachine", + "key": "ctrl+shift+v", + "mac": "cmd+shift+v", + "when": "editorTextFocus && editorLangId == asl || editorTextFocus && editorLangId == asl-yaml" + } + ], + "grammars": [ + { + "language": "asl", + "scopeName": "source.asl", + "path": "./syntaxes/ASL.tmLanguage" + }, + { + "language": "asl-yaml", + "scopeName": "source.asl.yaml", + "path": "./syntaxes/asl-yaml.tmLanguage.json" + }, + { + "language": "ssm-json", + "scopeName": "source.ssmjson", + "path": "./syntaxes/SSMJSON.tmLanguage" + }, + { + "language": "ssm-yaml", + "scopeName": "source.ssmyaml", + "path": "./syntaxes/SSMYAML.tmLanguage" + } + ], + "resourceLabelFormatters": [ + { + "scheme": "aws-cwl", + "formatting": { + "label": "${path}", + "separator": "/" + } + }, + { + "scheme": "s3*", + "formatting": { + "label": "[S3] ${path}", + "separator": "/" + } + } + ], + "walkthroughs": [], + "icons": { + "aws-amazonq-q-gradient": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1aa" + } + }, + "aws-amazonq-q-squid-ink": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1ab" + } + }, + "aws-amazonq-q-white": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1ac" + } + }, + "aws-amazonq-transform-arrow-dark": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1ad" + } + }, + "aws-amazonq-transform-arrow-light": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1ae" + } + }, + "aws-amazonq-transform-default-dark": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1af" + } + }, + "aws-amazonq-transform-default-light": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1b0" + } + }, + "aws-amazonq-transform-dependencies-dark": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1b1" + } + }, + "aws-amazonq-transform-dependencies-light": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1b2" + } + }, + "aws-amazonq-transform-file-dark": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1b3" + } + }, + "aws-amazonq-transform-file-light": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1b4" + } + }, + "aws-amazonq-transform-logo": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1b5" + } + }, + "aws-amazonq-transform-step-into-dark": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1b6" + } + }, + "aws-amazonq-transform-step-into-light": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1b7" + } + }, + "aws-amazonq-transform-variables-dark": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1b8" + } + }, + "aws-amazonq-transform-variables-light": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1b9" + } + }, + "aws-applicationcomposer-icon": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", @@ -1014,6 +3945,33 @@ "fontCharacter": "\\f1da" } } + }, + "notebooks": [ + { + "type": "aws-redshift-sql-notebook", + "displayName": "Redshift SQL notebook", + "selector": [ + { + "filenamePattern": "*.redshiftnb" + } + ] + } + ], + "customEditors": [ + { + "viewType": "threatComposer.tc.json", + "displayName": "%AWS.threatComposer.title%", + "selector": [ + { + "filenamePattern": "*.tc.json" + } + ] + } + ], + "configurationDefaults": { + "workbench.editorAssociations": { + "{git,gitlens,conflictResolution,vscode-local-history}:/**/*.tc.json": "default" + } } }, "devDependencies": {}, From 49efa8bef040dd278bd7122584d95286805e09ce Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 5 Sep 2024 14:18:51 -0400 Subject: [PATCH 153/172] add changelog --- .../Feature-363c6a15-223a-4e38-aad7-7325e60183a6.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 packages/toolkit/.changes/next-release/Feature-363c6a15-223a-4e38-aad7-7325e60183a6.json diff --git a/packages/toolkit/.changes/next-release/Feature-363c6a15-223a-4e38-aad7-7325e60183a6.json b/packages/toolkit/.changes/next-release/Feature-363c6a15-223a-4e38-aad7-7325e60183a6.json new file mode 100644 index 00000000000..9ab3fe770f5 --- /dev/null +++ b/packages/toolkit/.changes/next-release/Feature-363c6a15-223a-4e38-aad7-7325e60183a6.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "ec2 explorer nodes periodically poll for updates" +} From 0cc83e3e92f849199e678c6fc50bedc1c098efdd Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 5 Sep 2024 15:05:27 -0400 Subject: [PATCH 154/172] start to generalize polling set --- .../apprunner/explorer/apprunnerNode.ts | 24 +- .../awsService/ec2/explorer/ec2ParentNode.ts | 22 +- .../core/src/shared/utilities/pollingSet.ts | 35 + .../ec2/explorer/ec2ParentNode.test.ts | 6 +- packages/toolkit/package.json | 3066 +---------------- 5 files changed, 117 insertions(+), 3036 deletions(-) create mode 100644 packages/core/src/shared/utilities/pollingSet.ts diff --git a/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts b/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts index 13739b9a0a8..03555255163 100644 --- a/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts +++ b/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts @@ -12,14 +12,14 @@ import { AppRunnerClient } from '../../../shared/clients/apprunnerClient' import { getPaginatedAwsCallIter } from '../../../shared/utilities/collectionUtils' import { AppRunner } from 'aws-sdk' import globals from '../../../shared/extensionGlobals' +import { PollingSet } from '../../../shared/utilities/pollingSet' const localize = nls.loadMessageBundle() - const pollingInterval = 20000 + export class AppRunnerNode extends AWSTreeNodeBase { private readonly serviceNodes: Map = new Map() - private readonly pollingNodes: Set = new Set() - private pollTimer?: NodeJS.Timeout + private readonly pollingSet: PollingSet = new PollingSet(pollingInterval) public constructor( public override readonly regionCode: string, @@ -79,7 +79,7 @@ export class AppRunnerNode extends AWSTreeNodeBase { if (this.serviceNodes.has(summary.ServiceArn)) { this.serviceNodes.get(summary.ServiceArn)!.update(summary) if (summary.Status !== 'OPERATION_IN_PROGRESS') { - this.pollingNodes.delete(summary.ServiceArn) + this.pollingSet.delete(summary.ServiceArn) this.clearPollTimer() } } else { @@ -92,27 +92,29 @@ export class AppRunnerNode extends AWSTreeNodeBase { deletedNodeArns.forEach(this.deleteNode.bind(this)) } + // TODO: generalize this method. private clearPollTimer(): void { - if (this.pollingNodes.size === 0 && this.pollTimer) { - globals.clock.clearInterval(this.pollTimer) - this.pollTimer = undefined + if (this.pollingSet.isEmpty() && this.pollingSet.hasTimer()) { + globals.clock.clearInterval(this.pollingSet.pollTimer) + this.pollingSet.pollTimer = undefined } } public startPolling(id: string): void { - this.pollingNodes.add(id) - this.pollTimer = this.pollTimer ?? globals.clock.setInterval(this.refresh.bind(this), pollingInterval) + this.pollingSet.add(id) + this.pollingSet.pollTimer = + this.pollingSet.pollTimer ?? globals.clock.setInterval(this.refresh.bind(this), pollingInterval) } public stopPolling(id: string): void { - this.pollingNodes.delete(id) + this.pollingSet.delete(id) this.serviceNodes.get(id)?.refresh() this.clearPollTimer() } public deleteNode(id: string): void { this.serviceNodes.delete(id) - this.pollingNodes.delete(id) + this.pollingSet.delete(id) } public async createService(request: AppRunner.CreateServiceRequest): Promise { diff --git a/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts b/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts index 4eec697adf8..4ceb9451197 100644 --- a/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts +++ b/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts @@ -10,6 +10,7 @@ import { PlaceholderNode } from '../../../shared/treeview/nodes/placeholderNode' import { Ec2InstanceNode } from './ec2InstanceNode' import { Ec2Client } from '../../../shared/clients/ec2Client' import { updateInPlace } from '../../../shared/utilities/collectionUtils' +import { PollingSet } from '../../../shared/utilities/pollingSet' export const parentContextValue = 'awsEc2ParentNode' export type Ec2Node = Ec2InstanceNode | Ec2ParentNode @@ -20,8 +21,9 @@ export class Ec2ParentNode extends AWSTreeNodeBase { protected readonly placeHolderMessage = '[No EC2 Instances Found]' protected ec2InstanceNodes: Map public override readonly contextValue: string = parentContextValue - public pollingNodes: Set = new Set() - private pollTimer?: NodeJS.Timeout + // private readonly pollingNodes: Set = new Set() + // private pollTimer?: NodeJS.Timeout + public readonly pollingSet: PollingSet = new PollingSet(pollingInterval) public constructor( public override readonly regionCode: string, @@ -56,21 +58,21 @@ export class Ec2ParentNode extends AWSTreeNodeBase { } public isPolling(): boolean { - return this.pollingNodes.size !== 0 + return !this.pollingSet.isEmpty() } public startPolling(newNode: string) { - this.pollingNodes.add(newNode) - this.pollTimer = - this.pollTimer ?? globals.clock.setInterval(this.updatePollingNodes.bind(this), pollingInterval) + this.pollingSet.add(newNode) + this.pollingSet.pollTimer = + this.pollingSet.pollTimer ?? globals.clock.setInterval(this.updatePollingNodes.bind(this), pollingInterval) } private checkForPendingNodes() { - this.pollingNodes.forEach(async (instanceId) => { + this.pollingSet.pollingNodes.forEach(async (instanceId) => { const childNode = this.ec2InstanceNodes.get(instanceId)! await childNode.updateStatus() if (!childNode.isPending()) { - this.pollingNodes.delete(instanceId) + this.pollingSet.pollingNodes.delete(instanceId) childNode.refreshNode() } }) @@ -84,8 +86,8 @@ export class Ec2ParentNode extends AWSTreeNodeBase { } public clearPollTimer() { - globals.clock.clearInterval(this.pollTimer!) - this.pollTimer = undefined + globals.clock.clearInterval(this.pollingSet.pollTimer!) + this.pollingSet.pollTimer = undefined } public async clearChildren() { diff --git a/packages/core/src/shared/utilities/pollingSet.ts b/packages/core/src/shared/utilities/pollingSet.ts new file mode 100644 index 00000000000..38c51fa4864 --- /dev/null +++ b/packages/core/src/shared/utilities/pollingSet.ts @@ -0,0 +1,35 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { globals } from '..' + +export class PollingSet { + public readonly pollingNodes: Set + public pollTimer?: NodeJS.Timeout + + public constructor(private readonly interval: number) { + this.pollingNodes = new Set() + } + + public add(id: T): void { + this.pollingNodes.add(id) + } + + public delete(id: T): void { + this.pollingNodes.delete(id) + } + + public size(): number { + return this.pollingNodes.size + } + + public isEmpty(): boolean { + return this.pollingNodes.size == 0 + } + + public hasTimer(): boolean { + return this.pollTimer != undefined + } +} diff --git a/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts b/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts index 54c6ef914a5..789e99ab4eb 100644 --- a/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts +++ b/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts @@ -159,7 +159,7 @@ describe('ec2ParentNode', function () { getInstanceStub.resolves(mapToInstanceCollection(instances)) await testNode.updateChildren() - assert.strictEqual(testNode.pollingNodes.size, 1) + assert.strictEqual(testNode.pollingSet.pollingNodes.size, 1) getInstanceStub.restore() }) @@ -183,7 +183,7 @@ describe('ec2ParentNode', function () { it('does refresh explorer when timer goes and status changed', async function () { sinon.assert.notCalled(refreshStub) const statusUpdateStub = sinon.stub(Ec2Client.prototype, 'getInstanceStatus').resolves('running') - testNode.pollingNodes.add('0') + testNode.pollingSet.pollingNodes.add('0') await clock.tickAsync(6000) sinon.assert.called(refreshStub) statusUpdateStub.restore() @@ -201,7 +201,7 @@ describe('ec2ParentNode', function () { await testNode.updateChildren() assert.strictEqual(testNode.isPolling(), true) - testNode.pollingNodes.delete('0') + testNode.pollingSet.pollingNodes.delete('0') await clock.tickAsync(6000) assert.strictEqual(testNode.isPolling(), false) sinon.assert.calledOn(clearTimerStub, testNode) diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 8c0e23d0e15..f6ca970581b 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -51,10 +51,7 @@ ], "main": "./dist/src/extensionNode", "browser": "./dist/src/extensionWeb", - "engines": { - "npm": "^10.1.0", - "vscode": "^1.68.0" - }, + "engines": "This field will be autopopulated from the core module during debugging and packaging.", "scripts": { "vscode:prepublish": "npm run clean && npm run buildScripts && webpack --mode production", "buildScripts": "npm run generateNonCodeFiles && npm run copyFiles && npm run generateIcons && npm run generateSettings && npm run generateConfigurationAttributes && tsc -p ./ --noEmit", @@ -673,2996 +670,68 @@ ] } ], - "viewsContainers": { - "activitybar": [ - { - "id": "aws-explorer", - "title": "%AWS.title%", - "icon": "resources/aws-logo.svg", - "cloud9": { - "cn": { - "title": "%AWS.title.cn%", - "icon": "resources/aws-cn-logo.svg" - } - } + "icons": { + "aws-amazonq-q-gradient": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1aa" } - ] - }, - "views": { - "aws-explorer": [ - { - "id": "aws.amazonq.codewhisperer", - "name": "%AWS.amazonq.codewhisperer.title%", - "when": "!isCloud9 && !aws.isSageMaker && !aws.toolkit.amazonq.dismissed && !aws.explorer.showAuthView" - }, - { - "id": "aws.explorer", - "name": "%AWS.lambda.explorerTitle%", - "when": "(isCloud9 || !aws.isWebExtHost) && !aws.explorer.showAuthView" - }, - { - "id": "aws.cdk", - "name": "%AWS.cdk.explorerTitle%", - "when": "!aws.explorer.showAuthView" - }, - { - "id": "aws.codecatalyst", - "name": "%AWS.codecatalyst.explorerTitle%", - "when": "(!isCloud9 && !aws.isSageMaker || isCloud9CodeCatalyst) && !aws.explorer.showAuthView" - }, - { - "type": "webview", - "id": "aws.toolkit.AmazonCommonAuth", - "name": "%AWS.amazonq.login%", - "when": "!isCloud9 && !aws.isSageMaker && aws.explorer.showAuthView" + }, + "aws-amazonq-q-squid-ink": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1ab" } - ] - }, - "submenus": [ - { - "id": "aws.toolkit.auth", - "label": "%AWS.submenu.auth.title%", - "icon": "$(ellipsis)", - "when": "isCloud9 || !aws.isWebExtHost" }, - { - "id": "aws.codecatalyst.submenu", - "label": "%AWS.codecatalyst.submenu.title%", - "icon": "$(ellipsis)", - "when": "isCloud9 || !aws.isWebExtHost" + "aws-amazonq-q-white": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1ac" + } }, - { - "label": "%AWS.generic.feedback%", - "id": "aws.toolkit.submenu.feedback" + "aws-amazonq-transform-arrow-dark": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1ad" + } }, - { - "label": "%AWS.generic.help%", - "id": "aws.toolkit.submenu.help" - } - ], - "menus": { - "commandPalette": [ - { - "command": "aws.apig.copyUrl", - "when": "false" - }, - { - "command": "aws.apig.invokeRemoteRestApi", - "when": "false" - }, - { - "command": "aws.deleteCloudFormation", - "when": "false" - }, - { - "command": "aws.downloadStateMachineDefinition", - "when": "false" - }, - { - "command": "aws.ecr.createRepository", - "when": "false" - }, - { - "command": "aws.executeStateMachine", - "when": "false" - }, - { - "command": "aws.copyArn", - "when": "false" - }, - { - "command": "aws.copyName", - "when": "false" - }, - { - "command": "aws.listCommands", - "when": "false" - }, - { - "command": "aws.codecatalyst.listCommands", - "when": "false" - }, - { - "command": "aws.codecatalyst.manageConnections", - "when": "false" - }, - { - "command": "aws.codecatalyst.openDevEnv", - "when": "!isCloud9" - }, - { - "command": "aws.codecatalyst.createDevEnv", - "when": "!isCloud9" - }, - { - "command": "aws.downloadSchemaItemCode", - "when": "false" - }, - { - "command": "aws.deleteLambda", - "when": "false" - }, - { - "command": "aws.downloadLambda", - "when": "false" - }, - { - "command": "aws.invokeLambda", - "when": "false" - }, - { - "command": "aws.copyLambdaUrl", - "when": "false" - }, - { - "command": "aws.viewSchemaItem", - "when": "false" - }, - { - "command": "aws.searchSchema", - "when": "false" - }, - { - "command": "aws.searchSchemaPerRegistry", - "when": "false" - }, - { - "command": "aws.refreshAwsExplorer", - "when": "false" - }, - { - "command": "aws.cdk.refresh", - "when": "false" - }, - { - "command": "aws.cdk.viewDocs", - "when": "false" - }, - { - "command": "aws.ssmDocument.openLocalDocument", - "when": "false" - }, - { - "command": "aws.ssmDocument.openLocalDocumentJson", - "when": "false" - }, - { - "command": "aws.ssmDocument.openLocalDocumentYaml", - "when": "false" - }, - { - "command": "aws.ssmDocument.deleteDocument", - "when": "false" - }, - { - "command": "aws.ssmDocument.updateDocumentVersion", - "when": "false" - }, - { - "command": "aws.copyLogResource", - "when": "resourceScheme == aws-cwl" - }, - { - "command": "aws.saveCurrentLogDataContent", - "when": "resourceScheme == aws-cwl" - }, - { - "command": "aws.s3.editFile", - "when": "resourceScheme == s3-readonly" - }, - { - "command": "aws.cwl.viewLogStream", - "when": "false" - }, - { - "command": "aws.cwl.changeFilterPattern", - "when": "false" - }, - { - "command": "aws.cwl.changeTimeFilter", - "when": "false" - }, - { - "command": "aws.ecr.deleteRepository", - "when": "false" - }, - { - "command": "aws.ecr.copyTagUri", - "when": "false" - }, - { - "command": "aws.ecr.copyRepositoryUri", - "when": "false" - }, - { - "command": "aws.ecr.deleteTag", - "when": "false" - }, - { - "command": "aws.iot.createThing", - "when": "false" - }, - { - "command": "aws.iot.deleteThing", - "when": "false" - }, - { - "command": "aws.iot.createCert", - "when": "false" - }, - { - "command": "aws.iot.deleteCert", - "when": "false" - }, - { - "command": "aws.iot.attachCert", - "when": "false" - }, - { - "command": "aws.iot.attachPolicy", - "when": "false" - }, - { - "command": "aws.iot.activateCert", - "when": "false" - }, - { - "command": "aws.iot.deactivateCert", - "when": "false" - }, - { - "command": "aws.iot.revokeCert", - "when": "false" - }, - { - "command": "aws.iot.createPolicy", - "when": "false" - }, - { - "command": "aws.iot.deletePolicy", - "when": "false" - }, - { - "command": "aws.iot.createPolicyVersion", - "when": "false" - }, - { - "command": "aws.iot.deletePolicyVersion", - "when": "false" - }, - { - "command": "aws.iot.detachCert", - "when": "false" - }, - { - "command": "aws.iot.detachPolicy", - "when": "false" - }, - { - "command": "aws.iot.viewPolicyVersion", - "when": "false" - }, - { - "command": "aws.iot.setDefaultPolicy", - "when": "false" - }, - { - "command": "aws.iot.copyEndpoint", - "when": "false" - }, - { - "command": "aws.deploySamApplication", - "when": "config.aws.samcli.legacyDeploy" - }, - { - "command": "aws.redshift.editConnection", - "when": "false" - }, - { - "command": "aws.redshift.deleteConnection", - "when": "false" - }, - { - "command": "aws.samcli.sync", - "when": "!config.aws.samcli.legacyDeploy" - }, - { - "command": "aws.s3.copyPath", - "when": "false" - }, - { - "command": "aws.s3.createBucket", - "when": "false" - }, - { - "command": "aws.s3.createFolder", - "when": "false" - }, - { - "command": "aws.s3.deleteBucket", - "when": "false" - }, - { - "command": "aws.s3.deleteFile", - "when": "false" - }, - { - "command": "aws.s3.downloadFileAs", - "when": "false" - }, - { - "command": "aws.s3.openFile", - "when": "false" - }, - { - "command": "aws.s3.editFile", - "when": "false" - }, - { - "command": "aws.s3.uploadFileToParent", - "when": "false" - }, - { - "command": "aws.apprunner.startDeployment", - "when": "false" - }, - { - "command": "aws.apprunner.createService", - "when": "false" - }, - { - "command": "aws.apprunner.pauseService", - "when": "false" - }, - { - "command": "aws.apprunner.resumeService", - "when": "false" - }, - { - "command": "aws.apprunner.copyServiceUrl", - "when": "false" - }, - { - "command": "aws.apprunner.open", - "when": "false" - }, - { - "command": "aws.apprunner.deleteService", - "when": "false" - }, - { - "command": "aws.apprunner.createServiceFromEcr", - "when": "false" - }, - { - "command": "aws.resources.copyIdentifier", - "when": "false" - }, - { - "command": "aws.resources.openResourcePreview", - "when": "false" - }, - { - "command": "aws.resources.createResource", - "when": "false" - }, - { - "command": "aws.resources.deleteResource", - "when": "false" - }, - { - "command": "aws.resources.updateResource", - "when": "false" - }, - { - "command": "aws.resources.updateResourceInline", - "when": "false" - }, - { - "command": "aws.resources.saveResource", - "when": "false" - }, - { - "command": "aws.resources.closeResource", - "when": "false" - }, - { - "command": "aws.resources.viewDocs", - "when": "false" - }, - { - "command": "aws.ecs.runCommandInContainer", - "when": "false" - }, - { - "command": "aws.ecs.openTaskInTerminal", - "when": "false" - }, - { - "command": "aws.ecs.enableEcsExec", - "when": "false" - }, - { - "command": "aws.ecs.disableEcsExec", - "when": "false" - }, - { - "command": "aws.ecs.viewDocumentation", - "when": "false" - }, - { - "command": "aws.renderStateMachineGraph", - "when": "false" - }, - { - "command": "aws.toolkit.auth.addConnection", - "when": "false" - }, - { - "command": "aws.toolkit.auth.switchConnections", - "when": "false" - }, - { - "command": "aws.toolkit.auth.help", - "when": "false" - }, - { - "command": "aws.toolkit.auth.manageConnections" - }, - { - "command": "aws.ec2.openRemoteConnection", - "when": "aws.isDevMode" - }, - { - "command": "aws.ec2.openTerminal", - "when": "aws.isDevMode" - }, - { - "command": "aws.ec2.startInstance", - "when": "aws.isDevMode" - }, - { - "command": "aws.ec2.stopInstance", - "when": "aws.isDevMode" - }, - { - "command": "aws.ec2.rebootInstance", - "when": "aws.isDevMode" - }, - { - "command": "aws.dev.openMenu", - "when": "aws.isDevMode || isCloud9" - }, - { - "command": "aws.openInApplicationComposer", - "when": "false" - }, - { - "command": "aws.toolkit.amazonq.learnMore", - "when": "false" - }, - { - "command": "aws.toolkit.amazonq.extensionpage", - "when": "false" - }, - { - "command": "aws.newThreatComposerFile", - "when": "false" + "aws-amazonq-transform-arrow-light": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1ae" } - ], - "editor/title": [ - { - "command": "aws.previewStateMachine", - "when": "editorLangId == asl || editorLangId == asl-yaml", - "group": "navigation" - }, - { - "command": "aws.saveCurrentLogDataContent", - "when": "resourceScheme == aws-cwl", - "group": "navigation" - }, - { - "command": "aws.cwl.changeFilterPattern", - "when": "resourceScheme == aws-cwl", - "group": "navigation" - }, - { - "command": "aws.cwl.changeTimeFilter", - "when": "resourceScheme == aws-cwl", - "group": "navigation" - }, - { - "command": "aws.s3.editFile", - "when": "resourceScheme == s3-readonly", - "group": "navigation" - }, - { - "command": "aws.ssmDocument.publishDocument", - "when": "editorLangId =~ /^(ssm-yaml|ssm-json)$/", - "group": "navigation" - }, - { - "command": "aws.resources.updateResourceInline", - "when": "resourceScheme == awsResource && !isCloud9 && config.aws.experiments.jsonResourceModification", - "group": "navigation" - }, - { - "command": "aws.resources.closeResource", - "when": "resourcePath =~ /^.+(awsResource.json)$/", - "group": "navigation" - }, - { - "command": "aws.resources.saveResource", - "when": "resourcePath =~ /^.+(awsResource.json)$/", - "group": "navigation" - }, - { - "command": "aws.openInApplicationComposer", - "when": "(editorLangId == json && !(resourceFilename =~ /^.*\\.tc\\.json$/)) || editorLangId == yaml || resourceFilename =~ /^.*\\.(template)$/", - "group": "navigation" + }, + "aws-amazonq-transform-default-dark": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1af" } - ], - "editor/title/context": [ - { - "command": "aws.copyLogResource", - "when": "resourceScheme == aws-cwl", - "group": "1_cutcopypaste@1" + }, + "aws-amazonq-transform-default-light": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1b0" } - ], - "view/title": [ - { - "command": "aws.toolkit.submitFeedback", - "when": "view == aws.explorer && !aws.isWebExtHost", - "group": "navigation@6" - }, - { - "command": "aws.refreshAwsExplorer", - "when": "view == aws.explorer", - "group": "navigation@5" - }, - { - "command": "aws.cdk.refresh", - "when": "view == aws.cdk", - "group": "navigation@1" - }, - { - "command": "aws.toolkit.login", - "when": "view == aws.explorer", - "group": "1_account@1" - }, - { - "command": "aws.showRegion", - "when": "view == aws.explorer", - "group": "1_account@2" - }, - { - "command": "aws.listCommands", - "when": "view == aws.explorer && !isCloud9", - "group": "1_account@3" - }, - { - "command": "aws.lambda.createNewSamApp", - "when": "view == aws.explorer", - "group": "3_lambda@1" - }, - { - "command": "aws.launchConfigForm", - "when": "view == aws.explorer", - "group": "3_lambda@2" - }, - { - "command": "aws.deploySamApplication", - "when": "config.aws.samcli.legacyDeploy && view == aws.explorer", - "group": "3_lambda@3" - }, - { - "command": "aws.samcli.sync", - "when": "!config.aws.samcli.legacyDeploy && view == aws.explorer", - "group": "3_lambda@3" - }, - { - "submenu": "aws.toolkit.submenu.feedback", - "when": "view =~ /^aws\\./ && view != aws.AmazonQChatView && view != aws.amazonq.AmazonCommonAuth", - "group": "y_toolkitMeta@1" - }, - { - "submenu": "aws.toolkit.submenu.help", - "when": "view =~ /^aws\\./ && view != aws.AmazonQChatView && view != aws.amazonq.AmazonCommonAuth", - "group": "y_toolkitMeta@2" - }, - { - "command": "aws.codecatalyst.cloneRepo", - "when": "view == aws.codecatalyst && !isCloud9 && aws.codecatalyst.connected", - "group": "1_codeCatalyst@1" - }, - { - "command": "aws.codecatalyst.createDevEnv", - "when": "view == aws.codecatalyst && !isCloud9 && aws.codecatalyst.connected", - "group": "1_codeCatalyst@1" - }, - { - "command": "aws.codecatalyst.listCommands", - "when": "view == aws.codecatalyst && !isCloud9 && aws.codecatalyst.connected", - "group": "1_codeCatalyst@1" - }, - { - "command": "aws.codecatalyst.openDevEnv", - "when": "view == aws.codecatalyst && !isCloud9 && aws.codecatalyst.connected", - "group": "1_codeCatalyst@1" - }, - { - "command": "aws.codecatalyst.manageConnections", - "when": "view == aws.codecatalyst && !isCloud9 && !aws.codecatalyst.connected", - "group": "2_codeCatalyst@1" - }, - { - "command": "aws.codecatalyst.signout", - "when": "view == aws.codecatalyst && !isCloud9 && aws.codecatalyst.connected", - "group": "2_codeCatalyst@1" + }, + "aws-amazonq-transform-dependencies-dark": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1b1" } - ], - "explorer/context": [ - { - "command": "aws.deploySamApplication", - "when": "config.aws.samcli.legacyDeploy && isFileSystemResource && resourceFilename =~ /^template\\.(json|yml|yaml)$/", - "group": "z_aws@1" - }, - { - "command": "aws.samcli.sync", - "when": "!config.aws.samcli.legacyDeploy && isFileSystemResource && resourceFilename =~ /^(template\\.(json|yml|yaml))|(samconfig\\.toml)$/", - "group": "z_aws@1" - }, - { - "command": "aws.uploadLambda", - "when": "explorerResourceIsFolder || isFileSystemResource && resourceFilename =~ /^template\\.(json|yml|yaml)$/", - "group": "z_aws@3" - }, - { - "command": "aws.openInApplicationComposer", - "when": "isFileSystemResource && !(resourceFilename =~ /^.*\\.tc\\.json$/) && resourceFilename =~ /^.*\\.(json|yml|yaml|template)$/", - "group": "z_aws@1" - } - ], - "view/item/context": [ - { - "command": "aws.apig.invokeRemoteRestApi", - "when": "view == aws.explorer && viewItem =~ /^(awsApiGatewayNode)$/", - "group": "0@1" - }, - { - "command": "aws.ec2.openTerminal", - "group": "0@1", - "when": "viewItem =~ /^(awsEc2(Parent|Running)Node)$/" - }, - { - "command": "aws.ec2.openTerminal", - "group": "inline@1", - "when": "viewItem =~ /^(awsEc2(Parent|Running)Node)$/" - }, - { - "command": "aws.ec2.openRemoteConnection", - "group": "0@1", - "when": "viewItem =~ /^(awsEc2(Parent|Running)Node)$/" - }, - { - "command": "aws.ec2.openRemoteConnection", - "group": "inline@1", - "when": "viewItem =~ /^(awsEc2(Parent|Running)Node)$/" - }, - { - "command": "aws.ec2.startInstance", - "group": "0@1", - "when": "viewItem == awsEc2StoppedNode" - }, - { - "command": "aws.ec2.startInstance", - "group": "inline@1", - "when": "viewItem == awsEc2StoppedNode" - }, - { - "command": "aws.ec2.stopInstance", - "group": "0@1", - "when": "viewItem == awsEc2RunningNode" - }, - { - "command": "aws.ec2.stopInstance", - "group": "inline@1", - "when": "viewItem == awsEc2RunningNode" - }, - { - "command": "aws.ec2.rebootInstance", - "group": "0@1", - "when": "viewItem == awsEc2RunningNode" - }, - { - "command": "aws.ec2.rebootInstance", - "group": "inline@1", - "when": "viewItem == awsEc2RunningNode" - }, - { - "command": "aws.ecr.createRepository", - "when": "view == aws.explorer && viewItem == awsEcrNode", - "group": "inline@1" - }, - { - "command": "aws.iot.createThing", - "when": "view == aws.explorer && viewItem == awsIotThingsNode", - "group": "inline@1" - }, - { - "command": "aws.iot.createCert", - "when": "view == aws.explorer && viewItem == awsIotCertsNode", - "group": "inline@1" - }, - { - "command": "aws.iot.createPolicy", - "when": "view == aws.explorer && viewItem == awsIotPoliciesNode", - "group": "inline@1" - }, - { - "command": "aws.iot.attachCert", - "when": "view == aws.explorer && viewItem == awsIotThingNode", - "group": "inline@1" - }, - { - "command": "aws.iot.attachPolicy", - "when": "view == aws.explorer && viewItem =~ /^awsIotCertificateNode.(Things|Policies)/", - "group": "inline@1" - }, - { - "command": "aws.redshift.editConnection", - "when": "view == aws.explorer && viewItem == awsRedshiftWarehouseNode", - "group": "0@1" - }, - { - "command": "aws.redshift.deleteConnection", - "when": "view == aws.explorer && viewItem == awsRedshiftWarehouseNode", - "group": "0@2" - }, - { - "command": "aws.s3.openFile", - "when": "view == aws.explorer && viewItem == awsS3FileNode && !isCloud9", - "group": "0@1" - }, - { - "command": "aws.s3.editFile", - "when": "view == aws.explorer && viewItem == awsS3FileNode && !isCloud9", - "group": "inline@1" - }, - { - "command": "aws.s3.downloadFileAs", - "when": "view == aws.explorer && viewItem == awsS3FileNode", - "group": "inline@2" - }, - { - "command": "aws.s3.createBucket", - "when": "view == aws.explorer && viewItem == awsS3Node", - "group": "inline@1" - }, - { - "command": "aws.s3.createFolder", - "when": "view == aws.explorer && viewItem =~ /^(awsS3BucketNode|awsS3FolderNode)$/", - "group": "inline@1" - }, - { - "command": "aws.ssmDocument.openLocalDocument", - "when": "view == aws.explorer && viewItem =~ /^(awsDocumentItemNode|awsDocumentItemNodeWriteable)$/", - "group": "inline@1" - }, - { - "command": "aws.s3.uploadFile", - "when": "view == aws.explorer && viewItem =~ /^(awsS3BucketNode|awsS3FolderNode)$/", - "group": "inline@2" - }, - { - "command": "aws.showRegion", - "when": "view == aws.explorer && viewItem == awsRegionNode", - "group": "0@1" - }, - { - "command": "aws.lambda.createNewSamApp", - "when": "view == aws.explorer && viewItem == awsLambdaNode || viewItem == awsRegionNode", - "group": "1@1" - }, - { - "command": "aws.launchConfigForm", - "when": "view == aws.explorer && viewItem == awsLambdaNode || viewItem == awsRegionNode || viewItem == awsCloudFormationRootNode", - "group": "1@1" - }, - { - "command": "aws.deploySamApplication", - "when": "config.aws.samcli.legacyDeploy && view == aws.explorer && viewItem =~ /^(awsLambdaNode|awsRegionNode|awsCloudFormationRootNode)$/", - "group": "1@2" - }, - { - "command": "aws.samcli.sync", - "when": "!config.aws.samcli.legacyDeploy && view == aws.explorer && viewItem =~ /^(awsLambdaNode|awsRegionNode|awsCloudFormationRootNode)$/", - "group": "1@2" - }, - { - "command": "aws.ec2.copyInstanceId", - "when": "view == aws.explorer && viewItem =~ /^(awsEc2(Parent|Running|Stopped)Node)$/", - "group": "2@0" - }, - { - "command": "aws.ecr.copyTagUri", - "when": "view == aws.explorer && viewItem == awsEcrTagNode", - "group": "2@1" - }, - { - "command": "aws.ecr.deleteTag", - "when": "view == aws.explorer && viewItem == awsEcrTagNode", - "group": "3@1" - }, - { - "command": "aws.ecr.copyRepositoryUri", - "when": "view == aws.explorer && viewItem == awsEcrRepositoryNode", - "group": "2@1" - }, - { - "command": "aws.ecr.createRepository", - "when": "view == aws.explorer && viewItem == awsEcrNode", - "group": "0@1" - }, - { - "command": "aws.ecr.deleteRepository", - "when": "view == aws.explorer && viewItem == awsEcrRepositoryNode", - "group": "3@1" - }, - { - "command": "aws.invokeLambda", - "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode)$/", - "group": "0@1" - }, - { - "command": "aws.downloadLambda", - "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable)$/", - "group": "0@2" - }, - { - "command": "aws.uploadLambda", - "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable)$/", - "group": "1@1" - }, - { - "command": "aws.deleteLambda", - "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable)$/", - "group": "4@1" - }, - { - "command": "aws.copyLambdaUrl", - "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable)$/", - "group": "2@0" - }, - { - "command": "aws.deleteCloudFormation", - "when": "view == aws.explorer && viewItem == awsCloudFormationNode", - "group": "3@5" - }, - { - "command": "aws.searchSchema", - "when": "view == aws.explorer && viewItem == awsSchemasNode", - "group": "0@1" - }, - { - "command": "aws.searchSchemaPerRegistry", - "when": "view == aws.explorer && viewItem == awsRegistryItemNode", - "group": "0@1" - }, - { - "command": "aws.viewSchemaItem", - "when": "view == aws.explorer && viewItem == awsSchemaItemNode", - "group": "0@1" - }, - { - "command": "aws.stepfunctions.createStateMachineFromTemplate", - "when": "view == aws.explorer && viewItem == awsStepFunctionsNode", - "group": "0@1" - }, - { - "command": "aws.downloadStateMachineDefinition", - "when": "view == aws.explorer && viewItem == awsStateMachineNode", - "group": "0@1" - }, - { - "command": "aws.renderStateMachineGraph", - "when": "view == aws.explorer && viewItem == awsStateMachineNode", - "group": "0@2" - }, - { - "command": "aws.cdk.renderStateMachineGraph", - "when": "viewItem == awsCdkStateMachineNode", - "group": "inline@1" - }, - { - "command": "aws.cdk.renderStateMachineGraph", - "when": "viewItem == awsCdkStateMachineNode", - "group": "0@1" - }, - { - "command": "aws.executeStateMachine", - "when": "view == aws.explorer && viewItem == awsStateMachineNode", - "group": "0@3" - }, - { - "command": "aws.iot.createThing", - "when": "view == aws.explorer && viewItem == awsIotThingsNode", - "group": "0@1" - }, - { - "command": "aws.iot.createCert", - "when": "view == aws.explorer && viewItem == awsIotCertsNode", - "group": "0@1" - }, - { - "command": "aws.iot.createPolicy", - "when": "view == aws.explorer && viewItem == awsIotPoliciesNode", - "group": "0@1" - }, - { - "command": "aws.iot.createPolicyVersion", - "when": "view == aws.explorer && viewItem == awsIotPolicyNode.WithVersions", - "group": "0@1" - }, - { - "command": "aws.iot.viewPolicyVersion", - "when": "view == aws.explorer && viewItem =~ /^awsIotPolicyVersionNode./", - "group": "0@1" - }, - { - "command": "aws.iot.attachCert", - "when": "view == aws.explorer && viewItem == awsIotThingNode", - "group": "0@1" - }, - { - "command": "aws.iot.attachPolicy", - "when": "view == aws.explorer && viewItem =~ /^awsIotCertificateNode.(Things|Policies)/", - "group": "0@1" - }, - { - "command": "aws.s3.createBucket", - "when": "view == aws.explorer && viewItem == awsS3Node", - "group": "0@1" - }, - { - "command": "aws.s3.downloadFileAs", - "when": "view == aws.explorer && viewItem == awsS3FileNode", - "group": "0@1" - }, - { - "command": "aws.s3.uploadFile", - "when": "view == aws.explorer && viewItem =~ /^(awsS3BucketNode|awsS3FolderNode)$/", - "group": "0@1" - }, - { - "command": "aws.s3.uploadFileToParent", - "when": "view == aws.explorer && viewItem == awsS3FileNode", - "group": "1@1" - }, - { - "command": "aws.s3.createFolder", - "when": "view == aws.explorer && viewItem =~ /^(awsS3BucketNode|awsS3FolderNode)$/", - "group": "1@1" - }, - { - "command": "aws.iot.deactivateCert", - "when": "view == aws.explorer && viewItem =~ /^awsIotCertificateNode.(Things|Policies).ACTIVE$/", - "group": "1@1" - }, - { - "command": "aws.iot.activateCert", - "when": "view == aws.explorer && viewItem =~ /^awsIotCertificateNode.(Things|Policies).INACTIVE$/", - "group": "1@1" - }, - { - "command": "aws.iot.revokeCert", - "when": "view == aws.explorer && viewItem =~ /^awsIotCertificateNode.(Things|Policies).(ACTIVE|INACTIVE)$/", - "group": "1@2" - }, - { - "command": "aws.iot.setDefaultPolicy", - "when": "view == aws.explorer && viewItem == awsIotPolicyVersionNode.NONDEFAULT", - "group": "1@1" - }, - { - "command": "aws.iot.copyEndpoint", - "when": "view == aws.explorer && viewItem == awsIotNode", - "group": "2@1" - }, - { - "command": "aws.copyName", - "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode|awsStateMachineNode|awsCloudFormationNode|awsS3BucketNode|awsS3FolderNode|awsS3FileNode|awsApiGatewayNode|awsIotThingNode)$|^(awsAppRunnerServiceNode|awsIotCertificateNode|awsIotPolicyNode|awsIotPolicyVersionNode|(awsEc2(Running|Pending|Stopped)Node))/", - "group": "2@1" - }, - { - "command": "aws.copyArn", - "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode|awsStateMachineNode|awsCloudFormationNode|awsCloudWatchLogNode|awsS3BucketNode|awsS3FolderNode|awsS3FileNode|awsApiGatewayNode|awsEcrRepositoryNode|awsIotThingNode)$|^(awsAppRunnerServiceNode|awsEcsServiceNode|awsIotCertificateNode|awsIotPolicyNode|awsIotPolicyVersionNode|awsMdeInstanceNode|(awsEc2(Running|Pending|Stopped)Node))/", - "group": "2@2" - }, - { - "command": "aws.cwl.searchLogGroup", - "group": "0@1", - "when": "view == aws.explorer && viewItem =~ /^awsCloudWatchLogNode|awsCloudWatchLogParentNode$/" - }, - { - "command": "aws.cwl.searchLogGroup", - "group": "inline@1", - "when": "view == aws.explorer && viewItem =~ /^awsCloudWatchLogNode|awsCloudWatchLogParentNode$/" - }, - { - "command": "aws.apig.copyUrl", - "when": "view == aws.explorer && viewItem =~ /^(awsApiGatewayNode)$/", - "group": "2@0" - }, - { - "command": "aws.s3.copyPath", - "when": "view == aws.explorer && viewItem =~ /^(awsS3FolderNode|awsS3FileNode)$/", - "group": "2@3" - }, - { - "command": "aws.s3.presignedURL", - "when": "view == aws.explorer && viewItem =~ /^(awsS3FileNode)$/", - "group": "2@4" - }, - { - "command": "aws.iot.detachCert", - "when": "view == aws.explorer && viewItem =~ /^(awsIotCertificateNode.Things)/", - "group": "3@1" - }, - { - "command": "aws.iot.detachPolicy", - "when": "view == aws.explorer && viewItem == awsIotPolicyNode.Certificates", - "group": "3@1" - }, - { - "command": "aws.iot.deleteThing", - "when": "view == aws.explorer && viewItem == awsIotThingNode", - "group": "3@1" - }, - { - "command": "aws.iot.deleteCert", - "when": "view == aws.explorer && viewItem =~ /^awsIotCertificateNode.Policies/", - "group": "3@1" - }, - { - "command": "aws.iot.deletePolicy", - "when": "view == aws.explorer && viewItem == awsIotPolicyNode.WithVersions", - "group": "3@1" - }, - { - "command": "aws.iot.deletePolicyVersion", - "when": "view == aws.explorer && viewItem == awsIotPolicyVersionNode.NONDEFAULT", - "group": "3@1" - }, - { - "command": "aws.s3.deleteBucket", - "when": "view == aws.explorer && viewItem == awsS3BucketNode", - "group": "3@1" - }, - { - "command": "aws.s3.deleteFile", - "when": "view == aws.explorer && viewItem == awsS3FileNode", - "group": "3@1" - }, - { - "command": "aws.downloadSchemaItemCode", - "when": "view == aws.explorer && viewItem == awsSchemaItemNode", - "group": "1@1" - }, - { - "command": "aws.cwl.viewLogStream", - "group": "0@1", - "when": "view == aws.explorer && viewItem == awsCloudWatchLogNode" - }, - { - "command": "aws.ssmDocument.openLocalDocumentYaml", - "group": "0@1", - "when": "view == aws.explorer && viewItem =~ /^(awsDocumentItemNode|awsDocumentItemNodeWriteable)$/" - }, - { - "command": "aws.ssmDocument.openLocalDocumentJson", - "group": "0@2", - "when": "view == aws.explorer && viewItem =~ /^(awsDocumentItemNode|awsDocumentItemNodeWriteable)$/" - }, - { - "command": "aws.ssmDocument.updateDocumentVersion", - "group": "2@1", - "when": "view == aws.explorer && viewItem == awsDocumentItemNodeWriteable" - }, - { - "command": "aws.ssmDocument.deleteDocument", - "group": "3@2", - "when": "view == aws.explorer && viewItem == awsDocumentItemNodeWriteable" - }, - { - "command": "aws.ecs.runCommandInContainer", - "group": "0@1", - "when": "view == aws.explorer && viewItem =~ /^(awsEcsContainerNodeExec)(.*)$/" - }, - { - "command": "aws.ecs.openTaskInTerminal", - "group": "0@2", - "when": "view == aws.explorer && viewItem =~ /^(awsEcsContainerNodeExec)(.*)$/ && !isCloud9" - }, - { - "command": "aws.ecs.enableEcsExec", - "group": "0@2", - "when": "view == aws.explorer && viewItem == awsEcsServiceNode.DISABLED" - }, - { - "command": "aws.ecs.disableEcsExec", - "group": "0@2", - "when": "view == aws.explorer && viewItem == awsEcsServiceNode.ENABLED" - }, - { - "command": "aws.ecs.viewDocumentation", - "group": "1@3", - "when": "view == aws.explorer && viewItem =~ /^(awsEcsClusterNode|awsEcsContainerNode)$|^awsEcsServiceNode/" - }, - { - "command": "aws.resources.configure", - "when": "view == aws.explorer && viewItem == resourcesRootNode", - "group": "1@1" - }, - { - "command": "aws.resources.configure", - "when": "view == aws.explorer && viewItem == resourcesRootNode", - "group": "inline@1" - }, - { - "command": "aws.resources.openResourcePreview", - "when": "view == aws.explorer && viewItem =~ /^(.*)(ResourceNode)$/", - "group": "1@1" - }, - { - "command": "aws.resources.copyIdentifier", - "when": "view == aws.explorer && viewItem =~ /^(.*)(ResourceNode)$/", - "group": "1@1" - }, - { - "command": "aws.resources.viewDocs", - "when": "view == aws.explorer && viewItem =~ /^(.*)(Documented)(.*)(ResourceTypeNode)$/", - "group": "1@1" - }, - { - "command": "aws.resources.createResource", - "when": "view == aws.explorer && viewItem =~ /^(.*)(Creatable)(.*)(ResourceTypeNode)$/ && !isCloud9 && config.aws.experiments.jsonResourceModification", - "group": "2@1" - }, - { - "command": "aws.resources.createResource", - "when": "view == aws.explorer && viewItem =~ /^(.*)(Creatable)(.*)(ResourceTypeNode)$/ && !isCloud9 && config.aws.experiments.jsonResourceModification", - "group": "inline@1" - }, - { - "command": "aws.resources.updateResource", - "when": "view == aws.explorer && viewItem =~ /^(.*)(Updatable)(.*)(ResourceNode)$/ && !isCloud9 && config.aws.experiments.jsonResourceModification", - "group": "2@1" - }, - { - "command": "aws.resources.deleteResource", - "when": "view == aws.explorer && viewItem =~ /^(.*)(Deletable)(.*)(ResourceNode)$/ && !isCloud9 && config.aws.experiments.jsonResourceModification", - "group": "2@2" - }, - { - "command": "aws.apprunner.createServiceFromEcr", - "group": "0@2", - "when": "view == aws.explorer && viewItem =~ /awsEcrTagNode|awsEcrRepositoryNode/" - }, - { - "command": "aws.apprunner.startDeployment", - "group": "0@1", - "when": "view == aws.explorer && viewItem == awsAppRunnerServiceNode.RUNNING" - }, - { - "command": "aws.apprunner.createService", - "group": "0@2", - "when": "view == aws.explorer && viewItem == awsAppRunnerNode" - }, - { - "command": "aws.apprunner.pauseService", - "group": "0@3", - "when": "view == aws.explorer && viewItem == awsAppRunnerServiceNode.RUNNING" - }, - { - "command": "aws.apprunner.resumeService", - "group": "0@3", - "when": "view == aws.explorer && viewItem == awsAppRunnerServiceNode.PAUSED" - }, - { - "command": "aws.apprunner.copyServiceUrl", - "group": "1@1", - "when": "view == aws.explorer && viewItem == awsAppRunnerServiceNode.RUNNING" - }, - { - "command": "aws.apprunner.open", - "group": "1@2", - "when": "view == aws.explorer && viewItem == awsAppRunnerServiceNode.RUNNING" - }, - { - "command": "aws.apprunner.deleteService", - "group": "3@1", - "when": "view == aws.explorer && viewItem =~ /awsAppRunnerServiceNode.[RUNNING|PAUSED|CREATE_FAILED]/" - }, - { - "command": "aws.cloudFormation.newTemplate", - "group": "0@1", - "when": "view == aws.explorer && viewItem == awsCloudFormationRootNode" - }, - { - "command": "aws.sam.newTemplate", - "group": "0@2", - "when": "view == aws.explorer && viewItem == awsCloudFormationRootNode" - }, - { - "command": "aws.cdk.viewDocs", - "when": "viewItem == awsCdkRootNode", - "group": "0@2" - }, - { - "command": "aws.toolkit.auth.addConnection", - "when": "viewItem == awsAuthNode", - "group": "0@1" - }, - { - "command": "aws.toolkit.auth.switchConnections", - "when": "viewItem == awsAuthNode", - "group": "0@2" - }, - { - "command": "aws.toolkit.auth.signout", - "when": "viewItem == awsAuthNode && !isCloud9", - "group": "0@3" - }, - { - "command": "aws.toolkit.auth.help", - "when": "viewItem == awsAuthNode", - "group": "inline@1" - }, - { - "submenu": "aws.toolkit.auth", - "when": "viewItem == awsAuthNode", - "group": "inline@2" - }, - { - "submenu": "aws.codecatalyst.submenu", - "when": "viewItem =~ /^awsCodeCatalystNode/", - "group": "inline@1" - }, - { - "command": "aws.codecatalyst.manageConnections", - "when": "viewItem =~ /^awsCodeCatalystNode/", - "group": "0@1" - }, - { - "command": "aws.codecatalyst.signout", - "when": "viewItem =~ /^awsCodeCatalystNode/&& !isCloud9 && aws.codecatalyst.connected", - "group": "0@2" - } - ], - "aws.toolkit.auth": [ - { - "command": "aws.toolkit.auth.manageConnections", - "group": "0@1" - }, - { - "command": "aws.toolkit.auth.switchConnections", - "group": "0@2" - }, - { - "command": "aws.toolkit.auth.signout", - "enablement": "!isCloud9", - "group": "0@3" - } - ], - "aws.toolkit.submenu.feedback": [ - { - "command": "aws.toolkit.submitFeedback", - "when": "!aws.isWebExtHost", - "group": "1_feedback@1" - }, - { - "command": "aws.toolkit.createIssueOnGitHub", - "group": "1_feedback@2" - } - ], - "aws.toolkit.submenu.help": [ - { - "command": "aws.quickStart", - "when": "isCloud9", - "group": "1_help@1" - }, - { - "command": "aws.toolkit.help", - "group": "1_help@2" - }, - { - "command": "aws.toolkit.github", - "group": "1_help@3" - }, - { - "command": "aws.toolkit.aboutExtension", - "group": "1_help@4" - }, - { - "command": "aws.toolkit.viewLogs", - "group": "1_help@5" - } - ], - "file/newFile": [ - { - "command": "aws.newThreatComposerFile" - } - ] - }, - "commands": [ - { - "command": "aws.accessanalyzer.iamPolicyChecks", - "title": "%AWS.command.accessanalyzer.iamPolicyChecks%", - "category": "%AWS.title%" - }, - { - "command": "aws.launchConfigForm", - "title": "%AWS.command.launchConfigForm.title%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.apig.copyUrl", - "title": "%AWS.command.apig.copyUrl%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.apig.invokeRemoteRestApi", - "title": "%AWS.command.apig.invokeRemoteRestApi%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%", - "title": "%AWS.command.apig.invokeRemoteRestApi.cn%" - } - } - }, - { - "command": "aws.lambda.createNewSamApp", - "title": "%AWS.command.createNewSamApp%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.toolkit.login", - "title": "%AWS.command.login%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "title": "%AWS.command.login.cn%", - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.toolkit.credentials.profile.create", - "title": "%AWS.command.credentials.profile.create%", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.toolkit.credentials.edit", - "title": "%AWS.command.credentials.edit%", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.codecatalyst.openOrg", - "title": "%AWS.command.codecatalyst.openOrg%", - "category": "AWS", - "enablement": "isCloud9 || !aws.isWebExtHost" - }, - { - "command": "aws.codecatalyst.openProject", - "title": "%AWS.command.codecatalyst.openProject%", - "category": "AWS", - "enablement": "isCloud9 || !aws.isWebExtHost" - }, - { - "command": "aws.codecatalyst.openRepo", - "title": "%AWS.command.codecatalyst.openRepo%", - "category": "AWS", - "enablement": "isCloud9 || !aws.isWebExtHost" - }, - { - "command": "aws.codecatalyst.openDevEnv", - "title": "%AWS.command.codecatalyst.openDevEnv%", - "category": "AWS", - "enablement": "!isCloud9 && !aws.isWebExtHost" - }, - { - "command": "aws.codecatalyst.listCommands", - "title": "%AWS.command.codecatalyst.listCommands%", - "category": "AWS", - "enablement": "!isCloud9 && !aws.isWebExtHost" - }, - { - "command": "aws.codecatalyst.cloneRepo", - "title": "%AWS.command.codecatalyst.cloneRepo%", - "category": "AWS", - "enablement": "!isCloud9 && !aws.isWebExtHost" - }, - { - "command": "aws.codecatalyst.createDevEnv", - "title": "%AWS.command.codecatalyst.createDevEnv%", - "category": "AWS", - "enablement": "!isCloud9 && !aws.isWebExtHost" - }, - { - "command": "aws.codecatalyst.signout", - "title": "%AWS.command.codecatalyst.signout%", - "category": "AWS", - "icon": "$(debug-disconnect)", - "enablement": "isCloud9 || !aws.isWebExtHost" - }, - { - "command": "aws.toolkit.auth.addConnection", - "title": "%AWS.command.auth.addConnection%", - "category": "%AWS.title%" - }, - { - "command": "aws.toolkit.auth.manageConnections", - "title": "%AWS.command.auth.showConnectionsPage%", - "category": "%AWS.title%" - }, - { - "command": "aws.codecatalyst.manageConnections", - "title": "%AWS.command.auth.showConnectionsPage%", - "category": "%AWS.title%" - }, - { - "command": "aws.toolkit.auth.switchConnections", - "title": "%AWS.command.auth.switchConnections%", - "category": "%AWS.title%" - }, - { - "command": "aws.toolkit.auth.signout", - "title": "%AWS.command.auth.signout%", - "category": "%AWS.title%", - "enablement": "!isCloud9" - }, - { - "command": "aws.toolkit.auth.help", - "title": "%AWS.generic.viewDocs%", - "category": "%AWS.title%", - "icon": "$(question)" - }, - { - "command": "aws.toolkit.createIssueOnGitHub", - "title": "%AWS.command.createIssueOnGitHub%", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ec2.openTerminal", - "title": "%AWS.command.ec2.openTerminal%", - "icon": "$(terminal-view-icon)", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ec2.openRemoteConnection", - "title": "%AWS.command.ec2.openRemoteConnection%", - "icon": "$(remote-explorer)", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ec2.startInstance", - "title": "%AWS.command.ec2.startInstance%", - "icon": "$(debug-start)", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ec2.stopInstance", - "title": "%AWS.command.ec2.stopInstance%", - "icon": "$(debug-stop)", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ec2.rebootInstance", - "title": "%AWS.command.ec2.rebootInstance%", - "icon": "$(debug-restart)", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ec2.copyInstanceId", - "title": "%AWS.command.ec2.copyInstanceId%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ecr.copyTagUri", - "title": "%AWS.command.ecr.copyTagUri%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ecr.deleteTag", - "title": "%AWS.command.ecr.deleteTag%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ecr.copyRepositoryUri", - "title": "%AWS.command.ecr.copyRepositoryUri%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ecr.createRepository", - "title": "%AWS.command.ecr.createRepository%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(add)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ecr.deleteRepository", - "title": "%AWS.command.ecr.deleteRepository%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.showRegion", - "title": "%AWS.command.showRegion%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.iot.createThing", - "title": "%AWS.command.iot.createThing%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(add)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.iot.deleteThing", - "title": "%AWS.generic.promptDelete%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.iot.createCert", - "title": "%AWS.command.iot.createCert%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(add)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.iot.deleteCert", - "title": "%AWS.generic.promptDelete%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.iot.attachCert", - "title": "%AWS.command.iot.attachCert%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(aws-generic-attach-file)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.iot.attachPolicy", - "title": "%AWS.command.iot.attachPolicy%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(aws-generic-attach-file)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.iot.activateCert", - "title": "%AWS.command.iot.activateCert%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.iot.deactivateCert", - "title": "%AWS.command.iot.deactivateCert%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.iot.revokeCert", - "title": "%AWS.command.iot.revokeCert%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.iot.createPolicy", - "title": "%AWS.command.iot.createPolicy%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(add)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.iot.deletePolicy", - "title": "%AWS.generic.promptDelete%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.iot.createPolicyVersion", - "title": "%AWS.command.iot.createPolicyVersion%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.iot.deletePolicyVersion", - "title": "%AWS.generic.promptDelete%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.iot.detachCert", - "title": "%AWS.command.iot.detachCert%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.iot.detachPolicy", - "title": "%AWS.command.iot.detachCert%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.iot.viewPolicyVersion", - "title": "%AWS.command.iot.viewPolicyVersion%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.iot.setDefaultPolicy", - "title": "%AWS.command.iot.setDefaultPolicy%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.iot.copyEndpoint", - "title": "%AWS.command.iot.copyEndpoint%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.redshift.editConnection", - "title": "Edit connection", - "category": "%AWS.title%" - }, - { - "command": "aws.redshift.deleteConnection", - "title": "Delete connection", - "category": "%AWS.title%" - }, - { - "command": "aws.s3.presignedURL", - "title": "%AWS.command.s3.presignedURL%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost" - }, - { - "command": "aws.s3.copyPath", - "title": "%AWS.command.s3.copyPath%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.s3.downloadFileAs", - "title": "%AWS.command.s3.downloadFileAs%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(cloud-download)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.s3.openFile", - "title": "%AWS.command.s3.openFile%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(open-preview)" - }, - { - "command": "aws.s3.editFile", - "title": "%AWS.command.s3.editFile%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(edit)" - }, - { - "command": "aws.s3.uploadFile", - "title": "%AWS.command.s3.uploadFile%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(cloud-upload)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.s3.uploadFileToParent", - "title": "%AWS.command.s3.uploadFileToParent%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.s3.createFolder", - "title": "%AWS.command.s3.createFolder%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(new-folder)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.s3.createBucket", - "title": "%AWS.command.s3.createBucket%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(aws-s3-create-bucket)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.s3.deleteBucket", - "title": "%AWS.generic.promptDelete%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.s3.deleteFile", - "title": "%AWS.generic.promptDelete%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.invokeLambda", - "title": "%AWS.command.invokeLambda%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "title": "%AWS.command.invokeLambda.cn%", - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.downloadLambda", - "title": "%AWS.command.downloadLambda%", - "category": "%AWS.title%", - "enablement": "viewItem == awsRegionFunctionNodeDownloadable", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.uploadLambda", - "title": "%AWS.command.uploadLambda%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.deleteLambda", - "title": "%AWS.generic.promptDelete%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.copyLambdaUrl", - "title": "%AWS.generic.copyUrl%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.deploySamApplication", - "title": "%AWS.command.deploySamApplication%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.toolkit.submitFeedback", - "title": "%AWS.command.submitFeedback%", - "enablement": "!aws.isWebExtHost", - "category": "%AWS.title%", - "icon": "$(comment)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.refreshAwsExplorer", - "title": "%AWS.command.refreshAwsExplorer%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "category": "%AWS.title%", - "icon": { - "dark": "resources/icons/vscode/dark/refresh.svg", - "light": "resources/icons/vscode/light/refresh.svg" - } - }, - { - "command": "aws.samcli.detect", - "title": "%AWS.command.samcli.detect%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.deleteCloudFormation", - "title": "%AWS.command.deleteCloudFormation%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.downloadStateMachineDefinition", - "title": "%AWS.command.downloadStateMachineDefinition%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.executeStateMachine", - "title": "%AWS.command.executeStateMachine%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.renderStateMachineGraph", - "title": "%AWS.command.renderStateMachineGraph%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.copyArn", - "title": "%AWS.command.copyArn%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.copyName", - "title": "%AWS.command.copyName%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.listCommands", - "title": "%AWS.command.listCommands%", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "title": "%AWS.command.listCommands.cn%", - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.viewSchemaItem", - "title": "%AWS.command.viewSchemaItem%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.searchSchema", - "title": "%AWS.command.searchSchema%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.searchSchemaPerRegistry", - "title": "%AWS.command.searchSchemaPerRegistry%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.downloadSchemaItemCode", - "title": "%AWS.command.downloadSchemaItemCode%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.toolkit.viewLogs", - "title": "%AWS.command.viewLogs%", - "category": "%AWS.title%" - }, - { - "command": "aws.toolkit.help", - "title": "%AWS.command.help%", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.toolkit.github", - "title": "%AWS.command.github%", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.quickStart", - "title": "%AWS.command.quickStart%", - "category": "%AWS.title%", - "enablement": "isCloud9", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.cdk.refresh", - "title": "%AWS.command.refreshCdkExplorer%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": { - "dark": "resources/icons/vscode/dark/refresh.svg", - "light": "resources/icons/vscode/light/refresh.svg" - }, - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.cdk.viewDocs", - "title": "%AWS.generic.viewDocs%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost" - }, - { - "command": "aws.stepfunctions.createStateMachineFromTemplate", - "title": "%AWS.command.stepFunctions.createStateMachineFromTemplate%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.stepfunctions.publishStateMachine", - "title": "%AWS.command.stepFunctions.publishStateMachine%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.previewStateMachine", - "title": "%AWS.command.stepFunctions.previewStateMachine%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(aws-stepfunctions-preview)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.cdk.renderStateMachineGraph", - "title": "%AWS.command.cdk.previewStateMachine%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "category": "AWS", - "icon": "$(aws-stepfunctions-preview)" - }, - { - "command": "aws.toolkit.aboutExtension", - "title": "%AWS.command.aboutToolkit%", - "category": "%AWS.title%" - }, - { - "command": "aws.cwl.viewLogStream", - "title": "%AWS.command.viewLogStream%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ssmDocument.createLocalDocument", - "title": "%AWS.command.ssmDocument.createLocalDocument%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ssmDocument.openLocalDocument", - "title": "%AWS.command.ssmDocument.openLocalDocument%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(cloud-download)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ssmDocument.openLocalDocumentJson", - "title": "%AWS.command.ssmDocument.openLocalDocumentJson%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ssmDocument.openLocalDocumentYaml", - "title": "%AWS.command.ssmDocument.openLocalDocumentYaml%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ssmDocument.deleteDocument", - "title": "%AWS.command.ssmDocument.deleteDocument%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ssmDocument.publishDocument", - "title": "%AWS.command.ssmDocument.publishDocument%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(cloud-upload)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ssmDocument.updateDocumentVersion", - "title": "%AWS.command.ssmDocument.updateDocumentVersion%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.copyLogResource", - "title": "%AWS.command.copyLogResource%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(files)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.cwl.searchLogGroup", - "title": "%AWS.command.cloudWatchLogs.searchLogGroup%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(search-view-icon)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.saveCurrentLogDataContent", - "title": "%AWS.command.saveCurrentLogDataContent%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(save)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.cwl.changeFilterPattern", - "title": "%AWS.command.cwl.changeFilterPattern%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(search-view-icon)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.cwl.changeTimeFilter", - "title": "%AWS.command.cwl.changeTimeFilter%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(calendar)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.addSamDebugConfig", - "title": "%AWS.command.addSamDebugConfig%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.toggleSamCodeLenses", - "title": "%AWS.command.toggleSamCodeLenses%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ecs.runCommandInContainer", - "title": "%AWS.ecs.runCommandInContainer%", - "category": "%AWS.title%", - "enablement": "viewItem == awsEcsContainerNodeExecEnabled", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ecs.openTaskInTerminal", - "title": "%AWS.ecs.openTaskInTerminal%", - "category": "%AWS.title%", - "enablement": "viewItem == awsEcsContainerNodeExecEnabled", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ecs.enableEcsExec", - "title": "%AWS.ecs.enableEcsExec%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ecs.viewDocumentation", - "title": "%AWS.generic.viewDocs%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.resources.copyIdentifier", - "title": "%AWS.command.resources.copyIdentifier%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.resources.openResourcePreview", - "title": "%AWS.generic.preview%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(open-preview)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.resources.createResource", - "title": "%AWS.generic.create%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(add)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.resources.deleteResource", - "title": "%AWS.generic.promptDelete%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.resources.updateResource", - "title": "%AWS.generic.promptUpdate%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(pencil)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.resources.updateResourceInline", - "title": "%AWS.generic.promptUpdate%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(pencil)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.resources.saveResource", - "title": "%AWS.generic.save%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(save)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.resources.closeResource", - "title": "%AWS.generic.close%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(close)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.resources.viewDocs", - "title": "%AWS.generic.viewDocs%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(book)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.resources.configure", - "title": "%AWS.command.resources.configure%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(gear)", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.apprunner.createService", - "title": "%AWS.command.apprunner.createService%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.ecs.disableEcsExec", - "title": "%AWS.ecs.disableEcsExec%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.apprunner.createServiceFromEcr", - "title": "%AWS.command.apprunner.createServiceFromEcr%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.apprunner.pauseService", - "title": "%AWS.command.apprunner.pauseService%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.apprunner.resumeService", - "title": "%AWS.command.apprunner.resumeService%", - "category": "AWS", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.apprunner.copyServiceUrl", - "title": "%AWS.command.apprunner.copyServiceUrl%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.apprunner.open", - "title": "%AWS.command.apprunner.open%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.apprunner.deleteService", - "title": "%AWS.generic.promptDelete%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.apprunner.startDeployment", - "title": "%AWS.command.apprunner.startDeployment%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.cloudFormation.newTemplate", - "title": "%AWS.command.cloudFormation.newTemplate%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.sam.newTemplate", - "title": "%AWS.command.sam.newTemplate%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.samcli.sync", - "title": "%AWS.command.samcli.sync%", - "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost" - }, - { - "command": "aws.toolkit.amazonq.learnMore", - "title": "%AWS.amazonq.learnMore%", - "category": "%AWS.title%" - }, - { - "command": "aws.toolkit.amazonq.extensionpage", - "title": "Open Amazon Q Extension", - "category": "%AWS.title%" - }, - { - "command": "aws.dev.openMenu", - "title": "Open Developer Menu", - "category": "AWS (Developer)", - "enablement": "aws.isDevMode" - }, - { - "command": "aws.dev.viewLogs", - "title": "Watch Logs", - "category": "AWS (Developer)" - }, - { - "command": "aws.openInApplicationComposerDialog", - "title": "%AWS.command.applicationComposer.openDialog%", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.openInApplicationComposer", - "title": "%AWS.command.applicationComposer.open%", - "category": "%AWS.title%", - "icon": { - "dark": "resources/icons/aws/applicationcomposer/icon-dark.svg", - "light": "resources/icons/aws/applicationcomposer/icon.svg" - }, - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.createNewThreatComposer", - "title": "%AWS.command.threatComposer.createNew%", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - }, - { - "command": "aws.newThreatComposerFile", - "title": "%AWS.command.threatComposer.newFile%", - "category": "%AWS.title%", - "cloud9": { - "cn": { - "category": "%AWS.title.cn%" - } - } - } - ], - "jsonValidation": [ - { - "fileMatch": ".aws/templates.json", - "url": "./dist/src/templates/templates.json" - }, - { - "fileMatch": "*ecs-task-def.json", - "url": "https://ecs-intellisense.s3-us-west-2.amazonaws.com/task-definition/schema.json" - } - ], - "languages": [ - { - "id": "asl", - "extensions": [ - ".asl.json", - ".asl" - ], - "aliases": [ - "Amazon States Language" - ] - }, - { - "id": "asl-yaml", - "aliases": [ - "Amazon States Language (YAML)" - ], - "extensions": [ - ".asl.yaml", - ".asl.yml" - ] - }, - { - "id": "ssm-json", - "extensions": [ - ".ssm.json" - ], - "aliases": [ - "AWS Systems Manager Document (JSON)" - ] - }, - { - "id": "ssm-yaml", - "extensions": [ - ".ssm.yaml", - ".ssm.yml" - ], - "aliases": [ - "AWS Systems Manager Document (YAML)" - ] - } - ], - "keybindings": [ - { - "command": "aws.previewStateMachine", - "key": "ctrl+shift+v", - "mac": "cmd+shift+v", - "when": "editorTextFocus && editorLangId == asl || editorTextFocus && editorLangId == asl-yaml" - } - ], - "grammars": [ - { - "language": "asl", - "scopeName": "source.asl", - "path": "./syntaxes/ASL.tmLanguage" - }, - { - "language": "asl-yaml", - "scopeName": "source.asl.yaml", - "path": "./syntaxes/asl-yaml.tmLanguage.json" - }, - { - "language": "ssm-json", - "scopeName": "source.ssmjson", - "path": "./syntaxes/SSMJSON.tmLanguage" - }, - { - "language": "ssm-yaml", - "scopeName": "source.ssmyaml", - "path": "./syntaxes/SSMYAML.tmLanguage" - } - ], - "resourceLabelFormatters": [ - { - "scheme": "aws-cwl", - "formatting": { - "label": "${path}", - "separator": "/" - } - }, - { - "scheme": "s3*", - "formatting": { - "label": "[S3] ${path}", - "separator": "/" - } - } - ], - "walkthroughs": [], - "icons": { - "aws-amazonq-q-gradient": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1aa" - } - }, - "aws-amazonq-q-squid-ink": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1ab" - } - }, - "aws-amazonq-q-white": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1ac" - } - }, - "aws-amazonq-transform-arrow-dark": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1ad" - } - }, - "aws-amazonq-transform-arrow-light": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1ae" - } - }, - "aws-amazonq-transform-default-dark": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1af" - } - }, - "aws-amazonq-transform-default-light": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1b0" - } - }, - "aws-amazonq-transform-dependencies-dark": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1b1" - } - }, - "aws-amazonq-transform-dependencies-light": { - "description": "AWS Contributed Icon", - "default": { - "fontPath": "./resources/fonts/aws-toolkit-icons.woff", - "fontCharacter": "\\f1b2" + }, + "aws-amazonq-transform-dependencies-light": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1b2" } }, "aws-amazonq-transform-file-dark": { @@ -3945,33 +1014,6 @@ "fontCharacter": "\\f1da" } } - }, - "notebooks": [ - { - "type": "aws-redshift-sql-notebook", - "displayName": "Redshift SQL notebook", - "selector": [ - { - "filenamePattern": "*.redshiftnb" - } - ] - } - ], - "customEditors": [ - { - "viewType": "threatComposer.tc.json", - "displayName": "%AWS.threatComposer.title%", - "selector": [ - { - "filenamePattern": "*.tc.json" - } - ] - } - ], - "configurationDefaults": { - "workbench.editorAssociations": { - "{git,gitlens,conflictResolution,vscode-local-history}:/**/*.tc.json": "default" - } } }, "devDependencies": {}, From 996362c7b1de889cc0b36d9ac081b5708597fed4 Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 5 Sep 2024 16:02:15 -0400 Subject: [PATCH 155/172] minimize code dupe --- .../apprunner/explorer/apprunnerNode.ts | 24 ++++----------- .../explorer/apprunnerServiceNode.ts | 4 +-- .../ec2/explorer/ec2InstanceNode.ts | 2 +- .../awsService/ec2/explorer/ec2ParentNode.ts | 29 ++----------------- .../core/src/shared/utilities/pollingSet.ts | 24 ++++++++++++++- .../explorer/apprunnerServiceNode.test.ts | 1 + .../ec2/explorer/ec2ParentNode.test.ts | 15 +++++----- 7 files changed, 43 insertions(+), 56 deletions(-) diff --git a/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts b/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts index 03555255163..889f94a51cc 100644 --- a/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts +++ b/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts @@ -11,15 +11,13 @@ import * as nls from 'vscode-nls' import { AppRunnerClient } from '../../../shared/clients/apprunnerClient' import { getPaginatedAwsCallIter } from '../../../shared/utilities/collectionUtils' import { AppRunner } from 'aws-sdk' -import globals from '../../../shared/extensionGlobals' import { PollingSet } from '../../../shared/utilities/pollingSet' const localize = nls.loadMessageBundle() -const pollingInterval = 20000 export class AppRunnerNode extends AWSTreeNodeBase { private readonly serviceNodes: Map = new Map() - private readonly pollingSet: PollingSet = new PollingSet(pollingInterval) + private readonly pollingSet: PollingSet = new PollingSet(20000, this.refresh.bind(this)) public constructor( public override readonly regionCode: string, @@ -80,7 +78,7 @@ export class AppRunnerNode extends AWSTreeNodeBase { this.serviceNodes.get(summary.ServiceArn)!.update(summary) if (summary.Status !== 'OPERATION_IN_PROGRESS') { this.pollingSet.delete(summary.ServiceArn) - this.clearPollTimer() + this.pollingSet.clearTimer() } } else { this.serviceNodes.set(summary.ServiceArn, new AppRunnerServiceNode(this, this.client, summary)) @@ -92,24 +90,14 @@ export class AppRunnerNode extends AWSTreeNodeBase { deletedNodeArns.forEach(this.deleteNode.bind(this)) } - // TODO: generalize this method. - private clearPollTimer(): void { - if (this.pollingSet.isEmpty() && this.pollingSet.hasTimer()) { - globals.clock.clearInterval(this.pollingSet.pollTimer) - this.pollingSet.pollTimer = undefined - } - } - - public startPolling(id: string): void { - this.pollingSet.add(id) - this.pollingSet.pollTimer = - this.pollingSet.pollTimer ?? globals.clock.setInterval(this.refresh.bind(this), pollingInterval) + public startPollingNode(id: string): void { + this.pollingSet.start(id) } - public stopPolling(id: string): void { + public stopPollingNode(id: string): void { this.pollingSet.delete(id) this.serviceNodes.get(id)?.refresh() - this.clearPollTimer() + this.pollingSet.clearTimer() } public deleteNode(id: string): void { diff --git a/packages/core/src/awsService/apprunner/explorer/apprunnerServiceNode.ts b/packages/core/src/awsService/apprunner/explorer/apprunnerServiceNode.ts index 75a8a30b49b..e5c28404d3c 100644 --- a/packages/core/src/awsService/apprunner/explorer/apprunnerServiceNode.ts +++ b/packages/core/src/awsService/apprunner/explorer/apprunnerServiceNode.ts @@ -107,12 +107,12 @@ export class AppRunnerServiceNode extends CloudWatchLogsBase implements AWSResou } if (this._info.Status === 'OPERATION_IN_PROGRESS') { - this.parent.startPolling(this._info.ServiceArn) + this.parent.startPollingNode(this._info.ServiceArn) } else if (this.currentOperation.Type !== undefined) { this.currentOperation.Id = undefined this.currentOperation.Type = undefined this.setLabel() - this.parent.stopPolling(this._info.ServiceArn) + this.parent.stopPollingNode(this._info.ServiceArn) } } diff --git a/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts b/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts index 72b0b4bb305..7fbbad6d62f 100644 --- a/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts +++ b/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts @@ -40,7 +40,7 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode this.tooltip = `${this.name}\n${this.InstanceId}\n${this.instance.status}\n${this.arn}` if (this.isPending()) { - this.parent.startPolling(this.InstanceId) + this.parent.pollingSet.start(this.InstanceId) } } diff --git a/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts b/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts index 4ceb9451197..7751669a4d9 100644 --- a/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts +++ b/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' -import globals from '../../../shared/extensionGlobals' import { AWSTreeNodeBase } from '../../../shared/treeview/nodes/awsTreeNodeBase' import { makeChildrenNodes } from '../../../shared/treeview/utils' import { PlaceholderNode } from '../../../shared/treeview/nodes/placeholderNode' @@ -21,9 +20,7 @@ export class Ec2ParentNode extends AWSTreeNodeBase { protected readonly placeHolderMessage = '[No EC2 Instances Found]' protected ec2InstanceNodes: Map public override readonly contextValue: string = parentContextValue - // private readonly pollingNodes: Set = new Set() - // private pollTimer?: NodeJS.Timeout - public readonly pollingSet: PollingSet = new PollingSet(pollingInterval) + public readonly pollingSet: PollingSet = new PollingSet(pollingInterval, this.updatePendingNodes.bind(this)) public constructor( public override readonly regionCode: string, @@ -57,17 +54,7 @@ export class Ec2ParentNode extends AWSTreeNodeBase { ) } - public isPolling(): boolean { - return !this.pollingSet.isEmpty() - } - - public startPolling(newNode: string) { - this.pollingSet.add(newNode) - this.pollingSet.pollTimer = - this.pollingSet.pollTimer ?? globals.clock.setInterval(this.updatePollingNodes.bind(this), pollingInterval) - } - - private checkForPendingNodes() { + private updatePendingNodes() { this.pollingSet.pollingNodes.forEach(async (instanceId) => { const childNode = this.ec2InstanceNodes.get(instanceId)! await childNode.updateStatus() @@ -78,18 +65,6 @@ export class Ec2ParentNode extends AWSTreeNodeBase { }) } - public updatePollingNodes() { - this.checkForPendingNodes() - if (!this.isPolling()) { - this.clearPollTimer() - } - } - - public clearPollTimer() { - globals.clock.clearInterval(this.pollingSet.pollTimer!) - this.pollingSet.pollTimer = undefined - } - public async clearChildren() { this.ec2InstanceNodes = new Map() } diff --git a/packages/core/src/shared/utilities/pollingSet.ts b/packages/core/src/shared/utilities/pollingSet.ts index 38c51fa4864..f12fd055556 100644 --- a/packages/core/src/shared/utilities/pollingSet.ts +++ b/packages/core/src/shared/utilities/pollingSet.ts @@ -9,7 +9,10 @@ export class PollingSet { public readonly pollingNodes: Set public pollTimer?: NodeJS.Timeout - public constructor(private readonly interval: number) { + public constructor( + private readonly interval: number, + private readonly action: () => void + ) { this.pollingNodes = new Set() } @@ -32,4 +35,23 @@ export class PollingSet { public hasTimer(): boolean { return this.pollTimer != undefined } + + public clearTimer(): void { + if (this.isEmpty() && this.hasTimer()) { + globals.clock.clearInterval(this.pollTimer) + this.pollTimer = undefined + } + } + + public start(id: T): void { + this.add(id) + this.pollTimer = + this.pollTimer ?? + globals.clock.setInterval(() => { + this.action() + if (this.isEmpty()) { + this.clearTimer() + } + }, this.interval) + } } diff --git a/packages/core/src/test/awsService/apprunner/explorer/apprunnerServiceNode.test.ts b/packages/core/src/test/awsService/apprunner/explorer/apprunnerServiceNode.test.ts index 5c1b992e390..e0109e21b21 100644 --- a/packages/core/src/test/awsService/apprunner/explorer/apprunnerServiceNode.test.ts +++ b/packages/core/src/test/awsService/apprunner/explorer/apprunnerServiceNode.test.ts @@ -14,6 +14,7 @@ import { asyncGenerator } from '../../../../shared/utilities/collectionUtils' import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBase' import { stub } from '../../../utilities/stubber' import { getLabel } from '../../../../shared/treeview/utils' +import { PollingSet } from '../../../../shared/utilities/pollingSet' describe('AppRunnerServiceNode', function () { let mockApprunnerClient: ReturnType> diff --git a/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts b/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts index 789e99ab4eb..3c00fcc3a5d 100644 --- a/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts +++ b/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts @@ -17,6 +17,7 @@ import { EC2 } from 'aws-sdk' import { AsyncCollection } from '../../../../shared/utilities/asyncCollection' import * as FakeTimers from '@sinonjs/fake-timers' import { installFakeClock } from '../../../testUtil' +import { PollingSet } from '../../../../shared/utilities/pollingSet' describe('ec2ParentNode', function () { let testNode: Ec2ParentNode @@ -44,7 +45,7 @@ describe('ec2ParentNode', function () { client = new Ec2Client(testRegion) clock = installFakeClock() refreshStub = sinon.stub(Ec2InstanceNode.prototype, 'refreshNode') - clearTimerStub = sinon.stub(Ec2ParentNode.prototype, 'clearPollTimer') + clearTimerStub = sinon.stub(PollingSet.prototype, 'clearTimer') defaultInstances = [ { name: 'firstOne', InstanceId: '0' }, { name: 'secondOne', InstanceId: '1' }, @@ -73,6 +74,7 @@ describe('ec2ParentNode', function () { testNode = new Ec2ParentNode(testRegion, testPartition, client) refreshStub.resetHistory() + clearTimerStub.resetHistory() }) afterEach(function () { @@ -146,7 +148,7 @@ describe('ec2ParentNode', function () { }) it('is not polling on initialization', async function () { - assert.strictEqual(testNode.isPolling(), false) + assert.strictEqual(testNode.pollingSet.isEmpty(), true) }) it('adds pending nodes to the polling nodes set', async function () { @@ -195,16 +197,15 @@ describe('ec2ParentNode', function () { { name: 'secondOne', InstanceId: '1', status: 'stopped' }, { name: 'thirdOne', InstanceId: '2', status: 'running' }, ] - getInstanceStub.resolves(mapToInstanceCollection(instances)) await testNode.updateChildren() - - assert.strictEqual(testNode.isPolling(), true) + sinon.assert.notCalled(clearTimerStub) + assert.strictEqual(testNode.pollingSet.isEmpty(), false) testNode.pollingSet.pollingNodes.delete('0') await clock.tickAsync(6000) - assert.strictEqual(testNode.isPolling(), false) - sinon.assert.calledOn(clearTimerStub, testNode) + assert.strictEqual(testNode.pollingSet.isEmpty(), true) + sinon.assert.callCount(clearTimerStub, instances.length) getInstanceStub.restore() }) }) From 1e6cdcfaccaf60ba44a47a3f9a165b15120d1b3b Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 5 Sep 2024 16:05:51 -0400 Subject: [PATCH 156/172] cleanup --- packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts | 4 +--- .../apprunner/explorer/apprunnerServiceNode.test.ts | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts b/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts index 7751669a4d9..cf81d19855f 100644 --- a/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts +++ b/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts @@ -14,13 +14,11 @@ import { PollingSet } from '../../../shared/utilities/pollingSet' export const parentContextValue = 'awsEc2ParentNode' export type Ec2Node = Ec2InstanceNode | Ec2ParentNode -const pollingInterval = 5000 - export class Ec2ParentNode extends AWSTreeNodeBase { protected readonly placeHolderMessage = '[No EC2 Instances Found]' protected ec2InstanceNodes: Map public override readonly contextValue: string = parentContextValue - public readonly pollingSet: PollingSet = new PollingSet(pollingInterval, this.updatePendingNodes.bind(this)) + public readonly pollingSet: PollingSet = new PollingSet(5000, this.updatePendingNodes.bind(this)) public constructor( public override readonly regionCode: string, diff --git a/packages/core/src/test/awsService/apprunner/explorer/apprunnerServiceNode.test.ts b/packages/core/src/test/awsService/apprunner/explorer/apprunnerServiceNode.test.ts index e0109e21b21..5c1b992e390 100644 --- a/packages/core/src/test/awsService/apprunner/explorer/apprunnerServiceNode.test.ts +++ b/packages/core/src/test/awsService/apprunner/explorer/apprunnerServiceNode.test.ts @@ -14,7 +14,6 @@ import { asyncGenerator } from '../../../../shared/utilities/collectionUtils' import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBase' import { stub } from '../../../utilities/stubber' import { getLabel } from '../../../../shared/treeview/utils' -import { PollingSet } from '../../../../shared/utilities/pollingSet' describe('AppRunnerServiceNode', function () { let mockApprunnerClient: ReturnType> From 4e9766656a0e1e7c335cf93749938f2227cf8246 Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 5 Sep 2024 16:55:23 -0400 Subject: [PATCH 157/172] generalize further, extend tests --- .../awsService/ec2/explorer/ec2ParentNode.ts | 4 +- .../core/src/shared/utilities/pollingSet.ts | 25 ++--- .../ec2/explorer/ec2ParentNode.test.ts | 30 +----- .../test/shared/utilities/pollingSet.test.ts | 102 ++++++++++++++++++ 4 files changed, 112 insertions(+), 49 deletions(-) create mode 100644 packages/core/src/test/shared/utilities/pollingSet.test.ts diff --git a/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts b/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts index cf81d19855f..70f6ea97d4f 100644 --- a/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts +++ b/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts @@ -53,11 +53,11 @@ export class Ec2ParentNode extends AWSTreeNodeBase { } private updatePendingNodes() { - this.pollingSet.pollingNodes.forEach(async (instanceId) => { + this.pollingSet.forEach(async (instanceId) => { const childNode = this.ec2InstanceNodes.get(instanceId)! await childNode.updateStatus() if (!childNode.isPending()) { - this.pollingSet.pollingNodes.delete(instanceId) + this.pollingSet.delete(instanceId) childNode.refreshNode() } }) diff --git a/packages/core/src/shared/utilities/pollingSet.ts b/packages/core/src/shared/utilities/pollingSet.ts index f12fd055556..8379a26a1be 100644 --- a/packages/core/src/shared/utilities/pollingSet.ts +++ b/packages/core/src/shared/utilities/pollingSet.ts @@ -5,31 +5,18 @@ import { globals } from '..' -export class PollingSet { - public readonly pollingNodes: Set +export class PollingSet extends Set { public pollTimer?: NodeJS.Timeout public constructor( private readonly interval: number, private readonly action: () => void ) { - this.pollingNodes = new Set() + super() } - public add(id: T): void { - this.pollingNodes.add(id) - } - - public delete(id: T): void { - this.pollingNodes.delete(id) - } - - public size(): number { - return this.pollingNodes.size - } - - public isEmpty(): boolean { - return this.pollingNodes.size == 0 + public isActive(): boolean { + return this.size != 0 } public hasTimer(): boolean { @@ -37,7 +24,7 @@ export class PollingSet { } public clearTimer(): void { - if (this.isEmpty() && this.hasTimer()) { + if (!this.isActive() && this.hasTimer()) { globals.clock.clearInterval(this.pollTimer) this.pollTimer = undefined } @@ -49,7 +36,7 @@ export class PollingSet { this.pollTimer ?? globals.clock.setInterval(() => { this.action() - if (this.isEmpty()) { + if (!this.isActive()) { this.clearTimer() } }, this.interval) diff --git a/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts b/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts index 3c00fcc3a5d..6e2b3e6af8b 100644 --- a/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts +++ b/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts @@ -52,10 +52,6 @@ describe('ec2ParentNode', function () { ] }) - after(function () { - sinon.restore() - }) - beforeEach(function () { getInstanceStub = sinon.stub(Ec2Client.prototype, 'getInstances') defaultInstances = [ @@ -147,10 +143,6 @@ describe('ec2ParentNode', function () { getInstanceStub.restore() }) - it('is not polling on initialization', async function () { - assert.strictEqual(testNode.pollingSet.isEmpty(), true) - }) - it('adds pending nodes to the polling nodes set', async function () { const instances = [ { name: 'firstOne', InstanceId: '0', status: 'pending' }, @@ -161,7 +153,7 @@ describe('ec2ParentNode', function () { getInstanceStub.resolves(mapToInstanceCollection(instances)) await testNode.updateChildren() - assert.strictEqual(testNode.pollingSet.pollingNodes.size, 1) + assert.strictEqual(testNode.pollingSet.size, 1) getInstanceStub.restore() }) @@ -185,27 +177,9 @@ describe('ec2ParentNode', function () { it('does refresh explorer when timer goes and status changed', async function () { sinon.assert.notCalled(refreshStub) const statusUpdateStub = sinon.stub(Ec2Client.prototype, 'getInstanceStatus').resolves('running') - testNode.pollingSet.pollingNodes.add('0') + testNode.pollingSet.add('0') await clock.tickAsync(6000) sinon.assert.called(refreshStub) statusUpdateStub.restore() }) - - it('stops timer once polling nodes are empty', async function () { - const instances = [ - { name: 'firstOne', InstanceId: '0', status: 'pending' }, - { name: 'secondOne', InstanceId: '1', status: 'stopped' }, - { name: 'thirdOne', InstanceId: '2', status: 'running' }, - ] - getInstanceStub.resolves(mapToInstanceCollection(instances)) - - await testNode.updateChildren() - sinon.assert.notCalled(clearTimerStub) - assert.strictEqual(testNode.pollingSet.isEmpty(), false) - testNode.pollingSet.pollingNodes.delete('0') - await clock.tickAsync(6000) - assert.strictEqual(testNode.pollingSet.isEmpty(), true) - sinon.assert.callCount(clearTimerStub, instances.length) - getInstanceStub.restore() - }) }) diff --git a/packages/core/src/test/shared/utilities/pollingSet.test.ts b/packages/core/src/test/shared/utilities/pollingSet.test.ts new file mode 100644 index 00000000000..71a83059dff --- /dev/null +++ b/packages/core/src/test/shared/utilities/pollingSet.test.ts @@ -0,0 +1,102 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import { PollingSet } from '../../../shared/utilities/pollingSet' +import * as FakeTimers from '@sinonjs/fake-timers' +import { installFakeClock } from '../../testUtil' + +describe('pollingSet', function () { + let pollingSet: PollingSet + let clock: FakeTimers.InstalledClock + before(function () { + clock = installFakeClock() + }) + + after(function () {}) + + beforeEach(function () {}) + + afterEach(function () { + if (pollingSet) { + pollingSet.clear() + pollingSet.clearTimer() + } + }) + + after(function () { + sinon.restore() + clock.uninstall() + }) + + it('inherits basic set properties', function () { + let pollingSet = new PollingSet(0, () => {}) + let item1 = { id: 1 } + let item2 = { id: 2 } + + pollingSet.add(item1) + assert.strictEqual(pollingSet.size, 1) + assert(pollingSet.has(item1)) + assert.strictEqual(pollingSet.has(item2), false) + + pollingSet.delete(item1) + assert.strictEqual(pollingSet.size, 0) + assert.strictEqual(pollingSet.has(item1), false) + }) + + it('does not poll on initialization', async function () { + let pollingSet = new PollingSet(0, () => {}) + assert.strictEqual(pollingSet.isActive(), false) + }) + + it('does not trigger prematurely', async function () { + let action = sinon.spy() + pollingSet = new PollingSet(10, action) + sinon.assert.notCalled(action) + pollingSet.start('item') + + await clock.tickAsync(9) + sinon.assert.notCalled(action) + await clock.tickAsync(1) + sinon.assert.calledOnce(action) + }) + + it('stops timer once polling set is empty', async function () { + let pollingSet = new PollingSet(10, () => {}) + pollingSet.start('1') + pollingSet.add('2') + + let clearStub = sinon.stub(pollingSet, 'clearTimer') + sinon.assert.notCalled(clearStub) + assert.strictEqual(pollingSet.isActive(), true) + + pollingSet.delete('1') + + sinon.assert.notCalled(clearStub) + assert.strictEqual(pollingSet.isActive(), true) + + pollingSet.delete('2') + + await clock.tick(20) + + assert.strictEqual(pollingSet.isActive(), false) + sinon.assert.callCount(clearStub, 2) + clearStub.restore() + }) + + it('runs action once per interval', async function () { + let action = sinon.spy() + pollingSet = new PollingSet(10, action) + pollingSet.start('1') + pollingSet.add('2') + + sinon.assert.callCount(action, 0) + await clock.tick(11) + sinon.assert.callCount(action, 1) + await clock.tick(22) + sinon.assert.callCount(action, 3) + }) +}) From 9d9fa8d315b19e8d0a425198b375f4cc248282bc Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 5 Sep 2024 17:17:14 -0400 Subject: [PATCH 158/172] fix linting problems --- .../awsService/ec2/explorer/ec2InstanceNode.ts | 4 ++-- .../src/awsService/ec2/explorer/ec2ParentNode.ts | 2 +- packages/core/src/shared/utilities/pollingSet.ts | 4 ++-- .../src/test/shared/utilities/pollingSet.test.ts | 16 ++++++++-------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts b/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts index 7fbbad6d62f..262e4b056a8 100644 --- a/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts +++ b/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts @@ -45,7 +45,7 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode } public isPending(): boolean { - return this.getStatus() != 'running' && this.getStatus() != 'stopped' + return this.getStatus() !== 'running' && this.getStatus() !== 'stopped' } public async updateStatus() { @@ -96,6 +96,6 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode public async refreshNode(): Promise { await this.updateStatus() - vscode.commands.executeCommand('aws.refreshAwsExplorerNode', this) + await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', this) } } diff --git a/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts b/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts index 70f6ea97d4f..b01a310323f 100644 --- a/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts +++ b/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts @@ -58,7 +58,7 @@ export class Ec2ParentNode extends AWSTreeNodeBase { await childNode.updateStatus() if (!childNode.isPending()) { this.pollingSet.delete(instanceId) - childNode.refreshNode() + await childNode.refreshNode() } }) } diff --git a/packages/core/src/shared/utilities/pollingSet.ts b/packages/core/src/shared/utilities/pollingSet.ts index 8379a26a1be..7bfa58e8865 100644 --- a/packages/core/src/shared/utilities/pollingSet.ts +++ b/packages/core/src/shared/utilities/pollingSet.ts @@ -16,11 +16,11 @@ export class PollingSet extends Set { } public isActive(): boolean { - return this.size != 0 + return this.size !== 0 } public hasTimer(): boolean { - return this.pollTimer != undefined + return this.pollTimer !== undefined } public clearTimer(): void { diff --git a/packages/core/src/test/shared/utilities/pollingSet.test.ts b/packages/core/src/test/shared/utilities/pollingSet.test.ts index 71a83059dff..d2f509d8b7c 100644 --- a/packages/core/src/test/shared/utilities/pollingSet.test.ts +++ b/packages/core/src/test/shared/utilities/pollingSet.test.ts @@ -33,9 +33,9 @@ describe('pollingSet', function () { }) it('inherits basic set properties', function () { - let pollingSet = new PollingSet(0, () => {}) - let item1 = { id: 1 } - let item2 = { id: 2 } + const pollingSet = new PollingSet(0, () => {}) + const item1 = { id: 1 } + const item2 = { id: 2 } pollingSet.add(item1) assert.strictEqual(pollingSet.size, 1) @@ -48,12 +48,12 @@ describe('pollingSet', function () { }) it('does not poll on initialization', async function () { - let pollingSet = new PollingSet(0, () => {}) + const pollingSet = new PollingSet(0, () => {}) assert.strictEqual(pollingSet.isActive(), false) }) it('does not trigger prematurely', async function () { - let action = sinon.spy() + const action = sinon.spy() pollingSet = new PollingSet(10, action) sinon.assert.notCalled(action) pollingSet.start('item') @@ -80,7 +80,7 @@ describe('pollingSet', function () { pollingSet.delete('2') - await clock.tick(20) + await clock.tickAsync(20) assert.strictEqual(pollingSet.isActive(), false) sinon.assert.callCount(clearStub, 2) @@ -94,9 +94,9 @@ describe('pollingSet', function () { pollingSet.add('2') sinon.assert.callCount(action, 0) - await clock.tick(11) + await clock.tickAsync(11) sinon.assert.callCount(action, 1) - await clock.tick(22) + await clock.tickAsync(22) sinon.assert.callCount(action, 3) }) }) From 79b8f816bae94c4e0139f9a5526513e5e4a4e0be Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 6 Sep 2024 09:47:17 -0400 Subject: [PATCH 159/172] fix linting 2 --- packages/core/src/test/shared/utilities/pollingSet.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/test/shared/utilities/pollingSet.test.ts b/packages/core/src/test/shared/utilities/pollingSet.test.ts index d2f509d8b7c..ff92474dbbf 100644 --- a/packages/core/src/test/shared/utilities/pollingSet.test.ts +++ b/packages/core/src/test/shared/utilities/pollingSet.test.ts @@ -65,11 +65,11 @@ describe('pollingSet', function () { }) it('stops timer once polling set is empty', async function () { - let pollingSet = new PollingSet(10, () => {}) + const pollingSet = new PollingSet(10, () => {}) pollingSet.start('1') pollingSet.add('2') - let clearStub = sinon.stub(pollingSet, 'clearTimer') + const clearStub = sinon.stub(pollingSet, 'clearTimer') sinon.assert.notCalled(clearStub) assert.strictEqual(pollingSet.isActive(), true) @@ -88,7 +88,7 @@ describe('pollingSet', function () { }) it('runs action once per interval', async function () { - let action = sinon.spy() + const action = sinon.spy() pollingSet = new PollingSet(10, action) pollingSet.start('1') pollingSet.add('2') From 955308cfa2370026271a7db1e8ffcea52042261e Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 6 Sep 2024 11:11:52 -0400 Subject: [PATCH 160/172] clean up inlining --- packages/core/src/shared/utilities/pollingSet.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/core/src/shared/utilities/pollingSet.ts b/packages/core/src/shared/utilities/pollingSet.ts index 7bfa58e8865..16a97b62e3d 100644 --- a/packages/core/src/shared/utilities/pollingSet.ts +++ b/packages/core/src/shared/utilities/pollingSet.ts @@ -30,15 +30,15 @@ export class PollingSet extends Set { } } + private poll() { + this.action() + if (!this.isActive()) { + this.clearTimer() + } + } + public start(id: T): void { this.add(id) - this.pollTimer = - this.pollTimer ?? - globals.clock.setInterval(() => { - this.action() - if (!this.isActive()) { - this.clearTimer() - } - }, this.interval) + this.pollTimer = this.pollTimer ?? globals.clock.setInterval(() => this.poll(), this.interval) } } From b77dc8d39bd72bb826d9e7fa349e60a16c934795 Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 12 Sep 2024 12:45:35 -0400 Subject: [PATCH 161/172] move test files over --- .../ec2/explorer/ec2InstanceNode.test.ts | 7 +- .../ec2/explorer/ec2ParentNode.test.ts | 120 ++++++++++++++---- .../test/shared/utilities/pollingSet.test.ts | 102 +++++++++++++++ 3 files changed, 203 insertions(+), 26 deletions(-) create mode 100644 packages/core/src/test/shared/utilities/pollingSet.test.ts diff --git a/packages/core/src/test/awsService/ec2/explorer/ec2InstanceNode.test.ts b/packages/core/src/test/awsService/ec2/explorer/ec2InstanceNode.test.ts index a3e20ca3576..0a335acaf0a 100644 --- a/packages/core/src/test/awsService/ec2/explorer/ec2InstanceNode.test.ts +++ b/packages/core/src/test/awsService/ec2/explorer/ec2InstanceNode.test.ts @@ -83,9 +83,10 @@ describe('ec2InstanceNode', function () { assert.strictEqual(testNode.contextValue, Ec2InstancePendingContext) }) - it('updates label with new instance', async function () { - const newIdInstance = { ...testInstance, InstanceId: 'testId2' } + it('updates status with new instance', async function () { + const newStatus = 'pending' + const newIdInstance = { ...testInstance, InstanceId: 'testId2', status: newStatus } testNode.updateInstance(newIdInstance) - assert.strictEqual(testNode.label, `${getNameOfInstance(newIdInstance)} (${newIdInstance.InstanceId})`) + assert.strictEqual(testNode.getStatus(), newStatus) }) }) diff --git a/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts b/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts index 3ce43a260ee..6e2b3e6af8b 100644 --- a/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts +++ b/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts @@ -15,34 +15,53 @@ import { import { Ec2InstanceNode } from '../../../../awsService/ec2/explorer/ec2InstanceNode' import { EC2 } from 'aws-sdk' import { AsyncCollection } from '../../../../shared/utilities/asyncCollection' +import * as FakeTimers from '@sinonjs/fake-timers' +import { installFakeClock } from '../../../testUtil' +import { PollingSet } from '../../../../shared/utilities/pollingSet' describe('ec2ParentNode', function () { let testNode: Ec2ParentNode - let instances: Ec2Instance[] + let defaultInstances: Ec2Instance[] let client: Ec2Client let getInstanceStub: sinon.SinonStub<[filters?: EC2.Filter[] | undefined], Promise>> + let clock: FakeTimers.InstalledClock + let refreshStub: sinon.SinonStub<[], Promise> + let clearTimerStub: sinon.SinonStub<[], void> const testRegion = 'testRegion' const testPartition = 'testPartition' + function mapToInstanceCollection(instances: Ec2Instance[]) { + return intoCollection( + instances.map((instance) => ({ + InstanceId: instance.InstanceId, + status: instance.status, + Tags: [{ Key: 'Name', Value: instance.name }], + })) + ) + } + before(function () { client = new Ec2Client(testRegion) - }) - - after(function () { - sinon.restore() + clock = installFakeClock() + refreshStub = sinon.stub(Ec2InstanceNode.prototype, 'refreshNode') + clearTimerStub = sinon.stub(PollingSet.prototype, 'clearTimer') + defaultInstances = [ + { name: 'firstOne', InstanceId: '0' }, + { name: 'secondOne', InstanceId: '1' }, + ] }) beforeEach(function () { getInstanceStub = sinon.stub(Ec2Client.prototype, 'getInstances') - instances = [ - { name: 'firstOne', InstanceId: '0' }, - { name: 'secondOne', InstanceId: '1' }, + defaultInstances = [ + { name: 'firstOne', InstanceId: '0', status: 'running' }, + { name: 'secondOne', InstanceId: '1', status: 'stopped' }, ] getInstanceStub.callsFake(async () => intoCollection( - instances.map((instance) => ({ + defaultInstances.map((instance) => ({ InstanceId: instance.InstanceId, Tags: [{ Key: 'Name', Value: instance.name }], })) @@ -50,45 +69,57 @@ describe('ec2ParentNode', function () { ) testNode = new Ec2ParentNode(testRegion, testPartition, client) + refreshStub.resetHistory() + clearTimerStub.resetHistory() }) afterEach(function () { getInstanceStub.restore() }) + after(function () { + clock.uninstall() + sinon.restore() + }) + it('returns placeholder node if no children are present', async function () { - instances = [] + getInstanceStub.resolves(mapToInstanceCollection([])) const childNodes = await testNode.getChildren() - assertNodeListOnlyHasPlaceholderNode(childNodes) + getInstanceStub.restore() }) it('has instance child nodes', async function () { + getInstanceStub.resolves(mapToInstanceCollection(defaultInstances)) const childNodes = await testNode.getChildren() - assert.strictEqual(childNodes.length, instances.length, 'Unexpected child count') + assert.strictEqual(childNodes.length, defaultInstances.length, 'Unexpected child count') childNodes.forEach((node) => assert.ok(node instanceof Ec2InstanceNode, 'Expected child node to be Ec2InstanceNode') ) + getInstanceStub.restore() }) it('sorts child nodes', async function () { const sortedText = ['aa', 'ab', 'bb', 'bc', 'cc', 'cd'] - instances = [ - { name: 'ab', InstanceId: '0' }, - { name: 'bb', InstanceId: '1' }, - { name: 'bc', InstanceId: '2' }, - { name: 'aa', InstanceId: '3' }, - { name: 'cc', InstanceId: '4' }, - { name: 'cd', InstanceId: '5' }, + const instances = [ + { name: 'ab', InstanceId: '0', status: 'running' }, + { name: 'bb', InstanceId: '1', status: 'running' }, + { name: 'bc', InstanceId: '2', status: 'running' }, + { name: 'aa', InstanceId: '3', status: 'running' }, + { name: 'cc', InstanceId: '4', status: 'running' }, + { name: 'cd', InstanceId: '5', status: 'running' }, ] + getInstanceStub.resolves(mapToInstanceCollection(instances)) + const childNodes = await testNode.getChildren() const actualChildOrder = childNodes.map((node) => (node instanceof Ec2InstanceNode ? node.name : undefined)) assert.deepStrictEqual(actualChildOrder, sortedText, 'Unexpected child sort order') + getInstanceStub.restore() }) it('has an error node for a child if an error happens during loading', async function () { @@ -99,13 +130,56 @@ describe('ec2ParentNode', function () { }) it('is able to handle children with duplicate names', async function () { - instances = [ - { name: 'firstOne', InstanceId: '0' }, - { name: 'secondOne', InstanceId: '1' }, - { name: 'firstOne', InstanceId: '2' }, + const instances = [ + { name: 'firstOne', InstanceId: '0', status: 'running' }, + { name: 'secondOne', InstanceId: '1', status: 'running' }, + { name: 'firstOne', InstanceId: '2', status: 'running' }, ] + getInstanceStub.resolves(mapToInstanceCollection(instances)) + const childNodes = await testNode.getChildren() assert.strictEqual(childNodes.length, instances.length, 'Unexpected child count') + getInstanceStub.restore() + }) + + it('adds pending nodes to the polling nodes set', async function () { + const instances = [ + { name: 'firstOne', InstanceId: '0', status: 'pending' }, + { name: 'secondOne', InstanceId: '1', status: 'stopped' }, + { name: 'thirdOne', InstanceId: '2', status: 'running' }, + ] + + getInstanceStub.resolves(mapToInstanceCollection(instances)) + + await testNode.updateChildren() + assert.strictEqual(testNode.pollingSet.size, 1) + getInstanceStub.restore() + }) + + it('does not refresh explorer when timer goes off if status unchanged', async function () { + const statusUpdateStub = sinon.stub(Ec2Client.prototype, 'getInstanceStatus').resolves('pending') + const instances = [ + { name: 'firstOne', InstanceId: '0', status: 'pending' }, + { name: 'secondOne', InstanceId: '1', status: 'stopped' }, + { name: 'thirdOne', InstanceId: '2', status: 'running' }, + ] + + getInstanceStub.resolves(mapToInstanceCollection(instances)) + + await testNode.updateChildren() + await clock.tickAsync(6000) + sinon.assert.notCalled(refreshStub) + statusUpdateStub.restore() + getInstanceStub.restore() + }) + + it('does refresh explorer when timer goes and status changed', async function () { + sinon.assert.notCalled(refreshStub) + const statusUpdateStub = sinon.stub(Ec2Client.prototype, 'getInstanceStatus').resolves('running') + testNode.pollingSet.add('0') + await clock.tickAsync(6000) + sinon.assert.called(refreshStub) + statusUpdateStub.restore() }) }) diff --git a/packages/core/src/test/shared/utilities/pollingSet.test.ts b/packages/core/src/test/shared/utilities/pollingSet.test.ts new file mode 100644 index 00000000000..ff92474dbbf --- /dev/null +++ b/packages/core/src/test/shared/utilities/pollingSet.test.ts @@ -0,0 +1,102 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import { PollingSet } from '../../../shared/utilities/pollingSet' +import * as FakeTimers from '@sinonjs/fake-timers' +import { installFakeClock } from '../../testUtil' + +describe('pollingSet', function () { + let pollingSet: PollingSet + let clock: FakeTimers.InstalledClock + before(function () { + clock = installFakeClock() + }) + + after(function () {}) + + beforeEach(function () {}) + + afterEach(function () { + if (pollingSet) { + pollingSet.clear() + pollingSet.clearTimer() + } + }) + + after(function () { + sinon.restore() + clock.uninstall() + }) + + it('inherits basic set properties', function () { + const pollingSet = new PollingSet(0, () => {}) + const item1 = { id: 1 } + const item2 = { id: 2 } + + pollingSet.add(item1) + assert.strictEqual(pollingSet.size, 1) + assert(pollingSet.has(item1)) + assert.strictEqual(pollingSet.has(item2), false) + + pollingSet.delete(item1) + assert.strictEqual(pollingSet.size, 0) + assert.strictEqual(pollingSet.has(item1), false) + }) + + it('does not poll on initialization', async function () { + const pollingSet = new PollingSet(0, () => {}) + assert.strictEqual(pollingSet.isActive(), false) + }) + + it('does not trigger prematurely', async function () { + const action = sinon.spy() + pollingSet = new PollingSet(10, action) + sinon.assert.notCalled(action) + pollingSet.start('item') + + await clock.tickAsync(9) + sinon.assert.notCalled(action) + await clock.tickAsync(1) + sinon.assert.calledOnce(action) + }) + + it('stops timer once polling set is empty', async function () { + const pollingSet = new PollingSet(10, () => {}) + pollingSet.start('1') + pollingSet.add('2') + + const clearStub = sinon.stub(pollingSet, 'clearTimer') + sinon.assert.notCalled(clearStub) + assert.strictEqual(pollingSet.isActive(), true) + + pollingSet.delete('1') + + sinon.assert.notCalled(clearStub) + assert.strictEqual(pollingSet.isActive(), true) + + pollingSet.delete('2') + + await clock.tickAsync(20) + + assert.strictEqual(pollingSet.isActive(), false) + sinon.assert.callCount(clearStub, 2) + clearStub.restore() + }) + + it('runs action once per interval', async function () { + const action = sinon.spy() + pollingSet = new PollingSet(10, action) + pollingSet.start('1') + pollingSet.add('2') + + sinon.assert.callCount(action, 0) + await clock.tickAsync(11) + sinon.assert.callCount(action, 1) + await clock.tickAsync(22) + sinon.assert.callCount(action, 3) + }) +}) From 548f69ecda3d1a1e69caf34dacd181e6fb715966 Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 12 Sep 2024 12:49:08 -0400 Subject: [PATCH 162/172] move rest of changes from other branch --- .../apprunner/explorer/apprunnerNode.ts | 30 +++++-------- .../explorer/apprunnerServiceNode.ts | 4 +- .../ec2/explorer/ec2InstanceNode.ts | 26 +++++++++-- .../awsService/ec2/explorer/ec2ParentNode.ts | 13 ++++++ .../core/src/shared/utilities/pollingSet.ts | 44 +++++++++++++++++++ 5 files changed, 91 insertions(+), 26 deletions(-) create mode 100644 packages/core/src/shared/utilities/pollingSet.ts diff --git a/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts b/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts index 13739b9a0a8..889f94a51cc 100644 --- a/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts +++ b/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts @@ -11,15 +11,13 @@ import * as nls from 'vscode-nls' import { AppRunnerClient } from '../../../shared/clients/apprunnerClient' import { getPaginatedAwsCallIter } from '../../../shared/utilities/collectionUtils' import { AppRunner } from 'aws-sdk' -import globals from '../../../shared/extensionGlobals' +import { PollingSet } from '../../../shared/utilities/pollingSet' const localize = nls.loadMessageBundle() -const pollingInterval = 20000 export class AppRunnerNode extends AWSTreeNodeBase { private readonly serviceNodes: Map = new Map() - private readonly pollingNodes: Set = new Set() - private pollTimer?: NodeJS.Timeout + private readonly pollingSet: PollingSet = new PollingSet(20000, this.refresh.bind(this)) public constructor( public override readonly regionCode: string, @@ -79,8 +77,8 @@ export class AppRunnerNode extends AWSTreeNodeBase { if (this.serviceNodes.has(summary.ServiceArn)) { this.serviceNodes.get(summary.ServiceArn)!.update(summary) if (summary.Status !== 'OPERATION_IN_PROGRESS') { - this.pollingNodes.delete(summary.ServiceArn) - this.clearPollTimer() + this.pollingSet.delete(summary.ServiceArn) + this.pollingSet.clearTimer() } } else { this.serviceNodes.set(summary.ServiceArn, new AppRunnerServiceNode(this, this.client, summary)) @@ -92,27 +90,19 @@ export class AppRunnerNode extends AWSTreeNodeBase { deletedNodeArns.forEach(this.deleteNode.bind(this)) } - private clearPollTimer(): void { - if (this.pollingNodes.size === 0 && this.pollTimer) { - globals.clock.clearInterval(this.pollTimer) - this.pollTimer = undefined - } - } - - public startPolling(id: string): void { - this.pollingNodes.add(id) - this.pollTimer = this.pollTimer ?? globals.clock.setInterval(this.refresh.bind(this), pollingInterval) + public startPollingNode(id: string): void { + this.pollingSet.start(id) } - public stopPolling(id: string): void { - this.pollingNodes.delete(id) + public stopPollingNode(id: string): void { + this.pollingSet.delete(id) this.serviceNodes.get(id)?.refresh() - this.clearPollTimer() + this.pollingSet.clearTimer() } public deleteNode(id: string): void { this.serviceNodes.delete(id) - this.pollingNodes.delete(id) + this.pollingSet.delete(id) } public async createService(request: AppRunner.CreateServiceRequest): Promise { diff --git a/packages/core/src/awsService/apprunner/explorer/apprunnerServiceNode.ts b/packages/core/src/awsService/apprunner/explorer/apprunnerServiceNode.ts index 75a8a30b49b..e5c28404d3c 100644 --- a/packages/core/src/awsService/apprunner/explorer/apprunnerServiceNode.ts +++ b/packages/core/src/awsService/apprunner/explorer/apprunnerServiceNode.ts @@ -107,12 +107,12 @@ export class AppRunnerServiceNode extends CloudWatchLogsBase implements AWSResou } if (this._info.Status === 'OPERATION_IN_PROGRESS') { - this.parent.startPolling(this._info.ServiceArn) + this.parent.startPollingNode(this._info.ServiceArn) } else if (this.currentOperation.Type !== undefined) { this.currentOperation.Id = undefined this.currentOperation.Type = undefined this.setLabel() - this.parent.stopPolling(this._info.ServiceArn) + this.parent.stopPollingNode(this._info.ServiceArn) } } diff --git a/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts b/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts index 5ac4365c512..262e4b056a8 100644 --- a/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts +++ b/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts @@ -24,7 +24,8 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode public readonly client: Ec2Client, public override readonly regionCode: string, private readonly partitionId: string, - protected instance: Ec2Instance + // XXX: this variable is marked as readonly, but the 'status' attribute is updated when polling the nodes. + public readonly instance: Ec2Instance ) { super('') this.updateInstance(instance) @@ -32,11 +33,19 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode } public updateInstance(newInstance: Ec2Instance) { - this.setInstance(newInstance) + this.setInstanceStatus(newInstance.status!) this.label = `${this.name} (${this.InstanceId})` this.contextValue = this.getContext() this.iconPath = new vscode.ThemeIcon(getIconCode(this.instance)) this.tooltip = `${this.name}\n${this.InstanceId}\n${this.instance.status}\n${this.arn}` + + if (this.isPending()) { + this.parent.pollingSet.start(this.InstanceId) + } + } + + public isPending(): boolean { + return this.getStatus() !== 'running' && this.getStatus() !== 'stopped' } public async updateStatus() { @@ -56,8 +65,8 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode return Ec2InstancePendingContext } - public setInstance(newInstance: Ec2Instance) { - this.instance = newInstance + public setInstanceStatus(instanceStatus: string) { + this.instance.status = instanceStatus } public toSelection(): Ec2Selection { @@ -67,6 +76,10 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode } } + public getStatus(): string { + return this.instance.status! + } + public get name(): string { return getNameOfInstance(this.instance) ?? `(no name)` } @@ -80,4 +93,9 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode this.regionCode }:${globals.awsContext.getCredentialAccountId()}:instance/${this.InstanceId}` } + + public async refreshNode(): Promise { + await this.updateStatus() + await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', this) + } } diff --git a/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts b/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts index 1c4b344b267..b01a310323f 100644 --- a/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts +++ b/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts @@ -9,6 +9,7 @@ import { PlaceholderNode } from '../../../shared/treeview/nodes/placeholderNode' import { Ec2InstanceNode } from './ec2InstanceNode' import { Ec2Client } from '../../../shared/clients/ec2Client' import { updateInPlace } from '../../../shared/utilities/collectionUtils' +import { PollingSet } from '../../../shared/utilities/pollingSet' export const parentContextValue = 'awsEc2ParentNode' export type Ec2Node = Ec2InstanceNode | Ec2ParentNode @@ -17,6 +18,7 @@ export class Ec2ParentNode extends AWSTreeNodeBase { protected readonly placeHolderMessage = '[No EC2 Instances Found]' protected ec2InstanceNodes: Map public override readonly contextValue: string = parentContextValue + public readonly pollingSet: PollingSet = new PollingSet(5000, this.updatePendingNodes.bind(this)) public constructor( public override readonly regionCode: string, @@ -50,6 +52,17 @@ export class Ec2ParentNode extends AWSTreeNodeBase { ) } + private updatePendingNodes() { + this.pollingSet.forEach(async (instanceId) => { + const childNode = this.ec2InstanceNodes.get(instanceId)! + await childNode.updateStatus() + if (!childNode.isPending()) { + this.pollingSet.delete(instanceId) + await childNode.refreshNode() + } + }) + } + public async clearChildren() { this.ec2InstanceNodes = new Map() } diff --git a/packages/core/src/shared/utilities/pollingSet.ts b/packages/core/src/shared/utilities/pollingSet.ts new file mode 100644 index 00000000000..16a97b62e3d --- /dev/null +++ b/packages/core/src/shared/utilities/pollingSet.ts @@ -0,0 +1,44 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { globals } from '..' + +export class PollingSet extends Set { + public pollTimer?: NodeJS.Timeout + + public constructor( + private readonly interval: number, + private readonly action: () => void + ) { + super() + } + + public isActive(): boolean { + return this.size !== 0 + } + + public hasTimer(): boolean { + return this.pollTimer !== undefined + } + + public clearTimer(): void { + if (!this.isActive() && this.hasTimer()) { + globals.clock.clearInterval(this.pollTimer) + this.pollTimer = undefined + } + } + + private poll() { + this.action() + if (!this.isActive()) { + this.clearTimer() + } + } + + public start(id: T): void { + this.add(id) + this.pollTimer = this.pollTimer ?? globals.clock.setInterval(() => this.poll(), this.interval) + } +} From f152943fa286b502ed16b972cd30deb37aff0895 Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 12 Sep 2024 12:50:15 -0400 Subject: [PATCH 163/172] fix imports --- packages/core/src/test/shared/utilities/pollingSet.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/test/shared/utilities/pollingSet.test.ts b/packages/core/src/test/shared/utilities/pollingSet.test.ts index ff92474dbbf..55fb1e87a88 100644 --- a/packages/core/src/test/shared/utilities/pollingSet.test.ts +++ b/packages/core/src/test/shared/utilities/pollingSet.test.ts @@ -5,9 +5,9 @@ import assert from 'assert' import * as sinon from 'sinon' -import { PollingSet } from '../../../shared/utilities/pollingSet' import * as FakeTimers from '@sinonjs/fake-timers' import { installFakeClock } from '../../testUtil' +import { PollingSet } from '../../../shared/utilities/pollingSet' describe('pollingSet', function () { let pollingSet: PollingSet From 1af46cddcfbcb9c534f94f6a4f4c2bfaf1026d23 Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 12 Sep 2024 12:59:24 -0400 Subject: [PATCH 164/172] update changelog --- .../Feature-24874272-371b-40cb-8cbb-27f51a9c45e3.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 packages/toolkit/.changes/next-release/Feature-24874272-371b-40cb-8cbb-27f51a9c45e3.json diff --git a/packages/toolkit/.changes/next-release/Feature-24874272-371b-40cb-8cbb-27f51a9c45e3.json b/packages/toolkit/.changes/next-release/Feature-24874272-371b-40cb-8cbb-27f51a9c45e3.json new file mode 100644 index 00000000000..2ff850168a2 --- /dev/null +++ b/packages/toolkit/.changes/next-release/Feature-24874272-371b-40cb-8cbb-27f51a9c45e3.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "EC2 nodes in explorer update status automatically" +} From f1c6fee8838acbea546a2d7dc0c54dc14e754a90 Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 12 Sep 2024 15:15:01 -0400 Subject: [PATCH 165/172] move over changes --- packages/core/src/awsService/ec2/activation.ts | 5 ++++- packages/core/src/shared/telemetry/vscodeTelemetry.json | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/core/src/awsService/ec2/activation.ts b/packages/core/src/awsService/ec2/activation.ts index 94da1906e48..b1ae50799e4 100644 --- a/packages/core/src/awsService/ec2/activation.ts +++ b/packages/core/src/awsService/ec2/activation.ts @@ -31,7 +31,10 @@ export async function activate(ctx: ExtContext): Promise { }), Commands.register('aws.ec2.openRemoteConnection', async (node?: Ec2Node) => { - await openRemoteConnection(node) + await telemetry.ec2_connectToInstance.run(async (span) => { + span.record({ ec2ConnectionType: 'remoteWorkspace' }) + await openRemoteConnection(node) + }) }), Commands.register('aws.ec2.startInstance', async (node?: Ec2Node) => { diff --git a/packages/core/src/shared/telemetry/vscodeTelemetry.json b/packages/core/src/shared/telemetry/vscodeTelemetry.json index fffeec357b2..0e62913f7ac 100644 --- a/packages/core/src/shared/telemetry/vscodeTelemetry.json +++ b/packages/core/src/shared/telemetry/vscodeTelemetry.json @@ -1,5 +1,11 @@ { "types": [ + { + "name": "ec2ConnectionType", + "type": "string", + "allowedValues": ["remoteDesktop", "ssh", "scp", "ssm", "remoteWorkspace"], + "description": "Ways to connect to an EC2 Instance" + }, { "name": "amazonGenerateApproachLatency", "type": "double", From 3dae58cf277d701e8f1d1330e272ea9b44f5e957 Mon Sep 17 00:00:00 2001 From: hkobew Date: Mon, 21 Oct 2024 09:08:50 -0400 Subject: [PATCH 166/172] remove temp check --- packages/core/src/shared/telemetry/vscodeTelemetry.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/core/src/shared/telemetry/vscodeTelemetry.json b/packages/core/src/shared/telemetry/vscodeTelemetry.json index 208bebec567..9c3dbbdfd67 100644 --- a/packages/core/src/shared/telemetry/vscodeTelemetry.json +++ b/packages/core/src/shared/telemetry/vscodeTelemetry.json @@ -1,11 +1,5 @@ { "types": [ - { - "name": "ec2ConnectionType", - "type": "string", - "allowedValues": ["remoteDesktop", "ssh", "scp", "ssm", "remoteWorkspace"], - "description": "Ways to connect to an EC2 Instance" - }, { "name": "amazonGenerateApproachLatency", "type": "double", From ea2a1cec4e5d7c77ac4cf85ec9428c2445a5160c Mon Sep 17 00:00:00 2001 From: hkobew Date: Mon, 21 Oct 2024 11:22:56 -0400 Subject: [PATCH 167/172] add tests for telemetry --- .../test/awsService/ec2/activation.test.ts | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 packages/core/src/test/awsService/ec2/activation.test.ts diff --git a/packages/core/src/test/awsService/ec2/activation.test.ts b/packages/core/src/test/awsService/ec2/activation.test.ts new file mode 100644 index 00000000000..a5581cf2044 --- /dev/null +++ b/packages/core/src/test/awsService/ec2/activation.test.ts @@ -0,0 +1,65 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import { assertTelemetry } from '../../testUtil' +import { Ec2InstanceNode } from '../../../awsService/ec2/explorer/ec2InstanceNode' +import { Ec2ParentNode } from '../../../awsService/ec2/explorer/ec2ParentNode' +import { Ec2Client } from '../../../shared/clients/ec2Client' +import { Ec2ConnectionManager } from '../../../awsService/ec2/model' + +describe('ec2 telemetry', function () { + let testNode: Ec2InstanceNode + + before(function () { + const testRegion = 'test-region' + const testPartition = 'test-partition' + const testClient = new Ec2Client(testRegion) + const parentNode = new Ec2ParentNode(testRegion, testPartition, new Ec2Client(testRegion)) + testNode = new Ec2InstanceNode(parentNode, testClient, testRegion, testPartition, { + InstanceId: 'testId', + LastSeenStatus: 'status', + }) + }) + it('emits correct telemetry on terminal open', async function () { + const terminalStub = sinon.stub(Ec2ConnectionManager.prototype, 'attemptToOpenEc2Terminal') + await vscode.commands.executeCommand('aws.ec2.openTerminal', testNode) + + assertTelemetry('ec2_connectToInstance', { ec2ConnectionType: 'ssm' }) + terminalStub.restore() + }) + + it('emits correct telemetry on remote window open', async function () { + const remoteWindowStub = sinon.stub(Ec2ConnectionManager.prototype, 'tryOpenRemoteConnection') + await vscode.commands.executeCommand('aws.ec2.openRemoteConnection', testNode) + + assertTelemetry('ec2_connectToInstance', { ec2ConnectionType: 'remoteWorkspace' }) + remoteWindowStub.restore() + }) + + it('emits correct telemetry on state stop', async function () { + const stopInstanceStub = sinon.stub(Ec2Client.prototype, 'stopInstanceWithCancel') + await vscode.commands.executeCommand('aws.ec2.stopInstance', testNode) + + assertTelemetry('ec2_changeState', { ec2InstanceState: 'stop' }) + stopInstanceStub.restore() + }) + + it('emits correct telemetry on state start', async function () { + const startInstanceStub = sinon.stub(Ec2Client.prototype, 'startInstance') + await vscode.commands.executeCommand('aws.ec2.startInstance', testNode) + + assertTelemetry('ec2_changeState', { ec2InstanceState: 'start' }) + startInstanceStub.restore() + }) + + it('emits correct telemetry on state reboot', async function () { + const rebootInstanceStub = sinon.stub(Ec2Client.prototype, 'rebootInstance') + await vscode.commands.executeCommand('aws.ec2.rebootInstance', testNode) + + assertTelemetry('ec2_changeState', { ec2InstanceState: 'reboot' }) + rebootInstanceStub.restore() + }) +}) From ceba452b1f09c8f0c6829fa525046c9f2dda9dce Mon Sep 17 00:00:00 2001 From: hkobew Date: Tue, 22 Oct 2024 11:36:15 -0400 Subject: [PATCH 168/172] bump telemetry version --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2a25b9529dd..2fd5bc98b7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "vscode-nls-dev": "^4.0.4" }, "devDependencies": { - "@aws-toolkits/telemetry": "^1.0.272", + "@aws-toolkits/telemetry": "^1.0.273", "@playwright/browser-chromium": "^1.43.1", "@types/vscode": "^1.68.0", "@types/vscode-webview": "^1.57.1", @@ -5193,9 +5193,9 @@ } }, "node_modules/@aws-toolkits/telemetry": { - "version": "1.0.272", - "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.272.tgz", - "integrity": "sha512-cWmyTkiNDcDXaRjX7WdWO5FiNzXUKcrKpkYkkPgi8t+I9ZwwWwt6FKps7FjEGvFzTFx6I9XbsWM1mRrkuvxdQw==", + "version": "1.0.273", + "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.273.tgz", + "integrity": "sha512-E3XEG03A/lRqnyGTNseBjpftYHgFdUInJ3Lm3glGXP0USZsg1AwPh/07n5mkBAdKazihAmqnvwWvyzA2kzcZvw==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 39f4fe478eb..e896a5b9a51 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "generateNonCodeFiles": "npm run generateNonCodeFiles -w packages/ --if-present" }, "devDependencies": { - "@aws-toolkits/telemetry": "^1.0.272", + "@aws-toolkits/telemetry": "^1.0.273", "@playwright/browser-chromium": "^1.43.1", "@types/vscode": "^1.68.0", "@types/vscode-webview": "^1.57.1", From f5f82c9f4ef21de36091497c8ff105c2345f8048 Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 25 Oct 2024 17:27:23 -0400 Subject: [PATCH 169/172] disable polling set --- packages/core/src/test/awsService/ec2/activation.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/test/awsService/ec2/activation.test.ts b/packages/core/src/test/awsService/ec2/activation.test.ts index a5581cf2044..be5add9cbb5 100644 --- a/packages/core/src/test/awsService/ec2/activation.test.ts +++ b/packages/core/src/test/awsService/ec2/activation.test.ts @@ -9,6 +9,7 @@ import { Ec2InstanceNode } from '../../../awsService/ec2/explorer/ec2InstanceNod import { Ec2ParentNode } from '../../../awsService/ec2/explorer/ec2ParentNode' import { Ec2Client } from '../../../shared/clients/ec2Client' import { Ec2ConnectionManager } from '../../../awsService/ec2/model' +import { PollingSet } from '../../../shared/utilities/pollingSet' describe('ec2 telemetry', function () { let testNode: Ec2InstanceNode @@ -16,6 +17,9 @@ describe('ec2 telemetry', function () { before(function () { const testRegion = 'test-region' const testPartition = 'test-partition' + // Don't want to be polling here, that is tested in ../ec2ParentNode.test.ts + // disabled here for convenience (avoiding race conditions with timeout) + sinon.stub(PollingSet.prototype, 'start') const testClient = new Ec2Client(testRegion) const parentNode = new Ec2ParentNode(testRegion, testPartition, new Ec2Client(testRegion)) testNode = new Ec2InstanceNode(parentNode, testClient, testRegion, testPartition, { From 0cc31e31b9b173b79fa2914b620d9634c5186b38 Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 25 Oct 2024 17:28:07 -0400 Subject: [PATCH 170/172] restore stubs afterwards --- packages/core/src/test/awsService/ec2/activation.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/test/awsService/ec2/activation.test.ts b/packages/core/src/test/awsService/ec2/activation.test.ts index be5add9cbb5..d929c3162f9 100644 --- a/packages/core/src/test/awsService/ec2/activation.test.ts +++ b/packages/core/src/test/awsService/ec2/activation.test.ts @@ -27,6 +27,10 @@ describe('ec2 telemetry', function () { LastSeenStatus: 'status', }) }) + + after(function () { + sinon.restore() + }) it('emits correct telemetry on terminal open', async function () { const terminalStub = sinon.stub(Ec2ConnectionManager.prototype, 'attemptToOpenEc2Terminal') await vscode.commands.executeCommand('aws.ec2.openTerminal', testNode) From 26b25bf90f7a074213770a466ff66f020a09205b Mon Sep 17 00:00:00 2001 From: hkobew Date: Tue, 29 Oct 2024 11:14:24 -0400 Subject: [PATCH 171/172] rename tests to be general activation tests --- .../core/src/test/awsService/ec2/activation.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/src/test/awsService/ec2/activation.test.ts b/packages/core/src/test/awsService/ec2/activation.test.ts index 1dfd33fa5fa..34d24a01808 100644 --- a/packages/core/src/test/awsService/ec2/activation.test.ts +++ b/packages/core/src/test/awsService/ec2/activation.test.ts @@ -11,7 +11,7 @@ import { Ec2Client } from '../../../shared/clients/ec2Client' import { Ec2Connecter } from '../../../awsService/ec2/model' import { PollingSet } from '../../../shared/utilities/pollingSet' -describe('ec2 telemetry', function () { +describe('ec2 activation', function () { let testNode: Ec2InstanceNode before(function () { @@ -31,7 +31,7 @@ describe('ec2 telemetry', function () { after(function () { sinon.restore() }) - it('emits correct telemetry on terminal open', async function () { + it('terminal open', async function () { const terminalStub = sinon.stub(Ec2Connecter.prototype, 'attemptToOpenEc2Terminal') await vscode.commands.executeCommand('aws.ec2.openTerminal', testNode) @@ -39,7 +39,7 @@ describe('ec2 telemetry', function () { terminalStub.restore() }) - it('emits correct telemetry on remote window open', async function () { + it('remote window open', async function () { const remoteWindowStub = sinon.stub(Ec2Connecter.prototype, 'tryOpenRemoteConnection') await vscode.commands.executeCommand('aws.ec2.openRemoteConnection', testNode) @@ -47,7 +47,7 @@ describe('ec2 telemetry', function () { remoteWindowStub.restore() }) - it('emits correct telemetry on state stop', async function () { + it('state stop', async function () { const stopInstanceStub = sinon.stub(Ec2Client.prototype, 'stopInstanceWithCancel') await vscode.commands.executeCommand('aws.ec2.stopInstance', testNode) @@ -55,7 +55,7 @@ describe('ec2 telemetry', function () { stopInstanceStub.restore() }) - it('emits correct telemetry on state start', async function () { + it('state start', async function () { const startInstanceStub = sinon.stub(Ec2Client.prototype, 'startInstance') await vscode.commands.executeCommand('aws.ec2.startInstance', testNode) @@ -63,7 +63,7 @@ describe('ec2 telemetry', function () { startInstanceStub.restore() }) - it('emits correct telemetry on state reboot', async function () { + it('state reboot', async function () { const rebootInstanceStub = sinon.stub(Ec2Client.prototype, 'rebootInstance') await vscode.commands.executeCommand('aws.ec2.rebootInstance', testNode) From 6b9eb69591056f4b6583e5f20df301ebeae12385 Mon Sep 17 00:00:00 2001 From: hkobew Date: Wed, 6 Nov 2024 19:17:14 -0500 Subject: [PATCH 172/172] move telemetry into a single test --- package-lock.json | 2 +- .../test/awsService/ec2/activation.test.ts | 29 ++----------------- 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3d069c7d1fb..f5afc020fa2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "vscode-nls-dev": "^4.0.4" }, "devDependencies": { - "@aws-toolkits/telemetry": "^1.0.273", + "@aws-toolkits/telemetry": "^1.0.274", "@playwright/browser-chromium": "^1.43.1", "@types/he": "^1.2.3", "@types/vscode": "^1.68.0", diff --git a/packages/core/src/test/awsService/ec2/activation.test.ts b/packages/core/src/test/awsService/ec2/activation.test.ts index 34d24a01808..f604ada56cb 100644 --- a/packages/core/src/test/awsService/ec2/activation.test.ts +++ b/packages/core/src/test/awsService/ec2/activation.test.ts @@ -31,43 +31,18 @@ describe('ec2 activation', function () { after(function () { sinon.restore() }) - it('terminal open', async function () { + + it('telemetry', async function () { const terminalStub = sinon.stub(Ec2Connecter.prototype, 'attemptToOpenEc2Terminal') await vscode.commands.executeCommand('aws.ec2.openTerminal', testNode) assertTelemetry('ec2_connectToInstance', { ec2ConnectionType: 'ssm' }) terminalStub.restore() - }) - - it('remote window open', async function () { - const remoteWindowStub = sinon.stub(Ec2Connecter.prototype, 'tryOpenRemoteConnection') - await vscode.commands.executeCommand('aws.ec2.openRemoteConnection', testNode) - assertTelemetry('ec2_connectToInstance', { ec2ConnectionType: 'remoteWorkspace' }) - remoteWindowStub.restore() - }) - - it('state stop', async function () { const stopInstanceStub = sinon.stub(Ec2Client.prototype, 'stopInstanceWithCancel') await vscode.commands.executeCommand('aws.ec2.stopInstance', testNode) assertTelemetry('ec2_changeState', { ec2InstanceState: 'stop' }) stopInstanceStub.restore() }) - - it('state start', async function () { - const startInstanceStub = sinon.stub(Ec2Client.prototype, 'startInstance') - await vscode.commands.executeCommand('aws.ec2.startInstance', testNode) - - assertTelemetry('ec2_changeState', { ec2InstanceState: 'start' }) - startInstanceStub.restore() - }) - - it('state reboot', async function () { - const rebootInstanceStub = sinon.stub(Ec2Client.prototype, 'rebootInstance') - await vscode.commands.executeCommand('aws.ec2.rebootInstance', testNode) - - assertTelemetry('ec2_changeState', { ec2InstanceState: 'reboot' }) - rebootInstanceStub.restore() - }) })