skypilot-nightly 1.0.0.dev20250523__py3-none-any.whl → 1.0.0.dev20250526__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 (95) hide show
  1. sky/__init__.py +2 -2
  2. sky/backends/backend_utils.py +62 -45
  3. sky/backends/cloud_vm_ray_backend.py +3 -1
  4. sky/check.py +335 -170
  5. sky/cli.py +56 -13
  6. sky/client/cli.py +56 -13
  7. sky/client/sdk.py +54 -10
  8. sky/clouds/gcp.py +19 -3
  9. sky/core.py +5 -2
  10. sky/dashboard/out/404.html +1 -1
  11. sky/dashboard/out/_next/static/7GEgRyZKRaSnYZCV1Jwol/_buildManifest.js +1 -0
  12. sky/dashboard/out/_next/static/chunks/25-062253ea41fb8eec.js +6 -0
  13. sky/dashboard/out/_next/static/chunks/480-5a0de8b6570ea105.js +1 -0
  14. sky/dashboard/out/_next/static/chunks/488-50d843fdb5396d32.js +15 -0
  15. sky/dashboard/out/_next/static/chunks/498-d7722313e5e5b4e6.js +21 -0
  16. sky/dashboard/out/_next/static/chunks/573-f17bd89d9f9118b3.js +66 -0
  17. sky/dashboard/out/_next/static/chunks/578-d351125af46c293f.js +6 -0
  18. sky/dashboard/out/_next/static/chunks/734-a6e01d7f98904741.js +1 -0
  19. sky/dashboard/out/_next/static/chunks/937.f97f83652028e944.js +1 -0
  20. sky/dashboard/out/_next/static/chunks/938-59956af3950b02ed.js +1 -0
  21. sky/dashboard/out/_next/static/chunks/9f96d65d-5a3e4af68c26849e.js +1 -0
  22. sky/dashboard/out/_next/static/chunks/pages/_app-96a715a6fb01e228.js +1 -0
  23. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-3b5aad09a25f64b7.js +1 -0
  24. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-9529d9e882a0e75c.js +16 -0
  25. sky/dashboard/out/_next/static/chunks/pages/clusters-9e6d1ec6e1ac5b29.js +1 -0
  26. sky/dashboard/out/_next/static/chunks/pages/infra-abb7d744ecf15109.js +1 -0
  27. sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-48dc8d67d4b60be1.js +1 -0
  28. sky/dashboard/out/_next/static/chunks/pages/jobs-73d5e0c369d00346.js +16 -0
  29. sky/dashboard/out/_next/static/chunks/pages/users-b8acf6e6735323a2.js +1 -0
  30. sky/dashboard/out/_next/static/chunks/pages/workspace/new-bbf436f41381e169.js +1 -0
  31. sky/dashboard/out/_next/static/chunks/pages/workspaces/[name]-7733c960685b4385.js +1 -0
  32. sky/dashboard/out/_next/static/chunks/pages/workspaces-5ed48b3201b998c8.js +1 -0
  33. sky/dashboard/out/_next/static/chunks/webpack-deda68c926e8d0bc.js +1 -0
  34. sky/dashboard/out/_next/static/css/28558d57108b05ae.css +3 -0
  35. sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
  36. sky/dashboard/out/clusters/[cluster].html +1 -1
  37. sky/dashboard/out/clusters.html +1 -1
  38. sky/dashboard/out/index.html +1 -1
  39. sky/dashboard/out/infra.html +1 -1
  40. sky/dashboard/out/jobs/[job].html +1 -1
  41. sky/dashboard/out/jobs.html +1 -1
  42. sky/dashboard/out/users.html +1 -0
  43. sky/dashboard/out/workspace/new.html +1 -0
  44. sky/dashboard/out/workspaces/[name].html +1 -0
  45. sky/dashboard/out/workspaces.html +1 -0
  46. sky/data/storage.py +1 -1
  47. sky/global_user_state.py +606 -543
  48. sky/jobs/constants.py +1 -1
  49. sky/jobs/server/core.py +72 -56
  50. sky/jobs/state.py +26 -5
  51. sky/jobs/utils.py +65 -13
  52. sky/optimizer.py +6 -3
  53. sky/provision/fluidstack/instance.py +1 -0
  54. sky/serve/server/core.py +9 -6
  55. sky/server/html/token_page.html +6 -1
  56. sky/server/requests/executor.py +1 -0
  57. sky/server/requests/payloads.py +28 -0
  58. sky/server/server.py +59 -5
  59. sky/setup_files/dependencies.py +1 -0
  60. sky/skylet/constants.py +4 -1
  61. sky/skypilot_config.py +107 -11
  62. sky/utils/cli_utils/status_utils.py +18 -8
  63. sky/utils/db_utils.py +53 -0
  64. sky/utils/kubernetes/config_map_utils.py +133 -0
  65. sky/utils/kubernetes/deploy_remote_cluster.py +166 -147
  66. sky/utils/kubernetes/kubernetes_deploy_utils.py +49 -5
  67. sky/utils/kubernetes/ssh-tunnel.sh +20 -28
  68. sky/utils/log_utils.py +4 -0
  69. sky/utils/schemas.py +54 -0
  70. sky/workspaces/__init__.py +0 -0
  71. sky/workspaces/core.py +295 -0
  72. sky/workspaces/server.py +62 -0
  73. {skypilot_nightly-1.0.0.dev20250523.dist-info → skypilot_nightly-1.0.0.dev20250526.dist-info}/METADATA +2 -1
  74. {skypilot_nightly-1.0.0.dev20250523.dist-info → skypilot_nightly-1.0.0.dev20250526.dist-info}/RECORD +79 -63
  75. sky/dashboard/out/_next/static/ECKwDNS9v9y3_IKFZ2lpp/_buildManifest.js +0 -1
  76. sky/dashboard/out/_next/static/chunks/236-1a3a9440417720eb.js +0 -6
  77. sky/dashboard/out/_next/static/chunks/312-c3c8845990db8ffc.js +0 -15
  78. sky/dashboard/out/_next/static/chunks/37-d584022b0da4ac3b.js +0 -6
  79. sky/dashboard/out/_next/static/chunks/393-e1eaa440481337ec.js +0 -1
  80. sky/dashboard/out/_next/static/chunks/480-f28cd152a98997de.js +0 -1
  81. sky/dashboard/out/_next/static/chunks/582-683f4f27b81996dc.js +0 -59
  82. sky/dashboard/out/_next/static/chunks/pages/_app-8cfab319f9fb3ae8.js +0 -1
  83. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-33bc2bec322249b1.js +0 -1
  84. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-e2fc2dd1955e6c36.js +0 -1
  85. sky/dashboard/out/_next/static/chunks/pages/clusters-3a748bd76e5c2984.js +0 -1
  86. sky/dashboard/out/_next/static/chunks/pages/infra-abf08c4384190a39.js +0 -1
  87. sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-70756c2dad850a7e.js +0 -1
  88. sky/dashboard/out/_next/static/chunks/pages/jobs-ecd804b9272f4a7c.js +0 -1
  89. sky/dashboard/out/_next/static/chunks/webpack-830f59b8404e96b8.js +0 -1
  90. sky/dashboard/out/_next/static/css/7e7ce4ff31d3977b.css +0 -3
  91. /sky/dashboard/out/_next/static/{ECKwDNS9v9y3_IKFZ2lpp → 7GEgRyZKRaSnYZCV1Jwol}/_ssgManifest.js +0 -0
  92. {skypilot_nightly-1.0.0.dev20250523.dist-info → skypilot_nightly-1.0.0.dev20250526.dist-info}/WHEEL +0 -0
  93. {skypilot_nightly-1.0.0.dev20250523.dist-info → skypilot_nightly-1.0.0.dev20250526.dist-info}/entry_points.txt +0 -0
  94. {skypilot_nightly-1.0.0.dev20250523.dist-info → skypilot_nightly-1.0.0.dev20250526.dist-info}/licenses/LICENSE +0 -0
  95. {skypilot_nightly-1.0.0.dev20250523.dist-info → skypilot_nightly-1.0.0.dev20250526.dist-info}/top_level.txt +0 -0
