Skip to content

Commit 52f1011

Browse files
committed
test: add fake clone
1 parent b353118 commit 52f1011

File tree

1 file changed

+260
-0
lines changed

1 file changed

+260
-0
lines changed
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { AWSError, EC2 } from 'aws-sdk'
7+
import { AsyncCollection } from '../utilities/asyncCollection'
8+
import { pageableToCollection } from '../utilities/collectionUtils'
9+
import { IamInstanceProfile } from 'aws-sdk/clients/ec2'
10+
import globals from '../extensionGlobals'
11+
import { PromiseResult } from 'aws-sdk/lib/request'
12+
import { Timeout } from '../utilities/timeoutUtils'
13+
import { showMessageWithCancel } from '../utilities/messages'
14+
import { ToolkitError, isAwsError } from '../errors'
15+
import { decodeBase64 } from '../utilities/textUtilities'
16+
17+
/**
18+
* A wrapper around EC2.Instance where we can safely assume InstanceId field exists.
19+
*/
20+
export interface SafeEc2Instance extends EC2.Instance {
21+
InstanceId: string
22+
Name?: string
23+
LastSeenStatus: EC2.InstanceStateName
24+
}
25+
26+
interface SafeEc2GetConsoleOutputResult extends EC2.GetConsoleOutputRequest {
27+
Output: string
28+
InstanceId: string
29+
}
30+
31+
export class Ec2Client {
32+
public constructor(public readonly regionCode: string) {}
33+
34+
private async createSdkClient(): Promise<EC2> {
35+
return await globals.sdkClientBuilder.createAwsService(EC2, undefined, this.regionCode)
36+
}
37+
38+
public async getInstances(filters?: EC2.Filter[]): Promise<AsyncCollection<SafeEc2Instance>> {
39+
const client = await this.createSdkClient()
40+
41+
const requester = async (request: EC2.DescribeInstancesRequest) => client.describeInstances(request).promise()
42+
const collection = pageableToCollection(
43+
requester,
44+
filters ? { Filters: filters } : {},
45+
'NextToken',
46+
'Reservations'
47+
)
48+
const extractedInstances = this.getInstancesFromReservations(collection)
49+
const instances = await this.updateInstancesDetail(extractedInstances)
50+
51+
return instances
52+
}
53+
54+
/** Updates status and name in-place for displaying to humans. */
55+
protected async updateInstancesDetail(
56+
instances: AsyncCollection<EC2.Instance>
57+
): Promise<AsyncCollection<SafeEc2Instance>> {
58+
// Intermediate interface so that I can coerce EC2.Instance to SafeEc2Instnace
59+
interface SafeEc2InstanceWithoutStatus extends EC2.Instance {
60+
InstanceId: string
61+
Name?: string
62+
}
63+
64+
const safeInstances: AsyncCollection<SafeEc2InstanceWithoutStatus> = instances.filter(
65+
(instance) => instance.InstanceId !== undefined
66+
)
67+
68+
return safeInstances
69+
.map(async (instance) => {
70+
return { ...instance, LastSeenStatus: await this.getInstanceStatus(instance.InstanceId) }
71+
})
72+
.map((instance) => {
73+
return instanceHasName(instance!)
74+
? { ...instance, Name: lookupTagKey(instance!.Tags!, 'Name') }
75+
: instance!
76+
})
77+
}
78+
79+
public getInstancesFromReservations(
80+
reservations: AsyncCollection<EC2.ReservationList | undefined>
81+
): AsyncCollection<EC2.Instance> {
82+
return reservations
83+
.flatten()
84+
.map((instanceList) => instanceList?.Instances)
85+
.flatten()
86+
.filter((instance) => instance!.InstanceId !== undefined)
87+
}
88+
89+
public async getInstanceStatus(instanceId: string): Promise<EC2.InstanceStateName> {
90+
const client = await this.createSdkClient()
91+
const requester = async (request: EC2.DescribeInstanceStatusRequest) =>
92+
client.describeInstanceStatus(request).promise()
93+
94+
const response = await pageableToCollection(
95+
requester,
96+
{ InstanceIds: [instanceId], IncludeAllInstances: true },
97+
'NextToken',
98+
'InstanceStatuses'
99+
)
100+
.flatten()
101+
.map((instanceStatus) => instanceStatus!.InstanceState!.Name!)
102+
.promise()
103+
104+
return response[0]
105+
}
106+
107+
public async isInstanceRunning(instanceId: string): Promise<boolean> {
108+
const status = await this.getInstanceStatus(instanceId)
109+
return status === 'running'
110+
}
111+
112+
public getInstancesFilter(instanceIds: string[]): EC2.Filter[] {
113+
return [
114+
{
115+
Name: 'instance-id',
116+
Values: instanceIds,
117+
},
118+
]
119+
}
120+
121+
private handleStatusError(instanceId: string, err: unknown) {
122+
if (isAwsError(err)) {
123+
throw new ToolkitError(`EC2: failed to change status of instance ${instanceId}`, {
124+
cause: err as Error,
125+
})
126+
} else {
127+
throw err
128+
}
129+
}
130+
131+
public async ensureInstanceNotInStatus(instanceId: string, targetStatus: string) {
132+
const isAlreadyInStatus = (await this.getInstanceStatus(instanceId)) === targetStatus
133+
if (isAlreadyInStatus) {
134+
throw new ToolkitError(
135+
`EC2: Instance is currently ${targetStatus}. Unable to update status of ${instanceId}.`
136+
)
137+
}
138+
}
139+
140+
public async startInstance(instanceId: string): Promise<PromiseResult<EC2.StartInstancesResult, AWSError>> {
141+
const client = await this.createSdkClient()
142+
143+
const response = await client.startInstances({ InstanceIds: [instanceId] }).promise()
144+
145+
return response
146+
}
147+
148+
public async startInstanceWithCancel(instanceId: string): Promise<void> {
149+
const timeout = new Timeout(5000)
150+
151+
await showMessageWithCancel(`EC2: Starting instance ${instanceId}`, timeout)
152+
153+
try {
154+
await this.ensureInstanceNotInStatus(instanceId, 'running')
155+
await this.startInstance(instanceId)
156+
} catch (err) {
157+
this.handleStatusError(instanceId, err)
158+
} finally {
159+
timeout.cancel()
160+
}
161+
}
162+
163+
public async stopInstance(instanceId: string): Promise<PromiseResult<EC2.StopInstancesResult, AWSError>> {
164+
const client = await this.createSdkClient()
165+
166+
const response = await client.stopInstances({ InstanceIds: [instanceId] }).promise()
167+
168+
return response
169+
}
170+
171+
public async stopInstanceWithCancel(instanceId: string): Promise<void> {
172+
const timeout = new Timeout(5000)
173+
174+
await showMessageWithCancel(`EC2: Stopping instance ${instanceId}`, timeout)
175+
176+
try {
177+
await this.ensureInstanceNotInStatus(instanceId, 'stopped')
178+
await this.stopInstance(instanceId)
179+
} catch (err) {
180+
this.handleStatusError(instanceId, err)
181+
} finally {
182+
timeout.cancel()
183+
}
184+
}
185+
186+
public async rebootInstance(instanceId: string): Promise<void> {
187+
const client = await this.createSdkClient()
188+
189+
await client.rebootInstances({ InstanceIds: [instanceId] }).promise()
190+
}
191+
192+
public async rebootInstanceWithCancel(instanceId: string): Promise<void> {
193+
const timeout = new Timeout(5000)
194+
195+
await showMessageWithCancel(`EC2: Rebooting instance ${instanceId}`, timeout)
196+
197+
try {
198+
await this.rebootInstance(instanceId)
199+
} catch (err) {
200+
this.handleStatusError(instanceId, err)
201+
} finally {
202+
timeout.cancel()
203+
}
204+
}
205+
206+
/**
207+
* Retrieve IAM Association for a given EC2 instance.
208+
* @param instanceId target EC2 instance ID
209+
* @returns IAM Association for instance
210+
*/
211+
private async getIamInstanceProfileAssociation(instanceId: string): Promise<EC2.IamInstanceProfileAssociation> {
212+
const client = await this.createSdkClient()
213+
const instanceFilter = this.getInstancesFilter([instanceId])
214+
const requester = async (request: EC2.DescribeIamInstanceProfileAssociationsRequest) =>
215+
client.describeIamInstanceProfileAssociations(request).promise()
216+
const response = await pageableToCollection(
217+
requester,
218+
{ Filters: instanceFilter },
219+
'NextToken',
220+
'IamInstanceProfileAssociations'
221+
)
222+
.flatten()
223+
.filter((association) => association !== undefined)
224+
.promise()
225+
226+
return response[0]!
227+
}
228+
229+
/**
230+
* Gets the IAM Instance Profile (not role) attached to given EC2 instance.
231+
* @param instanceId target EC2 instance ID
232+
* @returns IAM Instance Profile associated with instance or undefined if none exists.
233+
*/
234+
public async getAttachedIamInstanceProfile(instanceId: string): Promise<IamInstanceProfile | undefined> {
235+
const association = await this.getIamInstanceProfileAssociation(instanceId)
236+
return association ? association.IamInstanceProfile : undefined
237+
}
238+
239+
public async getConsoleOutput(instanceId: string, latest: boolean): Promise<SafeEc2GetConsoleOutputResult> {
240+
const client = await this.createSdkClient()
241+
const response = await client.getConsoleOutput({ InstanceId: instanceId, Latest: latest }).promise()
242+
return {
243+
...response,
244+
InstanceId: instanceId,
245+
Output: response.Output ? decodeBase64(response.Output) : '',
246+
}
247+
}
248+
}
249+
250+
export function getNameOfInstance(instance: EC2.Instance): string | undefined {
251+
return instanceHasName(instance) ? lookupTagKey(instance.Tags!, 'Name')! : undefined
252+
}
253+
254+
export function instanceHasName(instance: EC2.Instance): boolean {
255+
return instance.Tags !== undefined && instance.Tags.some((tag) => tag.Key === 'Name')
256+
}
257+
258+
function lookupTagKey(tags: EC2.Tag[], targetKey: string) {
259+
return tags.filter((tag) => tag.Key === targetKey)[0].Value
260+
}

0 commit comments

Comments
 (0)