skypilot-nightly 1.0.0.dev20240923__py3-none-any.whl → 1.0.0.dev20240925__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 CHANGED
@@ -5,7 +5,7 @@ from typing import Optional
5
5
  import urllib.request
6
6
 
7
7
  # Replaced with the current commit when building the wheels.
8
- _SKYPILOT_COMMIT_SHA = 'd602225c897f10ca67a2d5b5db21982c0dc8c1ec'
8
+ _SKYPILOT_COMMIT_SHA = 'e13c39104cc3fca974e2afa207bcca24817f4e17'
9
9
 
10
10
 
11
11
  def _get_git_commit():
@@ -35,7 +35,7 @@ def _get_git_commit():
35
35
 
36
36
 
37
37
  __commit__ = _get_git_commit()
38
- __version__ = '1.0.0.dev20240923'
38
+ __version__ = '1.0.0.dev20240925'
39
39
  __root_dir__ = os.path.dirname(os.path.abspath(__file__))
40
40
 
41
41
 
@@ -82,6 +82,9 @@ _set_http_proxy_env_vars()
82
82
  from sky import backends
83
83
  from sky import benchmark
84
84
  from sky import clouds
85
+ from sky.admin_policy import AdminPolicy
86
+ from sky.admin_policy import MutatedUserRequest
87
+ from sky.admin_policy import UserRequest
85
88
  from sky.clouds.service_catalog import list_accelerators
86
89
  from sky.core import autostop
87
90
  from sky.core import cancel
@@ -112,6 +115,7 @@ from sky.optimizer import Optimizer
112
115
  from sky.optimizer import OptimizeTarget
113
116
  from sky.resources import Resources
114
117
  from sky.skylet.job_lib import JobStatus
118
+ from sky.skypilot_config import Config
115
119
  from sky.status_lib import ClusterStatus
116
120
  from sky.task import Task
117
121
 
@@ -185,4 +189,9 @@ __all__ = [
185
189
  # core APIs Storage Management
186
190
  'storage_ls',
187
191
  'storage_delete',
192
+ # Admin Policy
193
+ 'UserRequest',
194
+ 'MutatedUserRequest',
195
+ 'AdminPolicy',
196
+ 'Config',
188
197
  ]
sky/admin_policy.py ADDED
@@ -0,0 +1,101 @@
1
+ """Interface for admin-defined policy for user requests."""
2
+ import abc
3
+ import dataclasses
4
+ import typing
5
+ from typing import Optional
6
+
7
+ if typing.TYPE_CHECKING:
8
+ import sky
9
+
10
+
11
+ @dataclasses.dataclass
12
+ class RequestOptions:
13
+ """Request options for admin policy.
14
+
15
+ Args:
16
+ cluster_name: Name of the cluster to create/reuse. It is None if not
17
+ specified by the user.
18
+ idle_minutes_to_autostop: Autostop setting requested by a user. The
19
+ cluster will be set to autostop after this many minutes of idleness.
20
+ down: If true, use autodown rather than autostop.
21
+ dryrun: Is the request a dryrun?
22
+ """
23
+ cluster_name: Optional[str]
24
+ idle_minutes_to_autostop: Optional[int]
25
+ down: bool
26
+ dryrun: bool
27
+
28
+
29
+ @dataclasses.dataclass
30
+ class UserRequest:
31
+ """A user request.
32
+
33
+ A "user request" is defined as a `sky launch / exec` command or its API
34
+ equivalent.
35
+
36
+ `sky jobs launch / serve up` involves multiple launch requests, including
37
+ the launch of controller and clusters for a job (which can have multiple
38
+ tasks if it is a pipeline) or service replicas. Each launch is a separate
39
+ request.
40
+
41
+ This class wraps the underlying task, the global skypilot config used to run
42
+ a task, and the request options.
43
+
44
+ Args:
45
+ task: User specified task.
46
+ skypilot_config: Global skypilot config to be used in this request.
47
+ request_options: Request options. It is None for jobs and services.
48
+ """
49
+ task: 'sky.Task'
50
+ skypilot_config: 'sky.Config'
51
+ request_options: Optional['RequestOptions'] = None
52
+
53
+
54
+ @dataclasses.dataclass
55
+ class MutatedUserRequest:
56
+ task: 'sky.Task'
57
+ skypilot_config: 'sky.Config'
58
+
59
+
60
+ # pylint: disable=line-too-long
61
+ class AdminPolicy:
62
+ """Abstract interface of an admin-defined policy for all user requests.
63
+
64
+ Admins can implement a subclass of AdminPolicy with the following signature:
65
+
66
+ import sky
67
+
68
+ class SkyPilotPolicyV1(sky.AdminPolicy):
69
+ def validate_and_mutate(user_request: UserRequest) -> MutatedUserRequest:
70
+ ...
71
+ return MutatedUserRequest(task=..., skypilot_config=...)
72
+
73
+ The policy can mutate both task and skypilot_config. Admins then distribute
74
+ a simple module that contains this implementation, installable in a way
75
+ that it can be imported by users from the same Python environment where
76
+ SkyPilot is running.
77
+
78
+ Users can register a subclass of AdminPolicy in the SkyPilot config file
79
+ under the key 'admin_policy', e.g.
80
+
81
+ admin_policy: my_package.SkyPilotPolicyV1
82
+ """
83
+
84
+ @classmethod
85
+ @abc.abstractmethod
86
+ def validate_and_mutate(cls,
87
+ user_request: UserRequest) -> MutatedUserRequest:
88
+ """Validates and mutates the user request and returns mutated request.
89
+
90
+ Args:
91
+ user_request: The user request to validate and mutate.
92
+ UserRequest contains (sky.Task, sky.Config)
93
+
94
+ Returns:
95
+ MutatedUserRequest: The mutated user request.
96
+
97
+ Raises:
98
+ Exception to throw if the user request failed the validation.
99
+ """
100
+ raise NotImplementedError(
101
+ 'Your policy must implement validate_and_mutate')
@@ -4147,11 +4147,21 @@ class CloudVmRayBackend(backends.Backend['CloudVmRayResourceHandle']):
4147
4147
  idle_minutes_to_autostop >= 0):
4148
4148
  # We should hit this code path only for the controllers on
4149
4149
  # Kubernetes and RunPod clusters.
4150
- assert (controller_utils.Controllers.from_name(
4151
- handle.cluster_name) is not None), handle.cluster_name
4152
- logger.info('Auto-stop is not supported for Kubernetes '
4153
- 'and RunPod clusters. Skipping.')
4154
- return
4150
+ controller = controller_utils.Controllers.from_name(
4151
+ handle.cluster_name)
4152
+ assert (controller is not None), handle.cluster_name
4153
+ if (controller
4154
+ == controller_utils.Controllers.SKY_SERVE_CONTROLLER and
4155
+ isinstance(handle.launched_resources.cloud,
4156
+ clouds.Kubernetes)):
4157
+ # For SkyServe controllers on Kubernetes: override autostop
4158
+ # behavior to force autodown (instead of no-op)
4159
+ # to avoid dangling controllers.
4160
+ down = True
4161
+ else:
4162
+ logger.info('Auto-stop is not supported for Kubernetes '
4163
+ 'and RunPod clusters. Skipping.')
4164
+ return
4155
4165
 
4156
4166
  # Check if we're stopping spot
