Full project docs: README.md, SPEC.md, API reference, test plan, setup guide
cmd/ Cobra CLI (root, subcommands, TUI)
βββ root.go entrypoint, plugin dispatch, post-quit actions
βββ commands.go list, start, stop, ssh (TCP/vsock), 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 root model (state, Update loop, list/doctor/detail views, context filter)
βββ tui_items.go TUI list items (VMs, CTs, actions) and which actions apply to each
βββ tui_actions.go TUI action dispatch (run selected action, SSH/viewer/export/clone)
βββ tui_delegates.go TUI list delegates (VM rows, action menu rows)
βββ tui_edit.go TUI port-toggle edit model and port helpers
βββ tui_hwedit.go TUI hardware (CPU/RAM) edit model
βββ tui_modes.go TUI screen modes and the legal transitions between them
βββ tui_screens.go TUI screens: one key handler and one view per mode
βββ tui_ops.go TUI backend work run as tea.Cmds off the Update loop, and their result messages
βββ tui_views.go TUI views at parity with the web UI (snapshots via pkg/snapshot, events, template mark, CT scaling)
βββ 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)
βββ backend/ the parity matrix + conformance tests; see docs/backend-parity.md
βββ 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
βββ proxmoxbe/ Proxmox VE as a backend β HTTPS API + token; see docs/adr/0009
β (pkg/proxmox is the compat server β the other direction)
βββ 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 Serve: startup, embedded static assets, registry store
βββ routes.go route table (routes/newMux), cluster-only guard, console websockets, themed index
βββ middleware.go request pipeline (peer token β admin gate β activity β peer gate) and host allowlist
βββ respond.go JSON response and error-status helpers
βββ tasks.go async task tracker (asyncTask, taskTracker) behind GET /api/tasks/{id}
βββ tasklog.go task log ring (GET /api/tasklog) and task status handler
βββ inventory.go VM list across backends and peers, /api/v1/inventory, /api/v1/meta
βββ health.go /healthz and /readyz probes
βββ create.go POST /api/vms: dispatch to the bootc/Windows/generic/local/backend create strategies
βββ createtarget.go create-target resolution over the configured contexts, /api/contexts
βββ vms.go VM info, power actions, delete
βββ migrate.go live migration as a tracked task
βββ export.go disk export (raw.gz, qcow2)
βββ nodes.go node list, with this host as a synthetic node
βββ console.go VNC and TTY websocket bridges (VM VNC/serial + CT exec)
βββ 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 entry module: poll loop, content router, boot, mobile drawer
βββ state.js shared state store and event bus (select, inventory, tasks)
βββ api.js fetch wrapper, VM/CT API URLs and selection keys
βββ tree.js sidebar tree (Server/Namespace/Pool views, VMs+CTs merged), filter, multi-select
βββ menus.js context menu items per inventory object
βββ tags.js VM tag chips
βββ console.js noVNC, serial terminal and IronRDP consoles, console tabs
βββ create.js create VM/CT dialogs, OS wizard, bootc build streaming
βββ dock.js task panel (Alpine.js island)
βββ content/ one module per screen: datacenter, node, namespace, vm, vm-table, ct,
β multiview, hostpower, doctor, extensions, settings
βββ ui/ primitives: dom.js (query, toast, escape), menu.js (context menu)
βββ 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 production namespace, RBAC, web Deployment, Service, Tailscale Ingress
βββ corral-web-e2e.yaml isolated web Deployment for live end-to-end tests
βββ corral-web-servicemonitor.yaml optional Prometheus Operator scrape config
βββ corral-web-alerts.yaml optional PrometheusRule alerts for collection health
βββ registry-cache.yaml optional pull-through cache for bootc image pulls
βββ multus/ pinned Multus DaemonSet and test NetworkAttachmentDefinition
βββ grafana/corral-dashboard.json Grafana fleet-health dashboard
docs/ Documentation
βββ api.md REST API reference
βββ architecture.md This file
βββ adr/ Architecture Decision Records (0001-0013)
βββ deployment-modes.md web reachability, authentication, and supervision modes
βββ backend-*.md backend support, parity, and setup guides
βββ testing.md, troubleshooting.md, user-guide.md
βββ rfc/, runbooks/, releases/, blog/, research/
βββ 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()
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.
corral is one Go binary that serves three interfaces:
- CLI β Cobra subcommands (
corral create,corral ssh, etc.) - TUI β interactive terminal UI in Bubble Tea (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.
The web UI is native ES modules and CSS embedded via //go:embed. There is
no Node, no bundler, no build step. Corral vendors the console libraries
(xterm.js, noVNC, iron-remote-desktop, gridstack and uPlot) under
pkg/web/static/vendor with their provenance. Corral does not fetch them from
a CDN. Therefore a host with no outbound network still serves a console that
works. Heroicons are inline SVGs.
For a new screen, dock panel, widget, tree view or resizable edge, see the extension points for the web UI. That page also gives the rules for DOM that holds a live connection.
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. If the registry entry is missing,
it falls back to a live probe. The user never specifies --kubevirt again.
Bootc (which builds 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 a build with
-tags bootc.
The web UI checks GET /api/capabilities β bootc: true/false and
shows/hides the bootc source option to match.
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, and migratable storage (RWX). It also needs 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, so the user does not see a failure at submit time.
- Corral generates a cloud-init password for each VM and stores it 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.
- 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.
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})
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.
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.
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
CONTRIBUTING.md and Justfile are
authoritative for local validation. This gate checks format, vet, build, race
tests, and lint for both plain and bootc tag sets. A separate target runs
checks on browser modules. Containerfile packages the CLI with kubectl and
virtctl for cluster deployment.
For web reachability, authentication, probes, and operation modes, see deployment modes. The deployment manifests listed above are the source of truth for the cluster shape.
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 requests that change state, with everyone else read-only (see
ADR-0003). Unset = single-user/open.
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. The root command exports CORRAL_PLUGIN=<name>, so
each plugin knows the name that invoked it.
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.