skypilot-nightly 1.0.0.dev20250627__py3-none-any.whl → 1.0.0.dev20250628__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sky/__init__.py +2 -2
- sky/adaptors/kubernetes.py +7 -0
- sky/adaptors/nebius.py +2 -2
- sky/authentication.py +12 -5
- sky/backends/backend_utils.py +92 -26
- sky/check.py +5 -2
- sky/client/cli/command.py +38 -6
- sky/client/sdk.py +217 -167
- sky/client/service_account_auth.py +47 -0
- sky/clouds/aws.py +10 -4
- sky/clouds/azure.py +5 -2
- sky/clouds/cloud.py +5 -2
- sky/clouds/gcp.py +31 -18
- sky/clouds/kubernetes.py +54 -34
- sky/clouds/nebius.py +8 -2
- sky/clouds/ssh.py +5 -2
- sky/clouds/utils/aws_utils.py +10 -4
- sky/clouds/utils/gcp_utils.py +22 -7
- sky/clouds/utils/oci_utils.py +62 -14
- sky/dashboard/out/404.html +1 -1
- sky/dashboard/out/_next/static/{HudU4f4Xsy-cP51JvXSZ- → ZYLkkWSYZjJhLVsObh20y}/_buildManifest.js +1 -1
- sky/dashboard/out/_next/static/chunks/43-f38a531f6692f281.js +1 -0
- sky/dashboard/out/_next/static/chunks/601-111d06d9ded11d00.js +1 -0
- sky/dashboard/out/_next/static/chunks/{616-d6128fa9e7cae6e6.js → 616-50a620ac4a23deb4.js} +1 -1
- sky/dashboard/out/_next/static/chunks/691.fd9292250ab089af.js +21 -0
- sky/dashboard/out/_next/static/chunks/{785.dc2686c3c1235554.js → 785.3446c12ffdf3d188.js} +1 -1
- sky/dashboard/out/_next/static/chunks/871-e547295e7e21399c.js +6 -0
- sky/dashboard/out/_next/static/chunks/937.72796f7afe54075b.js +1 -0
- sky/dashboard/out/_next/static/chunks/938-0a770415b5ce4649.js +1 -0
- sky/dashboard/out/_next/static/chunks/982.d7bd80ed18cad4cc.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-21080826c6095f21.js +6 -0
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-77d4816945b04793.js +6 -0
- sky/dashboard/out/_next/static/chunks/pages/{clusters-f119a5630a1efd61.js → clusters-65b2c90320b8afb8.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-64bdc0b2d3a44709.js +16 -0
- sky/dashboard/out/_next/static/chunks/pages/{jobs-0a5695ff3075d94a.js → jobs-df7407b5e37d3750.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/{users-4978cbb093e141e7.js → users-d7684eaa04c4f58f.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/workspaces/{[name]-cb7e720b739de53a.js → [name]-04e1b3ad4207b1e9.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/{workspaces-50e230828730cfb3.js → workspaces-c470366a6179f16e.js} +1 -1
- sky/dashboard/out/_next/static/chunks/{webpack-08fdb9e6070127fc.js → webpack-75a3310ef922a299.js} +1 -1
- sky/dashboard/out/_next/static/css/605ac87514049058.css +3 -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/volumes.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/data/storage.py +8 -3
- sky/global_user_state.py +257 -9
- sky/jobs/client/sdk.py +20 -25
- sky/models.py +16 -0
- sky/provision/kubernetes/config.py +1 -1
- sky/provision/kubernetes/instance.py +7 -4
- sky/provision/kubernetes/network.py +15 -9
- sky/provision/kubernetes/network_utils.py +42 -23
- sky/provision/kubernetes/utils.py +73 -35
- sky/provision/nebius/utils.py +10 -4
- sky/resources.py +10 -4
- sky/serve/client/sdk.py +28 -34
- sky/server/common.py +51 -3
- sky/server/constants.py +3 -0
- sky/server/requests/executor.py +4 -0
- sky/server/requests/payloads.py +33 -0
- sky/server/requests/requests.py +19 -0
- sky/server/rest.py +6 -15
- sky/server/server.py +121 -6
- sky/skylet/constants.py +6 -0
- sky/skypilot_config.py +32 -4
- sky/users/permission.py +29 -0
- sky/users/server.py +384 -5
- sky/users/token_service.py +196 -0
- sky/utils/common_utils.py +4 -5
- sky/utils/config_utils.py +41 -0
- sky/utils/controller_utils.py +5 -1
- sky/utils/resource_checker.py +153 -0
- sky/utils/resources_utils.py +12 -4
- sky/utils/schemas.py +87 -60
- sky/utils/subprocess_utils.py +2 -6
- sky/workspaces/core.py +9 -117
- {skypilot_nightly-1.0.0.dev20250627.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/METADATA +1 -1
- {skypilot_nightly-1.0.0.dev20250627.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/RECORD +94 -91
- sky/dashboard/out/_next/static/chunks/43-36177d00f6956ab2.js +0 -1
- sky/dashboard/out/_next/static/chunks/690.55f9eed3be903f56.js +0 -16
- sky/dashboard/out/_next/static/chunks/871-3db673be3ee3750b.js +0 -6
- sky/dashboard/out/_next/static/chunks/937.3759f538f11a0953.js +0 -1
- sky/dashboard/out/_next/static/chunks/938-068520cc11738deb.js +0 -1
- sky/dashboard/out/_next/static/chunks/973-81b2d057178adb76.js +0 -1
- sky/dashboard/out/_next/static/chunks/982.1b61658204416b0f.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-aff040d7bc5d0086.js +0 -6
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-8040f2483897ed0c.js +0 -6
- sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-e4b23128db0774cd.js +0 -16
- sky/dashboard/out/_next/static/css/52082cf558ec9705.css +0 -3
- /sky/dashboard/out/_next/static/{HudU4f4Xsy-cP51JvXSZ- → ZYLkkWSYZjJhLVsObh20y}/_ssgManifest.js +0 -0
- /sky/dashboard/out/_next/static/chunks/pages/{_app-9a3ce3170d2edcec.js → _app-050a9e637b057b24.js} +0 -0
- {skypilot_nightly-1.0.0.dev20250627.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/WHEEL +0 -0
- {skypilot_nightly-1.0.0.dev20250627.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/entry_points.txt +0 -0
- {skypilot_nightly-1.0.0.dev20250627.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/licenses/LICENSE +0 -0
- {skypilot_nightly-1.0.0.dev20250627.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/top_level.txt +0 -0
sky/data/storage.py
CHANGED
@@ -1802,7 +1802,8 @@ class S3Store(AbstractStore):
|
|
1802
1802
|
|
1803
1803
|
# Add AWS tags configured in config.yaml to the bucket.
|
1804
1804
|
# This is useful for cost tracking and external cleanup.
|
1805
|
-
bucket_tags = skypilot_config.
|
1805
|
+
bucket_tags = skypilot_config.get_effective_region_config(
|
1806
|
+
cloud='aws', region=None, keys=('labels',), default_value={})
|
1806
1807
|
if bucket_tags:
|
1807
1808
|
s3_client.put_bucket_tagging(
|
1808
1809
|
Bucket=bucket_name,
|
@@ -2765,8 +2766,12 @@ class AzureBlobStore(AbstractStore):
|
|
2765
2766
|
# Creates new resource group and storage account or use the
|
2766
2767
|
# storage_account provided by the user through config.yaml
|
2767
2768
|
else:
|
2768
|
-
config_storage_account =
|
2769
|
-
(
|
2769
|
+
config_storage_account = (
|
2770
|
+
skypilot_config.get_effective_region_config(
|
2771
|
+
cloud='azure',
|
2772
|
+
region=None,
|
2773
|
+
keys=('storage_account',),
|
2774
|
+
default_value=None))
|
2770
2775
|
if config_storage_account is not None:
|
2771
2776
|
# using user provided storage account from config.yaml
|
2772
2777
|
storage_account_name = config_storage_account
|
sky/global_user_state.py
CHANGED
@@ -65,6 +65,7 @@ user_table = sqlalchemy.Table(
|
|
65
65
|
sqlalchemy.Column('id', sqlalchemy.Text, primary_key=True),
|
66
66
|
sqlalchemy.Column('name', sqlalchemy.Text),
|
67
67
|
sqlalchemy.Column('password', sqlalchemy.Text),
|
68
|
+
sqlalchemy.Column('created_at', sqlalchemy.Integer),
|
68
69
|
)
|
69
70
|
|
70
71
|
cluster_table = sqlalchemy.Table(
|
@@ -167,6 +168,21 @@ ssh_key_table = sqlalchemy.Table(
|
|
167
168
|
sqlalchemy.Column('ssh_private_key', sqlalchemy.Text),
|
168
169
|
)
|
169
170
|
|
171
|
+
service_account_token_table = sqlalchemy.Table(
|
172
|
+
'service_account_tokens',
|
173
|
+
Base.metadata,
|
174
|
+
sqlalchemy.Column('token_id', sqlalchemy.Text, primary_key=True),
|
175
|
+
sqlalchemy.Column('token_name', sqlalchemy.Text),
|
176
|
+
sqlalchemy.Column('token_hash', sqlalchemy.Text),
|
177
|
+
sqlalchemy.Column('created_at', sqlalchemy.Integer),
|
178
|
+
sqlalchemy.Column('last_used_at', sqlalchemy.Integer, server_default=None),
|
179
|
+
sqlalchemy.Column('expires_at', sqlalchemy.Integer, server_default=None),
|
180
|
+
sqlalchemy.Column('creator_user_hash',
|
181
|
+
sqlalchemy.Text), # Who created this token
|
182
|
+
sqlalchemy.Column('service_account_user_id',
|
183
|
+
sqlalchemy.Text), # Service account's own user ID
|
184
|
+
)
|
185
|
+
|
170
186
|
cluster_yaml_table = sqlalchemy.Table(
|
171
187
|
'cluster_yaml',
|
172
188
|
Base.metadata,
|
@@ -174,6 +190,15 @@ cluster_yaml_table = sqlalchemy.Table(
|
|
174
190
|
sqlalchemy.Column('yaml', sqlalchemy.Text),
|
175
191
|
)
|
176
192
|
|
193
|
+
system_config_table = sqlalchemy.Table(
|
194
|
+
'system_config',
|
195
|
+
Base.metadata,
|
196
|
+
sqlalchemy.Column('config_key', sqlalchemy.Text, primary_key=True),
|
197
|
+
sqlalchemy.Column('config_value', sqlalchemy.Text),
|
198
|
+
sqlalchemy.Column('created_at', sqlalchemy.Integer),
|
199
|
+
sqlalchemy.Column('updated_at', sqlalchemy.Integer),
|
200
|
+
)
|
201
|
+
|
177
202
|
|
178
203
|
def _glob_to_similar(glob_pattern):
|
179
204
|
"""Converts a glob pattern to a PostgreSQL LIKE pattern."""
|
@@ -331,6 +356,12 @@ def create_table():
|
|
331
356
|
'password',
|
332
357
|
sqlalchemy.Text(),
|
333
358
|
default_statement='DEFAULT NULL')
|
359
|
+
db_utils.add_column_to_table_sqlalchemy(
|
360
|
+
session,
|
361
|
+
'users',
|
362
|
+
'created_at',
|
363
|
+
sqlalchemy.Integer(),
|
364
|
+
default_statement='DEFAULT NULL')
|
334
365
|
|
335
366
|
db_utils.add_column_to_table_sqlalchemy(
|
336
367
|
session,
|
@@ -383,7 +414,8 @@ def _init_db(func):
|
|
383
414
|
|
384
415
|
|
385
416
|
@_init_db
|
386
|
-
def add_or_update_user(user: models.User
|
417
|
+
def add_or_update_user(user: models.User,
|
418
|
+
allow_duplicate_name: bool = True) -> bool:
|
387
419
|
"""Store the mapping from user hash to user name for display purposes.
|
388
420
|
|
389
421
|
Returns:
|
@@ -394,7 +426,18 @@ def add_or_update_user(user: models.User) -> bool:
|
|
394
426
|
if user.name is None:
|
395
427
|
return False
|
396
428
|
|
429
|
+
# Set created_at if not already set
|
430
|
+
created_at = user.created_at
|
431
|
+
if created_at is None:
|
432
|
+
created_at = int(time.time())
|
397
433
|
with orm.Session(_SQLALCHEMY_ENGINE) as session:
|
434
|
+
# Check for duplicate names if not allowed (within the same transaction)
|
435
|
+
if not allow_duplicate_name:
|
436
|
+
existing_user = session.query(user_table).filter(
|
437
|
+
user_table.c.name == user.name).first()
|
438
|
+
if existing_user is not None:
|
439
|
+
return False
|
440
|
+
|
398
441
|
if (_SQLALCHEMY_ENGINE.dialect.name ==
|
399
442
|
db_utils.SQLAlchemyDialect.SQLITE.value):
|
400
443
|
# For SQLite, use INSERT OR IGNORE followed by UPDATE to detect new
|
@@ -405,14 +448,15 @@ def add_or_update_user(user: models.User) -> bool:
|
|
405
448
|
insert_stmnt = insert_func(user_table).prefix_with(
|
406
449
|
'OR IGNORE').values(id=user.id,
|
407
450
|
name=user.name,
|
408
|
-
password=user.password
|
451
|
+
password=user.password,
|
452
|
+
created_at=created_at)
|
409
453
|
result = session.execute(insert_stmnt)
|
410
454
|
|
411
455
|
# Check if the INSERT actually inserted a row
|
412
456
|
was_inserted = result.rowcount > 0
|
413
457
|
|
414
458
|
if not was_inserted:
|
415
|
-
# User existed, so update it
|
459
|
+
# User existed, so update it (but don't update created_at)
|
416
460
|
if user.password:
|
417
461
|
session.query(user_table).filter_by(id=user.id).update({
|
418
462
|
user_table.c.name: user.name,
|
@@ -430,8 +474,12 @@ def add_or_update_user(user: models.User) -> bool:
|
|
430
474
|
# For PostgreSQL, use INSERT ... ON CONFLICT with RETURNING to
|
431
475
|
# detect insert vs update
|
432
476
|
insert_func = postgresql.insert
|
477
|
+
|
433
478
|
insert_stmnt = insert_func(user_table).values(
|
434
|
-
id=user.id,
|
479
|
+
id=user.id,
|
480
|
+
name=user.name,
|
481
|
+
password=user.password,
|
482
|
+
created_at=created_at)
|
435
483
|
|
436
484
|
# Use a sentinel in the RETURNING clause to detect insert vs update
|
437
485
|
if user.password:
|
@@ -464,7 +512,10 @@ def get_user(user_id: str) -> Optional[models.User]:
|
|
464
512
|
row = session.query(user_table).filter_by(id=user_id).first()
|
465
513
|
if row is None:
|
466
514
|
return None
|
467
|
-
return models.User(id=row.id,
|
515
|
+
return models.User(id=row.id,
|
516
|
+
name=row.name,
|
517
|
+
password=row.password,
|
518
|
+
created_at=row.created_at)
|
468
519
|
|
469
520
|
|
470
521
|
def get_user_by_name(username: str) -> List[models.User]:
|
@@ -473,8 +524,10 @@ def get_user_by_name(username: str) -> List[models.User]:
|
|
473
524
|
if len(rows) == 0:
|
474
525
|
return []
|
475
526
|
return [
|
476
|
-
models.User(id=row.id,
|
477
|
-
|
527
|
+
models.User(id=row.id,
|
528
|
+
name=row.name,
|
529
|
+
password=row.password,
|
530
|
+
created_at=row.created_at) for row in rows
|
478
531
|
]
|
479
532
|
|
480
533
|
|
@@ -490,8 +543,10 @@ def get_all_users() -> List[models.User]:
|
|
490
543
|
with orm.Session(_SQLALCHEMY_ENGINE) as session:
|
491
544
|
rows = session.query(user_table).all()
|
492
545
|
return [
|
493
|
-
models.User(id=row.id,
|
494
|
-
|
546
|
+
models.User(id=row.id,
|
547
|
+
name=row.name,
|
548
|
+
password=row.password,
|
549
|
+
created_at=row.created_at) for row in rows
|
495
550
|
]
|
496
551
|
|
497
552
|
|
@@ -1592,6 +1647,137 @@ def set_ssh_keys(user_hash: str, ssh_public_key: str, ssh_private_key: str):
|
|
1592
1647
|
session.commit()
|
1593
1648
|
|
1594
1649
|
|
1650
|
+
@_init_db
|
1651
|
+
def add_service_account_token(token_id: str,
|
1652
|
+
token_name: str,
|
1653
|
+
token_hash: str,
|
1654
|
+
creator_user_hash: str,
|
1655
|
+
service_account_user_id: str,
|
1656
|
+
expires_at: Optional[int] = None) -> None:
|
1657
|
+
"""Add a service account token to the database."""
|
1658
|
+
assert _SQLALCHEMY_ENGINE is not None
|
1659
|
+
created_at = int(time.time())
|
1660
|
+
|
1661
|
+
with orm.Session(_SQLALCHEMY_ENGINE) as session:
|
1662
|
+
if (_SQLALCHEMY_ENGINE.dialect.name ==
|
1663
|
+
db_utils.SQLAlchemyDialect.SQLITE.value):
|
1664
|
+
insert_func = sqlite.insert
|
1665
|
+
elif (_SQLALCHEMY_ENGINE.dialect.name ==
|
1666
|
+
db_utils.SQLAlchemyDialect.POSTGRESQL.value):
|
1667
|
+
insert_func = postgresql.insert
|
1668
|
+
else:
|
1669
|
+
raise ValueError('Unsupported database dialect')
|
1670
|
+
|
1671
|
+
insert_stmnt = insert_func(service_account_token_table).values(
|
1672
|
+
token_id=token_id,
|
1673
|
+
token_name=token_name,
|
1674
|
+
token_hash=token_hash,
|
1675
|
+
created_at=created_at,
|
1676
|
+
expires_at=expires_at,
|
1677
|
+
creator_user_hash=creator_user_hash,
|
1678
|
+
service_account_user_id=service_account_user_id)
|
1679
|
+
session.execute(insert_stmnt)
|
1680
|
+
session.commit()
|
1681
|
+
|
1682
|
+
|
1683
|
+
@_init_db
|
1684
|
+
def get_service_account_token(token_id: str) -> Optional[Dict[str, Any]]:
|
1685
|
+
"""Get a service account token by token_id."""
|
1686
|
+
assert _SQLALCHEMY_ENGINE is not None
|
1687
|
+
with orm.Session(_SQLALCHEMY_ENGINE) as session:
|
1688
|
+
row = session.query(service_account_token_table).filter_by(
|
1689
|
+
token_id=token_id).first()
|
1690
|
+
if row is None:
|
1691
|
+
return None
|
1692
|
+
return {
|
1693
|
+
'token_id': row.token_id,
|
1694
|
+
'token_name': row.token_name,
|
1695
|
+
'token_hash': row.token_hash,
|
1696
|
+
'created_at': row.created_at,
|
1697
|
+
'last_used_at': row.last_used_at,
|
1698
|
+
'expires_at': row.expires_at,
|
1699
|
+
'creator_user_hash': row.creator_user_hash,
|
1700
|
+
'service_account_user_id': row.service_account_user_id,
|
1701
|
+
}
|
1702
|
+
|
1703
|
+
|
1704
|
+
@_init_db
|
1705
|
+
def get_user_service_account_tokens(user_hash: str) -> List[Dict[str, Any]]:
|
1706
|
+
"""Get all service account tokens for a user (as creator)."""
|
1707
|
+
assert _SQLALCHEMY_ENGINE is not None
|
1708
|
+
with orm.Session(_SQLALCHEMY_ENGINE) as session:
|
1709
|
+
rows = session.query(service_account_token_table).filter_by(
|
1710
|
+
creator_user_hash=user_hash).all()
|
1711
|
+
return [{
|
1712
|
+
'token_id': row.token_id,
|
1713
|
+
'token_name': row.token_name,
|
1714
|
+
'token_hash': row.token_hash,
|
1715
|
+
'created_at': row.created_at,
|
1716
|
+
'last_used_at': row.last_used_at,
|
1717
|
+
'expires_at': row.expires_at,
|
1718
|
+
'creator_user_hash': row.creator_user_hash,
|
1719
|
+
'service_account_user_id': row.service_account_user_id,
|
1720
|
+
} for row in rows]
|
1721
|
+
|
1722
|
+
|
1723
|
+
@_init_db
|
1724
|
+
def update_service_account_token_last_used(token_id: str) -> None:
|
1725
|
+
"""Update the last_used_at timestamp for a service account token."""
|
1726
|
+
assert _SQLALCHEMY_ENGINE is not None
|
1727
|
+
last_used_at = int(time.time())
|
1728
|
+
|
1729
|
+
with orm.Session(_SQLALCHEMY_ENGINE) as session:
|
1730
|
+
session.query(service_account_token_table).filter_by(
|
1731
|
+
token_id=token_id).update(
|
1732
|
+
{service_account_token_table.c.last_used_at: last_used_at})
|
1733
|
+
session.commit()
|
1734
|
+
|
1735
|
+
|
1736
|
+
@_init_db
|
1737
|
+
def delete_service_account_token(token_id: str) -> bool:
|
1738
|
+
"""Delete a service account token.
|
1739
|
+
|
1740
|
+
Returns:
|
1741
|
+
True if token was found and deleted.
|
1742
|
+
"""
|
1743
|
+
assert _SQLALCHEMY_ENGINE is not None
|
1744
|
+
with orm.Session(_SQLALCHEMY_ENGINE) as session:
|
1745
|
+
result = session.query(service_account_token_table).filter_by(
|
1746
|
+
token_id=token_id).delete()
|
1747
|
+
session.commit()
|
1748
|
+
return result > 0
|
1749
|
+
|
1750
|
+
|
1751
|
+
@_init_db
|
1752
|
+
def rotate_service_account_token(token_id: str,
|
1753
|
+
new_token_hash: str,
|
1754
|
+
new_expires_at: Optional[int] = None) -> None:
|
1755
|
+
"""Rotate a service account token by updating its hash and expiration.
|
1756
|
+
|
1757
|
+
Args:
|
1758
|
+
token_id: The token ID to rotate.
|
1759
|
+
new_token_hash: The new hashed token value.
|
1760
|
+
new_expires_at: New expiration timestamp, or None for no expiration.
|
1761
|
+
"""
|
1762
|
+
assert _SQLALCHEMY_ENGINE is not None
|
1763
|
+
current_time = int(time.time())
|
1764
|
+
|
1765
|
+
with orm.Session(_SQLALCHEMY_ENGINE) as session:
|
1766
|
+
count = session.query(service_account_token_table).filter_by(
|
1767
|
+
token_id=token_id
|
1768
|
+
).update({
|
1769
|
+
service_account_token_table.c.token_hash: new_token_hash,
|
1770
|
+
service_account_token_table.c.expires_at: new_expires_at,
|
1771
|
+
service_account_token_table.c.last_used_at: None, # Reset last used
|
1772
|
+
# Update creation time
|
1773
|
+
service_account_token_table.c.created_at: current_time,
|
1774
|
+
})
|
1775
|
+
session.commit()
|
1776
|
+
|
1777
|
+
if count == 0:
|
1778
|
+
raise ValueError(f'Service account token {token_id} not found.')
|
1779
|
+
|
1780
|
+
|
1595
1781
|
@_init_db
|
1596
1782
|
def get_cluster_yaml_str(cluster_yaml_path: Optional[str]) -> Optional[str]:
|
1597
1783
|
"""Get the cluster yaml from the database or the local file system.
|
@@ -1662,3 +1848,65 @@ def remove_cluster_yaml(cluster_name: str):
|
|
1662
1848
|
session.query(cluster_yaml_table).filter_by(
|
1663
1849
|
cluster_name=cluster_name).delete()
|
1664
1850
|
session.commit()
|
1851
|
+
|
1852
|
+
|
1853
|
+
@_init_db
|
1854
|
+
def get_all_service_account_tokens() -> List[Dict[str, Any]]:
|
1855
|
+
"""Get all service account tokens across all users (for admin access)."""
|
1856
|
+
assert _SQLALCHEMY_ENGINE is not None
|
1857
|
+
with orm.Session(_SQLALCHEMY_ENGINE) as session:
|
1858
|
+
rows = session.query(service_account_token_table).all()
|
1859
|
+
return [{
|
1860
|
+
'token_id': row.token_id,
|
1861
|
+
'token_name': row.token_name,
|
1862
|
+
'token_hash': row.token_hash,
|
1863
|
+
'created_at': row.created_at,
|
1864
|
+
'last_used_at': row.last_used_at,
|
1865
|
+
'expires_at': row.expires_at,
|
1866
|
+
'creator_user_hash': row.creator_user_hash,
|
1867
|
+
'service_account_user_id': row.service_account_user_id,
|
1868
|
+
} for row in rows]
|
1869
|
+
|
1870
|
+
|
1871
|
+
@_init_db
|
1872
|
+
def get_system_config(config_key: str) -> Optional[str]:
|
1873
|
+
"""Get a system configuration value by key."""
|
1874
|
+
assert _SQLALCHEMY_ENGINE is not None
|
1875
|
+
with orm.Session(_SQLALCHEMY_ENGINE) as session:
|
1876
|
+
row = session.query(system_config_table).filter_by(
|
1877
|
+
config_key=config_key).first()
|
1878
|
+
if row is None:
|
1879
|
+
return None
|
1880
|
+
return row.config_value
|
1881
|
+
|
1882
|
+
|
1883
|
+
@_init_db
|
1884
|
+
def set_system_config(config_key: str, config_value: str) -> None:
|
1885
|
+
"""Set a system configuration value."""
|
1886
|
+
assert _SQLALCHEMY_ENGINE is not None
|
1887
|
+
current_time = int(time.time())
|
1888
|
+
|
1889
|
+
with orm.Session(_SQLALCHEMY_ENGINE) as session:
|
1890
|
+
if (_SQLALCHEMY_ENGINE.dialect.name ==
|
1891
|
+
db_utils.SQLAlchemyDialect.SQLITE.value):
|
1892
|
+
insert_func = sqlite.insert
|
1893
|
+
elif (_SQLALCHEMY_ENGINE.dialect.name ==
|
1894
|
+
db_utils.SQLAlchemyDialect.POSTGRESQL.value):
|
1895
|
+
insert_func = postgresql.insert
|
1896
|
+
else:
|
1897
|
+
raise ValueError('Unsupported database dialect')
|
1898
|
+
|
1899
|
+
insert_stmnt = insert_func(system_config_table).values(
|
1900
|
+
config_key=config_key,
|
1901
|
+
config_value=config_value,
|
1902
|
+
created_at=current_time,
|
1903
|
+
updated_at=current_time)
|
1904
|
+
|
1905
|
+
upsert_stmnt = insert_stmnt.on_conflict_do_update(
|
1906
|
+
index_elements=[system_config_table.c.config_key],
|
1907
|
+
set_={
|
1908
|
+
system_config_table.c.config_value: config_value,
|
1909
|
+
system_config_table.c.updated_at: current_time,
|
1910
|
+
})
|
1911
|
+
session.execute(upsert_stmnt)
|
1912
|
+
session.commit()
|
sky/jobs/client/sdk.py
CHANGED
@@ -82,12 +82,11 @@ def launch(
|
|
82
82
|
task=dag_str,
|
83
83
|
name=name,
|
84
84
|
)
|
85
|
-
response =
|
86
|
-
|
85
|
+
response = server_common.make_authenticated_request(
|
86
|
+
'POST',
|
87
|
+
'/jobs/launch',
|
87
88
|
json=json.loads(body.model_dump_json()),
|
88
|
-
timeout=(5, None)
|
89
|
-
cookies=server_common.get_api_cookie_jar(),
|
90
|
-
)
|
89
|
+
timeout=(5, None))
|
91
90
|
return server_common.get_request_id(response)
|
92
91
|
|
93
92
|
|
@@ -142,12 +141,11 @@ def queue(refresh: bool,
|
|
142
141
|
all_users=all_users,
|
143
142
|
job_ids=job_ids,
|
144
143
|
)
|
145
|
-
response =
|
146
|
-
|
144
|
+
response = server_common.make_authenticated_request(
|
145
|
+
'POST',
|
146
|
+
'/jobs/queue',
|
147
147
|
json=json.loads(body.model_dump_json()),
|
148
|
-
timeout=(5, None)
|
149
|
-
cookies=server_common.get_api_cookie_jar(),
|
150
|
-
)
|
148
|
+
timeout=(5, None))
|
151
149
|
return server_common.get_request_id(response=response)
|
152
150
|
|
153
151
|
|
@@ -182,12 +180,11 @@ def cancel(
|
|
182
180
|
all=all,
|
183
181
|
all_users=all_users,
|
184
182
|
)
|
185
|
-
response =
|
186
|
-
|
183
|
+
response = server_common.make_authenticated_request(
|
184
|
+
'POST',
|
185
|
+
'/jobs/cancel',
|
187
186
|
json=json.loads(body.model_dump_json()),
|
188
|
-
timeout=(5, None)
|
189
|
-
cookies=server_common.get_api_cookie_jar(),
|
190
|
-
)
|
187
|
+
timeout=(5, None))
|
191
188
|
return server_common.get_request_id(response=response)
|
192
189
|
|
193
190
|
|
@@ -233,13 +230,12 @@ def tail_logs(name: Optional[str] = None,
|
|
233
230
|
refresh=refresh,
|
234
231
|
tail=tail,
|
235
232
|
)
|
236
|
-
response =
|
237
|
-
|
233
|
+
response = server_common.make_authenticated_request(
|
234
|
+
'POST',
|
235
|
+
'/jobs/logs',
|
238
236
|
json=json.loads(body.model_dump_json()),
|
239
237
|
stream=True,
|
240
|
-
timeout=(5, None)
|
241
|
-
cookies=server_common.get_api_cookie_jar(),
|
242
|
-
)
|
238
|
+
timeout=(5, None))
|
243
239
|
request_id = server_common.get_request_id(response)
|
244
240
|
# Log request is idempotent when tail is 0, thus can resume previous
|
245
241
|
# streaming point on retry.
|
@@ -283,12 +279,11 @@ def download_logs(
|
|
283
279
|
controller=controller,
|
284
280
|
local_dir=local_dir,
|
285
281
|
)
|
286
|
-
response =
|
287
|
-
|
282
|
+
response = server_common.make_authenticated_request(
|
283
|
+
'POST',
|
284
|
+
'/jobs/download_logs',
|
288
285
|
json=json.loads(body.model_dump_json()),
|
289
|
-
timeout=(5, None)
|
290
|
-
cookies=server_common.get_api_cookie_jar(),
|
291
|
-
)
|
286
|
+
timeout=(5, None))
|
292
287
|
job_id_remote_path_dict = sdk.stream_and_get(
|
293
288
|
server_common.get_request_id(response))
|
294
289
|
remote2local_path_dict = client_common.download_logs_from_api_server(
|
sky/models.py
CHANGED
@@ -20,6 +20,18 @@ class User:
|
|
20
20
|
# Display name of the user
|
21
21
|
name: Optional[str] = None
|
22
22
|
password: Optional[str] = None
|
23
|
+
created_at: Optional[int] = None
|
24
|
+
|
25
|
+
def __init__(
|
26
|
+
self,
|
27
|
+
id: str, # pylint: disable=redefined-builtin
|
28
|
+
name: Optional[str] = None,
|
29
|
+
password: Optional[str] = None,
|
30
|
+
created_at: Optional[int] = None):
|
31
|
+
self.id = id.strip().lower()
|
32
|
+
self.name = name
|
33
|
+
self.password = password
|
34
|
+
self.created_at = created_at
|
23
35
|
|
24
36
|
def to_dict(self) -> Dict[str, Any]:
|
25
37
|
return {'id': self.id, 'name': self.name}
|
@@ -37,6 +49,10 @@ class User:
|
|
37
49
|
user_hash = common_utils.get_user_hash()
|
38
50
|
return User(id=user_hash, name=user_name)
|
39
51
|
|
52
|
+
def is_service_account(self) -> bool:
|
53
|
+
"""Check if the user is a service account."""
|
54
|
+
return self.id.lower().startswith('sa-')
|
55
|
+
|
40
56
|
|
41
57
|
RealtimeGpuAvailability = collections.namedtuple(
|
42
58
|
'RealtimeGpuAvailability', ['gpu', 'counts', 'capacity', 'available'])
|
@@ -35,7 +35,7 @@ def bootstrap_instances(
|
|
35
35
|
_configure_services(namespace, context, config.provider_config)
|
36
36
|
|
37
37
|
networking_mode = network_utils.get_networking_mode(
|
38
|
-
config.provider_config.get('networking_mode'))
|
38
|
+
config.provider_config.get('networking_mode'), context)
|
39
39
|
if networking_mode == kubernetes_enums.KubernetesNetworkingMode.NODEPORT:
|
40
40
|
config = _configure_ssh_jump(namespace, context, config)
|
41
41
|
|
@@ -941,7 +941,7 @@ def _create_pods(region: str, cluster_name_on_cloud: str,
|
|
941
941
|
head_pod_name = pod.metadata.name
|
942
942
|
|
943
943
|
networking_mode = network_utils.get_networking_mode(
|
944
|
-
config.provider_config.get('networking_mode'))
|
944
|
+
config.provider_config.get('networking_mode'), context)
|
945
945
|
if networking_mode == kubernetes_enums.KubernetesNetworkingMode.NODEPORT:
|
946
946
|
# Adding the jump pod to the new_nodes list as well so it can be
|
947
947
|
# checked if it's scheduled and running along with other pods.
|
@@ -1102,7 +1102,7 @@ def terminate_instances(
|
|
1102
1102
|
|
1103
1103
|
# Clean up the SSH jump pod if in use
|
1104
1104
|
networking_mode = network_utils.get_networking_mode(
|
1105
|
-
provider_config.get('networking_mode'))
|
1105
|
+
provider_config.get('networking_mode'), context)
|
1106
1106
|
if networking_mode == kubernetes_enums.KubernetesNetworkingMode.NODEPORT:
|
1107
1107
|
pod_name = list(pods.keys())[0]
|
1108
1108
|
try:
|
@@ -1147,8 +1147,11 @@ def get_cluster_info(
|
|
1147
1147
|
head_pod_name = None
|
1148
1148
|
|
1149
1149
|
port_forward_mode = kubernetes_enums.KubernetesNetworkingMode.PORTFORWARD
|
1150
|
-
network_mode_str = skypilot_config.
|
1151
|
-
|
1150
|
+
network_mode_str = skypilot_config.get_effective_region_config(
|
1151
|
+
cloud='kubernetes',
|
1152
|
+
region=context,
|
1153
|
+
keys=('networking_mode',),
|
1154
|
+
default_value=port_forward_mode.value)
|
1152
1155
|
network_mode = kubernetes_enums.KubernetesNetworkingMode.from_str(
|
1153
1156
|
network_mode_str)
|
1154
1157
|
external_ip = kubernetes_utils.get_external_ip(network_mode, context)
|
@@ -22,8 +22,9 @@ def open_ports(
|
|
22
22
|
) -> None:
|
23
23
|
"""See sky/provision/__init__.py"""
|
24
24
|
assert provider_config is not None, 'provider_config is required'
|
25
|
+
context = kubernetes_utils.get_context_from_config(provider_config)
|
25
26
|
port_mode = network_utils.get_port_mode(
|
26
|
-
provider_config.get('port_mode', None))
|
27
|
+
provider_config.get('port_mode', None), context)
|
27
28
|
ports = list(port_ranges_to_set(ports))
|
28
29
|
if port_mode == kubernetes_enums.KubernetesPortMode.LOADBALANCER:
|
29
30
|
_open_ports_using_loadbalancer(
|
@@ -46,8 +47,10 @@ def _open_ports_using_loadbalancer(
|
|
46
47
|
) -> None:
|
47
48
|
service_name = _LOADBALANCER_SERVICE_NAME.format(
|
48
49
|
cluster_name_on_cloud=cluster_name_on_cloud)
|
50
|
+
context = kubernetes_utils.get_context_from_config(provider_config)
|
49
51
|
content = network_utils.fill_loadbalancer_template(
|
50
52
|
namespace=provider_config.get('namespace', 'default'),
|
53
|
+
context=context,
|
51
54
|
service_name=service_name,
|
52
55
|
ports=ports,
|
53
56
|
selector_key='skypilot-cluster',
|
@@ -59,7 +62,7 @@ def _open_ports_using_loadbalancer(
|
|
59
62
|
|
60
63
|
network_utils.create_or_replace_namespaced_service(
|
61
64
|
namespace=kubernetes_utils.get_namespace_from_config(provider_config),
|
62
|
-
context=
|
65
|
+
context=context,
|
63
66
|
service_name=service_name,
|
64
67
|
service_spec=content['service_spec'])
|
65
68
|
|
@@ -70,6 +73,7 @@ def _open_ports_using_ingress(
|
|
70
73
|
provider_config: Dict[str, Any],
|
71
74
|
) -> None:
|
72
75
|
context = kubernetes_utils.get_context_from_config(provider_config)
|
76
|
+
namespace = kubernetes_utils.get_namespace_from_config(provider_config)
|
73
77
|
# Check if an ingress controller exists
|
74
78
|
if not network_utils.ingress_controller_exists(context):
|
75
79
|
raise Exception(
|
@@ -100,6 +104,7 @@ def _open_ports_using_ingress(
|
|
100
104
|
# multiple rules.
|
101
105
|
content = network_utils.fill_ingress_template(
|
102
106
|
namespace=provider_config.get('namespace', 'default'),
|
107
|
+
context=context,
|
103
108
|
service_details=service_details,
|
104
109
|
ingress_name=f'{cluster_name_on_cloud}-skypilot-ingress',
|
105
110
|
selector_key='skypilot-cluster',
|
@@ -111,9 +116,8 @@ def _open_ports_using_ingress(
|
|
111
116
|
# Update metadata from config
|
112
117
|
kubernetes_utils.merge_custom_metadata(service_spec['metadata'])
|
113
118
|
network_utils.create_or_replace_namespaced_service(
|
114
|
-
namespace=
|
115
|
-
|
116
|
-
context=kubernetes_utils.get_context_from_config(provider_config),
|
119
|
+
namespace=namespace,
|
120
|
+
context=context,
|
117
121
|
service_name=service_name,
|
118
122
|
service_spec=service_spec,
|
119
123
|
)
|
@@ -121,8 +125,8 @@ def _open_ports_using_ingress(
|
|
121
125
|
kubernetes_utils.merge_custom_metadata(content['ingress_spec']['metadata'])
|
122
126
|
# Create or update the single ingress for all services
|
123
127
|
network_utils.create_or_replace_namespaced_ingress(
|
124
|
-
namespace=
|
125
|
-
context=
|
128
|
+
namespace=namespace,
|
129
|
+
context=context,
|
126
130
|
ingress_name=f'{cluster_name_on_cloud}-skypilot-ingress',
|
127
131
|
ingress_spec=content['ingress_spec'],
|
128
132
|
)
|
@@ -135,8 +139,9 @@ def cleanup_ports(
|
|
135
139
|
) -> None:
|
136
140
|
"""See sky/provision/__init__.py"""
|
137
141
|
assert provider_config is not None, 'provider_config is required'
|
142
|
+
context = kubernetes_utils.get_context_from_config(provider_config)
|
138
143
|
port_mode = network_utils.get_port_mode(
|
139
|
-
provider_config.get('port_mode', None))
|
144
|
+
provider_config.get('port_mode', None), context)
|
140
145
|
ports = list(port_ranges_to_set(ports))
|
141
146
|
if port_mode == kubernetes_enums.KubernetesPortMode.LOADBALANCER:
|
142
147
|
_cleanup_ports_for_loadbalancer(
|
@@ -202,8 +207,9 @@ def query_ports(
|
|
202
207
|
"""See sky/provision/__init__.py"""
|
203
208
|
del head_ip # unused
|
204
209
|
assert provider_config is not None, 'provider_config is required'
|
210
|
+
context = kubernetes_utils.get_context_from_config(provider_config)
|
205
211
|
port_mode = network_utils.get_port_mode(
|
206
|
-
provider_config.get('port_mode', None))
|
212
|
+
provider_config.get('port_mode', None), context)
|
207
213
|
ports = list(port_ranges_to_set(ports))
|
208
214
|
|
209
215
|
try:
|