@@ -15,6 +15,13 @@ import { CancellationError } from '../../shared/utilities/timeoutUtils'
1515import { getSshConfigPath } from '../../shared/extensions/ssh'
1616import { fileExists , readFileAsString } from '../../shared/filesystemUtilities'
1717import fs from '../../shared/fs/fs'
18+ import {
19+ SshConfigUpdateDeclinedMessage ,
20+ SshConfigOpenedForEditMessage ,
21+ SshConfigSyntaxErrorMessage ,
22+ SshConfigRemovalFailedMessage ,
23+ SshConfigUpdateFailedMessage ,
24+ } from './constants'
1825
1926const localize = nls . loadMessageBundle ( )
2027
@@ -24,13 +31,15 @@ const localize = nls.loadMessageBundle()
2431 */
2532export 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}
0 commit comments