Skip to content

Latest commit

Β 

History

History
311 lines (260 loc) Β· 16.5 KB

architecture.md

File metadata and controls

311 lines (260 loc) Β· 16.5 KB

Corral Architecture

Full project docs: README.md, SPEC.md, API reference, test 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 (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()

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:

  1. CLI β€” Cobra subcommands (corral create, corral ssh, etc.)
  2. TUI β€” interactive terminal UI in Bubble Tea (bare corral)
  3. 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 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.

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. If the registry entry is missing, it falls back to a live probe. The user never specifies --kubevirt again.

Bootc as a plugin

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.

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, 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 HotplugVolumes feature 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.

Secrets stay local

  • 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.yaml or the TS_AUTHKEY environment 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 --namespace or -n).
  • VM names must match [a-z0-9][a-z0-9-]* (Kubernetes DNS label).
  • On-disk state uses tailvm prefix (~/.local/share/tailvm/, ~/.config/tailvm/) for backward compat with the legacy Python tailvm.

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

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.

On-cluster deployment

The deploy/corral-web.yaml manifest:

  1. Creates the tailvm namespace (privileged pod-security)
  2. ServiceAccount + scoped ClusterRole (VM lifecycle, subresources, snapshots, CDI, PVCs, nodes, events, metrics, instancetypes, etc.)
  3. Deployment (ghcr.io/tuna-os/corral:latest, port 8006)
  4. ClusterIP Service (port 80 β†’ 8006)
  5. Tailscale Ingress (ingressClassName: tailscale) β†’ TLS at corral.<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.

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. 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.