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.
Files changed (97) hide show
  1. sky/__init__.py +2 -2
  2. sky/adaptors/kubernetes.py +7 -0
  3. sky/authentication.py +2 -2
  4. sky/backends/backend_utils.py +31 -3
  5. sky/backends/cloud_vm_ray_backend.py +22 -29
  6. sky/backends/wheel_utils.py +9 -0
  7. sky/check.py +1 -1
  8. sky/cli.py +253 -74
  9. sky/client/cli.py +253 -74
  10. sky/client/common.py +10 -3
  11. sky/client/sdk.py +11 -8
  12. sky/clouds/aws.py +2 -2
  13. sky/clouds/kubernetes.py +0 -8
  14. sky/clouds/oci.py +1 -1
  15. sky/core.py +17 -11
  16. sky/dashboard/out/404.html +1 -0
  17. sky/dashboard/out/_next/static/chunks/236-d437cf66e68a6f64.js +6 -0
  18. sky/dashboard/out/_next/static/chunks/312-c3c8845990db8ffc.js +15 -0
  19. sky/dashboard/out/_next/static/chunks/37-72fdc8f71d6e4784.js +6 -0
  20. sky/dashboard/out/_next/static/chunks/678-206dddca808e6d16.js +59 -0
  21. sky/dashboard/out/_next/static/chunks/845-2ea1cc63ba1f4067.js +1 -0
  22. sky/dashboard/out/_next/static/chunks/979-7cd0778078b9cfad.js +1 -0
  23. sky/dashboard/out/_next/static/chunks/fd9d1056-2821b0f0cabcd8bd.js +1 -0
  24. sky/dashboard/out/_next/static/chunks/framework-87d061ee6ed71b28.js +33 -0
  25. sky/dashboard/out/_next/static/chunks/main-app-241eb28595532291.js +1 -0
  26. sky/dashboard/out/_next/static/chunks/main-e0e2335212e72357.js +1 -0
  27. sky/dashboard/out/_next/static/chunks/pages/_app-3001e84c61acddfb.js +1 -0
  28. sky/dashboard/out/_next/static/chunks/pages/_error-1be831200e60c5c0.js +1 -0
  29. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-b09f7fbf6d5d74f6.js +1 -0
  30. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-b57ec043f09c5813.js +1 -0
  31. sky/dashboard/out/_next/static/chunks/pages/clusters-a93b93e10b8b074e.js +1 -0
  32. sky/dashboard/out/_next/static/chunks/pages/index-f9f039532ca8cbc4.js +1 -0
  33. sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-ef2e0e91a9222cac.js +1 -0
  34. sky/dashboard/out/_next/static/chunks/pages/jobs-a75029b67aab6a2e.js +1 -0
  35. sky/dashboard/out/_next/static/chunks/polyfills-78c92fac7aa8fdd8.js +1 -0
  36. sky/dashboard/out/_next/static/chunks/webpack-830f59b8404e96b8.js +1 -0
  37. sky/dashboard/out/_next/static/css/f3538cd90cfca88c.css +3 -0
  38. sky/dashboard/out/_next/static/mS9YfLA5hhsJMeBj9W8J7/_buildManifest.js +1 -0
  39. sky/dashboard/out/_next/static/mS9YfLA5hhsJMeBj9W8J7/_ssgManifest.js +1 -0
  40. sky/dashboard/out/clusters/[cluster]/[job].html +1 -0
  41. sky/dashboard/out/clusters/[cluster].html +1 -0
  42. sky/dashboard/out/clusters.html +1 -0
  43. sky/dashboard/out/favicon.ico +0 -0
  44. sky/dashboard/out/index.html +1 -0
  45. sky/dashboard/out/jobs/[job].html +1 -0
  46. sky/dashboard/out/jobs.html +1 -0
  47. sky/dashboard/out/skypilot.svg +15 -0
  48. sky/dashboard/out/videos/cursor-small.mp4 +0 -0
  49. sky/data/data_transfer.py +2 -1
  50. sky/data/storage.py +24 -14
  51. sky/exceptions.py +5 -0
  52. sky/jobs/constants.py +8 -1
  53. sky/jobs/server/core.py +12 -8
  54. sky/models.py +28 -0
  55. sky/optimizer.py +7 -9
  56. sky/provision/kubernetes/config.py +1 -1
  57. sky/provision/kubernetes/instance.py +16 -14
  58. sky/provision/kubernetes/network_utils.py +1 -1
  59. sky/provision/kubernetes/utils.py +50 -22
  60. sky/provision/provisioner.py +2 -1
  61. sky/resources.py +56 -2
  62. sky/serve/__init__.py +2 -0
  63. sky/serve/autoscalers.py +6 -2
  64. sky/serve/client/sdk.py +61 -0
  65. sky/serve/constants.py +6 -0
  66. sky/serve/load_balancing_policies.py +0 -4
  67. sky/serve/replica_managers.py +6 -8
  68. sky/serve/serve_state.py +0 -6
  69. sky/serve/serve_utils.py +33 -1
  70. sky/serve/server/core.py +192 -7
  71. sky/serve/server/server.py +28 -0
  72. sky/server/common.py +152 -47
  73. sky/server/constants.py +7 -1
  74. sky/server/requests/executor.py +4 -0
  75. sky/server/requests/payloads.py +12 -15
  76. sky/server/requests/serializers/decoders.py +2 -5
  77. sky/server/requests/serializers/encoders.py +2 -5
  78. sky/server/server.py +44 -1
  79. sky/setup_files/MANIFEST.in +1 -0
  80. sky/setup_files/dependencies.py +1 -0
  81. sky/sky_logging.py +12 -2
  82. sky/skylet/constants.py +5 -7
  83. sky/skylet/job_lib.py +3 -3
  84. sky/skypilot_config.py +225 -84
  85. sky/templates/kubernetes-ray.yml.j2 +7 -3
  86. sky/utils/cli_utils/status_utils.py +12 -5
  87. sky/utils/config_utils.py +39 -15
  88. sky/utils/controller_utils.py +44 -7
  89. sky/utils/kubernetes/generate_kubeconfig.sh +2 -2
  90. sky/utils/kubernetes/gpu_labeler.py +99 -16
  91. sky/utils/schemas.py +24 -0
  92. {skypilot_nightly-1.0.0.dev20250413.dist-info → skypilot_nightly-1.0.0.dev20250421.dist-info}/METADATA +2 -1
  93. {skypilot_nightly-1.0.0.dev20250413.dist-info → skypilot_nightly-1.0.0.dev20250421.dist-info}/RECORD +97 -64
  94. {skypilot_nightly-1.0.0.dev20250413.dist-info → skypilot_nightly-1.0.0.dev20250421.dist-info}/WHEEL +1 -1
  95. {skypilot_nightly-1.0.0.dev20250413.dist-info → skypilot_nightly-1.0.0.dev20250421.dist-info}/entry_points.txt +0 -0
  96. {skypilot_nightly-1.0.0.dev20250413.dist-info → skypilot_nightly-1.0.0.dev20250421.dist-info}/licenses/LICENSE +0 -0
  97. {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 _USER_CONFIG_PATH
4
- (default: ~/.sky/skyconfig.yaml). Caller can then use
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/skyconfig.yaml exists and can be loaded:
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/skyconfig.yaml doesn't exist or failed to be loaded:
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 pprint
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 {ENV_VAR_USER_CONFIG} exists, use its path as the user
81
- # config file. Else, use the default path {_USER_CONFIG_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
- # (Used by users) Environment variables for setting non-default user and
98
- # project config files on clients.
99
- ENV_VAR_USER_CONFIG = f'{constants.SKYPILOT_ENV_VAR_PREFIX}USER_CONFIG'
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 local config files.
103
- _LEGACY_USER_CONFIG_PATH = '~/.sky/config.yaml'
104
- _USER_CONFIG_PATH = '~/.sky/skyconfig.yaml'
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
- If only the legacy user config file exists, return
119
- the legacy user config path.
120
- Otherwise, return the new user config path.
121
- """
122
- user_config_path = os.path.expanduser(_USER_CONFIG_PATH)
123
- legacy_user_config_path = os.path.expanduser(_LEGACY_USER_CONFIG_PATH)
124
- if (os.path.exists(legacy_user_config_path) and
125
- not os.path.exists(user_config_path)):
126
- return _LEGACY_USER_CONFIG_PATH
127
- return _USER_CONFIG_PATH
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], config_path: str) -> None:
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 ({config_path}). See: '
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 _overlay_skypilot_config(
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
- _reload_config_hierarchical()
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.debug(
225
- f'Config loaded from {config_path}:\n{pprint.pformat(config)}')
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 _reload_config_hierarchical() -> None:
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
- # find the user config file
260
- user_config_path = _get_config_file_path(ENV_VAR_USER_CONFIG)
261
- if user_config_path:
262
- logger.debug('using user config file specified by '
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
- # find the project config file
280
- project_config_path = _get_config_file_path(ENV_VAR_PROJECT_CONFIG)
281
- if project_config_path:
282
- logger.debug('using project config file specified by '
283
- f'{ENV_VAR_PROJECT_CONFIG}: {project_config_path}')
284
- project_config_path = os.path.expanduser(project_config_path)
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'using default project config file: {_PROJECT_CONFIG_PATH}')
295
- project_config_path = _PROJECT_CONFIG_PATH
296
- project_config_path = os.path.expanduser(project_config_path)
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
- if os.path.exists(project_config_path):
305
- project_config = _parse_config_file(project_config_path)
306
- _validate_config(project_config, project_config_path)
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 = _overlay_skypilot_config(
381
+ overlaid_client_config = overlay_skypilot_config(
313
382
  original_config=overlaid_client_config, override_configs=override)
314
- logger.debug(f'final config: {overlaid_client_config}')
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 FUSE device manager and
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
- echo "Installing missing packages: $MISSING_PACKAGES";
388
- DEBIAN_FRONTEND=noninteractive $(prefix_cmd) apt-get install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" $MISSING_PACKAGES;
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
- autostop_minutes = constants.CONTROLLER_IDLE_MINUTES_TO_AUTOSTOP
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'{colorama.Style.DIM} (will be autostopped if idle for '
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
- merge_k8s_configs(base_config[key], value,
121
- next_allowed_override_keys,
122
- next_disallowed_override_keys)
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' or 'volumeMounts', we search for
190
- # item with the same name and merge it.
191
- for new_volume in value:
192
- new_volume_name = new_volume.get('name')
193
- if new_volume_name is not None:
194
- destination_volume = next(
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') == new_volume_name), None)
197
- if destination_volume is not None:
198
- merge_k8s_configs(destination_volume, new_volume)
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(new_volume)
224
+ base_config[key].append(override_item)
201
225
  else:
202
226
  base_config[key].extend(value)
203
227
  else: