Files
firecracker-orchestrator/main.go
Honza Novak 82c11dd2f8 feat: add web terminal UI with WebSocket console and clone management API
Introduces a browser-based terminal interface backed by xterm.js, served
directly from the binary via an embedded HTML asset.

New HTTP server (`serve [addr]`, default :8080):
  GET  /              — xterm.js terminal UI; ?id=N selects a clone
  GET  /clones        — JSON list of running clone IDs
  POST /clones        — spawn a new clone; returns {"id": N}
  DELETE /clones/{id} — destroy a clone by ID
  GET  /ws/{id}       — WebSocket console for clone {id}
                        binary frames = raw PTY I/O
                        text frames   = JSON resize {"rows":N,"cols":M}

Supporting changes:
- orchestrator: add SpawnSingle() and KillClone(id) for per-clone lifecycle
  management from the HTTP layer
- console: add a resize sideband Unix socket (console-resize.sock) that
  accepts newline-delimited JSON {"rows","cols"} messages and applies them
  to the PTY master via pty.Setsize; the WebSocket handler writes to this
  socket on text frames so browser window resizes propagate into the VM
- deps: add gorilla/websocket v1.5.3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 10:53:41 +00:00

149 lines
3.7 KiB
Go

// fc-orchestrator — Poor man's Firecracker snapshot orchestrator in Go.
//
// Creates a golden VM, snapshots it, then spawns N clones that share the
// base memory file via Firecracker's MAP_PRIVATE (kernel-level COW).
// Rootfs gets a filesystem-level COW copy (reflink where supported).
//
// Usage:
//
// go build -o fc-orch .
// sudo ./fc-orch init
// sudo ./fc-orch golden
// sudo ./fc-orch spawn 10
// sudo ./fc-orch status
// sudo ./fc-orch kill
// sudo ./fc-orch cleanup
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
"runtime"
log "github.com/sirupsen/logrus"
"github.com/kacerr/fc-orchestrator/orchestrator"
)
func main() {
// strip --dev flag before subcommand routing
dev := false
filtered := os.Args[:1]
for _, a := range os.Args[1:] {
if a == "--dev" {
dev = true
} else {
filtered = append(filtered, a)
}
}
os.Args = filtered
if dev {
log.SetReportCaller(true)
log.SetFormatter(&log.TextFormatter{
CallerPrettyfier: func(f *runtime.Frame) (string, string) {
return "", fmt.Sprintf("%s:%d", filepath.Base(f.File), f.Line)
},
})
}
// figure out if we are running as root
if os.Geteuid() == 0 {
fmt.Println("Running with root/sudo privileges!")
} else {
fmt.Println("Running as a normal user.")
}
if len(os.Args) < 2 {
usage()
os.Exit(1)
}
orch := orchestrator.New(orchestrator.DefaultConfig())
switch os.Args[1] {
case "init":
fatal(orch.Init())
case "golden":
fatal(orch.Golden())
case "spawn":
n := 1
if len(os.Args) > 2 {
fmt.Sscanf(os.Args[2], "%d", &n)
}
fatal(orch.Spawn(n))
case "status":
orch.Status()
case "kill":
fatal(orch.Kill())
case "cleanup":
fatal(orch.Cleanup())
case "serve":
addr := ":8080"
if len(os.Args) > 2 {
addr = os.Args[2]
}
fatal(orchestrator.Serve(orch, addr))
case "console":
if len(os.Args) < 3 {
fmt.Fprintf(os.Stderr, "usage: %s console <id>\n", os.Args[0])
os.Exit(1)
}
var id int
fmt.Sscanf(os.Args[2], "%d", &id)
fatal(orchestrator.ConnectConsole(orchestrator.DefaultConfig(), id))
case "_console-proxy":
// Internal subcommand: started by spawnOne, runs as a background daemon.
fs := flag.NewFlagSet("_console-proxy", flag.ContinueOnError)
var id int
var tap string
fs.IntVar(&id, "id", 0, "clone ID")
fs.StringVar(&tap, "tap", "", "TAP device name")
if err := fs.Parse(os.Args[2:]); err != nil {
fmt.Fprintf(os.Stderr, "console-proxy: %v\n", err)
os.Exit(1)
}
fatal(orchestrator.RunConsoleProxy(orchestrator.DefaultConfig(), id, tap))
default:
usage()
os.Exit(1)
}
}
func usage() {
fmt.Fprintf(os.Stderr, `Usage: %s [--dev] <command> [args]
Flags:
--dev log format with source file:line (e.g. file="orchestrator.go:123")
Commands:
init Download kernel + create Alpine rootfs
golden Boot golden VM → pause → snapshot
spawn [N] Restore N clones from golden snapshot (default: 1)
serve [addr] Start terminal web UI (default: :8080)
console <id> Attach to the serial console of a running clone (Ctrl+] to detach)
status Show running clones
kill Kill all running VMs
cleanup Kill VMs + remove all state
Environment:
FC_BIN firecracker binary path (default: firecracker)
FC_BASE_DIR working directory (default: /tmp/fc-orch)
FC_KERNEL vmlinux path
FC_KERNEL_URL vmlinux download URL (default: pinned Firecracker CI build)
FC_ROOTFS rootfs.ext4 path
FC_VCPUS vCPUs per VM (default: 1)
FC_MEM_MIB MiB per VM (default: 128)
FC_BRIDGE bridge name or "none" (default: fcbr0)
`, os.Args[0])
}
func fatal(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "fatal: %v\n", err)
os.Exit(1)
}
}