// 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 }