Skip to content

Commit d097668

Browse files
bharathGuntamaduguguntamb
andauthored
feat(smus): Add Project Space management functionality (aws#2190)
## Problem SMUS users need Project Space management functionality ## Solution - Add SageMakerUnifiedStudioSpaceNode for individual space representation - Add SageMakerUnifiedStudioSpacesParentNode for space container management - Add SagemakerSpace class for unified space operations and status management - Enhance SageMaker credential mapping with SMUS project support - Add space-specific icons and detached server credential resolution - Update SageMaker commands and model types for space functionality - Added test cases. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. ###Note: Due to lot of constriants - I apologies that I had to raise such a big PR. Co-authored-by: guntamb <[email protected]>
1 parent fd73378 commit d097668

39 files changed

+2960
-486
lines changed

packages/core/package.json

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -421,26 +421,40 @@
421421
"fontCharacter": "\\f1e0"
422422
}
423423
},
424-
"aws-schemas-registry": {
424+
"aws-sagemakerunifiedstudio-spaces": {
425425
"description": "AWS Contributed Icon",
426426
"default": {
427427
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
428428
"fontCharacter": "\\f1e1"
429429
}
430430
},
431-
"aws-schemas-schema": {
431+
"aws-sagemakerunifiedstudio-spaces-dark": {
432432
"description": "AWS Contributed Icon",
433433
"default": {
434434
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
435435
"fontCharacter": "\\f1e2"
436436
}
437437
},
438-
"aws-stepfunctions-preview": {
438+
"aws-schemas-registry": {
439439
"description": "AWS Contributed Icon",
440440
"default": {
441441
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
442442
"fontCharacter": "\\f1e3"
443443
}
444+
},
445+
"aws-schemas-schema": {
446+
"description": "AWS Contributed Icon",
447+
"default": {
448+
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
449+
"fontCharacter": "\\f1e4"
450+
}
451+
},
452+
"aws-stepfunctions-preview": {
453+
"description": "AWS Contributed Icon",
454+
"default": {
455+
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
456+
"fontCharacter": "\\f1e5"
457+
}
444458
}
445459
}
446460
},

packages/core/package.nls.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@
231231
"AWS.command.s3.uploadFile": "Upload Files...",
232232
"AWS.command.s3.uploadFileToParent": "Upload to Parent...",
233233
"AWS.command.smus.switchProject": "Switch Project",
234+
"AWS.command.smus.refreshProject": "Refresh Project",
234235
"AWS.command.smus.signOut": "Sign Out",
235236
"AWS.command.sagemaker.filterSpaces": "Filter Sagemaker Spaces",
236237
"AWS.command.stepFunctions.createStateMachineFromTemplate": "Create a new Step Functions state machine",
@@ -483,6 +484,5 @@
483484
"AWS.toolkit.lambda.walkthrough.step1.description": "Locally test and debug your code.",
484485
"AWS.toolkit.lambda.walkthrough.step2.title": "Deploy to the cloud",
485486
"AWS.toolkit.lambda.walkthrough.step2.description": "Test your application in the cloud from within VS Code. \n\nNote: The AWS CLI and the SAM CLI require AWS Credentials to interact with the cloud. For information on setting up your credentials, see [Authentication and access credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html). \n\n[Configure credentials](command:aws.toolkit.lambda.walkthrough.credential)",
486-
"AWS.toolkit.lambda.serverlessLand.quickpickTitle": "Create application with Serverless template",
487-
"AWS.command.smus.signout": "Sign Out"
487+
"AWS.toolkit.lambda.serverlessLand.quickpickTitle": "Create application with Serverless template"
488488
}
Lines changed: 4 additions & 0 deletions
Loading
Lines changed: 4 additions & 0 deletions
Loading

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { ToolkitError } from '../../shared/errors'
2020
import { showConfirmationMessage } from '../../shared/utilities/messages'
2121
import { RemoteSessionError } from '../../shared/remoteSession'
2222
import { ConnectFromRemoteWorkspaceMessage, InstanceTypeError } from './constants'
23+
import { SagemakerUnifiedStudioSpaceNode } from '../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpaceNode'
2324