sky/check.py CHANGED
@@ -15,6 +15,7 @@ from sky import global_user_state
15
15
  from sky import skypilot_config
16
16
  from sky.adaptors import cloudflare
17
17
  from sky.clouds import cloud as sky_cloud
18
+ from sky.skylet import constants
18
19
  from sky.utils import registry
19
20
  from sky.utils import rich_utils
20
21
  from sky.utils import subprocess_utils
@@ -29,178 +30,253 @@ def check_capabilities(
29
30
  verbose: bool = False,
30
31
  clouds: Optional[Iterable[str]] = None,
31
32
  capabilities: Optional[List[sky_cloud.CloudCapability]] = None,
32
- ) -> Dict[str, List[sky_cloud.CloudCapability]]:
33
+ workspace: Optional[str] = None,
34
+ ) -> Dict[str, Dict[str, List[sky_cloud.CloudCapability]]]:
35
+ # pylint: disable=import-outside-toplevel
36
+ from sky.workspaces import core
37
+
33
38
  echo = (lambda *_args, **_kwargs: None
34
39
  ) if quiet else lambda *args, **kwargs: click.echo(
35
40
  *args, **kwargs, color=True)
36
- echo('Checking credentials to enable infra for SkyPilot.')
41
+ all_workspaces_results: Dict[str,
42
+ Dict[str,
43
+ List[sky_cloud.CloudCapability]]] = {}
44
+ available_workspaces = list(core.get_workspaces().keys())
45
+ hide_workspace_str = (available_workspaces == [
46
+ constants.SKYPILOT_DEFAULT_WORKSPACE
47
+ ])
48
+ initial_hint = 'Checking credentials to enable infra for SkyPilot.'
49
+ if len(available_workspaces) > 1:
50
+ initial_hint = (f'Checking credentials to enable infra for SkyPilot '
51
+ f'(Workspaces: {", ".join(available_workspaces)}).')
52
+ echo(initial_hint)
37
53
  if capabilities is None:
38
54
  capabilities = sky_cloud.ALL_CAPABILITIES
39
55
  assert capabilities is not None
40
- enabled_clouds: Dict[str, List[sky_cloud.CloudCapability]] = {}
41
- disabled_clouds: Dict[str, List[sky_cloud.CloudCapability]] = {}
42
-
43
- def check_one_cloud_one_capability(
44
- payload: Tuple[Tuple[str, Union[sky_clouds.Cloud, ModuleType]],
45
- sky_cloud.CloudCapability]
46
- ) -> Optional[Tuple[sky_cloud.CloudCapability, bool, Optional[Union[
47
- str, Dict[str, str]]]]]:
48
- cloud_tuple, capability = payload
49
- _, cloud = cloud_tuple
50
- try:
51
- ok, reason = cloud.check_credentials(capability)
52
- except exceptions.NotSupportedError:
53
- return None
54
- except Exception: # pylint: disable=broad-except
55
- ok, reason = False, traceback.format_exc()
56
- if not isinstance(reason, dict):
57
- reason = reason.strip() if reason else None
58
- return (capability, ok, reason)
59
-
60
- def get_cloud_tuple(
61
- cloud_name: str) -> Tuple[str, Union[sky_clouds.Cloud, ModuleType]]:
62
- # Validates cloud_name and returns a tuple of the cloud's name and
63
- # the cloud object. Includes special handling for Cloudflare.
64
- if cloud_name.lower().startswith('cloudflare'):
65
- return cloudflare.NAME, cloudflare
66
- else:
67
- cloud_obj = registry.CLOUD_REGISTRY.from_str(cloud_name)
68
- assert cloud_obj is not None, f'Cloud {cloud_name!r} not found'
69
- return repr(cloud_obj), cloud_obj
70
56
 
71
- def get_all_clouds():
57
+ def get_all_clouds() -> Tuple[str, ...]:
72
58
  return tuple([repr(c) for c in registry.CLOUD_REGISTRY.values()] +
73
59
  [cloudflare.NAME])
74
60
 
