Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Feature request
description: Propose a capability or a change in behaviour.
description: Propose a capability or a change in behavior.
labels: [enhancement, needs-triage]
body:
- type: textarea
Expand Down
4 changes: 2 additions & 2 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Keep the title in Conventional Commits form, e.g.

## What this changes

<!-- The behaviour before and after. Link the issue with "Fixes #123" when there is one. -->
<!-- The behavior before and after. Link the issue with "Fixes #123" when there is one. -->

## Why

Expand All @@ -28,4 +28,4 @@ Keep the title in Conventional Commits form, e.g.
- [ ] The `GShareSession` CRD changed — `make -C operator manifests` was re-run and the chart CRD updated
- [ ] The backend API surface changed — `make gen-openapi` was re-run
- [ ] User-visible strings changed — both `en` and `ko` bundles are updated
- [ ] Documentation under `docs/` reflects the new behaviour
- [ ] Documentation under `docs/` reflects the new behavior
13 changes: 13 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ updates:
open-pull-requests-limit: 5
labels: [dependencies, frontend]
groups:
# These four families declare peer ranges on each other, so a package raised on its own is
# simply uninstallable: npm refuses the tree and the PR fails at the install step before a
# test ever runs. Grouping them means one PR that can actually be evaluated.
vite:
patterns: ["vite", "@vitejs/*", "vitest", "@vitest/*", "jsdom"]
i18n:
patterns: ["i18next", "i18next-*", "react-i18next"]
eslint:
patterns: ["eslint", "eslint-*", "@typescript-eslint/*"]
tailwind:
patterns: ["tailwindcss", "@tailwindcss/*", "postcss", "postcss-*", "autoprefixer"]
# Everything else, minor and patch only. A major outside the families above still arrives
# as its own PR, which is what you want for something like React.
frontend-minor:
update-types: [minor, patch]

Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ jobs:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "20"
# vitest 5 and jsdom 30 declare Node ^22.12 as their floor; the image builds on node:26.
node-version: "22"
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install
Expand Down
20 changes: 15 additions & 5 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
# the result straight to Pages, so there is no gh-pages branch to keep in sync.
#
# Before the first run, an administrator has to set Settings → Pages → Source to "GitHub Actions"
# on this repository. The site is then served at https://boanlab.github.io/gshare/ as a project
# on this repository. The site is then served at https://boanlab.github.io/gShare/ as a project
# site, which is per-repository and does not occupy the organization's single boanlab.github.io.
# Pages paths are case-sensitive, so the base path matches the repository name exactly.
name: Docs site

on:
Expand All @@ -30,7 +31,7 @@ concurrency:

env:
SITE_URL: https://boanlab.github.io
SITE_BASE_URL: /gshare/
SITE_BASE_URL: /gShare/

jobs:
build:
Expand All @@ -54,15 +55,24 @@ jobs:
# Nothing here needs editing to preview a fork.
- name: Resolve the site address
run: |
if [ "$GITHUB_REPOSITORY" != "boanlab/gshare" ]; then
if [ "$GITHUB_REPOSITORY" != "boanlab/gShare" ]; then
owner="${GITHUB_REPOSITORY%%/*}"
name="${GITHUB_REPOSITORY##*/}"
echo "SITE_URL=https://${owner}.github.io" >> "$GITHUB_ENV"
echo "SITE_BASE_URL=/${name}/" >> "$GITHUB_ENV"
fi

- name: Build
run: npm run build
# One build per locale. Korean is served at the site root and English at /en, so the two
# locales write to different paths and a single combined build would have the second
# overwrite the first. The trees are merged into build/ for upload.
- name: Build both locales
run: |
rm -rf build build-ko build-en
npx docusaurus build --locale ko --out-dir build-ko
npx docusaurus build --locale en --out-dir build-en
mv build-ko build
mv build-en/en build/en
rmdir build-en

- uses: actions/configure-pages@v5

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-session-images.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Publish session images

# The session catalogue images (build/images) are large and change rarely, so they are published
# The session catalog images (build/images) are large and change rarely, so they are published
# on demand and on release tags — not on every push to main. The control plane seeds
# boanlab/gshare-session:<tag> references at startup (app/auth/bootstrap.py), so every tag the
# seed names must exist here or a fresh install offers images that cannot be pulled.
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ hack/cluster-info
hack/storage-csi-key
hack/storage-csi-key.pub

# UX audit reports are a point-in-time artefact of a local run, not source.
# UX audit reports are a point-in-time artifact of a local run, not source.
test/e2e/ux/out/
test/e2e/ux/out-*/

# Beta-test findings and their run logs: the same kind of point-in-time artefact.
# Beta-test findings and their run logs: the same kind of point-in-time artifact.
beta-report/

# ── Local assistant/agent tooling: workstation config, not project source. ──
Expand Down
4 changes: 2 additions & 2 deletions .trivyignore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
misconfigurations:
# ── Observability agents: they exist to read host and GPU state ──────────────────────────
# node-exporter and dcgm-exporter are upstream DaemonSets. Host namespaces, hostPath mounts
# and SYS_ADMIN are what let them export node metrics and per-process GPU utilisation; the
# scheduler's idle reaper depends on that utilisation signal. Removing the access does not
# and SYS_ADMIN are what let them export node metrics and per-process GPU utilization; the
# scheduler's idle reaper depends on that utilization signal. Removing the access does not
# harden the deployment, it disables monitoring.
- id: AVD-KSV-0005 # SYS_ADMIN capability added (DCGM profiling counters)
paths:
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ prerequisites are in [`test/e2e/README.md`](./test/e2e/README.md).
- Commit messages and pull request titles follow
[Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `docs:`,
`refactor:`, `test:`, `chore:`, with an optional scope such as `feat(scheduler):`.
- Keep a pull request to one concern. A refactor and a behaviour change in the same diff
- Keep a pull request to one concern. A refactor and a behavior change in the same diff
are two pull requests.

## Code style
Expand Down Expand Up @@ -100,7 +100,7 @@ CI fails the build if the operator's generated files are stale.
domain layer, `fakeredis` for idempotency and queue paths.
- Anything that needs real compute, real VRAM isolation, or numerical accuracy is marked
`realgpu` and only runs against a real GPU cluster from `test/e2e/`.
- New behaviour needs a test. Bug fixes need a test that fails before the fix.
- New behavior needs a test. Bug fixes need a test that fails before the fix.

```bash
make test # everything that runs without a GPU
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ sitting on it.
the card; billing stops entirely. Resuming re-acquires a card and rebuilds the pod. Volumes
are kept and are not billed.
- **Idle reaping.** Sessions that stop using their GPU can be paused automatically, driven
by real per-GPU utilisation from DCGM or the HAMi device-plugin monitor.
by real per-GPU utilization from DCGM or the HAMi device-plugin monitor.
- **Multi-tenancy with three role planes.** Organization → group → user, with `super_admin`,
`org_admin`, `group_admin`, and `member`/`guest` memberships.
- **Multi-cluster.** One control plane can drive several GPU clusters, each running its own
Expand All @@ -57,8 +57,8 @@ sitting on it.
| `frontend/` | The console: a React + TypeScript single-page app with live updates over SSE |
| `charts/gshare/` | The Helm chart for the whole platform |
| `deploy/` | Values overlays (`values/`), security baselines, monitoring, supply-chain policy, secret examples |
| `build/` | Image build contexts: catalogue session images (`images/`) and the patched HAMi scheduler (`hami-fork/`) |
| `hack/` | Developer and operator scripts: cluster bootstrap, secret generation, OpenAPI generation, catalogue seeding |
| `build/` | Image build contexts: catalog session images (`images/`) and the patched HAMi scheduler (`hami-fork/`) |
| `hack/` | Developer and operator scripts: cluster bootstrap, secret generation, OpenAPI generation, catalog seeding |
| `test/e2e/` | End-to-end assets: real-GPU suites, plus role and UX checks that need only the control plane |
| `docs/` | User manual, administrator manual, deployment and design documentation |

Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ enough detail to reproduce. A proof of concept helps but is not required.

What to expect:

- Acknowledgement within 5 working days.
- Acknowledgment within 5 working days.
- An assessment, and a fix or mitigation plan, within 30 days for confirmed issues.
- Credit in the release notes unless you ask otherwise.

Expand All @@ -45,7 +45,7 @@ Out of scope:
`third_party/`).
- Findings that require cluster-admin on the hosting cluster, which is already a full compromise.
- Denial of service caused by a tenant exhausting the credits or quota assigned to them —
that is the intended accounting behaviour.
that is the intended accounting behavior.

## Hardening expectations for operators

Expand Down
2 changes: 1 addition & 1 deletion backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The FastAPI control plane, and the authority on **money and session state**: REST,
authentication and RBAC, the credit engine (hold, consume, settle, refund), budget and
FinOps gates, CRUD for the catalogue, presets, policies, users, and organizations, the
FinOps gates, CRUD for the catalog, presets, policies, users, and organizations, the
audit trail, connect-token issuing, and **session admission**.

It pairs with the Go cluster operator in [`../operator`](../operator), which owns
Expand Down
2 changes: 1 addition & 1 deletion backend/alembic/versions/0017_org_scoped_membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def upgrade() -> None:
"fk_membership_org_id_organization", "membership", "organization", ["org_id"], ["id"]
)

# 2. Drop the (user_id, group_id) unique constraint in favour of two partial unique indexes.
# 2. Drop the (user_id, group_id) unique constraint in favor of two partial unique indexes.
op.drop_constraint("uq_membership_user_id", "membership", type_="unique")
op.create_index(
"uq_membership_user_group", "membership", ["user_id", "group_id"],
Expand Down
2 changes: 1 addition & 1 deletion backend/alembic/versions/0021_image_public.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""image: add a public column

Private images are hidden from the session wizard, while the administrative catalogue always shows
Private images are hidden from the session wizard, while the administrative catalog always shows
them. Existing rows are marked public.

Revision ID: 0021_image_public
Expand Down
2 changes: 1 addition & 1 deletion backend/alembic/versions/0022_rename_base_image_tags.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""base image registry tags: drop the -cpu and -gpu suffixes

Realigns the catalogue rows with the simplified build.sh tags (ml-ubuntu24.04-cpu becomes
Realigns the catalog rows with the simplified build.sh tags (ml-ubuntu24.04-cpu becomes
ml-ubuntu24.04, ml-cuda12.4-cudnn9-gpu becomes ml-cuda12.4-cudnn9). Seeding deduplicates on the
registry reference, so without this the next startup would create duplicate rows.

Expand Down
2 changes: 1 addition & 1 deletion backend/alembic/versions/0029_alloc_kind_resident_spot.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""allocation.kind terminology: owner becomes resident, borrow becomes spot

Aligns the code identifier (Allocation.kind) with the terminology used in the design notes. Updates
both the existing rows and the server default. Behaviour is unchanged; only the role labels move.
both the existing rows and the server default. Behavior is unchanged; only the role labels move.

Revision ID: 0029_alloc_kind_resident_spot
Revises: 0028_webhook_org
Expand Down
2 changes: 1 addition & 1 deletion backend/alembic/versions/0040_image_registry_per_owner.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Imports used to be admin-only, so one row per registry ref was enough. Members may now import a
public image for themselves, which means two users can legitimately hold the same ref. Uniqueness
therefore moves to (registry, owner_user_id); because Postgres considers NULL owners distinct, the
shared catalogue keeps its own partial unique index on registry alone.
shared catalog keeps its own partial unique index on registry alone.

Any older single-column UNIQUE on image.registry is dropped first (constraint or index — earlier
deployments differ), so this migration is safe on a database that never had one.
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/credits_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -1232,7 +1232,7 @@ async def reject_allocation_request(

# ── ledger reads ───────────────────────────────────────────────
async def _resolve_ref_names(db: AsyncSession, refs: set[str]) -> dict[str, str]:
"""Map a transaction ref to something a person recognises: the session's name, or a label."""
"""Map a transaction ref to something a person recognizes: the session's name, or a label."""
out: dict[str, str] = {}
ses_ids = {r for r in refs if r.startswith("ses_")}
if ses_ids:
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/groups_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,7 @@ async def create_project(
async def _cascade_group_users(
db: AsyncSession, group_ids: list[str], to_status: str, actor: str,
) -> int:
"""Deactivate/reactivate the users of the given groups, honouring multi-membership.
"""Deactivate/reactivate the users of the given groups, honoring multi-membership.

Deactivate: only users whose EVERY OTHER group membership is in an inactive/archived or
deleted group go inactive — someone also in an active department keeps working.
Expand Down
18 changes: 9 additions & 9 deletions backend/app/api/images_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class _Validation(DomainError):

class _ImageLimit(DomainError):
# A member may own only MAX_USER_IMAGES images; the cap keeps one user from filling the
# catalogue (and the build registry) on their own.
# catalog (and the build registry) on their own.
code, http = "image_limit_reached", 409


Expand Down Expand Up @@ -126,7 +126,7 @@ def _serialize_image(img: Image) -> dict[str, Any]:
# GPUs.
"cuda_version": (img.tags or {}).get("cuda_version"),
"gpu_ready": bool((img.tags or {}).get("gpu_ready", False)),
# A private image is hidden from the session wizard, though the admin catalogue always lists
# A private image is hidden from the session wizard, though the admin catalog always lists
# it.
"public": getattr(img, "public", True),
"owner_user_id": img.owner_user_id,
Expand Down Expand Up @@ -168,11 +168,11 @@ async def list_images(
"""List images/templates/ISOs with optional kind/q/tag/public filters. any authenticated.

``public=true`` is what the session wizard uses, listing public images only. The administrative
catalogue passes no filter and sees both public and private images.
catalog passes no filter and sees both public and private images.
"""
base = select(Image)
# Only the catalogue administrators (image.create) see every row. Everyone else is limited to
# the shared catalogue plus their own private images, whatever filter they pass — a private
# Only the catalog administrators (image.create) see every row. Everyone else is limited to
# the shared catalog plus their own private images, whatever filter they pass — a private
# image built by another user (its registry path included) is not theirs to list.
if not rbac_allows(principal, "image.create"):
base = base.where(or_(Image.public.is_(True), Image.owner_user_id == principal.user_id))
Expand Down Expand Up @@ -276,7 +276,7 @@ async def get_image(
principal: Principal = Depends(get_current_principal),
db: AsyncSession = Depends(get_db),
):
"""Image detail. any authenticated, for the shared catalogue and one's own images; another
"""Image detail. any authenticated, for the shared catalog and one's own images; another
user's private image answers 404 so its existence is not confirmed either."""
img = await db.get(Image, image_id)
if img is None:
Expand Down Expand Up @@ -359,7 +359,7 @@ async def delete_image(
principal: Principal = Depends(get_current_principal),
db: AsyncSession = Depends(get_db),
):
"""Delete a catalogue image that no session has ever used. super_admin, gated on image.create.
"""Delete a catalog image that no session has ever used. super_admin, gated on image.create.

Sessions keep a foreign key to their image for the audit trail, so an image with any session
history cannot be deleted — retire it instead by setting ``public: false``.
Expand Down Expand Up @@ -452,7 +452,7 @@ async def import_image(
only they and admins can see. The same public ref may therefore be imported by many members.

Always answers 202. Deduplication never fails the request: re-importing your own ref, or a ref
that is already in the shared catalogue, returns that row with ``existing: true`` instead of
that is already in the shared catalog, returns that row with ``existing: true`` instead of
creating a second one — the caller uses the returned ``id`` either way.
"""
if getattr(body, "registry_auth", None):
Expand All @@ -477,7 +477,7 @@ async def import_image(
raise _Conflict("registry already registered", {"registry": ref})
else:
# Users may not bring their own images any more: everything a session can run comes
# from the administrator-curated catalogue (build or import on the admin side). A
# from the administrator-curated catalog (build or import on the admin side). A
# member import used to mint a private row here; that path is closed for security.
raise Forbidden("importing images is restricted to administrators")

Expand Down
Loading