api
Full project docs: README.md, SPEC.md, architecture, setup guide
The Corral web UI serves a REST API at port 8006 (corral web), plus
WebSocket bridges for VNC and serial consoles. All responses are JSON unless
noted otherwise.
The same API is available from the on-cluster deployment at
https://corral.<tailnet>.ts.net.
Errorsβ
Every error response has shape {"error": "<message>"} with a 4xx or 5xx
status code. 5xx means the cluster couldn't be reached (kubectl failed);
4xx means invalid input.
VM lifecycleβ
GET /api/vmsβ
List all VMs across all namespaces. Merges live VMI data (IP, node) for running VMs.
Response (array):
[
{
"name": "web",
"namespace": "tailvm",
"status": "Running",
"ready": true,
"running": true,
"cpu": 2,
"mem": "4Gi",
"disk": "20Gi",
"ip": "10.42.0.15",
"node": "bihar"
}
]
POST /api/vmsβ
Create a VM.
Request fields:
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | [a-z0-9][a-z0-9-]* |
namespace | string | no | defaults to tailvm |
cpu | int | no | default 2 |
mem | string | no | e.g. 4G, default 4G |
disk | string | no | e.g. 20G, default 20G |
containerDisk | string | source | OCI containerdisk URI |
image | string | source | catalog entry name (see /api/images) |
import | string | source | qcow2/raw URL (CDI import) |
iso | string | source | ISO URL (CDI import) |
bootc | string | source | bootc image URI (needs bootc plugin) |
pvc | string | source | existing PVC name |
sshKey | string | bootc only | SSH public key baked into the disk |
node | string | no | schedule on a specific node |
cloudInit | string | no | cloud-init user-data YAML |
instancetype | string | no | cluster instancetype name |
preference | string | no | cluster preference name |
Exactly one source field must be set.
Response (201 Created):
{"name": "web", "namespace": "tailvm"}
Bootc response (202 Accepted, build is async):
{"task": "bootc-web-1712345678"}
The task ID can be polled at GET /api/tasks/{id}.
GET /api/vms/{ns}/{name}β
Get raw VM manifest JSON (the kubevirt VirtualMachine object).
POST /api/vms/{ns}/{name}/{action}β
Execute an action on a VM. Valid actions:
| Action | Meaning |
|---|---|
start | Power on |
stop | Graceful shutdown |
restart | Restart |
pause | Freeze (kubevirt only) |
unpause | Resume (kubevirt only) |
migrate | Live-migrate (kubevirt only) β prefer the dedicated endpoint below |
Response: {"status": "ok"}
POST /api/vms/{ns}/{name}/migrateβ
Live-migrate as a tracked background task with progress in the activity panel. The trigger runs synchronously (so "not migratable / cross-vendor" errors come back immediately); the migration is then watched to completion.
Body: {"targetNode": "bihar"} (optional β empty lets the scheduler choose,
from among same-CPU-vendor nodes).
Response: 202 {"task": "migrate-β¦"} β poll GET /api/tasks/{id}.
POST /api/vms/{ns}/{name}/tagsβ
Add or remove a tag, persisted as a corral.dev/tag.<name> VM label (so tags
survive round-trips and are kubectl get vm -l-selectable). Tags are surfaced
on every VM in GET /api/vms.
Body: {"tag": "prod", "on": true}
Response: {"tag": "prod", "on": true}
DELETE /api/vms/{ns}/{name}β
Delete a VM, its PVCs, DataVolumes, hotplug disks, snapshots, proxy resources, and registry entry.
Response: {"status": "deleted"}
Containers (CT)β
Proxmox-style pet pods β plain Kubernetes pods, not KubeVirt VMs. See
docs/adr/0005-containers-as-pet-pods.md for the design.
GET /api/ctsβ
List all Containers across every namespace.
Response: 200
[{"name": "devbox", "namespace": "corral-vms", "node": "karnataka",
"phase": "Running", "ready": true, "image": "debian:bookworm",
"cpu": 2, "mem": "2Gi", "privileged": true}]
phase is the pod phase, or "Stopped" when no pod exists (the CT's
durable identity is its data PVC, which survives Stop). node is empty
when stopped/unscheduled.
POST /api/ctsβ
Create a Container.
Body:
{"name": "devbox", "namespace": "corral-vms", "image": "docker.io/library/debian:bookworm",
"cpu": 2, "mem": "2Gi", "disk": "10Gi", "storageClass": "", "privileged": true}
privileged: true seeds a persistent, mutable root filesystem onto the
data volume and chroots into it (distrobox-style β package installs and
dotfiles survive Stop/Start). false (default) mounts the volume at
/data only.
Response: 201 {"name": "devbox", "namespace": "corral-vms"}
POST /api/cts/{ns}/{name}/{action}β
action is start or stop. Stop deletes the pod but keeps the data
volume; Start recreates the pod from the spec recorded on the volume's
annotation.
Response: {"status": "ok"}
DELETE /api/cts/{ns}/{name}β
Delete a Container: its pod, Service (if a tailnet proxy was applied), and data volume.
Response: {"status": "deleted"}
Hardware operationsβ
POST /api/vms/{ns}/{name}/scaleβ
Change CPU count and/or memory. Body: {"cpu": 4, "mem": "8G"}. Live-hotplugs
when the VM is live-migratable; otherwise does a stopβpatchβstart.
Response: {"status": "ok"}
POST /api/vms/{ns}/{name}/volumesβ
Hotplug a new disk. Body: {"size": "10Gi"}.
Response: {"pvc": "web-disk-2"}
DELETE /api/vms/{ns}/{name}/volumes/{vol}β
Detach a hotplugged disk. {vol} is the PVC name.
Response: {"status": "removed"}
POST /api/vms/{ns}/{name}/expandβ
Expand an existing PVC. Body: {"pvc": "web-disk-1", "size": "40Gi"}. Needs a
StorageClass with allowVolumeExpansion: true.
Response: {"status": "ok"}
POST /api/vms/{ns}/{name}/nicsβ
Add a secondary NIC. Body: {"nad": "lan", "iface": "eth1"}. Needs Multus + a
NetworkAttachmentDefinition in the VM's namespace.
Response: {"status": "ok"}
Snapshotsβ
GET /api/vms/{ns}/{name}/snapshotsβ
List snapshots for a VM. Needs the Snapshot feature gate.
Response: array of {name, phase, creationTime}
POST /api/vms/{ns}/{name}/snapshotsβ
Create a snapshot. Body: {"name": "before-upgrade"} (optional; auto-named).
Response: {"name": "before-upgrade"}
POST /api/vms/{ns}/{name}/snapshots/{snap}/restoreβ
Restore a VM to a snapshot. The VM is shut down during restore.
Response: {"status": "restoring"}
DELETE /api/vms/{ns}/{name}/snapshots/{snap}β
Delete a snapshot.
Response: {"status": "deleted"}
Clone & templateβ
POST /api/vms/{ns}/{name}/cloneβ
Clone a VM (definition and disks). The VM must be stopped. Body:
{"target": "web-clone"}. Needs the Snapshot feature gate + a
VolumeSnapshotClass.
Response: {"target": "web-clone"}
POST /api/vms/{ns}/{name}/templateβ
Mark or unmark a VM as a template. Body: {"on": true}.
Response: {"isTemplate": true}
Image library & catalogβ
GET /api/imagesβ
The built-in OS image catalog β curated, ready-to-boot containerdisks.
Response (array):
[
{
"name": "fedora",
"description": "Fedora 42 cloud",
"containerDisk": "quay.io/containerdisks/fedora:42",
"defaultUser": "fedora"
}
]
User-defined custom sources are appended, each flagged "custom": true.
GET /api/sourcesβ
The user-defined custom sources only (for management UI). Same entry shape as
/api/images, all "custom": true. Persisted in the corral-sources
ConfigMap so they survive web-pod restarts.
POST /api/sourcesβ
Add or replace a custom source (idempotent by name).
{"name": "my-image", "kind": "containerDisk", "uri": "ghcr.io/me/img:tag", "description": "optional"}
kind is one of containerDisk (boots directly), url (qcow2/raw, CDI
import), or iso (installer ISO). Response: {"status": "ok", "name": "my-image"}.
DELETE /api/sources/{name}β
Remove a custom source. Response: {"status": "removed"}.
GET /api/datavolumesβ
List imported images (CDI DataVolumes). These are ISO/qcow2/raw images imported from URLs.
Response: array of {name, namespace, size, phase, progress, source}
POST /api/datavolumesβ
Start a CDI import. Body:
{"name": "jammy", "namespace": "tailvm", "url": "https://.../jammy.qcow2", "size": "10Gi"}
Response: {"name": "jammy"}
DELETE /api/datavolumes/{ns}/{name}β
Delete an imported image (DataVolume).
Response: {"status": "deleted"}
Bootc buildsβ
GET /api/tasks/{id}β
Poll a bootc build task. Returns live build log.
Response:
{"status": "running", "log": "Pulling image...\nInstalling...", "error": ""}
Statuses: "running" β "done" (success) or "error" (failure).
Guest infoβ
GET /api/vms/{ns}/{name}/guestinfoβ
Guest-agent info (OS, hostname, users, filesystems). Returns 503 if the
agent isn't reachable.
GET /api/vms/{ns}/{name}/eventsβ
Recent K8s events for the VM (Proxmox-style task log).
GET /api/vms/{ns}/{name}/metricsβ
Live CPU/memory/disk metrics. Returns null values if the metrics-server isn't available.
GET /api/vms/{ns}/{name}/metrics/historyβ
Retained CPU samples for the Summary-panel sparkline. The server samples every
running VM every ~15s into a bounded in-memory ring buffer (~1h). Returns
[{"t": <epoch-ms>, "cpu": <millicores>}, β¦] β an empty array when
metrics-server is absent.
Infrastructureβ
GET /api/whoamiβ
The caller's tailnet identity and privilege, for the UI to show the logged-in
user and switch to read-only for non-admins. Identity comes from the Tailscale
ingress headers; admin is governed by CORRAL_ADMINS (see
ADR-0003).
Response: {"login": "alice@github", "name": "Alice", "admin": true, "enforced": false}
Mutating requests (non-GET) are rejected with 403 for non-admins when an
allowlist is configured.
GET /api/nodesβ
List cluster nodes with readiness, roles, kubelet version, and architecture.
Response (array):
[{"name": "bihar", "ready": true, "roles": "control-plane,master", "kubelet": "v1.36.1", "arch": "amd64"}]
GET /api/capabilitiesβ
Cluster capability flags β the UI gates controls on these.
Response:
{
"storageClass": "longhorn",
"canExpand": true,
"canSnapshot": true,
"bootc": false
}
| Field | Meaning |
|---|---|
storageClass | Default StorageClass for new VM disks |
canExpand | Whether allowVolumeExpansion: true exists |
canSnapshot | Whether a VolumeSnapshotClass is available |
bootc | Whether the bootc plugin is compiled in |
GET /api/instancetypesβ
Cluster instancetypes and preferences for the create wizard.
Response:
{"instancetypes": ["u1.medium", "u1.large"], "preferences": ["fedora", "ubuntu"]}
GET /api/nadsβ
Multus NetworkAttachmentDefinitions available for secondary NICs.
GET /api/doctorβ
Run all Corral cluster diagnostics. Returns a list of checks (name, status, message).
POST /api/doctor/fixβ
Run the fixable diagnostics (e.g. create missing namespaces).
Response: {"fixed": ["created namespace tailvm"]}
Extensions (plugins)β
GET /api/pluginsβ
Marketplace entries merged with locally-installed state.
Response (array):
[{"name": "bootc", "description": "...", "version": "0.1.0", "installed": false, "inStore": true}]
POST /api/plugins/{name}/installβ
Install a plugin from the marketplace.
Response: {"installed": "bootc"}
DELETE /api/plugins/{name}β
Remove an installed plugin.
Response: {"status": "removed"}
Console & export (WebSocket)β
GET /api/vnc/{ns}/{name} (WebSocket)β
Upgrade to WebSocket β bridges virtctl vnc --proxy-only. Use with noVNC in
the browser.
GET /api/tty/{ns}/{name} (WebSocket)β
Upgrade to WebSocket β bridges virtctl console. Use with xterm.js in the
browser.
GET /api/vms/{ns}/{name}/exportβ
Download a VM disk backup. The VM must be stopped (RWO disk is busy while
running). Triggers virtctl vmexport and streams the result.
| Query | Format | Content-Type |
|---|---|---|
| (default) | gzipped raw (name.img.gz) | application/gzip |
?format=qcow2 | compressed qcow2 (name.qcow2) | application/octet-stream |
qcow2 needs qemu-img on the server (it converts the raw export); if absent the
request returns 501 and the default raw.gz still works.
Static assetsβ
The embedded SPA is served at the root:
| Route | File |
|---|---|
/ | index.html |
/style.css | style.css |
/app.js | app.js |
/icons.js | icons.js |
All other paths fall through to the Go 1.22 http.ServeMux 404.