skypilot-nightly 1.0.0.dev20250607__py3-none-any.whl → 1.0.0.dev20250610__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.
- sky/__init__.py +2 -2
- sky/admin_policy.py +3 -0
- sky/authentication.py +1 -7
- sky/backends/backend_utils.py +18 -2
- sky/backends/cloud_vm_ray_backend.py +9 -20
- sky/check.py +4 -3
- sky/cli.py +6 -9
- sky/client/cli.py +6 -9
- sky/client/sdk.py +49 -4
- sky/clouds/kubernetes.py +15 -24
- sky/core.py +3 -2
- sky/dashboard/out/404.html +1 -1
- sky/dashboard/out/_next/static/4lwUJxN6KwBqUxqO1VccB/_buildManifest.js +1 -0
- sky/dashboard/out/_next/static/chunks/211.692afc57e812ae1a.js +1 -0
- sky/dashboard/out/_next/static/chunks/350.9e123a4551f68b0d.js +1 -0
- sky/dashboard/out/_next/static/chunks/37-d8aebf1683522a0b.js +6 -0
- sky/dashboard/out/_next/static/chunks/42.d39e24467181b06b.js +6 -0
- sky/dashboard/out/_next/static/chunks/443.b2242d0efcdf5f47.js +1 -0
- sky/dashboard/out/_next/static/chunks/470-4d1a5dbe58a8a2b9.js +1 -0
- sky/dashboard/out/_next/static/chunks/{121-865d2bf8a3b84c6a.js → 491.b3d264269613fe09.js} +3 -3
- sky/dashboard/out/_next/static/chunks/513.211357a2914a34b2.js +1 -0
- sky/dashboard/out/_next/static/chunks/600.9cc76ec442b22e10.js +16 -0
- sky/dashboard/out/_next/static/chunks/616-d6128fa9e7cae6e6.js +39 -0
- sky/dashboard/out/_next/static/chunks/664-047bc03493fda379.js +1 -0
- sky/dashboard/out/_next/static/chunks/682.4dd5dc116f740b5f.js +6 -0
- sky/dashboard/out/_next/static/chunks/760-a89d354797ce7af5.js +1 -0
- sky/dashboard/out/_next/static/chunks/799-3625946b2ec2eb30.js +8 -0
- sky/dashboard/out/_next/static/chunks/804-4c9fc53aa74bc191.js +21 -0
- sky/dashboard/out/_next/static/chunks/843-6fcc4bf91ac45b39.js +11 -0
- sky/dashboard/out/_next/static/chunks/856-0776dc6ed6000c39.js +1 -0
- sky/dashboard/out/_next/static/chunks/901-b424d293275e1fd7.js +1 -0
- sky/dashboard/out/_next/static/chunks/938-a75b7712639298b7.js +1 -0
- sky/dashboard/out/_next/static/chunks/947-6620842ef80ae879.js +35 -0
- sky/dashboard/out/_next/static/chunks/969-20d54a9d998dc102.js +1 -0
- sky/dashboard/out/_next/static/chunks/973-c807fc34f09c7df3.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/_app-4768de0aede04dc9.js +20 -0
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-89216c616dbaa9c5.js +6 -0
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-451a14e7e755ebbc.js +6 -0
- sky/dashboard/out/_next/static/chunks/pages/clusters-e56b17fd85d0ba58.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/config-497a35a7ed49734a.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/infra/[context]-d2910be98e9227cb.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/infra-780860bcc1103945.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-b3dbf38b51cb29be.js +16 -0
- sky/dashboard/out/_next/static/chunks/pages/jobs-fe233baf3d073491.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/users-c69ffcab9d6e5269.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/workspace/new-31aa8bdcb7592635.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/workspaces/[name]-c8c2191328532b7d.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/workspaces-82e6601baa5dd280.js +1 -0
- sky/dashboard/out/_next/static/chunks/webpack-0574a5a4ba3cf0ac.js +1 -0
- sky/dashboard/out/_next/static/css/8b1c8321d4c02372.css +3 -0
- sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
- sky/dashboard/out/clusters/[cluster].html +1 -1
- sky/dashboard/out/clusters.html +1 -1
- sky/dashboard/out/config.html +1 -1
- sky/dashboard/out/index.html +1 -1
- sky/dashboard/out/infra/[context].html +1 -1
- sky/dashboard/out/infra.html +1 -1
- sky/dashboard/out/jobs/[job].html +1 -1
- sky/dashboard/out/jobs.html +1 -1
- sky/dashboard/out/users.html +1 -1
- sky/dashboard/out/workspace/new.html +1 -1
- sky/dashboard/out/workspaces/[name].html +1 -1
- sky/dashboard/out/workspaces.html +1 -1
- sky/exceptions.py +23 -0
- sky/global_user_state.py +192 -80
- sky/jobs/client/sdk.py +29 -21
- sky/jobs/server/core.py +9 -1
- sky/jobs/server/server.py +0 -95
- sky/jobs/utils.py +2 -1
- sky/models.py +18 -0
- sky/provision/kubernetes/constants.py +9 -0
- sky/provision/kubernetes/utils.py +106 -7
- sky/serve/client/sdk.py +56 -45
- sky/serve/server/core.py +1 -1
- sky/server/common.py +5 -7
- sky/server/constants.py +0 -2
- sky/server/requests/executor.py +60 -22
- sky/server/requests/payloads.py +3 -0
- sky/server/requests/process.py +69 -29
- sky/server/requests/requests.py +4 -3
- sky/server/server.py +23 -5
- sky/server/stream_utils.py +111 -55
- sky/skylet/constants.py +4 -2
- sky/skylet/job_lib.py +2 -1
- sky/skypilot_config.py +108 -25
- sky/users/model.conf +1 -1
- sky/users/permission.py +149 -32
- sky/users/rbac.py +26 -0
- sky/users/server.py +14 -13
- sky/utils/admin_policy_utils.py +9 -3
- sky/utils/common.py +6 -1
- sky/utils/common_utils.py +21 -3
- sky/utils/context.py +21 -1
- sky/utils/controller_utils.py +16 -1
- sky/utils/kubernetes/exec_kubeconfig_converter.py +19 -47
- sky/utils/schemas.py +9 -0
- sky/workspaces/core.py +100 -8
- sky/workspaces/server.py +15 -2
- sky/workspaces/utils.py +56 -0
- {skypilot_nightly-1.0.0.dev20250607.dist-info → skypilot_nightly-1.0.0.dev20250610.dist-info}/METADATA +1 -1
- {skypilot_nightly-1.0.0.dev20250607.dist-info → skypilot_nightly-1.0.0.dev20250610.dist-info}/RECORD +106 -94
- sky/dashboard/out/_next/static/1qG0HTmVilJPxQdBk0fX5/_buildManifest.js +0 -1
- sky/dashboard/out/_next/static/chunks/236-619ed0248fb6fdd9.js +0 -6
- sky/dashboard/out/_next/static/chunks/293-351268365226d251.js +0 -1
- sky/dashboard/out/_next/static/chunks/37-600191c5804dcae2.js +0 -6
- sky/dashboard/out/_next/static/chunks/470-ad1e0db3afcbd9c9.js +0 -1
- sky/dashboard/out/_next/static/chunks/614-635a84e87800f99e.js +0 -66
- sky/dashboard/out/_next/static/chunks/682-b60cfdacc15202e8.js +0 -6
- sky/dashboard/out/_next/static/chunks/843-c296541442d4af88.js +0 -11
- sky/dashboard/out/_next/static/chunks/856-3a32da4b84176f6d.js +0 -1
- sky/dashboard/out/_next/static/chunks/969-2c584e28e6b4b106.js +0 -1
- sky/dashboard/out/_next/static/chunks/973-6d78a0814682d771.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/_app-cb81dc4d27f4d009.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-18aed9b56247d074.js +0 -6
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-b919a73aecdfa78f.js +0 -6
- sky/dashboard/out/_next/static/chunks/pages/clusters-4f6b9dd9abcb33ad.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/config-fe375a56342cf609.js +0 -6
- sky/dashboard/out/_next/static/chunks/pages/infra/[context]-3a18d0eeb5119fe4.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/infra-a1a6abeeb58c1051.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-1354e28c81eeb686.js +0 -16
- sky/dashboard/out/_next/static/chunks/pages/jobs-23bfc8bf373423db.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/users-5800045bd04e69c2.js +0 -16
- sky/dashboard/out/_next/static/chunks/pages/workspace/new-e1f9c0c3ff7ac4bd.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/workspaces/[name]-686590e0ee4b2412.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/workspaces-76b07aa5da91b0df.js +0 -1
- sky/dashboard/out/_next/static/chunks/webpack-65d465f948974c0d.js +0 -1
- sky/dashboard/out/_next/static/css/667d941a2888ce6e.css +0 -3
- /sky/dashboard/out/_next/static/{1qG0HTmVilJPxQdBk0fX5 → 4lwUJxN6KwBqUxqO1VccB}/_ssgManifest.js +0 -0
- {skypilot_nightly-1.0.0.dev20250607.dist-info → skypilot_nightly-1.0.0.dev20250610.dist-info}/WHEEL +0 -0
- {skypilot_nightly-1.0.0.dev20250607.dist-info → skypilot_nightly-1.0.0.dev20250610.dist-info}/entry_points.txt +0 -0
- {skypilot_nightly-1.0.0.dev20250607.dist-info → skypilot_nightly-1.0.0.dev20250610.dist-info}/licenses/LICENSE +0 -0
- {skypilot_nightly-1.0.0.dev20250607.dist-info → skypilot_nightly-1.0.0.dev20250610.dist-info}/top_level.txt +0 -0
sky/utils/common.py
CHANGED
@@ -5,6 +5,7 @@ import enum
|
|
5
5
|
import os
|
6
6
|
from typing import Generator
|
7
7
|
|
8
|
+
from sky import models
|
8
9
|
from sky.skylet import constants
|
9
10
|
from sky.utils import common_utils
|
10
11
|
|
@@ -25,10 +26,13 @@ JOB_CONTROLLER_NAME: str = f'{JOB_CONTROLLER_PREFIX}{SERVER_ID}'
|
|
25
26
|
|
26
27
|
|
27
28
|
@contextlib.contextmanager
|
28
|
-
def
|
29
|
+
def with_server_user() -> Generator[None, None, None]:
|
29
30
|
"""Temporarily set the user hash to common.SERVER_ID."""
|
30
31
|
old_env_user_hash = os.getenv(constants.USER_ID_ENV_VAR)
|
32
|
+
# TODO(zhwu): once we have fully moved our code to use `get_current_user()`
|
33
|
+
# instead of `common_utils.get_user_hash()`, we can remove the env override.
|
31
34
|
os.environ[constants.USER_ID_ENV_VAR] = SERVER_ID
|
35
|
+
common_utils.set_current_user(models.User.get_current_user())
|
32
36
|
try:
|
33
37
|
yield
|
34
38
|
finally:
|
@@ -36,6 +40,7 @@ def with_server_user_hash() -> Generator[None, None, None]:
|
|
36
40
|
os.environ[constants.USER_ID_ENV_VAR] = old_env_user_hash
|
37
41
|
else:
|
38
42
|
os.environ.pop(constants.USER_ID_ENV_VAR)
|
43
|
+
common_utils.set_current_user(models.User.get_current_user())
|
39
44
|
|
40
45
|
|
41
46
|
class StatusRefreshMode(enum.Enum):
|
sky/utils/common_utils.py
CHANGED
@@ -20,6 +20,7 @@ import uuid
|
|
20
20
|
import jsonschema
|
21
21
|
|
22
22
|
from sky import exceptions
|
23
|
+
from sky import models
|
23
24
|
from sky import sky_logging
|
24
25
|
from sky.adaptors import common as adaptors_common
|
25
26
|
from sky.skylet import constants
|
@@ -256,11 +257,13 @@ class Backoff:
|
|
256
257
|
_current_command: Optional[str] = None
|
257
258
|
_current_client_entrypoint: Optional[str] = None
|
258
259
|
_using_remote_api_server: Optional[bool] = None
|
260
|
+
_current_user: Optional['models.User'] = None
|
259
261
|
|
260
262
|
|
261
|
-
def
|
262
|
-
|
263
|
-
|
263
|
+
def set_request_context(client_entrypoint: Optional[str],
|
264
|
+
client_command: Optional[str],
|
265
|
+
using_remote_api_server: bool,
|
266
|
+
user: Optional['models.User']):
|
264
267
|
"""Override the current client entrypoint and command.
|
265
268
|
|
266
269
|
This is useful when we are on the SkyPilot API server side and we have a
|
@@ -269,9 +272,11 @@ def set_client_status(client_entrypoint: Optional[str],
|
|
269
272
|
global _current_command
|
270
273
|
global _current_client_entrypoint
|
271
274
|
global _using_remote_api_server
|
275
|
+
global _current_user
|
272
276
|
_current_command = client_command
|
273
277
|
_current_client_entrypoint = client_entrypoint
|
274
278
|
_using_remote_api_server = using_remote_api_server
|
279
|
+
_current_user = user
|
275
280
|
|
276
281
|
|
277
282
|
def get_current_command() -> str:
|
@@ -286,6 +291,19 @@ def get_current_command() -> str:
|
|
286
291
|
return get_pretty_entrypoint_cmd()
|
287
292
|
|
288
293
|
|
294
|
+
def get_current_user() -> 'models.User':
|
295
|
+
"""Returns the current user."""
|
296
|
+
if _current_user is not None:
|
297
|
+
return _current_user
|
298
|
+
return models.User.get_current_user()
|
299
|
+
|
300
|
+
|
301
|
+
def set_current_user(user: 'models.User'):
|
302
|
+
"""Sets the current user."""
|
303
|
+
global _current_user
|
304
|
+
_current_user = user
|
305
|
+
|
306
|
+
|
289
307
|
def get_current_client_entrypoint(server_entrypoint: str) -> str:
|
290
308
|
"""Returns the current client entrypoint.
|
291
309
|
|
sky/utils/context.py
CHANGED
@@ -4,11 +4,13 @@ import asyncio
|
|
4
4
|
from collections.abc import Mapping
|
5
5
|
from collections.abc import MutableMapping
|
6
6
|
import contextvars
|
7
|
+
import functools
|
7
8
|
import os
|
8
9
|
import pathlib
|
9
10
|
import subprocess
|
10
11
|
import sys
|
11
|
-
|
12
|
+
import typing
|
13
|
+
from typing import Any, Callable, Dict, Optional, TextIO, TypeVar
|
12
14
|
|
13
15
|
|
14
16
|
class Context(object):
|
@@ -256,6 +258,24 @@ class Popen(subprocess.Popen):
|
|
256
258
|
super().__init__(*args, env=env, **kwargs)
|
257
259
|
|
258
260
|
|
261
|
+
F = TypeVar('F', bound=Callable[..., Any])
|
262
|
+
|
263
|
+
|
264
|
+
def contextual(func: F) -> F:
|
265
|
+
"""Decorator to intiailize a context before executing the function.
|
266
|
+
|
267
|
+
If a context is already initialized, this decorator will reset the context,
|
268
|
+
i.e. all contextual variables set previously will be cleared.
|
269
|
+
"""
|
270
|
+
|
271
|
+
@functools.wraps(func)
|
272
|
+
def wrapper(*args, **kwargs):
|
273
|
+
initialize()
|
274
|
+
return func(*args, **kwargs)
|
275
|
+
|
276
|
+
return typing.cast(F, wrapper)
|
277
|
+
|
278
|
+
|
259
279
|
def initialize():
|
260
280
|
"""Initialize the current SkyPilot context."""
|
261
281
|
_CONTEXT.set(Context())
|
sky/utils/controller_utils.py
CHANGED
@@ -24,6 +24,7 @@ from sky.clouds import gcp
|
|
24
24
|
from sky.data import data_utils
|
25
25
|
from sky.data import storage as storage_lib
|
26
26
|
from sky.jobs import constants as managed_job_constants
|
27
|
+
from sky.provision.kubernetes import constants as kubernetes_constants
|
27
28
|
from sky.serve import constants as serve_constants
|
28
29
|
from sky.setup_files import dependencies
|
29
30
|
from sky.skylet import constants
|
@@ -272,6 +273,18 @@ def _get_cloud_dependencies_installation_commands(
|
|
272
273
|
step_prefix = prefix_str.replace('<step>', str(len(commands) + 1))
|
273
274
|
commands.append(f'echo -en "\\r{step_prefix}GCP SDK{empty_str}" &&'
|
274
275
|
f'{gcp.GOOGLE_SDK_INSTALLATION_COMMAND}')
|
276
|
+
if clouds.cloud_in_iterable(clouds.Kubernetes(), enabled_clouds):
|
277
|
+
# Install gke-gcloud-auth-plugin used for exec-auth with GKE.
|
278
|
+
# We install the plugin here instead of the next elif branch
|
279
|
+
# because gcloud is required to install the plugin, so the order
|
280
|
+
# of command execution is critical.
|
281
|
+
|
282
|
+
# We install plugin here regardless of whether exec-auth is
|
283
|
+
# actually used as exec-auth may be used in the future.
|
284
|
+
# TODO (kyuds): how to implement conservative installation?
|
285
|
+
commands.append(
|
286
|
+
'(command -v gke-gcloud-auth-plugin &>/dev/null || '
|
287
|
+
'(gcloud components install gke-gcloud-auth-plugin --quiet &>/dev/null))') # pylint: disable=line-too-long
|
275
288
|
elif isinstance(cloud, clouds.Kubernetes):
|
276
289
|
step_prefix = prefix_str.replace('<step>', str(len(commands) + 1))
|
277
290
|
commands.append(
|
@@ -295,7 +308,9 @@ def _get_cloud_dependencies_installation_commands(
|
|
295
308
|
'(curl -s -LO "https://dl.k8s.io/release/v1.31.6'
|
296
309
|
'/bin/linux/$ARCH/kubectl" && '
|
297
310
|
'sudo install -o root -g root -m 0755 '
|
298
|
-
'kubectl /usr/local/bin/kubectl))'
|
311
|
+
'kubectl /usr/local/bin/kubectl)) && '
|
312
|
+
f'echo -e \'#!/bin/bash\\nexport PATH="{kubernetes_constants.SKY_K8S_EXEC_AUTH_PATH}"\\nexec "$@"\' | sudo tee /usr/local/bin/{kubernetes_constants.SKY_K8S_EXEC_AUTH_WRAPPER} > /dev/null && ' # pylint: disable=line-too-long
|
313
|
+
f'sudo chmod +x /usr/local/bin/{kubernetes_constants.SKY_K8S_EXEC_AUTH_WRAPPER}') # pylint: disable=line-too-long
|
299
314
|
elif isinstance(cloud, clouds.Cudo):
|
300
315
|
step_prefix = prefix_str.replace('<step>', str(len(commands) + 1))
|
301
316
|
commands.append(
|
@@ -12,6 +12,12 @@ It assumes the target environment has the auth executable available in PATH.
|
|
12
12
|
If not, you'll need to update your environment container to include the auth
|
13
13
|
executable in PATH.
|
14
14
|
|
15
|
+
When using LOCAL_CREDENTIALS (aka exec auth) with Kubernetes, though, SkyPilot
|
16
|
+
will automatically inject a wrapper script for common exec auth providers like
|
17
|
+
GKE and EKS. This wrapper script helps to resolve path issues that may arise
|
18
|
+
from executables installed on non system-default paths. Thus, the kubeconfig
|
19
|
+
file may look different on the sky jobs controller.
|
20
|
+
|
15
21
|
Usage:
|
16
22
|
python -m sky.utils.kubernetes.exec_kubeconfig_converter
|
17
23
|
"""
|
@@ -20,52 +26,7 @@ import os
|
|
20
26
|
|
21
27
|
import yaml
|
22
28
|
|
23
|
-
|
24
|
-
def strip_auth_plugin_paths(kubeconfig_path: str, output_path: str):
|
25
|
-
"""Strip path information from exec plugin commands in a kubeconfig file.
|
26
|
-
|
27
|
-
For Nebius kubeconfigs, also changes the --profile argument to 'sky'.
|
28
|
-
|
29
|
-
Args:
|
30
|
-
kubeconfig_path (str): Path to the input kubeconfig file
|
31
|
-
output_path (str): Path where the modified kubeconfig will be saved
|
32
|
-
"""
|
33
|
-
with open(kubeconfig_path, 'r', encoding='utf-8') as file:
|
34
|
-
config = yaml.safe_load(file)
|
35
|
-
|
36
|
-
updated = False
|
37
|
-
for user in config.get('users', []):
|
38
|
-
exec_info = user.get('user', {}).get('exec', {})
|
39
|
-
current_command = exec_info.get('command', '')
|
40
|
-
|
41
|
-
if current_command:
|
42
|
-
# Strip the path and keep only the executable name
|
43
|
-
executable = os.path.basename(current_command)
|
44
|
-
if executable != current_command:
|
45
|
-
exec_info['command'] = executable
|
46
|
-
updated = True
|
47
|
-
|
48
|
-
# Handle Nebius kubeconfigs: change --profile to 'sky'
|
49
|
-
if executable == 'nebius' or current_command == 'nebius':
|
50
|
-
args = exec_info.get('args', [])
|
51
|
-
if args and '--profile' in args:
|
52
|
-
try:
|
53
|
-
profile_index = args.index('--profile')
|
54
|
-
if profile_index + 1 < len(args):
|
55
|
-
old_profile = args[profile_index + 1]
|
56
|
-
if old_profile != 'sky':
|
57
|
-
args[profile_index + 1] = 'sky'
|
58
|
-
updated = True
|
59
|
-
except ValueError:
|
60
|
-
pass # --profile not found in args
|
61
|
-
|
62
|
-
if updated:
|
63
|
-
with open(output_path, 'w', encoding='utf-8') as file:
|
64
|
-
yaml.safe_dump(config, file)
|
65
|
-
print('Kubeconfig updated with path-less exec auth. '
|
66
|
-
f'Saved to {output_path}')
|
67
|
-
else:
|
68
|
-
print('No updates made. No exec-based auth commands paths found.')
|
29
|
+
from sky.provision.kubernetes import utils as kubernetes_utils
|
69
30
|
|
70
31
|
|
71
32
|
def main():
|
@@ -85,7 +46,18 @@ def main():
|
|
85
46
|
help='Output kubeconfig file path (default: %(default)s)')
|
86
47
|
|
87
48
|
args = parser.parse_args()
|
88
|
-
|
49
|
+
|
50
|
+
with open(args.input, 'r', encoding='utf-8') as file:
|
51
|
+
config = yaml.safe_load(file)
|
52
|
+
|
53
|
+
updated = kubernetes_utils.format_kubeconfig_exec_auth(
|
54
|
+
config, args.output, False)
|
55
|
+
|
56
|
+
if updated:
|
57
|
+
print('Kubeconfig updated with path-less exec auth. '
|
58
|
+
f'Saved to {args.output}')
|
59
|
+
else:
|
60
|
+
print('No updates made.')
|
89
61
|
|
90
62
|
|
91
63
|
if __name__ == '__main__':
|
sky/utils/schemas.py
CHANGED
@@ -1249,6 +1249,15 @@ def get_config_schema():
|
|
1249
1249
|
'properties': {
|
1250
1250
|
# Explicit definition for GCP allows both project_id and
|
1251
1251
|
# disabled
|
1252
|
+
'private': {
|
1253
|
+
'type': 'boolean',
|
1254
|
+
},
|
1255
|
+
'allowed_users': {
|
1256
|
+
'type': 'array',
|
1257
|
+
'items': {
|
1258
|
+
'type': 'string',
|
1259
|
+
},
|
1260
|
+
},
|
1252
1261
|
'gcp': {
|
1253
1262
|
'type': 'object',
|
1254
1263
|
'properties': {
|
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
|
-
|
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=
|
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=
|
84
|
+
request_body=get_config_body,
|
72
85
|
func=core.get_config,
|
73
86
|
schedule_type=api_requests.ScheduleType.SHORT,
|
74
87
|
)
|
sky/workspaces/utils.py
ADDED
@@ -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 ['*']
|