Skip to content

Commit eeda867

Browse files
authored
refactor(sdkv3): migrate ssm client (#6137)
## Problem There is a lot of duplicate code across the sdk clients. Additionally, we want to only construct the clients once (per wrapper), and have these clients call destroy. ## Solution Note: the line changes are mostly the added dependency of an `@aws-sdk` client and its hundreds of dependencies. - create abstract class `ClientWrapper` to minimize code dupe. - cache sdk clients on creation within the wrappers. - migrate ssm client to sdkv3, using new common wrapper class. ## Alternative Solution - Make all wrappers singletons so clients are created once for the toolkit lifetime. - Pro: Makes it impossible for a client to be re-created. - Con: Zero flexibility when it comes to client lifetime. All sdk clients would be created whenever their wrapper is referenced. Without being very careful, we could create many SDK clients up front that we never use and can't delete. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 66f050c commit eeda867

File tree

12 files changed

+1585
-611
lines changed

12 files changed

+1585
-611
lines changed

package-lock.json

Lines changed: 1367 additions & 467 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,7 @@
511511
"@aws-sdk/property-provider": "3.46.0",
512512
"@aws-sdk/smithy-client": "^3.46.0",
513513
"@aws-sdk/util-arn-parser": "^3.46.0",
514+
"@aws-sdk/client-ssm": "^3.699.0",
514515
"@aws/mynah-ui": "^4.22.1",
515516
"@gerhobbelt/gitignore-parser": "^0.2.0-9",
516517
"@iarna/toml": "^2.2.5",

packages/core/src/awsService/ec2/model.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55
import * as vscode from 'vscode'
6-
import { Session } from 'aws-sdk/clients/ssm'
7-
import { EC2, IAM, SSM } from 'aws-sdk'
6+
import { EC2, IAM } from 'aws-sdk'
87
import { Ec2Selection } from './prompter'
98
import { getOrInstallCli } from '../../shared/utilities/cliUtils'
109
import { isCloud9 } from '../../shared/extensionUtilities'
1110
import { ToolkitError } from '../../shared/errors'
12-
import { SsmClient } from '../../shared/clients/ssmClient'
11+
import { SsmClient } from '../../shared/clients/ssm'
1312
import { Ec2Client } from '../../shared/clients/ec2Client'
1413
import {
1514
VscodeRemoteConnection,
@@ -35,13 +34,14 @@ import { SshConfig } from '../../shared/sshConfig'
3534
import { SshKeyPair } from './sshKeyPair'
3635
import { Ec2SessionTracker } from './remoteSessionManager'
3736
import { getEc2SsmEnv } from './utils'
37+
import { Session, StartSessionResponse } from '@aws-sdk/client-ssm'
3838

3939
export type Ec2ConnectErrorCode = 'EC2SSMStatus' | 'EC2SSMPermission' | 'EC2SSMConnect' | 'EC2SSMAgentStatus'
4040

4141
export interface Ec2RemoteEnv extends VscodeRemoteConnection {
4242
selection: Ec2Selection
4343
keyPair: SshKeyPair
44-
ssmSession: SSM.StartSessionResponse
44+
ssmSession: StartSessionResponse
4545
}
4646

4747
export type Ec2OS = 'Amazon Linux' | 'Ubuntu' | 'macOS'
@@ -51,7 +51,7 @@ interface RemoteUser {
5151
}
5252

5353
export class Ec2Connecter implements vscode.Disposable {
54-
protected ssmClient: SsmClient
54+
protected ssm: SsmClient
5555
protected ec2Client: Ec2Client
5656
protected iamClient: DefaultIamClient
5757
protected sessionManager: Ec2SessionTracker
@@ -65,10 +65,10 @@ export class Ec2Connecter implements vscode.Disposable {
6565
)
6666

6767
public constructor(readonly regionCode: string) {
68-
this.ssmClient = this.createSsmSdkClient()
68+
this.ssm = this.createSsmSdkClient()
6969
this.ec2Client = this.createEc2SdkClient()
7070
this.iamClient = this.createIamSdkClient()
71-
this.sessionManager = new Ec2SessionTracker(regionCode, this.ssmClient)
71+
this.sessionManager = new Ec2SessionTracker(regionCode, this.ssm)
7272
}
7373

7474
protected createSsmSdkClient(): SsmClient {
@@ -83,7 +83,7 @@ export class Ec2Connecter implements vscode.Disposable {
8383
return new DefaultIamClient(this.regionCode)
8484
}
8585

86-
public async addActiveSession(sessionId: SSM.SessionId, instanceId: EC2.InstanceId): Promise<void> {
86+
public async addActiveSession(sessionId: string, instanceId: EC2.InstanceId): Promise<void> {
8787
await this.sessionManager.addSession(instanceId, sessionId)
8888
}
8989

@@ -151,7 +151,7 @@ export class Ec2Connecter implements vscode.Disposable {
151151
}
152152

153153
private async checkForInstanceSsmError(selection: Ec2Selection): Promise<void> {
154-
const isSsmAgentRunning = (await this.ssmClient.getInstanceAgentPingStatus(selection.instanceId)) === 'Online'
154+
const isSsmAgentRunning = (await this.ssm.getInstanceAgentPingStatus(selection.instanceId)) === 'Online'
155155

156156
if (!isSsmAgentRunning) {
157157
this.throwConnectionError('Is SSM Agent running on the target instance?', selection, {
@@ -178,15 +178,15 @@ export class Ec2Connecter implements vscode.Disposable {
178178
shellArgs: shellArgs,
179179
}
180180

181-
await openRemoteTerminal(terminalOptions, () => this.ssmClient.terminateSession(session)).catch((err) => {
181+
await openRemoteTerminal(terminalOptions, () => this.ssm.terminateSession(session)).catch((err) => {
182182
throw ToolkitError.chain(err, 'Failed to open ec2 instance.')
183183
})
184184
}
185185

186186
public async attemptToOpenEc2Terminal(selection: Ec2Selection): Promise<void> {
187187
await this.checkForStartSessionError(selection)
188188
try {
189-
const response = await this.ssmClient.startSession(selection.instanceId)
189+
const response = await this.ssm.startSession(selection.instanceId)
190190
await this.openSessionInTerminal(response, selection)
191191
} catch (err: unknown) {
192192
this.throwConnectionError('', selection, err as Error)
@@ -198,7 +198,7 @@ export class Ec2Connecter implements vscode.Disposable {
198198

199199
const remoteUser = await this.getRemoteUser(selection.instanceId)
200200
const remoteEnv = await this.prepareEc2RemoteEnvWithProgress(selection, remoteUser)
201-
const testSession = await this.ssmClient.startSession(selection.instanceId, 'AWS-StartSSHSession')
201+
const testSession = await this.ssm.startSession(selection.instanceId, 'AWS-StartSSHSession')
202202
try {
203203
await testSshConnection(
204204
remoteEnv.SessionProcess,
@@ -218,7 +218,7 @@ export class Ec2Connecter implements vscode.Disposable {
218218
const message = err instanceof SshError ? 'Testing SSH connection to instance failed' : ''
219219
this.throwConnectionError(message, selection, err as Error)
220220
} finally {
221-
await this.ssmClient.terminateSession(testSession)
221+
await this.ssm.terminateSession(testSession)
222222
}
223223
}
224224

@@ -232,8 +232,8 @@ export class Ec2Connecter implements vscode.Disposable {
232232
return remoteEnv
233233
}
234234

235-
private async startSSMSession(instanceId: string): Promise<SSM.StartSessionResponse> {
236-
const ssmSession = await this.ssmClient.startSession(instanceId, 'AWS-StartSSHSession')
235+
private async startSSMSession(instanceId: string): Promise<StartSessionResponse> {
236+
const ssmSession = await this.ssm.startSession(instanceId, 'AWS-StartSSHSession')
237237
await this.addActiveSession(instanceId, ssmSession.SessionId!)
238238
return ssmSession
239239
}
@@ -308,7 +308,7 @@ export class Ec2Connecter implements vscode.Disposable {
308308
}
309309

310310
private async sendCommandAndWait(instanceId: string, command: string) {
311-
return await this.ssmClient.sendCommandAndWait(instanceId, 'AWS-RunShellScript', {
311+
return await this.ssm.sendCommandAndWait(instanceId, 'AWS-RunShellScript', {
312312
commands: [command],
313313
})
314314
}
@@ -331,7 +331,7 @@ export class Ec2Connecter implements vscode.Disposable {
331331
}
332332

333333
public async getRemoteUser(instanceId: string): Promise<RemoteUser> {
334-
const os = await this.ssmClient.getTargetPlatformName(instanceId)
334+
const os = await this.ssm.getTargetPlatformName(instanceId)
335335
if (os === 'Amazon Linux') {
336336
return { name: 'ec2-user', os }
337337
}

packages/core/src/awsService/ec2/remoteSessionManager.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,30 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { EC2, SSM } from 'aws-sdk'
7-
import { SsmClient } from '../../shared/clients/ssmClient'
6+
import { EC2 } from 'aws-sdk'
7+
import { SsmClient } from '../../shared/clients/ssm'
88
import { Disposable } from 'vscode'
99

10-
export class Ec2SessionTracker extends Map<EC2.InstanceId, SSM.SessionId> implements Disposable {
10+
export class Ec2SessionTracker extends Map<EC2.InstanceId, string> implements Disposable {
1111
public constructor(
1212
readonly regionCode: string,
13-
protected ssmClient: SsmClient
13+
protected ssm: SsmClient
1414
) {
1515
super()
1616
}
1717

18-
public async addSession(instanceId: EC2.InstanceId, sessionId: SSM.SessionId): Promise<void> {
18+
public async addSession(instanceId: EC2.InstanceId, sessionId: string): Promise<void> {
1919
if (this.isConnectedTo(instanceId)) {
2020
const existingSessionId = this.get(instanceId)!
21-
await this.ssmClient.terminateSessionFromId(existingSessionId)
21+
await this.ssm.terminateSessionFromId(existingSessionId)
2222
this.set(instanceId, sessionId)
2323
} else {
2424
this.set(instanceId, sessionId)
2525
}
2626
}
2727

2828
private async disconnectEnv(instanceId: EC2.InstanceId): Promise<void> {
29-
await this.ssmClient.terminateSessionFromId(this.get(instanceId)!)
29+
await this.ssm.terminateSessionFromId(this.get(instanceId)!)
3030
this.delete(instanceId)
3131
}
3232

packages/core/src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { registerCommands } from './commands'
5050
// The following imports the endpoints file, which causes webpack to bundle it in the final output file
5151
import endpoints from '../resources/endpoints.json'
5252
import { showViewLogsMessage } from './shared/utilities/messages'
53+
import { AWSClientBuilderV3 } from './shared/awsClientBuilderV3'
5354
import { setupUninstallHandler } from './shared/handleUninstall'
5455
import { maybeShowMinVscodeWarning } from './shared/extensionStartup'
5556
import { getLogger } from './shared/logger/logger'
@@ -104,6 +105,7 @@ export async function activateCommon(
104105
globals.machineId = await getMachineId()
105106
globals.awsContext = new DefaultAwsContext()
106107
globals.sdkClientBuilder = new DefaultAWSClientBuilder(globals.awsContext)
108+
globals.sdkClientBuilderV3 = new AWSClientBuilderV3(globals.awsContext)
107109
globals.loginManager = new LoginManager(globals.awsContext, new CredentialsStore())
108110

109111
// order matters here

packages/core/src/shared/awsClientBuilderV3.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
BuildMiddleware,
1414
DeserializeHandler,
1515
DeserializeMiddleware,
16+
Handler,
1617
FinalizeHandler,
1718
FinalizeRequestMiddleware,
1819
HandlerExecutionContext,
@@ -37,10 +38,18 @@ export type AwsClientConstructor<C> = new (o: AwsClientOptions) => C
3738

3839
// AWS-SDKv3 does not export generic types for clients so we need to build them as needed
3940
// https://github.com/aws/aws-sdk-js-v3/issues/5856#issuecomment-2096950979
40-
interface AwsClient {
41+
export interface AwsClient {
4142
middlewareStack: {
4243
add: MiddlewareStack<any, MetadataBearer>['add']
4344
}
45+
send: (command: AwsCommand, options?: any) => Promise<any>
46+
destroy: () => void
47+
}
48+
49+
export interface AwsCommand {
50+
input: object
51+
middlewareStack: any
52+
resolveMiddleware: (stack: any, configuration: any, options: any) => Handler<any, any>
4453
}
4554

4655
interface AwsClientOptions {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import * as vscode from 'vscode'
6+
import globals from '../extensionGlobals'
7+
import { AwsClient, AwsClientConstructor, AwsCommand } from '../awsClientBuilderV3'
8+
import { pageableToCollection } from '../utilities/collectionUtils'
9+
10+
export abstract class ClientWrapper<C extends AwsClient> implements vscode.Disposable {
11+
protected client?: C
12+
13+
public constructor(
14+
public readonly regionCode: string,
15+
private readonly clientType: AwsClientConstructor<C>
16+
) {}
17+
18+
protected async getClient() {
19+
if (this.client) {
20+
return this.client
21+
}
22+
this.client = await globals.sdkClientBuilderV3.createAwsService(this.clientType, undefined, this.regionCode)
23+
return this.client!
24+
}
25+
26+
protected async makeRequest<CommandInput extends object, Command extends AwsCommand>(
27+
command: new (o: CommandInput) => Command,
28+
commandOptions: CommandInput
29+
) {
30+
const client = await this.getClient()
31+
return await client.send(new command(commandOptions))
32+
}
33+
34+
protected makePaginatedRequest<
35+
CommandInput extends object,
36+
CommandOutput extends object,
37+
Command extends AwsCommand,
38+
>(
39+
command: new (o: CommandInput) => Command,
40+
commandOptions: CommandInput,
41+
collectKey: keyof CommandOutput & string,
42+
nextTokenKey?: keyof CommandOutput & keyof CommandInput & string
43+
) {
44+
const requester = async (req: CommandInput) => await this.makeRequest(command, req)
45+
const response = pageableToCollection(
46+
requester,
47+
commandOptions,
48+
nextTokenKey ?? ('NextToken' as never),
49+
collectKey
50+
)
51+
return response
52+
}
53+
54+
public dispose() {
55+
this.client?.destroy()
56+
}
57+
}

0 commit comments

Comments
 (0)