skypilot-nightly 1.0.0.dev20250606__py3-none-any.whl → 1.0.0.dev20250609__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. sky/__init__.py +2 -2
  2. sky/backends/backend_utils.py +21 -3
  3. sky/check.py +18 -22
  4. sky/cli.py +5 -8
  5. sky/client/cli.py +5 -8
  6. sky/client/sdk.py +2 -1
  7. sky/clouds/cloud.py +4 -0
  8. sky/clouds/nebius.py +44 -4
  9. sky/core.py +3 -2
  10. sky/dashboard/out/404.html +1 -1
  11. sky/dashboard/out/_next/static/chunks/236-619ed0248fb6fdd9.js +6 -0
  12. sky/dashboard/out/_next/static/chunks/470-680c19413b8f808b.js +1 -0
  13. sky/dashboard/out/_next/static/chunks/{614-635a84e87800f99e.js → 63-e2d7b1e75e67c713.js} +8 -8
  14. sky/dashboard/out/_next/static/chunks/843-16c7194621b2b512.js +11 -0
  15. sky/dashboard/out/_next/static/chunks/969-2c584e28e6b4b106.js +1 -0
  16. sky/dashboard/out/_next/static/chunks/973-aed916d5b02d2d63.js +1 -0
  17. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/{[job]-65d04d5d77cbb6b6.js → [job]-d31688d3e52736dd.js} +1 -1
  18. sky/dashboard/out/_next/static/chunks/pages/clusters/{[cluster]-35cbeb5214fd4036.js → [cluster]-e7d8710a9b0491e5.js} +1 -1
  19. sky/dashboard/out/_next/static/chunks/pages/{clusters-5549a350f97d7ef3.js → clusters-3c674e5d970e05cb.js} +1 -1
  20. sky/dashboard/out/_next/static/chunks/pages/config-3aac7a015c6eede1.js +6 -0
  21. sky/dashboard/out/_next/static/chunks/pages/infra/{[context]-b68ddeed712d45b5.js → [context]-46d2e4ad6c487260.js} +1 -1
  22. sky/dashboard/out/_next/static/chunks/pages/{infra-13b117a831702196.js → infra-7013d816a2a0e76c.js} +1 -1
  23. sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-f7f0c9e156d328bc.js +16 -0
  24. sky/dashboard/out/_next/static/chunks/pages/{jobs-a76b2700eca236f7.js → jobs-87e60396c376292f.js} +1 -1
  25. sky/dashboard/out/_next/static/chunks/pages/users-9355a0f13d1db61d.js +16 -0
  26. sky/dashboard/out/_next/static/chunks/pages/workspace/{new-c7516f2b4c3727c0.js → new-9a749cca1813bd27.js} +1 -1
  27. sky/dashboard/out/_next/static/chunks/pages/workspaces/{[name]-7799de9e691e35d8.js → [name]-8eeb628e03902f1b.js} +1 -1
  28. sky/dashboard/out/_next/static/chunks/pages/workspaces-8fbcc5ab4af316d0.js +1 -0
  29. sky/dashboard/out/_next/static/css/8b1c8321d4c02372.css +3 -0
  30. sky/dashboard/out/_next/static/xos0euNCptbGAM7_Q3Acl/_buildManifest.js +1 -0
  31. sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
  32. sky/dashboard/out/clusters/[cluster].html +1 -1
  33. sky/dashboard/out/clusters.html +1 -1
  34. sky/dashboard/out/config.html +1 -1
  35. sky/dashboard/out/index.html +1 -1
  36. sky/dashboard/out/infra/[context].html +1 -1
  37. sky/dashboard/out/infra.html +1 -1
  38. sky/dashboard/out/jobs/[job].html +1 -1
  39. sky/dashboard/out/jobs.html +1 -1
  40. sky/dashboard/out/users.html +1 -1
  41. sky/dashboard/out/workspace/new.html +1 -1
  42. sky/dashboard/out/workspaces/[name].html +1 -1
  43. sky/dashboard/out/workspaces.html +1 -1
  44. sky/exceptions.py +5 -0
  45. sky/global_user_state.py +11 -6
  46. sky/jobs/scheduler.py +9 -4
  47. sky/jobs/server/core.py +23 -2
  48. sky/jobs/server/server.py +0 -95
  49. sky/jobs/state.py +18 -15
  50. sky/jobs/utils.py +2 -1
  51. sky/models.py +18 -0
  52. sky/provision/kubernetes/utils.py +12 -5
  53. sky/provision/nebius/constants.py +47 -0
  54. sky/provision/nebius/instance.py +2 -1
  55. sky/provision/nebius/utils.py +28 -7
  56. sky/serve/server/core.py +1 -1
  57. sky/server/common.py +4 -2
  58. sky/server/constants.py +0 -2
  59. sky/server/requests/executor.py +10 -2
  60. sky/server/requests/requests.py +4 -3
  61. sky/server/server.py +22 -5
  62. sky/skylet/constants.py +4 -0
  63. sky/skylet/job_lib.py +2 -1
  64. sky/skypilot_config.py +13 -1
  65. sky/templates/jobs-controller.yaml.j2 +3 -1
  66. sky/templates/nebius-ray.yml.j2 +6 -0
  67. sky/users/model.conf +1 -1
  68. sky/users/permission.py +148 -31
  69. sky/users/rbac.py +26 -0
  70. sky/users/server.py +14 -13
  71. sky/utils/common.py +6 -1
  72. sky/utils/common_utils.py +21 -3
  73. sky/utils/kubernetes/deploy_remote_cluster.py +5 -3
  74. sky/utils/resources_utils.py +3 -1
  75. sky/utils/schemas.py +9 -0
  76. sky/workspaces/core.py +100 -8
  77. sky/workspaces/server.py +15 -2
  78. sky/workspaces/utils.py +56 -0
  79. {skypilot_nightly-1.0.0.dev20250606.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/METADATA +1 -1
  80. {skypilot_nightly-1.0.0.dev20250606.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/RECORD +89 -87
  81. sky/dashboard/out/_next/static/99m-BAySO8Q7J-ul1jZVL/_buildManifest.js +0 -1
  82. sky/dashboard/out/_next/static/chunks/236-a90f0a9753a10420.js +0 -6
  83. sky/dashboard/out/_next/static/chunks/470-9e7a479cc8303baa.js +0 -1
  84. sky/dashboard/out/_next/static/chunks/843-c296541442d4af88.js +0 -11
  85. sky/dashboard/out/_next/static/chunks/969-c7abda31c10440ac.js +0 -1
  86. sky/dashboard/out/_next/static/chunks/973-1a09cac61cfcc1e1.js +0 -1
  87. sky/dashboard/out/_next/static/chunks/pages/config-1a1eeb949dab8897.js +0 -6
  88. sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-2d23a9c7571e6320.js +0 -16
  89. sky/dashboard/out/_next/static/chunks/pages/users-262aab38b9baaf3a.js +0 -16
  90. sky/dashboard/out/_next/static/chunks/pages/workspaces-384ea5fa0cea8f28.js +0 -1
  91. sky/dashboard/out/_next/static/css/667d941a2888ce6e.css +0 -3
  92. /sky/dashboard/out/_next/static/chunks/{37-beedd583fea84cc8.js → 37-600191c5804dcae2.js} +0 -0
  93. /sky/dashboard/out/_next/static/chunks/{682-6647f0417d5662f0.js → 682-b60cfdacc15202e8.js} +0 -0
  94. /sky/dashboard/out/_next/static/chunks/{856-3a32da4b84176f6d.js → 856-affc52adf5403a3a.js} +0 -0
  95. /sky/dashboard/out/_next/static/chunks/pages/{_app-cb81dc4d27f4d009.js → _app-5f16aba5794ee8e7.js} +0 -0
  96. /sky/dashboard/out/_next/static/{99m-BAySO8Q7J-ul1jZVL → xos0euNCptbGAM7_Q3Acl}/_ssgManifest.js +0 -0
  97. {skypilot_nightly-1.0.0.dev20250606.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/WHEEL +0 -0
  98. {skypilot_nightly-1.0.0.dev20250606.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/entry_points.txt +0 -0
  99. {skypilot_nightly-1.0.0.dev20250606.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/licenses/LICENSE +0 -0
  100. {skypilot_nightly-1.0.0.dev20250606.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/top_level.txt +0 -0
sky/workspaces/core.py CHANGED
@@ -1,20 +1,24 @@
1
1
  """Workspace management core."""
2
2
 
3
3
  import concurrent.futures
4
- from typing import Any, Callable, Dict
4
+ from typing import Any, Callable, Dict, List
5
5
 
6
6
  import filelock
7
7
 
8
8
  from sky import check as sky_check
9
9
  from sky import exceptions
10
10
  from sky import global_user_state
11
+ from sky import models
11
12
  from sky import sky_logging
12
13
  from sky import skypilot_config
13
14
  from sky.skylet import constants
14
15
  from sky.usage import usage_lib
16
+ from sky.users import permission
17
+ from sky.utils import annotations
15
18
  from sky.utils import common_utils
16
19
  from sky.utils import config_utils
17
20
  from sky.utils import schemas
21
+ from sky.workspaces import utils as workspaces_utils
18
22
 
19
23
  logger = sky_logging.init_logger(__name__)
20
24
 
@@ -28,10 +32,7 @@ _WORKSPACE_CONFIG_LOCK_TIMEOUT_SECONDS = 60
28
32
 
29
33
  def get_workspaces() -> Dict[str, Any]:
30
34
  """Returns the workspace config."""
31
- workspaces = skypilot_config.get_nested(('workspaces',), default_value={})
32
- if constants.SKYPILOT_DEFAULT_WORKSPACE not in workspaces:
33
- workspaces[constants.SKYPILOT_DEFAULT_WORKSPACE] = {}
34
- return workspaces
35
+ return workspaces_for_user(common_utils.get_current_user().id)
35
36
 
36
37
 
37
38
  def _update_workspaces_config(
@@ -160,7 +161,7 @@ def _check_workspaces_have_no_active_resources(
160
161
  f'{len(workspace_clusters)} active cluster(s): {cluster_list}')
161
162
 
162
163
  if workspace_active_jobs:
163
- job_names = [job['job_id'] for job in workspace_active_jobs]
164
+ job_names = [str(job['job_id']) for job in workspace_active_jobs]
164
165
  job_list = ', '.join(job_names)
165
166
  workspace_errors.append(
166
167
  f'{len(workspace_active_jobs)} active managed job(s): '
@@ -223,14 +224,19 @@ def update_workspace(workspace_name: str, config: Dict[str,
223
224
  FileNotFoundError: If the config file cannot be found.
224
225
  PermissionError: If the config file cannot be written.
225
226
  """
227
+ _validate_workspace_config(workspace_name, config)
228
+
226
229
  # Check for active clusters and managed jobs in the workspace
230
+ # TODO(zhwu): we should allow the edits that only contain changes to
231
+ # allowed_users or private.
227
232
  _check_workspace_has_no_active_resources(workspace_name, 'update')
228
233
 
229
- _validate_workspace_config(workspace_name, config)
230
-
231
234
  def update_workspace_fn(workspaces: Dict[str, Any]) -> None:
232
235
  """Function to update workspace inside the lock."""
233
236
  workspaces[workspace_name] = config
237
+ users = workspaces_utils.get_workspace_users(config)
238
+ permission_service = permission.permission_service
239
+ permission_service.update_workspace_policy(workspace_name, users)
234
240
 
235
241
  # Use the internal helper function to save
236
242
  result = _update_workspaces_config(update_workspace_fn)
@@ -275,6 +281,10 @@ def create_workspace(workspace_name: str, config: Dict[str,
275
281
  raise ValueError(f'Workspace {workspace_name!r} already exists. '
276
282
  'Use update instead.')
277
283
  workspaces[workspace_name] = config
284
+ # Add policy for the workspace and allowed users
285
+ users = workspaces_utils.get_workspace_users(config)
286
+ permission_service = permission.permission_service
287
+ permission_service.add_workspace_policy(workspace_name, users)
278
288
 
279
289
  # Use the internal helper function to save
280
290
  result = _update_workspaces_config(create_workspace_fn)
@@ -324,6 +334,8 @@ def delete_workspace(workspace_name: str) -> Dict[str, Any]:
324
334
  if workspace_name not in workspaces:
325
335
  raise ValueError(f'Workspace {workspace_name!r} does not exist.')
326
336
  del workspaces[workspace_name]
337
+ permission_service = permission.permission_service
338
+ permission_service.remove_workspace_policy(workspace_name)
327
339
 
328
340
  # Use the internal helper function to save
329
341
  return _update_workspaces_config(delete_workspace_fn)
@@ -369,6 +381,9 @@ def update_config(config: Dict[str, Any]) -> Dict[str, Any]:
369
381
 
370
382
  # Check for API server changes and validate them
371
383
  current_config = skypilot_config.to_dict()
384
+ # If there is no changes to the config, we can return early
385
+ if current_config == config:
386
+ return config
372
387
 
373
388
  current_endpoint = current_config.get('api_server', {}).get('endpoint')
374
389
  new_endpoint = config.get('api_server', {}).get('endpoint')
@@ -382,15 +397,27 @@ def update_config(config: Dict[str, Any]) -> Dict[str, Any]:
382
397
 
383
398
  # Collect all workspaces that need to be checked for active resources
384
399
  workspaces_to_check = []
400
+ workspaces_to_check_policy: Dict[str, Dict[str, List[str]]] = {
401
+ 'add': {},
402
+ 'update': {},
403
+ 'delete': {}
404
+ }
385
405
 
386
406
  # Check each workspace that is being modified
387
407
  for workspace_name, new_workspace_config in new_workspaces.items():
408
+ if workspace_name not in current_workspaces:
409
+ users = workspaces_utils.get_workspace_users(new_workspace_config)
410
+ workspaces_to_check_policy['add'][workspace_name] = users
411
+ continue
412
+
388
413
  current_workspace_config = current_workspaces.get(workspace_name, {})
389
414
 
390
415
  # If workspace configuration is changing, validate and mark for checking
391
416
  if current_workspace_config != new_workspace_config:
392
417
  _validate_workspace_config(workspace_name, new_workspace_config)
393
418
  workspaces_to_check.append((workspace_name, 'update'))
419
+ users = workspaces_utils.get_workspace_users(new_workspace_config)
420
+ workspaces_to_check_policy['update'][workspace_name] = users
394
421
 
395
422
  # Check for workspace deletions
396
423
  for workspace_name in current_workspaces:
@@ -400,6 +427,7 @@ def update_config(config: Dict[str, Any]) -> Dict[str, Any]:
400
427
  raise ValueError(f'Cannot delete the default workspace '
401
428
  f'{constants.SKYPILOT_DEFAULT_WORKSPACE!r}.')
402
429
  workspaces_to_check.append((workspace_name, 'delete'))
430
+ workspaces_to_check_policy['delete'][workspace_name] = ['*']
403
431
 
404
432
  # Check all workspaces for active resources in one efficient call
405
433
  _check_workspaces_have_no_active_resources(workspaces_to_check)
@@ -412,6 +440,18 @@ def update_config(config: Dict[str, Any]) -> Dict[str, Any]:
412
440
  # Convert to config_utils.Config and save
413
441
  config_obj = config_utils.Config.from_dict(config)
414
442
  skypilot_config.update_api_server_config_no_lock(config_obj)
443
+ permission_service = permission.permission_service
444
+ for operation, workspaces in workspaces_to_check_policy.items():
445
+ for workspace_name, users in workspaces.items():
446
+ if operation == 'add':
447
+ permission_service.add_workspace_policy(
448
+ workspace_name, users)
449
+ elif operation == 'update':
450
+ permission_service.update_workspace_policy(
451
+ workspace_name, users)
452
+ elif operation == 'delete':
453
+ permission_service.remove_workspace_policy(
454
+ workspace_name)
415
455
  except filelock.Timeout as e:
416
456
  raise RuntimeError(
417
457
  f'Failed to update configuration due to a timeout '
@@ -429,3 +469,55 @@ def update_config(config: Dict[str, Any]) -> Dict[str, Any]:
429
469
  # Don't fail the update if the check fails, just warn
430
470
 
431
471
  return config
472
+
473
+
474
+ def reject_request_for_unauthorized_workspace(user: models.User) -> None:
475
+ """Rejects a request that has no permission to access active workspace.
476
+
477
+ Args:
478
+ user: The user making the request.
479
+
480
+ Raises:
481
+ PermissionDeniedError: If the user does not have permission to access
482
+ the active workspace.
483
+ """
484
+ active_workspace = skypilot_config.get_active_workspace()
485
+ if not permission.permission_service.check_workspace_permission(
486
+ user.id, active_workspace):
487
+ raise exceptions.PermissionDeniedError(
488
+ f'User {user.name} ({user.id}) does not have '
489
+ f'permission to access workspace {active_workspace!r}')
490
+
491
+
492
+ def is_workspace_private(workspace_config: Dict[str, Any]) -> bool:
493
+ """Check if a workspace is private.
494
+
495
+ Args:
496
+ workspace_config: The workspace configuration dictionary.
497
+
498
+ Returns:
499
+ True if the workspace is private, False if it's public.
500
+ """
501
+ return workspace_config.get('private', False)
502
+
503
+
504
+ @annotations.lru_cache(scope='request', maxsize=1)
505
+ def workspaces_for_user(user_id: str) -> Dict[str, Any]:
506
+ """Returns the workspaces that the user has access to.
507
+
508
+ Args:
509
+ user_id: The user id to check.
510
+
511
+ Returns:
512
+ A map from workspace name to workspace configuration.
513
+ """
514
+ workspaces = skypilot_config.get_nested(('workspaces',), default_value={})
515
+ if constants.SKYPILOT_DEFAULT_WORKSPACE not in workspaces:
516
+ workspaces[constants.SKYPILOT_DEFAULT_WORKSPACE] = {}
517
+ user_workspaces = {}
518
+
519
+ for workspace_name, workspace_config in workspaces.items():
520
+ if permission.permission_service.check_workspace_permission(
521
+ user_id, workspace_name):
522
+ user_workspaces[workspace_name] = workspace_config
523
+ return user_workspaces
sky/workspaces/server.py CHANGED
@@ -14,10 +14,18 @@ router = fastapi.APIRouter()
14
14
  # pylint: disable=redefined-builtin
15
15
  async def get(request: fastapi.Request) -> None:
16
16
  """Gets workspace config on the server."""
17
+ # Have to manually inject user info into the request body because the
18
+ # request body is not available in the GET endpoint.
19
+ auth_user = request.state.auth_user
20
+ auth_user_env_vars_kwargs = {
21
+ 'env_vars': auth_user.to_env_vars()
22
+ } if auth_user else {}
23
+ request_body = payloads.RequestBody(**auth_user_env_vars_kwargs)
24
+
17
25
  executor.schedule_request(
18
26
  request_id=request.state.request_id,
19
27
  request_name='workspaces.get',
20
- request_body=payloads.RequestBody(),
28
+ request_body=request_body,
21
29
  func=core.get_workspaces,
22
30
  schedule_type=api_requests.ScheduleType.SHORT,
23
31
  )
@@ -65,10 +73,15 @@ async def delete(request: fastapi.Request,
65
73
  @router.get('/config')
66
74
  async def get_config(request: fastapi.Request) -> None:
67
75
  """Gets the entire SkyPilot configuration."""
76
+ auth_user = request.state.auth_user
77
+ auth_user_env_vars_kwargs = {
78
+ 'env_vars': auth_user.to_env_vars()
79
+ } if auth_user else {}
80
+ get_config_body = payloads.GetConfigBody(**auth_user_env_vars_kwargs)
68
81
  executor.schedule_request(
69
82
  request_id=request.state.request_id,
70
83
  request_name='workspaces.get_config',
71
- request_body=payloads.GetConfigBody(),
84
+ request_body=get_config_body,
72
85
  func=core.get_config,
73
86
  schedule_type=api_requests.ScheduleType.SHORT,
74
87
  )
@@ -0,0 +1,56 @@
1
+ """Utils for workspaces."""
2
+ import collections
3
+ from typing import Any, Dict, List
4
+
5
+ from sky import global_user_state
6
+ from sky import sky_logging
7
+
8
+ logger = sky_logging.init_logger(__name__)
9
+
10
+
11
+ def get_workspace_users(workspace_config: Dict[str, Any]) -> List[str]:
12
+ """Get the users that should have access to a workspace.
13
+
14
+ workspace_config is a dict with the following keys:
15
+ - private: bool
16
+ - allowed_users: list of user names or IDs
17
+
18
+ This function will automatically resolve the user names to IDs.
19
+
20
+ Args:
21
+ workspace_config: The configuration of the workspace.
22
+
23
+ Returns:
24
+ List of user IDs that should have access to the workspace.
25
+ For private workspaces, returns specific user IDs.
26
+ For public workspaces, returns ['*'] to indicate all users.
27
+ """
28
+ if workspace_config.get('private', False):
29
+ user_ids = []
30
+ workspace_user_name_or_ids = workspace_config.get('allowed_users', [])
31
+ all_users = global_user_state.get_all_users()
32
+ all_user_ids = {user.id for user in all_users}
33
+ all_user_map = collections.defaultdict(list)
34
+ for user in all_users:
35
+ all_user_map[user.name].append(user.id)
36
+
37
+ # Resolve user names to IDs
38
+ for user_name_or_id in workspace_user_name_or_ids:
39
+ if user_name_or_id in all_user_ids:
40
+ user_ids.append(user_name_or_id)
41
+ elif user_name_or_id in all_user_map:
42
+ if len(all_user_map[user_name_or_id]) > 1:
43
+ user_ids_str = ', '.join(all_user_map[user_name_or_id])
44
+ raise ValueError(
45
+ f'User {user_name_or_id!r} has multiple IDs: '
46
+ f'{user_ids_str}. Please specify the user '
47
+ f'ID instead.')
48
+ user_ids.append(all_user_map[user_name_or_id][0])
49
+ else:
50
+ logger.warning(
51
+ f'User {user_name_or_id!r} not found in all users')
52
+ continue
53
+ return user_ids
54
+ else:
55
+ # Public workspace - return '*' to indicate all users should have access
56
+ return ['*']
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skypilot-nightly
3
- Version: 1.0.0.dev20250606
3
+ Version: 1.0.0.dev20250609
4
4
  Summary: SkyPilot: Run AI on Any Infra — Unified, Faster, Cheaper.
5
5
  Author: SkyPilot Team
6
6
  License: Apache 2.0