skypilot-nightly 1.0.0.dev20250522__py3-none-any.whl → 1.0.0.dev20250524__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 (120) hide show
  1. sky/__init__.py +2 -2
  2. sky/adaptors/kubernetes.py +46 -16
  3. sky/backends/backend_utils.py +62 -45
  4. sky/backends/cloud_vm_ray_backend.py +19 -5
  5. sky/check.py +398 -171
  6. sky/cli.py +302 -98
  7. sky/client/cli.py +302 -98
  8. sky/client/sdk.py +104 -12
  9. sky/clouds/__init__.py +3 -0
  10. sky/clouds/aws.py +4 -2
  11. sky/clouds/azure.py +4 -2
  12. sky/clouds/cloud.py +24 -6
  13. sky/clouds/cudo.py +2 -1
  14. sky/clouds/do.py +2 -1
  15. sky/clouds/fluidstack.py +2 -1
  16. sky/clouds/gcp.py +23 -5
  17. sky/clouds/ibm.py +4 -2
  18. sky/clouds/kubernetes.py +66 -22
  19. sky/clouds/lambda_cloud.py +2 -1
  20. sky/clouds/nebius.py +18 -2
  21. sky/clouds/oci.py +4 -2
  22. sky/clouds/paperspace.py +2 -1
  23. sky/clouds/runpod.py +2 -1
  24. sky/clouds/scp.py +2 -1
  25. sky/clouds/service_catalog/constants.py +1 -1
  26. sky/clouds/service_catalog/ssh_catalog.py +167 -0
  27. sky/clouds/ssh.py +203 -0
  28. sky/clouds/vast.py +2 -1
  29. sky/clouds/vsphere.py +2 -1
  30. sky/core.py +58 -11
  31. sky/dashboard/out/404.html +1 -1
  32. sky/dashboard/out/_next/static/aHej19bZyl4hoHgrzPCn7/_buildManifest.js +1 -0
  33. sky/dashboard/out/_next/static/chunks/480-ee58038f1a4afd5c.js +1 -0
  34. sky/dashboard/out/_next/static/chunks/488-50d843fdb5396d32.js +15 -0
  35. sky/dashboard/out/_next/static/chunks/498-d7722313e5e5b4e6.js +21 -0
  36. sky/dashboard/out/_next/static/chunks/573-f17bd89d9f9118b3.js +66 -0
  37. sky/dashboard/out/_next/static/chunks/578-7a4795009a56430c.js +6 -0
  38. sky/dashboard/out/_next/static/chunks/734-5f5ce8f347b7f417.js +1 -0
  39. sky/dashboard/out/_next/static/chunks/937.f97f83652028e944.js +1 -0
  40. sky/dashboard/out/_next/static/chunks/938-f347f6144075b0c8.js +1 -0
  41. sky/dashboard/out/_next/static/chunks/9f96d65d-5a3e4af68c26849e.js +1 -0
  42. sky/dashboard/out/_next/static/chunks/pages/_app-dec800f9ef1b10f4.js +1 -0
  43. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-37c042a356f8e608.js +1 -0
  44. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-9529d9e882a0e75c.js +16 -0
  45. sky/dashboard/out/_next/static/chunks/pages/clusters-9e6d1ec6e1ac5b29.js +1 -0
  46. sky/dashboard/out/_next/static/chunks/pages/infra-e690d864aa00e2ea.js +1 -0
  47. sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-db6558a5ec687011.js +1 -0
  48. sky/dashboard/out/_next/static/chunks/pages/jobs-73d5e0c369d00346.js +16 -0
  49. sky/dashboard/out/_next/static/chunks/pages/users-2d319455c3f1c3e2.js +1 -0
  50. sky/dashboard/out/_next/static/chunks/pages/workspaces-02a7b60f2ead275f.js +1 -0
  51. sky/dashboard/out/_next/static/chunks/webpack-deda68c926e8d0bc.js +1 -0
  52. sky/dashboard/out/_next/static/css/d2cdba64c9202dd7.css +3 -0
  53. sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
  54. sky/dashboard/out/clusters/[cluster].html +1 -1
  55. sky/dashboard/out/clusters.html +1 -1
  56. sky/dashboard/out/index.html +1 -1
  57. sky/dashboard/out/infra.html +1 -1
  58. sky/dashboard/out/jobs/[job].html +1 -1
  59. sky/dashboard/out/jobs.html +1 -1
  60. sky/dashboard/out/users.html +1 -0
  61. sky/dashboard/out/workspaces.html +1 -0
  62. sky/data/storage.py +1 -1
  63. sky/global_user_state.py +42 -19
  64. sky/jobs/constants.py +1 -1
  65. sky/jobs/server/core.py +72 -56
  66. sky/jobs/state.py +26 -5
  67. sky/jobs/utils.py +65 -13
  68. sky/optimizer.py +29 -7
  69. sky/provision/__init__.py +1 -0
  70. sky/provision/aws/instance.py +17 -1
  71. sky/provision/fluidstack/instance.py +1 -0
  72. sky/provision/kubernetes/instance.py +16 -5
  73. sky/provision/kubernetes/utils.py +37 -19
  74. sky/provision/nebius/instance.py +3 -1
  75. sky/provision/nebius/utils.py +14 -2
  76. sky/provision/ssh/__init__.py +18 -0
  77. sky/resources.py +4 -1
  78. sky/serve/server/core.py +9 -6
  79. sky/server/html/token_page.html +6 -1
  80. sky/server/requests/executor.py +1 -0
  81. sky/server/requests/payloads.py +18 -0
  82. sky/server/server.py +108 -5
  83. sky/setup_files/dependencies.py +1 -0
  84. sky/skylet/constants.py +4 -1
  85. sky/skypilot_config.py +83 -9
  86. sky/templates/nebius-ray.yml.j2 +12 -0
  87. sky/utils/cli_utils/status_utils.py +18 -8
  88. sky/utils/infra_utils.py +21 -1
  89. sky/utils/kubernetes/cleanup-tunnel.sh +62 -0
  90. sky/utils/kubernetes/create_cluster.sh +1 -0
  91. sky/utils/kubernetes/deploy_remote_cluster.py +1440 -0
  92. sky/utils/kubernetes/kubernetes_deploy_utils.py +117 -10
  93. sky/utils/kubernetes/ssh-tunnel.sh +387 -0
  94. sky/utils/log_utils.py +218 -1
  95. sky/utils/schemas.py +75 -0
  96. sky/utils/ux_utils.py +2 -1
  97. {skypilot_nightly-1.0.0.dev20250522.dist-info → skypilot_nightly-1.0.0.dev20250524.dist-info}/METADATA +6 -1
  98. {skypilot_nightly-1.0.0.dev20250522.dist-info → skypilot_nightly-1.0.0.dev20250524.dist-info}/RECORD +103 -91
  99. sky/dashboard/out/_next/static/CzOVV6JpRQBRt5GhZuhyK/_buildManifest.js +0 -1
  100. sky/dashboard/out/_next/static/chunks/236-1a3a9440417720eb.js +0 -6
  101. sky/dashboard/out/_next/static/chunks/312-c3c8845990db8ffc.js +0 -15
  102. sky/dashboard/out/_next/static/chunks/37-d584022b0da4ac3b.js +0 -6
  103. sky/dashboard/out/_next/static/chunks/393-e1eaa440481337ec.js +0 -1
  104. sky/dashboard/out/_next/static/chunks/480-f28cd152a98997de.js +0 -1
  105. sky/dashboard/out/_next/static/chunks/582-683f4f27b81996dc.js +0 -59
  106. sky/dashboard/out/_next/static/chunks/pages/_app-8cfab319f9fb3ae8.js +0 -1
  107. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-33bc2bec322249b1.js +0 -1
  108. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-e2fc2dd1955e6c36.js +0 -1
  109. sky/dashboard/out/_next/static/chunks/pages/clusters-3a748bd76e5c2984.js +0 -1
  110. sky/dashboard/out/_next/static/chunks/pages/infra-9180cd91cee64b96.js +0 -1
  111. sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-70756c2dad850a7e.js +0 -1
  112. sky/dashboard/out/_next/static/chunks/pages/jobs-ecd804b9272f4a7c.js +0 -1
  113. sky/dashboard/out/_next/static/chunks/webpack-830f59b8404e96b8.js +0 -1
  114. sky/dashboard/out/_next/static/css/7e7ce4ff31d3977b.css +0 -3
  115. sky/utils/kubernetes/deploy_remote_cluster.sh +0 -308
  116. /sky/dashboard/out/_next/static/{CzOVV6JpRQBRt5GhZuhyK → aHej19bZyl4hoHgrzPCn7}/_ssgManifest.js +0 -0
  117. {skypilot_nightly-1.0.0.dev20250522.dist-info → skypilot_nightly-1.0.0.dev20250524.dist-info}/WHEEL +0 -0
  118. {skypilot_nightly-1.0.0.dev20250522.dist-info → skypilot_nightly-1.0.0.dev20250524.dist-info}/entry_points.txt +0 -0
  119. {skypilot_nightly-1.0.0.dev20250522.dist-info → skypilot_nightly-1.0.0.dev20250524.dist-info}/licenses/LICENSE +0 -0
  120. {skypilot_nightly-1.0.0.dev20250522.dist-info → skypilot_nightly-1.0.0.dev20250524.dist-info}/top_level.txt +0 -0
