KubeSpaces — Component Contracts (v0.1)¶
Single source of truth for how components talk to each other. Changes here require touching every consumer — keep it small.
Architecture (Pattern B, decided 2026-07-18)¶
UI (Next.js) / kubespaces CLI ──HTTP+JWT──> backend API ──┐
├─ persists to Postgres (UI metadata, audit)
└─ creates/deletes Tenant CRs
GitOps / kubectl ────────────────────────────────────> Tenant CRs
operator ──watches──> Tenant CRs ──provisions──> vCluster
- The Tenant CR is the source of truth for desired + observed tenant state.
- Postgres stores what Kubernetes shouldn't: user/session metadata, audit log, display metadata. The API reads tenant status from the CR, not the DB.
- Only the operator provisions. The API never touches vCluster.
Versions¶
| Thing | Version |
|---|---|
| Postgres | 18 (official image) |
| Keycloak | 26.7 (official image) |
| Go | 1.26.x |
| Next.js | 15.x |
| CRD | kubespaces.io/v1alpha1, kind Tenant, cluster-scoped |
Backend HTTP API¶
Base path /api/v1, JSON. Auth: Authorization: Bearer <access token> (JWT,
validated against OIDC issuer discovery; audience/azp = kubespaces).
Roles from realm_access.roles: kubespaces-admin (sees all tenants),
kubespaces-member (sees own only, matched on token sub/email vs owner).
| Method | Path | Body | Returns |
|---|---|---|---|
| GET | /healthz | – | 200 {"status":"ok"} (no auth) |
| GET | /api/v1/me | – | {subject, email, roles} |
| GET | /api/v1/tenants | – | [Tenant] |
| POST | /api/v1/tenants | {name, displayName?, resources?, vcluster?} |
201 Tenant |
| GET | /api/v1/tenants/{name} | – | Tenant |
| DELETE | /api/v1/tenants/{name} | – | 202 |
| GET | /api/v1/tenants/{name}/kubeconfig | – | kubeconfig YAML (text/yaml) — read from the Secret referenced by CR status |
Tenant JSON: {name, displayName, owner, phase, message, resources{cpu,memory,storage}, createdAt}
— phase mirrors CR status (Pending|Provisioning|Ready|Deleting|Failed);
API returns Unknown if the CR is missing.
Errors: {"error": "<message>"} with 400/401/403/404/409/500.
name: DNS-1123 label, max 40 chars (leaves room for kubespaces-tenant- prefix).
Backend environment (set by the Helm chart)¶
| Var | Meaning |
|---|---|
| KUBESPACES_DB_HOST / PORT / NAME / USER / PASSWORD | Postgres connection |
| KUBESPACES_OIDC_ISSUER_URL | OIDC issuer (discovery) |
| KUBESPACES_OIDC_CLIENT_ID | expected audience/azp (kubespaces) |
| KUBESPACES_LISTEN_ADDR | default :8080 |
In-cluster kubeconfig is implicit (ServiceAccount).
Database schema (owned by backend, migrations embedded, run at startup)¶
CREATE TABLE tenants (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL DEFAULT '',
owner TEXT NOT NULL, -- OIDC subject or email
spec JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
CREATE TABLE audit_log (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
at TIMESTAMPTZ NOT NULL DEFAULT now(),
actor TEXT NOT NULL,
action TEXT NOT NULL, -- tenant.create | tenant.delete | ...
detail JSONB NOT NULL DEFAULT '{}'::jsonb
);
Tenant CR interaction¶
- API creates Tenant with
spec.owner= token subject (email preferred), labelsapp.kubernetes.io/managed-by: kubespaces-api. - API deletes the CR on DELETE; operator finalizer
(
kubespaces.io/finalizer) tears down and then the CR disappears. DB row is soft-deleted (deleted_at) immediately. - Operator sets
status.phase,status.message,status.kubeconfigSecretRef({name, key}, Secret lives inspec.targetNamespace). - vCluster's kubeconfig Secret is
vc-<vcluster-name>with keyconfig; operator copies/points status at it. - Tenant namespace:
kubespaces-tenant-<name>; vCluster release name = tenant name.
Frontend¶
- Next.js 15 (app router) + Auth.js with Keycloak provider (PKCE public client
kubespaces). - Calls backend same-origin under
/api/v1/...(ingress routes/api→ API). Dev override:KUBESPACES_API_URL. - Env:
AUTH_KEYCLOAK_ISSUER,AUTH_KEYCLOAK_ID(=kubespaces),AUTH_SECRET,AUTH_URL.
Container images¶
| Component | Image |
|---|---|
| API | ghcr.io/kubespaces-io/api |
| Frontend | ghcr.io/kubespaces-io/frontend |
| Operator | ghcr.io/kubespaces-io/operator |