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.
- sky/__init__.py +2 -2
- sky/admin_policy.py +134 -5
- sky/authentication.py +1 -7
- sky/backends/cloud_vm_ray_backend.py +9 -20
- sky/benchmark/benchmark_state.py +39 -1
- sky/cli.py +3 -5
- sky/client/cli.py +3 -5
- sky/client/sdk.py +49 -4
- sky/clouds/kubernetes.py +15 -24
- sky/dashboard/out/404.html +1 -1
- sky/dashboard/out/_next/static/chunks/211.692afc57e812ae1a.js +1 -0
- sky/dashboard/out/_next/static/chunks/350.9e123a4551f68b0d.js +1 -0
- sky/dashboard/out/_next/static/chunks/37-d8aebf1683522a0b.js +6 -0
- sky/dashboard/out/_next/static/chunks/42.d39e24467181b06b.js +6 -0
- sky/dashboard/out/_next/static/chunks/443.b2242d0efcdf5f47.js +1 -0
- sky/dashboard/out/_next/static/chunks/470-4d1a5dbe58a8a2b9.js +1 -0
- sky/dashboard/out/_next/static/chunks/{121-865d2bf8a3b84c6a.js → 491.b3d264269613fe09.js} +3 -3
- sky/dashboard/out/_next/static/chunks/513.211357a2914a34b2.js +1 -0
- sky/dashboard/out/_next/static/chunks/600.15a0009177e86b86.js +16 -0
- sky/dashboard/out/_next/static/chunks/616-d6128fa9e7cae6e6.js +39 -0
- sky/dashboard/out/_next/static/chunks/664-047bc03493fda379.js +1 -0
- sky/dashboard/out/_next/static/chunks/682.4dd5dc116f740b5f.js +6 -0
- sky/dashboard/out/_next/static/chunks/760-a89d354797ce7af5.js +1 -0
- sky/dashboard/out/_next/static/chunks/799-3625946b2ec2eb30.js +8 -0
- sky/dashboard/out/_next/static/chunks/804-4c9fc53aa74bc191.js +21 -0
- sky/dashboard/out/_next/static/chunks/843-6fcc4bf91ac45b39.js +11 -0
- sky/dashboard/out/_next/static/chunks/856-0776dc6ed6000c39.js +1 -0
- sky/dashboard/out/_next/static/chunks/901-b424d293275e1fd7.js +1 -0
- sky/dashboard/out/_next/static/chunks/938-ab185187a63f9cdb.js +1 -0
- sky/dashboard/out/_next/static/chunks/947-6620842ef80ae879.js +35 -0
- sky/dashboard/out/_next/static/chunks/969-20d54a9d998dc102.js +1 -0
- sky/dashboard/out/_next/static/chunks/973-c807fc34f09c7df3.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/_app-7bbd9d39d6f9a98a.js +20 -0
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-89216c616dbaa9c5.js +6 -0
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-451a14e7e755ebbc.js +6 -0
- sky/dashboard/out/_next/static/chunks/pages/clusters-e56b17fd85d0ba58.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/config-497a35a7ed49734a.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/infra/[context]-d2910be98e9227cb.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/infra-780860bcc1103945.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-b3dbf38b51cb29be.js +16 -0
- sky/dashboard/out/_next/static/chunks/pages/jobs-fe233baf3d073491.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/users-c69ffcab9d6e5269.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/workspace/new-31aa8bdcb7592635.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/workspaces/[name]-c8c2191328532b7d.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/workspaces-82e6601baa5dd280.js +1 -0
- sky/dashboard/out/_next/static/chunks/webpack-208a9812ab4f61c9.js +1 -0
- sky/dashboard/out/_next/static/css/{8b1c8321d4c02372.css → 5d71bfc09f184bab.css} +1 -1
- sky/dashboard/out/_next/static/zJqasksBQ3HcqMpA2wTUZ/_buildManifest.js +1 -0
- sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
- sky/dashboard/out/clusters/[cluster].html +1 -1
- sky/dashboard/out/clusters.html +1 -1
- sky/dashboard/out/config.html +1 -1
- sky/dashboard/out/index.html +1 -1
- sky/dashboard/out/infra/[context].html +1 -1
- sky/dashboard/out/infra.html +1 -1
- sky/dashboard/out/jobs/[job].html +1 -1
- sky/dashboard/out/jobs.html +1 -1
- sky/dashboard/out/users.html +1 -1
- sky/dashboard/out/workspace/new.html +1 -1
- sky/dashboard/out/workspaces/[name].html +1 -1
- sky/dashboard/out/workspaces.html +1 -1
- sky/exceptions.py +18 -0
- sky/global_user_state.py +181 -74
- sky/jobs/client/sdk.py +29 -21
- sky/jobs/scheduler.py +4 -5
- sky/jobs/state.py +104 -11
- sky/jobs/utils.py +5 -5
- sky/provision/kubernetes/constants.py +9 -0
- sky/provision/kubernetes/utils.py +106 -7
- sky/serve/client/sdk.py +56 -45
- sky/server/common.py +1 -5
- sky/server/requests/executor.py +50 -20
- sky/server/requests/payloads.py +3 -0
- sky/server/requests/process.py +69 -29
- sky/server/server.py +1 -0
- sky/server/stream_utils.py +111 -55
- sky/skylet/constants.py +1 -2
- sky/skylet/job_lib.py +95 -40
- sky/skypilot_config.py +99 -25
- sky/users/permission.py +34 -17
- sky/utils/admin_policy_utils.py +41 -16
- sky/utils/context.py +21 -1
- sky/utils/controller_utils.py +16 -1
- sky/utils/kubernetes/exec_kubeconfig_converter.py +19 -47
- sky/utils/schemas.py +11 -3
- {skypilot_nightly-1.0.0.dev20250609.dist-info → skypilot_nightly-1.0.0.dev20250611.dist-info}/METADATA +1 -1
- {skypilot_nightly-1.0.0.dev20250609.dist-info → skypilot_nightly-1.0.0.dev20250611.dist-info}/RECORD +92 -81
- sky/dashboard/out/_next/static/chunks/236-619ed0248fb6fdd9.js +0 -6
- sky/dashboard/out/_next/static/chunks/293-351268365226d251.js +0 -1
- sky/dashboard/out/_next/static/chunks/37-600191c5804dcae2.js +0 -6
- sky/dashboard/out/_next/static/chunks/470-680c19413b8f808b.js +0 -1
- sky/dashboard/out/_next/static/chunks/63-e2d7b1e75e67c713.js +0 -66
- sky/dashboard/out/_next/static/chunks/682-b60cfdacc15202e8.js +0 -6
- sky/dashboard/out/_next/static/chunks/843-16c7194621b2b512.js +0 -11
- sky/dashboard/out/_next/static/chunks/856-affc52adf5403a3a.js +0 -1
- sky/dashboard/out/_next/static/chunks/969-2c584e28e6b4b106.js +0 -1
- sky/dashboard/out/_next/static/chunks/973-aed916d5b02d2d63.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/_app-5f16aba5794ee8e7.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-d31688d3e52736dd.js +0 -6
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-e7d8710a9b0491e5.js +0 -6
- sky/dashboard/out/_next/static/chunks/pages/clusters-3c674e5d970e05cb.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/config-3aac7a015c6eede1.js +0 -6
- sky/dashboard/out/_next/static/chunks/pages/infra/[context]-46d2e4ad6c487260.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/infra-7013d816a2a0e76c.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-f7f0c9e156d328bc.js +0 -16
- sky/dashboard/out/_next/static/chunks/pages/jobs-87e60396c376292f.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/users-9355a0f13d1db61d.js +0 -16
- sky/dashboard/out/_next/static/chunks/pages/workspace/new-9a749cca1813bd27.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/workspaces/[name]-8eeb628e03902f1b.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/workspaces-8fbcc5ab4af316d0.js +0 -1
- sky/dashboard/out/_next/static/chunks/webpack-65d465f948974c0d.js +0 -1
- sky/dashboard/out/_next/static/xos0euNCptbGAM7_Q3Acl/_buildManifest.js +0 -1
- /sky/dashboard/out/_next/static/{xos0euNCptbGAM7_Q3Acl → zJqasksBQ3HcqMpA2wTUZ}/_ssgManifest.js +0 -0
- {skypilot_nightly-1.0.0.dev20250609.dist-info → skypilot_nightly-1.0.0.dev20250611.dist-info}/WHEEL +0 -0
- {skypilot_nightly-1.0.0.dev20250609.dist-info → skypilot_nightly-1.0.0.dev20250611.dist-info}/entry_points.txt +0 -0
- {skypilot_nightly-1.0.0.dev20250609.dist-info → skypilot_nightly-1.0.0.dev20250611.dist-info}/licenses/LICENSE +0 -0
- {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 = '
|
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.
|
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
|
16
|
+
import requests
|
17
|
+
else:
|
18
|
+
requests = adaptors_common.LazyImport('requests')
|
9
19
|
|
10
20
|
|
11
|
-
|
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']
|
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
|
-
|
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
|
-
|
2965
|
-
|
2966
|
-
|
2967
|
-
|
2968
|
-
|
2969
|
-
|
2970
|
-
|
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.
|
sky/benchmark/benchmark_state.py
CHANGED
@@ -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 =
|
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
|
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
|
-
|
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.
|
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
|
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
|
-
|
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.
|
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
|
-
|
1849
|
-
|
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
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
#
|
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':
|
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}
|
sky/dashboard/out/404.html
CHANGED
@@ -1 +1 @@
|
|
1
|
-
<!DOCTYPE html><html><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><
|
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))}}}]);
|