Skip to content

tuna-os/corral

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

86 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🀠 Corral

Herd your VMs into your tailnet.

You have VMs in two places: quick ones on your laptop, big ones on the Kubernetes cluster in the closet. Two sets of tooling, two networking stories, and none of it reachable from the couch.

Corral fixes that. One command, two backends, and every VM lands inside the one network all your devices already share β€” your Tailscale tailnet.

corral create web --kubevirt --container-disk quay.io/containerdisks/fedora:42
corral ssh web        # from this machine, your laptop, or your phone's terminal

VMs are cattle. Stop treating each one like a networking project.

Why you'll like it

  • Same five commands everywhere. create / start / ssh / viewer / delete work identically whether the VM is local QEMU/KVM or KubeVirt on your cluster. Corral remembers which is which β€” you never specify it again.
  • SSH that just works. Your public key is injected at create time, a fallback password is generated and stored locally, and corral ssh picks the right path: a Kubernetes API tunnel for cluster VMs, a Tailscale-bound port-forward for local ones. Zero config files touched.
  • VMs that join the tailnet themselves. Drop a Tailscale auth key in ~/.config/tailvm/config.yaml (or TS_AUTHKEY) and every cloud-init VM runs tailscale up on first boot β€” it shows up as a real machine on your tailnet, MagicDNS name and all.
  • Extensions with a marketplace. Niche features ship as plugins: corral plugin search, corral plugin install bootc, then corral bootc create dev --image ghcr.io/... builds an OS disk on the cluster from a bootable container image and boots it as a VM. Browse/install from the web UI's Extensions tab too. The core binary stays lean.
  • Point-and-shoot TUI. Run corral bare for a Bubble Tea interface: pick a VM, hit Start / Stop / SSH / VNC / Delete, or toggle which ports (SSH, VNC, RDP, HTTP, …) are published to the tailnet as <name>-vm.your-tailnet.ts.net.
  • A Proxmox-style web UI. corral web serves a dark, mobile-friendly dashboard: datacenter β†’ node β†’ VM tree, live status, create wizard, start/stop/restart/pause, one-click live migration with a target-node picker, multi-select bulk start/stop, tags (chips + tree filter), a per-VM CPU usage sparkline, disk export (qcow2 or raw.gz), your own image/ISO sources alongside the built-in catalog (saved in a ConfigMap), and real consoles in the browser β€” noVNC graphics and an xterm.js serial TTY. It also runs on the cluster itself (deploy/corral-web.yaml), exposed to your tailnet by the Tailscale operator. CLI, TUI, and web all share the same state.
  • One static Go binary. No daemons, no controllers to install, no client-side K8s SDK. It drives kubectl/virtctl/systemctl β€” the tools you already trust β€” and gets out of the way.

Install

git clone https://github.com/tuna-os/corral
cd corral
go build -o corral .
install corral ~/.local/bin/

Or grab a prebuilt corral-linux-{amd64,arm64} from the CI artifacts.

Optional: corral completion fish | source (bash/zsh/fish, via Cobra).

Development tasks run through just: just lists them β€” build, test, vet, ci (the pre-push gate), and regen-catalog (refresh the Universal Blue / Bluefin / TunaOS bootc catalog from ghcr, dropping anything not rebuilt in ~60 days).

Quick start

# Local VM on this machine (QEMU/KVM, runs as a systemd user service)
corral create scratch --iso ~/Downloads/ubuntu-24.04.iso
corral start scratch            # VNC + SSH bound to this host's Tailscale IP

# Cluster VM (KubeVirt) from a container disk
corral create web --kubevirt --container-disk quay.io/containerdisks/ubuntu:24.04
corral start web
corral ssh web

# Cluster VM from an installer ISO (CDI imports it for you, progress in `corral list`)
corral create bluefin --kubevirt --iso https://download.example/bluefin.iso

# Bootable container β†’ running VM, disk built on-cluster (bootc extension)
corral plugin install bootc
corral bootc create dev --image quay.io/centos-bootc/centos-bootc:stream9
corral start dev && corral ssh dev -u root

# Everything, both backends, one table
corral list

How VMs are reached

Local (qemu) Cluster (kubevirt)
SSH host's Tailscale IP, forwarded port virtctl ssh API tunnel β€” works with zero exposure
VNC host's Tailscale IP, vnc://… virtctl vnc proxy
Published ports β€” (host is already on the tailnet) per-VM proxy Service tagged tailscale.com/expose β†’ <name>-vm.<tailnet>.ts.net
VM on the tailnet itself β€” automatic via cloud-init when an auth key is configured

Nothing is ever bound to 0.0.0.0 β€” local VM ports attach to the host's Tailscale IP only.

Extensions & the marketplace

Corral has a krew-style plugin system. Plugins are standalone corral-<name> binaries in ~/.local/share/corral/plugins; once installed they run as corral <name> …. A curated marketplace (marketplace/index.json) lists installable ones:

corral plugin search
corral plugin install bootc
corral plugin list

The web UI has an Extensions tab to browse and install the same plugins.

The bootc plugin

The flagship extension, corral bootc, turns a bootable container image into a running VM without any local tooling:

  1. Corral provisions a block-mode PVC and runs a short-lived builder VM (not a pod) that runs bootc install to-disk onto it β€” so the VM's own kernel does the filesystem work. This is what lets it install images the node kernel can't handle, e.g. Universal Blue desktops (bluefin/dakota) that need btrfs + composefs. The right backend (ostree vs composefs) and filesystem are auto-detected from the image; your SSH key is baked in and sshd enabled.
  2. Build logs stream to your terminal live.
  3. The finished disk is self-bootable (GPT + ESP + bootloader), so the final VM UEFI-boots it β€” no kernelBoot, no bootloader gymnastics.