75
- if clouds is not None:
76
- cloud_list = clouds
77
- else:
78
- cloud_list = get_all_clouds()
79
- clouds_to_check = [get_cloud_tuple(c) for c in cloud_list]
80
-
81
- # Use allowed_clouds from config if it exists, otherwise check all clouds.
82
- # Also validate names with get_cloud_tuple.
83
- config_allowed_cloud_names = sorted([
84
- get_cloud_tuple(c)[0] for c in skypilot_config.get_nested((
85
- 'allowed_clouds',), get_all_clouds())
86
- ])
87
- # Use disallowed_cloud_names for logging the clouds that will be disabled
88
- # because they are not included in allowed_clouds in config.yaml.
89
- disallowed_cloud_names = [
90
- c for c in get_all_clouds() if c not in config_allowed_cloud_names
91
- ]
92
- # Check only the clouds which are allowed in the config.
93
- clouds_to_check = [
94
- c for c in clouds_to_check if c[0] in config_allowed_cloud_names
95
- ]
96
-
97
- combinations = list(itertools.product(clouds_to_check, capabilities))
98
- with rich_utils.safe_status('Checking infra choices...'):
99
- check_results = subprocess_utils.run_in_parallel(
100
- check_one_cloud_one_capability, combinations)
101
-
102
- check_results_dict: Dict[
103
- Tuple[str, Union[sky_clouds.Cloud, ModuleType]],
104
- List[Tuple[sky_cloud.CloudCapability, bool,
105
- Optional[Union[str,
106
- Dict[str,
107
- str]]]]]] = collections.defaultdict(list)
108
- cloud2ctx2text: Dict[str, Dict[str, str]] = {}
109
- for combination, check_result in zip(combinations, check_results):
110
- if check_result is None:
111
- continue
112
- capability, ok, ctx2text = check_result
113
- cloud_tuple, _ = combination
114
- cloud_repr = cloud_tuple[0]
115
- if isinstance(ctx2text, dict):
116
- cloud2ctx2text[cloud_repr] = ctx2text
117
- if ok:
118
- enabled_clouds.setdefault(cloud_repr, []).append(capability)
61
+ def _execute_check_logic_for_workspace(
62
+ current_workspace_name: str,
63
+ hide_per_cloud_details: bool,
64
+ hide_workspace_str: bool,
65
+ ) -> Dict[str, List[sky_cloud.CloudCapability]]:
66
+ nonlocal echo, verbose, clouds, quiet
67
+
68
+ enabled_clouds: Dict[str, List[sky_cloud.CloudCapability]] = {}
69
+ disabled_clouds: Dict[str, List[sky_cloud.CloudCapability]] = {}
70
+
71
+ def check_one_cloud_one_capability(
72
+ payload: Tuple[Tuple[str, Union[sky_clouds.Cloud, ModuleType]],
73
+ sky_cloud.CloudCapability]
74
+ ) -> Optional[Tuple[sky_cloud.CloudCapability, bool, Optional[Union[
75
+ str, Dict[str, str]]]]]:
76
+ with skypilot_config.local_active_workspace_ctx(
77
+ current_workspace_name):
78
+ # Have to override again for specific thread, as the
79
+ # local_active_workspace_ctx is thread-local.
80
+ cloud_tuple, capability = payload
81
+ _, cloud = cloud_tuple
82
+ try:
83
+ ok, reason = cloud.check_credentials(capability)
84
+ except exceptions.NotSupportedError:
85
+ return None
86
+ except Exception: # pylint: disable=broad-except
87
+ ok, reason = False, traceback.format_exc()
88
+ if not isinstance(reason, dict):
89
+ reason = reason.strip() if reason else None
90
+ return (capability, ok, reason)
91
+
92
+ def get_cloud_tuple(
93
+ cloud_name: str
94
+ ) -> Tuple[str, Union[sky_clouds.Cloud, ModuleType]]:
95
+ # Validates cloud_name and returns a tuple of the cloud's name and
96
+ # the cloud object. Includes special handling for Cloudflare.
97
+ if cloud_name.lower().startswith('cloudflare'):
98
+ return cloudflare.NAME, cloudflare
99
+ else:
100
+ cloud_obj = registry.CLOUD_REGISTRY.from_str(cloud_name)
101
+ assert cloud_obj is not None, f'Cloud {cloud_name!r} not found'
102
+ return repr(cloud_obj), cloud_obj
103
+
104
+ if clouds is not None:
105
+ cloud_list = clouds
119
106
  else:
