@@ -16,10 +16,19 @@ import _ from 'lodash'
1616import { prepareDevEnvConnection , tryRemoteConnection } from './model'
1717import { ExtContext } from '../../shared/extensions'
1818import { SagemakerClient } from '../../shared/clients/sagemaker'
19+ import { AccessDeniedException } from '@amzn/sagemaker-client'
1920import { ToolkitError } from '../../shared/errors'
2021import { showConfirmationMessage } from '../../shared/utilities/messages'
2122import { RemoteSessionError } from '../../shared/remoteSession'
22- import { ConnectFromRemoteWorkspaceMessage , InstanceTypeError } from './constants'
23+ import {
24+ ConnectFromRemoteWorkspaceMessage ,
25+ InstanceTypeError ,
26+ InstanceTypeInsufficientMemory ,
27+ InstanceTypeInsufficientMemoryMessage ,
28+ RemoteAccess ,
29+ RemoteAccessRequiredMessage ,
30+ SpaceStatus ,
31+ } from './constants'
2332import { SagemakerUnifiedStudioSpaceNode } from '../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpaceNode'
2433
2534const localize = nls . loadMessageBundle ( )
@@ -136,10 +145,10 @@ export async function stopSpace(
136145 sageMakerClient ?: SagemakerClient
137146) {
138147 await tryRefreshNode ( node )
139- if ( node . getStatus ( ) === 'Stopped' || node . getStatus ( ) === 'Stopping' ) {
148+ if ( node . getStatus ( ) === SpaceStatus . STOPPED || node . getStatus ( ) === SpaceStatus . STOPPING ) {
140149 void vscode . window . showWarningMessage ( `Space ${ node . spaceApp . SpaceName } is already in Stopped/Stopping state.` )
141150 return
142- } else if ( node . getStatus ( ) === 'Starting' ) {
151+ } else if ( node . getStatus ( ) === SpaceStatus . STARTING ) {
143152 void vscode . window . showWarningMessage (
144153 `Space ${ node . spaceApp . SpaceName } is in Starting state. Wait until it is Running to attempt stop again.`
145154 )
@@ -167,7 +176,7 @@ export async function stopSpace(
167176 } )
168177 } catch ( err ) {
169178 const error = err as Error
170- if ( error . name === ' AccessDeniedException' ) {
179+ if ( error instanceof AccessDeniedException ) {
171180 throw new ToolkitError ( 'You do not have permission to stop spaces. Please contact your administrator' , {
172181 cause : error ,
173182 code : error . name ,
@@ -195,56 +204,205 @@ export async function openRemoteConnect(
195204 const spaceName = node . spaceApp . SpaceName !
196205 await tryRefreshNode ( node )
197206
198- // for Stopped SM spaces - check instance type before showing progress
199- if ( node . getStatus ( ) === 'Stopped' ) {
200- // In case of SMUS, we pass in a SM Client and for SM AI, it creates a new SM Client.
201- const client = sageMakerClient ? sageMakerClient : new SagemakerClient ( node . regionCode )
202-
203- try {
204- await client . startSpace ( spaceName , node . spaceApp . DomainId ! )
205- await tryRefreshNode ( node )
206- const appType = node . spaceApp . SpaceSettingsSummary ?. AppType
207- if ( ! appType ) {
208- throw new ToolkitError ( 'AppType is undefined for the selected space. Cannot start remote connection.' , {
209- code : 'undefinedAppType' ,
210- } )
207+ const remoteAccess = node . spaceApp . SpaceSettingsSummary ?. RemoteAccess
208+ const nodeStatus = node . getStatus ( )
209+
210+ // Route to appropriate handler based on space state
211+ if ( nodeStatus === SpaceStatus . RUNNING && remoteAccess !== RemoteAccess . ENABLED ) {
212+ return handleRunningSpaceWithDisabledAccess ( node , ctx , spaceName , sageMakerClient )
213+ } else if ( nodeStatus === SpaceStatus . STOPPED ) {
214+ return handleStoppedSpace ( node , ctx , spaceName , sageMakerClient )
215+ } else if ( nodeStatus === SpaceStatus . RUNNING ) {
216+ return handleRunningSpaceWithEnabledAccess ( node , ctx , spaceName )
217+ }
218+ }
219+
220+ /**
221+ * Checks if an instance type upgrade will be needed for remote access
222+ */
223+ export async function checkInstanceTypeUpgradeNeeded (
224+ node : SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode ,
225+ sageMakerClient ?: SagemakerClient
226+ ) : Promise < { upgradeNeeded : boolean ; currentType ?: string ; recommendedType ?: string } > {
227+ const client = sageMakerClient || new SagemakerClient ( node . regionCode )
228+
229+ try {
230+ const spaceDetails = await client . describeSpace ( {
231+ DomainId : node . spaceApp . DomainId ! ,
232+ SpaceName : node . spaceApp . SpaceName ! ,
233+ } )
234+
235+ const appType = spaceDetails . SpaceSettings ! . AppType !
236+
237+ // Get current instance type
238+ const currentResourceSpec =
239+ appType === 'JupyterLab'
240+ ? spaceDetails . SpaceSettings ! . JupyterLabAppSettings ?. DefaultResourceSpec
241+ : spaceDetails . SpaceSettings ! . CodeEditorAppSettings ?. DefaultResourceSpec
242+
243+ const currentInstanceType = currentResourceSpec ?. InstanceType
244+
245+ // Check if upgrade is needed
246+ if ( currentInstanceType && currentInstanceType in InstanceTypeInsufficientMemory ) {
247+ // Current type has insufficient memory
248+ return {
249+ upgradeNeeded : true ,
250+ currentType : currentInstanceType ,
251+ recommendedType : InstanceTypeInsufficientMemory [ currentInstanceType ] ,
211252 }
253+ }
254+
255+ return { upgradeNeeded : false , currentType : currentInstanceType }
256+ } catch ( err ) {
257+ const error = err as Error
258+ if ( error instanceof AccessDeniedException ) {
259+ throw new ToolkitError ( 'You do not have permission to describe spaces. Please contact your administrator' , {
260+ cause : error ,
261+ code : error . name ,
262+ } )
263+ }
264+ throw err
265+ }
266+ }
267+
268+ /**
269+ * Handles connecting to a running space with disabled remote access
270+ * Requires stopping the space, enabling remote access, and restarting
271+ */
272+ async function handleRunningSpaceWithDisabledAccess (
273+ node : SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode ,
274+ ctx : vscode . ExtensionContext ,
275+ spaceName : string ,
276+ sageMakerClient ?: SagemakerClient
277+ ) {
278+ // Check if instance type upgrade will be needed
279+ const instanceTypeInfo = await checkInstanceTypeUpgradeNeeded ( node , sageMakerClient )
280+
281+ let prompt : string
282+ if ( instanceTypeInfo . upgradeNeeded ) {
283+ prompt = InstanceTypeInsufficientMemoryMessage (
284+ spaceName ,
285+ instanceTypeInfo . currentType ! ,
286+ instanceTypeInfo . recommendedType !
287+ )
288+ } else {
289+ // Only remote access needs to be enabled
290+ prompt = RemoteAccessRequiredMessage
291+ }
292+
293+ const confirmed = await showConfirmationMessage ( {
294+ prompt,
295+ confirm : 'Restart and Connect' ,
296+ cancel : 'Cancel' ,
297+ type : 'warning' ,
298+ } )
299+
300+ if ( ! confirmed ) {
301+ return
302+ }
303+
304+ // Enable remote access and connect
305+ const client = sageMakerClient || new SagemakerClient ( node . regionCode )
306+
307+ return await vscode . window . withProgress (
308+ {
309+ location : vscode . ProgressLocation . Notification ,
310+ cancellable : false ,
311+ title : `Connecting to ${ spaceName } ` ,
312+ } ,
313+ async ( progress ) => {
314+ try {
315+ // Show initial progress message
316+ progress . report ( { message : 'Stopping the space' } )
317+
318+ // Stop the running space
319+ await client . deleteApp ( {
320+ DomainId : node . spaceApp . DomainId ! ,
321+ SpaceName : spaceName ,
322+ AppType : node . spaceApp . App ! . AppType ! ,
323+ AppName : node . spaceApp . App ?. AppName ,
324+ } )
212325
213- // Only start showing progress after instance type validation
214- return await vscode . window . withProgress (
215- {
216- location : vscode . ProgressLocation . Notification ,
217- cancellable : false ,
218- title : `Connecting to ${ spaceName } ` ,
219- } ,
220- async ( progress ) => {
221- progress . report ( { message : 'Starting the space.' } )
222- await client . waitForAppInService ( node . spaceApp . DomainId ! , spaceName , appType )
223- await tryRemoteConnection ( node , ctx , progress )
326+ // Update progress message
327+ progress . report ( { message : 'Starting the space' } )
328+
329+ // Start the space with remote access enabled (skip prompts since user already consented)
330+ await client . startSpace ( spaceName , node . spaceApp . DomainId ! , true )
331+ await tryRefreshNode ( node )
332+ await client . waitForAppInService ( node . spaceApp . DomainId ! , spaceName , node . spaceApp . App ! . AppType ! )
333+ await tryRemoteConnection ( node , ctx , progress )
334+ } catch ( err : any ) {
335+ // Handle user declining instance type upgrade
336+ if ( err . code === InstanceTypeError ) {
337+ return
224338 }
225- )
226- } catch ( err : any ) {
227- // Ignore InstanceTypeError since it means the user decided not to use an instanceType with more memory
228- // just return without showing progress
229- if ( err . code === InstanceTypeError ) {
230- return
339+ throw new ToolkitError ( `Remote connection failed: ${ err . message } ` , {
340+ cause : err ,
341+ code : err . code ,
342+ } )
231343 }
232- throw new ToolkitError ( `Remote connection failed: ${ ( err as Error ) . message } ` , {
233- cause : err as Error ,
234- code : err . code ,
235- } )
236344 }
237- } else if ( node . getStatus ( ) === 'Running' ) {
238- // For running spaces, show progress
345+ )
346+ }
347+
348+ /**
349+ * Handles connecting to a stopped space
350+ * Starts the space and connects (remote access enabled automatically if needed)
351+ */
352+ async function handleStoppedSpace (
353+ node : SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode ,
354+ ctx : vscode . ExtensionContext ,
355+ spaceName : string ,
356+ sageMakerClient ?: SagemakerClient
357+ ) {
358+ const client = sageMakerClient || new SagemakerClient ( node . regionCode )
359+
360+ try {
361+ await client . startSpace ( spaceName , node . spaceApp . DomainId ! )
362+ await tryRefreshNode ( node )
363+
239364 return await vscode . window . withProgress (
240365 {
241366 location : vscode . ProgressLocation . Notification ,
242367 cancellable : false ,
243368 title : `Connecting to ${ spaceName } ` ,
244369 } ,
245370 async ( progress ) => {
371+ progress . report ( { message : 'Starting the space' } )
372+ await client . waitForAppInService ( node . spaceApp . DomainId ! , spaceName , node . spaceApp . App ! . AppType ! )
246373 await tryRemoteConnection ( node , ctx , progress )
247374 }
248375 )
376+ } catch ( err : any ) {
377+ // Handle user declining instance type upgrade
378+ if ( err . code === InstanceTypeError ) {
379+ return
380+ }
381+ throw new ToolkitError ( `Remote connection failed: ${ ( err as Error ) . message } ` , {
382+ cause : err as Error ,
383+ code : err . code ,
384+ } )
249385 }
250386}
387+
388+ /**
389+ * Handles connecting to a running space with enabled remote access
390+ * Direct connection without any space modifications
391+ */
392+ async function handleRunningSpaceWithEnabledAccess (
393+ node : SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode ,
394+ ctx : vscode . ExtensionContext ,
395+ spaceName : string ,
396+ sageMakerClient ?: SagemakerClient
397+ ) {
398+ return await vscode . window . withProgress (
399+ {
400+ location : vscode . ProgressLocation . Notification ,
401+ cancellable : false ,
402+ title : `Connecting to ${ spaceName } ` ,
403+ } ,
404+ async ( progress ) => {
405+ await tryRemoteConnection ( node , ctx , progress )
406+ }
407+ )
408+ }
0 commit comments