API reference¶
The REST API's machine-readable schema is at openapi.yaml.
Group and version¶
- API group:
dwpk.devops-ia.io - Version:
v1alpha1
Kinds and scope¶
| Kind | Scope |
|---|---|
WorkspaceImage |
Cluster |
UserSpace |
Cluster |
Workspace |
Namespaced |
ImageRegistry |
Cluster |
Common status shape¶
Every CRD listed above uses status.conditions as []metav1.Condition.
Each condition item has the standard Kubernetes fields:
typestatus, one ofTrue,False,UnknownreasonmessagelastTransitionTimeobservedGeneration, optional
WorkspaceImage¶
Scope: cluster.
Printer columns from api/v1alpha1/workspaceimage_types.go:
Display Namefrom.spec.displayNameImagefrom.spec.imageDeprecatedfrom.spec.deprecatedAgefrom.metadata.creationTimestamp
spec¶
| Field | Type | Required | Default | Validation | Notes |
|---|---|---|---|---|---|
spec.displayName |
string |
Yes | None | None | Catalog title shown to users. |
spec.description |
string |
No | None | None | Catalog description text. |
spec.icon |
string |
No | None | None | Icon URL. |
spec.tags[] |
string |
No | None | None | Tag list used by the UI filters. |
spec.maintainer |
string |
No | None | None | Contact string for the entry. |
spec.deprecated |
boolean |
No | false |
None | Hidden from the catalog unless the user asks to show deprecated entries. |
spec.image |
string |
Yes | None | None | Container image reference for the workspace pod. |
spec.imagePullPolicy |
corev1.PullPolicy |
No | IfNotPresent |
Always, IfNotPresent, Never |
Passed into the workspace container. |
spec.shell |
string |
No | /bin/bash |
None | Login shell the gateway execs. |
spec.homePath |
string |
No | /home/dev |
None | Working directory and PVC mount path. |
spec.runAsUser |
integer |
No | 1000 |
None | Also used as fsGroup on the pod. |
spec.command[] |
string |
No | ['sleep','infinity'] |
None | Container command. Keeps generic images alive by default. |
spec.allowRoot |
boolean |
No | false |
None | Lets a workspace from this entry run as uid 0. Administrator-only. |
spec.deprecateAt |
metav1.Time |
No | None | None | The entry stays fully usable until this moment, with a warning on the catalog; after it, it behaves as deprecated. |
spec.placement |
WorkspacePlacement |
No | None | None | Scheduling constraints. |
spec.placement.nodeSelector |
map[string]string |
No | None | None | Copied into the pod spec. |
spec.placement.tolerations[] |
corev1.Toleration |
No | None | Standard Kubernetes schema | Copied into the pod spec. |
spec.imagePullSecretRef |
corev1.LocalObjectReference |
No | None | None | Names a pull Secret for a private image. dwpk replicates the Secret of this name from the manager's own namespace into every user namespace, so this field only ever needs to carry a name. An ImageRegistry sync stamps its own imagePullSecretRef here automatically. |
A catalog entry no longer carries sizes or storage defaults. Resources are chosen per workspace and checked against the owner's quota at admission.
status¶
| Field | Type | Required | Default | Validation | Notes |
|---|---|---|---|---|---|
status.conditions[] |
metav1.Condition |
No | None | Standard Kubernetes schema | Current state of the catalog entry. |
example¶
apiVersion: dwpk.devops-ia.io/v1alpha1
kind: WorkspaceImage
metadata:
name: python-3.12
spec:
displayName: "Python 3.12"
description: "Python 3.12 with poetry, ruff and the AWS CLI."
icon: "https://cdn.example.com/icons/python.svg"
tags:
- python
- backend
maintainer: "platform@example.com"
deprecated: false
image: "ghcr.io/devops-ia/dwpk/python:3.12"
imagePullPolicy: IfNotPresent
shell: /bin/bash
homePath: /home/dev
runAsUser: 1000
command:
- sleep
- infinity
placement:
nodeSelector:
workload: worker
tolerations: []
ImageRegistry¶
Scope: cluster.
Configures one external container registry to sync into the catalog (§4.6). Several
ImageRegistry objects, including several of the same provider, may exist side by side -
each syncs and prunes only the WorkspaceImages it owns (ownerReferences, labelled
dwpk.devops-ia.io/image-registry=<registry name>).
Printer columns from api/v1alpha1/imageregistry_types.go:
Providerfrom.spec.providerImagesfrom.status.imagesLast Syncfrom.status.lastSyncTimeAgefrom.metadata.creationTimestamp
spec¶
| Field | Type | Required | Default | Validation | Notes |
|---|---|---|---|---|---|
spec.provider |
string |
Yes | None | aws-ecr |
Which registry API this object talks to. A second provider is a new enum value plus a new registry.Provider implementation, not a change to this type. |
spec.aws |
AWSRegistry |
No | None | Required (CEL) when spec.provider is aws-ecr |
AWS ECR configuration. |
spec.aws.region |
string |
Yes | None | Max length 32 | AWS region the registry lives in. |
spec.aws.registryId |
string |
No | None | Max length 12 | AWS account ID. Empty resolves to the account the credentials belong to - what IRSA, EKS Pod Identity and an instance profile all resolve to without this ever being set. |
spec.aws.roleArn |
string |
No | None | Max length 2048 | Assumed via STS before listing, for a registry in another account. |
spec.sync.intervalSeconds |
integer |
No | 900 |
Minimum 60 | How often the registry is re-listed. |
spec.sync.include[] |
string |
No | None | RE2 | Repository name patterns; empty means every repository is a candidate. |
spec.sync.exclude[] |
string |
No | None | RE2 | Wins over include when both match. |
spec.sync.tags.mode |
string |
No | latest |
latest, pattern |
Newest tag by push time, or every tag matching patterns. |
spec.sync.tags.patterns[] |
string |
No | None | RE2 | Used only when mode is pattern. |
spec.sync.tags.limit |
integer |
No | 1 |
Minimum 1 | Newest N matching tags per repository become catalog entries. |
spec.sync.prune |
boolean |
No | false |
None | Deletes a WorkspaceImage this registry previously created once its remote image is gone. Off by default: a briefly unreachable registry should not delete a catalog entry somebody may be using. |
spec.imagePullSecretRef |
corev1.LocalObjectReference |
No | None | None | Names a Secret, in the manager's own namespace, that dwpk replicates into every user namespace. Stamped onto every WorkspaceImage this registry creates. |
Force-syncing outside the configured interval is not a field: bump the
dwpk.devops-ia.io/force-sync annotation to any new value and the standard watch
retriggers a sync immediately.
status¶
| Field | Type | Required | Default | Validation | Notes |
|---|---|---|---|---|---|
status.conditions[] |
metav1.Condition |
No | None | Standard Kubernetes schema | Ready/Degraded. A failed sync leaves status.images and status.lastSyncTime at their last known good value rather than clearing them. |
status.observedGeneration |
integer |
No | None | None | The metadata.generation this status was computed from. |
status.lastSyncTime |
metav1.Time |
No | None | None | When the registry was last successfully listed. |
status.images |
integer |
No | None | None | How many WorkspaceImages this registry currently owns. |
example¶
apiVersion: dwpk.devops-ia.io/v1alpha1
kind: ImageRegistry
metadata:
name: team-ecr
spec:
provider: aws-ecr
aws:
region: eu-west-1
sync:
intervalSeconds: 900
include: ["dwpk/.*"]
exclude: ["dwpk/scratch.*"]
tags:
mode: latest
limit: 1
prune: false
UserSpace¶
Scope: cluster.
Printer columns from api/v1alpha1/userspace_types.go:
Ownerfrom.spec.ownerNamespacefrom.status.namespacePhasefrom.status.phaseWorkspacesfrom.spec.quota.workspacesAgefrom.metadata.creationTimestamp
spec¶
| Field | Type | Required | Default | Validation | Notes |
|---|---|---|---|---|---|
spec.owner |
string |
Yes | None | Minimum length 1; immutable by CEL rule self == oldSelf |
Email or Kubernetes username that owns this space. |
spec.quota |
UserSpaceQuota |
Yes | None | None | Namespace budget. |
spec.quota.cpu |
resource.Quantity |
Yes | None | Kubernetes quantity syntax | Total requested CPU budget. |
spec.quota.memory |
resource.Quantity |
Yes | None | Kubernetes quantity syntax | Total requested memory budget. |
spec.quota.storage |
resource.Quantity |
Yes | None | Kubernetes quantity syntax | Total PVC storage budget. |
spec.quota.gpu |
integer |
No | 0 |
Minimum 0 |
GPUs the running workspaces may hold at once, counted against PlatformConfig.spec.gpuResourceName. |
spec.quota.workspaces |
integer |
Yes | None | Minimum 0 |
Maximum number of Workspace objects in the namespace. |
spec.networkPolicy |
string |
Yes | None | Isolated, ClusterEgress |
Selects the egress posture the controller writes. |
status¶
| Field | Type | Required | Default | Validation | Notes |
|---|---|---|---|---|---|
status.namespace |
string |
No | None | None | Namespace reconciled for this user. |
status.phase |
string |
No | None | None | Coarse summary of current state. Code exports Pending, Ready, Failed. |
status.observedGeneration |
integer |
No | None | None | Generation the status reflects. |
status.conditions[] |
metav1.Condition |
No | None | Standard Kubernetes schema | Readiness and degradation details. |
example¶
apiVersion: dwpk.devops-ia.io/v1alpha1
kind: UserSpace
metadata:
name: alice
spec:
owner: "alice@example.com"
quota:
cpu: "8"
memory: 32Gi
storage: 100Gi
gpu: 0
workspaces: 1
networkPolicy: Isolated
There is no configurable floor under spec.quota - the minimum is always
zero. Only the maximum values above are ever set.
Workspace¶
Scope: namespaced.
Printer columns from api/v1alpha1/workspace_types.go:
Imagefrom.spec.imageRef.nameStatusfrom.status.phaseEndpointfrom.status.endpointAgefrom.metadata.creationTimestamp
spec¶
| Field | Type | Required | Default | Validation | Notes |
|---|---|---|---|---|---|
spec.imageRef |
WorkspaceImageReference |
Yes | None | None | Names the WorkspaceImage catalog entry. |
spec.imageRef.name |
string |
Yes | None | Minimum length 1 | Cluster-scoped WorkspaceImage name. |
spec.resources |
corev1.ResourceRequirements |
No | None | Every request must be at most its limit; an extended resource must have request equal to limit | CPU, memory and GPU for the container. Checked against the owner's remaining quota at admission. |
spec.nodeSelector |
map[string]string |
No | The image's placement.nodeSelector |
None | Overrides the catalog entry rather than merging with it. |
spec.tolerations[] |
corev1.Toleration |
No | The image's placement.tolerations |
Standard Kubernetes schema | Overrides the catalog entry. |
spec.affinity |
corev1.Affinity |
No | None | Standard Kubernetes schema | Copied into the pod spec. |
spec.env[] |
corev1.EnvVar |
No | None | Standard Kubernetes schema | Copied into the container. |
spec.envFrom[] |
corev1.EnvFromSource |
No | None | Standard Kubernetes schema | Whole Secrets and ConfigMaps as environment. |
spec.volumes[] |
corev1.Volume |
No | None | Standard Kubernetes schema | Extra volumes beside the home PVC. |
spec.volumeMounts[] |
corev1.VolumeMount |
No | None | Standard Kubernetes schema | Where those volumes land in the container. |
spec.storage |
resource.Quantity |
No | None | Immutable after first set by CEL rule | Size of the home PVC. Counted against UserSpace.spec.quota.storage at admission. |
spec.sshAuthorizedKeys[] |
string |
No | None | Each key must start with ssh-, ecdsa-sha2- or sk- |
OpenSSH public keys accepted by the gateway. |
spec.running |
boolean |
No | true |
If true, the object must have at least one SSH key |
Stop and start switch for the StatefulSet. No omitempty in Go type. |
spec.idleTimeout |
metav1.Duration |
No | 4h |
None | Present in the API, but auto-stop is not implemented yet. |
spec.observability |
WorkspaceObservability |
No | None | None | Per-workspace label and annotation toggles. |
spec.observability.logsEnabled |
boolean |
No | true |
None | Writes pod annotation dwpk.devops-ia.io/logs-enabled. |
spec.observability.metricsEnabled |
boolean |
No | true |
None | Writes pod label dwpk.devops-ia.io/metrics-enabled. |
Object-level CEL validations on Workspace.spec:
storageis immutable once set- every
sshAuthorizedKeysentry is an OpenSSH public key:ssh-rsa,ssh-ed25519,ecdsa-sha2-nistp256/384/521, or ansk-security key running=truerequires at least one SSH key
The validating webhook adds what CEL cannot see, because it needs another object:
the name must be unique in the namespace, and the requested resources must fit in
what the owner's UserSpace quota has left.
status¶
| Field | Type | Required | Default | Validation | Notes |
|---|---|---|---|---|---|
status.phase |
string |
No | None | Pending, Starting, Running, Suspended, Failed |
Current coarse lifecycle state. |
status.endpoint |
string |
No | None | None | Published as <workspace-name>@<gateway-host>. |
status.podName |
string |
No | None | None | Current pod name. Usually <workspace-name>-0. |
status.lastActivityTime |
metav1.Time |
No | None | None | Updated by the gateway on session open and close. |
status.observedGeneration |
integer |
No | None | None | Generation the status reflects. |
status.conditions[] |
metav1.Condition |
No | None | Standard Kubernetes schema | Includes Ready, Degraded, and ImageResolved in current controller code. |
admission behavior¶
On CREATE, the mutating webhook also stamps this annotation:
| Field | Type | Set by | Notes |
|---|---|---|---|
metadata.annotations["dwpk.devops-ia.io/requester"] |
string |
Mutating webhook | Authenticated username from request.userInfo.username |
example¶
apiVersion: dwpk.devops-ia.io/v1alpha1
kind: Workspace
metadata:
name: dev
namespace: dwpk-alice
spec:
imageRef:
name: python-3.12
resources:
requests:
cpu: "500m"
memory: 1Gi
limits:
cpu: "2"
memory: 4Gi
storage: 20Gi
sshAuthorizedKeys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleKeyForSamplesOnly000000000 alice@laptop"
running: true
idleTimeout: 4h
observability:
logsEnabled: true
metricsEnabled: true
REST API¶
The UI also serves a JSON API at /api/v1, so the same operations are available
to a script without driving a browser. It is a convenience over the Kubernetes
API, never a way around it.
Authentication¶
Two ways in, both landing on the same server-side session:
- Cookie -
POST /api/v1/loginwith{"username","password"}. The session id comes back as aSet-Cookie; the CSRF token for later writes comes back in a header, and again fromGET /api/v1/session. Every mutating request must carry it asX-CSRF-Token. - Bearer -
Authorization: Bearer dwpk_…, using a token fromPOST /api/v1/tokens. No CSRF token is needed, since there is no cookie to ride on.
Authorization¶
There is none here. Every call is made against the Kubernetes API with the
caller's own forwarded ServiceAccount token, so a person who cannot delete a
workspace receives the API server's own 403 - the same message, from the same
RBAC rule, as they would see in the browser or in kubectl. There is no second
implementation to keep in step. See SPEC §8.1.
Conventions¶
- Lists come back as
{"items": [...]}; single objects come back bare. - Field names are
snake_case. Kubernetes objects are returned as they are, so their own fields staycamelCase. PATCHbodies use optional fields: an omitted key keeps its current value rather than resetting it. Sending{"role":"admin"}to a disabled account leaves it disabled.- Errors are
{"error": "<message>"}with a matching status code.
Endpoints¶
Session¶
| Method | Path | Notes |
|---|---|---|
POST |
/api/v1/login |
Username and password; sets the session cookie |
GET |
/api/v1/session |
Who you are: email, namespace, role, CSRF token |
POST |
/api/v1/logout |
Clears the server-side session |
Workspaces¶
| Method | Path | Notes |
|---|---|---|
GET |
/api/v1/workspaces |
Scoped by role: an administrator lists the cluster, everyone else their own namespace. ?namespace= narrows the result |
POST |
/api/v1/workspaces |
Creates in your own namespace |
GET |
/api/v1/workspaces/{name} |
?namespace= reaches one you can see but do not own |
DELETE |
/api/v1/workspaces/{name}?delete_volume= |
Deletes the workspace. The home PVC (home-{name}-0) is deleted too by default - a StatefulSet never removes its own claim, so the API does it explicitly, matching the web UI's delete dialog (also checked by default). Pass ?delete_volume=false to keep the PVC. If the workspace deletes but the PVC delete fails, the response is 409 Conflict naming exactly what's left |
POST |
/api/v1/workspaces/{name}/start |
Patches spec.running |
POST |
/api/v1/workspaces/{name}/stop |
Patches spec.running |
Resources are not patchable. spec.resources and spec.storage are set at
creation: storage because a PVC cannot shrink, and CPU and memory because
changing them replaces the pod, which the stop/start pair does explicitly.
Catalog¶
| Method | Path | Notes |
|---|---|---|
GET |
/api/v1/workspace-images |
Only entries you may use |
GET |
/api/v1/workspace-images/{name} |
|
POST |
/api/v1/workspace-images |
Administrator only |
PATCH |
/api/v1/workspace-images/{name} |
Administrator only. Omitted fields keep their current value |
DELETE |
/api/v1/workspace-images/{name} |
Administrator only |
GET |
/api/v1/image-registries |
Configured registries |
GET |
/api/v1/image-registries/{name} |
|
POST |
/api/v1/image-registries |
Administrator only |
PATCH |
/api/v1/image-registries/{name} |
Administrator only. Omitted fields keep their current value |
DELETE |
/api/v1/image-registries/{name} |
Administrator only. Every WorkspaceImage it synced is deleted too |
POST |
/api/v1/image-registries/{name}/force-sync |
Administrator only. Retriggers a sync outside its configured interval |
Creating, editing and deleting catalog entries and registries over REST needs the same administrator RBAC the web UI's catalog admin screen requires - this is not a second, looser permission model.
Administration¶
| Method | Path | Notes |
|---|---|---|
GET |
/api/v1/admin/userspaces |
Every UserSpace, with its quota and role |
POST |
/api/v1/admin/userspaces |
Provisions a new UserSpace |
PATCH |
/api/v1/admin/userspaces/{name} |
Role and disabled |
DELETE |
/api/v1/admin/userspaces/{name} |
Deletes the UserSpace (not its Kubernetes namespace - the controller's own finalizer handles that) |
GET |
/api/v1/admin/quota |
Usage against limit for every UserSpace |
PATCH |
/api/v1/admin/quota/{name} |
Updates one UserSpace's quota. Omitted fields keep their current value |
GET POST |
/api/v1/admin/local-users |
Only when local auth is enabled |
DELETE |
/api/v1/admin/local-users/{name} |
Profile and tokens¶
| Method | Path | Notes |
|---|---|---|
POST |
/api/v1/profile/password |
{"current_password","new_password"}. The account comes from your session, never from the body. Local logins only: a session from an identity provider gets 409 |
GET POST |
/api/v1/tokens |
The plaintext token is returned once, by the issuing call only |
DELETE |
/api/v1/tokens/{name} |
Logs¶
| Method | Path | Notes |
|---|---|---|
GET |
/api/v1/workspaces/{name}/logs |
Tail of the workspace container's logs. ?tail= bounds line count, default matches the UI |
Example¶
# Sign in and keep the cookie.
curl -sc jar -d '{"username":"alice","password":"..."}' \
-H 'Content-Type: application/json' https://dwpk.example.com/api/v1/login
CSRF=$(curl -sb jar https://dwpk.example.com/api/v1/session | jq -r .csrf_token)
# Stop a workspace, then look at what is left.
curl -sb jar -X POST -H "X-CSRF-Token: $CSRF" \
https://dwpk.example.com/api/v1/workspaces/dev/stop
curl -sb jar https://dwpk.example.com/api/v1/workspaces/dev | jq .status