120
- disabled_clouds.setdefault(cloud_repr, []).append(capability)
121
- check_results_dict[cloud_tuple].append(check_result)
122
-
123
- for cloud_tuple, check_result_list in sorted(check_results_dict.items(),
124
- key=lambda item: item[0][0]):
125
- _print_checked_cloud(echo, verbose, cloud_tuple, check_result_list,
126
- cloud2ctx2text.get(cloud_tuple[0], {}))
127
-
128
- # Determine the set of enabled clouds: (previously enabled clouds + newly
129
- # enabled clouds - newly disabled clouds) intersected with
130
- # config_allowed_clouds, if specified in config.yaml.
131
- # This means that if a cloud is already enabled and is not included in
132
- # allowed_clouds in config.yaml, it will be disabled.
133
- all_enabled_clouds: Set[str] = set()
134
- for capability in capabilities:
135
- # Cloudflare is not a real cloud in registry.CLOUD_REGISTRY, and should
136
- # not be inserted into the DB (otherwise `sky launch` and other code
137
- # would error out when it's trying to look it up in the registry).
138
- enabled_clouds_set = {
139
- cloud for cloud, capabilities in enabled_clouds.items()
140
- if capability in capabilities and not cloud.startswith('Cloudflare')
141
- }
142
- disabled_clouds_set = {
143
- cloud for cloud, capabilities in disabled_clouds.items()
144
- if capability in capabilities and not cloud.startswith('Cloudflare')
145
- }
146
- config_allowed_clouds_set = {
147
- cloud for cloud in config_allowed_cloud_names
148
- if not cloud.startswith('Cloudflare')
149
- }
150
- previously_enabled_clouds_set = {
151
- repr(cloud)
152
- for cloud in global_user_state.get_cached_enabled_clouds(capability)
153
- }
154
- enabled_clouds_for_capability = (config_allowed_clouds_set & (
155
- (previously_enabled_clouds_set | enabled_clouds_set) -
156
- disabled_clouds_set))
157
- global_user_state.set_enabled_clouds(
158
- list(enabled_clouds_for_capability), capability)
159
- all_enabled_clouds = all_enabled_clouds.union(
160
- enabled_clouds_for_capability)
161
- disallowed_clouds_hint = None
162
- if disallowed_cloud_names:
163
- disallowed_clouds_hint = (
164
- '\nNote: The following clouds were disabled because they were not '
165
- 'included in allowed_clouds in ~/.sky/config.yaml: '
166
- f'{", ".join([c for c in disallowed_cloud_names])}')
167
- if not all_enabled_clouds:
107
+ cloud_list = get_all_clouds()
108
+
109
+ clouds_to_check = [get_cloud_tuple(c) for c in cloud_list]
110
+
111
+ # Use allowed_clouds from config if it exists, otherwise check all
112
+ # clouds. Also validate names with get_cloud_tuple.
113
+ config_allowed_cloud_names = sorted([
114
+ get_cloud_tuple(c)[0] for c in skypilot_config.get_nested((
115
+ 'allowed_clouds',), get_all_clouds())
116
+ ])
117
+
118
+ # filter out the clouds that are disabled in the workspace config
119
+ workspace_disabled_clouds = []
120
+ for cloud in config_allowed_cloud_names:
121
+ cloud_config = skypilot_config.get_workspace_cloud(
122
+ cloud, workspace=current_workspace_name)
123
+ cloud_disabled = cloud_config.get('disabled', False)
124
+ if cloud_disabled:
125
+ workspace_disabled_clouds.append(cloud)
126
+
127
+ config_allowed_cloud_names = [
128
+ c for c in config_allowed_cloud_names
129
+ if c not in workspace_disabled_clouds
130
+ ]
131
+
132
+ # Use disallowed_cloud_names for logging the clouds that will be
133
+ # disabled because they are not included in allowed_clouds in
134
+ # config.yaml.
135
+ disallowed_cloud_names = [
136
+ c for c in get_all_clouds() if c not in config_allowed_cloud_names
137
+ ]
138
+ # Check only the clouds which are allowed in the config.
139
+ clouds_to_check = [
140
+ c for c in clouds_to_check if c[0] in config_allowed_cloud_names
141
+ ]
142
+
143
+ combinations = list(itertools.product(clouds_to_check, capabilities))
144
+
145
+ cloud2ctx2text: Dict[str, Dict[str, str]] = {}
146
+ if not combinations:
147
+ echo(
148
+ _summary_message(enabled_clouds, cloud2ctx2text,
149
+ current_workspace_name, hide_workspace_str,
150
+ disallowed_cloud_names))
151
+
152
+ return {}
153
+
154
+ workspace_str = f' for workspace: {current_workspace_name!r}'
155
+ if hide_workspace_str:
156
+ workspace_str = ''
157
+ with rich_utils.safe_status(
158
+ ux_utils.spinner_message(
159
+ f'Checking infra choices{workspace_str}...')):
160
+ check_results = subprocess_utils.run_in_parallel(
161
+ check_one_cloud_one_capability, combinations)
162
+
163
+ check_results_dict: Dict[
164
+ Tuple[str, Union[sky_clouds.Cloud, ModuleType]],
165
+ List[Tuple[sky_cloud.CloudCapability, bool,
166
+ Optional[Union[str, Dict[str, str]]]]]] = (
167
+ collections.defaultdict(list))
168
+ for combination, check_result in zip(combinations, check_results):
169
+ if check_result is None:
170
+ continue
171
+ capability, ok, ctx2text = check_result
172
+ cloud_tuple, _ = combination
173
+ cloud_repr = cloud_tuple[0]
174
+ if isinstance(ctx2text, dict):
175
+ cloud2ctx2text[cloud_repr] = ctx2text
176
+ if ok:
177
+ enabled_clouds.setdefault(cloud_repr, []).append(capability)
178
+ else:
179
+ disabled_clouds.setdefault(cloud_repr, []).append(capability)
180
+ check_results_dict[cloud_tuple].append(check_result)
181
+
182
+ if not hide_per_cloud_details:
183
+ for cloud_tuple, check_result_list in sorted(
184
+ check_results_dict.items(), key=lambda item: item[0][0]):
185
+ _print_checked_cloud(echo, verbose, cloud_tuple,
186
+ check_result_list,
187
+ cloud2ctx2text.get(cloud_tuple[0], {}))
188
+
189
+ # Determine the set of enabled clouds: (previously enabled clouds +
190
+ # newly enabled clouds - newly disabled clouds) intersected with
191
+ # config_allowed_clouds, if specified in config.yaml.
192
+ # This means that if a cloud is already enabled and is not included in
193
+ # allowed_clouds in config.yaml, it will be disabled.
194
+ all_enabled_clouds: Set[str] = set()
195
+ for capability in capabilities:
196
+ # Cloudflare is not a real cloud in registry.CLOUD_REGISTRY, and
197
+ # should not be inserted into the DB (otherwise `sky launch` and
198
+ # other code would error out when it's trying to look it up in the
199
+ # registry).
200
+ enabled_clouds_set = {
201
+ cloud for cloud, capabilities in enabled_clouds.items()
202
+ if capability in capabilities and
203
+ not cloud.startswith('Cloudflare')
204
+ }
205
+ disabled_clouds_set = {
206
+ cloud for cloud, capabilities in disabled_clouds.items()
207
+ if capability in capabilities and
208
+ not cloud.startswith('Cloudflare')
209
+ }
210
+ config_allowed_clouds_set = {
211
+ cloud for cloud in config_allowed_cloud_names
212
+ if not cloud.startswith('Cloudflare')
213
+ }
214
+ previously_enabled_clouds_set = {
215
+ repr(cloud)
216
+ for cloud in global_user_state.get_cached_enabled_clouds(
217
+ capability, current_workspace_name)
218
+ }
219
+ enabled_clouds_for_capability = (config_allowed_clouds_set & (
220
+ (previously_enabled_clouds_set | enabled_clouds_set) -
221
+ disabled_clouds_set))
222
+
223
+ global_user_state.set_enabled_clouds(
224
+ list(enabled_clouds_for_capability), capability,
225
+ current_workspace_name)
226
+ all_enabled_clouds = all_enabled_clouds.union(
227
+ enabled_clouds_for_capability)
228
+
168
229
  echo(
169
- click.style(
170
- 'No cloud is enabled. SkyPilot will not be able to run any '
171
- 'task. Run `sky check` for more info.',
172
- fg='red',
173
- bold=True))
174
- if disallowed_clouds_hint:
175
- echo(click.style(disallowed_clouds_hint, dim=True))
176
- raise SystemExit()
230
+ _summary_message(enabled_clouds, cloud2ctx2text,
231
+ current_workspace_name, hide_workspace_str,
232
+ disallowed_cloud_names))
233
+
234
+ return enabled_clouds
235
+
236
+ # --- Main check_capabilities logic ---
237
+
238
+ if workspace is not None:
239
+ # Check only the specified workspace
240
+ if workspace not in available_workspaces:
241
+ with ux_utils.print_exception_no_traceback():
242
+ raise ValueError(
243
+ f'Workspace {workspace!r} not found in SkyPilot '
244
+ 'configuration. '
245
+ f'Available workspaces: {", ".join(available_workspaces)}')
246
+
247
+ # Always show details for single specified check (if verbose)
248
+ hide_per_cloud_details_flag = False
249
+ with skypilot_config.local_active_workspace_ctx(workspace):
250
+ enabled_ws_clouds = _execute_check_logic_for_workspace(
251
+ workspace, hide_per_cloud_details_flag, hide_workspace_str)
252
+ all_workspaces_results[workspace] = enabled_ws_clouds
177
253
  else:
178
- clouds_arg = (f' {" ".join(disabled_clouds).lower()}'
179
- if clouds is not None else '')
254
+ # Check all workspaces
255
+ workspaces_to_check = available_workspaces
256
+
257
+ hide_per_cloud_details_flag = (not verbose and
258
+ len(workspaces_to_check) > 1)
259
+
260
+ for ws_name in workspaces_to_check:
261
+ if not hide_workspace_str:
262
+ echo(f'\nChecking enabled infra for workspace: {ws_name!r}')
263
+ with skypilot_config.local_active_workspace_ctx(ws_name):
264
+ enabled_ws_clouds = _execute_check_logic_for_workspace(
265
+ ws_name, hide_per_cloud_details_flag, hide_workspace_str)
266
+ all_workspaces_results[ws_name] = enabled_ws_clouds
267
+
268
+ # Global "To enable a cloud..." message, printed once if relevant
269
+ if not quiet:
180
270
  echo(
181
271
  click.style(
182
272
  '\nTo enable a cloud, follow the hints above and rerun: ',
183
- dim=True) + click.style(f'sky check{clouds_arg}', bold=True) +
184
- '\n' + click.style(
273
+ dim=True) + click.style('sky check', bold=True) + '\n' +
274
+ click.style(
185
275
  'If any problems remain, refer to detailed docs at: '
186
276
  'https://docs.skypilot.co/en/latest/getting-started/installation.html', # pylint: disable=line-too-long
187
277
  dim=True))
188
278
 
