skypilot-nightly 1.0.0.dev20250609__py3-none-any.whl → 1.0.0.dev20250611__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 (117) hide show
  1. sky/__init__.py +2 -2
  2. sky/admin_policy.py +134 -5
  3. sky/authentication.py +1 -7
  4. sky/backends/cloud_vm_ray_backend.py +9 -20
  5. sky/benchmark/benchmark_state.py +39 -1
  6. sky/cli.py +3 -5
  7. sky/client/cli.py +3 -5
  8. sky/client/sdk.py +49 -4
  9. sky/clouds/kubernetes.py +15 -24
  10. sky/dashboard/out/404.html +1 -1
  11. sky/dashboard/out/_next/static/chunks/211.692afc57e812ae1a.js +1 -0
  12. sky/dashboard/out/_next/static/chunks/350.9e123a4551f68b0d.js +1 -0
  13. sky/dashboard/out/_next/static/chunks/37-d8aebf1683522a0b.js +6 -0
  14. sky/dashboard/out/_next/static/chunks/42.d39e24467181b06b.js +6 -0
  15. sky/dashboard/out/_next/static/chunks/443.b2242d0efcdf5f47.js +1 -0
  16. sky/dashboard/out/_next/static/chunks/470-4d1a5dbe58a8a2b9.js +1 -0
  17. sky/dashboard/out/_next/static/chunks/{121-865d2bf8a3b84c6a.js → 491.b3d264269613fe09.js} +3 -3
  18. sky/dashboard/out/_next/static/chunks/513.211357a2914a34b2.js +1 -0
  19. sky/dashboard/out/_next/static/chunks/600.15a0009177e86b86.js +16 -0
  20. sky/dashboard/out/_next/static/chunks/616-d6128fa9e7cae6e6.js +39 -0
  21. sky/dashboard/out/_next/static/chunks/664-047bc03493fda379.js +1 -0
  22. sky/dashboard/out/_next/static/chunks/682.4dd5dc116f740b5f.js +6 -0
  23. sky/dashboard/out/_next/static/chunks/760-a89d354797ce7af5.js +1 -0
  24. sky/dashboard/out/_next/static/chunks/799-3625946b2ec2eb30.js +8 -0
  25. sky/dashboard/out/_next/static/chunks/804-4c9fc53aa74bc191.js +21 -0
  26. sky/dashboard/out/_next/static/chunks/843-6fcc4bf91ac45b39.js +11 -0
  27. sky/dashboard/out/_next/static/chunks/856-0776dc6ed6000c39.js +1 -0
  28. sky/dashboard/out/_next/static/chunks/901-b424d293275e1fd7.js +1 -0
  29. sky/dashboard/out/_next/static/chunks/938-ab185187a63f9cdb.js +1 -0
  30. sky/dashboard/out/_next/static/chunks/947-6620842ef80ae879.js +35 -0
  31. sky/dashboard/out/_next/static/chunks/969-20d54a9d998dc102.js +1 -0
  32. sky/dashboard/out/_next/static/chunks/973-c807fc34f09c7df3.js +1 -0
  33. sky/dashboard/out/_next/static/chunks/pages/_app-7bbd9d39d6f9a98a.js +20 -0
  34. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-89216c616dbaa9c5.js +6 -0
  35. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-451a14e7e755ebbc.js +6 -0
  36. sky/dashboard/out/_next/static/chunks/pages/clusters-e56b17fd85d0ba58.js +1 -0
  37. sky/dashboard/out/_next/static/chunks/pages/config-497a35a7ed49734a.js +1 -0
  38. sky/dashboard/out/_next/static/chunks/pages/infra/[context]-d2910be98e9227cb.js +1 -0
  39. sky/dashboard/out/_next/static/chunks/pages/infra-780860bcc1103945.js +1 -0
  40. sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-b3dbf38b51cb29be.js +16 -0
  41. sky/dashboard/out/_next/static/chunks/pages/jobs-fe233baf3d073491.js +1 -0
  42. sky/dashboard/out/_next/static/chunks/pages/users-c69ffcab9d6e5269.js +1 -0
  43. sky/dashboard/out/_next/static/chunks/pages/workspace/new-31aa8bdcb7592635.js +1 -0
  44. sky/dashboard/out/_next/static/chunks/pages/workspaces/[name]-c8c2191328532b7d.js +1 -0
  45. sky/dashboard/out/_next/static/chunks/pages/workspaces-82e6601baa5dd280.js +1 -0
  46. sky/dashboard/out/_next/static/chunks/webpack-208a9812ab4f61c9.js +1 -0
  47. sky/dashboard/out/_next/static/css/{8b1c8321d4c02372.css → 5d71bfc09f184bab.css} +1 -1
  48. sky/dashboard/out/_next/static/zJqasksBQ3HcqMpA2wTUZ/_buildManifest.js +1 -0
  49. sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
  50. sky/dashboard/out/clusters/[cluster].html +1 -1
  51. sky/dashboard/out/clusters.html +1 -1
  52. sky/dashboard/out/config.html +1 -1
  53. sky/dashboard/out/index.html +1 -1
  54. sky/dashboard/out/infra/[context].html +1 -1
  55. sky/dashboard/out/infra.html +1 -1
  56. sky/dashboard/out/jobs/[job].html +1 -1
  57. sky/dashboard/out/jobs.html +1 -1
  58. sky/dashboard/out/users.html +1 -1
  59. sky/dashboard/out/workspace/new.html +1 -1
  60. sky/dashboard/out/workspaces/[name].html +1 -1
  61. sky/dashboard/out/workspaces.html +1 -1
  62. sky/exceptions.py +18 -0
  63. sky/global_user_state.py +181 -74
  64. sky/jobs/client/sdk.py +29 -21
  65. sky/jobs/scheduler.py +4 -5
  66. sky/jobs/state.py +104 -11
  67. sky/jobs/utils.py +5 -5
  68. sky/provision/kubernetes/constants.py +9 -0
  69. sky/provision/kubernetes/utils.py +106 -7
  70. sky/serve/client/sdk.py +56 -45
  71. sky/server/common.py +1 -5
  72. sky/server/requests/executor.py +50 -20
  73. sky/server/requests/payloads.py +3 -0
  74. sky/server/requests/process.py +69 -29
  75. sky/server/server.py +1 -0
  76. sky/server/stream_utils.py +111 -55
  77. sky/skylet/constants.py +1 -2
  78. sky/skylet/job_lib.py +95 -40
  79. sky/skypilot_config.py +99 -25
  80. sky/users/permission.py +34 -17
  81. sky/utils/admin_policy_utils.py +41 -16
  82. sky/utils/context.py +21 -1
  83. sky/utils/controller_utils.py +16 -1
  84. sky/utils/kubernetes/exec_kubeconfig_converter.py +19 -47
  85. sky/utils/schemas.py +11 -3
  86. {skypilot_nightly-1.0.0.dev20250609.dist-info → skypilot_nightly-1.0.0.dev20250611.dist-info}/METADATA +1 -1
  87. {skypilot_nightly-1.0.0.dev20250609.dist-info → skypilot_nightly-1.0.0.dev20250611.dist-info}/RECORD +92 -81
  88. sky/dashboard/out/_next/static/chunks/236-619ed0248fb6fdd9.js +0 -6
  89. sky/dashboard/out/_next/static/chunks/293-351268365226d251.js +0 -1
  90. sky/dashboard/out/_next/static/chunks/37-600191c5804dcae2.js +0 -6
  91. sky/dashboard/out/_next/static/chunks/470-680c19413b8f808b.js +0 -1
  92. sky/dashboard/out/_next/static/chunks/63-e2d7b1e75e67c713.js +0 -66
  93. sky/dashboard/out/_next/static/chunks/682-b60cfdacc15202e8.js +0 -6
  94. sky/dashboard/out/_next/static/chunks/843-16c7194621b2b512.js +0 -11
  95. sky/dashboard/out/_next/static/chunks/856-affc52adf5403a3a.js +0 -1
  96. sky/dashboard/out/_next/static/chunks/969-2c584e28e6b4b106.js +0 -1
  97. sky/dashboard/out/_next/static/chunks/973-aed916d5b02d2d63.js +0 -1
  98. sky/dashboard/out/_next/static/chunks/pages/_app-5f16aba5794ee8e7.js +0 -1
  99. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-d31688d3e52736dd.js +0 -6
  100. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-e7d8710a9b0491e5.js +0 -6
  101. sky/dashboard/out/_next/static/chunks/pages/clusters-3c674e5d970e05cb.js +0 -1
  102. sky/dashboard/out/_next/static/chunks/pages/config-3aac7a015c6eede1.js +0 -6
  103. sky/dashboard/out/_next/static/chunks/pages/infra/[context]-46d2e4ad6c487260.js +0 -1
  104. sky/dashboard/out/_next/static/chunks/pages/infra-7013d816a2a0e76c.js +0 -1
  105. sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-f7f0c9e156d328bc.js +0 -16
  106. sky/dashboard/out/_next/static/chunks/pages/jobs-87e60396c376292f.js +0 -1
  107. sky/dashboard/out/_next/static/chunks/pages/users-9355a0f13d1db61d.js +0 -16
  108. sky/dashboard/out/_next/static/chunks/pages/workspace/new-9a749cca1813bd27.js +0 -1
  109. sky/dashboard/out/_next/static/chunks/pages/workspaces/[name]-8eeb628e03902f1b.js +0 -1
  110. sky/dashboard/out/_next/static/chunks/pages/workspaces-8fbcc5ab4af316d0.js +0 -1
  111. sky/dashboard/out/_next/static/chunks/webpack-65d465f948974c0d.js +0 -1
  112. sky/dashboard/out/_next/static/xos0euNCptbGAM7_Q3Acl/_buildManifest.js +0 -1
  113. /sky/dashboard/out/_next/static/{xos0euNCptbGAM7_Q3Acl → zJqasksBQ3HcqMpA2wTUZ}/_ssgManifest.js +0 -0
  114. {skypilot_nightly-1.0.0.dev20250609.dist-info → skypilot_nightly-1.0.0.dev20250611.dist-info}/WHEEL +0 -0
  115. {skypilot_nightly-1.0.0.dev20250609.dist-info → skypilot_nightly-1.0.0.dev20250611.dist-info}/entry_points.txt +0 -0
  116. {skypilot_nightly-1.0.0.dev20250609.dist-info → skypilot_nightly-1.0.0.dev20250611.dist-info}/licenses/LICENSE +0 -0
  117. {skypilot_nightly-1.0.0.dev20250609.dist-info → skypilot_nightly-1.0.0.dev20250611.dist-info}/top_level.txt +0 -0
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 = 'a2f62cdbc1e7e0e9f9f7f38e01271b040e0ec2f2'
8
+ _SKYPILOT_COMMIT_SHA = 'b85fa521454fb935e9609773c4cfe52e2237e60f'
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.dev20250609'
38
+ __version__ = '1.0.0.dev20250611'
39
39
  __root_dir__ = os.path.dirname(os.path.abspath(__file__))
40
40
 
41
41
 
sky/admin_policy.py CHANGED
@@ -2,14 +2,23 @@
2
2
  import abc
3
3
  import dataclasses
4
4
  import typing
5
- from typing import Optional
5
+ from typing import Any, Dict, Optional
6
+
7
+ import pydantic
8
+
9
+ import sky
10
+ from sky import exceptions
11
+ from sky.adaptors import common as adaptors_common
12
+ from sky.utils import config_utils
13
+ from sky.utils import ux_utils
6
14
 
7
15
  if typing.TYPE_CHECKING:
8
- import sky
16
+ import requests
17
+ else:
18
+ requests = adaptors_common.LazyImport('requests')
9
19
 
10
20
 
11
- @dataclasses.dataclass
12
- class RequestOptions:
21
+ class RequestOptions(pydantic.BaseModel):
13
22
  """Request options for admin policy.
14
23
 
15
24
  Args:
@@ -31,6 +40,14 @@ class RequestOptions:
31
40
  dryrun: bool
32
41
 
33
42
 
43
+ class _UserRequestBody(pydantic.BaseModel):
44
+ """Auxiliary model to validate and serialize a user request."""
45
+ task: Dict[str, Any]
46
+ skypilot_config: Dict[str, Any]
47
+ request_options: Optional[RequestOptions] = None
48
+ at_client_side: bool = False
49
+
50
+
34
51
  @dataclasses.dataclass
35
52
  class UserRequest:
36
53
  """A user request.
@@ -50,20 +67,71 @@ class UserRequest:
50
67
  task: User specified task.
51
68
  skypilot_config: Global skypilot config to be used in this request.
52
69
  request_options: Request options. It is None for jobs and services.
70
+ at_client_side: Is the request intercepted by the policy at client-side?
53
71
  """
54
72
  task: 'sky.Task'
55
73
  skypilot_config: 'sky.Config'
56
74
  request_options: Optional['RequestOptions'] = None
75
+ at_client_side: bool = False
76
+
77
+ def encode(self) -> str:
78
+ return _UserRequestBody(
79
+ task=self.task.to_yaml_config(),
80
+ skypilot_config=dict(self.skypilot_config),
81
+ request_options=self.request_options,
82
+ at_client_side=self.at_client_side).model_dump_json()
83
+
84
+ @classmethod
85
+ def decode(cls, body: str) -> 'UserRequest':
86
+ user_request_body = _UserRequestBody.model_validate_json(body)
87
+ return cls(task=sky.Task.from_yaml_config(user_request_body.task),
88
+ skypilot_config=config_utils.Config.from_dict(
89
+ user_request_body.skypilot_config),
90
+ request_options=user_request_body.request_options,
91
+ at_client_side=user_request_body.at_client_side)
92
+
93
+
94
+ class _MutatedUserRequestBody(pydantic.BaseModel):
95
+ """Auxiliary model to validate and serialize a user request."""
96
+ task: Dict[str, Any]
97
+ skypilot_config: Dict[str, Any]
57
98
 
58
99
 
59
100
  @dataclasses.dataclass
60
101
  class MutatedUserRequest:
102
+ """Mutated user request."""
103
+
61
104
  task: 'sky.Task'
62
105
  skypilot_config: 'sky.Config'
63
106
 
107
+ def encode(self) -> str:
108
+ return _MutatedUserRequestBody(
109
+ task=self.task.to_yaml_config(),
110
+ skypilot_config=dict(self.skypilot_config)).model_dump_json()
111
+
112
+ @classmethod
113
+ def decode(cls, mutated_user_request_body: str) -> 'MutatedUserRequest':
114
+ mutated_user_request_body = _MutatedUserRequestBody.model_validate_json(
115
+ mutated_user_request_body)
116
+ return cls(task=sky.Task.from_yaml_config(
117
+ mutated_user_request_body.task),
118
+ skypilot_config=config_utils.Config.from_dict(
119
+ mutated_user_request_body.skypilot_config))
120
+
121
+
122
+ class PolicyInterface:
123
+ """Interface for admin-defined policy for user requests."""
124
+
125
+ @abc.abstractmethod
126
+ def apply(self, user_request: UserRequest) -> MutatedUserRequest:
127
+ """Apply the admin policy to the user request."""
128
+
129
+ def __str__(self):
130
+ return f'{self.__class__.__name__}'
131
+
64
132
 
65
133
  # pylint: disable=line-too-long
66
- class AdminPolicy:
134
+ class AdminPolicy(PolicyInterface):
67
135
  """Abstract interface of an admin-defined policy for all user requests.
68
136
 
69
137
  Admins can implement a subclass of AdminPolicy with the following signature:
@@ -104,3 +172,64 @@ class AdminPolicy:
104
172
  """
105
173
  raise NotImplementedError(
106
174
  'Your policy must implement validate_and_mutate')
175
+
176
+ def apply(self, user_request: UserRequest) -> MutatedUserRequest:
177
+ return self.validate_and_mutate(user_request)
178
+
179
+
180
+ class PolicyTemplate(PolicyInterface):
181
+ """Admin policy template that can be instantiated to create a policy."""
182
+
183
+ @abc.abstractmethod
184
+ def validate_and_mutate(self,
185
+ user_request: UserRequest) -> MutatedUserRequest:
186
+ """Validates and mutates the user request and returns mutated request.
187
+
188
+ Args:
189
+ user_request: The user request to validate and mutate.
190
+ UserRequest contains (sky.Task, sky.Config)
191
+
192
+ Returns:
193
+ MutatedUserRequest: The mutated user request.
194
+
195
+ Raises:
196
+ Exception to throw if the user request failed the validation.
197
+ """
198
+ raise NotImplementedError(
199
+ 'Your policy must implement validate_and_mutate')
200
+
201
+ def apply(self, user_request: UserRequest) -> MutatedUserRequest:
202
+ return self.validate_and_mutate(user_request)
203
+
204
+
205
+ class RestfulAdminPolicy(PolicyTemplate):
206
+ """Admin policy that calls a RESTful API for validation."""
207
+
208
+ def __init__(self, policy_url: str):
209
+ super().__init__()
210
+ self.policy_url = policy_url
211
+
212
+ def validate_and_mutate(self,
213
+ user_request: UserRequest) -> MutatedUserRequest:
214
+ try:
215
+ response = requests.post(
216
+ self.policy_url,
217
+ json=user_request.encode(),
218
+ headers={'Content-Type': 'application/json'},
219
+ # TODO(aylei): make this configurable
220
+ timeout=30)
221
+ response.raise_for_status()
222
+ except requests.exceptions.RequestException as e:
223
+ with ux_utils.print_exception_no_traceback():
224
+ raise exceptions.UserRequestRejectedByPolicy(
225
+ f'Failed to validate request with admin policy URL '
226
+ f'{self.policy_url}: {e}') from e
227
+
228
+ try:
229
+ mutated_user_request = MutatedUserRequest.decode(response.json())
230
+ except Exception as e: # pylint: disable=broad-except
231
+ with ux_utils.print_exception_no_traceback():
232
+ raise exceptions.UserRequestRejectedByPolicy(
233
+ f'Failed to decode response from admin policy URL '
234
+ f'{self.policy_url}: {e}') from e
235
+ return mutated_user_request
sky/authentication.py CHANGED
@@ -439,13 +439,7 @@ def setup_kubernetes_authentication(config: Dict[str, Any]) -> Dict[str, Any]:
439
439
  # Add the user's public key to the SkyPilot cluster.
440
440
  secret_name = clouds.Kubernetes.SKY_SSH_KEY_SECRET_NAME
441
441
  secret_field_name = clouds.Kubernetes().ssh_key_secret_field_name
442
- context = config['provider'].get(
443
- 'context', kubernetes_utils.get_current_kube_config_context_name())
444
- if context == kubernetes.in_cluster_context_name():
445
- # If the context is an in-cluster context name, we are running in a pod
446
- # with in-cluster configuration. We need to set the context to None
447
- # to use the mounted service account.
448
- context = None
442
+ context = kubernetes_utils.get_context_from_config(config['provider'])
449
443
  namespace = kubernetes_utils.get_namespace_from_config(config['provider'])
450
444
  k8s = kubernetes.kubernetes
451
445
  with open(public_key_path, 'r', encoding='utf-8') as f:
@@ -2904,14 +2904,6 @@ class CloudVmRayBackend(backends.Backend['CloudVmRayResourceHandle']):
2904
2904
  # TODO(suquark): once we have sky on PyPI, we should directly
2905
2905
  # install sky from PyPI.
2906
2906
  local_wheel_path, wheel_hash = wheel_utils.build_sky_wheel()
2907
- # The most frequent reason for the failure of a provision
2908
- # request is resource unavailability instead of rate
2909
- # limiting; to make users wait shorter, we do not make
2910
- # backoffs exponential.
2911
- backoff = common_utils.Backoff(
2912
- initial_backoff=_RETRY_UNTIL_UP_INIT_GAP_SECONDS,
2913
- max_backoff_factor=1)
2914
- attempt_cnt = 1
2915
2907
  while True:
2916
2908
  # For on-demand instances, RetryingVmProvisioner will retry
2917
2909
  # within the given region first, then optionally retry on all
@@ -2955,19 +2947,16 @@ class CloudVmRayBackend(backends.Backend['CloudVmRayResourceHandle']):
2955
2947
  error_message = str(e)
2956
2948
 
2957
2949
  if retry_until_up:
2958
- logger.error(error_message)
2959
- # Sleep and retry.
2960
- gap_seconds = backoff.current_backoff()
2961
- plural = 's' if attempt_cnt > 1 else ''
2950
+ gap_seconds = _RETRY_UNTIL_UP_INIT_GAP_SECONDS
2962
2951
  retry_message = ux_utils.retry_message(
2963
- f'Retry after {gap_seconds:.0f}s '
2964
- f'({attempt_cnt} attempt{plural}). ')
2965
- logger.info(f'\n{retry_message} '
2966
- f'{ux_utils.log_path_hint(log_path)}'
2967
- f'{colorama.Style.RESET_ALL}')
2968
- attempt_cnt += 1
2969
- time.sleep(gap_seconds)
2970
- continue
2952
+ f'Retry after {gap_seconds:.0f}s ')
2953
+ hint_message = (f'\n{retry_message} '
2954
+ f'{ux_utils.log_path_hint(log_path)}'
2955
+ f'{colorama.Style.RESET_ALL}')
2956
+ raise exceptions.ExecutionRetryableError(
2957
+ error_message,
2958
+ hint=hint_message,
2959
+ retry_wait_seconds=gap_seconds)
2971
2960
  # Clean up the cluster's entry in `sky status`.
2972
2961
  # Do not remove the stopped cluster from the global state
2973
2962
  # if failed to start.
@@ -1,5 +1,6 @@
1
1
  """Sky benchmark database, backed by sqlite."""
2
2
  import enum
3
+ import functools
3
4
  import os
4
5
  import pathlib
5
6
  import pickle
@@ -65,7 +66,24 @@ class _BenchmarkSQLiteConn(threading.local):
65
66
  self.conn.commit()
66
67
 
67
68
 
68
- _BENCHMARK_DB = _BenchmarkSQLiteConn()
69
+ _BENCHMARK_DB = None
70
+ _benchmark_db_init_lock = threading.Lock()
71
+
72
+
73
+ def _init_db(func):
74
+ """Initialize the database."""
75
+
76
+ @functools.wraps(func)
77
+ def wrapper(*args, **kwargs):
78
+ global _BENCHMARK_DB
79
+ if _BENCHMARK_DB:
80
+ return func(*args, **kwargs)
81
+ with _benchmark_db_init_lock:
82
+ if not _BENCHMARK_DB:
83
+ _BENCHMARK_DB = _BenchmarkSQLiteConn()
84
+ return func(*args, **kwargs)
85
+
86
+ return wrapper
69
87
 
70
88
 
71
89
  class BenchmarkStatus(enum.Enum):
@@ -121,9 +139,11 @@ class BenchmarkRecord(NamedTuple):
121
139
  estimated_total_seconds: Optional[float] = None
122
140
 
123
141
 
142
+ @_init_db
124
143
  def add_benchmark(benchmark_name: str, task_name: Optional[str],
125
144
  bucket_name: str) -> None:
126
145
  """Add a new benchmark."""
146
+ assert _BENCHMARK_DB is not None
127
147
  launched_at = int(time.time())
128
148
  _BENCHMARK_DB.cursor.execute(
129
149
  'INSERT INTO benchmark'
@@ -133,8 +153,10 @@ def add_benchmark(benchmark_name: str, task_name: Optional[str],
133
153
  _BENCHMARK_DB.conn.commit()
134
154
 
135
155
 
156
+ @_init_db
136
157
  def add_benchmark_result(benchmark_name: str,
137
158
  cluster_handle: 'backend_lib.ResourceHandle') -> None:
159
+ assert _BENCHMARK_DB is not None
138
160
  name = cluster_handle.cluster_name
139
161
  num_nodes = cluster_handle.launched_nodes
140
162
  resources = pickle.dumps(cluster_handle.launched_resources)
@@ -146,10 +168,12 @@ def add_benchmark_result(benchmark_name: str,
146
168
  _BENCHMARK_DB.conn.commit()
147
169
 
148
170
 
171
+ @_init_db
149
172
  def update_benchmark_result(
150
173
  benchmark_name: str, cluster_name: str,
151
174
  benchmark_status: BenchmarkStatus,
152
175
  benchmark_record: Optional[BenchmarkRecord]) -> None:
176
+ assert _BENCHMARK_DB is not None
153
177
  _BENCHMARK_DB.cursor.execute(
154
178
  'UPDATE benchmark_results SET '
155
179
  'status=(?), record=(?) WHERE benchmark=(?) AND cluster=(?)',
@@ -158,8 +182,10 @@ def update_benchmark_result(
158
182
  _BENCHMARK_DB.conn.commit()
159
183
 
160
184
 
185
+ @_init_db
161
186
  def delete_benchmark(benchmark_name: str) -> None:
162
187
  """Delete a benchmark result."""
188
+ assert _BENCHMARK_DB is not None
163
189
  _BENCHMARK_DB.cursor.execute(
164
190
  'DELETE FROM benchmark_results WHERE benchmark=(?)', (benchmark_name,))
165
191
  _BENCHMARK_DB.cursor.execute('DELETE FROM benchmark WHERE name=(?)',
@@ -167,8 +193,10 @@ def delete_benchmark(benchmark_name: str) -> None:
167
193
  _BENCHMARK_DB.conn.commit()
168
194
 
169
195
 
196
+ @_init_db
170
197
  def get_benchmark_from_name(benchmark_name: str) -> Optional[Dict[str, Any]]:
171
198
  """Get a benchmark from its name."""
199
+ assert _BENCHMARK_DB is not None
172
200
  rows = _BENCHMARK_DB.cursor.execute(
173
201
  'SELECT * FROM benchmark WHERE name=(?)', (benchmark_name,))
174
202
  for name, task, bucket, launched_at in rows:
@@ -181,8 +209,10 @@ def get_benchmark_from_name(benchmark_name: str) -> Optional[Dict[str, Any]]:
181
209
  return record
182
210
 
183
211
 
212
+ @_init_db
184
213
  def get_benchmarks() -> List[Dict[str, Any]]:
185
214
  """Get all benchmarks."""
215
+ assert _BENCHMARK_DB is not None
186
216
  rows = _BENCHMARK_DB.cursor.execute('SELECT * FROM benchmark')
187
217
  records = []
188
218
  for name, task, bucket, launched_at in rows:
@@ -196,8 +226,10 @@ def get_benchmarks() -> List[Dict[str, Any]]:
196
226
  return records
197
227
 
198
228
 
229
+ @_init_db
199
230
  def set_benchmark_bucket(bucket_name: str, bucket_type: str) -> None:
200
231
  """Save the benchmark bucket name and type."""
232
+ assert _BENCHMARK_DB is not None
201
233
  _BENCHMARK_DB.cursor.execute(
202
234
  'REPLACE INTO benchmark_config (key, value) VALUES (?, ?)',
203
235
  (_BENCHMARK_BUCKET_NAME_KEY, bucket_name))
@@ -207,8 +239,10 @@ def set_benchmark_bucket(bucket_name: str, bucket_type: str) -> None:
207
239
  _BENCHMARK_DB.conn.commit()
208
240
 
209
241
 
242
+ @_init_db
210
243
  def get_benchmark_bucket() -> Tuple[Optional[str], Optional[str]]:
211
244
  """Get the benchmark bucket name and type."""
245
+ assert _BENCHMARK_DB is not None
212
246
  rows = _BENCHMARK_DB.cursor.execute(
213
247
  'SELECT value FROM benchmark_config WHERE key=(?)',
214
248
  (_BENCHMARK_BUCKET_NAME_KEY,))
@@ -227,15 +261,19 @@ def get_benchmark_bucket() -> Tuple[Optional[str], Optional[str]]:
227
261
  return bucket_name, bucket_type
228
262
 
229
263
 
264
+ @_init_db
230
265
  def get_benchmark_clusters(benchmark_name: str) -> List[str]:
231
266
  """Get all clusters for a benchmark."""
267
+ assert _BENCHMARK_DB is not None
232
268
  rows = _BENCHMARK_DB.cursor.execute(
233
269
  'SELECT cluster FROM benchmark_results WHERE benchmark=(?)',
234
270
  (benchmark_name,))
235
271
  return [row[0] for row in rows]
236
272
 
237
273
 
274
+ @_init_db
238
275
  def get_benchmark_results(benchmark_name: str) -> List[Dict[str, Any]]:
276
+ assert _BENCHMARK_DB is not None
239
277
  rows = _BENCHMARK_DB.cursor.execute(
240
278
  'SELECT * FROM benchmark_results WHERE benchmark=(?)',
241
279
  (benchmark_name,))
sky/cli.py CHANGED
@@ -4199,7 +4199,7 @@ def jobs():
4199
4199
  type=click.IntRange(0, 1000),
4200
4200
  default=None,
4201
4201
  show_default=True,
4202
- help=('Job priority from 0 to 1000. A lower number is higher '
4202
+ help=('Job priority from 0 to 1000. A higher number is higher '
4203
4203
  'priority. Default is 500.'))
4204
4204
  @click.option(
4205
4205
  '--detach-run',
@@ -6248,13 +6248,11 @@ def api_info():
6248
6248
  name=api_server_user['name'])
6249
6249
  else:
6250
6250
  user = models.User.get_current_user()
6251
- dashboard_url = server_common.get_dashboard_url(url)
6252
- click.echo(f'Using SkyPilot API server: {url}\n'
6251
+ click.echo(f'Using SkyPilot API server and dashboard: {url}\n'
6253
6252
  f'{ux_utils.INDENT_SYMBOL}Status: {api_server_info["status"]}, '
6254
6253
  f'commit: {api_server_info["commit"]}, '
6255
6254
  f'version: {api_server_info["version"]}\n'
6256
- f'{ux_utils.INDENT_SYMBOL}User: {user.name} ({user.id})\n'
6257
- f'{ux_utils.INDENT_LAST_SYMBOL}Dashboard: {dashboard_url}')
6255
+ f'{ux_utils.INDENT_LAST_SYMBOL}User: {user.name} ({user.id})')
6258
6256
 
6259
6257
 
6260
6258
  @cli.group(cls=_NaturalOrderGroup)
sky/client/cli.py CHANGED
@@ -4199,7 +4199,7 @@ def jobs():
4199
4199
  type=click.IntRange(0, 1000),
4200
4200
  default=None,
4201
4201
  show_default=True,
4202
- help=('Job priority from 0 to 1000. A lower number is higher '
4202
+ help=('Job priority from 0 to 1000. A higher number is higher '
4203
4203
  'priority. Default is 500.'))
4204
4204
  @click.option(
4205
4205
  '--detach-run',
@@ -6248,13 +6248,11 @@ def api_info():
6248
6248
  name=api_server_user['name'])
6249
6249
  else:
6250
6250
  user = models.User.get_current_user()
6251
- dashboard_url = server_common.get_dashboard_url(url)
6252
- click.echo(f'Using SkyPilot API server: {url}\n'
6251
+ click.echo(f'Using SkyPilot API server and dashboard: {url}\n'
6253
6252
  f'{ux_utils.INDENT_SYMBOL}Status: {api_server_info["status"]}, '
6254
6253
  f'commit: {api_server_info["commit"]}, '
6255
6254
  f'version: {api_server_info["version"]}\n'
6256
- f'{ux_utils.INDENT_SYMBOL}User: {user.name} ({user.id})\n'
6257
- f'{ux_utils.INDENT_LAST_SYMBOL}Dashboard: {dashboard_url}')
6255
+ f'{ux_utils.INDENT_LAST_SYMBOL}User: {user.name} ({user.id})')
6258
6256
 
6259
6257
 
6260
6258
  @cli.group(cls=_NaturalOrderGroup)
sky/client/sdk.py CHANGED
@@ -42,10 +42,12 @@ from sky.server.requests import payloads
42
42
  from sky.server.requests import requests as requests_lib
43
43
  from sky.skylet import constants
44
44
  from sky.usage import usage_lib
45
+ from sky.utils import admin_policy_utils
45
46
  from sky.utils import annotations
46
47
  from sky.utils import cluster_utils
47
48
  from sky.utils import common
48
49
  from sky.utils import common_utils
50
+ from sky.utils import context as sky_context
49
51
  from sky.utils import dag_utils
50
52
  from sky.utils import env_options
51
53
  from sky.utils import infra_utils
@@ -355,6 +357,7 @@ def dashboard(starting_page: Optional[str] = None) -> None:
355
357
  @usage_lib.entrypoint
356
358
  @server_common.check_server_healthy_or_start
357
359
  @annotations.client_api
360
+ @sky_context.contextual
358
361
  def launch(
359
362
  task: Union['sky.Task', 'sky.Dag'],
360
363
  cluster_name: Optional[str] = None,
@@ -490,6 +493,50 @@ def launch(
490
493
  idle_minutes_to_autostop=idle_minutes_to_autostop,
491
494
  down=down,
492
495
  dryrun=dryrun)
496
+ with admin_policy_utils.apply_and_use_config_in_current_request(
497
+ dag, request_options=request_options, at_client_side=True) as dag:
498
+ return _launch(
499
+ dag,
500
+ cluster_name,
501
+ request_options,
502
+ retry_until_up,
503
+ idle_minutes_to_autostop,
504
+ dryrun,
505
+ down,
506
+ backend,
507
+ optimize_target,
508
+ no_setup,
509
+ clone_disk_from,
510
+ fast,
511
+ _need_confirmation,
512
+ _is_launched_by_jobs_controller,
513
+ _is_launched_by_sky_serve_controller,
514
+ _disable_controller_check,
515
+ )
516
+
517
+
518
+ def _launch(
519
+ dag: 'sky.Dag',
520
+ cluster_name: str,
521
+ request_options: admin_policy.RequestOptions,
522
+ retry_until_up: bool = False,
523
+ idle_minutes_to_autostop: Optional[int] = None,
524
+ dryrun: bool = False,
525
+ down: bool = False, # pylint: disable=redefined-outer-name
526
+ backend: Optional[backends.Backend] = None,
527
+ optimize_target: common.OptimizeTarget = common.OptimizeTarget.COST,
528
+ no_setup: bool = False,
529
+ clone_disk_from: Optional[str] = None,
530
+ fast: bool = False,
531
+ # Internal only:
532
+ # pylint: disable=invalid-name
533
+ _need_confirmation: bool = False,
534
+ _is_launched_by_jobs_controller: bool = False,
535
+ _is_launched_by_sky_serve_controller: bool = False,
536
+ _disable_controller_check: bool = False,
537
+ ) -> server_common.RequestId:
538
+ """Auxiliary function for launch(), refer to launch() for details."""
539
+
493
540
  validate(dag, admin_policy_request_options=request_options)
494
541
  # The flags have been applied to the task YAML and the backward
495
542
  # compatibility of admin policy has been handled. We should no longer use
@@ -1845,10 +1892,8 @@ def api_start(
1845
1892
  # Explain why current process exited
1846
1893
  logger.info('API server is already running:')
1847
1894
  api_server_url = server_common.get_server_url(host)
1848
- dashboard_url = server_common.get_dashboard_url(api_server_url)
1849
- dashboard_msg = f'Dashboard: {dashboard_url}'
1850
- logger.info(f'{ux_utils.INDENT_SYMBOL}SkyPilot API server: '
1851
- f'{api_server_url} {dashboard_msg}\n'
1895
+ logger.info(f'{ux_utils.INDENT_SYMBOL}SkyPilot API server and dashboard: '
1896
+ f'{api_server_url}\n'
1852
1897
  f'{ux_utils.INDENT_LAST_SYMBOL}'
1853
1898
  f'View API server logs at: {constants.API_SERVER_LOGS}')
1854
1899
 
sky/clouds/kubernetes.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Kubernetes."""
2
+ import os
2
3
  import re
3
4
  import subprocess
4
5
  import tempfile
@@ -106,17 +107,6 @@ class Kubernetes(clouds.Cloud):
106
107
  context = resources.region
107
108
  if context is None:
108
109
  context = kubernetes_utils.get_current_kube_config_context_name()
109
- # Features to be disabled for exec auth
110
- is_exec_auth, message = kubernetes_utils.is_kubeconfig_exec_auth(
111
- context)
112
- if is_exec_auth:
113
- assert isinstance(message, str), message
114
- # Controllers cannot spin up new pods with exec auth.
115
- unsupported_features[
116
- clouds.CloudImplementationFeatures.HOST_CONTROLLERS] = message
117
- # Pod does not have permissions to down itself with exec auth.
118
- unsupported_features[
119
- clouds.CloudImplementationFeatures.AUTODOWN] = message
120
110
  unsupported_features[clouds.CloudImplementationFeatures.STOP] = (
121
111
  'Stopping clusters is not supported on Kubernetes.')
122
112
  unsupported_features[clouds.CloudImplementationFeatures.AUTOSTOP] = (
@@ -537,20 +527,17 @@ class Kubernetes(clouds.Cloud):
537
527
  # If remote_identity is not a dict, use
538
528
  k8s_service_account_name = remote_identity
539
529
 
540
- if (k8s_service_account_name ==
541
- schemas.RemoteIdentityOptions.LOCAL_CREDENTIALS.value):
542
- # SA name doesn't matter since automounting credentials is disabled
543
- k8s_service_account_name = 'default'
544
- k8s_automount_sa_token = 'false'
545
- elif (k8s_service_account_name ==
546
- schemas.RemoteIdentityOptions.SERVICE_ACCOUNT.value):
547
- # Use the default service account
530
+ lc = schemas.RemoteIdentityOptions.LOCAL_CREDENTIALS.value
531
+ sa = schemas.RemoteIdentityOptions.SERVICE_ACCOUNT.value
532
+
533
+ if k8s_service_account_name == lc or k8s_service_account_name == sa:
534
+ # Use the default service account if remote identity is not set.
535
+ # For LOCAL_CREDENTIALS, this is for in-cluster authentication
536
+ # which needs a serviceaccount (specifically for SSH node pools
537
+ # which uses in-cluster authentication internally, and we would
538
+ # like to support exec-auth when the user is also using SSH infra)
548
539
  k8s_service_account_name = (
549
540
  kubernetes_utils.DEFAULT_SERVICE_ACCOUNT_NAME)
550
- k8s_automount_sa_token = 'true'
551
- else:
552
- # User specified a custom service account
553
- k8s_automount_sa_token = 'true'
554
541
 
555
542
  fuse_device_required = bool(resources.requires_fuse)
556
543
 
@@ -624,7 +611,7 @@ class Kubernetes(clouds.Cloud):
624
611
  'k8s_ssh_jump_name': self.SKY_SSH_JUMP_NAME,
625
612
  'k8s_ssh_jump_image': ssh_jump_image,
626
613
  'k8s_service_account_name': k8s_service_account_name,
627
- 'k8s_automount_sa_token': k8s_automount_sa_token,
614
+ 'k8s_automount_sa_token': 'true',
628
615
  'k8s_fuse_device_required': fuse_device_required,
629
616
  # Namespace to run the fusermount-server daemonset in
630
617
  'k8s_skypilot_system_namespace': _SKYPILOT_SYSTEM_NAMESPACE,
@@ -863,6 +850,10 @@ class Kubernetes(clouds.Cloud):
863
850
  f'> {kubeconfig_file}',
864
851
  shell=True,
865
852
  check=True)
