skypilot-nightly 1.0.0.dev20250413__py3-none-any.whl → 1.0.0.dev20250421__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/adaptors/kubernetes.py +7 -0
- sky/authentication.py +2 -2
- sky/backends/backend_utils.py +31 -3
- sky/backends/cloud_vm_ray_backend.py +22 -29
- sky/backends/wheel_utils.py +9 -0
- sky/check.py +1 -1
- sky/cli.py +253 -74
- sky/client/cli.py +253 -74
- sky/client/common.py +10 -3
- sky/client/sdk.py +11 -8
- sky/clouds/aws.py +2 -2
- sky/clouds/kubernetes.py +0 -8
- sky/clouds/oci.py +1 -1
- sky/core.py +17 -11
- sky/dashboard/out/404.html +1 -0
- sky/dashboard/out/_next/static/chunks/236-d437cf66e68a6f64.js +6 -0
- sky/dashboard/out/_next/static/chunks/312-c3c8845990db8ffc.js +15 -0
- sky/dashboard/out/_next/static/chunks/37-72fdc8f71d6e4784.js +6 -0
- sky/dashboard/out/_next/static/chunks/678-206dddca808e6d16.js +59 -0
- sky/dashboard/out/_next/static/chunks/845-2ea1cc63ba1f4067.js +1 -0
- sky/dashboard/out/_next/static/chunks/979-7cd0778078b9cfad.js +1 -0
- sky/dashboard/out/_next/static/chunks/fd9d1056-2821b0f0cabcd8bd.js +1 -0
- sky/dashboard/out/_next/static/chunks/framework-87d061ee6ed71b28.js +33 -0
- sky/dashboard/out/_next/static/chunks/main-app-241eb28595532291.js +1 -0
- sky/dashboard/out/_next/static/chunks/main-e0e2335212e72357.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/_app-3001e84c61acddfb.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/_error-1be831200e60c5c0.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-b09f7fbf6d5d74f6.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-b57ec043f09c5813.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/clusters-a93b93e10b8b074e.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/index-f9f039532ca8cbc4.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-ef2e0e91a9222cac.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/jobs-a75029b67aab6a2e.js +1 -0
- sky/dashboard/out/_next/static/chunks/polyfills-78c92fac7aa8fdd8.js +1 -0
- sky/dashboard/out/_next/static/chunks/webpack-830f59b8404e96b8.js +1 -0
- sky/dashboard/out/_next/static/css/f3538cd90cfca88c.css +3 -0
- sky/dashboard/out/_next/static/mS9YfLA5hhsJMeBj9W8J7/_buildManifest.js +1 -0
- sky/dashboard/out/_next/static/mS9YfLA5hhsJMeBj9W8J7/_ssgManifest.js +1 -0
- sky/dashboard/out/clusters/[cluster]/[job].html +1 -0
- sky/dashboard/out/clusters/[cluster].html +1 -0
- sky/dashboard/out/clusters.html +1 -0
- sky/dashboard/out/favicon.ico +0 -0
- sky/dashboard/out/index.html +1 -0
- sky/dashboard/out/jobs/[job].html +1 -0
- sky/dashboard/out/jobs.html +1 -0
- sky/dashboard/out/skypilot.svg +15 -0
- sky/dashboard/out/videos/cursor-small.mp4 +0 -0
- sky/data/data_transfer.py +2 -1
- sky/data/storage.py +24 -14
- sky/exceptions.py +5 -0
- sky/jobs/constants.py +8 -1
- sky/jobs/server/core.py +12 -8
- sky/models.py +28 -0
- sky/optimizer.py +7 -9
- sky/provision/kubernetes/config.py +1 -1
- sky/provision/kubernetes/instance.py +16 -14
- sky/provision/kubernetes/network_utils.py +1 -1
- sky/provision/kubernetes/utils.py +50 -22
- sky/provision/provisioner.py +2 -1
- sky/resources.py +56 -2
- sky/serve/__init__.py +2 -0
- sky/serve/autoscalers.py +6 -2
- sky/serve/client/sdk.py +61 -0
- sky/serve/constants.py +6 -0
- sky/serve/load_balancing_policies.py +0 -4
- sky/serve/replica_managers.py +6 -8
- sky/serve/serve_state.py +0 -6
- sky/serve/serve_utils.py +33 -1
- sky/serve/server/core.py +192 -7
- sky/serve/server/server.py +28 -0
- sky/server/common.py +152 -47
- sky/server/constants.py +7 -1
- sky/server/requests/executor.py +4 -0
- sky/server/requests/payloads.py +12 -15
- sky/server/requests/serializers/decoders.py +2 -5
- sky/server/requests/serializers/encoders.py +2 -5
- sky/server/server.py +44 -1
- sky/setup_files/MANIFEST.in +1 -0
- sky/setup_files/dependencies.py +1 -0
- sky/sky_logging.py +12 -2
- sky/skylet/constants.py +5 -7
- sky/skylet/job_lib.py +3 -3
- sky/skypilot_config.py +225 -84
- sky/templates/kubernetes-ray.yml.j2 +7 -3
- sky/utils/cli_utils/status_utils.py +12 -5
- sky/utils/config_utils.py +39 -15
- sky/utils/controller_utils.py +44 -7
- sky/utils/kubernetes/generate_kubeconfig.sh +2 -2
- sky/utils/kubernetes/gpu_labeler.py +99 -16
- sky/utils/schemas.py +24 -0
- {skypilot_nightly-1.0.0.dev20250413.dist-info → skypilot_nightly-1.0.0.dev20250421.dist-info}/METADATA +2 -1
- {skypilot_nightly-1.0.0.dev20250413.dist-info → skypilot_nightly-1.0.0.dev20250421.dist-info}/RECORD +97 -64
- {skypilot_nightly-1.0.0.dev20250413.dist-info → skypilot_nightly-1.0.0.dev20250421.dist-info}/WHEEL +1 -1
- {skypilot_nightly-1.0.0.dev20250413.dist-info → skypilot_nightly-1.0.0.dev20250421.dist-info}/entry_points.txt +0 -0
- {skypilot_nightly-1.0.0.dev20250413.dist-info → skypilot_nightly-1.0.0.dev20250421.dist-info}/licenses/LICENSE +0 -0
- {skypilot_nightly-1.0.0.dev20250413.dist-info → skypilot_nightly-1.0.0.dev20250421.dist-info}/top_level.txt +0 -0
sky/skypilot_config.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
"""Immutable user configurations (EXPERIMENTAL).
|
2
2
|
|
3
|
-
On module import, we attempt to parse the config located at
|
4
|
-
(default: ~/.sky/
|
3
|
+
On module import, we attempt to parse the config located at _GLOBAL_CONFIG_PATH
|
4
|
+
(default: ~/.sky/config.yaml). Caller can then use
|
5
5
|
|
6
6
|
>> skypilot_config.loaded()
|
7
7
|
|
@@ -35,14 +35,14 @@ Consider the following config contents:
|
|
35
35
|
|
36
36
|
then:
|
37
37
|
|
38
|
-
# Assuming ~/.sky/
|
38
|
+
# Assuming ~/.sky/config.yaml exists and can be loaded:
|
39
39
|
skypilot_config.loaded() # ==> True
|
40
40
|
|
41
41
|
skypilot_config.get_nested(('a', 'nested'), None) # ==> 1
|
42
42
|
skypilot_config.get_nested(('a', 'nonexist'), None) # ==> None
|
43
43
|
skypilot_config.get_nested(('a',), None) # ==> {'nested': 1}
|
44
44
|
|
45
|
-
# If ~/.sky/
|
45
|
+
# If ~/.sky/config.yaml doesn't exist or failed to be loaded:
|
46
46
|
skypilot_config.loaded() # ==> False
|
47
47
|
skypilot_config.get_nested(('a', 'nested'), None) # ==> None
|
48
48
|
skypilot_config.get_nested(('a', 'nonexist'), None) # ==> None
|
@@ -50,10 +50,13 @@ then:
|
|
50
50
|
"""
|
51
51
|
import contextlib
|
52
52
|
import copy
|
53
|
+
import json
|
53
54
|
import os
|
54
|
-
import
|
55
|
+
import threading
|
55
56
|
import typing
|
56
|
-
from typing import Any, Dict, Iterator, Optional, Tuple
|
57
|
+
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
58
|
+
|
59
|
+
from omegaconf import OmegaConf
|
57
60
|
|
58
61
|
from sky import exceptions
|
59
62
|
from sky import sky_logging
|
@@ -77,8 +80,8 @@ logger = sky_logging.init_logger(__name__)
|
|
77
80
|
# path as the config file. Do not use any other config files.
|
78
81
|
# This behavior is subject to change and should not be relied on by users.
|
79
82
|
# Else,
|
80
|
-
# (1) If env var {
|
81
|
-
# config file. Else, use the default path {
|
83
|
+
# (1) If env var {ENV_VAR_GLOBAL_CONFIG} exists, use its path as the user
|
84
|
+
# config file. Else, use the default path {_GLOBAL_CONFIG_PATH}.
|
82
85
|
# (2) If env var {ENV_VAR_PROJECT_CONFIG} exists, use its path as the project
|
83
86
|
# config file. Else, use the default path {_PROJECT_CONFIG_PATH}.
|
84
87
|
# (3) Override any config keys in (1) with the ones in (2).
|
@@ -94,37 +97,113 @@ logger = sky_logging.init_logger(__name__)
|
|
94
97
|
# use the same config file.
|
95
98
|
ENV_VAR_SKYPILOT_CONFIG = f'{constants.SKYPILOT_ENV_VAR_PREFIX}CONFIG'
|
96
99
|
|
97
|
-
#
|
98
|
-
#
|
99
|
-
|
100
|
+
# Environment variables for setting non-default server and user
|
101
|
+
# config files.
|
102
|
+
ENV_VAR_GLOBAL_CONFIG = f'{constants.SKYPILOT_ENV_VAR_PREFIX}GLOBAL_CONFIG'
|
103
|
+
# Environment variables for setting non-default project config files.
|
100
104
|
ENV_VAR_PROJECT_CONFIG = f'{constants.SKYPILOT_ENV_VAR_PREFIX}PROJECT_CONFIG'
|
101
105
|
|
102
|
-
# Path to the
|
103
|
-
|
104
|
-
|
105
|
-
_PROJECT_CONFIG_PATH = 'skyconfig.yaml'
|
106
|
+
# Path to the client config files.
|
107
|
+
_GLOBAL_CONFIG_PATH = '~/.sky/config.yaml'
|
108
|
+
_PROJECT_CONFIG_PATH = '.sky.yaml'
|
106
109
|
|
107
110
|
# The loaded config.
|
108
111
|
_dict = config_utils.Config()
|
109
112
|
_loaded_config_path: Optional[str] = None
|
110
113
|
_config_overridden: bool = False
|
114
|
+
_reload_config_lock = threading.Lock()
|
111
115
|
|
112
116
|
|
113
|
-
# This function exists solely to maintain backward compatibility with the
|
114
|
-
# legacy user config file located at ~/.sky/config.yaml.
|
115
117
|
def get_user_config_path() -> str:
|
116
|
-
"""Returns the path to the user config file.
|
118
|
+
"""Returns the path to the user config file."""
|
119
|
+
return _GLOBAL_CONFIG_PATH
|
117
120
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
user_config_path =
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
121
|
+
|
122
|
+
def get_user_config() -> config_utils.Config:
|
123
|
+
"""Returns the user config."""
|
124
|
+
# find the user config file
|
125
|
+
user_config_path = _get_config_file_path(ENV_VAR_GLOBAL_CONFIG)
|
126
|
+
if user_config_path:
|
127
|
+
logger.debug('using user config file specified by '
|
128
|
+
f'{ENV_VAR_GLOBAL_CONFIG}: {user_config_path}')
|
129
|
+
user_config_path = os.path.expanduser(user_config_path)
|
130
|
+
if not os.path.exists(user_config_path):
|
131
|
+
with ux_utils.print_exception_no_traceback():
|
132
|
+
raise FileNotFoundError(
|
133
|
+
'Config file specified by env var '
|
134
|
+
f'{ENV_VAR_GLOBAL_CONFIG} ({user_config_path!r}) '
|
135
|
+
'does not exist. Please double check the path or unset the '
|
136
|
+
f'env var: unset {ENV_VAR_GLOBAL_CONFIG}')
|
137
|
+
else:
|
138
|
+
user_config_path = get_user_config_path()
|
139
|
+
logger.debug(f'using default user config file: {user_config_path}')
|
140
|
+
user_config_path = os.path.expanduser(user_config_path)
|
141
|
+
|
142
|
+
# load the user config file
|
143
|
+
if os.path.exists(user_config_path):
|
144
|
+
user_config = _parse_config_file(user_config_path)
|
145
|
+
_validate_config(user_config, user_config_path)
|
146
|
+
else:
|
147
|
+
user_config = config_utils.Config()
|
148
|
+
return user_config
|
149
|
+
|
150
|
+
|
151
|
+
def _get_project_config() -> config_utils.Config:
|
152
|
+
# find the project config file
|
153
|
+
project_config_path = _get_config_file_path(ENV_VAR_PROJECT_CONFIG)
|
154
|
+
if project_config_path:
|
155
|
+
logger.debug('using project config file specified by '
|
156
|
+
f'{ENV_VAR_PROJECT_CONFIG}: {project_config_path}')
|
157
|
+
project_config_path = os.path.expanduser(project_config_path)
|
158
|
+
if not os.path.exists(project_config_path):
|
159
|
+
with ux_utils.print_exception_no_traceback():
|
160
|
+
raise FileNotFoundError(
|
161
|
+
'Config file specified by env var '
|
162
|
+
f'{ENV_VAR_PROJECT_CONFIG} ({project_config_path!r}) '
|
163
|
+
'does not exist. Please double check the path or unset the '
|
164
|
+
f'env var: unset {ENV_VAR_PROJECT_CONFIG}')
|
165
|
+
else:
|
166
|
+
logger.debug(
|
167
|
+
f'using default project config file: {_PROJECT_CONFIG_PATH}')
|
168
|
+
project_config_path = _PROJECT_CONFIG_PATH
|
169
|
+
project_config_path = os.path.expanduser(project_config_path)
|
170
|
+
|
171
|
+
# load the project config file
|
172
|
+
if os.path.exists(project_config_path):
|
173
|
+
project_config = _parse_config_file(project_config_path)
|
174
|
+
_validate_config(project_config, project_config_path)
|
175
|
+
else:
|
176
|
+
project_config = config_utils.Config()
|
177
|
+
return project_config
|
178
|
+
|
179
|
+
|
180
|
+
def get_server_config() -> config_utils.Config:
|
181
|
+
"""Returns the server config."""
|
182
|
+
# find the server config file
|
183
|
+
server_config_path = _get_config_file_path(ENV_VAR_GLOBAL_CONFIG)
|
184
|
+
if server_config_path:
|
185
|
+
logger.debug('using server config file specified by '
|
186
|
+
f'{ENV_VAR_GLOBAL_CONFIG}: {server_config_path}')
|
187
|
+
server_config_path = os.path.expanduser(server_config_path)
|
188
|
+
if not os.path.exists(server_config_path):
|
189
|
+
with ux_utils.print_exception_no_traceback():
|
190
|
+
raise FileNotFoundError(
|
191
|
+
'Config file specified by env var '
|
192
|
+
f'{ENV_VAR_GLOBAL_CONFIG} ({server_config_path!r}) '
|
193
|
+
'does not exist. Please double check the path or unset the '
|
194
|
+
f'env var: unset {ENV_VAR_GLOBAL_CONFIG}')
|
195
|
+
else:
|
196
|
+
server_config_path = _GLOBAL_CONFIG_PATH
|
197
|
+
logger.debug(f'using default server config file: {server_config_path}')
|
198
|
+
server_config_path = os.path.expanduser(server_config_path)
|
199
|
+
|
200
|
+
# load the server config file
|
201
|
+
if os.path.exists(server_config_path):
|
202
|
+
server_config = _parse_config_file(server_config_path)
|
203
|
+
_validate_config(server_config, server_config_path)
|
204
|
+
else:
|
205
|
+
server_config = config_utils.Config()
|
206
|
+
return server_config
|
128
207
|
|
129
208
|
|
130
209
|
def get_nested(keys: Tuple[str, ...],
|
@@ -177,18 +256,18 @@ def _get_config_file_path(envvar: str) -> Optional[str]:
|
|
177
256
|
return None
|
178
257
|
|
179
258
|
|
180
|
-
def _validate_config(config: Dict[str, Any],
|
259
|
+
def _validate_config(config: Dict[str, Any], config_source: str) -> None:
|
181
260
|
"""Validates the config."""
|
182
261
|
common_utils.validate_schema(
|
183
262
|
config,
|
184
263
|
schemas.get_config_schema(),
|
185
|
-
f'Invalid config YAML ({
|
264
|
+
f'Invalid config YAML from ({config_source}). See: '
|
186
265
|
'https://docs.skypilot.co/en/latest/reference/config.html. ' # pylint: disable=line-too-long
|
187
266
|
'Error: ',
|
188
267
|
skip_none=False)
|
189
268
|
|
190
269
|
|
191
|
-
def
|
270
|
+
def overlay_skypilot_config(
|
192
271
|
original_config: Optional[config_utils.Config],
|
193
272
|
override_configs: Optional[config_utils.Config]) -> config_utils.Config:
|
194
273
|
"""Overlays the override configs on the original configs."""
|
@@ -202,6 +281,12 @@ def _overlay_skypilot_config(
|
|
202
281
|
return config
|
203
282
|
|
204
283
|
|
284
|
+
def safe_reload_config() -> None:
|
285
|
+
"""Reloads the config, safe to be called concurrently."""
|
286
|
+
with _reload_config_lock:
|
287
|
+
_reload_config()
|
288
|
+
|
289
|
+
|
205
290
|
def _reload_config() -> None:
|
206
291
|
internal_config_path = os.environ.get(ENV_VAR_SKYPILOT_CONFIG)
|
207
292
|
if internal_config_path is not None:
|
@@ -213,7 +298,10 @@ def _reload_config() -> None:
|
|
213
298
|
_reload_config_from_internal_file(internal_config_path)
|
214
299
|
return
|
215
300
|
|
216
|
-
|
301
|
+
if os.environ.get(constants.ENV_VAR_IS_SKYPILOT_SERVER) is not None:
|
302
|
+
_reload_config_as_server()
|
303
|
+
else:
|
304
|
+
_reload_config_as_client()
|
217
305
|
|
218
306
|
|
219
307
|
def _parse_config_file(config_path: str) -> config_utils.Config:
|
@@ -221,8 +309,9 @@ def _parse_config_file(config_path: str) -> config_utils.Config:
|
|
221
309
|
try:
|
222
310
|
config_dict = common_utils.read_yaml(config_path)
|
223
311
|
config = config_utils.Config.from_dict(config_dict)
|
224
|
-
logger.
|
225
|
-
f'Config loaded from {config_path}:\n
|
312
|
+
if sky_logging.logging_enabled(logger, sky_logging.DEBUG):
|
313
|
+
logger.debug(f'Config loaded from {config_path}:\n'
|
314
|
+
f'{common_utils.dump_yaml_str(dict(config))}')
|
226
315
|
except yaml.YAMLError as e:
|
227
316
|
logger.error(f'Error in loading config file ({config_path}):', e)
|
228
317
|
if config:
|
@@ -251,67 +340,50 @@ def _reload_config_from_internal_file(internal_config_path: str) -> None:
|
|
251
340
|
_loaded_config_path = config_path
|
252
341
|
|
253
342
|
|
254
|
-
def
|
343
|
+
def _reload_config_as_server() -> None:
|
255
344
|
global _dict
|
256
345
|
# Reset the global variables, to avoid using stale values.
|
257
346
|
_dict = config_utils.Config()
|
258
347
|
|
259
|
-
|
260
|
-
|
261
|
-
if
|
262
|
-
|
263
|
-
f'{ENV_VAR_USER_CONFIG}: {user_config_path}')
|
264
|
-
user_config_path = os.path.expanduser(user_config_path)
|
265
|
-
if not os.path.exists(user_config_path):
|
266
|
-
with ux_utils.print_exception_no_traceback():
|
267
|
-
raise FileNotFoundError(
|
268
|
-
'Config file specified by env var '
|
269
|
-
f'{ENV_VAR_USER_CONFIG} ({user_config_path!r}) '
|
270
|
-
'does not exist. Please double check the path or unset the '
|
271
|
-
f'env var: unset {ENV_VAR_USER_CONFIG}')
|
272
|
-
else:
|
273
|
-
user_config_path = get_user_config_path()
|
274
|
-
logger.debug(f'using default user config file: {user_config_path}')
|
275
|
-
user_config_path = os.path.expanduser(user_config_path)
|
276
|
-
|
277
|
-
overrides = []
|
348
|
+
overrides: List[config_utils.Config] = []
|
349
|
+
server_config = get_server_config()
|
350
|
+
if server_config:
|
351
|
+
overrides.append(server_config)
|
278
352
|
|
279
|
-
#
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
if not os.path.exists(project_config_path):
|
286
|
-
with ux_utils.print_exception_no_traceback():
|
287
|
-
raise FileNotFoundError(
|
288
|
-
'Config file specified by env var '
|
289
|
-
f'{ENV_VAR_PROJECT_CONFIG} ({project_config_path!r}) '
|
290
|
-
'does not exist. Please double check the path or unset the '
|
291
|
-
f'env var: unset {ENV_VAR_PROJECT_CONFIG}')
|
292
|
-
else:
|
353
|
+
# layer the configs on top of each other based on priority
|
354
|
+
overlaid_server_config: config_utils.Config = config_utils.Config()
|
355
|
+
for override in overrides:
|
356
|
+
overlaid_server_config = overlay_skypilot_config(
|
357
|
+
original_config=overlaid_server_config, override_configs=override)
|
358
|
+
if sky_logging.logging_enabled(logger, sky_logging.DEBUG):
|
293
359
|
logger.debug(
|
294
|
-
f'
|
295
|
-
|
296
|
-
|
360
|
+
f'server config: \n'
|
361
|
+
f'{common_utils.dump_yaml_str(dict(overlaid_server_config))}')
|
362
|
+
_dict = overlaid_server_config
|
297
363
|
|
298
|
-
# load the user config file
|
299
|
-
if os.path.exists(user_config_path):
|
300
|
-
user_config = _parse_config_file(user_config_path)
|
301
|
-
_validate_config(user_config, user_config_path)
|
302
|
-
overrides.append(user_config)
|
303
364
|
|
304
|
-
|
305
|
-
|
306
|
-
|
365
|
+
def _reload_config_as_client() -> None:
|
366
|
+
global _dict
|
367
|
+
# Reset the global variables, to avoid using stale values.
|
368
|
+
_dict = config_utils.Config()
|
369
|
+
|
370
|
+
overrides: List[config_utils.Config] = []
|
371
|
+
user_config = get_user_config()
|
372
|
+
if user_config:
|
373
|
+
overrides.append(user_config)
|
374
|
+
project_config = _get_project_config()
|
375
|
+
if project_config:
|
307
376
|
overrides.append(project_config)
|
308
377
|
|
309
378
|
# layer the configs on top of each other based on priority
|
310
379
|
overlaid_client_config: config_utils.Config = config_utils.Config()
|
311
380
|
for override in overrides:
|
312
|
-
overlaid_client_config =
|
381
|
+
overlaid_client_config = overlay_skypilot_config(
|
313
382
|
original_config=overlaid_client_config, override_configs=override)
|
314
|
-
|
383
|
+
if sky_logging.logging_enabled(logger, sky_logging.DEBUG):
|
384
|
+
logger.debug(
|
385
|
+
f'client config (before task and CLI overrides): \n'
|
386
|
+
f'{common_utils.dump_yaml_str(dict(overlaid_client_config))}')
|
315
387
|
_dict = overlaid_client_config
|
316
388
|
|
317
389
|
|
@@ -323,7 +395,7 @@ def loaded_config_path() -> Optional[str]:
|
|
323
395
|
return _loaded_config_path
|
324
396
|
|
325
397
|
|
326
|
-
# Load on import.
|
398
|
+
# Load on import, synchronization is guaranteed by python interpreter.
|
327
399
|
_reload_config()
|
328
400
|
|
329
401
|
|
@@ -344,10 +416,26 @@ def override_skypilot_config(
|
|
344
416
|
yield
|
345
417
|
return
|
346
418
|
original_config = _dict
|
419
|
+
override_configs = config_utils.Config(override_configs)
|
420
|
+
disallowed_diff_keys = []
|
421
|
+
for key in constants.SKIPPED_CLIENT_OVERRIDE_KEYS:
|
422
|
+
value = override_configs.pop_nested(key, default_value=None)
|
423
|
+
if (value is not None and
|
424
|
+
value != original_config.get_nested(key, default_value=None)):
|
425
|
+
disallowed_diff_keys.append('.'.join(key))
|
426
|
+
# Only warn if there is a diff in disallowed override keys, as the client
|
427
|
+
# use the same config file when connecting to a local server.
|
428
|
+
if disallowed_diff_keys:
|
429
|
+
logger.warning(
|
430
|
+
f'The following keys ({json.dumps(disallowed_diff_keys)}) have '
|
431
|
+
'different values in the client SkyPilot config with the server '
|
432
|
+
'and will be ignored. Remove these keys to disable this warning. '
|
433
|
+
'If you want to specify it, please modify it on server side or '
|
434
|
+
'contact your administrator.')
|
347
435
|
config = _dict.get_nested(
|
348
436
|
keys=tuple(),
|
349
437
|
default_value=None,
|
350
|
-
override_configs=override_configs,
|
438
|
+
override_configs=dict(override_configs),
|
351
439
|
allowed_override_keys=None,
|
352
440
|
disallowed_override_keys=constants.SKIPPED_CLIENT_OVERRIDE_KEYS)
|
353
441
|
try:
|
@@ -369,8 +457,61 @@ def override_skypilot_config(
|
|
369
457
|
'=== SkyPilot config on API server ===\n'
|
370
458
|
f'{common_utils.dump_yaml_str(dict(original_config))}\n'
|
371
459
|
'=== Your local SkyPilot config ===\n'
|
372
|
-
f'{common_utils.dump_yaml_str(override_configs)}\n'
|
460
|
+
f'{common_utils.dump_yaml_str(dict(override_configs))}\n'
|
373
461
|
f'Details: {e}') from e
|
374
462
|
finally:
|
375
463
|
_dict = original_config
|
376
464
|
_config_overridden = False
|
465
|
+
|
466
|
+
|
467
|
+
def _compose_cli_config(cli_config: Optional[str],) -> config_utils.Config:
|
468
|
+
"""Composes the skypilot CLI config.
|
469
|
+
CLI config can either be:
|
470
|
+
- A path to a config file
|
471
|
+
- A comma-separated list of key-value pairs
|
472
|
+
"""
|
473
|
+
|
474
|
+
if not cli_config:
|
475
|
+
return config_utils.Config()
|
476
|
+
|
477
|
+
config_source = 'CLI'
|
478
|
+
maybe_config_path = os.path.expanduser(cli_config)
|
479
|
+
try:
|
480
|
+
if os.path.isfile(maybe_config_path):
|
481
|
+
config_source = maybe_config_path
|
482
|
+
# cli_config is a path to a config file
|
483
|
+
parsed_config = OmegaConf.to_object(
|
484
|
+
OmegaConf.load(maybe_config_path))
|
485
|
+
else: # cli_config is a comma-separated list of key-value pairs
|
486
|
+
variables: List[str] = []
|
487
|
+
variables = cli_config.split(',')
|
488
|
+
parsed_config = OmegaConf.to_object(
|
489
|
+
OmegaConf.from_dotlist(variables))
|
490
|
+
_validate_config(parsed_config, config_source)
|
491
|
+
except ValueError as e:
|
492
|
+
raise ValueError(f'Invalid config override: {cli_config}. '
|
493
|
+
f'Check if config file exists or if the dotlist '
|
494
|
+
f'is formatted as: key1=value1,key2=value2') from e
|
495
|
+
logger.debug('CLI overrides config syntax check passed.')
|
496
|
+
|
497
|
+
return parsed_config
|
498
|
+
|
499
|
+
|
500
|
+
def apply_cli_config(cli_config: Optional[str]) -> Dict[str, Any]:
|
501
|
+
"""Applies the CLI provided config.
|
502
|
+
SAFETY:
|
503
|
+
This function directly modifies the global _dict variable.
|
504
|
+
This is considered fine in CLI context because the program will exit after
|
505
|
+
a single CLI command is executed.
|
506
|
+
Args:
|
507
|
+
cli_config: A path to a config file or a comma-separated
|
508
|
+
list of key-value pairs.
|
509
|
+
"""
|
510
|
+
global _dict
|
511
|
+
parsed_config = _compose_cli_config(cli_config)
|
512
|
+
if sky_logging.logging_enabled(logger, sky_logging.DEBUG):
|
513
|
+
logger.debug(f'applying following CLI overrides: \n'
|
514
|
+
f'{common_utils.dump_yaml_str(dict(parsed_config))}')
|
515
|
+
_dict = overlay_skypilot_config(original_config=_dict,
|
516
|
+
override_configs=parsed_config)
|
517
|
+
return parsed_config
|
@@ -96,7 +96,7 @@ provider:
|
|
96
96
|
name: skypilot-service-account-role
|
97
97
|
apiGroup: rbac.authorization.k8s.io
|
98
98
|
|
99
|
-
# Role for the skypilot-system namespace to create
|
99
|
+
# Role for the skypilot-system namespace to create fusermount-server and
|
100
100
|
# any other system components.
|
101
101
|
autoscaler_skypilot_system_role:
|
102
102
|
kind: Role
|
@@ -384,8 +384,12 @@ available_node_types:
|
|
384
384
|
set +e
|
385
385
|
|
386
386
|
if [ ! -z "$MISSING_PACKAGES" ]; then
|
387
|
-
|
388
|
-
|
387
|
+
# Install missing packages individually to avoid failure installation breaks the whole install process,
|
388
|
+
# e.g. fuse3 is not available on some distributions.
|
389
|
+
echo "Installing missing packages individually: $MISSING_PACKAGES";
|
390
|
+
for pkg in $MISSING_PACKAGES; do
|
391
|
+
DEBIAN_FRONTEND=noninteractive $(prefix_cmd) apt-get install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" $pkg || echo "Optional package $pkg installation failed, skip.";
|
392
|
+
done
|
389
393
|
fi;
|
390
394
|
|
391
395
|
{% if k8s_fuse_device_required %}
|
@@ -6,8 +6,8 @@ import click
|
|
6
6
|
import colorama
|
7
7
|
|
8
8
|
from sky import backends
|
9
|
-
from sky.skylet import constants
|
10
9
|
from sky.utils import common_utils
|
10
|
+
from sky.utils import controller_utils
|
11
11
|
from sky.utils import log_utils
|
12
12
|
from sky.utils import resources_utils
|
13
13
|
from sky.utils import status_lib
|
@@ -198,12 +198,19 @@ def show_cost_report_table(cluster_records: List[_ClusterCostReportRecord],
|
|
198
198
|
|
199
199
|
if cluster_records:
|
200
200
|
if controller_name is not None:
|
201
|
-
|
201
|
+
controller = controller_utils.Controllers.from_name(controller_name)
|
202
|
+
if controller is None:
|
203
|
+
raise ValueError(f'Controller {controller_name} not found.')
|
204
|
+
autostop_minutes, _ = (
|
205
|
+
controller_utils.get_controller_autostop_config(
|
206
|
+
controller=controller))
|
207
|
+
if autostop_minutes is not None:
|
208
|
+
autostop_str = (f'{colorama.Style.DIM} (will be autostopped if '
|
209
|
+
f'idle for {autostop_minutes}min)'
|
210
|
+
f'{colorama.Style.RESET_ALL}')
|
202
211
|
click.echo(f'\n{colorama.Fore.CYAN}{colorama.Style.BRIGHT}'
|
203
212
|
f'{controller_name}{colorama.Style.RESET_ALL}'
|
204
|
-
f'{
|
205
|
-
f'{autostop_minutes}min)'
|
206
|
-
f'{colorama.Style.RESET_ALL}')
|
213
|
+
f'{autostop_str}')
|
207
214
|
else:
|
208
215
|
click.echo(f'{colorama.Fore.CYAN}{colorama.Style.BRIGHT}Clusters'
|
209
216
|
f'{colorama.Style.RESET_ALL}')
|
sky/utils/config_utils.py
CHANGED
@@ -112,14 +112,39 @@ def _recursive_update(
|
|
112
112
|
disallowed_override_keys: Optional[List[Tuple[str,
|
113
113
|
...]]] = None) -> Config:
|
114
114
|
"""Recursively updates base configuration with override configuration"""
|
115
|
+
|
116
|
+
def _update_k8s_config(
|
117
|
+
base_config: Config,
|
118
|
+
override_config: Dict[str, Any],
|
119
|
+
allowed_override_keys: Optional[List[Tuple[str, ...]]] = None,
|
120
|
+
disallowed_override_keys: Optional[List[Tuple[str,
|
121
|
+
...]]] = None) -> Config:
|
122
|
+
"""Updates the top-level k8s config with the override config."""
|
123
|
+
for key, value in override_config.items():
|
124
|
+
(next_allowed_override_keys, next_disallowed_override_keys
|
125
|
+
) = _check_allowed_and_disallowed_override_keys(
|
126
|
+
key, allowed_override_keys, disallowed_override_keys)
|
127
|
+
if key in ['custom_metadata', 'pod_config'] and key in base_config:
|
128
|
+
merge_k8s_configs(base_config[key], value,
|
129
|
+
next_allowed_override_keys,
|
130
|
+
next_disallowed_override_keys)
|
131
|
+
elif (isinstance(value, dict) and key in base_config and
|
132
|
+
isinstance(base_config[key], dict)):
|
133
|
+
_recursive_update(base_config[key], value,
|
134
|
+
next_allowed_override_keys,
|
135
|
+
next_disallowed_override_keys)
|
136
|
+
else:
|
137
|
+
base_config[key] = value
|
138
|
+
return base_config
|
139
|
+
|
115
140
|
for key, value in override_config.items():
|
116
141
|
(next_allowed_override_keys, next_disallowed_override_keys
|
117
142
|
) = _check_allowed_and_disallowed_override_keys(
|
118
143
|
key, allowed_override_keys, disallowed_override_keys)
|
119
144
|
if key == 'kubernetes' and key in base_config:
|
120
|
-
|
121
|
-
|
122
|
-
|
145
|
+
_update_k8s_config(base_config[key], value,
|
146
|
+
next_allowed_override_keys,
|
147
|
+
next_disallowed_override_keys)
|
123
148
|
elif (isinstance(value, dict) and key in base_config and
|
124
149
|
isinstance(base_config[key], dict)):
|
125
150
|
_recursive_update(base_config[key], value,
|
@@ -146,7 +171,6 @@ def _get_nested(configs: Optional[Dict[str, Any]],
|
|
146
171
|
curr = value
|
147
172
|
else:
|
148
173
|
return default_value
|
149
|
-
logger.debug(f'Config: {".".join(keys)} -> {curr}')
|
150
174
|
return curr
|
151
175
|
|
152
176
|
|
@@ -185,19 +209,19 @@ def merge_k8s_configs(
|
|
185
209
|
merge_k8s_configs(base_config[key][0], value[0],
|
186
210
|
next_allowed_override_keys,
|
187
211
|
next_disallowed_override_keys)
|
188
|
-
elif key in ['volumes', 'volumeMounts']:
|
189
|
-
# If the key is 'volumes'
|
190
|
-
# item with the same name and merge it.
|
191
|
-
for
|
192
|
-
|
193
|
-
if
|
194
|
-
|
212
|
+
elif key in ['volumes', 'volumeMounts', 'initContainers']:
|
213
|
+
# If the key is 'volumes', 'volumeMounts', or 'initContainers',
|
214
|
+
# we search for item with the same name and merge it.
|
215
|
+
for override_item in value:
|
216
|
+
override_item_name = override_item.get('name')
|
217
|
+
if override_item_name is not None:
|
218
|
+
existing_base_item = next(
|
195
219
|
(v for v in base_config[key]
|
196
|
-
if v.get('name') ==
|
197
|
-
if
|
198
|
-
merge_k8s_configs(
|
220
|
+
if v.get('name') == override_item_name), None)
|
221
|
+
if existing_base_item is not None:
|
222
|
+
merge_k8s_configs(existing_base_item, override_item)
|
199
223
|
else:
|
200
|
-
base_config[key].append(
|
224
|
+
base_config[key].append(override_item)
|
201
225
|
else:
|
202
226
|
base_config[key].extend(value)
|
203
227
|
else:
|