Files
orca/internal/runtime/runtime.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

177 lines
6.0 KiB
Go

// Package runtime implements the runtime abstraction (REQ-078, I-B-006).
//
// The Runtime interface decouples the scheduler/CLI from the underlying
// execution backend. Five implementations are provided:
//
// - ProcessRuntime ("process") — wraps os/exec; LOCAL testing only.
// - PodmanRuntime ("podman") — SSH-push podman run on the peer.
// - WasmRuntime ("wasm") — wasmtime CLI via SSH (NO CGO; see
// C01_WASMTIME_CGO_EVAL.md for the C-01 grill gate evaluation).
// - PveVMRuntime ("pve-vm") — `qm` over SSH to a Proxmox peer.
// - PveCTRuntime ("pve-ct") — `pct` over SSH to a Proxmox peer.
//
// The Registry is keyed by the runtime.one_of frontmatter value. The
// Alloc carries a Runtime field that can change on migration (R-004).
package runtime
import (
"context"
"fmt"
"git.cloudinit.dev/coreci/orca/internal/jobspec"
)
// State is the lifecycle state of an alloc as observed by a Runtime.
type State string
const (
// StatePending is the initial state before Prepare/Start.
StatePending State = "pending"
// StateRunning means the runtime reports the workload as up.
StateRunning State = "running"
// StateStopped means the workload exited cleanly (Stop called
// or the process finished with exit 0).
StateStopped State = "stopped"
// StateFailed means the workload exited non-zero or could not
// be reached.
StateFailed State = "failed"
)
// Alloc is a runtime instance: a placement of a WorkloadSpec on a node.
// The Runtime field is the runtime.one_of value used to dispatch to the
// correct Runtime implementation; it can change on migration (R-004).
type Alloc struct {
ID string
Spec *jobspec.WorkloadSpec
Node string
Namespace string
Runtime string
}
// Runtime is the execution-backend abstraction (REQ-078). Each method
// takes a context for cancellation/timeout. Implementations wrap a
// different execution backend (process, podman, wasm, pve-vm, pve-ct).
//
// Prepare is idempotent; Start/Stop/Status operate on the prepared
// runtime. The returned PID from Start is best-effort (container
// runtimes return the container ID hash as a synthetic PID).
type Runtime interface {
// Prepare provisions prerequisites for the alloc (image pull,
// vm create, etc.). It is idempotent.
Prepare(ctx context.Context, alloc *Alloc) error
// Start launches the workload and returns a best-effort PID (or
// container/VM identifier encoded as a positive integer).
Start(ctx context.Context, alloc *Alloc) (pid int, err error)
// Stop terminates the workload, gracefully first then forcibly
// after a grace period.
Stop(ctx context.Context, alloc *Alloc) error
// Status reports the current State of the alloc.
Status(ctx context.Context, alloc *Alloc) (State, error)
}
// Registry maps runtime.one_of values to Runtime implementations. The
// zero value is NOT usable; construct one with NewRegistry.
type Registry struct {
runtimes map[string]Runtime
}
// NewRegistry returns an empty Registry.
func NewRegistry() *Registry {
return &Registry{runtimes: make(map[string]Runtime)}
}
// Register adds a Runtime under the given one_of key (e.g. "process",
// "podman", "wasm", "pve-vm", "pve-ct"). Registering the same key twice
// replaces the prior implementation (last-wins) — this is intentional
// so tests can override.
func (r *Registry) Register(name string, rt Runtime) {
if r.runtimes == nil {
r.runtimes = make(map[string]Runtime)
}
r.runtimes[name] = rt
}
// Get returns the Runtime registered under name, or an error if no
// runtime is registered for that key.
func (r *Registry) Get(name string) (Runtime, error) {
rt, ok := r.runtimes[name]
if !ok {
return nil, fmt.Errorf("runtime: no backend registered for %q", name)
}
return rt, nil
}
// Prepare dispatches to the Runtime registered for alloc.Runtime. It
// returns an error if the runtime is unknown or Prepare fails.
func (r *Registry) Prepare(ctx context.Context, alloc *Alloc) error {
rt, err := r.Get(alloc.Runtime)
if err != nil {
return err
}
return rt.Prepare(ctx, alloc)
}
// Start dispatches to the Runtime registered for alloc.Runtime.
func (r *Registry) Start(ctx context.Context, alloc *Alloc) (int, error) {
rt, err := r.Get(alloc.Runtime)
if err != nil {
return 0, err
}
return rt.Start(ctx, alloc)
}
// Stop dispatches to the Runtime registered for alloc.Runtime.
func (r *Registry) Stop(ctx context.Context, alloc *Alloc) error {
rt, err := r.Get(alloc.Runtime)
if err != nil {
return err
}
return rt.Stop(ctx, alloc)
}
// Status dispatches to the Runtime registered for alloc.Runtime.
func (r *Registry) Status(ctx context.Context, alloc *Alloc) (State, error) {
rt, err := r.Get(alloc.Runtime)
if err != nil {
return StateFailed, err
}
return rt.Status(ctx, alloc)
}
// Names returns the registered runtime keys (unsorted).
func (r *Registry) Names() []string {
out := make([]string, 0, len(r.runtimes))
for k := range r.runtimes {
out = append(out, k)
}
return out
}
// commandFor returns the command string to run for an alloc. If the
// alloc has a top-level Runtime block with a Command, that is used.
// Otherwise the first task's command is used (task-group allocs, P06).
// Returns ("", error) if no command can be derived.
func commandFor(alloc *Alloc) (string, error) {
if alloc == nil || alloc.Spec == nil {
return "", fmt.Errorf("runtime: nil alloc or spec")
}
if alloc.Spec.Runtime != nil && alloc.Spec.Runtime.Command != "" {
return alloc.Spec.Runtime.Command, nil
}
if len(alloc.Spec.Tasks) > 0 && alloc.Spec.Tasks[0].Command != "" {
return alloc.Spec.Tasks[0].Command, nil
}
return "", fmt.Errorf("runtime: alloc %s has no command", alloc.ID)
}
// imageFor returns the image/wasm-file path for an alloc (podman/wasm).
func imageFor(alloc *Alloc) (string, error) {
if alloc == nil || alloc.Spec == nil || alloc.Spec.Runtime == nil {
return "", fmt.Errorf("runtime: nil alloc/spec/runtime")
}
if alloc.Spec.Runtime.Image == "" {
return "", fmt.Errorf("runtime: alloc %s has no image", alloc.ID)
}
return alloc.Spec.Runtime.Image, nil
}