853
+ if os.path.exists(kubeconfig_file):
854
+ # convert auth plugin paths (e.g.: gke-gcloud-auth-plugin)
855
+ kubeconfig_file = kubernetes_utils.format_kubeconfig_exec_auth_with_cache(kubeconfig_file) # pylint: disable=line-too-long
856
+
866
857
  # Upload kubeconfig to the default path to avoid having to set
867
858
  # KUBECONFIG in the environment.
868
859
  return {kubernetes.DEFAULT_KUBECONFIG_PATH: kubeconfig_file}
@@ -1 +1 @@
1
- <!DOCTYPE html><html><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><title>404: This page could not be found</title><meta name="next-head-count" content="3"/><link rel="preload" href="/dashboard/_next/static/css/8b1c8321d4c02372.css" as="style"/><link rel="stylesheet" href="/dashboard/_next/static/css/8b1c8321d4c02372.css" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="/dashboard/_next/static/chunks/polyfills-78c92fac7aa8fdd8.js"></script><script src="/dashboard/_next/static/chunks/webpack-65d465f948974c0d.js" defer=""></script><script src="/dashboard/_next/static/chunks/framework-87d061ee6ed71b28.js" defer=""></script><script src="/dashboard/_next/static/chunks/main-e0e2335212e72357.js" defer=""></script><script src="/dashboard/_next/static/chunks/pages/_app-5f16aba5794ee8e7.js" defer=""></script><script src="/dashboard/_next/static/chunks/pages/_error-1be831200e60c5c0.js" defer=""></script><script src="/dashboard/_next/static/xos0euNCptbGAM7_Q3Acl/_buildManifest.js" defer=""></script><script src="/dashboard/_next/static/xos0euNCptbGAM7_Q3Acl/_ssgManifest.js" defer=""></script></head><body><div id="__next"><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div style="line-height:48px"><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding-right:23px;font-size:24px;font-weight:500;vertical-align:top">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:28px">This page could not be found<!-- -->.</h2></div></div></div></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"statusCode":404}},"page":"/_error","query":{},"buildId":"xos0euNCptbGAM7_Q3Acl","assetPrefix":"/dashboard","nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}</script></body></html>
1
+ <!DOCTYPE html><html><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><meta name="next-head-count" content="2"/><link rel="preload" href="/dashboard/_next/static/css/5d71bfc09f184bab.css" as="style"/><link rel="stylesheet" href="/dashboard/_next/static/css/5d71bfc09f184bab.css" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="/dashboard/_next/static/chunks/polyfills-78c92fac7aa8fdd8.js"></script><script src="/dashboard/_next/static/chunks/webpack-208a9812ab4f61c9.js" defer=""></script><script src="/dashboard/_next/static/chunks/framework-87d061ee6ed71b28.js" defer=""></script><script src="/dashboard/_next/static/chunks/main-e0e2335212e72357.js" defer=""></script><script src="/dashboard/_next/static/chunks/pages/_app-7bbd9d39d6f9a98a.js" defer=""></script><script src="/dashboard/_next/static/chunks/pages/_error-1be831200e60c5c0.js" defer=""></script><script src="/dashboard/_next/static/zJqasksBQ3HcqMpA2wTUZ/_buildManifest.js" defer=""></script><script src="/dashboard/_next/static/zJqasksBQ3HcqMpA2wTUZ/_ssgManifest.js" defer=""></script></head><body><div id="__next"></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"statusCode":404}},"page":"/_error","query":{},"buildId":"zJqasksBQ3HcqMpA2wTUZ","assetPrefix":"/dashboard","nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}</script></body></html>
@@ -0,0 +1 @@
1
+ "use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[211],{803:function(e,r,n){n.d(r,{z:function(){return d}});var t=n(5893),l=n(7294),a=n(8426),s=n(2003),i=n(2350);let o=(0,s.j)("inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",{variants:{variant:{default:"bg-primary text-primary-foreground hover:bg-primary/90",destructive:"bg-destructive text-destructive-foreground hover:bg-destructive/90",outline:"border border-input bg-background hover:bg-accent hover:text-accent-foreground",secondary:"bg-secondary text-secondary-foreground hover:bg-secondary/80",ghost:"hover:bg-accent hover:text-accent-foreground",link:"text-primary underline-offset-4 hover:underline"},size:{default:"h-10 px-4 py-2",sm:"h-9 rounded-md px-3",lg:"h-11 rounded-md px-8",icon:"h-10 w-10"}},defaultVariants:{variant:"default",size:"default"}}),d=l.forwardRef((e,r)=>{let{className:n,variant:l,size:s,asChild:d=!1,...c}=e,u=d?a.g7:"button";return(0,t.jsx)(u,{className:(0,i.cn)(o({variant:l,size:s,className:n})),ref:r,...c})});d.displayName="Button"},7673:function(e,r,n){n.d(r,{Ol:function(){return d},Zb:function(){return o},aY:function(){return f},eW:function(){return p},ll:function(){return c}});var t=n(5893),l=n(7294),a=n(5697),s=n.n(a),i=n(2350);let o=l.forwardRef((e,r)=>{let{className:n,children:l,...a}=e;return(0,t.jsx)("div",{ref:r,className:(0,i.cn)("rounded-lg border bg-card text-card-foreground shadow-sm",n),...a,children:l})});o.displayName="Card",o.propTypes={className:s().string,children:s().node};let d=l.forwardRef((e,r)=>{let{className:n,children:l,...a}=e;return(0,t.jsx)("div",{ref:r,className:(0,i.cn)("flex flex-col space-y-1.5 p-6",n),...a,children:l})});d.displayName="CardHeader",d.propTypes={className:s().string,children:s().node};let c=l.forwardRef((e,r)=>{let{className:n,children:l,...a}=e;return(0,t.jsx)("h3",{ref:r,className:(0,i.cn)("text-2xl font-semibold leading-none tracking-tight",n),...a,children:l})});c.displayName="CardTitle",c.propTypes={className:s().string,children:s().node};let u=l.forwardRef((e,r)=>{let{className:n,children:l,...a}=e;return(0,t.jsx)("p",{ref:r,className:(0,i.cn)("text-sm text-muted-foreground",n),...a,children:l})});u.displayName="CardDescription",u.propTypes={className:s().string,children:s().node};let f=l.forwardRef((e,r)=>{let{className:n,children:l,...a}=e;return(0,t.jsx)("div",{ref:r,className:(0,i.cn)("p-6 pt-0",n),...a,children:l})});f.displayName="CardContent",f.propTypes={className:s().string,children:s().node};let p=l.forwardRef((e,r)=>{let{className:n,children:l,...a}=e;return(0,t.jsx)("div",{ref:r,className:(0,i.cn)("flex items-center p-6 pt-0",n),...a,children:l})});p.displayName="CardFooter",p.propTypes={className:s().string,children:s().node}},2003:function(e,r,n){n.d(r,{j:function(){return s}});var t=n(512);let l=e=>"boolean"==typeof e?`${e}`:0===e?"0":e,a=t.W,s=(e,r)=>n=>{var t;if((null==r?void 0:r.variants)==null)return a(e,null==n?void 0:n.class,null==n?void 0:n.className);let{variants:s,defaultVariants:i}=r,o=Object.keys(s).map(e=>{let r=null==n?void 0:n[e],t=null==i?void 0:i[e];if(null===r)return null;let a=l(r)||l(t);return s[e][a]}),d=n&&Object.entries(n).reduce((e,r)=>{let[n,t]=r;return void 0===t||(e[n]=t),e},{});return a(e,o,null==r?void 0:null===(t=r.compoundVariants)||void 0===t?void 0:t.reduce((e,r)=>{let{class:n,className:t,...l}=r;return Object.entries(l).every(e=>{let[r,n]=e;return Array.isArray(n)?n.includes({...i,...d}[r]):({...i,...d})[r]===n})?[...e,n,t]:e},[]),null==n?void 0:n.class,null==n?void 0:n.className)}}}]);
@@ -0,0 +1 @@
1
+ "use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[350],{2350:function(n,r,u){u.d(r,{cn:function(){return c}});var e=u(512),t=u(8388);function c(){for(var n=arguments.length,r=Array(n),u=0;u<n;u++)r[u]=arguments[u];return(0,t.m6)((0,e.W)(r))}}}]);