Skip to content

Commit 4535bf3

Browse files
ec2: improve QuickPick UI (#3596)
* add empty connect command * create sample QuickPick on click * hacky prompt + func to list instanceId * very hacky way to create prompt with instanceIds * move extractInstanceIds to own function * utilize lastTouchedRegion in prompt * throw cancellationError on user cancel * add a title to quickpick * refactor: increase testability * move test utility func to shared utility file * add general test cases for extractInstanceIdsFromReservations * add basic test for prompter * configure command to devMode * refactor to utilize wizard + integrate regionSubmenu * add new testing file * refactor to utilize regionSubmenu in isolation, rather than wrapped in Wizard class * fix awkward indent * add test file from feature/cwl branch * remove old prompter old + tests * delete tests that rely on feature/cwl changes * refactor to avoid circular dependency * fix improper headers + imports * introduce Ec2ConnectClient to handle connection * remove dead parameter * close connection on terminal close * add a little more to error msg * remove dead imports * log error to console * rename function Co-authored-by: Justin M. Keyes <[email protected]> * fix header Co-authored-by: Justin M. Keyes <[email protected]> * fix headers and imports * remove year from header * delete outdated test case * delete outdated test file * remove year from copyright header * fix formatting of files * remove alias in Ec2Instance * utilize interface for object-like shape * improve style + start of work on debugging error * make distinction between status error and permission error * start to add tests for error handling * move code to general Ec2Client * change Log Groups to selection in region submenu * refactor to avoid circular dependency * fix formatting * fix formatting * comment out the ec2Client we are not using * style fix * fix formatting * add test for error handling * fix formatting * add extra wrapper * generalize some code to a remoteSession file * add space between pieces * retrieve IAM role attached to instance when fails to connect * remove dead import * refactor to avoid circular dependency * checks if relevant policies are on attached role * use both probes to determine source of error * update tests * change callback to async/await * remove onError parameter and utilize .catch instead * add test for permissions detection * fix test * make error handling a try-catch * swap ec2Client to v3 * switch over SSM client * update tests and signatures for new error type in v3 * fix formatting * remove dead import * avoid unnecessary default prefix on client name Co-authored-by: Justin M. Keyes <[email protected]> * fix old import * refactor defaultEc2client -> ec2Client * rename Ec2ConnectClient * fix outdated imports in the test * add icons and detail to description * update tests to use sdk3 * update tests and function names * bubble error up * update test case * Remove please in text Co-authored-by: Justin M. Keyes <[email protected]> * Remove please in text in other error Co-authored-by: Justin M. Keyes <[email protected]> * remove unnecessary types and update tests to match * add match on detail, and instance id label * undo migration within package files and high-level model file * update tests to downgrade * undo some minor tweaks to the clients * use v2 import in the test file * display no-name instead of id when name not present * change Ec2Instance to extends Ec2.Instance * use 'get' over 'extract' in language. Co-authored-by: Justin M. Keyes <[email protected]> * update references to getInstancesFromReservations --------- Co-authored-by: Justin M. Keyes <[email protected]>
1 parent a997df9 commit 4535bf3

File tree

6 files changed

+61
-37
lines changed

6 files changed

+61
-37
lines changed

src/ec2/prompter.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
*/
55

66
import { RegionSubmenu, RegionSubmenuResponse } from '../shared/ui/common/regionSubmenu'
7-
import { Ec2Selection, getInstanceIdsFromRegion } from './utils'
7+
import { Ec2Selection, getInstancesFromRegion } from './utils'
88
import { DataQuickPickItem } from '../shared/ui/pickerPrompter'
9+
import { Ec2Instance } from '../shared/clients/ec2Client'
910

10-
function asQuickpickItem(instanceId: string): DataQuickPickItem<string> {
11+
function asQuickpickItem(instance: Ec2Instance): DataQuickPickItem<string> {
1112
return {
12-
label: instanceId,
13-
data: instanceId,
13+
label: '$(terminal) \t' + (instance.name ?? '(no name)'),
14+
detail: instance.InstanceId,
15+
data: instance.InstanceId,
1416
}
1517
}
1618

@@ -23,8 +25,8 @@ export function handleEc2ConnectPrompterResponse(response: RegionSubmenuResponse
2325

2426
export function createEc2ConnectPrompter(): RegionSubmenu<string> {
2527
return new RegionSubmenu(
26-
async region => (await getInstanceIdsFromRegion(region)).map(asQuickpickItem).promise(),
27-
{ title: 'Select EC2 Instance Id' },
28+
async region => (await getInstancesFromRegion(region)).map(asQuickpickItem).promise(),
29+
{ title: 'Select EC2 Instance Id', matchOnDetail: true },
2830
{ title: 'Select Region for EC2 Instance' },
2931
'Instances'
3032
)

src/ec2/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
*/
55

66
import { AsyncCollection } from '../shared/utilities/asyncCollection'
7-
import { Ec2Client } from '../shared/clients/ec2Client'
7+
import { Ec2Client, Ec2Instance } from '../shared/clients/ec2Client'
88

99
export interface Ec2Selection {
1010
instanceId: string
1111
region: string
1212
}
1313

14-
export async function getInstanceIdsFromRegion(regionCode: string): Promise<AsyncCollection<string>> {
14+
export async function getInstancesFromRegion(regionCode: string): Promise<AsyncCollection<Ec2Instance>> {
1515
const client = new Ec2Client(regionCode)
16-
return await client.getInstanceIds()
16+
return await client.getInstances()
1717
}

src/shared/clients/ec2Client.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,44 @@
44
*/
55

66
import { EC2 } from 'aws-sdk'
7-
import globals from '../extensionGlobals'
87
import { AsyncCollection } from '../utilities/asyncCollection'
98
import { pageableToCollection } from '../utilities/collectionUtils'
109
import { IamInstanceProfile } from 'aws-sdk/clients/ec2'
10+
import globals from '../extensionGlobals'
11+
12+
export interface Ec2Instance extends EC2.Instance {
13+
name?: string
14+
}
1115

1216
export class Ec2Client {
1317
public constructor(public readonly regionCode: string) {}
1418

1519
private async createSdkClient(): Promise<EC2> {
1620
return await globals.sdkClientBuilder.createAwsService(EC2, undefined, this.regionCode)
1721
}
18-
public async getInstanceIds(): Promise<AsyncCollection<string>> {
22+
public async getInstances(): Promise<AsyncCollection<Ec2Instance>> {
1923
const client = await this.createSdkClient()
2024
const requester = async (request: EC2.DescribeInstancesRequest) => client.describeInstances(request).promise()
25+
const collection = pageableToCollection(requester, {}, 'NextToken', 'Reservations')
26+
const instances = this.getInstancesFromReservations(collection)
27+
return instances
28+
}
2129

22-
const instanceIds = this.extractInstanceIdsFromReservations(
23-
pageableToCollection(requester, {}, 'NextToken', 'Reservations')
24-
)
25-
return instanceIds
30+
private lookupTagKey(tags: EC2.Tag[], targetKey: string): string | undefined {
31+
return tags.filter(tag => tag.Key == targetKey)[0].Value
2632
}
2733

28-
public extractInstanceIdsFromReservations(
34+
public getInstancesFromReservations(
2935
reservations: AsyncCollection<EC2.ReservationList | undefined>
30-
): AsyncCollection<string> {
36+
): AsyncCollection<Ec2Instance> {
3137
return reservations
3238
.flatten()
3339
.map(instanceList => instanceList?.Instances)
3440
.flatten()
35-
.map(instance => instance?.InstanceId)
36-
.filter(instanceId => instanceId !== undefined)
41+
.filter(instance => instance!.InstanceId !== undefined)
42+
.map(instance => {
43+
return instance!.Tags ? { ...instance, name: this.lookupTagKey(instance!.Tags!, 'Name') } : instance!
44+
})
3745
}
3846

3947
public async getInstanceStatus(instanceId: string): Promise<EC2.InstanceStateName> {

src/shared/clients/ssmClient.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
*/
55

66
import { AWSError, SSM } from 'aws-sdk'
7-
import globals from '../extensionGlobals'
8-
import { PromiseResult } from 'aws-sdk/lib/request'
97
import { getLogger } from '../logger/logger'
8+
import { PromiseResult } from 'aws-sdk/lib/request'
9+
import globals from '../extensionGlobals'
1010

1111
export class SsmClient {
1212
public constructor(public readonly regionCode: string) {}
@@ -26,7 +26,8 @@ export class SsmClient {
2626
.catch(err => {
2727
getLogger().warn(`ssm: failed to terminate session "${sessionId}": %s`, err)
2828
})
29-
return termination
29+
30+
return termination!
3031
}
3132

3233
public async startSession(target: string): Promise<PromiseResult<SSM.StartSessionResponse, AWSError>> {

src/test/ec2/model.test.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import * as assert from 'assert'
77
import { Ec2ConnectErrorCode, Ec2ConnectionManager } from '../../ec2/model'
88
import { SsmClient } from '../../shared/clients/ssmClient'
99
import { Ec2Client } from '../../shared/clients/ec2Client'
10-
import { AWSError } from 'aws-sdk'
1110
import { attachedPoliciesListType } from 'aws-sdk/clients/iam'
1211
import { Ec2Selection } from '../../ec2/utils'
1312
import { ToolkitError } from '../../shared/errors'
13+
import { AWSError, EC2 } from 'aws-sdk'
1414

1515
describe('Ec2ConnectClient', function () {
1616
class MockSsmClient extends SsmClient {
@@ -24,8 +24,8 @@ describe('Ec2ConnectClient', function () {
2424
super('test-region')
2525
}
2626

27-
public override async getInstanceStatus(instanceId: string): Promise<string> {
28-
return instanceId.split(':')[0]
27+
public override async getInstanceStatus(instanceId: string): Promise<EC2.InstanceStateName> {
28+
return instanceId.split(':')[0] as EC2.InstanceStateName
2929
}
3030
}
3131

@@ -44,7 +44,7 @@ describe('Ec2ConnectClient', function () {
4444
}
4545
describe('handleStartSessionError', async function () {
4646
let client: MockEc2ConnectClientForError
47-
let dummyError: AWSError
47+
const dummyError: AWSError = { name: 'testName', message: 'testMessage', code: 'testCode', time: new Date() }
4848

4949
class MockEc2ConnectClientForError extends MockEc2ConnectClient {
5050
public override async hasProperPolicies(instanceId: string): Promise<boolean> {
@@ -53,18 +53,17 @@ describe('Ec2ConnectClient', function () {
5353
}
5454
before(function () {
5555
client = new MockEc2ConnectClientForError()
56-
dummyError = {} as AWSError
5756
})
5857

5958
it('determines which error to throw based on if instance is running', async function () {
6059
async function assertThrowsErrorCode(testInstance: Ec2Selection, errCode: Ec2ConnectErrorCode) {
6160
try {
6261
await client.handleStartSessionError(dummyError, testInstance)
63-
throw new assert.AssertionError({ message: "Didn't throw any error" })
6462
} catch (err: unknown) {
6563
assert.strictEqual((err as ToolkitError).code, errCode)
6664
}
6765
}
66+
6867
await assertThrowsErrorCode(
6968
{
7069
instanceId: 'pending:noPolicies',

src/test/shared/clients/defaultEc2Client.test.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55

66
import * as assert from 'assert'
77
import { AsyncCollection } from '../../../shared/utilities/asyncCollection'
8-
import { EC2 } from 'aws-sdk'
98
import { toCollection } from '../../../shared/utilities/asyncCollection'
109
import { intoCollection } from '../../../shared/utilities/collectionUtils'
1110
import { Ec2Client } from '../../../shared/clients/ec2Client'
11+
import { EC2 } from 'aws-sdk'
1212

13-
describe('extractInstanceIdsFromReservations', function () {
13+
describe('extractInstancesFromReservations', function () {
1414
const client = new Ec2Client('')
1515
it('returns empty when given empty collection', async function () {
1616
const actualResult = await client
17-
.extractInstanceIdsFromReservations(
17+
.getInstancesFromReservations(
1818
toCollection(async function* () {
1919
yield []
2020
}) as AsyncCollection<EC2.ReservationList>
@@ -30,27 +30,37 @@ describe('extractInstanceIdsFromReservations', function () {
3030
Instances: [
3131
{
3232
InstanceId: 'id1',
33+
Tags: [{ Key: 'Name', Value: 'name1' }],
3334
},
3435
{
3536
InstanceId: 'id2',
37+
Tags: [{ Key: 'Name', Value: 'name2' }],
3638
},
3739
],
3840
},
3941
{
4042
Instances: [
4143
{
4244
InstanceId: 'id3',
45+
Tags: [{ Key: 'Name', Value: 'name3' }],
4346
},
4447
{
4548
InstanceId: 'id4',
49+
Tags: [{ Key: 'Name', Value: 'name4' }],
4650
},
4751
],
4852
},
4953
]
50-
const actualResult = await client
51-
.extractInstanceIdsFromReservations(intoCollection([testReservationsList]))
52-
.promise()
53-
assert.deepStrictEqual(['id1', 'id2', 'id3', 'id4'], actualResult)
54+
const actualResult = await client.getInstancesFromReservations(intoCollection([testReservationsList])).promise()
55+
assert.deepStrictEqual(
56+
[
57+
{ InstanceId: 'id1', name: 'name1', Tags: [{ Key: 'Name', Value: 'name1' }] },
58+
{ InstanceId: 'id2', name: 'name2', Tags: [{ Key: 'Name', Value: 'name2' }] },
59+
{ InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] },
60+
{ InstanceId: 'id4', name: 'name4', Tags: [{ Key: 'Name', Value: 'name4' }] },
61+
],
62+
actualResult
63+
)
5464
}),
5565
// Unsure if this test case is needed, but the return type in the SDK makes it possible these are unknown/not returned.
5666
it('handles undefined and missing pieces in the ReservationList.', async function () {
@@ -69,14 +79,18 @@ describe('extractInstanceIdsFromReservations', function () {
6979
Instances: [
7080
{
7181
InstanceId: 'id3',
82+
Tags: [{ Key: 'Name', Value: 'name3' }],
7283
},
7384
{},
7485
],
7586
},
7687
]
7788
const actualResult = await client
78-
.extractInstanceIdsFromReservations(intoCollection([testReservationsList]))
89+
.getInstancesFromReservations(intoCollection([testReservationsList]))
7990
.promise()
80-
assert.deepStrictEqual(['id1', 'id3'], actualResult)
91+
assert.deepStrictEqual(
92+
[{ InstanceId: 'id1' }, { InstanceId: 'id3', name: 'name3', Tags: [{ Key: 'Name', Value: 'name3' }] }],
93+
actualResult
94+
)
8195
})
8296
})

0 commit comments

Comments
 (0)