Skip to content

Commit c4f4462

Browse files
authored
feat(ec2): start, stop, reboot #3658
Problem: Missing useful actions on EC2 items in AWS Explorer. Solution: - Add new commands: `aws.ec2.startInstance`, `aws.ec2.stopInstance`, and `aws.ec2.rebootInstance`. - QuickPick only lists valid commands for action. Ex. doesn't show stopped containers for `aws.ec2.stopinstance`. - Commands will throw error if status already achieved. (Ex: stopping an already stopped instance). Can only be achieved on right-click of node. - Accessible from both command palette and the explorer. - Loading bar w/ cancel for all operations. - Also refactor the `prompter` code into its own object, to promote flexibility and testability (the prompter now has tests as well). - Does not show feedback on successful start/stop/reboot, only on failure. - Does not throw error if user runs reboot on stopped instance, just start it. TODO: - Add a way to check the status of an instance before starting/stopping.
1 parent b605cd9 commit c4f4462

File tree

13 files changed

+664
-333
lines changed

13 files changed

+664
-333
lines changed

package.json

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,18 @@
11201120
"command": "aws.ec2.openTerminal",
11211121
"when": "aws.isDevMode"
11221122
},
1123+
{
1124+
"command": "aws.ec2.startInstance",
1125+
"when": "aws.isDevMode"
1126+
},
1127+
{
1128+
"command": "aws.ec2.stopInstance",
1129+
"whem": "aws.isDevMode"
1130+
},
1131+
{
1132+
"command": "aws.ec2.rebootInstance",
1133+
"whem": "aws.isDevMode"
1134+
},
11231135
{
11241136
"command": "aws.dev.openMenu",
11251137
"when": "aws.isDevMode || isCloud9"
@@ -1324,6 +1336,21 @@
13241336
"group": "inline@1",
13251337
"when": "viewItem == awsEc2Node"
13261338
},
1339+
{
1340+
"command": "aws.ec2.startInstance",
1341+
"group": "0@1",
1342+
"when": "viewItem == awsEc2Node"
1343+
},
1344+
{
1345+
"command": "aws.ec2.stopInstance",
1346+
"group": "0@1",
1347+
"when": "viewItem == awsEc2Node"
1348+
},
1349+
{
1350+
"command": "aws.ec2.rebootInstance",
1351+
"group": "0@1",
1352+
"when": "viewItem == awsEc2Node"
1353+
},
13271354
{
13281355
"command": "aws.ecr.createRepository",
13291356
"when": "view == aws.explorer && viewItem == awsEcrNode",
@@ -2092,6 +2119,36 @@
20922119
}
20932120
}
20942121
},
2122+
{
2123+
"command": "aws.ec2.startInstance",
2124+
"title": "%AWS.command.ec2.startInstance%",
2125+
"category": "%AWS.title%",
2126+
"cloud9": {
2127+
"cn": {
2128+
"category": "%AWS.title.cn%"
2129+
}
2130+
}
2131+
},
2132+
{
2133+
"command": "aws.ec2.stopInstance",
2134+
"title": "%AWS.command.ec2.stopInstance%",
2135+
"category": "%AWS.title%",
2136+
"cloud9": {
2137+
"cn": {
2138+
"category": "%AWS.title.cn%"
2139+
}
2140+
}
2141+
},
2142+
{
2143+
"command": "aws.ec2.rebootInstance",
2144+
"title": "%AWS.command.ec2.rebootInstance%",
2145+
"category": "%AWS.title%",
2146+
"cloud9": {
2147+
"cn": {
2148+
"category": "%AWS.title.cn%"
2149+
}
2150+
}
2151+
},
20952152
{
20962153
"command": "aws.ec2.copyInstanceId",
20972154
"title": "%AWS.command.ec2.copyInstanceId%",

package.nls.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@
110110
"AWS.command.cdk.help": "View CDK Documentation",
111111
"AWS.command.ec2.openTerminal": "Open terminal to EC2 instance...",
112112
"AWS.command.ec2.openRemoteConnection": "Connect to EC2 instance in New Window...",
113+
"AWS.command.ec2.startInstance": "Start EC2 Instance",
114+
"AWS.command.ec2.stopInstance": "Stop EC2 Instance",
115+
"AWS.command.ec2.rebootInstance": "Reboot EC2 Instance",
113116
"AWS.command.ec2.copyInstanceId": "Copy Instance Id",
114117
"AWS.command.ecr.copyTagUri": "Copy Tag URI",
115118
"AWS.command.ecr.copyRepositoryUri": "Copy Repository URI",

src/ec2/activation.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,34 @@ import { telemetry } from '../shared/telemetry/telemetry'
88
import { Ec2InstanceNode } from './explorer/ec2InstanceNode'
99
import { copyTextCommand } from '../awsexplorer/commands/copyText'
1010
import { Ec2Node } from './explorer/ec2ParentNode'
11-
import { openRemoteConnection, openTerminal } from './commands'
11+
import { openRemoteConnection, openTerminal, rebootInstance, startInstance, stopInstance } from './commands'
1212

1313
export async function activate(ctx: ExtContext): Promise<void> {
1414
ctx.extensionContext.subscriptions.push(
1515
Commands.register('aws.ec2.openTerminal', async (node?: Ec2Node) => {
1616
await telemetry.ec2_connectToInstance.run(async span => {
1717
span.record({ ec2ConnectionType: 'ssm' })
18-
await (node ? openTerminal(node) : openTerminal(node))
18+
await openTerminal(node)
1919
})
2020
}),
2121

2222
Commands.register('aws.ec2.copyInstanceId', async (node: Ec2InstanceNode) => {
2323
await copyTextCommand(node, 'id')
2424
}),
2525
Commands.register('aws.ec2.openRemoteConnection', async (node?: Ec2Node) => {
26-
await (node ? openRemoteConnection(node) : openRemoteConnection(node))
26+
await openRemoteConnection(node)
27+
}),
28+
29+
Commands.register('aws.ec2.startInstance', async (node?: Ec2Node) => {
30+
await startInstance(node)
31+
}),
32+
33+
Commands.register('aws.ec2.stopInstance', async (node?: Ec2Node) => {
34+
await stopInstance(node)
35+
}),
36+
37+
Commands.register('aws.ec2.rebootInstance', async (node?: Ec2Node) => {
38+
await rebootInstance(node)
2739
})
2840
)
2941
}

src/ec2/commands.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,50 @@
66
import { Ec2InstanceNode } from './explorer/ec2InstanceNode'
77
import { Ec2Node } from './explorer/ec2ParentNode'
88
import { Ec2ConnectionManager } from './model'
9+
import { Ec2Prompter, instanceFilter } from './prompter'
10+
import { Ec2Selection } from './prompter'
11+
import { Ec2Client, Ec2Instance } from '../shared/clients/ec2Client'
912
import { copyToClipboard } from '../shared/utilities/messages'
10-
import { promptUserForEc2Selection } from './prompter'
1113

1214
export async function openTerminal(node?: Ec2Node) {
13-
const selection = node instanceof Ec2InstanceNode ? node.toSelection() : await promptUserForEc2Selection()
15+
const selection = await getSelection(node)
1416

1517
const connectionManager = new Ec2ConnectionManager(selection.region)
1618
await connectionManager.attemptToOpenEc2Terminal(selection)
1719
}
1820

1921
export async function openRemoteConnection(node?: Ec2Node) {
20-
const selection = node instanceof Ec2InstanceNode ? node.toSelection() : await promptUserForEc2Selection()
22+
const selection = await getSelection(node)
2123
//const connectionManager = new Ec2ConnectionManager(selection.region)
2224
console.log(selection)
2325
}
2426

27+
export async function startInstance(node?: Ec2Node) {
28+
const prompterFilter = (instance: Ec2Instance) => instance.status !== 'running'
29+
const selection = await getSelection(node, prompterFilter)
30+
const client = new Ec2Client(selection.region)
31+
await client.startInstanceWithCancel(selection.instanceId)
32+
}
33+
34+
export async function stopInstance(node?: Ec2Node) {
35+
const prompterFilter = (instance: Ec2Instance) => instance.status !== 'stopped'
36+
const selection = await getSelection(node, prompterFilter)
37+
const client = new Ec2Client(selection.region)
38+
await client.stopInstanceWithCancel(selection.instanceId)
39+
}
40+
41+
export async function rebootInstance(node?: Ec2Node) {
42+
const selection = await getSelection(node)
43+
const client = new Ec2Client(selection.region)
44+
await client.rebootInstanceWithCancel(selection.instanceId)
45+
}
46+
47+
async function getSelection(node?: Ec2Node, filter?: instanceFilter): Promise<Ec2Selection> {
48+
const prompter = new Ec2Prompter(filter)
49+
const selection = node && node instanceof Ec2InstanceNode ? node.toSelection() : await prompter.promptUser()
50+
return selection
51+
}
52+
2553
export async function copyInstanceId(instanceId: string): Promise<void> {
2654
await copyToClipboard(instanceId, 'Id')
2755
}

src/ec2/explorer/ec2InstanceNode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { AWSResourceNode } from '../../shared/treeview/nodes/awsResourceNode'
88
import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase'
99
import { Ec2Instance } from '../../shared/clients/ec2Client'
1010
import globals from '../../shared/extensionGlobals'
11-
import { Ec2Selection } from '../utils'
11+
import { Ec2Selection } from '../prompter'
1212

1313
export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode {
1414
public constructor(

src/ec2/model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import * as vscode from 'vscode'
66
import { Session } from 'aws-sdk/clients/ssm'
77
import { IAM } from 'aws-sdk'
8-
import { Ec2Selection } from './utils'
8+
import { Ec2Selection } from './prompter'
99
import { getOrInstallCli } from '../shared/utilities/cliUtils'
1010
import { isCloud9 } from '../shared/extensionUtilities'
1111
import { ToolkitError } from '../shared/errors'

src/ec2/prompter.ts

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,65 @@
44
*/
55

66
import { RegionSubmenu, RegionSubmenuResponse } from '../shared/ui/common/regionSubmenu'
7-
import { Ec2Selection, getInstancesFromRegion } from './utils'
87
import { DataQuickPickItem } from '../shared/ui/pickerPrompter'
9-
import { Ec2Instance } from '../shared/clients/ec2Client'
8+
import { Ec2Client, Ec2Instance } from '../shared/clients/ec2Client'
109
import { isValidResponse } from '../shared/wizards/wizard'
1110
import { CancellationError } from '../shared/utilities/timeoutUtils'
11+
import { AsyncCollection } from '../shared/utilities/asyncCollection'
1212

13-
function asQuickpickItem(instance: Ec2Instance): DataQuickPickItem<string> {
14-
return {
15-
label: '$(terminal) \t' + (instance.name ?? '(no name)'),
16-
detail: instance.InstanceId,
17-
data: instance.InstanceId,
18-
}
13+
export type instanceFilter = (instance: Ec2Instance) => boolean
14+
export interface Ec2Selection {
15+
instanceId: string
16+
region: string
1917
}
2018

21-
export async function promptUserForEc2Selection(): Promise<Ec2Selection> {
22-
const prompter = createEc2ConnectPrompter()
23-
const response = await prompter.prompt()
19+
export class Ec2Prompter {
20+
public constructor(protected filter?: instanceFilter) {}
2421

25-
if (isValidResponse(response)) {
26-
return handleEc2ConnectPrompterResponse(response)
27-
} else {
28-
throw new CancellationError('user')
22+
protected static asQuickPickItem(instance: Ec2Instance): DataQuickPickItem<string> {
23+
return {
24+
label: '$(terminal) \t' + (instance.name ?? '(no name)'),
25+
detail: instance.InstanceId,
26+
data: instance.InstanceId,
27+
}
2928
}
30-
}
3129

32-
export function handleEc2ConnectPrompterResponse(response: RegionSubmenuResponse<string>): Ec2Selection {
33-
return {
34-
instanceId: response.data,
35-
region: response.region,
30+
protected static getSelectionFromResponse(response: RegionSubmenuResponse<string>): Ec2Selection {
31+
return {
32+
instanceId: response.data,
33+
region: response.region,
34+
}
35+
}
36+
37+
public async promptUser(): Promise<Ec2Selection> {
38+
const prompter = this.createEc2ConnectPrompter()
39+
const response = await prompter.prompt()
40+
41+
if (isValidResponse(response)) {
42+
return Ec2Prompter.getSelectionFromResponse(response)
43+
} else {
44+
throw new CancellationError('user')
45+
}
3646
}
37-
}
3847

39-
export function createEc2ConnectPrompter(): RegionSubmenu<string> {
40-
return new RegionSubmenu(
41-
async region => (await getInstancesFromRegion(region)).map(asQuickpickItem).promise(),
42-
{ title: 'Select EC2 Instance', matchOnDetail: true },
43-
{ title: 'Select Region for EC2 Instance' },
44-
'Instances'
45-
)
48+
protected async getInstancesFromRegion(regionCode: string): Promise<AsyncCollection<Ec2Instance>> {
49+
const client = new Ec2Client(regionCode)
50+
return await client.getInstances()
51+
}
52+
53+
protected async getInstancesAsQuickPickItems(region: string): Promise<DataQuickPickItem<string>[]> {
54+
return (await this.getInstancesFromRegion(region))
55+
.filter(this.filter ? instance => this.filter!(instance) : instance => true)
56+
.map(instance => Ec2Prompter.asQuickPickItem(instance))
57+
.promise()
58+
}
59+
60+
private createEc2ConnectPrompter(): RegionSubmenu<string> {
61+
return new RegionSubmenu(
62+
async region => this.getInstancesAsQuickPickItems(region),
63+
{ title: 'Select EC2 Instance', matchOnDetail: true },
64+
{ title: 'Select Region for EC2 Instance' },
65+
'Instances'
66+
)
67+
}
4668
}

src/ec2/utils.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)