Skip to content

Commit f7f72b9

Browse files
authored
ec2: add explorer node w/ dropdown for instances. (#3622)
Problem: Feature currently only accessible from the command palette, and this therefore not discoverable. Solution: As a first step, add a node to the explorer that lists the ec2 instances associated with the account. On hover, we show the instance Id, but show the name in the explorer.
1 parent 91e54e0 commit f7f72b9

File tree

6 files changed

+254
-4
lines changed

6 files changed

+254
-4
lines changed

src/awsexplorer/regionNode.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import { DefaultS3Client } from '../shared/clients/s3Client'
2626
import { DefaultSchemaClient } from '../shared/clients/schemaClient'
2727
import { getEcsRootNode } from '../ecs/model'
2828
import { compareTreeItems, TreeShim } from '../shared/treeview/utils'
29+
import { Ec2ParentNode } from '../ec2/explorer/ec2ParentNode'
30+
import { DevSettings } from '../shared/settings'
31+
import { Ec2Client } from '../shared/clients/ec2Client'
2932

3033
const serviceCandidates = [
3134
{
@@ -44,6 +47,12 @@ const serviceCandidates = [
4447
serviceId: 'logs',
4548
createFn: (regionCode: string) => new CloudWatchLogsNode(regionCode),
4649
},
50+
{
51+
serviceId: 'ec2',
52+
when: () => DevSettings.instance.isDevMode(),
53+
createFn: (regionCode: string, partitionId: string) =>
54+
new Ec2ParentNode(regionCode, partitionId, new Ec2Client(regionCode)),
55+
},
4756
{
4857
serviceId: 'ecr',
4958
createFn: (regionCode: string) => new EcrNode(new DefaultEcrClient(regionCode)),
@@ -106,9 +115,12 @@ export class RegionNode extends AWSTreeNodeBase {
106115
// This interface exists so we can add additional nodes to the array (otherwise Typescript types the array to what's already in the array at creation)
107116
const partitionId = this.regionProvider.getPartitionId(this.regionCode) ?? defaultPartition
108117
const childNodes: AWSTreeNodeBase[] = []
109-
for (const { serviceId, createFn } of serviceCandidates) {
110-
if (this.regionProvider.isServiceInRegion(serviceId, this.regionCode)) {
111-
const node = createFn(this.regionCode, partitionId)
118+
for (const service of serviceCandidates) {
119+
if (service.when !== undefined && !service.when()) {
120+
continue
121+
}
122+
if (this.regionProvider.isServiceInRegion(service.serviceId, this.regionCode)) {
123+
const node = service.createFn(this.regionCode, partitionId)
112124
if (node !== undefined) {
113125
childNodes.push(node)
114126
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { getNameOfInstance } from '../../shared/clients/ec2Client'
7+
import { AWSResourceNode } from '../../shared/treeview/nodes/awsResourceNode'
8+
import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase'
9+
import { Ec2Instance } from '../../shared/clients/ec2Client'
10+
import { build } from '@aws-sdk/util-arn-parser'
11+
import globals from '../../shared/extensionGlobals'
12+
13+
export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode {
14+
public constructor(
15+
public override readonly regionCode: string,
16+
private readonly partitionId: string,
17+
private instance: Ec2Instance,
18+
public override readonly contextValue: string
19+
) {
20+
super('')
21+
this.update(instance)
22+
}
23+
24+
public update(newInstance: Ec2Instance) {
25+
this.setInstance(newInstance)
26+
this.label = this.name
27+
this.tooltip = this.InstanceId
28+
}
29+
30+
public setInstance(newInstance: Ec2Instance) {
31+
this.instance = newInstance
32+
}
33+
34+
public get name(): string {
35+
return getNameOfInstance(this.instance) ?? 'Unnamed instance'
36+
}
37+
38+
public get InstanceId(): string {
39+
return this.instance.InstanceId!
40+
}
41+
42+
public get arn(): string {
43+
return build({
44+
partition: this.partitionId,
45+
service: 'ec2',
46+
region: this.regionCode,
47+
accountId: globals.awsContext.getCredentialAccountId()!,
48+
resource: 'instance',
49+
})
50+
}
51+
}

src/ec2/explorer/ec2ParentNode.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase'
7+
import { makeChildrenNodes } from '../../shared/treeview/utils'
8+
import { PlaceholderNode } from '../../shared/treeview/nodes/placeholderNode'
9+
import { Ec2InstanceNode } from './ec2InstanceNode'
10+
import { Ec2Client } from '../../shared/clients/ec2Client'
11+
import { getNameOfInstance } from '../../shared/clients/ec2Client'
12+
import { updateInPlace } from '../../shared/utilities/collectionUtils'
13+
14+
export const contextValueEc2 = 'awsEc2Node'
15+
16+
export class Ec2ParentNode extends AWSTreeNodeBase {
17+
protected readonly placeHolderMessage = '[No EC2 Instances Found]'
18+
protected readonly ec2InstanceNodes: Map<string, Ec2InstanceNode>
19+
20+
public constructor(
21+
public override readonly regionCode: string,
22+
private readonly partitionId: string,
23+
protected readonly ec2Client: Ec2Client
24+
) {
25+
super('EC2', vscode.TreeItemCollapsibleState.Collapsed)
26+
this.ec2InstanceNodes = new Map<string, Ec2InstanceNode>()
27+
}
28+
29+
public override async getChildren(): Promise<AWSTreeNodeBase[]> {
30+
return await makeChildrenNodes({
31+
getChildNodes: async () => {
32+
await this.updateChildren()
33+
34+
return [...this.ec2InstanceNodes.values()]
35+
},
36+
getNoChildrenPlaceholderNode: async () => new PlaceholderNode(this, this.placeHolderMessage),
37+
sort: (nodeA, nodeB) => nodeA.name.localeCompare(nodeB.name),
38+
})
39+
}
40+
41+
public async updateChildren(): Promise<void> {
42+
const ec2Instances = await (await this.ec2Client.getInstances()).toMap(instance => getNameOfInstance(instance))
43+
44+
updateInPlace(
45+
this.ec2InstanceNodes,
46+
ec2Instances.keys(),
47+
key => this.ec2InstanceNodes.get(key)!.update(ec2Instances.get(key)!),
48+
key => new Ec2InstanceNode(this.regionCode, this.partitionId, ec2Instances.get(key)!, contextValueEc2)
49+
)
50+
}
51+
}

src/ec2/model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export class Ec2ConnectionManager {
121121
} catch (err: unknown) {
122122
// Default error if pre-check fails.
123123
this.throwConnectionError('Unable to connect to target instance. ', selection, {
124-
code: 'EC2SSMAgentStatus',
124+
code: 'EC2SSMConnect',
125125
cause: err as Error,
126126
})
127127
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as assert from 'assert'
7+
import { Ec2InstanceNode } from '../../../ec2/explorer/ec2InstanceNode'
8+
import { Ec2Instance, getNameOfInstance } from '../../../shared/clients/ec2Client'
9+
import { contextValueEc2 } from '../../../ec2/explorer/ec2ParentNode'
10+
11+
describe('ec2InstanceNode', function () {
12+
let testNode: Ec2InstanceNode
13+
let testInstance: Ec2Instance
14+
15+
before(function () {
16+
testInstance = {
17+
InstanceId: 'testId',
18+
Tags: [
19+
{
20+
Key: 'Name',
21+
Value: 'testName',
22+
},
23+
],
24+
}
25+
26+
testNode = new Ec2InstanceNode('testRegion', 'testPartition', testInstance, contextValueEc2)
27+
})
28+
29+
it('instantiates without issue', async function () {
30+
assert.ok(testNode)
31+
})
32+
33+
it('initializes the region code', async function () {
34+
assert.strictEqual(testNode.regionCode, 'testRegion')
35+
})
36+
37+
it('initializes the label', async function () {
38+
assert.strictEqual(testNode.label, getNameOfInstance(testInstance))
39+
})
40+
41+
it('initializes the functionName', async function () {
42+
assert.strictEqual(testNode.name, getNameOfInstance(testInstance))
43+
})
44+
45+
it('initializes the tooltip', async function () {
46+
assert.strictEqual(testNode.tooltip, testInstance.InstanceId)
47+
})
48+
49+
it('has no children', async function () {
50+
const childNodes = await testNode.getChildren()
51+
assert.ok(childNodes)
52+
assert.strictEqual(childNodes.length, 0, 'Expected node to have no children')
53+
})
54+
})
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as assert from 'assert'
7+
import { Ec2ParentNode, contextValueEc2 } from '../../../ec2/explorer/ec2ParentNode'
8+
import { stub } from '../../utilities/stubber'
9+
import { Ec2Client } from '../../../shared/clients/ec2Client'
10+
import { intoCollection } from '../../../shared/utilities/collectionUtils'
11+
import {
12+
assertNodeListOnlyHasErrorNode,
13+
assertNodeListOnlyHasPlaceholderNode,
14+
} from '../../utilities/explorerNodeAssertions'
15+
import { Ec2InstanceNode } from '../../../ec2/explorer/ec2InstanceNode'
16+
17+
describe('ec2ParentNode', function () {
18+
let testNode: Ec2ParentNode
19+
let instanceNames: string[]
20+
const testRegion = 'testRegion'
21+
const testPartition = 'testPartition'
22+
23+
function createClient() {
24+
const client = stub(Ec2Client, { regionCode: testRegion })
25+
client.getInstances.callsFake(async () =>
26+
intoCollection(
27+
instanceNames.map(name => ({ InstanceId: name + name, Tags: [{ Key: 'Name', Value: name }] }))
28+
)
29+
)
30+
31+
return client
32+
}
33+
34+
beforeEach(function () {
35+
instanceNames = ['firstOne', 'secondOne']
36+
testNode = new Ec2ParentNode(testRegion, testPartition, createClient())
37+
})
38+
39+
it('returns placeholder node if no children are present', async function () {
40+
instanceNames = []
41+
42+
const childNodes = await testNode.getChildren()
43+
44+
assertNodeListOnlyHasPlaceholderNode(childNodes)
45+
})
46+
47+
it('has instance child nodes', async function () {
48+
const childNodes = await testNode.getChildren()
49+
50+
assert.strictEqual(childNodes.length, instanceNames.length, 'Unexpected child count')
51+
52+
childNodes.forEach(node =>
53+
assert.ok(node instanceof Ec2InstanceNode, 'Expected child node to be Ec2InstanceNode')
54+
)
55+
})
56+
57+
it('has child nodes with ec2 contextValuue', async function () {
58+
const childNodes = await testNode.getChildren()
59+
60+
childNodes.forEach(node =>
61+
assert.strictEqual(node.contextValue, contextValueEc2, 'expected the node to have a ec2 contextValue')
62+
)
63+
})
64+
65+
it('sorts child nodes', async function () {
66+
const sortedText = ['aa', 'ab', 'bb', 'bc', 'cc', 'cd']
67+
instanceNames = ['ab', 'bb', 'bc', 'aa', 'cc', 'cd']
68+
69+
const childNodes = await testNode.getChildren()
70+
71+
const actualChildOrder = childNodes.map(node => node.label)
72+
assert.deepStrictEqual(actualChildOrder, sortedText, 'Unexpected child sort order')
73+
})
74+
75+
it('has an error node for a child if an error happens during loading', async function () {
76+
const client = createClient()
77+
client.getInstances.throws(new Error())
78+
79+
const node = new Ec2ParentNode(testRegion, testPartition, client)
80+
assertNodeListOnlyHasErrorNode(await node.getChildren())
81+
})
82+
})

0 commit comments

Comments
 (0)