189
- if disallowed_clouds_hint:
190
- echo(click.style(disallowed_clouds_hint, dim=True))
191
-
192
- # Pretty print for UX.
193
- if not quiet:
194
- enabled_clouds_str = '\n ' + '\n '.join([
195
- _format_enabled_cloud(cloud, capabilities,
196
- cloud2ctx2text.get(cloud, None))
197
- for cloud, capabilities in sorted(enabled_clouds.items(),
198
- key=lambda item: item[0])
199
- ])
200
- echo(f'\n{colorama.Fore.GREEN}{PARTY_POPPER_EMOJI} '
201
- f'Enabled infra {PARTY_POPPER_EMOJI}'
202
- f'{colorama.Style.RESET_ALL}{enabled_clouds_str}')
203
- return enabled_clouds
279
+ return all_workspaces_results
204
280
 
205
281
 
206
282
  def check_capability(
@@ -208,12 +284,15 @@ def check_capability(
208
284
  quiet: bool = False,
209
285
  verbose: bool = False,
210
286
  clouds: Optional[Iterable[str]] = None,
211
- ) -> List[str]:
212
- clouds_with_capability = []
213
- enabled_clouds = check_capabilities(quiet, verbose, clouds, [capability])
214
- for cloud, capabilities in enabled_clouds.items():
215
- if capability in capabilities:
216
- clouds_with_capability.append(cloud)
287
+ workspace: Optional[str] = None,
288
+ ) -> Dict[str, List[str]]:
289
+ clouds_with_capability = collections.defaultdict(list)
290
+ workspace_enabled_clouds = check_capabilities(quiet, verbose, clouds,
291
+ [capability], workspace)
292
+ for workspace, enabled_clouds in workspace_enabled_clouds.items():
293
+ for cloud, capabilities in enabled_clouds.items():
294
+ if capability in capabilities:
295
+ clouds_with_capability[workspace].append(cloud)
217
296
  return clouds_with_capability
218
297
 
219
298
 
@@ -221,10 +300,31 @@ def check(
221
300
  quiet: bool = False,
222
301
  verbose: bool = False,
223
302
  clouds: Optional[Iterable[str]] = None,
224
- ) -> List[str]:
225
- return list(
226
- check_capabilities(quiet, verbose, clouds,
227
- sky_cloud.ALL_CAPABILITIES).keys())
303
+ workspace: Optional[str] = None,
304
+ ) -> Dict[str, List[str]]:
305
+ enabled_clouds_by_workspace: Dict[str,
306
+ List[str]] = collections.defaultdict(list)
307
+ capabilities_result = check_capabilities(quiet, verbose, clouds,
308
+ sky_cloud.ALL_CAPABILITIES,
309
+ workspace)
310
+ for ws_name, enabled_clouds_with_capabilities in capabilities_result.items(
311
+ ):
312
+ # For each workspace, get a list of cloud names that have any
313
+ # capabilities enabled.
314
+ # The inner dict enabled_clouds_with_capabilities maps cloud_name to
315
+ # List[CloudCapability].
316
+ # If the list of capabilities is non-empty, the cloud is considered
317
+ # enabled.
318
+ # We are interested in the keys (cloud names) of this dict if their
319
+ # value (list of capabilities) is not empty.
320
+ # However, check_capabilities already ensures that only clouds with
321
+ # *some* enabled capabilities (from the ones being checked, i.e.
322
+ # ALL_CAPABILITIES here) are included in its return value.
323
+ # So, the keys of enabled_clouds_with_capabilities are the enabled cloud
324
+ # names for that workspace.
325
+ enabled_clouds_by_workspace[ws_name] = list(
326
+ enabled_clouds_with_capabilities.keys())
327
+ return enabled_clouds_by_workspace
228
328
 
229
329
 