corral bootc rebuild|upgrade|switch re-bakes the disk from a new image (the SSH key is re-applied across the --wipe). Rebuild your OS in CI, corral create it as a VM in minutes.

Faster builds: deploy deploy/registry-cache.yaml (an on-cluster pull-through cache for ghcr.io) and the builder routes image pulls through it automatically β€” no config. Disable with CORRAL_REGISTRY_MIRROR=off.

The backup plugin

corral backup ships VM disk backups to any S3/R2 bucket via rclone:

corral backup create web --dest r2:backups/corral      # export + upload
corral backup restore web-restored --src r2:backups/corral/web-….img.gz --size 20Gi
corral backup list --dest r2:backups/corral

Needs rclone configured for your remote (rclone config) and virtctl.

Configuration

# ~/.config/tailvm/config.yaml
tailscale:
  auth_key: tskey-...   # or export TS_AUTHKEY; flag --ts-authkey overrides

corral config shows what's active. State lives in ~/.local/share/tailvm/ (registry + local VM disks) β€” shared with the legacy tailvm tool, so existing VMs keep working.

Environment overrides (handy for the in-cluster web deployment):

Variable Effect
CORRAL_NAMESPACE default VM namespace (code default: corral-vms)
CORRAL_ADMINS comma-separated tailnet logins allowed to mutate; unset = single-user/open (see Web UI)
CORRAL_REGISTRY_MIRROR override/disable (off) the bootc builder's pull-through cache host
CORRAL_BOOTC_BUILD_TIMEOUT minutes to wait for a bootc builder VM (default 45)

Web UI

corral web                                  # http://127.0.0.1:8006
corral web --addr "$(tailscale ip -4):8006" # share with your tailnet

Or serve it from the cluster (public image built by CI to ghcr.io/hanthor/corral):

kubectl apply -f deploy/corral-web.yaml
# β†’ https://corral.<tailnet>.ts.net

Setting up from scratch? Build your own KubeVirt "Proxmox" walks through the whole stack β€” KubeVirt + CDI, the feature gates, Longhorn + snapshots, Multus, and deploying Corral.

Tailnet membership is the authentication β€” never bind a public interface. For authorization, set CORRAL_ADMINS to a comma-separated list of tailnet logins: listed users can mutate; everyone else gets a read-only UI and mutating API calls are rejected (403). Unset = single-user/open (the default). Identity comes from the Tailscale ingress headers β€” see ADR-0003. Feature roadmap: WEBUI-PLAN.md.

Command reference

corral                  TUI
corral web              Proxmox-style web UI [--addr host:port]
corral list             all VMs, both backends
corral create <name>    --kubevirt | (default: local qemu)
                        --mem 4G --cpu 2 --disk 20G --iso … --container-disk …
                        --pvc … --node … --cloud-init … --instancetype … --ts-authkey …
corral plugin           search | install <name> | list | remove <name>   (extensions)
corral start|stop <name>
corral restart <name>   restart a VM
corral pause|unpause    [kubevirt] freeze / resume a running VM
corral scale <name>     [kubevirt] --cpu N --mem 8G (live hotplug when possible)
corral migrate <name>   [kubevirt] --node X  live-migrate to another node
corral adddisk <name>   [kubevirt] --size 10Gi  hotplug a new disk
corral rmdisk <name>    [kubevirt] --volume PVC  detach a hotplugged disk
corral snapshot …       [kubevirt] create | ls | restore | rm
corral ssh <name>       [-u user] [-i key] [-c cmd] [-p port] [--password …]
corral viewer <name>    VNC via xdg-open
corral logs <name>      journald (local) / virt-launcher (cluster)
corral info <name>      raw JSON
corral delete <name>    [-f] removes VM, disks, proxy, registry entry

KubeVirt feature support & cluster requirements

Corral exposes the Proxmox-style operations above through both the CLI/TUI and the web UI (editable Hardware tab, Snapshots tab, in-browser consoles). What actually works depends on the cluster:

  • Change CPU / RAM β€” always works. On a genuinely live-migratable VM it is hotplugged with no downtime; otherwise Corral applies it in a single stopβ†’patchβ†’start. New VMs are created sockets-based with maxSockets / maxGuest headroom so they can hotplug.
  • Live migration / live hotplug β€” needs vmRolloutStrategy: LiveUpdate, masquerade networking (Corral sets this), migratable storage (RWX), and a target node with the same CPU vendor. You cannot live-migrate a running VM between an Intel and an AMD host, so on a mixed-vendor cluster Corral detects this and falls back to the offline path instead of hanging.
  • Add disk (hotplug) β€” needs the HotplugVolumes feature gate.
  • Online disk expansion β€” needs a StorageClass with allowVolumeExpansion: true.
  • Snapshots / clone / restore β€” need a VolumeSnapshotClass for VMs with persistent disks (ephemeral container-disk VMs can snapshot their definition without one). The web UI greys out controls the cluster can't support.

Full design document: SPEC.md.

Documentation

Requirements

  • Local backend: qemu-system-x86_64 + KVM, systemd user session
  • Cluster backend: kubectl context with KubeVirt (+ CDI for ISO import), virtctl; Tailscale operator for published ports
  • tailscale on the host; sshpass only if you use password SSH

Why "Corral"?

A corral is one fence that holds the whole herd. Your VMs are the cattle; your tailnet is the fence. (Formerly known as tailvm.)

About

🀠 Herd your VMs into your tailnet β€” QEMU + KubeVirt VM manager with a Proxmox-style web UI

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors