Skip to content

Commit dace451

Browse files
committed
Simplify quota system: remove config-based unlimited options
- Remove adminsUnlimited, unlimitedUsers, defaultUnlimited config options - Unlimited quota now managed exclusively via Admin UI - Auto-login mode still disables quota system entirely - Update documentation and values.yaml
1 parent cbf505b commit dace451

4 files changed

Lines changed: 28 additions & 142 deletions

File tree

docs/jupyterhub/quota-system.md

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,11 @@ custom:
3030
cpuRate: 1 # Cost per minute for CPU-only containers
3131
minimumToStart: 10 # Minimum quota required to start any container
3232
defaultQuota: 0 # Default quota granted to new users (0 = no initial allocation)
33-
defaultUnlimited: false # Grant unlimited quota to new users (overrides defaultQuota)
34-
adminsUnlimited: true # Admin users have unlimited quota
35-
unlimitedUsers: [] # List of usernames with unlimited quota
3633
```
3734
3835
### New User Default Quota
3936
40-
Two settings control quota allocation for new users:
41-
42-
#### defaultQuota
43-
44-
Controls how much quota new users receive automatically:
37+
The `defaultQuota` setting controls how much quota new users receive automatically:
4538

4639
- **Value `0` (default)**: New users start with zero quota and must be manually granted quota by an administrator
4740
- **Value `> 0`**: New users are automatically granted this amount when they first attempt to use the system
@@ -53,35 +46,20 @@ custom:
5346
defaultQuota: 100 # Automatically grant 100 quota units to new users
5447
```
5548

56-
#### defaultUnlimited
57-
58-
Grant unlimited quota to all new users:
59-
60-
- **Value `false` (default)**: New users receive quota based on `defaultQuota` setting
61-
- **Value `true`**: New users are automatically granted unlimited quota (overrides `defaultQuota`)
62-
63-
**Example configuration:**
64-
```yaml
65-
custom:
66-
quota:
67-
defaultUnlimited: true # All new users get unlimited quota
68-
```
69-
7049
#### How It Works
7150

7251
This automatic allocation happens when a new user first tries to start a container. The system will:
7352
1. Check if the user has a quota record in the database
74-
2. If not found:
75-
- If `defaultUnlimited: true`, create a record with unlimited quota status
76-
- Else if `defaultQuota > 0`, create a record with the default amount
77-
- Otherwise, create a record with zero quota
53+
2. If not found and `defaultQuota > 0`, create a record with the default amount
7854
3. Record this as an "initial_grant" transaction in the audit log
7955

80-
**Priority order:**
81-
1. Admin users (if `adminsUnlimited: true`)
82-
2. Users in `unlimitedUsers` list
83-
3. New users with `defaultUnlimited: true`
84-
4. New users with `defaultQuota` value
56+
### Unlimited Quota
57+
58+
Unlimited quota status is managed via the Admin UI. To grant unlimited quota to a user:
59+
1. Go to the Admin Panel (`/hub/admin/users`)
60+
2. Click on the user's quota value
61+
3. Enter `-1`, `∞`, or `unlimited` in the input field
62+
4. Click Save
8563

8664
### Quota Rates by Resource Type
8765

@@ -585,11 +563,7 @@ Sessions stuck for more than 8 hours are automatically cleaned up on hub startup
585563

586564
### Unlimited Quota Logic
587565

588-
A user has unlimited quota if ANY of these conditions are true:
589-
590-
1. User is admin AND `adminsUnlimited: true` in config
591-
2. Username is in `unlimitedUsers` config list (case-insensitive)
592-
3. User is marked `unlimited: true` in database
566+
A user has unlimited quota if marked `unlimited: true` in the database.
593567

594568
## Troubleshooting
595569