sky/check.py CHANGED
@@ -4,8 +4,7 @@ import itertools
4
4
  import os
5
5
  import traceback
6
6
  from types import ModuleType
7
- from typing import (Any, Callable, Dict, Iterable, List, Optional, Set, Tuple,
8
- Union)
7
+ from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
9
8
 
10
9
  import click
11
10
  import colorama
@@ -16,6 +15,7 @@ from sky import global_user_state
16
15
  from sky import skypilot_config
17
16
  from sky.adaptors import cloudflare
18
17
  from sky.clouds import cloud as sky_cloud
18
+ from sky.skylet import constants
19
19
  from sky.utils import registry
20
20
  from sky.utils import rich_utils
21
21
  from sky.utils import subprocess_utils
@@ -30,168 +30,250 @@ def check_capabilities(
30
30
  verbose: bool = False,
31
31
  clouds: Optional[Iterable[str]] = None,
32
32
  capabilities: Optional[List[sky_cloud.CloudCapability]] = None,
33
- ) -> Dict[str, List[sky_cloud.CloudCapability]]:
33
+ workspace: Optional[str] = None,
34
+ ) -> Dict[str, Dict[str, List[sky_cloud.CloudCapability]]]:
34
35
  echo = (lambda *_args, **_kwargs: None
35
36
  ) if quiet else lambda *args, **kwargs: click.echo(
36
37
  *args, **kwargs, color=True)
37
- echo('Checking credentials to enable infra for SkyPilot.')
38
+ all_workspaces_results: Dict[str,
39
+ Dict[str,
40
+ List[sky_cloud.CloudCapability]]] = {}
41
+ available_workspaces = list(skypilot_config.get_workspaces().keys())
42
+ hide_workspace_str = (available_workspaces == [
43
+ constants.SKYPILOT_DEFAULT_WORKSPACE
44
+ ])
45
+ initial_hint = 'Checking credentials to enable infra for SkyPilot.'
46
+ if len(available_workspaces) > 1:
47
+ initial_hint = (f'Checking credentials to enable infra for SkyPilot '
48
+ f'(Workspaces: {", ".join(available_workspaces)}).')
49
+ echo(initial_hint)
38
50
  if capabilities is None:
39
51
  capabilities = sky_cloud.ALL_CAPABILITIES
40
52
  assert capabilities is not None
41
- enabled_clouds: Dict[str, List[sky_cloud.CloudCapability]] = {}
42
- disabled_clouds: Dict[str, List[sky_cloud.CloudCapability]] = {}
43
-
44
- def check_one_cloud_one_capability(
45
- payload: Tuple[Tuple[str, Union[sky_clouds.Cloud, ModuleType]],
46
- sky_cloud.CloudCapability]
47
- ) -> Optional[Tuple[sky_cloud.CloudCapability, bool, Optional[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
- return capability, ok, reason.strip() if reason else None
57
-
58
- def get_cloud_tuple(
59
- cloud_name: str) -> Tuple[str, Union[sky_clouds.Cloud, ModuleType]]:
60
- # Validates cloud_name and returns a tuple of the cloud's name and
61
- # the cloud object. Includes special handling for Cloudflare.
62
- if cloud_name.lower().startswith('cloudflare'):
63
- return cloudflare.NAME, cloudflare
64
- else:
65
- cloud_obj = registry.CLOUD_REGISTRY.from_str(cloud_name)
66
- assert cloud_obj is not None, f'Cloud {cloud_name!r} not found'
67
- return repr(cloud_obj), cloud_obj
68
53
 
69
- def get_all_clouds():
54
+ def get_all_clouds() -> Tuple[str, ...]:
70
55
  return tuple([repr(c) for c in registry.CLOUD_REGISTRY.values()] +
71
56
  [cloudflare.NAME])
72
57
 
73
- if clouds is not None:
74
- cloud_list = clouds
75
- else:
76
- cloud_list = get_all_clouds()
77
- clouds_to_check = [get_cloud_tuple(c) for c in cloud_list]
78
-
79
- # Use allowed_clouds from config if it exists, otherwise check all clouds.
80
- # Also validate names with get_cloud_tuple.
81
- config_allowed_cloud_names = sorted([
82
- get_cloud_tuple(c)[0] for c in skypilot_config.get_nested((
83
- 'allowed_clouds',), get_all_clouds())
84
- ])
85
- # Use disallowed_cloud_names for logging the clouds that will be disabled
86
- # because they are not included in allowed_clouds in config.yaml.
87
- disallowed_cloud_names = [
88
- c for c in get_all_clouds() if c not in config_allowed_cloud_names
89
- ]
90
- # Check only the clouds which are allowed in the config.
91
- clouds_to_check = [
92
- c for c in clouds_to_check if c[0] in config_allowed_cloud_names
93
- ]
94
-
95
- combinations = list(itertools.product(clouds_to_check, capabilities))
96
- with rich_utils.safe_status('Checking infra choices...'):
97
- check_results = subprocess_utils.run_in_parallel(
98
- check_one_cloud_one_capability, combinations)
99
-
100
- check_results_dict: Dict[
101
- Tuple[str, Union[sky_clouds.Cloud, ModuleType]],
102
- List[Tuple[sky_cloud.CloudCapability, bool,
103
- Optional[str]]]] = collections.defaultdict(list)
104
- for combination, check_result in zip(combinations, check_results):
105
- if check_result is None:
106
- continue
107
- capability, ok, _ = check_result
108
- cloud_tuple, _ = combination
109
- cloud_repr = cloud_tuple[0]
110
- if ok:
111
- enabled_clouds.setdefault(cloud_repr, []).append(capability)
58
+ def _execute_check_logic_for_workspace(
59
+ current_workspace_name: str,
60
+ hide_per_cloud_details: bool,
61
+ hide_workspace_str: bool,
62
+ ) -> Dict[str, List[sky_cloud.CloudCapability]]:
63
+ nonlocal echo, verbose, clouds, quiet
64
+
65
+ enabled_clouds: Dict[str, List[sky_cloud.CloudCapability]] = {}
66
+ disabled_clouds: Dict[str, List[sky_cloud.CloudCapability]] = {}
67
+
68
+ def check_one_cloud_one_capability(
69
+ payload: Tuple[Tuple[str, Union[sky_clouds.Cloud, ModuleType]],
70
+ sky_cloud.CloudCapability]
71
+ ) -> Optional[Tuple[sky_cloud.CloudCapability, bool, Optional[Union[
72
+ str, Dict[str, str]]]]]:
73
+ with skypilot_config.local_active_workspace_ctx(
74
+ current_workspace_name):
75
+ # Have to override again for specific thread, as the
76
+ # local_active_workspace_ctx is thread-local.
77
+ cloud_tuple, capability = payload
78
+ _, cloud = cloud_tuple
79
+ try:
80
+ ok, reason = cloud.check_credentials(capability)
81
+ except exceptions.NotSupportedError:
82
+ return None
83
+ except Exception: # pylint: disable=broad-except
84
+ ok, reason = False, traceback.format_exc()
85
+ if not isinstance(reason, dict):
86
+ reason = reason.strip() if reason else None
87
+ return (capability, ok, reason)
88
+
89
+ def get_cloud_tuple(
90
+ cloud_name: str
91
+ ) -> Tuple[str, Union[sky_clouds.Cloud, ModuleType]]:
92
+ # Validates cloud_name and returns a tuple of the cloud's name and
93
+ # the cloud object. Includes special handling for Cloudflare.
94
+ if cloud_name.lower().startswith('cloudflare'):
95
+ return cloudflare.NAME, cloudflare
96
+ else:
97
+ cloud_obj = registry.CLOUD_REGISTRY.from_str(cloud_name)
98
+ assert cloud_obj is not None, f'Cloud {cloud_name!r} not found'
99
+ return repr(cloud_obj), cloud_obj
100
+
101
+ if clouds is not None:
102
+ cloud_list = clouds
112
103
  else:
113
- disabled_clouds.setdefault(cloud_repr, []).append(capability)
114
- check_results_dict[cloud_tuple].append(check_result)
115
-
116
- for cloud_tuple, check_result_list in sorted(check_results_dict.items(),
117
- key=lambda item: item[0][0]):
118
- _print_checked_cloud(echo, verbose, cloud_tuple, check_result_list)
119
-
120
- # Determine the set of enabled clouds: (previously enabled clouds + newly
121
- # enabled clouds - newly disabled clouds) intersected with
122
- # config_allowed_clouds, if specified in config.yaml.
123
- # This means that if a cloud is already enabled and is not included in
124
- # allowed_clouds in config.yaml, it will be disabled.
125
- all_enabled_clouds: Set[str] = set()
126
- for capability in capabilities:
127
- # Cloudflare is not a real cloud in registry.CLOUD_REGISTRY, and should
128
- # not be inserted into the DB (otherwise `sky launch` and other code
129
- # would error out when it's trying to look it up in the registry).
130
- enabled_clouds_set = {
131
- cloud for cloud, capabilities in enabled_clouds.items()
132
- if capability in capabilities and not cloud.startswith('Cloudflare')
133
- }
134
- disabled_clouds_set = {
135
- cloud for cloud, capabilities in disabled_clouds.items()
136
- if capability in capabilities and not cloud.startswith('Cloudflare')
137
- }
138
- config_allowed_clouds_set = {
139
- cloud for cloud in config_allowed_cloud_names
140
- if not cloud.startswith('Cloudflare')
141
- }
142
- previously_enabled_clouds_set = {
143
- repr(cloud)
144
- for cloud in global_user_state.get_cached_enabled_clouds(capability)
145
- }
146
- enabled_clouds_for_capability = (config_allowed_clouds_set & (
147
- (previously_enabled_clouds_set | enabled_clouds_set) -
148
- disabled_clouds_set))
149
- global_user_state.set_enabled_clouds(
150
- list(enabled_clouds_for_capability), capability)
151
- all_enabled_clouds = all_enabled_clouds.union(
152
- enabled_clouds_for_capability)
153
- disallowed_clouds_hint = None
154
- if disallowed_cloud_names:
155
- disallowed_clouds_hint = (
156
- '\nNote: The following clouds were disabled because they were not '
157
- 'included in allowed_clouds in ~/.sky/config.yaml: '
158
- f'{", ".join([c for c in disallowed_cloud_names])}')
159
- if not all_enabled_clouds:
104
+ cloud_list = get_all_clouds()
105
+
106
+ clouds_to_check = [get_cloud_tuple(c) for c in cloud_list]
107
+
108
+ # Use allowed_clouds from config if it exists, otherwise check all
109
+ # clouds. Also validate names with get_cloud_tuple.
110
+ config_allowed_cloud_names = sorted([
111
+ get_cloud_tuple(c)[0] for c in skypilot_config.get_nested((
112
+ 'allowed_clouds',), get_all_clouds())
113
+ ])
114
+
115
+ # filter out the clouds that are disabled in the workspace config
116
+ workspace_disabled_clouds = []
117
+ for cloud in config_allowed_cloud_names:
118
+ cloud_config = skypilot_config.get_workspace_cloud(
119
+ cloud, workspace=current_workspace_name)
120
+ cloud_disabled = cloud_config.get('disabled', False)
121
+ if cloud_disabled:
122
+ workspace_disabled_clouds.append(cloud)
123
+
124
+ config_allowed_cloud_names = [
125
+ c for c in config_allowed_cloud_names
126
+ if c not in workspace_disabled_clouds
127
+ ]
128
+
129
+ # Use disallowed_cloud_names for logging the clouds that will be
130
+ # disabled because they are not included in allowed_clouds in
131
+ # config.yaml.
132
+ disallowed_cloud_names = [
133
+ c for c in get_all_clouds() if c not in config_allowed_cloud_names
134
+ ]
135
+ # Check only the clouds which are allowed in the config.
136
+ clouds_to_check = [
137
+ c for c in clouds_to_check if c[0] in config_allowed_cloud_names
138
+ ]
139
+
140
+ combinations = list(itertools.product(clouds_to_check, capabilities))
141
+
142
+ cloud2ctx2text: Dict[str, Dict[str, str]] = {}
143
+ if not combinations:
144
+ echo(
145
+ _summary_message(enabled_clouds, cloud2ctx2text,
146
+ current_workspace_name, hide_workspace_str,
147
+ disallowed_cloud_names))
148
+
149
+ return {}
150
+
151
+ workspace_str = f' for workspace: {current_workspace_name!r}'
152
+ if hide_workspace_str:
153
+ workspace_str = ''
154
+ with rich_utils.safe_status(
155
+ ux_utils.spinner_message(
156
+ f'Checking infra choices{workspace_str}...')):
157
+ check_results = subprocess_utils.run_in_parallel(
158
+ check_one_cloud_one_capability, combinations)
159
+
160
+ check_results_dict: Dict[
161
+ Tuple[str, Union[sky_clouds.Cloud, ModuleType]],
162
+ List[Tuple[sky_cloud.CloudCapability, bool,
163
+ Optional[Union[str, Dict[str, str]]]]]] = (
164
+ collections.defaultdict(list))
165
+ for combination, check_result in zip(combinations, check_results):
166
+ if check_result is None:
167
+ continue
168
+ capability, ok, ctx2text = check_result
169
+ cloud_tuple, _ = combination
170
+ cloud_repr = cloud_tuple[0]
171
+ if isinstance(ctx2text, dict):
172
+ cloud2ctx2text[cloud_repr] = ctx2text
173
+ if ok:
174
+ enabled_clouds.setdefault(cloud_repr, []).append(capability)
175
+ else:
176
+ disabled_clouds.setdefault(cloud_repr, []).append(capability)
177
+ check_results_dict[cloud_tuple].append(check_result)
178
+
179
+ if not hide_per_cloud_details:
180
+ for cloud_tuple, check_result_list in sorted(
181
+ check_results_dict.items(), key=lambda item: item[0][0]):
182
+ _print_checked_cloud(echo, verbose, cloud_tuple,
183
+ check_result_list,
184
+ cloud2ctx2text.get(cloud_tuple[0], {}))
185
+
186
+ # Determine the set of enabled clouds: (previously enabled clouds +
187
+ # newly enabled clouds - newly disabled clouds) intersected with
188
+ # config_allowed_clouds, if specified in config.yaml.
189
+ # This means that if a cloud is already enabled and is not included in
190
+ # allowed_clouds in config.yaml, it will be disabled.
191
+ all_enabled_clouds: Set[str] = set()
192
+ for capability in capabilities:
193
+ # Cloudflare is not a real cloud in registry.CLOUD_REGISTRY, and
194
+ # should not be inserted into the DB (otherwise `sky launch` and
195
+ # other code would error out when it's trying to look it up in the
196
+ # registry).
197
+ enabled_clouds_set = {
198
+ cloud for cloud, capabilities in enabled_clouds.items()
199
+ if capability in capabilities and
200
+ not cloud.startswith('Cloudflare')
201
+ }
202
+ disabled_clouds_set = {
203
+ cloud for cloud, capabilities in disabled_clouds.items()
204
+ if capability in capabilities and
205
+ not cloud.startswith('Cloudflare')
206
+ }
207
+ config_allowed_clouds_set = {
208
+ cloud for cloud in config_allowed_cloud_names
209
+ if not cloud.startswith('Cloudflare')
210
+ }
211
+ previously_enabled_clouds_set = {
212
+ repr(cloud)
213
+ for cloud in global_user_state.get_cached_enabled_clouds(
214
+ capability, current_workspace_name)
215
+ }
216
+ enabled_clouds_for_capability = (config_allowed_clouds_set & (
217
+ (previously_enabled_clouds_set | enabled_clouds_set) -
218
+ disabled_clouds_set))
219
+
220
+ global_user_state.set_enabled_clouds(
221
+ list(enabled_clouds_for_capability), capability,
222
+ current_workspace_name)
223
+ all_enabled_clouds = all_enabled_clouds.union(
224
+ enabled_clouds_for_capability)
225
+
160
226
  echo(
161
- click.style(
162
- 'No cloud is enabled. SkyPilot will not be able to run any '
163
- 'task. Run `sky check` for more info.',
164
- fg='red',
165
- bold=True))
166
- if disallowed_clouds_hint:
167
- echo(click.style(disallowed_clouds_hint, dim=True))
168
- raise SystemExit()
227
+ _summary_message(enabled_clouds, cloud2ctx2text,
228
+ current_workspace_name, hide_workspace_str,
229
+ disallowed_cloud_names))
230
+
231
+ return enabled_clouds
232
+
233
+ # --- Main check_capabilities logic ---
234
+
235
+ if workspace is not None:
236
+ # Check only the specified workspace
237
+ if workspace not in available_workspaces:
238
+ with ux_utils.print_exception_no_traceback():
239
+ raise ValueError(
240
+ f'Workspace {workspace!r} not found in SkyPilot '
241
+ 'configuration. '
242
+ f'Available workspaces: {", ".join(available_workspaces)}')
243
+
244
+ # Always show details for single specified check (if verbose)
245
+ hide_per_cloud_details_flag = False
246
+ with skypilot_config.local_active_workspace_ctx(workspace):
247
+ enabled_ws_clouds = _execute_check_logic_for_workspace(
248
+ workspace, hide_per_cloud_details_flag, hide_workspace_str)
249
+ all_workspaces_results[workspace] = enabled_ws_clouds
169
250
  else:
170
- clouds_arg = (f' {" ".join(disabled_clouds).lower()}'
171
- if clouds is not None else '')
251
+ # Check all workspaces
252
+ workspaces_to_check = available_workspaces
253
+
254
+ hide_per_cloud_details_flag = (not verbose and
255
+ len(workspaces_to_check) > 1)
256
+
257
+ for ws_name in workspaces_to_check:
258
+ if not hide_workspace_str:
259
+ echo(f'\nChecking enabled infra for workspace: {ws_name!r}')
260
+ with skypilot_config.local_active_workspace_ctx(ws_name):
261
+ enabled_ws_clouds = _execute_check_logic_for_workspace(
262
+ ws_name, hide_per_cloud_details_flag, hide_workspace_str)
263
+ all_workspaces_results[ws_name] = enabled_ws_clouds
264
+
265
+ # Global "To enable a cloud..." message, printed once if relevant
266
+ if not quiet:
172
267
  echo(
173
268
  click.style(
174
269
  '\nTo enable a cloud, follow the hints above and rerun: ',
175
- dim=True) + click.style(f'sky check{clouds_arg}', bold=True) +
176
- '\n' + click.style(
270
+ dim=True) + click.style('sky check', bold=True) + '\n' +
271
+ click.style(
177
272
  'If any problems remain, refer to detailed docs at: '
178
273
  'https://docs.skypilot.co/en/latest/getting-started/installation.html', # pylint: disable=line-too-long
179
274
  dim=True))
180
275
 
181
- if disallowed_clouds_hint:
182
- echo(click.style(disallowed_clouds_hint, dim=True))
183
-
184
- # Pretty print for UX.
185
- if not quiet:
186
- enabled_clouds_str = '\n ' + '\n '.join([
187
- _format_enabled_cloud(cloud, capabilities)
188
- for cloud, capabilities in sorted(enabled_clouds.items(),
189
- key=lambda item: item[0])
190
- ])
191
- echo(f'\n{colorama.Fore.GREEN}{PARTY_POPPER_EMOJI} '
192
- f'Enabled infra {PARTY_POPPER_EMOJI}'
193
- f'{colorama.Style.RESET_ALL}{enabled_clouds_str}')
194
- return enabled_clouds
276
+ return all_workspaces_results
195
277
 
196
278
 
197
279
  def check_capability(
@@ -199,12 +281,15 @@ def check_capability(
199
281
  quiet: bool = False,
200
282
  verbose: bool = False,
201
283
  clouds: Optional[Iterable[str]] = None,
202
- ) -> List[str]:
203
- clouds_with_capability = []
204
- enabled_clouds = check_capabilities(quiet, verbose, clouds, [capability])
205
- for cloud, capabilities in enabled_clouds.items():
206
- if capability in capabilities:
207
- clouds_with_capability.append(cloud)
284
+ workspace: Optional[str] = None,
285
+ ) -> Dict[str, List[str]]:
286
+ clouds_with_capability = collections.defaultdict(list)
287
+ workspace_enabled_clouds = check_capabilities(quiet, verbose, clouds,
288
+ [capability], workspace)
289
+ for workspace, enabled_clouds in workspace_enabled_clouds.items():
290
+ for cloud, capabilities in enabled_clouds.items():
291
+ if capability in capabilities:
292
+ clouds_with_capability[workspace].append(cloud)
208
293
  return clouds_with_capability
209
294
 
210
295
 
@@ -212,10 +297,31 @@ def check(
212
297
  quiet: bool = False,
213
298
  verbose: bool = False,
214
299
  clouds: Optional[Iterable[str]] = None,
215
- ) -> List[str]:
216
- return list(
217
- check_capabilities(quiet, verbose, clouds,
218
- sky_cloud.ALL_CAPABILITIES).keys())
300
+ workspace: Optional[str] = None,
301
+ ) -> Dict[str, List[str]]:
302
+ enabled_clouds_by_workspace: Dict[str,
303
+ List[str]] = collections.defaultdict(list)
304
+ capabilities_result = check_capabilities(quiet, verbose, clouds,
305
+ sky_cloud.ALL_CAPABILITIES,
306
+ workspace)
307
+ for ws_name, enabled_clouds_with_capabilities in capabilities_result.items(
308
+ ):
309
+ # For each workspace, get a list of cloud names that have any
310
+ # capabilities enabled.
311
+ # The inner dict enabled_clouds_with_capabilities maps cloud_name to
312
+ # List[CloudCapability].
313
+ # If the list of capabilities is non-empty, the cloud is considered
314
+ # enabled.
315
+ # We are interested in the keys (cloud names) of this dict if their
316
+ # value (list of capabilities) is not empty.
317
+ # However, check_capabilities already ensures that only clouds with
318
+ # *some* enabled capabilities (from the ones being checked, i.e.
319
+ # ALL_CAPABILITIES here) are included in its return value.
320
+ # So, the keys of enabled_clouds_with_capabilities are the enabled cloud
321
+ # names for that workspace.
322
+ enabled_clouds_by_workspace[ws_name] = list(
323
+ enabled_clouds_with_capabilities.keys())
324
+ return enabled_clouds_by_workspace
219
325
 
220
326
 
221
327
  def get_cached_enabled_clouds_or_refresh(
@@ -234,17 +340,17 @@ def get_cached_enabled_clouds_or_refresh(
234
340
  raise_if_no_cloud_access is set to True.
235
341
  """
236
342
  cached_enabled_clouds = global_user_state.get_cached_enabled_clouds(
237
- capability)
343
+ capability, skypilot_config.get_active_workspace())
238
344
  if not cached_enabled_clouds:
239
345
  try:
240
- check_capability(sky_cloud.CloudCapability.COMPUTE, quiet=True)
346
+ check_capability(capability, quiet=True)
241
347
  except SystemExit:
242
348
  # If no cloud is enabled, check() will raise SystemExit.
243
349
  # Here we catch it and raise the exception later only if
244
350
  # raise_if_no_cloud_access is set to True.
245
351
  pass
246
352
  cached_enabled_clouds = global_user_state.get_cached_enabled_clouds(
247
- capability)
353
+ capability, skypilot_config.get_active_workspace())
248
354
  if raise_if_no_cloud_access and not cached_enabled_clouds:
249
355
  with ux_utils.print_exception_no_traceback():
250
356
  raise exceptions.NoCloudAccessError(
@@ -290,7 +396,8 @@ def _print_checked_cloud(
290
396
  verbose: bool,
291
397
  cloud_tuple: Tuple[str, Union[sky_clouds.Cloud, ModuleType]],
292
398
  cloud_capabilities: List[Tuple[sky_cloud.CloudCapability, bool,
293
- Optional[str]]],
399
+ Optional[Union[str, Dict[str, str]]]]],
400
+ ctx2text: Dict[str, str],
294
401
  ) -> None:
295
402
  """Prints whether a cloud is enabled, and the capabilities that are enabled.
296
403
  If any hints (for enabled capabilities) or
@@ -317,34 +424,113 @@ def _print_checked_cloud(
317
424
  for capability, ok, reason in cloud_capabilities:
318
425
  if ok:
319
426
  enabled_capabilities.append(capability)
427
+ # `dict` reasons for K8s and SSH will be printed in detail in
428
+ # _format_enabled_cloud. Skip here.
429
+ if not isinstance(reason, str):
430
+ continue
431
+ if ok:
320
432
  if reason is not None:
321
433
  hints_to_capabilities.setdefault(reason, []).append(capability)
322
434
  elif reason is not None:
323
435
  reasons_to_capabilities.setdefault(reason, []).append(capability)
436
+ style_str = f'{colorama.Style.DIM}'
324
437
  status_msg: str = 'disabled'
325
- styles: Dict[str, Any] = {'dim': True}
326
438
  capability_string: str = ''
439
+ detail_string: str = ''
327
440
  activated_account: Optional[str] = None
328
441
  if enabled_capabilities:
442
+ style_str = f'{colorama.Fore.GREEN}{colorama.Style.NORMAL}'
329
443
  status_msg = 'enabled'
330
- styles = {'fg': 'green', 'bold': False}
331
444
  capability_string = f'[{", ".join(enabled_capabilities)}]'
332
445
  if verbose and cloud is not cloudflare:
333
446
  activated_account = cloud.get_active_user_identity_str()
334
-
447
+ if isinstance(cloud_tuple[1], (sky_clouds.SSH, sky_clouds.Kubernetes)):
448
+ detail_string = _format_context_details(cloud_tuple[1],
449
+ show_details=True,
450
+ ctx2text=ctx2text)
335
451
  echo(
336
- click.style(f' {cloud_repr}: {status_msg} {capability_string}',
337
- **styles))
452
+ click.style(
453
+ f'{style_str} {cloud_repr}: {status_msg} {capability_string}'
454
+ f'{colorama.Style.RESET_ALL}{detail_string}'))
338
455
  if activated_account is not None:
339
456
  echo(f' Activated account: {activated_account}')
340
- for reason, caps in hints_to_capabilities.items():
341
- echo(f' Hint [{", ".join(caps)}]: {_yellow_color(reason)}')
342
- for reason, caps in reasons_to_capabilities.items():
343
- echo(f' Reason [{", ".join(caps)}]: {reason}')
457
+ for reason, capabilities in hints_to_capabilities.items():
458
+ echo(f' Hint [{", ".join(capabilities)}]: {_yellow_color(reason)}')
459
+ for reason, capabilities in reasons_to_capabilities.items():
460
+ echo(f' Reason [{", ".join(capabilities)}]: {reason}')
461
+
462
+
463
+ def _green_color(str_to_format: str) -> str:
464
+ return f'{colorama.Fore.GREEN}{str_to_format}{colorama.Style.RESET_ALL}'
465
+
466
+
467
+ def _format_context_details(cloud: Union[str, sky_clouds.Cloud],
468
+ show_details: bool,
469
+ ctx2text: Optional[Dict[str, str]] = None) -> str:
470
+ if isinstance(cloud, str):
471
+ cloud_type = registry.CLOUD_REGISTRY.from_str(cloud)
472
+ assert cloud_type is not None
473
+ else:
474
+ cloud_type = cloud
475
+ if isinstance(cloud_type, sky_clouds.SSH):
476
+ # Get the cluster names by reading from the node pools file
477
+ contexts = sky_clouds.SSH.get_ssh_node_pool_contexts()
478
+ else:
479
+ assert isinstance(cloud_type, sky_clouds.Kubernetes)
480
+ contexts = sky_clouds.Kubernetes.existing_allowed_contexts()
481
+
482
+ filtered_contexts = []
483
+ for context in contexts:
484
+ if not show_details:
485
+ # Skip
486
+ if (ctx2text is None or context not in ctx2text or
487
+ 'disabled' in ctx2text[context]):
488
+ continue
489
+ filtered_contexts.append(context)
490
+
491
+ if not filtered_contexts:
492
+ return ''
493
+
494
+ def _red_color(str_to_format: str) -> str:
495
+ return (f'{colorama.Fore.LIGHTRED_EX}'
496
+ f'{str_to_format}'
497
+ f'{colorama.Style.RESET_ALL}')
498
+
499
+ def _dim_color(str_to_format: str) -> str:
500
+ return (f'{colorama.Style.DIM}'
501
+ f'{str_to_format}'
502
+ f'{colorama.Style.RESET_ALL}')
503
+
504
+ # Format the context info with consistent styling
505
+ contexts_formatted = []
506
+ for i, context in enumerate(filtered_contexts):
507
+ if isinstance(cloud_type, sky_clouds.SSH):
508
+ # TODO: This is a hack to remove the 'ssh-' prefix from the
509
+ # context name. Once we have a separate kubeconfig for SSH,
510
+ # this will not be required.
511
+ cleaned_context = context.lstrip('ssh-')
512
+ else:
513
+ cleaned_context = context
514
+ symbol = (ux_utils.INDENT_LAST_SYMBOL if i == len(filtered_contexts) -
515
+ 1 else ux_utils.INDENT_SYMBOL)
516
+ text_suffix = ''
517
+ if show_details:
518
+ if ctx2text is not None:
519
+ text_suffix = (
520
+ f': {ctx2text[context]}' if context in ctx2text else
521
+ (': ' + _red_color('disabled. ') +
522
+ _dim_color('Reason: Not set up. Use `sky ssh up --infra '
523
+ f'{context.lstrip("ssh-")}` to set up.')))
524
+ contexts_formatted.append(
525
+ f'\n {symbol}{cleaned_context}{text_suffix}')
526
+ identity_str = ('SSH Node Pools' if isinstance(cloud_type, sky_clouds.SSH)
527
+ else 'Allowed contexts')
528
+ return f'\n {identity_str}:{"".join(contexts_formatted)}'
344
529
 
345
530
 
346
531
  def _format_enabled_cloud(cloud_name: str,
347
- capabilities: List[sky_cloud.CloudCapability]) -> str:
532
+ capabilities: List[sky_cloud.CloudCapability],
533
+ ctx2text: Optional[Dict[str, str]] = None) -> str:
348
534
  """Format the summary of enabled cloud and its enabled capabilities.
349
535
 
350
536
  Args:
@@ -355,10 +541,11 @@ def _format_enabled_cloud(cloud_name: str,
355
541
  A string of the formatted cloud and capabilities.
356
542
  """
357
543
  cloud_and_capabilities = f'{cloud_name} [{", ".join(capabilities)}]'
544
+ title = _green_color(cloud_and_capabilities)
358
545
 
359
- def _green_color(str_to_format: str) -> str:
360
- return (
361
- f'{colorama.Fore.GREEN}{str_to_format}{colorama.Style.RESET_ALL}')
546
+ if cloud_name in [repr(sky_clouds.Kubernetes()), repr(sky_clouds.SSH())]:
547
+ return (f'{title}' + _format_context_details(
548
+ cloud_name, show_details=False, ctx2text=ctx2text))
362
549
 
363
550
  if cloud_name == repr(sky_clouds.Kubernetes()):
364
551
  # Get enabled contexts for Kubernetes
@@ -386,3 +573,43 @@ def _format_enabled_cloud(cloud_name: str,
386
573
  f' {colorama.Style.DIM}{context_info}'
387
574
  f'{colorama.Style.RESET_ALL}')
388
575
  return _green_color(cloud_and_capabilities)
576
+
577
+
578
+ def _summary_message(
579
+ enabled_clouds: Dict[str, List[sky_cloud.CloudCapability]],
580
+ cloud2ctx2text: Dict[str, Dict[str, str]],
581
+ current_workspace_name: str,
582
+ hide_workspace_str: bool,
583
+ disallowed_cloud_names: List[str],
584
+ ) -> str:
585
+ if not enabled_clouds:
586
+ enabled_clouds_str = '\n No infra to check/enabled.'
587
+ else:
588
+ enabled_clouds_str = '\n ' + '\n '.join([
589
+ _format_enabled_cloud(cloud, capabilities,
590
+ cloud2ctx2text.get(cloud, None))
591
+ for cloud, capabilities in sorted(enabled_clouds.items(),
592
+ key=lambda item: item[0])
593
+ ])
594
+
595
+ workspace_str = f' for workspace: {current_workspace_name!r}'
596
+ if hide_workspace_str:
597
+ workspace_str = ''
598
+
599
+ disallowed_clouds_hint = ''
600
+ if disallowed_cloud_names:
601
+ disable_for_workspace_hint = (
602
+ f' or disabled for this workspace {current_workspace_name!r}')
603
+ if hide_workspace_str:
604
+ disable_for_workspace_hint = ''
605
+ disallowed_clouds_hint = (
606
+ '\nNote: The following clouds were disabled because they were not '
607
+ 'included in allowed_clouds in ~/.sky/config.yaml'
608
+ f'{disable_for_workspace_hint}: '
609
+ f'{", ".join([c for c in disallowed_cloud_names])}')
610
+
611
+ return (f'\n{colorama.Fore.GREEN}{PARTY_POPPER_EMOJI} '
612
+ f'Enabled infra{workspace_str} '
613
+ f'{PARTY_POPPER_EMOJI}'
614
+ f'{colorama.Style.RESET_ALL}{enabled_clouds_str}'
615
+ f'{disallowed_clouds_hint}')