skypilot-nightly 1.0.0.dev20250626__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.
Files changed (106) hide show
  1. sky/__init__.py +2 -2
  2. sky/adaptors/kubernetes.py +7 -0
  3. sky/adaptors/nebius.py +2 -2
  4. sky/admin_policy.py +27 -17
  5. sky/authentication.py +12 -5
  6. sky/backends/backend_utils.py +92 -26
  7. sky/check.py +5 -2
  8. sky/client/cli/command.py +38 -6
  9. sky/client/sdk.py +217 -167
  10. sky/client/service_account_auth.py +47 -0
  11. sky/clouds/aws.py +10 -4
  12. sky/clouds/azure.py +5 -2
  13. sky/clouds/cloud.py +5 -2
  14. sky/clouds/gcp.py +31 -18
  15. sky/clouds/kubernetes.py +54 -34
  16. sky/clouds/nebius.py +8 -2
  17. sky/clouds/ssh.py +5 -2
  18. sky/clouds/utils/aws_utils.py +10 -4
  19. sky/clouds/utils/gcp_utils.py +22 -7
  20. sky/clouds/utils/oci_utils.py +62 -14
  21. sky/dashboard/out/404.html +1 -1
  22. sky/dashboard/out/_next/static/{bs6UB9V4Jq10TIZ5x-kBK → ZYLkkWSYZjJhLVsObh20y}/_buildManifest.js +1 -1
  23. sky/dashboard/out/_next/static/chunks/43-f38a531f6692f281.js +1 -0
  24. sky/dashboard/out/_next/static/chunks/601-111d06d9ded11d00.js +1 -0
  25. sky/dashboard/out/_next/static/chunks/{616-d6128fa9e7cae6e6.js → 616-50a620ac4a23deb4.js} +1 -1
  26. sky/dashboard/out/_next/static/chunks/691.fd9292250ab089af.js +21 -0
  27. sky/dashboard/out/_next/static/chunks/{785.dc2686c3c1235554.js → 785.3446c12ffdf3d188.js} +1 -1
  28. sky/dashboard/out/_next/static/chunks/871-e547295e7e21399c.js +6 -0
  29. sky/dashboard/out/_next/static/chunks/937.72796f7afe54075b.js +1 -0
  30. sky/dashboard/out/_next/static/chunks/938-0a770415b5ce4649.js +1 -0
  31. sky/dashboard/out/_next/static/chunks/982.d7bd80ed18cad4cc.js +1 -0
  32. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-21080826c6095f21.js +6 -0
  33. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-77d4816945b04793.js +6 -0
  34. sky/dashboard/out/_next/static/chunks/pages/{clusters-f119a5630a1efd61.js → clusters-65b2c90320b8afb8.js} +1 -1
  35. sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-64bdc0b2d3a44709.js +16 -0
  36. sky/dashboard/out/_next/static/chunks/pages/{jobs-0a5695ff3075d94a.js → jobs-df7407b5e37d3750.js} +1 -1
  37. sky/dashboard/out/_next/static/chunks/pages/{users-4978cbb093e141e7.js → users-d7684eaa04c4f58f.js} +1 -1
  38. sky/dashboard/out/_next/static/chunks/pages/workspaces/{[name]-cb7e720b739de53a.js → [name]-04e1b3ad4207b1e9.js} +1 -1
  39. sky/dashboard/out/_next/static/chunks/pages/{workspaces-50e230828730cfb3.js → workspaces-c470366a6179f16e.js} +1 -1
  40. sky/dashboard/out/_next/static/chunks/{webpack-08fdb9e6070127fc.js → webpack-75a3310ef922a299.js} +1 -1
  41. sky/dashboard/out/_next/static/css/605ac87514049058.css +3 -0
  42. sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
  43. sky/dashboard/out/clusters/[cluster].html +1 -1
  44. sky/dashboard/out/clusters.html +1 -1
  45. sky/dashboard/out/config.html +1 -1
  46. sky/dashboard/out/index.html +1 -1
  47. sky/dashboard/out/infra/[context].html +1 -1
  48. sky/dashboard/out/infra.html +1 -1
  49. sky/dashboard/out/jobs/[job].html +1 -1
  50. sky/dashboard/out/jobs.html +1 -1
  51. sky/dashboard/out/users.html +1 -1
  52. sky/dashboard/out/volumes.html +1 -1
  53. sky/dashboard/out/workspace/new.html +1 -1
  54. sky/dashboard/out/workspaces/[name].html +1 -1
  55. sky/dashboard/out/workspaces.html +1 -1
  56. sky/data/storage.py +8 -3
  57. sky/global_user_state.py +257 -9
  58. sky/jobs/client/sdk.py +20 -25
  59. sky/models.py +16 -0
  60. sky/provision/kubernetes/config.py +1 -1
  61. sky/provision/kubernetes/instance.py +7 -4
  62. sky/provision/kubernetes/network.py +15 -9
  63. sky/provision/kubernetes/network_utils.py +42 -23
  64. sky/provision/kubernetes/utils.py +73 -35
  65. sky/provision/nebius/utils.py +10 -4
  66. sky/resources.py +10 -4
  67. sky/serve/client/sdk.py +28 -34
  68. sky/server/common.py +51 -3
  69. sky/server/constants.py +3 -0
  70. sky/server/requests/executor.py +4 -0
  71. sky/server/requests/payloads.py +33 -0
  72. sky/server/requests/requests.py +19 -0
  73. sky/server/rest.py +6 -15
  74. sky/server/server.py +121 -6
  75. sky/skylet/constants.py +6 -0
  76. sky/skypilot_config.py +32 -4
  77. sky/users/permission.py +29 -0
  78. sky/users/server.py +384 -5
  79. sky/users/token_service.py +196 -0
  80. sky/utils/common_utils.py +4 -5
  81. sky/utils/config_utils.py +41 -0
  82. sky/utils/controller_utils.py +5 -1
  83. sky/utils/resource_checker.py +153 -0
  84. sky/utils/resources_utils.py +12 -4
  85. sky/utils/schemas.py +87 -60
  86. sky/utils/subprocess_utils.py +2 -6
  87. sky/workspaces/core.py +9 -117
  88. {skypilot_nightly-1.0.0.dev20250626.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/METADATA +1 -1
  89. {skypilot_nightly-1.0.0.dev20250626.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/RECORD +95 -92
  90. sky/dashboard/out/_next/static/chunks/43-36177d00f6956ab2.js +0 -1
  91. sky/dashboard/out/_next/static/chunks/690.55f9eed3be903f56.js +0 -16
  92. sky/dashboard/out/_next/static/chunks/871-3db673be3ee3750b.js +0 -6
  93. sky/dashboard/out/_next/static/chunks/937.3759f538f11a0953.js +0 -1
  94. sky/dashboard/out/_next/static/chunks/938-068520cc11738deb.js +0 -1
  95. sky/dashboard/out/_next/static/chunks/973-81b2d057178adb76.js +0 -1
  96. sky/dashboard/out/_next/static/chunks/982.1b61658204416b0f.js +0 -1
  97. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-aff040d7bc5d0086.js +0 -6
  98. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-8040f2483897ed0c.js +0 -6
  99. sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-e4b23128db0774cd.js +0 -16
  100. sky/dashboard/out/_next/static/css/52082cf558ec9705.css +0 -3
  101. /sky/dashboard/out/_next/static/{bs6UB9V4Jq10TIZ5x-kBK → ZYLkkWSYZjJhLVsObh20y}/_ssgManifest.js +0 -0
  102. /sky/dashboard/out/_next/static/chunks/pages/{_app-9a3ce3170d2edcec.js → _app-050a9e637b057b24.js} +0 -0
  103. {skypilot_nightly-1.0.0.dev20250626.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/WHEEL +0 -0
  104. {skypilot_nightly-1.0.0.dev20250626.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/entry_points.txt +0 -0
  105. {skypilot_nightly-1.0.0.dev20250626.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/licenses/LICENSE +0 -0
  106. {skypilot_nightly-1.0.0.dev20250626.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.get_nested(('aws', 'labels'), {})
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 = skypilot_config.get_nested(
2769
- ('azure', 'storage_account'), None)
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) -> bool:
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, name=user.name, password=user.password)
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, name=row.name, password=row.password)
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, name=row.name, password=row.password)
477
- for row in rows
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, name=row.name, password=row.password)
494
- for row in rows
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 = rest.post(
86
- f'{server_common.get_server_url()}/jobs/launch',
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 = rest.post(
146
- f'{server_common.get_server_url()}/jobs/queue',
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 = rest.post(
186
- f'{server_common.get_server_url()}/jobs/cancel',
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 = rest.post(
237
- f'{server_common.get_server_url()}/jobs/logs',
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 = rest.post(
287
- f'{server_common.get_server_url()}/jobs/download_logs',
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.get_nested(('kubernetes', 'networking'),
1151
- port_forward_mode.value)
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=kubernetes_utils.get_context_from_config(provider_config),
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=kubernetes_utils.get_namespace_from_config(
115
- provider_config),
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=kubernetes_utils.get_namespace_from_config(provider_config),
125
- context=kubernetes_utils.get_context_from_config(provider_config),
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: