This layer provides a Kubernetes-native control plane for collaborative, data-driven workspaces.
It combines a FastAPI backend (workspace_api) with a Quasar/Vue frontend (workspace_ui) to deliver a seamless management layer on top of Kubernetes Custom Resources.
At its core, the Workspace API & UI Layer orchestrates three pillars:
- Memberships — define who belongs to a workspace and what role they hold.
- Storage — provision buckets, attach credentials, and manage access grants between workspaces.
- Interactive Sessions — track and control whether interactive workspace sessions are running (always-on) or can be started (on-demand), respectively.
By building on Kubernetes CRDs (Storage, Datalab), the API exposes a clean HTTP/JSON interface and an optional single-page UI to manage these resources without needing to interact directly with kubectl. This makes it equally suited for operators (who want Kubernetes-level control) and end users (who just need to join a workspace, get storage, and start analyzing data).
Kubernetes-native: The Workspace API sits on top of two CRDs — Storage and Datalab — and reads and patches them to present a unified “Workspace” view (including storage, memberships, and session state). It applies changes through standard REST calls, simplifying access and abstracting away the low-level details of the CRDs and Kubernetes.
See: Storage CRD · Datalab CRD
As the Workspace API runs directly on Kubernetes, the ServiceAccount executing it requires minimal RBAC (Role-Based Access Control) permissions to operate on resources such as secrets, storages, and datalabs.
These permissions allow the service to list, watch, and modify Custom Resources within its namespace and to read their CRD definitions.
Both a Role and ClusterRole are automatically provisioned through the Helm chart.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["pkg.internal"]
resources: ["storages", "datalabs"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
rules:
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get"]
resourceNames: ["storages.pkg.internal", "datalabs.pkg.internal"]These permissions enable the API to synchronize resource state and discover CRD schemas while maintaining namespace isolation and least privilege.
There are three modes that define how the API initializes the default Datalab session for a newly created team:
-
Disabled (
SESSION_MODE=off) — The API creates theDatalabresource without declaring a default session. The workspace is primarily used to provision storage buckets and manage bucket access. Operators can still manage sessions directly on the correspondingDatalabCustom Resource. -
Always On (
SESSION_MODE=on) — The API declares the default Datalab session with statestartedduring workspace provisioning. Operators can manually patch theDatalabresource in the cluster to stop or restart the session if required. -
On Demand (
SESSION_MODE=auto) — The API declares the default Datalab session with statestopped. The Workspace UI exposes a Datalab link that starts the session by patching it tostartedwhen a team needs access. Operators can define external policies for automatic shutdowns of sessions (for example, every day at 8 p.m. or every Friday night). When a team needs access again, they can relaunch the session via the Datalab link in the Workspace UI.
Note:
At the moment, there is only one (“default”) session per team. Operators can manually start additional sessions via the corresponding Datalab Custom Resource in the cluster, but multi-session support per team is not yet officially available and is considered experimental.
- Workspace API —
workspace_api/(FastAPI backend) - Workspace UI —
workspace_ui/(Quasar/Vue app; built assets placed inworkspace_ui/dist/)- Luigi Shell — provides the micro frontend navigation and layout
- Management app — a single-page application (SPA) embedded via Luigi as a view, using Quasar.js/Vue
.
├── workspace_api/ # Python FastAPI backend
└── workspace_ui/ # Luigi shell + Vue frontend views
├── luigi-shell/
│ ├── ui.html # Luigi shell template (rendered by FastAPI)
│ ├── logo.svg # Main logo
│ ├── icons/ # favicon.ico
│ └── standalone/ # Luigi shell with statically defined workspace data
├── management/ # Quasar App
│ ├── index.html # Vue app entry point (used inside Luigi iframe)
│ └── dist/ # Built Quasar App
└── dist/ # Combined built UI code, served as static content- Python 3.12 (e.g., via pyenv)
- uv for Python deps
- Node.js 20.x + npm for the frontend
- Docker (optional)
-
Backend setup (from repo root):
pyenv local 3.12.11 python --version # should be 3.12.11 uv lock --python python uv sync --python python --extra dev uv run pre-commit install
-
Frontend setup (from repo root):
cd workspace_ui ./build_dist.sh cd ..
KUBECONFIG=~/.kube/config-eoepca-demo uv run uvicorn workspace_api:app --reload --host=0.0.0.0 --port=8080 --log-level=infoThe API will be at http://localhost:8080.
Run the Quasar/Vite dev server (default: http://localhost:9000):
From the workspace_ui/management folder:
npm run devThen in another terminal, from the workspace_api/ folder:
KUBECONFIG=~/.kube/config-eoepca-demo UI_MODE="ui" FRONTEND_URL="http://localhost:9000" uv run uvicorn workspace_api:app --reload --host=0.0.0.0 --port=8080 --log-level=infoOpen http://localhost:8080/workspaces/<YOUR_WS_NAME> in a browser (sends Accept: text/html) to load the UI via the dev server.
Build the SPA into workspace_ui/dist/ and let the backend serve it as static content:
From the workspace_ui/ folder:
./build_dist.shKUBECONFIG=~/.kube/config-eoepca-demo UI_MODE="ui" uv run uvicorn workspace_api:app --reload --host=0.0.0.0 --port=8080 --log-level=infoThe Docker image (below) builds both the API and the UI and copies
workspace_ui/dist/into the container.
Python (from workspace_api/):
uv run ruff check .
uv run ruff format .
uv run mypy .Management Frontend (from workspace_ui/management):
npm run lintRun all pre-commit hooks from repo root:
uv run pre-commit run --all-filesBackend tests live in workspace_api/tests/:
cd workspace_api
uv run pytestWatch mode:
uv run pytest-watcher tests --nowInstalled via the backend dev extra:
- mypy – static typing
- ruff – linting & formatting
- pytest / pytest-watcher – testing
- pre-commit – git hooks
- ipdb – debugger
Run via uv run <tool> from workspace_api/.
The Workspace API uses a gateway-centric authentication model. Authentication is enforced upstream by an API gateway (for example APISIX) using OpenID Connect. The gateway validates the access token (signature, issuer, audience, expiration). Only authenticated requests are forwarded to the backend.
The backend does not re-validate tokens cryptographically. Instead, it treats the token as trusted input and extracts a minimal identity and authorization context, which is attached to each request via request.state.user.
Internally, the API retains only:
- the username (
preferred_username) - a workspace-to-permissions mapping derived from the token’s
resource_accessclaim
External roles are normalized into explicit permissions:
-
ws_accessVIEW_BUCKET_CREDENTIALSVIEW_MEMBERSVIEW_BUCKETSVIEW_STORES
-
ws_admin- all of the above, plus:
MANAGE_MEMBERSMANAGE_BUCKETSMANAGE_STORES
Authorization decisions are based exclusively on these permissions, not on raw roles.
When AUTH_MODE=no, authentication is disabled. The backend injects a synthetic user context with username Default and wildcard workspace permissions granting full access.
The following JSON document is an example of claims that matches the expectations of the Workspace API. For development or testing purposes, this payload can be encoded as a JWT and passed via the Authorization header as a Bearer token.
{
"preferred_username": "alice",
"resource_access": {
"ws-alice": {
"roles": ["ws_admin"]
},
"ws-bob": {
"roles": ["ws_access"]
}
}
}Environment variables used by the backend (besides KUBECONFIG for Kubernetes access):
| Variable | Default | What it does |
|---|---|---|
PREFIX_FOR_NAME |
Prefix applied to user-facing names to build K8s object names (e.g. ws to get ws-alice for alice). |
|
PROVIDER_ENVIRONMENT |
datalab |
EnvironmentConfig name selected for new provider-storage and provider-datalab XRs. The API writes this value to storages.pkg.internal/environment on Storage and datalabs.pkg.internal/environment on Datalab. |
USE_VCLUSTER |
false |
Whether to provision an isolated vcluster for each datalab session (true) or run in separate namespace on host cluster (false). |
SESSION_MODE |
on |
Initial default-session mode for newly created Datalabs: on declares it as started, auto declares it as stopped for UI launch, and off does not declare it. |
DISABLE_DOCKER_REGISTRY |
false |
By default, each datalab gets an in-session Docker registry. |
DISABLE_STORES |
false |
Disable creation and display of all Datalab store types. |
DISABLED_STORE_TYPES |
Comma- or semicolon-separated store types to disable even when their backing CRDs are installed. Accepted aliases include postgres, qdrant, redis, and mongodb. |
|
ENDPOINT |
from AWS_ENDPOINT_URL |
S3 endpoint URL used when falling back to environment-based config. |
REGION |
from AWS_REGION or AWS_DEFAULT_REGION |
S3 region used when falling back to environment-based config. |
UI_MODE |
no |
Set to ui to enable templated HTML shell and SPA embedding. |
FRONTEND_URL |
/ui/management |
Base path (prod) or absolute URL (dev server like http://localhost:9000) for the SPA. |
AUTH_MODE |
gateway |
Authentication mode gateway expects a validated Authorization: Bearer <access_token> header to be forwarded by an upstream gateway, no disables authentication entirely (for local development only). |
AUTH_DEBUG |
false |
Enable verbose authentication and workspace debug logging. |
The Workspace UI only offers store types that the cluster can currently reconcile. A store type is available when:
- the
DatalabCRD exposes the matching spec field, - the backing operator CRD is installed in the cluster, and
- the store type is not disabled through
DISABLE_STORESorDISABLED_STORE_TYPES.
The current store type mapping is:
| Store type | Datalab field | Required backing CRD |
|---|---|---|
| Database (Postgres) | spec.databases |
postgresclusters.postgres-operator.crunchydata.com |
| Vector store (Qdrant) | spec.vectorStores |
qdrantclusters.qdrant.io |
| Cache (Redis) | spec.cacheStores |
redis.redis.redis.opstreelabs.in |
| Document store (MongoDB) | spec.documentStores |
mongodbcommunity.mongodbcommunity.mongodb.com |
If a backing CRD is not installed, the UI hides that store type and the API rejects attempts to create it. Manual changes made directly to a Datalab XR are not blocked by the Workspace API; in that case Crossplane reports reconciliation failures on the Datalab status if the required operator CRD is missing.
Build the combined image (Python 3.12.11 + built UI) from repo root:
docker build . -t workspace-api:latest --build-arg VERSION=$(git rev-parse --short HEAD)Run it, e.g.
docker run --rm \
-p 8080:8080 \
-e GUNICORN_WORKERS=2 \
-e UI_MODE=ui \
-e PREFIX_FOR_NAME=ws \
-e AWS_REGION=eoepca-demo \
-e AWS_ENDPOINT_URL=https://minio.develop.eoepca.org \
-e KUBECONFIG=/kube/config \
--mount type=bind,src=$HOME/.kube/config-eoepca-demo,dst=/kube/config,readonly \
workspace-api:latestApache 2.0 (Apache License Version 2.0, January 2004) https://www.apache.org/licenses/LICENSE-2.0