Skip to content

Commit

Permalink
Merge pull request #1218 from lsst-sqre:tickets/DM-48390
Browse files Browse the repository at this point in the history
DM-48390: Add support for disallowing notebook spawns
  • Loading branch information
rra authored Jan 15, 2025
2 parents 30d7ed3 + 2308a2b commit 035608f
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 3 deletions.
3 changes: 3 additions & 0 deletions changelog.d/20250114_155651_rra_DM_48390.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### New features

- Add a flag to notebook quotas, defaulting to true, that indicates whether the user is allowed to spawn a new lab. This is not enforced by Gafaelfawr; it will be read and acted on by [Nublado](https://nublado.lsst.io).
14 changes: 13 additions & 1 deletion docs/user-guide/helm.rst
Original file line number Diff line number Diff line change
Expand Up @@ -523,14 +523,26 @@ Here is an example:
groups:
g_developers:
notebook:
cpu: 8.0
cpu: 0.0
memory: 4.0
g_limited:
notebook:
cpu: 0.0
memory: 0.0
spawn: false
bypass:
- "g_admins"
API quotas are in requests per 15 minutes.
Notebook quotas are in CPU equivalents and GiB of memory.
If spawn is set to false, users should not be allowed to spawn a new user notebook.
Members of groups listed in ``bypass`` ignore all quota restrictions.

The above example sets an API quota for the ``datalinker`` service of 1000 requests per 15 minutes, and a default quota for user notebooks of 2.0 CPU equivalents and 4.0GiB of memory.
Users who are members of the ``g_developers`` group get an additional 4.0GiB of memory for their notebooks.
Users who are members of the ``g_limited`` group are not allowed to spawn notebooks.
(Note that the CPU and memory quota additions must be specified, even if they are zero.)
Users who are members of the ``g_admins`` group ignore all quota restrictions.

The keys for API quotas are names of services.
This is the same name the service should use in the ``config.service`` key of a ``GafaelfawrIngress`` resource (see :ref:`ingress`).
Expand Down
1 change: 1 addition & 0 deletions src/gafaelfawr/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,7 @@ def calculate_quota(self, groups: set[str]) -> Quota | None:
if notebook:
notebook.cpu += extra.notebook.cpu
notebook.memory += extra.notebook.memory
notebook.spawn &= extra.notebook.spawn
else:
notebook = extra.notebook.model_copy()
for service, quota in extra.api.items():
Expand Down
6 changes: 6 additions & 0 deletions src/gafaelfawr/models/userinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ class NotebookQuota(BaseModel):
..., title="Maximum memory use (GiB)", examples=[16.0]
)

spawn: bool = Field(
True,
title="Spawning allowed",
description="Whether the user is allowed to spawn a notebook",
)


class Quota(BaseModel):
"""Quota information for a user."""
Expand Down
5 changes: 5 additions & 0 deletions tests/data/config/github-quota.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ quota:
cpu: 8
memory: 4.0
groups:
blocked:
notebook:
cpu: 0
memory: 0
spawn: false
foo:
api:
test: 1
Expand Down
35 changes: 33 additions & 2 deletions tests/handlers/quota_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from gafaelfawr.models.userinfo import Group

from ..support.config import reconfigure
from ..support.tokens import create_session_token


@pytest.mark.asyncio
Expand All @@ -32,7 +33,7 @@ async def test_info(client: AsyncClient, factory: Factory) -> None:
"groups": [{"name": "bar", "id": 12312}],
"quota": {
"api": {"datalinker": 1000, "test": 1},
"notebook": {"cpu": 8.0, "memory": 4.0},
"notebook": {"cpu": 8.0, "memory": 4.0, "spawn": True},
},
}

Expand All @@ -50,6 +51,36 @@ async def test_info(client: AsyncClient, factory: Factory) -> None:
"groups": [{"name": "foo", "id": 12313}],
"quota": {
"api": {"datalinker": 1000, "test": 2},
"notebook": {"cpu": 8.0, "memory": 8.0},
"notebook": {"cpu": 8.0, "memory": 8.0, "spawn": True},
},
}


@pytest.mark.asyncio
async def test_no_spawn(client: AsyncClient, factory: Factory) -> None:
await reconfigure("github-quota", factory)
token_data = await create_session_token(
factory, group_names=["blocked", "bar"], scopes={"read:all"}
)
assert token_data.groups

r = await client.get(
"/auth/api/v1/user-info",
headers={"Authorization": f"bearer {token_data.token}"},
)
assert r.status_code == 200
assert r.json() == {
"username": token_data.username,
"name": token_data.name,
"email": token_data.email,
"uid": token_data.uid,
"gid": token_data.gid,
"groups": [
g.model_dump(mode="json")
for g in sorted(token_data.groups, key=lambda g: g.name)
],
"quota": {
"api": {"datalinker": 1000, "test": 1},
"notebook": {"cpu": 8.0, "memory": 4.0, "spawn": False},
},
}

0 comments on commit 035608f

Please sign in to comment.