Files
orca/internal/runtime/wasm.go
T
Jon Chery 872ffcaf25 feat(P07a/b/c): runtime abstraction — 5 backends (process/podman/wasm/pve-vm/pve-ct), C-01 satisfied (REQ-078)
P07a/b/c — Runtime abstraction interface + 5 implementations.

Runtime interface (internal/runtime/runtime.go, REQ-078):
- Runtime interface { Prepare, Start, Stop, Status }. Alloc struct carries
  Runtime field (changeable on migration per R-004). Registry keyed by
  runtime.one_of. DefaultRegistry(transport) registers all 5.

Process (internal/runtime/process.go):
- ProcessRuntime wraps os/exec (LOCAL testing only; production uses systemd
  emitter). SIGTERM grace 10s then SIGKILL.

Podman (internal/runtime/podman.go):
- PodmanRuntime via sshpush.Transport. podman pull/run/stop/rm/inspect.

Wasm (internal/runtime/wasm.go, gate C-01 SATISFIED):
- WasmRuntime uses wasmtime CLI (apt-installed on peer) via SSH exec. NO CGO
  — does NOT import bytecodealliance/wasmtime-go. CGO_ENABLED=0 build
  passes. D-002 cross-compile story preserved. D-187 recorded.

PVE (internal/runtime/pve.go):
- PveVMRuntime (qm create/start/stop/status) + PveCTRuntime (pct
  create/start/stop/status) via sshpush.Transport. VMID = hash(alloc.ID)%99999.

C-01 evaluation: internal/runtime/C01_WASMTIME_CGO_EVAL.md. Auto-decision
(full autonomy): wasmtime remains primary; CLI-via-SSH avoids CGO entirely.
D-187 in PROJECT.md.

23 packages pass, 20 bats pass, gofmt clean, verify-reqs 90 consistent.
92.7% coverage on internal/runtime.

---ci---
project: orca
phase: P07a/b/c
milestone: v0.9
status: execute
---/ci---
2026-08-05 18:31:25 +00:00

100 lines
3.2 KiB
Go

package runtime
import (
"context"
"fmt"
"strings"
"git.cloudinit.dev/coreci/orca/internal/sshpush"
)
// WasmRuntime implements Runtime for the "wasm" one_of. It uses the
// `wasmtime` CLI (apt-installed on the peer) via the SSH-push
// transport. It does NOT use the Go wasmtime binding
// (github.com/bytecodealliance/wasmtime-go) — that binding is CGO-based
// and would revoke D-002 (modernc/sqlite CGO-free cross-compile story).
// See C01_WASMTIME_CGO_EVAL.md for the C-01 grill gate evaluation and
// the auto-decision D-187.
//
// Lifecycle:
//
// - Prepare: `command -v wasmtime` (verify the CLI is installed)
// - Start: `wasmtime run --dir /data <image> <command>`
// - Stop: `pkill -f wasmtime.*<alloc-id>`
// - Status: `pgrep -f wasmtime.*<alloc-id>`
//
// The image is a .wasm file path on the peer. For v0.9 it is
// pre-staged (downloaded out-of-band); the full OCI pull lands in v0.10.
type WasmRuntime struct {
transport *sshpush.Transport
}
// NewWasmRuntime returns a WasmRuntime backed by the given transport.
func NewWasmRuntime(t *sshpush.Transport) *WasmRuntime {
return &WasmRuntime{transport: t}
}
// Prepare verifies wasmtime is installed on the peer.
func (w *WasmRuntime) Prepare(ctx context.Context, alloc *Alloc) error {
if _, err := w.transport.Exec(ctx, alloc.Node, "command -v wasmtime"); err != nil {
return fmt.Errorf("wasm: wasmtime not installed on peer: %w", err)
}
return nil
}
// Start runs `wasmtime run --dir /data <image> <command>` on the peer.
// The PID returned is a synthetic derived from the alloc ID hash.
func (w *WasmRuntime) Start(ctx context.Context, alloc *Alloc) (int, error) {
image, err := imageFor(alloc)
if err != nil {
return 0, err
}
cmdStr, _ := commandFor(alloc)
// Tag the process so pkill/pgrep can find it by alloc ID. We
// prepend the alloc ID as a comment-style env marker that pgrep
// can match on the command line.
cmd := fmt.Sprintf("ORCA_ALLOC_ID=%s wasmtime run --dir /data %q %s",
alloc.ID, image, cmdStr)
if _, err := w.transport.Exec(ctx, alloc.Node, cmd); err != nil {
return 0, fmt.Errorf("wasm: start: %w", err)
}
return allocIDHash(alloc.ID), nil
}
// Stop kills the wasmtime process matching the alloc ID.
func (w *WasmRuntime) Stop(ctx context.Context, alloc *Alloc) error {
cmd := fmt.Sprintf("pkill -f %q", "wasmtime.*"+alloc.ID)
if _, err := w.transport.Exec(ctx, alloc.Node, cmd); err != nil {
return fmt.Errorf("wasm: stop: %w", err)
}
return nil
}
// Status reports whether the wasmtime process for the alloc is running.
func (w *WasmRuntime) Status(ctx context.Context, alloc *Alloc) (State, error) {
cmd := fmt.Sprintf("pgrep -f %q", "wasmtime.*"+alloc.ID)
out, err := w.transport.Exec(ctx, alloc.Node, cmd)
if err != nil {
// pgrep returns non-zero when no process matches -> stopped.
return StateStopped, nil
}
if strings.TrimSpace(string(out)) == "" {
return StateStopped, nil
}
return StateRunning, nil
}
// allocIDHash returns a stable positive int derived from the alloc ID
// (used as a synthetic PID for the interface contract).
func allocIDHash(id string) int {
var h uint32
for _, c := range id {
h = h*31 + uint32(c)
}
pid := int(h % 99999)
if pid <= 0 {
pid = 1
}
return pid
}