Skip to content

Commit 078ca7a

Browse files
chore: trigger CI
1 parent fddca6f commit 078ca7a

File tree

6 files changed

+167
-98
lines changed

6 files changed

+167
-98
lines changed

packages/core/src/awsService/sagemaker/commands.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ export async function openRemoteConnect(
223223
return
224224
}
225225

226+
// Validate SSH config before attempting connection
227+
await validateSshConfig()
228+
226229
const spaceName = node.spaceApp.SpaceName!
227230
await tryRefreshNode(node)
228231

@@ -323,9 +326,6 @@ async function handleRunningSpaceWithDisabledAccess(
323326
return
324327
}
325328

326-
// Validate SSH config before showing progress
327-
await validateSshConfig()
328-
329329
// Enable remote access and connect
330330
const client = sageMakerClient || new SagemakerClient(node.regionCode)
331331

@@ -384,9 +384,6 @@ async function handleStoppedSpace(
384384
spaceName: string,
385385
sageMakerClient?: SagemakerClient
386386
) {
387-
// Validate SSH config before showing progress
388-
await validateSshConfig()
389-
390387
const client = sageMakerClient || new SagemakerClient(node.regionCode)
391388

392389
try {
@@ -431,9 +428,6 @@ async function handleRunningSpaceWithEnabledAccess(
431428
spaceName: string,
432429
sageMakerClient?: SagemakerClient
433430
) {
434-
// Validate SSH config before showing progress
435-
await validateSshConfig()
436-
437431
return await vscode.window.withProgress(
438432
{
439433
location: vscode.ProgressLocation.Notification,

packages/core/src/awsService/sagemaker/constants.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,19 @@ export const InstanceTypeNotSelectedMessage = (spaceName: string) => {
4545

4646
export const RemoteAccessRequiredMessage =
4747
'This space requires remote access to be enabled.\nWould you like to restart the space and connect?\nAny unsaved work will be lost.'
48+
49+
// SSH Configuration Error Messages
50+
export const SshConfigUpdateDeclinedMessage = (configHostName: string, configPath: string) =>
51+
`SSH configuration has an outdated ${configHostName} section. Fix your ${configPath} file manually to enable remote connections.`
52+
53+
export const SshConfigOpenedForEditMessage = () =>
54+
`SSH configuration file opened for editing. Fix the issue and try connecting again.`
55+
56+
export const SshConfigSyntaxErrorMessage = (configPath: string) =>
57+
`SSH configuration has syntax errors in your ${configPath} file. Fix the configuration manually to enable remote connection.`
58+
59+
export const SshConfigRemovalFailedMessage = (configHostName: string) =>
60+
`Failed to remove SSH config section for ${configHostName}`
61+
62+
export const SshConfigUpdateFailedMessage = (configPath: string, configHostName: string) =>
63+
`Failed to update SSH config section. Fix your ${configPath} file manually or remove the outdated ${configHostName} section.`

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,6 @@ export async function prepareDevEnvConnection(
9090
await startLocalServer(ctx)
9191
await removeKnownHost(hostname)
9292

93-
// Note: SSH config validation is done in commands.ts before calling this function
94-
// to ensure users can fix config issues before the progress dialog appears
95-
9693
// set envirionment variables
9794
const vars = getSmSsmEnv(ssm, path.join(ctx.globalStorageUri.fsPath, 'sagemaker-local-server-info.json'))
9895
logger.info(`connect script logs at ${vars.LOG_FILE_LOCATION}`)

packages/core/src/awsService/sagemaker/sshConfig.ts

Lines changed: 100 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ import { CancellationError } from '../../shared/utilities/timeoutUtils'
1515
import { getSshConfigPath } from '../../shared/extensions/ssh'
1616
import { fileExists, readFileAsString } from '../../shared/filesystemUtilities'
1717
import fs from '../../shared/fs/fs'
18+
import {
19+
SshConfigUpdateDeclinedMessage,
20+
SshConfigOpenedForEditMessage,
21+
SshConfigSyntaxErrorMessage,
22+
SshConfigRemovalFailedMessage,
23+
SshConfigUpdateFailedMessage,
24+
} from './constants'
1825

1926
const localize = nls.loadMessageBundle()
2027

@@ -24,13 +31,15 @@ const localize = nls.loadMessageBundle()
2431
*/
2532
export class SageMakerSshConfig extends SshConfig {
2633
public override async verifySSHHost(proxyCommand: string) {
34+
// Read the current state of SSH config
2735
const configStateResult = await this.readSshConfigState(proxyCommand)
2836

29-
// If reading config state failed, return the error
37+
// If reading config state failed, return the error result
3038
if (configStateResult.isErr()) {
3139
return configStateResult
3240
}
3341

42+
// Extract the state if section exists and if it's outdated
3443
const configState = configStateResult.ok()
3544

3645
// Check if section exists and is outdated
@@ -45,56 +54,22 @@ export class SageMakerSshConfig extends SshConfig {
4554
// Write the new section
4655
await this.writeSectionToConfig(proxyCommand)
4756
getLogger().info('SSH config: Successfully updated sm_* section')
48-
// Update state to reflect that section now exists and is up to date
57+
58+
// Update state snapshot to reflect the changes
4959
configState.hasSshSection = true
5060
configState.isOutdated = false
5161
} catch (e) {
52-
// Failed to update, prompt user to fix manually
53-
try {
54-
await this.promptToFixUpdateFailure(e instanceof Error ? e : undefined)
55-
const configOpenedError = new ToolkitError(
56-
`SSH configuration file opened for editing. Fix the issue and try connecting again.`,
57-
{
58-
code: 'SshConfigOpenedForEdit',
59-
details: { configPath: getSshConfigPath() },
60-
}
61-
)
62-
return Result.err(configOpenedError)
63-
} catch (promptError) {
64-
// User cancelled opening the file
65-
if (promptError instanceof CancellationError) {
66-
return Result.err(
67-
ToolkitError.chain(
68-
e,
69-
`Failed to update SSH config section. Fix your ~/.ssh/config file manually or remove the outdated ${this.configHostName} section.`,
70-
{
71-
code: 'SshConfigUpdateFailed',
72-
details: {
73-
configHostName: this.configHostName,
74-
configPath: getSshConfigPath(),
75-
},
76-
}
77-
)
78-
)
79-
}
80-
return Result.err(
81-
ToolkitError.chain(
82-
promptError,
83-
'Unexpected error while handling SSH config update failure',
84-
{
85-
code: 'SshConfigErrorHandlingFailed',
86-
}
87-
)
88-
)
89-
}
62+
// Failed to update, handle the failure
63+
return await this.handleSshConfigUpdateFailure(e)
9064
}
9165
} else {
92-
// User declined
66+
// User declined the auto-update
67+
const configPath = getSshConfigPath()
9368
const userCancelledError = new ToolkitError(
94-
`SSH configuration has an outdated ${this.configHostName} section. Allow the toolkit to update it or fix your ~/.ssh/config file manually.`,
69+
SshConfigUpdateDeclinedMessage(this.configHostName, configPath),
9570
{
9671
code: 'SshConfigUpdateDeclined',
97-
details: { configHostName: this.configHostName },
72+
details: { configHostName: this.configHostName, configPath },
9873
}
9974
)
10075
return Result.err(userCancelledError)
@@ -110,24 +85,19 @@ export class SageMakerSshConfig extends SshConfig {
11085
// SM exists and is up-to-date but validation still failed means the error is elsewhere in the SSH config
11186
try {
11287
await this.promptOtherSshConfigError(sshError)
113-
const configOpenedError = new ToolkitError(
114-
`SSH configuration file opened for editing. Fix the syntax errors and try connecting again.`,
115-
{
116-
code: 'SshConfigOpenedForEdit',
117-
details: { configPath: getSshConfigPath() },
118-
}
119-
)
88+
const configOpenedError = new ToolkitError(SshConfigOpenedForEditMessage(), {
89+
code: 'SshConfigOpenedForEdit',
90+
details: { configPath: getSshConfigPath() },
91+
})
12092
return Result.err(configOpenedError)
12193
} catch (e) {
122-
// User cancelled
94+
// User cancelled the "Open SSH Config" prompt (from promptOtherSshConfigError)
12395
if (e instanceof CancellationError) {
124-
const externalConfigError = new ToolkitError(
125-
`SSH configuration has syntax errors in your ~/.ssh/config file. Fix the configuration manually to enable remote connection.`,
126-
{
127-
code: 'SshConfigExternalError',
128-
details: { configPath: getSshConfigPath() },
129-
}
130-
)
96+
const configPath = getSshConfigPath()
97+
const externalConfigError = new ToolkitError(SshConfigSyntaxErrorMessage(configPath), {
98+
code: 'SshConfigExternalError',
99+
details: { configPath },
100+
})
131101
return Result.err(externalConfigError)
132102
}
133103
return Result.err(
@@ -159,14 +129,21 @@ export class SageMakerSshConfig extends SshConfig {
159129
}
160130

161131
/**
162-
* Reads SSH config file and determines its state.
132+
* Reads SSH config file once and determines its current state.
133+
*
134+
* State represents the current condition of the SSH config:
135+
* - hasSshSection: Does the sm_* section exist in the file?
136+
* - isOutdated: Is the section in an old/incorrect format?
137+
* - existingSection: The actual content of the section (if it exists)
138+
*
139+
* @returns Result containing the state object or an error if file read fails
163140
*/
164141
public async readSshConfigState(proxyCommand: string): Promise<
165142
Result<
166143
{
167-
hasSshSection: boolean
168-
isOutdated: boolean
169-
existingSection?: string
144+
hasSshSection: boolean // True if sm_* section exists
145+
isOutdated: boolean // True if section needs updating
146+
existingSection?: string // Current section content (optional)
170147
},
171148
ToolkitError
172149
>
@@ -234,11 +211,13 @@ export class SageMakerSshConfig extends SshConfig {
234211
public async promptToUpdateSshConfig(): Promise<boolean> {
235212
getLogger().warn(`SSH config section is outdated for ${this.configHostName}`)
236213

214+
const configPath = getSshConfigPath()
237215
const confirmTitle = localize(
238216
'AWS.sshConfig.confirm.updateSshConfig.title',
239-
'{0} Toolkit will update the {1} section in ~/.ssh/config',
217+
'{0} Toolkit will update the {1} section in {2}',
240218
getIdeProperties().company,
241-
this.configHostName
219+
this.configHostName,
220+
configPath
242221
)
243222
const confirmText = localize('AWS.sshConfig.confirm.updateSshConfig.button', 'Update SSH config')
244223

@@ -262,7 +241,8 @@ export class SageMakerSshConfig extends SshConfig {
262241

263242
const message = localize(
264243
'AWS.sshConfig.error.updateFailed',
265-
'Failed to update your ~/.ssh/config file automatically.{0}\n\nOpen the file to fix the issue manually.',
244+
'Failed to update your {0} file automatically.{1}\n\nOpen the file to fix the issue manually.',
245+
sshConfigPath,
266246
errorDetails
267247
)
268248

@@ -297,7 +277,8 @@ export class SageMakerSshConfig extends SshConfig {
297277

298278
const message = localize(
299279
'AWS.sshConfig.error.otherError',
300-
'There is an error in your ~/.ssh/config file.{0}\n\nFix the error and try again.',
280+
'There is an error in your {0} file.{1}\n\nFix the error and try again.',
281+
sshConfigPath,
301282
errorDetails
302283
)
303284

@@ -358,9 +339,61 @@ export class SageMakerSshConfig extends SshConfig {
358339

359340
getLogger().info(`SSH config: Removed ${this.configHostName} section`)
360341
} catch (e) {
361-
throw ToolkitError.chain(e, `Failed to remove SSH config section for ${this.configHostName}`, {
342+
throw ToolkitError.chain(e, SshConfigRemovalFailedMessage(this.configHostName), {
362343
code: 'SshConfigRemovalFailed',
363344
})
364345
}
365346
}
347+
348+
/**
349+
* Handles SSH config update failure by prompting user to fix manually.
350+
*/
351+
private async handleSshConfigUpdateFailure(updateError: unknown): Promise<Result<void, ToolkitError>> {
352+
try {
353+
// Prompt user to open SSH config file to fix manually
354+
await this.promptToFixUpdateFailure(updateError instanceof Error ? updateError : undefined)
355+
356+
// User opened the file
357+
const configOpenedError = new ToolkitError(SshConfigOpenedForEditMessage(), {
358+
code: 'SshConfigOpenedForEdit',
359+
details: { configPath: getSshConfigPath() },
360+
})
361+
return Result.err(configOpenedError)
362+
} catch (promptError) {
363+
// User cancelled the "Open SSH Config" prompt (from promptToFixUpdateFailure)
364+
if (promptError instanceof CancellationError) {
365+
const configPath = getSshConfigPath()
366+
return Result.err(
367+
ToolkitError.chain(updateError, SshConfigUpdateFailedMessage(configPath, this.configHostName), {
368+
code: 'SshConfigUpdateFailed',
369+
details: {
370+
configHostName: this.configHostName,
371+
configPath,
372+
},
373+
})
374+
)
375+
}
376+
377+
// Unexpected error during prompt
378+
return Result.err(
379+
ToolkitError.chain(promptError, 'Unexpected error while handling SSH config update failure', {
380+
code: 'SshConfigErrorHandlingFailed',
381+
})
382+
)
383+
}
384+
}
385+
386+
/**
387+
* Creates SageMaker-specific SSH config section.
388+
*/
389+
protected override createSSHConfigSection(proxyCommand: string): string {
390+
return `
391+
# Created by AWS Toolkit for VSCode. https://github.com/aws/aws-toolkit-vscode
392+
Host ${this.configHostName}
393+
ForwardAgent yes
394+
AddKeysToAgent yes
395+
StrictHostKeyChecking accept-new
396+
ProxyCommand ${proxyCommand}
397+
`
398+
}
366399
}

packages/core/src/shared/sshConfig.ts

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,15 @@ export class SshConfig {
8585
protected async matchSshSection() {
8686
const result = await this.checkSshOnHost()
8787
if (result.exitCode !== 0) {
88-
// Include stderr in error message
89-
const errorMessage =
90-
result.stderr || result.error?.message || `ssh check against host failed: ${result.exitCode}`
91-
return Result.err(new Error(errorMessage))
88+
const errorMessage = result.stderr || `ssh check against host failed: ${result.exitCode}`
89+
90+
if (result.error) {
91+
// System level error
92+
return Result.err(ToolkitError.chain(result.error, errorMessage))
93+
}
94+
95+
// SSH ran but returned error exit code (config error, validation failed)
96+
return Result.err(new ToolkitError(errorMessage))
9297
}
9398
const matches = result.stdout.match(this.proxyCommandRegExp)
9499
return Result.ok(matches?.[0])
@@ -198,21 +203,8 @@ Host ${this.configHostName}
198203
`
199204
}
200205

201-
private getSageMakerSSHConfig(proxyCommand: string): string {
202-
return `
203-
# Created by AWS Toolkit for VSCode. https://github.com/aws/aws-toolkit-vscode
204-
Host ${this.configHostName}
205-
ForwardAgent yes
206-
AddKeysToAgent yes
207-
StrictHostKeyChecking accept-new
208-
ProxyCommand ${proxyCommand}
209-
`
210-
}
211-
212206
protected createSSHConfigSection(proxyCommand: string): string {
213-
if (this.scriptPrefix === 'sagemaker_connect') {
214-
return `${this.getSageMakerSSHConfig(proxyCommand)}`
215-
} else if (this.keyPath) {
207+
if (this.keyPath) {
216208
return `${this.getBaseSSHConfig(proxyCommand)}IdentityFile '${this.keyPath}'\n User '%r'\n`
217209
}
218210
return this.getBaseSSHConfig(proxyCommand)

0 commit comments

Comments
 (0)