Skip to content

Commit 34da225

Browse files
Merge master into feature/emr
2 parents e4cf912 + 24840fd commit 34da225

File tree

4 files changed

+118
-26
lines changed

4 files changed

+118
-26
lines changed

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export type Ec2ConnectErrorCode = 'EC2SSMStatus' | 'EC2SSMPermission' | 'EC2SSMC
3434

3535
interface Ec2RemoteEnv extends VscodeRemoteConnection {
3636
selection: Ec2Selection
37+
keyPair: SshKeyPair
3738
}
3839

3940
export class Ec2ConnectionManager {
@@ -194,9 +195,9 @@ export class Ec2ConnectionManager {
194195
public async prepareEc2RemoteEnv(selection: Ec2Selection, remoteUser: string): Promise<Ec2RemoteEnv> {
195196
const logger = this.configureRemoteConnectionLogger(selection.instanceId)
196197
const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap()
197-
const keyPath = await this.configureSshKeys(selection, remoteUser)
198+
const keyPair = await this.configureSshKeys(selection, remoteUser)
198199
const hostNamePrefix = 'aws-ec2-'
199-
const sshConfig = new SshConfig(ssh, hostNamePrefix, 'ec2_connect', keyPath)
200+
const sshConfig = new SshConfig(ssh, hostNamePrefix, 'ec2_connect', keyPair.getPrivateKeyPath())
200201

201202
const config = await sshConfig.ensureValid()
202203
if (config.isErr()) {
@@ -223,6 +224,7 @@ export class Ec2ConnectionManager {
223224
vscPath: vsc,
224225
SessionProcess,
225226
selection,
227+
keyPair,
226228
}
227229
}
228230

@@ -232,11 +234,11 @@ export class Ec2ConnectionManager {
232234
return logger
233235
}
234236

235-
public async configureSshKeys(selection: Ec2Selection, remoteUser: string): Promise<string> {
237+
public async configureSshKeys(selection: Ec2Selection, remoteUser: string): Promise<SshKeyPair> {
236238
const keyPath = path.join(globals.context.globalStorageUri.fsPath, `aws-ec2-key`)
237-
const keyPair = await SshKeyPair.getSshKeyPair(keyPath)
239+
const keyPair = await SshKeyPair.getSshKeyPair(keyPath, 30000)
238240
await this.sendSshKeyToInstance(selection, keyPair, remoteUser)
239-
return keyPath
241+
return keyPair
240242
}
241243

242244
public async sendSshKeyToInstance(

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

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,36 @@
22
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
5-
import * as fs from 'fs-extra'
5+
import { fs } from '../../shared'
6+
import { chmodSync } from 'fs'
67
import { ToolkitError } from '../../shared/errors'
78
import { ChildProcess } from '../../shared/utilities/childProcess'
9+
import { Timeout } from '../../shared/utilities/timeoutUtils'
810

911
export class SshKeyPair {
1012
private publicKeyPath: string
11-
private constructor(keyPath: string) {
13+
private lifeTimeout: Timeout
14+
private deleted: boolean = false
15+
16+
private constructor(
17+
private keyPath: string,
18+
lifetime: number
19+
) {
1220
this.publicKeyPath = `${keyPath}.pub`
21+
this.lifeTimeout = new Timeout(lifetime)
22+
23+
this.lifeTimeout.onCompletion(async () => {
24+
await this.delete()
25+
})
1326
}
1427

15-
public static async getSshKeyPair(keyPath: string) {
16-
const keyExists = await fs.pathExists(keyPath)
17-
if (!keyExists) {
18-
await SshKeyPair.generateSshKeyPair(keyPath)
28+
public static async getSshKeyPair(keyPath: string, lifetime: number) {
29+
// Overwrite key if already exists
30+
if (await fs.existsFile(keyPath)) {
31+
await fs.delete(keyPath)
1932
}
20-
return new SshKeyPair(keyPath)
33+
await SshKeyPair.generateSshKeyPair(keyPath)
34+
return new SshKeyPair(keyPath, lifetime)
2135
}
2236

2337
public static async generateSshKeyPair(keyPath: string): Promise<void> {
@@ -26,14 +40,38 @@ export class SshKeyPair {
2640
if (result.exitCode !== 0) {
2741
throw new ToolkitError('ec2: Failed to generate ssh key', { details: { stdout: result.stdout } })
2842
}
43+
chmodSync(keyPath, 0o600)
2944
}
3045

3146
public getPublicKeyPath(): string {
3247
return this.publicKeyPath
3348
}
3449

50+
public getPrivateKeyPath(): string {
51+
return this.keyPath
52+
}
53+
3554
public async getPublicKey(): Promise<string> {
36-
const contents = await fs.readFile(this.publicKeyPath, 'utf-8')
55+
const contents = await fs.readFileAsString(this.publicKeyPath)
3756
return contents
3857
}
58+
59+
public async delete(): Promise<void> {
60+
await fs.delete(this.keyPath)
61+
await fs.delete(this.publicKeyPath)
62+
63+
if (!this.lifeTimeout.completed) {
64+
this.lifeTimeout.cancel()
65+
}
66+
67+
this.deleted = true
68+
}
69+
70+
public isDeleted(): boolean {
71+
return this.deleted
72+
}
73+
74+
public timeAlive(): number {
75+
return this.lifeTimeout.elapsedTime
76+
}
3977
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ describe('Ec2ConnectClient', function () {
139139
instanceId: 'test-id',
140140
region: 'test-region',
141141
}
142-
const mockKeys = await SshKeyPair.getSshKeyPair('')
142+
const mockKeys = await SshKeyPair.getSshKeyPair('', 30000)
143143
await client.sendSshKeyToInstance(testSelection, mockKeys, '')
144144
sinon.assert.calledWith(sendCommandStub, testSelection.instanceId, 'AWS-RunShellScript')
145145
sinon.restore()

packages/core/src/test/awsService/ec2/sshKeyPair.test.ts

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,55 @@
22
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
5-
5+
import * as vscode from 'vscode'
66
import assert from 'assert'
7-
import * as fs from 'fs-extra'
87
import * as sinon from 'sinon'
9-
import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../../shared/filesystemUtilities'
8+
import * as path from 'path'
109
import { SshKeyPair } from '../../../awsService/ec2/sshKeyPair'
10+
import { createTestWorkspaceFolder, installFakeClock } from '../../testUtil'
11+
import { InstalledClock } from '@sinonjs/fake-timers'
1112
import { ChildProcess } from '../../../shared/utilities/childProcess'
13+
import { fs } from '../../../shared'
1214

1315
describe('SshKeyUtility', async function () {
1416
let temporaryDirectory: string
1517
let keyPath: string
1618
let keyPair: SshKeyPair
19+
let clock: InstalledClock
1720

1821
before(async function () {
19-
temporaryDirectory = await makeTemporaryToolkitFolder()
20-
keyPath = `${temporaryDirectory}/test-key`
21-
keyPair = await SshKeyPair.getSshKeyPair(keyPath)
22+
temporaryDirectory = (await createTestWorkspaceFolder()).uri.fsPath
23+
keyPath = path.join(temporaryDirectory, 'testKeyPair')
24+
clock = installFakeClock()
25+
})
26+
27+
beforeEach(async function () {
28+
keyPair = await SshKeyPair.getSshKeyPair(keyPath, 30000)
29+
})
30+
31+
afterEach(async function () {
32+
await keyPair.delete()
2233
})
2334

2435
after(async function () {
25-
await tryRemoveFolder(temporaryDirectory)
36+
await keyPair.delete()
37+
clock.uninstall()
38+
sinon.restore()
2639
})
2740

2841
describe('generateSshKeys', async function () {
2942
it('generates key in target file', async function () {
30-
const contents = await fs.readFile(keyPath, 'utf-8')
43+
const contents = await vscode.workspace.fs.readFile(vscode.Uri.file(keyPath))
3144
assert.notStrictEqual(contents.length, 0)
3245
})
3346

47+
it('generates unique key each time', async function () {
48+
const beforeContent = await vscode.workspace.fs.readFile(vscode.Uri.file(keyPath))
49+
keyPair = await SshKeyPair.getSshKeyPair(keyPath, 30000)
50+
const afterContent = await vscode.workspace.fs.readFile(vscode.Uri.file(keyPath))
51+
assert.notStrictEqual(beforeContent, afterContent)
52+
})
53+
3454
it('uses ed25519 algorithm to generate the keys', async function () {
3555
const process = new ChildProcess(`ssh-keygen`, ['-vvv', '-l', '-f', keyPath])
3656
const result = await process.run()
@@ -48,10 +68,42 @@ describe('SshKeyUtility', async function () {
4868
assert.notStrictEqual(key.length, 0)
4969
})
5070

51-
it('does not overwrite existing keys', async function () {
52-
const generateStub = sinon.stub(SshKeyPair, 'generateSshKeyPair')
53-
await SshKeyPair.getSshKeyPair(keyPath)
54-
sinon.assert.notCalled(generateStub)
71+
it('does overwrite existing keys on get call', async function () {
72+
const generateStub = sinon.spy(SshKeyPair, 'generateSshKeyPair')
73+
const keyBefore = await vscode.workspace.fs.readFile(vscode.Uri.file(keyPath))
74+
keyPair = await SshKeyPair.getSshKeyPair(keyPath, 30000)
75+
76+
const keyAfter = await vscode.workspace.fs.readFile(vscode.Uri.file(keyPath))
77+
sinon.assert.calledOnce(generateStub)
78+
79+
assert.notStrictEqual(keyBefore, keyAfter)
80+
sinon.restore()
81+
})
82+
83+
it('deletes key on delete', async function () {
84+
const pubKeyExistsBefore = await fs.existsFile(keyPair.getPublicKeyPath())
85+
const privateKeyExistsBefore = await fs.existsFile(keyPair.getPrivateKeyPath())
86+
87+
await keyPair.delete()
88+
89+
const pubKeyExistsAfter = await fs.existsFile(keyPair.getPublicKeyPath())
90+
const privateKeyExistsAfter = await fs.existsFile(keyPair.getPrivateKeyPath())
91+
92+
assert.strictEqual(pubKeyExistsBefore && privateKeyExistsBefore, true)
93+
assert.strictEqual(pubKeyExistsAfter && privateKeyExistsAfter, false)
94+
assert(keyPair.isDeleted())
95+
})
96+
97+
it('deletes keys after timeout', async function () {
98+
// Stub methods interacting with file system to avoid flaky test.
99+
sinon.stub(SshKeyPair, 'generateSshKeyPair')
100+
const deleteStub = sinon.stub(SshKeyPair.prototype, 'delete')
101+
102+
keyPair = await SshKeyPair.getSshKeyPair(keyPath, 50)
103+
await clock.tickAsync(10)
104+
sinon.assert.notCalled(deleteStub)
105+
await clock.tickAsync(100)
106+
sinon.assert.calledOnce(deleteStub)
55107
sinon.restore()
56108
})
57109
})

0 commit comments

Comments
 (0)