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 ` // - Start: `podman run -d --name orca- ` // - Stop: `podman stop ` then `podman rm ` // - Status: `podman inspect --format '{{.State.Running}}' ` // // The container name is `orca-` (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 `. 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 }