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

143 lines
4.0 KiB
Go

package runtime
import (
"context"
"fmt"
"strings"
"git.cloudinit.dev/coreci/orca/internal/sshpush"
)
// PodmanRuntime implements Runtime for the "podman" one_of. It runs
// `podman` on the peer over the SSH-push transport (P01). The
// transport is injected via the constructor (dependency injection).
//
// Container lifecycle:
//
// - Prepare: `podman pull <image>`
// - Start: `podman run -d --name orca-<alloc-id> <image> <command>`
// - Stop: `podman stop <name>` then `podman rm <name>`
// - Status: `podman inspect --format '{{.State.Running}}' <name>`
//
// The container name is `orca-<alloc-id>` (sanitized to lowercase +
// alnum). The runtime keeps no in-process state — each call is a fresh
// SSH exec against the peer.
type PodmanRuntime struct {
transport *sshpush.Transport
}
// NewPodmanRuntime returns a PodmanRuntime backed by the given transport.
func NewPodmanRuntime(t *sshpush.Transport) *PodmanRuntime {
return &PodmanRuntime{transport: t}
}
// containerName returns the deterministic container name for an alloc.
func containerName(alloc *Alloc) string {
id := strings.ToLower(alloc.ID)
id = strings.Map(func(r rune) rune {
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '-' || r == '_' {
return r
}
return '-'
}, id)
return "orca-" + id
}
// Prepare pulls the image on the peer.
func (p *PodmanRuntime) Prepare(ctx context.Context, alloc *Alloc) error {
image, err := imageFor(alloc)
if err != nil {
return err
}
cmd := fmt.Sprintf("podman pull %q", image)
if _, err := p.transport.Exec(ctx, alloc.Node, cmd); err != nil {
return fmt.Errorf("podman: pull: %w", err)
}
return nil
}
// Start runs `podman run -d --name <name> <image> <command>`.
func (p *PodmanRuntime) Start(ctx context.Context, alloc *Alloc) (int, error) {
image, err := imageFor(alloc)
if err != nil {
return 0, err
}
cmdStr, _ := commandFor(alloc)
name := containerName(alloc)
cmd := fmt.Sprintf("podman run -d --name %s %q %s", name, image, cmdStr)
out, err := p.transport.Exec(ctx, alloc.Node, cmd)
if err != nil {
return 0, fmt.Errorf("podman: run: %w", err)
}
// The container ID is the first 12 chars of the printed hash. We
// don't keep it — Stop/Status use the name — but return a stable
// synthetic PID derived from the first 4 bytes of the hash for
// the interface contract.
cid := strings.TrimSpace(string(out))
return podmanCidToPID(cid), nil
}
// Stop stops and removes the container.
func (p *PodmanRuntime) Stop(ctx context.Context, alloc *Alloc) error {
name := containerName(alloc)
if _, err := p.transport.Exec(ctx, alloc.Node, fmt.Sprintf("podman stop %s", name)); err != nil {
return fmt.Errorf("podman: stop: %w", err)
}
if _, err := p.transport.Exec(ctx, alloc.Node, fmt.Sprintf("podman rm %s", name)); err != nil {
return fmt.Errorf("podman: rm: %w", err)
}
return nil
}
// Status inspects the container's running state.
func (p *PodmanRuntime) Status(ctx context.Context, alloc *Alloc) (State, error) {
name := containerName(alloc)
cmd := fmt.Sprintf("podman inspect --format '{{.State.Running}}' %s", name)
out, err := p.transport.Exec(ctx, alloc.Node, cmd)
if err != nil {
return StateFailed, fmt.Errorf("podman: inspect: %w", err)
}
v := strings.TrimSpace(string(out))
switch v {
case "true":
return StateRunning, nil
case "false":
return StateStopped, nil
default:
return StateFailed, fmt.Errorf("podman: unexpected inspect output %q", v)
}
}
// podmanCidToPID converts a container ID (hex hash) to a positive int
// PID for the interface contract. It reads up to 4 hex chars.
func podmanCidToPID(cid string) int {
if len(cid) < 1 {
return 1
}
n := len(cid)
if n > 4 {
n = 4
}
var pid int
for i := 0; i < n; i++ {
c := cid[i]
pid = (pid << 4) | int(hexVal(c))
}
if pid <= 0 {
pid = 1
}
return pid
}
func hexVal(c byte) byte {
switch {
case c >= '0' && c <= '9':
return c - '0'
case c >= 'a' && c <= 'f':
return c - 'a' + 10
case c >= 'A' && c <= 'F':
return c - 'A' + 10
}
return 0
}