@@ -608,20 +582,14 @@ A user has unlimited quota if ANY of these conditions are true:
608582
python scripts/manage_users.py add-quota username --amount 500
609583
```
610584

611-
3. Or grant unlimited quota via API:
612-
```bash
613-
curl -X POST "$JUPYTERHUB_URL/admin/api/quota/username" \
614-
-H "Authorization: token $JUPYTERHUB_TOKEN" \
615-
-H "Content-Type: application/json" \
616-
-d '{"action": "set_unlimited", "unlimited": true}'
617-
```
585+
3. Or grant unlimited quota via Admin UI (click quota value and enter ``)
618586

619587
### Quota Not Being Deducted
620588

621589
**Symptom:** User's balance doesn't decrease after container use
622590

623591
**Possible causes:**
624-
1. User has unlimited quota (admin, in unlimitedUsers list, or marked unlimited)
592+
1. User has unlimited quota (set via Admin UI)
625593
2. Quota system is disabled (`custom.quota.enabled: false`)
626594
3. Session ended abnormally (cleaned up as stale)
627595

runtime/jupyterhub/files/hub/jupyterhub_config.py

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -262,27 +262,14 @@ def get_quota_rate(accelerator_type: str | None) -> int:
262262
# Config: custom.quota.minimumToStart (int)
263263
MINIMUM_QUOTA_TO_START = int(z2jh_get_config("custom.quota.minimumToStart", 10))
264264

265-
# Default quota for new users
265+
# Default quota for new users (0 = no quota granted, must be set by admin)
266266
# Config: custom.quota.defaultQuota (int)
267267
DEFAULT_QUOTA = int(z2jh_get_config("custom.quota.defaultQuota", 0))
268268

269-
# Grant unlimited quota to new users by default
270-
# Config: custom.quota.defaultUnlimited (bool)
271-
DEFAULT_UNLIMITED = bool(z2jh_get_config("custom.quota.defaultUnlimited", False))
272-
273-
# Admin users have unlimited quota (no quota deduction)
274-
# Config: custom.quota.adminsUnlimited (bool)
275-
QUOTA_ADMINS_UNLIMITED = bool(z2jh_get_config("custom.quota.adminsUnlimited", True))
276-
277-
# List of usernames with unlimited quota (case-insensitive)
278-
# Config: custom.quota.unlimitedUsers (list)
279-
QUOTA_UNLIMITED_USERS: list[str] = z2jh_get_config_list("custom.quota.unlimitedUsers", [])
280-
281269
if QUOTA_ENABLED:
282270
print(f"[CONFIG] Quota rates by accelerator: {QUOTA_RATES}")
283271
print(f"[CONFIG] Quota minimum to start: {MINIMUM_QUOTA_TO_START}")
284-
print(f"[CONFIG] Quota default for new users: {DEFAULT_QUOTA}" + (" (unlimited)" if DEFAULT_UNLIMITED else ""))
285-
print(f"[CONFIG] Quota admins unlimited: {QUOTA_ADMINS_UNLIMITED}, unlimited users: {QUOTA_UNLIMITED_USERS}")
272+
print(f"[CONFIG] Quota default for new users: {DEFAULT_QUOTA}")
286273

287274
LOCAL_ACCOUNT_PREFIX = "LocalAccount"
288275
LOCAL_ACCOUNT_PREFIX_UPPER = LOCAL_ACCOUNT_PREFIX.upper()
@@ -1028,14 +1015,11 @@ async def get(self):
10281015
from quota_manager import get_quota_manager
10291016

10301017
username = self.current_user.name
1031-
is_admin = getattr(self.current_user, "admin", False)
10321018
quota_manager = get_quota_manager()
10331019
balance = quota_manager.get_balance(username)
10341020

10351021
# Check if user has unlimited quota
1036-
has_unlimited = quota_manager.has_unlimited_quota(
1037-
username, is_admin, QUOTA_ADMINS_UNLIMITED, QUOTA_UNLIMITED_USERS
1038-
)
1022+
has_unlimited = quota_manager.is_unlimited_in_db(username)
10391023

10401024
self.set_header("Content-Type", "application/json")
10411025
self.finish(
@@ -1617,12 +1601,9 @@ async def start(self):
16171601
from quota_manager import get_quota_manager
16181602

16191603
quota_manager = get_quota_manager()
1620-
is_admin = getattr(self.user, "admin", False)
16211604

16221605
# Check if user has unlimited quota
1623-
has_unlimited = quota_manager.has_unlimited_quota(
1624-
username, is_admin, QUOTA_ADMINS_UNLIMITED, QUOTA_UNLIMITED_USERS
1625-
)
1606+
has_unlimited = quota_manager.is_unlimited_in_db(username)
16261607

16271608
if has_unlimited:
16281609
print(f"[QUOTA] User {username} has unlimited quota, skipping quota check")
@@ -1634,11 +1615,7 @@ async def start(self):
16341615
accelerator_type,
16351616
runtime_minutes,
16361617
QUOTA_RATES,
1637-
is_admin,
1638-
QUOTA_ADMINS_UNLIMITED,
1639-
QUOTA_UNLIMITED_USERS,
16401618
DEFAULT_QUOTA,
1641-
DEFAULT_UNLIMITED,
16421619
)
16431620

16441621
if not can_start:

runtime/jupyterhub/files/hub/quota_manager.py

Lines changed: 10 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -128,45 +128,22 @@ def get_balance(self, username: str) -> int:
128128
row = cursor.fetchone()
129129
return row["balance"] if row else 0
130130

131-
def ensure_user_quota(self, username: str, default_quota: int = 0, default_unlimited: bool = False) -> int:
131+
def ensure_user_quota(self, username: str, default_quota: int = 0) -> int:
132132
"""
133-
Ensure user has a quota record. If user doesn't exist:
134-
- If default_unlimited=True, grant unlimited quota
135-
- Else if default_quota > 0, grant the default quota amount
136-
Returns the user's current balance.
133+
Ensure user has a quota record. If user doesn't exist and default_quota > 0,
134+
grant the default quota amount. Returns the user's current balance.
137135
"""
138136
username = username.lower()
139137
with self._lock, self._get_connection() as conn:
140138
cursor = conn.cursor()
141-
cursor.execute("SELECT balance, unlimited FROM user_quota WHERE username = ?", (username,))
139+
cursor.execute("SELECT balance FROM user_quota WHERE username = ?", (username,))
142140
row = cursor.fetchone()
143141

144142
if row:
145143
# User already exists
146144
return row["balance"]
147145

148-
# New user - check if we should grant unlimited quota
149-
if default_unlimited:
150-
cursor.execute(
151-
"""
152-
INSERT INTO user_quota (username, balance, unlimited)
153-
VALUES (?, 0, 1)
154-
""",
155-
(username,),
156-
)
157-
cursor.execute(
158-
"""
159-
INSERT INTO quota_transactions
160-
(username, amount, transaction_type, balance_before, balance_after, description)
161-
VALUES (?, 0, 'initial_grant', 0, 0, 'Initial grant: unlimited quota')
162-
""",
163-
(username,),
164-
)
165-
conn.commit()
166-
print(f"[QUOTA] New user '{username}' granted unlimited quota")
167-
return 0
168-
169-
# New user - check if we should grant default quota
146+
# New user - grant default quota if configured
170147
if default_quota > 0:
171148
cursor.execute(
172149
"""
@@ -349,28 +326,11 @@ def set_unlimited(self, username: str, unlimited: bool, admin: str | None = None
349326
conn.commit()
350327
return unlimited
351328

352-
def has_unlimited_quota(
353-
self,
354-
username: str,
355-
is_admin: bool = False,
356-
admins_unlimited: bool = True,
357-
unlimited_users: list[str] | None = None,
358-
) -> bool:
329+
def has_unlimited_quota(self, username: str) -> bool:
359330
"""
360331
Check if user has unlimited quota.
361-
Returns True if:
362-
- User is admin and admins_unlimited is True
363-
- User is in the unlimited_users config list
364-
- User is marked as unlimited in the database
332+
Only checks the database - unlimited status is managed via admin UI.
365333
"""
366-
username = username.lower()
367-
# Check config: admin unlimited
368-
if is_admin and admins_unlimited:
369-
return True
370-
# Check config: unlimited users list
371-
if unlimited_users and username in [u.lower() for u in unlimited_users]:
372-
return True
373-
# Check database
374334
return self.is_unlimited_in_db(username)
375335

376336
def can_start_container(
@@ -379,11 +339,7 @@ def can_start_container(
379339
resource_type: str,
380340
duration_minutes: int,
381341
quota_rates: dict,
382-
is_admin: bool = False,
383-
admins_unlimited: bool = True,
384-
unlimited_users: list[str] | None = None,
385342
default_quota: int = 0,
386-
default_unlimited: bool = False,
387343
) -> tuple[bool, str, int]:
388344
"""
389345
Check if user has sufficient quota to start a container.
@@ -394,11 +350,10 @@ def can_start_container(
394350
estimated_cost = rate * duration_minutes
395351

396352
# Ensure user has a quota record (grant default if new user)
397-
# Must be called BEFORE has_unlimited_quota check to create DB record for new users
398-
balance = self.ensure_user_quota(username, default_quota, default_unlimited)
353+
balance = self.ensure_user_quota(username, default_quota)
399354

400-
# Check for unlimited quota (includes newly granted unlimited users)
401-
if self.has_unlimited_quota(username, is_admin, admins_unlimited, unlimited_users):
355+
# Check for unlimited quota
356+
if self.has_unlimited_quota(username):
402357
return True, "Unlimited quota", 0
403358

404359
if balance < estimated_cost:

runtime/values.yaml

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -96,25 +96,11 @@ custom:
9696
# Minimum quota required to start any container
9797
minimumToStart: 10
9898

99-
# Default quota for new users
99+
# Default quota for new users (0 = no initial allocation, must be set by admin)
100100
# When a new user first attempts to use the system, they will be automatically
101-
# granted this amount of quota if they don't have unlimited status
101+
# granted this amount of quota
102102
defaultQuota: 0
103103

104-
# Grant unlimited quota to new users by default
105-
# If true, new users will have unlimited quota (overrides defaultQuota)
106-
defaultUnlimited: false
107-
108-
# Admin users have unlimited quota (no quota deduction)
109-
adminsUnlimited: true
110-
111-
# List of usernames with unlimited quota (case-insensitive)
112-
unlimitedUsers: []
113-
# Example:
114-
# unlimitedUsers:
115-
# - instructor1
116-
# - researcher1
117-
118104
# Auto-refresh rules: each rule generates a K8s CronJob
119105
#
120106
# Action modes:

0 commit comments

Comments
 (0)