Skip to content

Commit b907682

Browse files
committed
use os to determine command
1 parent 7c602a3 commit b907682

File tree

5 files changed

+104
-72
lines changed

5 files changed

+104
-72
lines changed

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

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { showMessageWithCancel } from '../../shared/utilities/messages'
2828
import { SshConfig } from '../../shared/sshConfig'
2929
import { SshKeyPair } from './sshKeyPair'
3030
import { Ec2SessionTracker } from './remoteSessionManager'
31-
import { getEc2SsmEnv, getRemoveLinesCommand } from './utils'
31+
import { getEc2SsmEnv } from './utils'
3232

3333
export type Ec2ConnectErrorCode = 'EC2SSMStatus' | 'EC2SSMPermission' | 'EC2SSMConnect' | 'EC2SSMAgentStatus'
3434

@@ -38,6 +38,12 @@ export interface Ec2RemoteEnv extends VscodeRemoteConnection {
3838
ssmSession: SSM.StartSessionResponse
3939
}
4040

41+
export type Ec2OS = 'Amazon Linux' | 'Ubuntu' | 'macOS'
42+
interface RemoteUser {
43+
os: Ec2OS
44+
name: string
45+
}
46+
4147
export class Ec2Connecter implements vscode.Disposable {
4248
protected ssmClient: SsmClient
4349
protected ec2Client: Ec2Client
@@ -195,20 +201,29 @@ export class Ec2Connecter implements vscode.Disposable {
195201
const remoteEnv = await this.prepareEc2RemoteEnvWithProgress(selection, remoteUser)
196202

197203
try {
198-
await startVscodeRemote(remoteEnv.SessionProcess, remoteEnv.hostname, '/', remoteEnv.vscPath, remoteUser)
204+
await startVscodeRemote(
205+
remoteEnv.SessionProcess,
206+
remoteEnv.hostname,
207+
'/',
208+
remoteEnv.vscPath,
209+
remoteUser.name
210+
)
199211
} catch (err) {
200212
this.throwGeneralConnectionError(selection, err as Error)
201213
}
202214
}
203215

204-
public async prepareEc2RemoteEnvWithProgress(selection: Ec2Selection, remoteUser: string): Promise<Ec2RemoteEnv> {
216+
public async prepareEc2RemoteEnvWithProgress(
217+
selection: Ec2Selection,
218+
remoteUser: RemoteUser
219+
): Promise<Ec2RemoteEnv> {
205220
const timeout = new Timeout(60000)
206221
await showMessageWithCancel('AWS: Opening remote connection...', timeout)
207222
const remoteEnv = await this.prepareEc2RemoteEnv(selection, remoteUser).finally(() => timeout.cancel())
208223
return remoteEnv
209224
}
210225

211-
public async prepareEc2RemoteEnv(selection: Ec2Selection, remoteUser: string): Promise<Ec2RemoteEnv> {
226+
public async prepareEc2RemoteEnv(selection: Ec2Selection, remoteUser: RemoteUser): Promise<Ec2RemoteEnv> {
212227
const logger = this.configureRemoteConnectionLogger(selection.instanceId)
213228
const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap()
214229
const keyPair = await this.configureSshKeys(selection, remoteUser)
@@ -253,19 +268,23 @@ export class Ec2Connecter implements vscode.Disposable {
253268
return logger
254269
}
255270

256-
public async configureSshKeys(selection: Ec2Selection, remoteUser: string): Promise<SshKeyPair> {
271+
public async configureSshKeys(selection: Ec2Selection, remoteUser: RemoteUser): Promise<SshKeyPair> {
257272
const keyPair = await SshKeyPair.getSshKeyPair(`aws-ec2-key`, 30000)
258273
await this.sendSshKeyToInstance(selection, keyPair, remoteUser)
259274
return keyPair
260275
}
261276

262-
private async attemptToCleanKeys(instanceId: string, hintComment: string, remoteAuthorizedKeysPath: string) {
277+
private async attemptToCleanKeys(
278+
instanceId: string,
279+
hintComment: string,
280+
hostOS: Ec2OS,
281+
remoteAuthorizedKeysPath: string
282+
) {
263283
try {
264-
const deleteExistingKeyCommand = getRemoveLinesCommand(hintComment, remoteAuthorizedKeysPath)
265-
const result = await this.ssmClient.sendCommandAndWait(instanceId, 'AWS-RunShellScript', {
284+
const deleteExistingKeyCommand = getRemoveLinesCommand(hintComment, hostOS, remoteAuthorizedKeysPath)
285+
await this.ssmClient.sendCommandAndWait(instanceId, 'AWS-RunShellScript', {
266286
commands: [deleteExistingKeyCommand],
267287
})
268-
console.log(result)
269288
} catch (e) {
270289
getLogger().warn(`ec2: failed to clean keys: %O`, e)
271290
}
@@ -274,32 +293,47 @@ export class Ec2Connecter implements vscode.Disposable {
274293
public async sendSshKeyToInstance(
275294
selection: Ec2Selection,
276295
sshKeyPair: SshKeyPair,
277-
remoteUser: string
296+
remoteUser: RemoteUser
278297
): Promise<void> {
279298
const sshPubKey = await sshKeyPair.getPublicKey()
280299
const hintComment = '#AWSToolkitForVSCode'
281300

282-
const remoteAuthorizedKeysPath = `/home/${remoteUser}/.ssh/authorized_keys`
301+
const remoteAuthorizedKeysPath = `/home/${remoteUser.name}/.ssh/authorized_keys`
283302

284303
const appendStr = (s: string) => `echo "${s}" >> ${remoteAuthorizedKeysPath}`
285304
const writeKeyCommand = appendStr([sshPubKey.replace('\n', ''), hintComment].join(' '))
286305

287-
await this.attemptToCleanKeys(selection.instanceId, hintComment, remoteAuthorizedKeysPath)
306+
await this.attemptToCleanKeys(selection.instanceId, hintComment, remoteUser.os, remoteAuthorizedKeysPath)
288307
await this.ssmClient.sendCommandAndWait(selection.instanceId, 'AWS-RunShellScript', {
289308
commands: [writeKeyCommand],
290309
})
291310
}
292311

293-
public async getRemoteUser(instanceId: string) {
294-
const osName = await this.ssmClient.getTargetPlatformName(instanceId)
295-
if (osName === 'Amazon Linux') {
296-
return 'ec2-user'
312+
public async getRemoteUser(instanceId: string): Promise<RemoteUser> {
313+
const os = await this.ssmClient.getTargetPlatformName(instanceId)
314+
if (os === 'Amazon Linux') {
315+
return { name: 'ec2-user', os }
297316
}
298317

299-
if (osName === 'Ubuntu') {
300-
return 'ubuntu'
318+
if (os === 'Ubuntu') {
319+
return { name: 'ubuntu', os }
301320
}
302321

303-
throw new ToolkitError(`Unrecognized OS name ${osName} on instance ${instanceId}`, { code: 'UnknownEc2OS' })
322+
throw new ToolkitError(`Unrecognized OS name ${os} on instance ${instanceId}`, { code: 'UnknownEc2OS' })
304323
}
305324
}
325+
326+
/**
327+
* Generate bash command (as string) to remove lines in file suffixed by `hintComment`.
328+
* @param hintComment suffix comment for deleted lines.
329+
* @param filepath filepath (as string) to target with the command.
330+
* @returns bash command to remove lines from file.
331+
*/
332+
export function getRemoveLinesCommand(hintComment: string, hostOS: Ec2OS, filepath: string): string {
333+
// Linux allows not passing extension to -i, whereas macOS requires zero length extension.
334+
return `sed -i ${isLinux(hostOS) ? '' : "''"} /${hintComment}/d ${filepath}`
335+
}
336+
337+
function isLinux(os: Ec2OS): boolean {
338+
return os === 'Amazon Linux' || os === 'Ubuntu'
339+
}

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

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,3 @@ export function getEc2SsmEnv(
4646
process.env
4747
)
4848
}
49-
/**
50-
* Generate bash command (as string) to remove lines in file suffixed by `hintComment`.
51-
* @param hintComment suffix comment for deleted lines.
52-
* @param filepath filepath (as string) to target with the command.
53-
* @returns bash command to remove lines from file.
54-
*/
55-
export function getRemoveLinesCommand(hintComment: string, filepath: string): string {
56-
return `sed -i '' /${hintComment}/d ${filepath}`
57-
}

packages/core/src/shared/vscode/env.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export function isRemoteWorkspace(): boolean {
134134
* Example: `5.10.220-188.869.amzn2int.x86_64`
135135
*/
136136
export function isAmazonInternalOs() {
137-
return os.release().includes('amzn2int') && process.platform === 'linux'
137+
return os.release().includes('amzn2int') && isLinux()
138138
}
139139

140140
/**
@@ -158,6 +158,10 @@ export function isWin(): boolean {
158158
return process.platform === 'win32'
159159
}
160160

161+
export function isLinux(): boolean {
162+
return process.platform === 'linux'
163+
}
164+
161165
const UIKind = {
162166
[vscode.UIKind.Desktop]: 'desktop',
163167
[vscode.UIKind.Web]: 'web',

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

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import assert from 'assert'
77
import * as sinon from 'sinon'
8-
import { Ec2Connecter } from '../../../awsService/ec2/model'
8+
import { Ec2Connecter, getRemoveLinesCommand } from '../../../awsService/ec2/model'
99
import { SsmClient } from '../../../shared/clients/ssmClient'
1010
import { Ec2Client } from '../../../shared/clients/ec2Client'
1111
import { Ec2Selection } from '../../../awsService/ec2/prompter'
@@ -15,6 +15,9 @@ import { SshKeyPair } from '../../../awsService/ec2/sshKeyPair'
1515
import { DefaultIamClient } from '../../../shared/clients/iamClient'
1616
import { assertNoTelemetryMatch, createTestWorkspaceFolder } from '../../testUtil'
1717
import { fs } from '../../../shared'
18+
import path from 'path'
19+
import { ChildProcess } from '../../../shared/utilities/processUtils'
20+
import { isLinux, isWin } from '../../../shared/vscode/env'
1821

1922
describe('Ec2ConnectClient', function () {
2023
let client: Ec2Connecter
@@ -140,7 +143,7 @@ describe('Ec2ConnectClient', function () {
140143
}
141144

142145
const keys = await SshKeyPair.getSshKeyPair('key', 30000)
143-
await client.sendSshKeyToInstance(testSelection, keys, 'test-user')
146+
await client.sendSshKeyToInstance(testSelection, keys, { name: 'test-user', os: 'Amazon Linux' })
144147
sinon.assert.calledWith(sendCommandStub, testSelection.instanceId, 'AWS-RunShellScript')
145148
sinon.restore()
146149
})
@@ -154,7 +157,7 @@ describe('Ec2ConnectClient', function () {
154157
}
155158
const testWorkspaceFolder = await createTestWorkspaceFolder()
156159
const keys = await SshKeyPair.getSshKeyPair('key', 60000)
157-
await client.sendSshKeyToInstance(testSelection, keys, 'test-user')
160+
await client.sendSshKeyToInstance(testSelection, keys, { name: 'test-user', os: 'Amazon Linux' })
158161
const privKey = await fs.readFileText(keys.getPrivateKeyPath())
159162
assertNoTelemetryMatch(privKey)
160163
sinon.restore()
@@ -178,13 +181,13 @@ describe('Ec2ConnectClient', function () {
178181
it('identifies the user for ubuntu as ubuntu', async function () {
179182
getTargetPlatformNameStub.resolves('Ubuntu')
180183
const remoteUser = await client.getRemoteUser('testInstance')
181-
assert.strictEqual(remoteUser, 'ubuntu')
184+
assert.strictEqual(remoteUser.name, 'ubuntu')
182185
})
183186

184187
it('identifies the user for amazon linux as ec2-user', async function () {
185188
getTargetPlatformNameStub.resolves('Amazon Linux')
186189
const remoteUser = await client.getRemoteUser('testInstance')
187-
assert.strictEqual(remoteUser, 'ec2-user')
190+
assert.strictEqual(remoteUser.name, 'ec2-user')
188191
})
189192

190193
it('throws error when not given known OS', async function () {
@@ -198,3 +201,40 @@ describe('Ec2ConnectClient', function () {
198201
})
199202
})
200203
})
204+
205+
describe('getRemoveLinesCommand', async function () {
206+
let tempPath: { uri: { fsPath: string } }
207+
208+
before(async function () {
209+
tempPath = await createTestWorkspaceFolder()
210+
})
211+
212+
after(async function () {
213+
await fs.delete(tempPath.uri.fsPath, { recursive: true, force: true })
214+
})
215+
216+
it('removes lines prefixed by pattern', async function () {
217+
if (isWin()) {
218+
this.skip()
219+
}
220+
// For the test, we only need to distinguish mac and linux
221+
const hostOS = isLinux() ? 'Amazon Linux' : 'macOS'
222+
const lines = ['line1', 'line2 pattern', 'line3', 'line4 pattern', 'line5', 'line6 pattern', 'line7']
223+
const expected = ['line1', 'line3', 'line5', 'line7']
224+
225+
const lineToStr = (ls: string[]) => ls.join('\n') + '\n'
226+
227+
const textFile = path.join(tempPath.uri.fsPath, 'test.txt')
228+
const originalContent = lineToStr(lines)
229+
await fs.writeFile(textFile, originalContent)
230+
const [command, ...args] = getRemoveLinesCommand('pattern', hostOS, textFile).split(' ')
231+
const process = new ChildProcess(command, args, { collect: true })
232+
const result = await process.run()
233+
234+
assert.strictEqual(result.exitCode, 0, `ChildProcess failed with error=${result.error}`)
235+
236+
const newContent = await fs.readFileText(textFile)
237+
assert.notStrictEqual(newContent, originalContent)
238+
assert.strictEqual(newContent, lineToStr(expected))
239+
})
240+
})

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

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,8 @@
66
import assert from 'assert'
77
import * as sinon from 'sinon'
88
import { SafeEc2Instance } from '../../../shared/clients/ec2Client'
9-
import { getIconCode, getRemoveLinesCommand } from '../../../awsService/ec2/utils'
9+
import { getIconCode } from '../../../awsService/ec2/utils'
1010
import { DefaultAwsContext } from '../../../shared'
11-
import { fs } from '../../../shared/fs/fs'
12-
import { createTestWorkspaceFolder } from '../../testUtil'
13-
import path from 'path'
14-
import { ChildProcess } from '../../../shared/utilities/processUtils'
1511

1612
describe('utils', async function () {
1713
before(function () {
@@ -56,37 +52,4 @@ describe('utils', async function () {
5652
assert.strictEqual(getIconCode(stoppingInstance), 'loading~spin')
5753
})
5854
})
59-
60-
describe('getRemoveLinesCommand', async function () {
61-
let tempPath: { uri: { fsPath: string } }
62-
63-
before(async function () {
64-
tempPath = await createTestWorkspaceFolder()
65-
})
66-
67-
after(async function () {
68-
await fs.delete(tempPath.uri.fsPath, { recursive: true, force: true })
69-
})
70-
71-
it('removes lines prefixed by pattern', async function () {
72-
const lines = ['line1', 'line2 pattern', 'line3', 'line4 pattern', 'line5', 'line6 pattern', 'line7']
73-
const expected = ['line1', 'line3', 'line5', 'line7']
74-
75-
const lineToStr = (ls: string[]) => ls.join('\n') + '\n'
76-
77-
const textFile = path.join(tempPath.uri.fsPath, 'test.txt')
78-
const originalContent = lineToStr(lines)
79-
await fs.writeFile(textFile, originalContent)
80-
81-
const [command, ...args] = getRemoveLinesCommand('pattern', textFile).split(' ')
82-
const process = new ChildProcess(command, args, { collect: true })
83-
const result = await process.run()
84-
85-
assert.strictEqual(result.exitCode, 0, `ChildProcess failed with error=${result.error}`)
86-
87-
const newContent = await fs.readFileText(textFile)
88-
assert.notStrictEqual(newContent, originalContent)
89-
assert.strictEqual(newContent, lineToStr(expected))
90-
})
91-
})
9255
})

0 commit comments

Comments
 (0)