230
330
  def get_cached_enabled_clouds_or_refresh(
@@ -243,17 +343,17 @@ def get_cached_enabled_clouds_or_refresh(
243
343
  raise_if_no_cloud_access is set to True.
244
344
  """
245
345
  cached_enabled_clouds = global_user_state.get_cached_enabled_clouds(
246
- capability)
346
+ capability, skypilot_config.get_active_workspace())
247
347
  if not cached_enabled_clouds:
248
348
  try:
249
- check_capability(sky_cloud.CloudCapability.COMPUTE, quiet=True)
349
+ check_capability(capability, quiet=True)
250
350
  except SystemExit:
251
351
  # If no cloud is enabled, check() will raise SystemExit.
252
352
  # Here we catch it and raise the exception later only if
253
353
  # raise_if_no_cloud_access is set to True.
254
354
  pass
255
355
  cached_enabled_clouds = global_user_state.get_cached_enabled_clouds(
256
- capability)
356
+ capability, skypilot_config.get_active_workspace())
257
357
  if raise_if_no_cloud_access and not cached_enabled_clouds:
258
358
  with ux_utils.print_exception_no_traceback():
259
359
  raise exceptions.NoCloudAccessError(
@@ -357,10 +457,10 @@ def _print_checked_cloud(
357
457
  f'{colorama.Style.RESET_ALL}{detail_string}'))
358
458
  if activated_account is not None:
359
459
  echo(f' Activated account: {activated_account}')
360
- for reason, caps in hints_to_capabilities.items():
361
- echo(f' Hint [{", ".join(caps)}]: {_yellow_color(reason)}')
362
- for reason, caps in reasons_to_capabilities.items():
363
- echo(f' Reason [{", ".join(caps)}]: {reason}')
460
+ for reason, capabilities in hints_to_capabilities.items():
461
+ echo(f' Hint [{", ".join(capabilities)}]: {_yellow_color(reason)}')
462
+ for reason, capabilities in reasons_to_capabilities.items():
463
+ echo(f' Reason [{", ".join(capabilities)}]: {reason}')
364
464
 
365
465
 
366
466
  def _green_color(str_to_format: str) -> str:
@@ -450,4 +550,69 @@ def _format_enabled_cloud(cloud_name: str,
450
550
  return (f'{title}' + _format_context_details(
451
551
  cloud_name, show_details=False, ctx2text=ctx2text))
452
552
 
453
- return title
553
+ if cloud_name == repr(sky_clouds.Kubernetes()):
554
+ # Get enabled contexts for Kubernetes
555
+ existing_contexts = sky_clouds.Kubernetes.existing_allowed_contexts()
556
+ if not existing_contexts:
557
+ return _green_color(cloud_and_capabilities)
558
+
559
+ # Check if allowed_contexts is explicitly set in config
560
+ allowed_contexts = skypilot_config.get_nested(
561
+ ('kubernetes', 'allowed_contexts'), None)
562
+
563
+ # Format the context info with consistent styling
564
+ if allowed_contexts is not None:
565
+ contexts_formatted = []
566
+ for i, context in enumerate(existing_contexts):
567
+ symbol = (ux_utils.INDENT_LAST_SYMBOL
568
+ if i == len(existing_contexts) -
569
+ 1 else ux_utils.INDENT_SYMBOL)
570
+ contexts_formatted.append(f'\n {symbol}{context}')
571
+ context_info = f' Allowed contexts:{"".join(contexts_formatted)}'
572
+ else:
573
+ context_info = f' Active context: {existing_contexts[0]}'
574
+
575
+ return (f'{_green_color(cloud_and_capabilities)}\n'
576
+ f' {colorama.Style.DIM}{context_info}'
577
+ f'{colorama.Style.RESET_ALL}')
578
+ return _green_color(cloud_and_capabilities)
579
+
580
+
581
+ def _summary_message(
582
+ enabled_clouds: Dict[str, List[sky_cloud.CloudCapability]],
583
+ cloud2ctx2text: Dict[str, Dict[str, str]],
584
+ current_workspace_name: str,
585
+ hide_workspace_str: bool,
586
+ disallowed_cloud_names: List[str],
587
+ ) -> str:
588
+ if not enabled_clouds:
589
+ enabled_clouds_str = '\n No infra to check/enabled.'
590
+ else:
591
+ enabled_clouds_str = '\n ' + '\n '.join([
592
+ _format_enabled_cloud(cloud, capabilities,
593
+ cloud2ctx2text.get(cloud, None))
594
+ for cloud, capabilities in sorted(enabled_clouds.items(),
595
+ key=lambda item: item[0])
596
+ ])
597
+
598
+ workspace_str = f' for workspace: {current_workspace_name!r}'
599
+ if hide_workspace_str:
600
+ workspace_str = ''
601
+
602
+ disallowed_clouds_hint = ''
603
+ if disallowed_cloud_names:
604
+ disable_for_workspace_hint = (
605
+ f' or disabled for this workspace {current_workspace_name!r}')
606
+ if hide_workspace_str:
607
+ disable_for_workspace_hint = ''
608
+ disallowed_clouds_hint = (
609
+ '\nNote: The following clouds were disabled because they were not '
610
+ 'included in allowed_clouds in ~/.sky/config.yaml'
611
+ f'{disable_for_workspace_hint}: '
612
+ f'{", ".join([c for c in disallowed_cloud_names])}')
613
+
614
+ return (f'\n{colorama.Fore.GREEN}{PARTY_POPPER_EMOJI} '
615
+ f'Enabled infra{workspace_str} '
616
+ f'{PARTY_POPPER_EMOJI}'
617
+ f'{colorama.Style.RESET_ALL}{enabled_clouds_str}'
618
+ f'{disallowed_clouds_hint}')