From 463d18c9a5df7689a75901a89fd021206225ca07 Mon Sep 17 00:00:00 2001 From: hkobew Date: Wed, 15 Jan 2025 10:58:34 -0500 Subject: [PATCH 1/5] simplify output --- .github/workflows/filterDuplicates.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/filterDuplicates.js b/.github/workflows/filterDuplicates.js index 0284ea20654..47541fc8f72 100644 --- a/.github/workflows/filterDuplicates.js +++ b/.github/workflows/filterDuplicates.js @@ -84,6 +84,19 @@ function filterDuplicates(report, changes) { return duplicates } +function formatDuplicates(duplicates) { + return duplicates.map((dupe) => { + return { + firstFile: dupe.firstFile.name, + firstStart: dupe.firstFile.start, + firstEnd: dupe.firstFile.end, + secondFile: dupe.secondFile.name, + secondStart: dupe.secondFile.start, + secondEnd: dupe.secondFile.end, + } + }) +} + async function run() { const rawDiffPath = process.argv[3] const jscpdReportPath = process.argv[4] @@ -94,7 +107,7 @@ async function run() { console.log('%s files changes', changes.size) console.log('%s duplicates found', filteredDuplicates.length) if (filteredDuplicates.length > 0) { - console.log(filteredDuplicates) + console.log(formatDuplicates(filteredDuplicates)) process.exit(1) } } @@ -102,7 +115,6 @@ async function run() { /** * Mini-test Suite */ -console.log(__dirname) const testDiffFile = path.resolve(__dirname, 'test/test_diff.txt') let testCounter = 0 function assertEqual(actual, expected) { From c65df4bbfe26e2fd2ea69706fdab704a40f268f8 Mon Sep 17 00:00:00 2001 From: hkobew Date: Wed, 15 Jan 2025 10:59:46 -0500 Subject: [PATCH 2/5] add duplicate --- packages/core/src/awsService/ec2/model2.ts | 364 +++++++++++++++++++++ 1 file changed, 364 insertions(+) create mode 100644 packages/core/src/awsService/ec2/model2.ts diff --git a/packages/core/src/awsService/ec2/model2.ts b/packages/core/src/awsService/ec2/model2.ts new file mode 100644 index 00000000000..8abc42d3c82 --- /dev/null +++ b/packages/core/src/awsService/ec2/model2.ts @@ -0,0 +1,364 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +// Temp file to demonstrate the jscpd output +import * as vscode from 'vscode' +import { Session } from 'aws-sdk/clients/ssm' +import { EC2, IAM, SSM } from 'aws-sdk' +import { Ec2Selection } from './prompter' +import { getOrInstallCli } from '../../shared/utilities/cliUtils' +import { isCloud9 } from '../../shared/extensionUtilities' +import { ToolkitError } from '../../shared/errors' +import { SsmClient } from '../../shared/clients/ssmClient' +import { Ec2Client } from '../../shared/clients/ec2Client' +import { + VscodeRemoteConnection, + createBoundProcess, + ensureDependencies, + getDeniedSsmActions, + openRemoteTerminal, + promptToAddInlinePolicy, +} from '../../shared/remoteSession' +import { DefaultIamClient } from '../../shared/clients/iamClient' +import { ErrorInformation } from '../../shared/errors' +import { + sshAgentSocketVariable, + SshError, + startSshAgent, + startVscodeRemote, + testSshConnection, +} from '../../shared/extensions/ssh' +import { getLogger } from '../../shared/logger/logger' +import { CancellationError, Timeout } from '../../shared/utilities/timeoutUtils' +import { showMessageWithCancel } from '../../shared/utilities/messages' +import { SshConfig } from '../../shared/sshConfig' +import { SshKeyPair } from './sshKeyPair' +import { Ec2SessionTracker } from './remoteSessionManager' +import { getEc2SsmEnv } from './utils' + +export type Ec2ConnectErrorCode = 'EC2SSMStatus' | 'EC2SSMPermission' | 'EC2SSMConnect' | 'EC2SSMAgentStatus' + +export interface Ec2RemoteEnv extends VscodeRemoteConnection { + selection: Ec2Selection + keyPair: SshKeyPair + ssmSession: SSM.StartSessionResponse +} + +export type Ec2OS = 'Amazon Linux' | 'Ubuntu' | 'macOS' +interface RemoteUser { + os: Ec2OS + name: string +} + +export class Ec2Connecter implements vscode.Disposable { + protected ssmClient: SsmClient + protected ec2Client: Ec2Client + protected iamClient: DefaultIamClient + protected sessionManager: Ec2SessionTracker + + private policyDocumentationUri = vscode.Uri.parse( + 'https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-getting-started-instance-profile.html' + ) + + private ssmAgentDocumentationUri = vscode.Uri.parse( + 'https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html' + ) + + public constructor(readonly regionCode: string) { + this.ssmClient = this.createSsmSdkClient() + this.ec2Client = this.createEc2SdkClient() + this.iamClient = this.createIamSdkClient() + this.sessionManager = new Ec2SessionTracker(regionCode, this.ssmClient) + } + + protected createSsmSdkClient(): SsmClient { + return new SsmClient(this.regionCode) + } + + protected createEc2SdkClient(): Ec2Client { + return new Ec2Client(this.regionCode) + } + + protected createIamSdkClient(): DefaultIamClient { + return new DefaultIamClient(this.regionCode) + } + + public async addActiveSession(sessionId: SSM.SessionId, instanceId: EC2.InstanceId): Promise { + await this.sessionManager.addSession(instanceId, sessionId) + } + + public async dispose(): Promise { + await this.sessionManager.dispose() + } + + public isConnectedTo(instanceId: string): boolean { + return this.sessionManager.isConnectedTo(instanceId) + } + + public async getAttachedIamRole(instanceId: string): Promise { + const IamInstanceProfile = await this.ec2Client.getAttachedIamInstanceProfile(instanceId) + if (IamInstanceProfile && IamInstanceProfile.Arn) { + const IamRole = await this.iamClient.getIAMRoleFromInstanceProfile(IamInstanceProfile.Arn) + return IamRole + } + } + + public async hasProperPermissions(IamRoleArn: string): Promise { + const deniedActions = await getDeniedSsmActions(this.iamClient, IamRoleArn) + + return deniedActions.length === 0 + } + + public async isInstanceRunning(instanceId: string): Promise { + const instanceStatus = await this.ec2Client.getInstanceStatus(instanceId) + return instanceStatus === 'running' + } + + protected 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) + } + + private async checkForInstanceStatusError(selection: Ec2Selection): Promise { + const isInstanceRunning = await this.isInstanceRunning(selection.instanceId) + + if (!isInstanceRunning) { + const message = 'Ensure the target instance is running.' + this.throwConnectionError(message, selection, { code: 'EC2SSMStatus' }) + } + } + + private async checkForInstancePermissionsError(selection: Ec2Selection): Promise { + const IamRole = await this.getAttachedIamRole(selection.instanceId) + + if (!IamRole) { + const message = `No IAM role attached to instance: ${selection.instanceId}` + this.throwConnectionError(message, selection, { + code: 'EC2SSMPermission', + documentationUri: this.policyDocumentationUri, + }) + } + + const hasPermission = await this.hasProperPermissions(IamRole!.Arn) + + if (!hasPermission) { + const policiesAdded = await promptToAddInlinePolicy(this.iamClient, IamRole!.Arn!) + + if (!policiesAdded) { + throw new CancellationError('user') + } + } + } + + private async checkForInstanceSsmError(selection: Ec2Selection): Promise { + const isSsmAgentRunning = (await this.ssmClient.getInstanceAgentPingStatus(selection.instanceId)) === 'Online' + + if (!isSsmAgentRunning) { + this.throwConnectionError('Is SSM Agent running on the target instance?', selection, { + code: 'EC2SSMAgentStatus', + documentationUri: this.ssmAgentDocumentationUri, + }) + } + } + + public async checkForStartSessionError(selection: Ec2Selection): Promise { + await this.checkForInstanceStatusError(selection) + + await this.checkForInstancePermissionsError(selection) + + await this.checkForInstanceSsmError(selection) + } + + private async openSessionInTerminal(session: Session, selection: Ec2Selection) { + const ssmPlugin = await getOrInstallCli('session-manager-plugin', !isCloud9) + const shellArgs = [JSON.stringify(session), selection.region, 'StartSession'] + const terminalOptions = { + name: `${selection.region}/${selection.instanceId}`, + shellPath: ssmPlugin, + shellArgs: shellArgs, + } + + await openRemoteTerminal(terminalOptions, () => this.ssmClient.terminateSession(session)).catch((err) => { + throw ToolkitError.chain(err, 'Failed to open ec2 instance.') + }) + } + + public async attemptToOpenEc2Terminal(selection: Ec2Selection): Promise { + await this.checkForStartSessionError(selection) + try { + const response = await this.ssmClient.startSession(selection.instanceId) + await this.openSessionInTerminal(response, selection) + } catch (err: unknown) { + this.throwConnectionError('', selection, err as Error) + } + } + + public async tryOpenRemoteConnection(selection: Ec2Selection): Promise { + await this.checkForStartSessionError(selection) + + const remoteUser = await this.getRemoteUser(selection.instanceId) + const remoteEnv = await this.prepareEc2RemoteEnvWithProgress(selection, remoteUser) + const testSession = await this.ssmClient.startSession(selection.instanceId, 'AWS-StartSSHSession') + try { + await testSshConnection( + remoteEnv.SessionProcess, + remoteEnv.hostname, + remoteEnv.sshPath, + remoteUser.name, + testSession + ) + await startVscodeRemote( + remoteEnv.SessionProcess, + remoteEnv.hostname, + '/', + remoteEnv.vscPath, + remoteUser.name + ) + } catch (err) { + const message = err instanceof SshError ? 'Testing SSH connection to instance failed' : '' + this.throwConnectionError(message, selection, err as Error) + } finally { + await this.ssmClient.terminateSession(testSession) + } + } + + public async prepareEc2RemoteEnvWithProgress( + selection: Ec2Selection, + remoteUser: RemoteUser + ): Promise { + const timeout = new Timeout(60000) + await showMessageWithCancel('AWS: Opening remote connection...', timeout) + const remoteEnv = await this.prepareEc2RemoteEnv(selection, remoteUser).finally(() => timeout.cancel()) + return remoteEnv + } + + private async startSSMSession(instanceId: string): Promise { + const ssmSession = await this.ssmClient.startSession(instanceId, 'AWS-StartSSHSession') + await this.addActiveSession(instanceId, ssmSession.SessionId!) + return ssmSession + } + + public async prepareEc2RemoteEnv(selection: Ec2Selection, remoteUser: RemoteUser): Promise { + const logger = this.configureRemoteConnectionLogger(selection.instanceId) + const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() + const keyPair = await this.configureSshKeys(selection, remoteUser) + const hostnamePrefix = 'aws-ec2-' + const hostname = `${hostnamePrefix}${selection.instanceId}` + const sshConfig = new SshConfig(ssh, hostnamePrefix, 'ec2_connect', keyPair.getPrivateKeyPath()) + + 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 ssmSession = await this.startSSMSession(selection.instanceId) + + const vars = getEc2SsmEnv(selection, ssm, ssmSession) + getLogger().debug(`ec2: connect script logs at ${vars.LOG_FILE_LOCATION}`) + + const envProvider = async () => { + return { [sshAgentSocketVariable]: await startSshAgent(), ...vars } + } + const SessionProcess = createBoundProcess(envProvider).extend({ + onStdout: logger, + onStderr: logger, + rejectOnErrorCode: true, + }) + + return { + hostname, + envProvider, + sshPath: ssh, + vscPath: vsc, + SessionProcess, + selection, + keyPair, + ssmSession, + } + } + + private configureRemoteConnectionLogger(instanceId: string) { + const logPrefix = `ec2 (${instanceId})` + const logger = (data: string) => getLogger().verbose(`${logPrefix}: ${data}`) + return logger + } + + public async configureSshKeys(selection: Ec2Selection, remoteUser: RemoteUser): Promise { + const keyPair = await SshKeyPair.getSshKeyPair(`aws-ec2-key`, 30000) + await this.sendSshKeyToInstance(selection, keyPair, remoteUser) + return keyPair + } + + /** Removes old key(s) that we added to the remote ~/.ssh/authorized_keys file. */ + public async tryCleanKeys( + instanceId: string, + hintComment: string, + hostOS: Ec2OS, + remoteAuthorizedKeysPath: string + ) { + try { + const deleteExistingKeyCommand = getRemoveLinesCommand(hintComment, hostOS, remoteAuthorizedKeysPath) + await this.sendCommandAndWait(instanceId, deleteExistingKeyCommand) + } catch (e) { + getLogger().warn(`ec2: failed to clean keys: %O`, e) + } + } + + private async sendCommandAndWait(instanceId: string, command: string) { + return await this.ssmClient.sendCommandAndWait(instanceId, 'AWS-RunShellScript', { + commands: [command], + }) + } + + public async sendSshKeyToInstance( + selection: Ec2Selection, + sshKeyPair: SshKeyPair, + remoteUser: RemoteUser + ): Promise { + const sshPubKey = await sshKeyPair.getPublicKey() + const hintComment = '#AWSToolkitForVSCode' + + const remoteAuthorizedKeysPath = `/home/${remoteUser.name}/.ssh/authorized_keys` + + const appendStr = (s: string) => `echo "${s}" >> ${remoteAuthorizedKeysPath}` + const writeKeyCommand = appendStr([sshPubKey.replace('\n', ''), hintComment].join(' ')) + + await this.tryCleanKeys(selection.instanceId, hintComment, remoteUser.os, remoteAuthorizedKeysPath) + await this.sendCommandAndWait(selection.instanceId, writeKeyCommand) + } + + public async getRemoteUser(instanceId: string): Promise { + const os = await this.ssmClient.getTargetPlatformName(instanceId) + if (os === 'Amazon Linux') { + return { name: 'ec2-user', os } + } + + if (os === 'Ubuntu') { + return { name: 'ubuntu', os } + } + + throw new ToolkitError(`Unrecognized OS name ${os} on instance ${instanceId}`, { code: 'UnknownEc2OS' }) + } +} + +/** + * Generate bash command (as string) to remove lines containing `pattern`. + * @param pattern pattern for deleted lines. + * @param filepath filepath (as string) to target with the command. + * @returns bash command to remove lines from file. + */ +export function getRemoveLinesCommand(pattern: string, hostOS: Ec2OS, filepath: string): string { + if (pattern.includes('/')) { + throw new ToolkitError(`ec2: cannot match pattern containing '/', given: ${pattern}`) + } + // Linux allows not passing extension to -i, whereas macOS requires zero length extension. + return `sed -i${isLinux(hostOS) ? '' : " ''"} /${pattern}/d ${filepath}` +} + +function isLinux(os: Ec2OS): boolean { + return os === 'Amazon Linux' || os === 'Ubuntu' +} From 2ae83319b911fe830c6f766a6543afbcdbc09147 Mon Sep 17 00:00:00 2001 From: hkobew Date: Wed, 15 Jan 2025 11:17:18 -0500 Subject: [PATCH 3/5] generalize url --- .github/workflows/filterDuplicates.js | 19 +++++++++++-------- .github/workflows/node.js.yml | 5 ++++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/filterDuplicates.js b/.github/workflows/filterDuplicates.js index 47541fc8f72..a67dc1a4296 100644 --- a/.github/workflows/filterDuplicates.js +++ b/.github/workflows/filterDuplicates.js @@ -84,22 +84,25 @@ function filterDuplicates(report, changes) { return duplicates } -function formatDuplicates(duplicates) { +function formatDuplicates(duplicates, commitHash, repoName) { + const baseUrl = `https://github.com/${repoName}` return duplicates.map((dupe) => { return { - firstFile: dupe.firstFile.name, - firstStart: dupe.firstFile.start, - firstEnd: dupe.firstFile.end, - secondFile: dupe.secondFile.name, - secondStart: dupe.secondFile.start, - secondEnd: dupe.secondFile.end, + first: formUrl(dupe.firstFile, commitHash), + second: formUrl(dupe.secondFile, commitHash), + numberOfLines: dupe.lines, } }) + function formUrl(file, commitHash) { + return `${baseUrl}blob/${commitHash}/${file.name}#L${file.start}-L${file.end}` + } } async function run() { const rawDiffPath = process.argv[3] const jscpdReportPath = process.argv[4] + const commitHash = process.argv[5] + const repoName = process.argv[6] const changes = await parseDiff(rawDiffPath) const jscpdReport = JSON.parse(await fs.readFile(jscpdReportPath, 'utf8')) const filteredDuplicates = filterDuplicates(jscpdReport, changes) @@ -107,7 +110,7 @@ async function run() { console.log('%s files changes', changes.size) console.log('%s duplicates found', filteredDuplicates.length) if (filteredDuplicates.length > 0) { - console.log(formatDuplicates(filteredDuplicates)) + console.log(formatDuplicates(filteredDuplicates, commitHash, repoName)) process.exit(1) } } diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 04a289eded9..0cc8025125d 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -101,7 +101,10 @@ jobs: path: ./jscpd-report.json - name: Check for Duplicates - run: node "$GITHUB_WORKSPACE/.github/workflows/filterDuplicates.js" run diff_output.txt jscpd-report.json + env: + COMMIT_HASH: ${{ github.sha}} + REPO_NAME: ${{ github.repository }} + run: node "$GITHUB_WORKSPACE/.github/workflows/filterDuplicates.js" run diff_output.txt jscpd-report.json $COMMIT_HASH $REPO_NAME macos: needs: lint-commits From 09430995d07f37c1b5dd028da9d6f3b96c40ca65 Mon Sep 17 00:00:00 2001 From: hkobew Date: Wed, 15 Jan 2025 11:21:04 -0500 Subject: [PATCH 4/5] add missing slash --- .github/workflows/filterDuplicates.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/filterDuplicates.js b/.github/workflows/filterDuplicates.js index a67dc1a4296..ee92d161149 100644 --- a/.github/workflows/filterDuplicates.js +++ b/.github/workflows/filterDuplicates.js @@ -94,7 +94,7 @@ function formatDuplicates(duplicates, commitHash, repoName) { } }) function formUrl(file, commitHash) { - return `${baseUrl}blob/${commitHash}/${file.name}#L${file.start}-L${file.end}` + return `${baseUrl}/blob/${commitHash}/${file.name}#L${file.start}-L${file.end}` } } From bba57569305fb780d4d3b19f3f72f335f35d25f8 Mon Sep 17 00:00:00 2001 From: hkobew Date: Wed, 15 Jan 2025 11:31:41 -0500 Subject: [PATCH 5/5] clean up and add docs --- .github/workflows/filterDuplicates.js | 2 +- packages/core/src/awsService/ec2/model2.ts | 364 --------------------- 2 files changed, 1 insertion(+), 365 deletions(-) delete mode 100644 packages/core/src/awsService/ec2/model2.ts diff --git a/.github/workflows/filterDuplicates.js b/.github/workflows/filterDuplicates.js index ee92d161149..2bb9d440cb5 100644 --- a/.github/workflows/filterDuplicates.js +++ b/.github/workflows/filterDuplicates.js @@ -4,7 +4,7 @@ * the program exits with an error and logs the filtered report to console. * * Usage: - * node filterDuplicates.js run [path_to_git_diff] [path_to_jscpd_report] + * node filterDuplicates.js run [path_to_git_diff] [path_to_jscpd_report] [commit_hash] [repo_name] * * Tests: * node filterDuplicates.js test diff --git a/packages/core/src/awsService/ec2/model2.ts b/packages/core/src/awsService/ec2/model2.ts deleted file mode 100644 index 8abc42d3c82..00000000000 --- a/packages/core/src/awsService/ec2/model2.ts +++ /dev/null @@ -1,364 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -// Temp file to demonstrate the jscpd output -import * as vscode from 'vscode' -import { Session } from 'aws-sdk/clients/ssm' -import { EC2, IAM, SSM } from 'aws-sdk' -import { Ec2Selection } from './prompter' -import { getOrInstallCli } from '../../shared/utilities/cliUtils' -import { isCloud9 } from '../../shared/extensionUtilities' -import { ToolkitError } from '../../shared/errors' -import { SsmClient } from '../../shared/clients/ssmClient' -import { Ec2Client } from '../../shared/clients/ec2Client' -import { - VscodeRemoteConnection, - createBoundProcess, - ensureDependencies, - getDeniedSsmActions, - openRemoteTerminal, - promptToAddInlinePolicy, -} from '../../shared/remoteSession' -import { DefaultIamClient } from '../../shared/clients/iamClient' -import { ErrorInformation } from '../../shared/errors' -import { - sshAgentSocketVariable, - SshError, - startSshAgent, - startVscodeRemote, - testSshConnection, -} from '../../shared/extensions/ssh' -import { getLogger } from '../../shared/logger/logger' -import { CancellationError, Timeout } from '../../shared/utilities/timeoutUtils' -import { showMessageWithCancel } from '../../shared/utilities/messages' -import { SshConfig } from '../../shared/sshConfig' -import { SshKeyPair } from './sshKeyPair' -import { Ec2SessionTracker } from './remoteSessionManager' -import { getEc2SsmEnv } from './utils' - -export type Ec2ConnectErrorCode = 'EC2SSMStatus' | 'EC2SSMPermission' | 'EC2SSMConnect' | 'EC2SSMAgentStatus' - -export interface Ec2RemoteEnv extends VscodeRemoteConnection { - selection: Ec2Selection - keyPair: SshKeyPair - ssmSession: SSM.StartSessionResponse -} - -export type Ec2OS = 'Amazon Linux' | 'Ubuntu' | 'macOS' -interface RemoteUser { - os: Ec2OS - name: string -} - -export class Ec2Connecter implements vscode.Disposable { - protected ssmClient: SsmClient - protected ec2Client: Ec2Client - protected iamClient: DefaultIamClient - protected sessionManager: Ec2SessionTracker - - private policyDocumentationUri = vscode.Uri.parse( - 'https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-getting-started-instance-profile.html' - ) - - private ssmAgentDocumentationUri = vscode.Uri.parse( - 'https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html' - ) - - public constructor(readonly regionCode: string) { - this.ssmClient = this.createSsmSdkClient() - this.ec2Client = this.createEc2SdkClient() - this.iamClient = this.createIamSdkClient() - this.sessionManager = new Ec2SessionTracker(regionCode, this.ssmClient) - } - - protected createSsmSdkClient(): SsmClient { - return new SsmClient(this.regionCode) - } - - protected createEc2SdkClient(): Ec2Client { - return new Ec2Client(this.regionCode) - } - - protected createIamSdkClient(): DefaultIamClient { - return new DefaultIamClient(this.regionCode) - } - - public async addActiveSession(sessionId: SSM.SessionId, instanceId: EC2.InstanceId): Promise { - await this.sessionManager.addSession(instanceId, sessionId) - } - - public async dispose(): Promise { - await this.sessionManager.dispose() - } - - public isConnectedTo(instanceId: string): boolean { - return this.sessionManager.isConnectedTo(instanceId) - } - - public async getAttachedIamRole(instanceId: string): Promise { - const IamInstanceProfile = await this.ec2Client.getAttachedIamInstanceProfile(instanceId) - if (IamInstanceProfile && IamInstanceProfile.Arn) { - const IamRole = await this.iamClient.getIAMRoleFromInstanceProfile(IamInstanceProfile.Arn) - return IamRole - } - } - - public async hasProperPermissions(IamRoleArn: string): Promise { - const deniedActions = await getDeniedSsmActions(this.iamClient, IamRoleArn) - - return deniedActions.length === 0 - } - - public async isInstanceRunning(instanceId: string): Promise { - const instanceStatus = await this.ec2Client.getInstanceStatus(instanceId) - return instanceStatus === 'running' - } - - protected 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) - } - - private async checkForInstanceStatusError(selection: Ec2Selection): Promise { - const isInstanceRunning = await this.isInstanceRunning(selection.instanceId) - - if (!isInstanceRunning) { - const message = 'Ensure the target instance is running.' - this.throwConnectionError(message, selection, { code: 'EC2SSMStatus' }) - } - } - - private async checkForInstancePermissionsError(selection: Ec2Selection): Promise { - const IamRole = await this.getAttachedIamRole(selection.instanceId) - - if (!IamRole) { - const message = `No IAM role attached to instance: ${selection.instanceId}` - this.throwConnectionError(message, selection, { - code: 'EC2SSMPermission', - documentationUri: this.policyDocumentationUri, - }) - } - - const hasPermission = await this.hasProperPermissions(IamRole!.Arn) - - if (!hasPermission) { - const policiesAdded = await promptToAddInlinePolicy(this.iamClient, IamRole!.Arn!) - - if (!policiesAdded) { - throw new CancellationError('user') - } - } - } - - private async checkForInstanceSsmError(selection: Ec2Selection): Promise { - const isSsmAgentRunning = (await this.ssmClient.getInstanceAgentPingStatus(selection.instanceId)) === 'Online' - - if (!isSsmAgentRunning) { - this.throwConnectionError('Is SSM Agent running on the target instance?', selection, { - code: 'EC2SSMAgentStatus', - documentationUri: this.ssmAgentDocumentationUri, - }) - } - } - - public async checkForStartSessionError(selection: Ec2Selection): Promise { - await this.checkForInstanceStatusError(selection) - - await this.checkForInstancePermissionsError(selection) - - await this.checkForInstanceSsmError(selection) - } - - private async openSessionInTerminal(session: Session, selection: Ec2Selection) { - const ssmPlugin = await getOrInstallCli('session-manager-plugin', !isCloud9) - const shellArgs = [JSON.stringify(session), selection.region, 'StartSession'] - const terminalOptions = { - name: `${selection.region}/${selection.instanceId}`, - shellPath: ssmPlugin, - shellArgs: shellArgs, - } - - await openRemoteTerminal(terminalOptions, () => this.ssmClient.terminateSession(session)).catch((err) => { - throw ToolkitError.chain(err, 'Failed to open ec2 instance.') - }) - } - - public async attemptToOpenEc2Terminal(selection: Ec2Selection): Promise { - await this.checkForStartSessionError(selection) - try { - const response = await this.ssmClient.startSession(selection.instanceId) - await this.openSessionInTerminal(response, selection) - } catch (err: unknown) { - this.throwConnectionError('', selection, err as Error) - } - } - - public async tryOpenRemoteConnection(selection: Ec2Selection): Promise { - await this.checkForStartSessionError(selection) - - const remoteUser = await this.getRemoteUser(selection.instanceId) - const remoteEnv = await this.prepareEc2RemoteEnvWithProgress(selection, remoteUser) - const testSession = await this.ssmClient.startSession(selection.instanceId, 'AWS-StartSSHSession') - try { - await testSshConnection( - remoteEnv.SessionProcess, - remoteEnv.hostname, - remoteEnv.sshPath, - remoteUser.name, - testSession - ) - await startVscodeRemote( - remoteEnv.SessionProcess, - remoteEnv.hostname, - '/', - remoteEnv.vscPath, - remoteUser.name - ) - } catch (err) { - const message = err instanceof SshError ? 'Testing SSH connection to instance failed' : '' - this.throwConnectionError(message, selection, err as Error) - } finally { - await this.ssmClient.terminateSession(testSession) - } - } - - public async prepareEc2RemoteEnvWithProgress( - selection: Ec2Selection, - remoteUser: RemoteUser - ): Promise { - const timeout = new Timeout(60000) - await showMessageWithCancel('AWS: Opening remote connection...', timeout) - const remoteEnv = await this.prepareEc2RemoteEnv(selection, remoteUser).finally(() => timeout.cancel()) - return remoteEnv - } - - private async startSSMSession(instanceId: string): Promise { - const ssmSession = await this.ssmClient.startSession(instanceId, 'AWS-StartSSHSession') - await this.addActiveSession(instanceId, ssmSession.SessionId!) - return ssmSession - } - - public async prepareEc2RemoteEnv(selection: Ec2Selection, remoteUser: RemoteUser): Promise { - const logger = this.configureRemoteConnectionLogger(selection.instanceId) - const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() - const keyPair = await this.configureSshKeys(selection, remoteUser) - const hostnamePrefix = 'aws-ec2-' - const hostname = `${hostnamePrefix}${selection.instanceId}` - const sshConfig = new SshConfig(ssh, hostnamePrefix, 'ec2_connect', keyPair.getPrivateKeyPath()) - - 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 ssmSession = await this.startSSMSession(selection.instanceId) - - const vars = getEc2SsmEnv(selection, ssm, ssmSession) - getLogger().debug(`ec2: connect script logs at ${vars.LOG_FILE_LOCATION}`) - - const envProvider = async () => { - return { [sshAgentSocketVariable]: await startSshAgent(), ...vars } - } - const SessionProcess = createBoundProcess(envProvider).extend({ - onStdout: logger, - onStderr: logger, - rejectOnErrorCode: true, - }) - - return { - hostname, - envProvider, - sshPath: ssh, - vscPath: vsc, - SessionProcess, - selection, - keyPair, - ssmSession, - } - } - - private configureRemoteConnectionLogger(instanceId: string) { - const logPrefix = `ec2 (${instanceId})` - const logger = (data: string) => getLogger().verbose(`${logPrefix}: ${data}`) - return logger - } - - public async configureSshKeys(selection: Ec2Selection, remoteUser: RemoteUser): Promise { - const keyPair = await SshKeyPair.getSshKeyPair(`aws-ec2-key`, 30000) - await this.sendSshKeyToInstance(selection, keyPair, remoteUser) - return keyPair - } - - /** Removes old key(s) that we added to the remote ~/.ssh/authorized_keys file. */ - public async tryCleanKeys( - instanceId: string, - hintComment: string, - hostOS: Ec2OS, - remoteAuthorizedKeysPath: string - ) { - try { - const deleteExistingKeyCommand = getRemoveLinesCommand(hintComment, hostOS, remoteAuthorizedKeysPath) - await this.sendCommandAndWait(instanceId, deleteExistingKeyCommand) - } catch (e) { - getLogger().warn(`ec2: failed to clean keys: %O`, e) - } - } - - private async sendCommandAndWait(instanceId: string, command: string) { - return await this.ssmClient.sendCommandAndWait(instanceId, 'AWS-RunShellScript', { - commands: [command], - }) - } - - public async sendSshKeyToInstance( - selection: Ec2Selection, - sshKeyPair: SshKeyPair, - remoteUser: RemoteUser - ): Promise { - const sshPubKey = await sshKeyPair.getPublicKey() - const hintComment = '#AWSToolkitForVSCode' - - const remoteAuthorizedKeysPath = `/home/${remoteUser.name}/.ssh/authorized_keys` - - const appendStr = (s: string) => `echo "${s}" >> ${remoteAuthorizedKeysPath}` - const writeKeyCommand = appendStr([sshPubKey.replace('\n', ''), hintComment].join(' ')) - - await this.tryCleanKeys(selection.instanceId, hintComment, remoteUser.os, remoteAuthorizedKeysPath) - await this.sendCommandAndWait(selection.instanceId, writeKeyCommand) - } - - public async getRemoteUser(instanceId: string): Promise { - const os = await this.ssmClient.getTargetPlatformName(instanceId) - if (os === 'Amazon Linux') { - return { name: 'ec2-user', os } - } - - if (os === 'Ubuntu') { - return { name: 'ubuntu', os } - } - - throw new ToolkitError(`Unrecognized OS name ${os} on instance ${instanceId}`, { code: 'UnknownEc2OS' }) - } -} - -/** - * Generate bash command (as string) to remove lines containing `pattern`. - * @param pattern pattern for deleted lines. - * @param filepath filepath (as string) to target with the command. - * @returns bash command to remove lines from file. - */ -export function getRemoveLinesCommand(pattern: string, hostOS: Ec2OS, filepath: string): string { - if (pattern.includes('/')) { - throw new ToolkitError(`ec2: cannot match pattern containing '/', given: ${pattern}`) - } - // Linux allows not passing extension to -i, whereas macOS requires zero length extension. - return `sed -i${isLinux(hostOS) ? '' : " ''"} /${pattern}/d ${filepath}` -} - -function isLinux(os: Ec2OS): boolean { - return os === 'Amazon Linux' || os === 'Ubuntu' -}