Merge branch 'jupyterhub:main' into main

This commit is contained in:
Temidayo
2022-10-02 14:52:24 +01:00
committed by GitHub
7 changed files with 132 additions and 18 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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', '')

View File

@@ -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"],

View File

@@ -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)