architecture
Full project docs: README.md, SPEC.md, API reference, testing plan, setup guide
Package mapβ
cmd/ Cobra CLI (root, subcommands, TUI)
βββ root.go entrypoint, plugin dispatch, post-quit actions
βββ commands.go list, start, stop, ssh, viewer, logs, info, restart, pause, migrate, scale, adddisk, rmdisk, snapshot
βββ create.go corral create (all flags, both backends, CLI catalog/import/bootc)
βββ clone.go corral clone (kubevirt VM disk + config clone)
βββ ct.go corral ct (create/list/start/stop/delete/console β Containers, pkg/ct)
βββ images.go corral images (list catalog + imported datavolumes)
βββ config.go corral config (show, set auth key)
βββ web.go corral web (--addr, serve embedded SPA)
βββ tui.go Bubble Tea TUI (VMs + Containers side by side; VM and CT action menus, clone input state)
βββ plugin.go corral plugin (search, install, list, remove)
βββ doctor.go corral doctor (cluster diagnostics)
βββ helpers.go shared CLI helpers (flags, namespace resolution)
βββ corral-backup/ plugin: on-demand + scheduled (in-cluster CronJob) S3/R2 backups (built with -tags bootc-free, separate binary)
βββ corral-windows/ plugin: UEFI/TPM/virtio-tuned Windows VMs
βββ corral-vdi/ plugin: desktop pools (RFC-0001 phase 1) β pkg/vdi
βββ corral-bootc/ plugin binary (built with -tags bootc, separate from core)
pkg/ Library code (importable)
βββ catalog/ curated OS image catalog (containerdisks)
βββ config/ ~/.config/tailvm/config.yaml reader (Tailscale auth key)
βββ cronops/ shared CronJob/RBAC manifest builders for scheduled-op plugins (backup, snapsched, schedule)
βββ ct/ Containers (CT) β pet pods, not KubeVirt VMs; see docs/adr/0005
β βββ ct.go Create/Start/Stop/Delete/List/Exists, distrobox-style persistent-rootfs bootstrap for privileged CTs
βββ doctor/ cluster diagnostics + auto-fix (namespace, CDI, KubeVirt, GPU/PCI passthrough, StorageClass perf, etc.)
βββ kubevirt/ KubeVirt backend (the heavy lifter)
β βββ client.go VM CRUD, SSH via virtctl, cloud-init, registry
β βββ client_test.go integration tests (need kubectl context)
β βββ features.go scale, volumes, snapshots, clone, export, guest-info, metrics, capabilities, templates
β βββ options.go boot options (run strategy, firmware, machine type, boot order) β read-modify-apply
β βββ upload.go ISO/template upload via virtctl image-upload
β βββ bootc_core.go bootc interface seam (always compiled β nil when tag absent)
β βββ bootc.go bootc implementation (//go:build bootc β Job + kernel-boot VM)
βββ plugin/ extension system (krew-style, marketplace fetch, install/remove)
βββ qemu/ QEMU backend (local VMs via systemd user service)
βββ registry/ ~/.local/share/tailvm/registry.json β VM name β backend/namespace/password
βββ vdi/ desktop pools (RFC-0001 phase 1) β clone-based pool create, label-based assignment
βββ types/ shared types (VM, CreateOpts, RegistryEntry)
βββ web/ Proxmox-style web UI
βββ server.go HTTP server, mux, VM list/create/action/delete/info/export, nodes, tasks, WS bridges (VM VNC/serial + CT exec)
βββ server_test.go
βββ ct.go CT list/create/start-stop/delete HTTP handlers
βββ features.go capabilities, scale, volumes, expand, snapshots, clone, guest-info, events, metrics, templates,
β doctor, plugins, NADs, NICs, images, instancetypes, datavolumes, boot options, ISO upload
βββ static/
βββ index.html dark SPA shell, create dialog (6 source types), CT create dialog, build dialog
βββ app.js API client, tree (Server View/Folder View, VMs+CTs merged), VM/CT detail panels
β (Summary/Hardware/Options/Snapshots/Events/Console/Terminal), create wizards,
β image library + import, bootc build streaming, mobile drawer
βββ icons.js inline Heroicon SVGs
βββ style.css dark theme, responsive, dialog/modals, cards, tables
marketplace/ Plugin registry (hosted at tuna-os/corral)
βββ index.json plugin entries (name, description, version, platform download URLs)
deploy/ On-cluster manifests
βββ corral-web.yaml Namespace, SA, ClusterRole, Deployment, Service, Tailscale Ingress
docs/ Documentation
βββ api.md REST API reference
βββ architecture.md This file
βββ adr/ Architecture Decision Records (0001-0005)
βββ kubevirt-proxmox-setup.md From-scratch cluster setup guide
scripts/ CI and Docker helper scripts
Containerfile Alpine + corral binary + kubectl + virtctl (for on-cluster deployment)
main.go Entrypoint: cmd.Execute()
Design decisionsβ
No client-go β shell-out to kubectl/virtctlβ
Corral never imports client-go. Every cluster interaction shells out to
kubectl or virtctl. This keeps the binary under ~12 MB, avoids a
~60 MB SDK dependency, and respects whatever kubeconfig the operator already
has. The trade-off: slower per-call latency (process spawn). For a
single-operator tool this is fine; for a multi-tenant API server it would be
unacceptable.
Single binary, three UIsβ
corral is one Go binary that serves three interfaces:
- CLI β Cobra subcommands (
corral create,corral ssh, etc.) - TUI β Bubble Tea interactive terminal UI (bare
corral) - Web β Embedded SPA served at
corral web --addr :8006
All three share the same pkg/kubevirt backend and
~/.local/share/tailvm/registry.json. A VM created in the browser is
immediately corral ssh-able and vice versa.
Embedded SPA β no JS build stepβ
The web UI is vanilla JavaScript and CSS embedded via //go:embed. There is
no Node, no bundler, no build step. xterm.js and noVNC are loaded from CDN
at runtime (they're only needed on the console pages and are too large to
embed). Heroicons are inline SVGs.
Backend transparencyβ
After corral create, the registry stores backend: "kubevirt" or
backend: "qemu". Every subsequent command (start, ssh, viewer, etc.)
reads the registry to determine the backend, falling back to live probing if
the registry entry is missing. The user never specifies --kubevirt again.
Bootc as a pluginβ
Bootc (building a container image into a VM disk on-cluster) is an optional
plugin compiled behind a //go:build bootc tag. The core binary's
bootc_core.go defines the interface seam with nil implementations; the
tagged bootc.go replaces them via init(). This keeps the core binary
lean for users who only need containerdisks and ISOs, while the full
pipeline is available via corral plugin install bootc or by building with
-tags bootc.
The web UI checks GET /api/capabilities β bootc: true/false and
shows/hides the bootc source option accordingly.
KubeVirt LiveUpdate strategyβ
Corral creates VMs with cpu.maxSockets + memory.maxGuest headroom and
masquerade networking so they can hotplug/migrate. It gates live operations
on real viability:
- Live migration / hotplug: needs
vmRolloutStrategy: LiveUpdate, masquerade networking, migratable storage (RWX), and a target node with the same CPU vendor (you cannot live-migrate IntelβAMD). - Disk hotplug: needs the
HotplugVolumesfeature gate. - Snapshots: need a
VolumeSnapshotClass(Longhorn CSI). - Online expand: needs
allowVolumeExpansion: true.
The UI queries GET /api/capabilities and greys out controls the cluster
can't support, rather than failing at submit time.
Secrets stay localβ
- Cloud-init passwords are generated per-VM and stored in
~/.local/share/tailvm/registry.json(mode 0600). - The Tailscale auth key comes from
~/.config/tailvm/config.yamlor theTS_AUTHKEYenvironment variable (seeded from Bitwarden by the dotfiles Ansible role). - SSH public keys are read from
~/.ssh/id_ed25519.pub(fallbacks:id_rsa.pub,id_ecdsa.pub). - No secrets in git, no secrets in cluster state. The on-cluster deployment has no registry β it reads everything from cluster objects.
Namespace conventionsβ
- Default namespace:
tailvm(overridable with--namespaceor-n). - VM names must match
[a-z0-9][a-z0-9-]*(Kubernetes DNS label). - On-disk state uses
tailvmprefix (~/.local/share/tailvm/,~/.config/tailvm/) for backward compat with the legacy Pythontailvm.
Data flowβ
VM create (containerdisk)β
corral create web --kubevirt --image fedora
β
ββ catalog.Find("fedora") β containerDisk URI
ββ kubevirt.NewClient(ns).CreateVM(opts) β kubectl apply VM manifest
β ββ cloud-init: SSH key + Tailscale auth key + password
β ββ containerDisk registry URL
ββ registry.Set("web", {backend, namespace, password})
VM create (bootc)β
corral bootc create dev --image quay.io/centos-bootc/...
β
ββ kubevirt.BootcBuildDisk() β block PVC + short-lived builder VM
β ββ builder VM: bootc install to-disk (composefs/ostree auto-detected)
β ββ Progress streams to stderr / task log
ββ kubevirt.GenerateBootcVM() β VM manifest (UEFI-boots the disk)
ββ kubevirt.Apply(vm) β kubectl apply
ββ registry.Set("dev", {backend, namespace})
Same flow for the web UI, but the build runs as a background task polled at
GET /api/tasks/{id} with a live log displayed in the browser.
Web UI SSHβ
Browser: click SSH in VM detail panel
β
ββ GET /api/vms/{ns}/{name} β get VM info
ββ spawn: virtctl ssh {user}@{ns}/{name}
β ββ xterm.js + WebSocket terminal in browser
ββ (or copy the virtctl ssh command to clipboard)
The web SSH delegates to the browser's ability to open terminal URLs β it doesn't proxy SSH through the server.
Web UI consoleβ
Browser: Console tab β VNC or Serial
β
ββ VNC: WebSocket β /api/vnc/{ns}/{name}
β ββ server: virtctl vnc --proxy-only β WS bridge
β ββ browser: noVNC canvas
ββ Serial: WebSocket β /api/tty/{ns}/{name}
ββ server: virtctl console β WS bridge
ββ browser: xterm.js terminal
Build & CIβ
go build -o corral . # core binary (no bootc)
go build -tags bootc -o corral . # with bootc pipeline
# Container image (for on-cluster deployment)
docker build -t ghcr.io/tuna-os/corral .
# β Alpine + corral + kubectl + virtctl
# CI (.github/workflows/ci.yml)
# β go test ./..., go vet ./..., docker build, push to ghcr.io
On-cluster deploymentβ
The deploy/corral-web.yaml manifest:
- Creates the
tailvmnamespace (privileged pod-security) - ServiceAccount + scoped ClusterRole (VM lifecycle, subresources, snapshots, CDI, PVCs, nodes, events, metrics, instancetypes, etc.)
- Deployment (ghcr.io/tuna-os/corral:latest, port 8006)
- ClusterIP Service (port 80 β 8006)
- Tailscale Ingress (
ingressClassName: tailscale) β TLS atcorral.<tailnet>.ts.net
Tailnet membership is the authentication β never bind a public interface.
Authorization is an adminGate middleware: CORRAL_ADMINS (tailnet logins)
gates mutating requests, with everyone else read-only (see
ADR-0003). Unset = single-user/open.
Plugin systemβ
Plugins are standalone executables named corral-<name>, discovered in
~/.local/share/corral/plugins (and $PATH). When you run corral <name>,
the root command checks if <name> is a known subcommand; if not, it
dispatches to the plugin. CORRAL_PLUGIN=<name> is exported so plugins know
how they were invoked.
The marketplace is the marketplace/index.json file in the repo, fetched
from GitHub raw at runtime. Each entry has a name, description, version, and
platform-specific download URLs. Installation downloads the binary, sets the
executable bit, and places it in the plugin dir.