4157
4167
  assert (handle.launched_resources is not None and
sky/dag.py CHANGED
@@ -1,8 +1,12 @@
1
1
  """DAGs: user applications to be run."""
2
2
  import pprint
3
3
  import threading
4
+ import typing
4
5
  from typing import List, Optional
5
6
 
7
+ if typing.TYPE_CHECKING:
8
+ from sky import task
9
+
6
10
 
7
11
  class Dag:
8
12
  """Dag: a user application, represented as a DAG of Tasks.
@@ -13,37 +17,37 @@ class Dag:
13
17
  >>> task = sky.Task(...)
14
18
  """
15
19
 
16
- def __init__(self):
17
- self.tasks = []
20
+ def __init__(self) -> None:
21
+ self.tasks: List['task.Task'] = []
18
22
  import networkx as nx # pylint: disable=import-outside-toplevel
19
23
 
20
24
  self.graph = nx.DiGraph()
21
- self.name = None
25
+ self.name: Optional[str] = None
22
26
 
23
- def add(self, task):
27
+ def add(self, task: 'task.Task') -> None:
24
28
  self.graph.add_node(task)
25
29
  self.tasks.append(task)
26
30
 
27
- def remove(self, task):
31
+ def remove(self, task: 'task.Task') -> None:
28
32
  self.tasks.remove(task)
29
33
  self.graph.remove_node(task)
30
34
 
31
- def add_edge(self, op1, op2):
35
+ def add_edge(self, op1: 'task.Task', op2: 'task.Task') -> None:
32
36
  assert op1 in self.graph.nodes
33
37
  assert op2 in self.graph.nodes
34
38
  self.graph.add_edge(op1, op2)
35
39
 
36
- def __len__(self):
40
+ def __len__(self) -> int:
37
41
  return len(self.tasks)
38
42
 
39
- def __enter__(self):
43
+ def __enter__(self) -> 'Dag':
40
44
  push_dag(self)
41
45
  return self
42
46
 
43
- def __exit__(self, exc_type, exc_value, traceback):
47
+ def __exit__(self, exc_type, exc_value, traceback) -> None:
44
48
  pop_dag()
45
49
 
46
- def __repr__(self):
50
+ def __repr__(self) -> str:
47
51
  pformat = pprint.pformat(self.tasks)
48
52
  return f'DAG:\n{pformat}'
49
53
 
@@ -70,15 +74,15 @@ class Dag:
70
74
 
71
75
  class _DagContext(threading.local):
72
76
  """A thread-local stack of Dags."""
73
- _current_dag = None
77
+ _current_dag: Optional[Dag] = None
74
78
  _previous_dags: List[Dag] = []
75
79
 
76
- def push_dag(self, dag):
80
+ def push_dag(self, dag: Dag):
77
81
  if self._current_dag is not None:
78
82
  self._previous_dags.append(self._current_dag)
79
83
  self._current_dag = dag
80
84
 
81
- def pop_dag(self):
85
+ def pop_dag(self) -> Optional[Dag]:
82
86
  old_dag = self._current_dag
83
87
  if self._previous_dags:
84
88
  self._current_dag = self._previous_dags.pop()
sky/exceptions.py CHANGED
@@ -286,3 +286,8 @@ class ServeUserTerminatedError(Exception):
286
286
 
287
287
  class PortDoesNotExistError(Exception):
288
288
  """Raised when the port does not exist."""
289
+
290
+
291
+ class UserRequestRejectedByPolicy(Exception):
292
+ """Raised when a user request is rejected by an admin policy."""
293
+ pass
sky/execution.py CHANGED
@@ -9,6 +9,7 @@ from typing import List, Optional, Tuple, Union
9
9
  import colorama
10
10
 
11
11
  import sky
12
+ from sky import admin_policy
12
13
  from sky import backends
13
14
  from sky import clouds
14
15
  from sky import global_user_state
@@ -16,6 +17,7 @@ from sky import optimizer
16
17
  from sky import sky_logging
17
18
  from sky.backends import backend_utils
18
19
  from sky.usage import usage_lib
20
+ from sky.utils import admin_policy_utils
19
21
  from sky.utils import controller_utils
20
22
  from sky.utils import dag_utils
21
23
  from sky.utils import env_options
@@ -158,7 +160,16 @@ def _execute(
158
160
  handle: Optional[backends.ResourceHandle]; the handle to the cluster. None
159
161
  if dryrun.
160
162
  """
163
+
161
164
  dag = dag_utils.convert_entrypoint_to_dag(entrypoint)
165
+ dag, _ = admin_policy_utils.apply(
166
+ dag,
167
+ request_options=admin_policy.RequestOptions(
168
+ cluster_name=cluster_name,
169
+ idle_minutes_to_autostop=idle_minutes_to_autostop,
170
+ down=down,
171
+ dryrun=dryrun,
172
+ ))
162
173
  assert len(dag) == 1, f'We support 1 task for now. {dag}'
163
174
  task = dag.tasks[0]
164
175
 
@@ -170,9 +181,8 @@ def _execute(
170
181
 
171
182
  cluster_exists = False
172
183
  if cluster_name is not None:
173
- existing_handle = global_user_state.get_handle_from_cluster_name(
174
- cluster_name)
175
- cluster_exists = existing_handle is not None
184
+ cluster_record = global_user_state.get_cluster_from_name(cluster_name)
185
+ cluster_exists = cluster_record is not None
176
186
  # TODO(woosuk): If the cluster exists, print a warning that
177
187
  # `cpus` and `memory` are not used as a job scheduling constraint,
178
188
  # unlike `gpus`.
sky/jobs/controller.py CHANGED
@@ -64,6 +64,7 @@ class JobsController:
64
64
  if len(self._dag.tasks) <= 1:
65
65
  task_name = self._dag_name
66
66
  else:
67
+ assert task.name is not None, task
67
68
  task_name = task.name
68
69
  # This is guaranteed by the spot_launch API, where we fill in
69
70
  # the task.name with
@@ -447,6 +448,7 @@ def _cleanup(job_id: int, dag_yaml: str):
447
448
  # controller, we should keep it in sync with JobsController.__init__()
448
449
  dag, _ = _get_dag_and_name(dag_yaml)
449
450
  for task in dag.tasks:
451
+ assert task.name is not None, task
450
452
  cluster_name = managed_job_utils.generate_managed_job_cluster_name(
451
453
  task.name, job_id)
452
454
  recovery_strategy.terminate_cluster(cluster_name)
sky/jobs/core.py CHANGED
@@ -18,6 +18,7 @@ from sky.jobs import constants as managed_job_constants
18
18
  from sky.jobs import utils as managed_job_utils
19
19
  from sky.skylet import constants as skylet_constants
20
20
  from sky.usage import usage_lib
21
+ from sky.utils import admin_policy_utils
21
22
  from sky.utils import common_utils
22
23
  from sky.utils import controller_utils
23
24
  from sky.utils import dag_utils
@@ -54,6 +55,8 @@ def launch(
54
55
  dag_uuid = str(uuid.uuid4().hex[:4])
55
56
 
56
57
  dag = dag_utils.convert_entrypoint_to_dag(entrypoint)
58
+ dag, mutated_user_config = admin_policy_utils.apply(
59
+ dag, use_mutated_config_in_current_request=False)
57
60
  if not dag.is_chain():
58
61
  with ux_utils.print_exception_no_traceback():
59
62
  raise ValueError('Only single-task or chain DAG is '
@@ -103,6 +106,7 @@ def launch(
103
106
  **controller_utils.shared_controller_vars_to_fill(
104
107
  controller_utils.Controllers.JOBS_CONTROLLER,
105
108
  remote_user_config_path=remote_user_config_path,
109
+ local_user_config=mutated_user_config,
106
110
  ),
107
111
  }
108
112
 
sky/serve/core.py CHANGED
@@ -17,6 +17,7 @@ from sky.serve import serve_state
17
17
  from sky.serve import serve_utils
18
18
  from sky.skylet import constants
19
19
  from sky.usage import usage_lib
20
+ from sky.utils import admin_policy_utils
20
21
  from sky.utils import common_utils
21
22
  from sky.utils import controller_utils
22
23
  from sky.utils import resources_utils
@@ -124,6 +125,10 @@ def up(
124
125
 
125
126
  _validate_service_task(task)
126
127
 
128
+ dag, mutated_user_config = admin_policy_utils.apply(
129
+ task, use_mutated_config_in_current_request=False)
130
+ task = dag.tasks[0]
131
+
127
132
  controller_utils.maybe_translate_local_file_mounts_and_sync_up(task,
128
133
  path='serve')
129
134
 
@@ -158,6 +163,7 @@ def up(
158
163
  **controller_utils.shared_controller_vars_to_fill(
159
164
  controller=controller_utils.Controllers.SKY_SERVE_CONTROLLER,
160
165
  remote_user_config_path=remote_config_yaml_path,
166
+ local_user_config=mutated_user_config,
161
167
  ),
162
168
  }
163
169
  common_utils.fill_template(serve_constants.CONTROLLER_TEMPLATE,
sky/skypilot_config.py CHANGED
@@ -61,6 +61,8 @@ from sky.utils import common_utils
61
61
  from sky.utils import schemas
62
62
  from sky.utils import ux_utils
63
63
 
64
+ logger = sky_logging.init_logger(__name__)
65
+
64
66
  # The config path is discovered in this order:
65
67
  #
66
68
  # (1) (Used internally) If env var {ENV_VAR_SKYPILOT_CONFIG} exists, use its
@@ -78,11 +80,57 @@ ENV_VAR_SKYPILOT_CONFIG = 'SKYPILOT_CONFIG'
78
80
  # Path to the local config file.
79
81
  CONFIG_PATH = '~/.sky/config.yaml'
80
82
 
81
- logger = sky_logging.init_logger(__name__)
83
+
84
+ class Config(Dict[str, Any]):
85
+ """SkyPilot config that supports setting/getting values with nested keys."""
86
+
87
+ def get_nested(self,
88
+ keys: Tuple[str, ...],
89
+ default_value: Any,
90
+ override_configs: Optional[Dict[str, Any]] = None) -> Any:
91
+ """Gets a nested key.
92
+
93
+ If any key is not found, or any intermediate key does not point to a
94
+ dict value, returns 'default_value'.
95
+
96
+ Args:
97
+ keys: A tuple of strings representing the nested keys.
98
+ default_value: The default value to return if the key is not found.
99
+ override_configs: A dict of override configs with the same schema as
100
+ the config file, but only containing the keys to override.
101
+
102
+ Returns:
103
+ The value of the nested key, or 'default_value' if not found.
104
+ """
105
+ config = copy.deepcopy(self)
106
+ if override_configs is not None:
107
+ config = _recursive_update(config, override_configs)
108
+ return _get_nested(config, keys, default_value)
109
+
110
+ def set_nested(self, keys: Tuple[str, ...], value: Any) -> None:
111
+ """In-place sets a nested key to value.
112
+
113
+ Like get_nested(), if any key is not found, this will not raise an
114
+ error.
115
+ """
116
+ override = {}
117
+ for i, key in enumerate(reversed(keys)):
118
+ if i == 0:
119
+ override = {key: value}
120
+ else:
121
+ override = {key: override}
122
+ _recursive_update(self, override)
123
+
124
+ @classmethod
125
+ def from_dict(cls, config: Optional[Dict[str, Any]]) -> 'Config':
126
+ if config is None:
127
+ return cls()
128
+ return cls(**config)
129
+
82
130
 
83
131
  # The loaded config.
84
- _dict: Optional[Dict[str, Any]] = None
85
- _loaded_config_path = None
132
+ _dict = Config()
133
+ _loaded_config_path: Optional[str] = None
86
134
 
87
135
 
88
136
  def _get_nested(configs: Optional[Dict[str, Any]], keys: Iterable[str],
@@ -131,17 +179,11 @@ def get_nested(keys: Tuple[str, ...],
131
179
  ), (f'Override configs must not be provided when keys {keys} is not within '
132
180
  'constants.OVERRIDEABLE_CONFIG_KEYS: '
133
181
  f'{constants.OVERRIDEABLE_CONFIG_KEYS}')
134
- config: Dict[str, Any] = {}
135
- if _dict is not None:
136
- config = copy.deepcopy(_dict)
137
- if override_configs is None:
138
- override_configs = {}
139
- config = _recursive_update(config, override_configs)
140
- return _get_nested(config, keys, default_value)
182
+ return _dict.get_nested(keys, default_value, override_configs)
141
183
 
142
184
 
143
- def _recursive_update(base_config: Dict[str, Any],
144
- override_config: Dict[str, Any]) -> Dict[str, Any]:
185
+ def _recursive_update(base_config: Config,
186
+ override_config: Dict[str, Any]) -> Config:
145
187
  """Recursively updates base configuration with override configuration"""
146
188
  for key, value in override_config.items():
147
189
  if (isinstance(value, dict) and key in base_config and
@@ -157,22 +199,14 @@ def set_nested(keys: Tuple[str, ...], value: Any) -> Dict[str, Any]:
157
199
 
158
200
  Like get_nested(), if any key is not found, this will not raise an error.
159
201
  """
160
- _check_loaded_or_die()
161
- assert _dict is not None
162
- override = {}
163
- for i, key in enumerate(reversed(keys)):
164
- if i == 0:
165
- override = {key: value}
166
- else:
167
- override = {key: override}
168
- return _recursive_update(copy.deepcopy(_dict), override)
202
+ copied_dict = copy.deepcopy(_dict)
203
+ copied_dict.set_nested(keys, value)
204
+ return dict(**copied_dict)
169
205
 
170
206
 
171
- def to_dict() -> Dict[str, Any]:
207
+ def to_dict() -> Config:
172
208
  """Returns a deep-copied version of the current config."""
173
- if _dict is not None:
174
- return copy.deepcopy(_dict)
175
- return {}
209
+ return copy.deepcopy(_dict)
176
210
 
177
211
 
178
212
  def _try_load_config() -> None:
@@ -192,13 +226,14 @@ def _try_load_config() -> None:
192
226
  config_path = os.path.expanduser(config_path)
193
227
  if os.path.exists(config_path):
194
228
  logger.debug(f'Using config path: {config_path}')
195
- _loaded_config_path = config_path
196
229
  try:
197
- _dict = common_utils.read_yaml(config_path)
230
+ config = common_utils.read_yaml(config_path)
231
+ _dict = Config.from_dict(config)
232
+ _loaded_config_path = config_path
198
233
  logger.debug(f'Config loaded:\n{pprint.pformat(_dict)}')
199
234
  except yaml.YAMLError as e:
200
235
  logger.error(f'Error in loading config file ({config_path}):', e)
201
- if _dict is not None:
236
+ if _dict:
202
237
  common_utils.validate_schema(
203
238
  _dict,
204
239
  schemas.get_config_schema(),
@@ -219,14 +254,6 @@ def loaded_config_path() -> Optional[str]:
219
254
  _try_load_config()
220
255
 
221
256
 
222
- def _check_loaded_or_die():
223
- """Checks loaded() is true; otherwise raises RuntimeError."""
224
- if _dict is None:
225
- raise RuntimeError(
226
- f'No user configs loaded. Check {CONFIG_PATH} exists and '
227
- 'can be loaded.')
228
-
229
-
230
257
  def loaded() -> bool:
231
258
  """Returns if the user configurations are loaded."""
232
- return _dict is not None
259
+ return bool(_dict)
@@ -4,7 +4,9 @@ name: {{dag_name}}
4
4
 
5
5
  file_mounts:
6
6
  {{remote_user_yaml_path}}: {{user_yaml_path}}
7
- {{remote_user_config_path}}: skypilot:local_skypilot_config_path
7
+ {%- if local_user_config_path is not none %}
8
+ {{remote_user_config_path}}: {{local_user_config_path}}
9
+ {%- endif %}
8
10
  {%- for remote_catalog_path, local_catalog_path in modified_catalogs.items() %}
9
11
  {{remote_catalog_path}}: {{local_catalog_path}}
10
12
  {%- endfor %}
@@ -23,7 +23,9 @@ setup: |
23
23
 
24
24
  file_mounts:
25
25
  {{remote_task_yaml_path}}: {{local_task_yaml_path}}
26
- {{remote_user_config_path}}: skypilot:local_skypilot_config_path
26
+ {%- if local_user_config_path is not none %}
27
+ {{remote_user_config_path}}: {{local_user_config_path}}
28
+ {%- endif %}
27
29
  {%- for remote_catalog_path, local_catalog_path in modified_catalogs.items() %}
28
30
  {{remote_catalog_path}}: {{local_catalog_path}}
29
31
  {%- endfor %}
@@ -0,0 +1,145 @@
1
+ """Admin policy utils."""
2
+ import copy
3
+ import importlib
4
+ import os
5
+ import tempfile
6
+ from typing import Optional, Tuple, Union
7
+
8
+ import colorama
9
+
10
+ from sky import admin_policy
11
+ from sky import dag as dag_lib
12
+ from sky import exceptions
13
+ from sky import sky_logging
14
+ from sky import skypilot_config
15
+ from sky import task as task_lib
16
+ from sky.utils import common_utils
17
+ from sky.utils import ux_utils
18
+
19
+ logger = sky_logging.init_logger(__name__)
20
+
21
+
22
+ def _get_policy_cls(
23
+ policy: Optional[str]) -> Optional[admin_policy.AdminPolicy]:
24
+ """Gets admin-defined policy."""
25
+ if policy is None:
26
+ return None
27
+ try:
28
+ module_path, class_name = policy.rsplit('.', 1)
29
+ module = importlib.import_module(module_path)
30
+ except ImportError as e:
31
+ with ux_utils.print_exception_no_traceback():
32
+ raise ImportError(
33
+ f'Failed to import policy module: {policy}. '
34
+ 'Please check if the module is installed in your Python '
35
+ 'environment.') from e
36
+
37
+ try:
38
+ policy_cls = getattr(module, class_name)
39
+ except AttributeError as e:
40
+ with ux_utils.print_exception_no_traceback():
41
+ raise AttributeError(
42
+ f'Could not find {class_name} class in module {module_path}. '
43
+ 'Please check with your policy admin for details.') from e
44
+
45
+ # Check if the module implements the AdminPolicy interface.
46
+ if not issubclass(policy_cls, admin_policy.AdminPolicy):
47
+ with ux_utils.print_exception_no_traceback():
48
+ raise ValueError(
49
+ f'Policy class {policy!r} does not implement the AdminPolicy '
50
+ 'interface. Please check with your policy admin for details.')
51
+ return policy_cls
52
+
53
+
54
+ def apply(
55
+ entrypoint: Union['dag_lib.Dag', 'task_lib.Task'],
56
+ use_mutated_config_in_current_request: bool = True,
57
+ request_options: Optional[admin_policy.RequestOptions] = None,
58
+ ) -> Tuple['dag_lib.Dag', skypilot_config.Config]:
59
+ """Applies an admin policy (if registered) to a DAG or a task.
60
+
61
+ It mutates a Dag by applying any registered admin policy and also
62
+ potentially updates (controlled by `use_mutated_config_in_current_request`)
63
+ the global SkyPilot config if there is any changes made by the policy.
64
+
65
+ Args:
66
+ dag: The dag to be mutated by the policy.
67
+ use_mutated_config_in_current_request: Whether to use the mutated
68
+ config in the current request.
69
+ request_options: Additional options user passed for the current request.
70
+
71
+ Returns:
72
+ - The new copy of dag after applying the policy
73
+ - The new copy of skypilot config after applying the policy.
74
+ """
75
+ if isinstance(entrypoint, task_lib.Task):
76
+ dag = dag_lib.Dag()
77
+ dag.add(entrypoint)
78
+ else:
79
+ dag = entrypoint
80
+
81
+ policy = skypilot_config.get_nested(('admin_policy',), None)
82
+ policy_cls = _get_policy_cls(policy)
83
+ if policy_cls is None:
84
+ return dag, skypilot_config.to_dict()
85
+
86
+ logger.info(f'Applying policy: {policy}')
87
+ original_config = skypilot_config.to_dict()
88
+ config = copy.deepcopy(original_config)
89
+ mutated_dag = dag_lib.Dag()
90
+ mutated_dag.name = dag.name
91
+
92
+ mutated_config = None
93
+ for task in dag.tasks:
94
+ user_request = admin_policy.UserRequest(task, config, request_options)
95
+ try:
96
+ mutated_user_request = policy_cls.validate_and_mutate(user_request)
97
+ except Exception as e: # pylint: disable=broad-except
98
+ with ux_utils.print_exception_no_traceback():
99
+ raise exceptions.UserRequestRejectedByPolicy(
100
+ f'{colorama.Fore.RED}User request rejected by policy '
101
+ f'{policy!r}{colorama.Fore.RESET}: '
102
+ f'{common_utils.format_exception(e, use_bracket=True)}'
103
+ ) from e
104
+ if mutated_config is None:
105
+ mutated_config = mutated_user_request.skypilot_config
106
+ else:
107
+ if mutated_config != mutated_user_request.skypilot_config:
108
+ # In the case of a pipeline of tasks, the mutated config
109
+ # generated should remain the same for all tasks for now for
110
+ # simplicity.
111
+ # TODO(zhwu): We should support per-task mutated config or
112
+ # allowing overriding required global config in task YAML.
113
+ with ux_utils.print_exception_no_traceback():
114
+ raise exceptions.UserRequestRejectedByPolicy(
115
+ 'All tasks must have the same SkyPilot config after '
116
+ 'applying the policy. Please check with your policy '
117
+ 'admin for details.')
118
+ mutated_dag.add(mutated_user_request.task)
119
+ assert mutated_config is not None, dag
120
+
121
+ # Update the new_dag's graph with the old dag's graph
122
+ for u, v in dag.graph.edges:
123
+ u_idx = dag.tasks.index(u)
124
+ v_idx = dag.tasks.index(v)
125
+ mutated_dag.graph.add_edge(mutated_dag.tasks[u_idx],
126
+ mutated_dag.tasks[v_idx])
127
+
128
+ if (use_mutated_config_in_current_request and
129
+ original_config != mutated_config):
130
+ with tempfile.NamedTemporaryFile(
131
+ delete=False,
132
+ mode='w',
133
+ prefix='policy-mutated-skypilot-config-',
134
+ suffix='.yaml') as temp_file:
135
+
136
+ common_utils.dump_yaml(temp_file.name, dict(**mutated_config))
137
+ os.environ[skypilot_config.ENV_VAR_SKYPILOT_CONFIG] = temp_file.name
138
+ logger.debug(f'Updated SkyPilot config: {temp_file.name}')
139
+ # TODO(zhwu): This is not a clean way to update the SkyPilot config,
140
+ # because we are resetting the global context for a single DAG,
141
+ # which is conceptually weird.
142
+ importlib.reload(skypilot_config)
143
+
144
+ logger.debug(f'Mutated user request: {mutated_user_request}')
145
+ return mutated_dag, mutated_config
sky/utils/common_utils.py CHANGED
@@ -300,7 +300,7 @@ def user_and_hostname_hash() -> str:
300
300
  return f'{getpass.getuser()}-{hostname_hash}'
301
301
 
302
302
 
303
- def read_yaml(path) -> Dict[str, Any]:
303
+ def read_yaml(path: str) -> Dict[str, Any]:
304
304
  with open(path, 'r', encoding='utf-8') as f:
305
305
  config = yaml.safe_load(f)
306
306
  return config
@@ -316,12 +316,13 @@ def read_yaml_all(path: str) -> List[Dict[str, Any]]:
316
316
  return configs
317
317
 
318
318
 
319
- def dump_yaml(path, config) -> None:
319
+ def dump_yaml(path: str, config: Union[List[Dict[str, Any]],
320
+ Dict[str, Any]]) -> None:
320
321
  with open(path, 'w', encoding='utf-8') as f:
321
322
  f.write(dump_yaml_str(config))
322
323
 
323
324
 
324
- def dump_yaml_str(config):
325
+ def dump_yaml_str(config: Union[List[Dict[str, Any]], Dict[str, Any]]) -> str:
325
326
  # https://github.com/yaml/pyyaml/issues/127
326
327
  class LineBreakDumper(yaml.SafeDumper):
327
328
 
@@ -331,9 +332,9 @@ def dump_yaml_str(config):
331
332
  super().write_line_break()
332
333
 
333
334
  if isinstance(config, list):
334
- dump_func = yaml.dump_all
335
+ dump_func = yaml.dump_all # type: ignore
335
336
  else:
336
- dump_func = yaml.dump
337
+ dump_func = yaml.dump # type: ignore
337
338
  return dump_func(config,
338
339
  Dumper=LineBreakDumper,
339
340
  sort_keys=False,
@@ -44,8 +44,12 @@ CONTROLLER_RESOURCES_NOT_VALID_MESSAGE = (
44
44
  '{controller_type}.controller.resources is a valid resources spec. '
45
45
  'Details:\n {err}')
46
46
 
47
- # The placeholder for the local skypilot config path in file mounts.
48
- LOCAL_SKYPILOT_CONFIG_PATH_PLACEHOLDER = 'skypilot:local_skypilot_config_path'
47
+ # The suffix for local skypilot config path for a job/service in file mounts
48
+ # that tells the controller logic to update the config with specific settings,
49
+ # e.g., removing the ssh_proxy_command when a job/service is launched in a same
50
+ # cloud as controller.
51
+ _LOCAL_SKYPILOT_CONFIG_PATH_SUFFIX = (
52
+ '__skypilot:local_skypilot_config_path.yaml')
49
53
 
50
54
 
51
55
  @dataclasses.dataclass
@@ -350,8 +354,21 @@ def download_and_stream_latest_job_log(
350
354
 
351
355
 
352
356
  def shared_controller_vars_to_fill(
353
- controller: Controllers,
354
- remote_user_config_path: str) -> Dict[str, str]:
357
+ controller: Controllers, remote_user_config_path: str,
358
+ local_user_config: Dict[str, Any]) -> Dict[str, str]:
359
+ if not local_user_config:
360
+ local_user_config_path = None
361
+ else:
362
+ # Remove admin_policy from local_user_config so that it is not applied
363
+ # again on the controller. This is required since admin_policy is not
364
+ # installed on the controller.
365
+ local_user_config.pop('admin_policy', None)
366
+ with tempfile.NamedTemporaryFile(
367
+ delete=False,
368
+ suffix=_LOCAL_SKYPILOT_CONFIG_PATH_SUFFIX) as temp_file:
369
+ common_utils.dump_yaml(temp_file.name, dict(**local_user_config))
370
+ local_user_config_path = temp_file.name
371
+
355
372
  vars_to_fill: Dict[str, Any] = {
356
373
  'cloud_dependencies_installation_commands':
357
374
  _get_cloud_dependencies_installation_commands(controller),
@@ -360,6 +377,7 @@ def shared_controller_vars_to_fill(
360
377
  # accessed.
361
378
  'sky_activate_python_env': constants.ACTIVATE_SKY_REMOTE_PYTHON_ENV,
362
379
  'sky_python_cmd': constants.SKY_PYTHON_CMD,
380
+ 'local_user_config_path': local_user_config_path,
363
381
  }
364
382
  env_vars: Dict[str, str] = {
365
383
  env.value: '1' for env in env_options.Options if env.get()
@@ -481,7 +499,8 @@ def get_controller_resources(
481
499
 
482
500
 
483
501
  def _setup_proxy_command_on_controller(
484
- controller_launched_cloud: 'clouds.Cloud') -> Dict[str, Any]:
502
+ controller_launched_cloud: 'clouds.Cloud',
503
+ user_config: Dict[str, Any]) -> skypilot_config.Config:
485
504
  """Sets up proxy command on the controller.
486
505
 
487
506
  This function should be called on the controller (remote cluster), which
@@ -515,21 +534,20 @@ def _setup_proxy_command_on_controller(
515
534
  # (or name). It may not be a sufficient check (as it's always
516
535
  # possible that peering is not set up), but it may catch some
517
536
  # obvious errors.
537
+ config = skypilot_config.Config.from_dict(user_config)
518
538
  proxy_command_key = (str(controller_launched_cloud).lower(),
519
539
  'ssh_proxy_command')
520
- ssh_proxy_command = skypilot_config.get_nested(proxy_command_key, None)
521
- config_dict = skypilot_config.to_dict()
540
+ ssh_proxy_command = config.get_nested(proxy_command_key, None)
522
541
  if isinstance(ssh_proxy_command, str):
523
- config_dict = skypilot_config.set_nested(proxy_command_key, None)
542
+ config.set_nested(proxy_command_key, None)
524
543
  elif isinstance(ssh_proxy_command, dict):
525
544
  # Instead of removing the key, we set the value to empty string
526
545
  # so that the controller will only try the regions specified by
527
546
  # the keys.
528
547
  ssh_proxy_command = {k: None for k in ssh_proxy_command}
529
- config_dict = skypilot_config.set_nested(proxy_command_key,
530
- ssh_proxy_command)
548
+ config.set_nested(proxy_command_key, ssh_proxy_command)
531
549
 
532
- return config_dict
550
+ return config
533
551
 
534
552
 
535
553
  def replace_skypilot_config_path_in_file_mounts(
@@ -543,25 +561,20 @@ def replace_skypilot_config_path_in_file_mounts(
543
561
  if file_mounts is None:
544
562
  return
545
563
  replaced = False
546
- to_replace = True
547
- with tempfile.NamedTemporaryFile('w', delete=False) as f:
548
- if skypilot_config.loaded():
549
- new_skypilot_config = _setup_proxy_command_on_controller(cloud)
550
- common_utils.dump_yaml(f.name, new_skypilot_config)
551
- to_replace = True
552
- else:
553
- # Empty config. Remove the placeholder below.
554
- to_replace = False
555
- for remote_path, local_path in list(file_mounts.items()):
556
- if local_path == LOCAL_SKYPILOT_CONFIG_PATH_PLACEHOLDER:
557
- if to_replace:
558
- file_mounts[remote_path] = f.name
559
- replaced = True
560
- else:
561
- del file_mounts[remote_path]
564
+ for remote_path, local_path in list(file_mounts.items()):
565
+ if local_path is None:
566
+ del file_mounts[remote_path]
567
+ continue
568
+ if local_path.endswith(_LOCAL_SKYPILOT_CONFIG_PATH_SUFFIX):
569
+ with tempfile.NamedTemporaryFile('w', delete=False) as f:
570
+ user_config = common_utils.read_yaml(local_path)
571
+ config = _setup_proxy_command_on_controller(cloud, user_config)
572
+ common_utils.dump_yaml(f.name, dict(**config))
573
+ file_mounts[remote_path] = f.name
574
+ replaced = True
562
575
  if replaced:
563
- logger.debug(f'Replaced {LOCAL_SKYPILOT_CONFIG_PATH_PLACEHOLDER} with '
564
- f'the real path in file mounts: {file_mounts}')
576
+ logger.debug(f'Replaced {_LOCAL_SKYPILOT_CONFIG_PATH_SUFFIX} '
577
+ f'with the real path in file mounts: {file_mounts}')
565
578
 
566
579
 
567
580
  def maybe_translate_local_file_mounts_and_sync_up(task: 'task_lib.Task',
sky/utils/dag_utils.py CHANGED
@@ -36,30 +36,33 @@ The command can then be run as:
36
36
 
37
37
 
38
38
  def convert_entrypoint_to_dag(entrypoint: Any) -> 'dag_lib.Dag':
39
- """Convert the entrypoint to a sky.Dag.
39
+ """Converts the entrypoint to a sky.Dag and applies the policy.
40
40
 
41
41
  Raises TypeError if 'entrypoint' is not a 'sky.Task' or 'sky.Dag'.
42
42
  """
43
43
  # Not suppressing stacktrace: when calling this via API user may want to
44
44
  # see their own program in the stacktrace. Our CLI impl would not trigger
45
45
  # these errors.
46
+ converted_dag: 'dag_lib.Dag'
46
47
  if isinstance(entrypoint, str):
47
48
  with ux_utils.print_exception_no_traceback():
48
49
  raise TypeError(_ENTRYPOINT_STRING_AS_DAG_MESSAGE)
49
50
  elif isinstance(entrypoint, dag_lib.Dag):
50
- return copy.deepcopy(entrypoint)
51
+ converted_dag = copy.deepcopy(entrypoint)
51
52
  elif isinstance(entrypoint, task_lib.Task):
52
53
  entrypoint = copy.deepcopy(entrypoint)
53
54
  with dag_lib.Dag() as dag:
54
55
  dag.add(entrypoint)
55
56
  dag.name = entrypoint.name
56
- return dag
57
+ converted_dag = dag
57
58
  else:
58
59
  with ux_utils.print_exception_no_traceback():
59
60
  raise TypeError(
60
61
  'Expected a sky.Task or sky.Dag but received argument of type: '
61
62
  f'{type(entrypoint)}')
62
63
 
64
+ return converted_dag
65
+
63
66
 
64
67
  def load_chain_dag_from_yaml(
65
68
  path: str,
sky/utils/schemas.py CHANGED
@@ -848,6 +848,13 @@ def get_config_schema():
848
848
  },
849
849
  }
850
850
 
851
+ admin_policy_schema = {
852
+ 'type': 'string',
853
+ # Check regex to be a valid python module path
854
+ 'pattern': (r'^[a-zA-Z_][a-zA-Z0-9_]*'
855
+ r'(\.[a-zA-Z_][a-zA-Z0-9_]*)+$'),
856
+ }
857
+
851
858
  allowed_clouds = {
852
859
  # A list of cloud names that are allowed to be used
853
860
  'type': 'array',
@@ -905,6 +912,7 @@ def get_config_schema():
905
912
  'spot': controller_resources_schema,
906
913
  'serve': controller_resources_schema,
907
914
  'allowed_clouds': allowed_clouds,
915
+ 'admin_policy': admin_policy_schema,
908
916
  'docker': docker_configs,
909
917
  'nvidia_gpus': gpu_configs,
910
918
  **cloud_configs,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: skypilot-nightly
3
- Version: 1.0.0.dev20240923
3
+ Version: 1.0.0.dev20240925
4
4
  Summary: SkyPilot: An intercloud broker for the clouds
5
5
  Author: SkyPilot Team
6
6
  License: Apache 2.0
@@ -1,17 +1,18 @@
1
- sky/__init__.py,sha256=ABpdCy_YwMSkcSplyajbIKQGxWNoWGWXAZtJmSGIevs,5588
1
+ sky/__init__.py,sha256=n-Q-Qhc50FJ91_dAC8eBKZ84oIShcdcPKn3u9eWTR7M,5854
2
+ sky/admin_policy.py,sha256=hPo02f_A32gCqhUueF0QYy1fMSSKqRwYEg_9FxScN_s,3248
2
3
  sky/authentication.py,sha256=yvpdkXS9htf-X83DPCiSG3mQ41y0zV1BQ0YgOMgTYBU,20612
3
4
  sky/check.py,sha256=jLMIIJrseaZj1_o5WkbaD9XdyXIlCaT6pyAaIFdhdmA,9079
4
5
  sky/cli.py,sha256=2cOw3lXzRA-uLlEH-eK7N_1VmUpf1LR4Ztu-ZaKu3Is,201673
5
6
  sky/cloud_stores.py,sha256=RjFgmRhUh1Kk__f6g3KxzLp9s7dA0pFK4W1AukEuUaw,21153
6
7
  sky/core.py,sha256=YF_6kwj8Ja171Oycb8L25SZ7V_ylZYovFS_jpnjwGo0,34408
7
- sky/dag.py,sha256=d3gF030wYmit01jxszEygT4EvKJUky3DBlG3PfWPp_w,2529
8
- sky/exceptions.py,sha256=gfrmh8djfZ0oGn1XtYWn8ca7wEyghkbzg4Hkq6tdjj8,8594
9
- sky/execution.py,sha256=nyLjzYOY1e-NHJeufrx24Oz2_tyBrKVoAu_tQtrhL0I,25235
8
+ sky/dag.py,sha256=WLFWr5hfrwjd31uYlNvI-zWUk7tLaT_gzJn4LzbVtkE,2780
9
+ sky/exceptions.py,sha256=s7j0iCa1Ec0rU1ABb9EAhqn2qFm22bmKQV_ckgRlMGk,8720
10
+ sky/execution.py,sha256=97yhNh5BKBh2ZJW8GefGID_4KCYU-arnvemSJB9rf6U,25552
10
11
  sky/global_user_state.py,sha256=PywEmUutF97XBgRMClR6IS5_KM8JJC0oA1LsPUZebp0,28681
11
12
  sky/optimizer.py,sha256=YGBhJPlcvylYON7MLrYEMtBOqJLt4LdlguQclVvvl4E,58677
12
13
  sky/resources.py,sha256=_959wcQnoiAYesslN9BPXWABFaQfc_TFXPO_o7SPlxI,67325
13
14
  sky/sky_logging.py,sha256=I59__M9taBjDim15ie0m25Vtn6itLtR9Ao8W9FS36Xs,4253
14
- sky/skypilot_config.py,sha256=eyXxUljnLoQRLPO5sqm5MjuydhQIhu6RGPNn5Op0LpI,8063
15
+ sky/skypilot_config.py,sha256=E3g65cX3P3dT9b5N0GgFBG6yB0FXwIGpisKoozmJmWU,9094
15
16
  sky/status_lib.py,sha256=J7Jb4_Dz0v2T64ttOdyUgpokvl4S0sBJrMfH7Fvo51A,1457
16
17
  sky/task.py,sha256=KDsTBIxYpkCOPHv3_ei5H3LDMiGHvDeS9_2HeL6yyLA,49766
17
18
  sky/adaptors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -30,7 +31,7 @@ sky/adaptors/vsphere.py,sha256=zJP9SeObEoLrpgHW2VHvZE48EhgVf8GfAEIwBeaDMfM,2129
30
31
  sky/backends/__init__.py,sha256=UDjwbUgpTRApbPJnNfR786GadUuwgRk3vsWoVu5RB_c,536
31
32
  sky/backends/backend.py,sha256=xtxR6boDv1o-uSCjbJhOMkKMnZvBZh3gExx4khFWPTI,5932
32
33
  sky/backends/backend_utils.py,sha256=W11gOb3v6Z2PLFu5YbFHDCckjgfwiSieBiDtmMeJXpE,126590
33
- sky/backends/cloud_vm_ray_backend.py,sha256=do6bEyVKxojkF8zIC4AI3lSrjREbxteOkxqeXdplSi4,232350
34
+ sky/backends/cloud_vm_ray_backend.py,sha256=3bPAln6pAP5r_Xm1_d2whInS6wO_fh1i7G0heGxvjY8,232893
34
35
  sky/backends/docker_utils.py,sha256=Hyw1YY20EyghhEbYx6O2FIMDcGkNzBzV9TM7LFynei8,8358
35
36
  sky/backends/local_docker_backend.py,sha256=H4GBo0KFUC_EEf-ziv1OUbfAkOI5BrwkYs9fYOxSoNw,16741
36
37
  sky/backends/wheel_utils.py,sha256=3QS4T_Ydvo4DbYhogtyADyNBEf04I6jUCL71M285shQ,7963
@@ -94,8 +95,8 @@ sky/data/storage.py,sha256=JYeEWQtCXg9h6CjCaAsINV1MXcvFswuUgf3c0GOZQSs,162523
94
95
  sky/data/storage_utils.py,sha256=-s0iQhV8JVx1J2gWtoBFrN04MGv2oVYxo_Hw43R2BSY,6867
95
96
  sky/jobs/__init__.py,sha256=9cqFutVlfjQb7t8hzG-ZlQmMlbmfMirn0KNBxIFnJYQ,1398
96
97
  sky/jobs/constants.py,sha256=YLgcCg_RHSYr_rfsI_4UIdXk78KKKOK29Oem88t5j8I,1350
97
- sky/jobs/controller.py,sha256=VbEUusR85Mady-lJhf7jwliydiBewvlLlPbFcjOEiMU,26607
98
- sky/jobs/core.py,sha256=Be_RglG5vzJCf1tj9vLcOpkCjzCGfyY7it0EcjMX8lg,13426
98
+ sky/jobs/controller.py,sha256=k28bbicxtML6p1YxSetk-1nhBHPCubpvLWJsh7TtU9c,26701
99
+ sky/jobs/core.py,sha256=Q5ExRWnF7yAYWJxwnB9NfAGBVDNqKYBCrWsypiMLCpY,13637
99
100
  sky/jobs/recovery_strategy.py,sha256=G3iFicEajB-l9FefvcqjqPIazb1X8BJ_AgVmD5bDV2w,25556
100
101
  sky/jobs/state.py,sha256=nf6hduj0dSekmP7scW5mKHiYQs6WkdigJpJJbLEBdEw,23041
101
102
  sky/jobs/utils.py,sha256=ZB2dJxtJ4hbCRdxHmy8wrmtXIvvGGE80kk5BQTOQWkQ,35653
@@ -171,7 +172,7 @@ sky/serve/__init__.py,sha256=Qg_XPOtQsUxiN-Q3njHZRfzoMcQ_KKU1QthkiTbESDw,1661
171
172
  sky/serve/autoscalers.py,sha256=CAdp0vFN_eBx8K3svJToj1hTEc29M5gB2V1BhV9a_lI,30136
172
173
  sky/serve/constants.py,sha256=OansIC7a0Pwat-Y5SF43T9phad_EvyjKO3peZgKFEHk,4367
173
174
  sky/serve/controller.py,sha256=spXd-QXOy0xpyN5VtZ6E3BoHIvE3fOZTysKqS-X880U,7651
174
- sky/serve/core.py,sha256=NDs2fRRRHtsI-ydokSj3VJuj9nwKTrVeCqhMpAQ0-j0,28710
175
+ sky/serve/core.py,sha256=cW2SNMPMbGtOcqASHnL__B12BCIErUilGtFw3olQbjk,28947
175
176
  sky/serve/load_balancer.py,sha256=90HWkhhmfrW9kaGVSMxnqzYB_o5PwAOKX0mTAVfQVFM,11548
176
177
  sky/serve/load_balancing_policies.py,sha256=ExdwH_pxPYpJ6CkoTQCOPSa4lzwbq1LFFMKzmIu8ryk,2331
177
178
  sky/serve/replica_managers.py,sha256=c-S8m1Akgy13MjjX3BrqRlUiwbpjbicXEyWASjosPN0,57308
@@ -222,7 +223,7 @@ sky/templates/cudo-ray.yml.j2,sha256=SEHVY57iBauCOE2HYJtYVFEKlriAkdwQu_p86a1n_bA
222
223
  sky/templates/fluidstack-ray.yml.j2,sha256=t8TCULgiErCZdtFmBZVsA8ZdcqR7ccwsmQhuDFTBEAU,3541
223
224
  sky/templates/gcp-ray.yml.j2,sha256=q2xSWxxYI8MVAq_mA__8FF6PwEqXCAW1SOEOGTt0qPw,9591
224
225
  sky/templates/ibm-ray.yml.j2,sha256=RMBUqPId8i4CnVwcyfK3DbRapF1jFMuGQlY0E0PFbMU,6669
225
- sky/templates/jobs-controller.yaml.j2,sha256=QjlIcFBEay48xKT50UWqzgREzili-H7AuUrnGHWOpI8,1554
226
+ sky/templates/jobs-controller.yaml.j2,sha256=Gu3ogFxFYr09VEXP-6zEbrCUOFo1aYxWEjAq7whCrxo,1607
226
227
  sky/templates/kubernetes-ingress.yml.j2,sha256=73iDklVDWBMbItg0IexCa6_ClXPJOxw7PWz3leku4nE,1340
227
228
  sky/templates/kubernetes-loadbalancer.yml.j2,sha256=IxrNYM366N01bbkJEbZ_UPYxUP8wyVEbRNFHRsBuLsw,626
228
229
  sky/templates/kubernetes-port-forward-proxy-command.sh,sha256=HlG7CPBBedCVBlL9qv0erW_eKm6Irj0LFyaAWuJW_lc,3148
@@ -234,26 +235,27 @@ sky/templates/oci-ray.yml.j2,sha256=5XfIobW9XuspIpEhI4vFIEcJEFCdtFJqEGfX03zL6DE,
234
235
  sky/templates/paperspace-ray.yml.j2,sha256=HQjZNamrB_a4fOMCxQXSVdV5JIHtbGtAE0JzEO8uuVQ,4021
235
236
  sky/templates/runpod-ray.yml.j2,sha256=p3BtYBHzROtNJqnjEo1xCmGSJQfCZYdarWszhDYyl0Q,3697
236
237
  sky/templates/scp-ray.yml.j2,sha256=I9u8Ax-lit-d6UrCC9BVU8avst8w1cwK6TrzZBcz_JM,5608
237
- sky/templates/sky-serve-controller.yaml.j2,sha256=IWNdhBSoCLLE4GG87ztk_P0EmZ6LS_CHHgWFVzOph5c,1575
238
+ sky/templates/sky-serve-controller.yaml.j2,sha256=V1IiYhArv_D_7JzC3sVN4nKlSCCCL1AYtIdxFSa-f4c,1628
238
239
  sky/templates/vsphere-ray.yml.j2,sha256=cOQ-qdpxGA2FHajMMhTJI-SmlYzdPterX4Gsiq-nkb0,3587
239
240
  sky/usage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
240
241
  sky/usage/constants.py,sha256=8xpg9vhDU9A3eObtpkNFjwa42oCazqGEv4yw_vJSO7U,590
241
242
  sky/usage/usage_lib.py,sha256=uqclBc87_9D_QVWigCjOIfWFoVB6re68C7RnwjzRYvg,17870
242
243
  sky/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
243
244
  sky/utils/accelerator_registry.py,sha256=BO4iYH5bV80Xyp4EPfO0n1D3LL0FvESCy7xm59Je3_o,3798
245
+ sky/utils/admin_policy_utils.py,sha256=zFCu1OFIrZRfQNY0JFRO1502WFfdqZhwAU_QgM4fO9U,5943
244
246
  sky/utils/cluster_yaml_utils.py,sha256=1wRRYqI1kI-eFs1pMW4r_FFjHJ0zamq6v2RRI-Gtx5E,849
245
247
  sky/utils/command_runner.py,sha256=SKA90-i4FK7fn9TpkztXY6E3fCBGUjGj-jcrgvb8DAQ,33643
246
248
  sky/utils/command_runner.pyi,sha256=1khh14BhdOpMxvk9Ydnd3OFdas5Nha6dSOzy5xLBUU4,7710
247
- sky/utils/common_utils.py,sha256=HMCYoxOxLsgdKyRJW9PZHjE3fZ-CmYMSk-jpiINSduY,23850
248
- sky/utils/controller_utils.py,sha256=z5eclkS67dNz54KyHtftzu-HV7O1XPnURgelRY9N63s,36783
249
- sky/utils/dag_utils.py,sha256=ddSmnOjE9yMpaSC5jlC-x_tq1hPsVYJzBTyDch_cpr4,5487
249
+ sky/utils/common_utils.py,sha256=O6PlZTCNhbuXOzjuV2DKw43niWE_qPfYZNGhnMtZzQg,24028
250
+ sky/utils/controller_utils.py,sha256=VtdjKH9u1kWwUOMzPUxuLpT-XXQ2gCLCLOldB-vdh_8,37483
251
+ sky/utils/dag_utils.py,sha256=gjGZiJj4_GYsraXX67e6ElvbmOByJcyjSfvVgYZiXvs,5588
250
252
  sky/utils/db_utils.py,sha256=AOvMmBEN9cF4I7CoXihPCtus4mU2VDGjBQSVMMgzKlA,2786
251
253
  sky/utils/env_options.py,sha256=1VXyd3bhiUgGfCpmmTqM9PagRo1ILBH4-pzIxmIeE6E,861
252
254
  sky/utils/kubernetes_enums.py,sha256=imGqHSa8O07zD_6xH1SDMM7dBU5lF5fzFFlQuQy00QM,1384
253
255
  sky/utils/log_utils.py,sha256=W7FYK7xzvbq4V-8R-ihLtz939ryvtABug6O-4DFrjho,8139
254
256
  sky/utils/resources_utils.py,sha256=snByBxgx3Hnjfch2uysdAA3D-OAwrnuzTDHug36s5H4,6515
255
257
  sky/utils/rich_utils.py,sha256=5ZVhzlFx-nhqMXwv00eO9xC4rz7ibDlfD2lmGhZrJEY,1581
256
- sky/utils/schemas.py,sha256=dGjxAXpaVewRZ7vh4vLIAnOiflOPActnc1LVJJ5RMoU,28331
258
+ sky/utils/schemas.py,sha256=bJan9aOi2gK6jnTzxNkX1kivIUsQ5Mc5SzKiBj3A97g,28597
257
259
  sky/utils/subprocess_utils.py,sha256=zK0L3mvAkvKX1nFFI4IFEmRWX9ytpguhhxOTYUDKoDs,6507
258
260
  sky/utils/timeline.py,sha256=ao_nm0y52ZQILfL7Y92c3pSEFRyPm_ElORC3DrI5BwQ,3936
259
261
  sky/utils/ux_utils.py,sha256=318TRunQCyJpJXonfiJ1SVotNA-6K4F2XgMEYjvWvsk,3264
@@ -270,9 +272,9 @@ sky/utils/kubernetes/k8s_gpu_labeler_job.yaml,sha256=KPqp23B-zQ2SZK03jdHeF9fLTog
270
272
  sky/utils/kubernetes/k8s_gpu_labeler_setup.yaml,sha256=VLKT2KKimZu1GDg_4AIlIt488oMQvhRZWwsj9vBbPUg,3812
271
273
  sky/utils/kubernetes/rsync_helper.sh,sha256=Ma-N9a271fTfdgP5-8XIQL7KPf8IPUo-uY004PCdUFo,747
272
274
  sky/utils/kubernetes/ssh_jump_lifecycle_manager.py,sha256=RFLJ3k7MR5UN4SKHykQ0lV9SgXumoULpKYIAt1vh-HU,6560
273
- skypilot_nightly-1.0.0.dev20240923.dist-info/LICENSE,sha256=emRJAvE7ngL6x0RhQvlns5wJzGI3NEQ_WMjNmd9TZc4,12170
274
- skypilot_nightly-1.0.0.dev20240923.dist-info/METADATA,sha256=sZYi5_sZTkL-XXdjdk8XEsAFCy3zMgzhTHlIFI_TS7s,19011
275
- skypilot_nightly-1.0.0.dev20240923.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
276
- skypilot_nightly-1.0.0.dev20240923.dist-info/entry_points.txt,sha256=StA6HYpuHj-Y61L2Ze-hK2IcLWgLZcML5gJu8cs6nU4,36
277
- skypilot_nightly-1.0.0.dev20240923.dist-info/top_level.txt,sha256=qA8QuiNNb6Y1OF-pCUtPEr6sLEwy2xJX06Bd_CrtrHY,4
278
- skypilot_nightly-1.0.0.dev20240923.dist-info/RECORD,,
275
+ skypilot_nightly-1.0.0.dev20240925.dist-info/LICENSE,sha256=emRJAvE7ngL6x0RhQvlns5wJzGI3NEQ_WMjNmd9TZc4,12170
276
+ skypilot_nightly-1.0.0.dev20240925.dist-info/METADATA,sha256=ed5Jd2XESpKbZee7szKC7QnUHl8s73cNB0iEgdJrfpk,19011
277
+ skypilot_nightly-1.0.0.dev20240925.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
278
+ skypilot_nightly-1.0.0.dev20240925.dist-info/entry_points.txt,sha256=StA6HYpuHj-Y61L2Ze-hK2IcLWgLZcML5gJu8cs6nU4,36
279
+ skypilot_nightly-1.0.0.dev20240925.dist-info/top_level.txt,sha256=qA8QuiNNb6Y1OF-pCUtPEr6sLEwy2xJX06Bd_CrtrHY,4
280
+ skypilot_nightly-1.0.0.dev20240925.dist-info/RECORD,,