2425
const localize = nls.loadMessageBundle()
2526

@@ -101,6 +102,8 @@ export async function deeplinkConnect(
101102
connectionIdentifier,
102103
ctx.extensionContext,
103104
'sm_dl',
105+
false /* isSMUS */,
106+
undefined /* node */,
104107
session,
105108
wsUrl,
106109
token,
@@ -125,7 +128,11 @@ export async function deeplinkConnect(
125128
}
126129
}
127130

128-
export async function stopSpace(node: SagemakerSpaceNode, ctx: vscode.ExtensionContext) {
131+
export async function stopSpace(
132+
node: SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode,
133+
ctx: vscode.ExtensionContext,
134+
sageMakerClient?: SagemakerClient
135+
) {
129136
const spaceName = node.spaceApp.SpaceName!
130137
const confirmed = await showConfirmationMessage({
131138
prompt: `You are about to stop this space. Any active resource will also be stopped. Are you sure you want to stop the space?`,
@@ -137,8 +144,8 @@ export async function stopSpace(node: SagemakerSpaceNode, ctx: vscode.ExtensionC
137144
if (!confirmed) {
138145
return
139146
}
140-
141-
const client = new SagemakerClient(node.regionCode)
147+
// In case of SMUS, we pass in a SM Client and for SM AI, it creates a new SM Client.
148+
const client = sageMakerClient ? sageMakerClient : new SagemakerClient(node.regionCode)
142149
try {
143150
await client.deleteApp({
144151
DomainId: node.spaceApp.DomainId!,
@@ -159,14 +166,19 @@ export async function stopSpace(node: SagemakerSpaceNode, ctx: vscode.ExtensionC
159166
await tryRefreshNode(node)
160167
}
161168

162-
export async function openRemoteConnect(node: SagemakerSpaceNode, ctx: vscode.ExtensionContext) {
169+
export async function openRemoteConnect(
170+
node: SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode,
171+
ctx: vscode.ExtensionContext,
172+
sageMakerClient?: SagemakerClient
173+
) {
163174
if (isRemoteWorkspace()) {
164175
void vscode.window.showErrorMessage(ConnectFromRemoteWorkspaceMessage)
165176
return
166177
}
167178

168179
if (node.getStatus() === 'Stopped') {
169-
const client = new SagemakerClient(node.regionCode)
180+
// In case of SMUS, we pass in a SM Client and for SM AI, it creates a new SM Client.
181+
const client = sageMakerClient ? sageMakerClient : new SagemakerClient(node.regionCode)
170182

171183
try {
172184
await client.startSpace(node.spaceApp.SpaceName!, node.spaceApp.DomainId!)

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

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { Auth } from '../../auth/auth'
1313
import { SpaceMappings, SsmConnectionInfo } from './types'
1414
import { getLogger } from '../../shared/logger/logger'
1515
import { parseArn } from './detached-server/utils'
16+
import { SagemakerUnifiedStudioSpaceNode } from '../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpaceNode'
17+
import { SageMakerUnifiedStudioSpacesParentNode } from '../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpacesParentNode'
1618

1719
const mappingFileName = '.sagemaker-space-profiles'
1820
const mappingFilePath = path.join(os.homedir(), '.aws', mappingFileName)
@@ -44,9 +46,9 @@ export async function saveMappings(data: SpaceMappings): Promise<void> {
4446

4547
/**
4648
* Persists the current profile to the appropriate space mapping based on connection type and profile format.
47-
* @param appArn - The identifier for the SageMaker space.
49+
* @param spaceArn - The arn for the SageMaker space.
4850
*/
49-
export async function persistLocalCredentials(appArn: string): Promise<void> {
51+
export async function persistLocalCredentials(spaceArn: string): Promise<void> {
5052
const currentProfileId = Auth.instance.getCurrentProfileId()
5153
if (!currentProfileId) {
5254
throw new ToolkitError('No current profile ID available for saving space credentials.')
@@ -55,33 +57,45 @@ export async function persistLocalCredentials(appArn: string): Promise<void> {
5557
if (currentProfileId.startsWith('sso:')) {
5658
const credentials = globals.loginManager.store.credentialsCache[currentProfileId]
5759
await setSpaceSsoProfile(
58-
appArn,
60+
spaceArn,
5961
credentials.credentials.accessKeyId,
6062
credentials.credentials.secretAccessKey,
6163
credentials.credentials.sessionToken ?? ''
6264
)
6365
} else {
64-
await setSpaceIamProfile(appArn, currentProfileId)
66+
await setSpaceIamProfile(spaceArn, currentProfileId)
6567
}
6668
}
6769

70+
/**
71+
* Persists the current selected SMUS Project Role creds to the appropriate space mapping.
72+
* @param spaceArn - The identifier for the SageMaker Space.
73+
*/
74+
export async function persistSmusProjectCreds(spaceArn: string, node: SagemakerUnifiedStudioSpaceNode): Promise<void> {
75+
const nodeParent = node.getParent() as SageMakerUnifiedStudioSpacesParentNode
76+
const authProvider = nodeParent.getAuthProvider()
77+
const projectAuthProvider = await authProvider.getProjectCredentialProvider(nodeParent.getProjectId())
78+
await projectAuthProvider.getCredentials()
79+
await setSmusSpaceSsoProfile(spaceArn, nodeParent.getProjectId())
80+
}
81+
6882
/**
6983
* Persists deep link credentials for a SageMaker space using a derived refresh URL based on environment.
7084
*
71-
* @param appArn - ARN of the SageMaker space.
85+
* @param spaceArn - ARN of the SageMaker space.
7286
* @param domain - The domain ID associated with the space.
7387
* @param session - SSM session ID.
7488
* @param wsUrl - SSM WebSocket URL.
7589
* @param token - Bearer token for the session.
7690
*/
7791
export async function persistSSMConnection(
78-
appArn: string,
92+
spaceArn: string,
7993
domain: string,
8094
session?: string,
8195
wsUrl?: string,
8296
token?: string
8397
): Promise<void> {
84-
const { region } = parseArn(appArn)
98+
const { region } = parseArn(spaceArn)
8599
const endpoint = DevSettings.instance.get('endpoints', {})['sagemaker'] ?? ''
86100

87101
// TODO: Hardcoded to 'jupyterlab' due to a bug in Studio that only supports refreshing
@@ -107,7 +121,7 @@ export async function persistSSMConnection(
107121
: `${envSubdomain}.studio.${region}.asfiovnxocqpcry.com`
108122

109123
const refreshUrl = `https://studio-${domain}.${baseDomain}/${appSubDomain}`
110-
await setSpaceCredentials(appArn, refreshUrl, {
124+
await setSpaceCredentials(spaceArn, refreshUrl, {
111125
sessionId: session ?? '-',
112126
url: wsUrl ?? '-',
113127
token: token ?? '-',
@@ -116,51 +130,63 @@ export async function persistSSMConnection(
116130

117131
/**
118132
* Sets or updates an IAM credential profile for a given space.
119-
* @param spaceName - The name of the SageMaker space.
133+
* @param spaceArn - The name of the SageMaker space.
120134
* @param profileName - The local AWS profile name to associate.
121135
*/
122-
export async function setSpaceIamProfile(spaceName: string, profileName: string): Promise<void> {
136+
export async function setSpaceIamProfile(spaceArn: string, profileName: string): Promise<void> {
123137
const data = await loadMappings()
124138
data.localCredential ??= {}
125-
data.localCredential[spaceName] = { type: 'iam', profileName }
139+
data.localCredential[spaceArn] = { type: 'iam', profileName }
126140
await saveMappings(data)
127141
}
128142

129143
/**
130144
* Sets or updates an SSO credential profile for a given space.
131-
* @param spaceName - The name of the SageMaker space.
145+
* @param spaceArn - The arn of the SageMaker space.
132146
* @param accessKey - Temporary access key from SSO.
133147
* @param secret - Temporary secret key from SSO.
134148
* @param token - Session token from SSO.
135149
*/
136150
export async function setSpaceSsoProfile(
137-
spaceName: string,
151+
spaceArn: string,
138152
accessKey: string,
139153
secret: string,
140154
token: string
141155
): Promise<void> {
142156
const data = await loadMappings()
143157
data.localCredential ??= {}
144-
data.localCredential[spaceName] = { type: 'sso', accessKey, secret, token }
158+
data.localCredential[spaceArn] = { type: 'sso', accessKey, secret, token }
159+
await saveMappings(data)
160+
}
161+
162+
/**
163+
* Sets the SM Space to map to SageMaker Unified Studio Project.
164+
* @param spaceArn - The arn of the SageMaker Unified Studio space.
165+
* @param projectId - The project ID associated with the SageMaker Unified Studio space.
166+
*/
167+
export async function setSmusSpaceSsoProfile(spaceArn: string, projectId: string): Promise<void> {
168+
const data = await loadMappings()
169+
data.localCredential ??= {}
170+
data.localCredential[spaceArn] = { type: 'sso', smusProjectId: projectId }
145171
await saveMappings(data)
146172
}
147173

148174
/**
149175
* Stores SSM connection information for a given space, typically from a deep link session.
150176
* This initializes the request as 'fresh' and includes a refresh URL if provided.
151-
* @param spaceName - The name of the SageMaker space.
177+
* @param spaceArn - The arn of the SageMaker space.
152178
* @param refreshUrl - URL to use for refreshing session tokens.
153179
* @param credentials - The session information used to initiate the connection.
154180
*/
155181
export async function setSpaceCredentials(
156-
spaceName: string,
182+
spaceArn: string,
157183
refreshUrl: string,
158184
credentials: SsmConnectionInfo
159185
): Promise<void> {
160186
const data = await loadMappings()
161187
data.deepLink ??= {}
162188

163-
data.deepLink[spaceName] = {
189+
data.deepLink[spaceArn] = {
164190
refreshUrl,
165191
requests: {
166192
'initial-connection': {

packages/core/src/awsService/sagemaker/detached-server/credentials.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,30 @@ export async function resolveCredentialsFor(connectionIdentifier: string): Promi
3636
return fromIni({ profile: name })
3737
}
3838
case 'sso': {
39-
const { accessKey, secret, token } = profile
40-
if (!accessKey || !secret || !token) {
39+
if ('accessKey' in profile && 'secret' in profile && 'token' in profile) {
40+
const { accessKey, secret, token } = profile
41+
if (!accessKey || !secret || !token) {
42+
throw new Error(`Missing SSO credentials for "${connectionIdentifier}"`)
43+
}
44+
return {
45+
accessKeyId: accessKey,
46+
secretAccessKey: secret,
47+
sessionToken: token,
48+
}
49+
} else if ('smusProjectId' in profile) {
50+
// Handle SMUS project ID case
51+
const { accessKey, secret, token } = mapping.smusProjects?.[profile.smusProjectId] || {}
52+
if (!accessKey || !secret || !token) {
53+
throw new Error(`Missing ProjectRole credentials for SMUS Space "${connectionIdentifier}"`)
54+
}
55+
return {
56+
accessKeyId: accessKey,
57+
secretAccessKey: secret,
58+
sessionToken: token,
59+
}
60+
} else {
4161
throw new Error(`Missing SSO credentials for "${connectionIdentifier}"`)
4262
}
43-
return {
44-
accessKeyId: accessKey,
45-
secretAccessKey: secret,
46-
sessionToken: token,
47-
}
4863
}
4964
default:
5065
throw new Error(`Unsupported profile type "${profile}"`)

packages/core/src/awsService/sagemaker/detached-server/utils.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ export async function readMapping() {
9696
try {
9797
const content = await fs.readFile(mappingFilePath, 'utf-8')
9898
console.log(`Mapping file path: ${mappingFilePath}`)
99-
console.log(`Conents: ${content}`)
10099
return JSON.parse(content)
101100
} catch (err) {
102101
throw new Error(`Failed to read mapping file: ${err instanceof Error ? err.message : String(err)}`)

0 commit comments

Comments
 (0)