mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-14 13:33:00 +00:00
Merge branch 'jupyterhub:main' into main
This commit is contained in:
2
.github/workflows/test-docs.yml
vendored
2
.github/workflows/test-docs.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Validate REST API definition
|
||||
uses: char0n/swagger-editor-validate@v1.3.1
|
||||
uses: char0n/swagger-editor-validate@v1.3.2
|
||||
with:
|
||||
definition-file: docs/source/_static/rest-api.yml
|
||||
|
||||
|
@@ -183,12 +183,28 @@ as well as the admin page:
|
||||
Named servers can be accessed, created, started, stopped, and deleted
|
||||
from these pages. Activity tracking is now per-server as well.
|
||||
|
||||
The number of named servers per user can be limited by setting
|
||||
The number of named servers per user can be limited by setting a constant value:
|
||||
|
||||
```python
|
||||
c.JupyterHub.named_server_limit_per_user = 5
|
||||
```
|
||||
|
||||
or a callable/awaitable based on the handler object:
|
||||
|
||||
```python
|
||||
def named_server_limit_per_user_fn(handler):
|
||||
user = handler.current_user
|
||||
if user and user.admin:
|
||||
return 0
|
||||
return 5
|
||||
|
||||
c.JupyterHub.named_server_limit_per_user = named_server_limit_per_user_fn
|
||||
```
|
||||
|
||||
This can be useful for quota service implementations. The example above limits the number of named servers for non-admin users only.
|
||||
|
||||
If `named_server_limit_per_user` is set to `0`, no limit is enforced.
|
||||
|
||||
(classic-notebook-ui)=
|
||||
|
||||
## Switching back to classic notebook
|
||||
|
@@ -502,17 +502,19 @@ class UserServerAPIHandler(APIHandler):
|
||||
if server_name:
|
||||
if not self.allow_named_servers:
|
||||
raise web.HTTPError(400, "Named servers are not enabled.")
|
||||
if (
|
||||
self.named_server_limit_per_user > 0
|
||||
and server_name not in user.orm_spawners
|
||||
):
|
||||
|
||||
named_server_limit_per_user = (
|
||||
await self.get_current_user_named_server_limit()
|
||||
)
|
||||
|
||||
if named_server_limit_per_user > 0 and server_name not in user.orm_spawners:
|
||||
named_spawners = list(user.all_spawners(include_default=False))
|
||||
if self.named_server_limit_per_user <= len(named_spawners):
|
||||
if named_server_limit_per_user <= len(named_spawners):
|
||||
raise web.HTTPError(
|
||||
400,
|
||||
"User {} already has the maximum of {} named servers."
|
||||
" One must be deleted before a new server can be created".format(
|
||||
user_name, self.named_server_limit_per_user
|
||||
user_name, named_server_limit_per_user
|
||||
),
|
||||
)
|
||||
spawner = user.get_spawner(server_name, replace_failed=True)
|
||||
|
@@ -1150,14 +1150,27 @@ class JupyterHub(Application):
|
||||
False, help="Allow named single-user servers per user"
|
||||
).tag(config=True)
|
||||
|
||||
named_server_limit_per_user = Integer(
|
||||
0,
|
||||
named_server_limit_per_user = Union(
|
||||
[Integer(), Callable()],
|
||||
default_value=0,
|
||||
help="""
|
||||
Maximum number of concurrent named servers that can be created by a user at a time.
|
||||
|
||||
Setting this can limit the total resources a user can consume.
|
||||
|
||||
If set to 0, no limit is enforced.
|
||||
|
||||
Can be an integer or a callable/awaitable based on the handler object:
|
||||
|
||||
::
|
||||
|
||||
def named_server_limit_per_user_fn(handler):
|
||||
user = handler.current_user
|
||||
if user and user.admin:
|
||||
return 0
|
||||
return 5
|
||||
|
||||
c.JupyterHub.named_server_limit_per_user = named_server_limit_per_user_fn
|
||||
""",
|
||||
).tag(config=True)
|
||||
|
||||
|
@@ -248,6 +248,17 @@ class BaseHandler(RequestHandler):
|
||||
def authenticate_prometheus(self):
|
||||
return self.settings.get('authenticate_prometheus', True)
|
||||
|
||||
async def get_current_user_named_server_limit(self):
|
||||
"""
|
||||
Return named server limit for current user.
|
||||
"""
|
||||
named_server_limit_per_user = self.named_server_limit_per_user
|
||||
|
||||
if callable(named_server_limit_per_user):
|
||||
return await maybe_future(named_server_limit_per_user(self))
|
||||
|
||||
return named_server_limit_per_user
|
||||
|
||||
def get_auth_token(self):
|
||||
"""Get the authorization token from Authorization header"""
|
||||
auth_header = self.request.headers.get('Authorization', '')
|
||||
|
@@ -72,7 +72,7 @@ class HomeHandler(BaseHandler):
|
||||
user=user,
|
||||
url=url,
|
||||
allow_named_servers=self.allow_named_servers,
|
||||
named_server_limit_per_user=self.named_server_limit_per_user,
|
||||
named_server_limit_per_user=await self.get_current_user_named_server_limit(),
|
||||
url_path_join=url_path_join,
|
||||
# can't use user.spawners because the stop method of User pops named servers from user.spawners when they're stopped
|
||||
spawners=user.orm_user._orm_spawners,
|
||||
@@ -129,17 +129,19 @@ class SpawnHandler(BaseHandler):
|
||||
if server_name:
|
||||
if not self.allow_named_servers:
|
||||
raise web.HTTPError(400, "Named servers are not enabled.")
|
||||
if (
|
||||
self.named_server_limit_per_user > 0
|
||||
and server_name not in user.orm_spawners
|
||||
):
|
||||
|
||||
named_server_limit_per_user = (
|
||||
await self.get_current_user_named_server_limit()
|
||||
)
|
||||
|
||||
if named_server_limit_per_user > 0 and server_name not in user.orm_spawners:
|
||||
named_spawners = list(user.all_spawners(include_default=False))
|
||||
if self.named_server_limit_per_user <= len(named_spawners):
|
||||
if named_server_limit_per_user <= len(named_spawners):
|
||||
raise web.HTTPError(
|
||||
400,
|
||||
"User {} already has the maximum of {} named servers."
|
||||
" One must be deleted before a new server can be created".format(
|
||||
user.name, self.named_server_limit_per_user
|
||||
user.name, named_server_limit_per_user
|
||||
),
|
||||
)
|
||||
|
||||
@@ -458,7 +460,7 @@ class AdminHandler(BaseHandler):
|
||||
auth_state=auth_state,
|
||||
admin_access=True,
|
||||
allow_named_servers=self.allow_named_servers,
|
||||
named_server_limit_per_user=self.named_server_limit_per_user,
|
||||
named_server_limit_per_user=await self.get_current_user_named_server_limit(),
|
||||
server_version=f'{__version__} {self.version_hash}',
|
||||
api_page_limit=self.settings["api_page_default_limit"],
|
||||
base_url=self.settings["base_url"],
|
||||
|
@@ -25,6 +25,25 @@ def named_servers(app):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def named_servers_with_callable_limit(app):
|
||||
def named_server_limit_per_user_fn(handler):
|
||||
"""Limit number of named servers to `2` for non-admin users. No limit for admin users."""
|
||||
user = handler.current_user
|
||||
if user and user.admin:
|
||||
return 0
|
||||
return 2
|
||||
|
||||
with mock.patch.dict(
|
||||
app.tornado_settings,
|
||||
{
|
||||
'allow_named_servers': True,
|
||||
'named_server_limit_per_user': named_server_limit_per_user_fn,
|
||||
},
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def default_server_name(app, named_servers):
|
||||
"""configure app to use a default server name"""
|
||||
@@ -292,6 +311,57 @@ async def test_named_server_limit(app, named_servers):
|
||||
assert r.text == ''
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'username,admin',
|
||||
[
|
||||
('nonsuperfoo', False),
|
||||
('superfoo', True),
|
||||
],
|
||||
)
|
||||
async def test_named_server_limit_as_callable(
|
||||
app, named_servers_with_callable_limit, username, admin
|
||||
):
|
||||
"""Test named server limit based on `named_server_limit_per_user_fn` callable"""
|
||||
user = add_user(app.db, app, name=username, admin=admin)
|
||||
cookies = await app.login_user(username)
|
||||
|
||||
# Create 1st named server
|
||||
servername1 = 'bar-1'
|
||||
r = await api_request(
|
||||
app, 'users', username, 'servers', servername1, method='post', cookies=cookies
|
||||
)
|
||||
r.raise_for_status()
|
||||
assert r.status_code == 201
|
||||
assert r.text == ''
|
||||
|
||||
# Create 2nd named server
|
||||
servername2 = 'bar-2'
|
||||
r = await api_request(
|
||||
app, 'users', username, 'servers', servername2, method='post', cookies=cookies
|
||||
)
|
||||
r.raise_for_status()
|
||||
assert r.status_code == 201
|
||||
assert r.text == ''
|
||||
|
||||
# Create 3rd named server
|
||||
servername3 = 'bar-3'
|
||||
r = await api_request(
|
||||
app, 'users', username, 'servers', servername3, method='post', cookies=cookies
|
||||
)
|
||||
|
||||
# No named server limit for admin users as in `named_server_limit_per_user_fn` callable
|
||||
if admin:
|
||||
r.raise_for_status()
|
||||
assert r.status_code == 201
|
||||
assert r.text == ''
|
||||
else:
|
||||
assert r.status_code == 400
|
||||
assert r.json() == {
|
||||
"status": 400,
|
||||
"message": f"User {username} already has the maximum of 2 named servers. One must be deleted before a new server can be created",
|
||||
}
|
||||
|
||||
|
||||
async def test_named_server_spawn_form(app, username, named_servers):
|
||||
server_name = "myserver"
|
||||
base_url = public_url(app)
|
||||
|
Reference in New Issue
Block a user