@@ -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 )
@@ -162,12 +171,12 @@ export async function stopSpace(
162171 await client . deleteApp ( {
163172 DomainId : node . spaceApp . DomainId ! ,
164173 SpaceName : spaceName ,
165- AppType : node . spaceApp . App ! . AppType ! ,
174+ AppType : node . spaceApp . SpaceSettingsSummary ! . AppType ! ,
166175 AppName : node . spaceApp . App ?. AppName ,
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,213 @@ 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 . SpaceSettingsSummary ! . 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 (
333+ node . spaceApp . DomainId ! ,
334+ spaceName ,
335+ node . spaceApp . SpaceSettingsSummary ! . AppType !
336+ )
337+ await tryRemoteConnection ( node , ctx , progress )
338+ } catch ( err : any ) {
339+ // Handle user declining instance type upgrade
340+ if ( err . code === InstanceTypeError ) {
341+ return
224342 }
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
343+ throw new ToolkitError ( `Remote connection failed: ${ err . message } ` , {
344+ cause : err ,
345+ code : err . code ,
346+ } )
231347 }
232- throw new ToolkitError ( `Remote connection failed: ${ ( err as Error ) . message } ` , {
233- cause : err as Error ,
234- code : err . code ,
235- } )
236348 }
237- } else if ( node . getStatus ( ) === 'Running' ) {
238- // For running spaces, show progress
349+ )
350+ }
351+
352+ /**
353+ * Handles connecting to a stopped space
354+ * Starts the space and connects (remote access enabled automatically if needed)
355+ */
356+ async function handleStoppedSpace (
357+ node : SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode ,
358+ ctx : vscode . ExtensionContext ,
359+ spaceName : string ,
360+ sageMakerClient ?: SagemakerClient
361+ ) {
362+ const client = sageMakerClient || new SagemakerClient ( node . regionCode )
363+
364+ try {
365+ await client . startSpace ( spaceName , node . spaceApp . DomainId ! )
366+ await tryRefreshNode ( node )
367+
239368 return await vscode . window . withProgress (
240369 {
241370 location : vscode . ProgressLocation . Notification ,
242371 cancellable : false ,
243372 title : `Connecting to ${ spaceName } ` ,
244373 } ,
245374 async ( progress ) => {
375+ progress . report ( { message : 'Starting the space' } )
376+ await client . waitForAppInService (
377+ node . spaceApp . DomainId ! ,
378+ spaceName ,
379+ node . spaceApp . SpaceSettingsSummary ! . AppType !
380+ )
246381 await tryRemoteConnection ( node , ctx , progress )
247382 }
248383 )
384+ } catch ( err : any ) {
385+ // Handle user declining instance type upgrade
386+ if ( err . code === InstanceTypeError ) {
387+ return
388+ }
389+ throw new ToolkitError ( `Remote connection failed: ${ ( err as Error ) . message } ` , {
390+ cause : err as Error ,
391+ code : err . code ,
392+ } )
249393 }
250394}
395+
396+ /**
397+ * Handles connecting to a running space with enabled remote access
398+ * Direct connection without any space modifications
399+ */
400+ async function handleRunningSpaceWithEnabledAccess (
401+ node : SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode ,
402+ ctx : vscode . ExtensionContext ,
403+ spaceName : string ,
404+ sageMakerClient ?: SagemakerClient
405+ ) {
406+ return await vscode . window . withProgress (
407+ {
408+ location : vscode . ProgressLocation . Notification ,
409+ cancellable : false ,
410+ title : `Connecting to ${ spaceName } ` ,
411+ } ,
412+ async ( progress ) => {
413+ await tryRemoteConnection ( node , ctx , progress )
414+ }
415+ )
416+ }
0 commit comments