4b70e31cf4
Critical fixes:
- logs --job: validate ^[A-Za-z0-9_-]+$ + shellQuote (was %q backtick RCE)
- pprof: isLoopback treats empty host as bind-all (was :6060 bypass)
- backup restore: filepath.Rel containment check (was tar-slip via a/../..)
- WebAuthn reg auth deferred to P04 (requires session infra)
High fixes:
- txn rollback/show/apply: validate ^T-[0-9a-f]{16}$ + shellQuote
- nft diff --against: validate txn ID before filepath.Join
- drain stopAlloc: validate allocID ^[A-Za-z0-9_-]+$
- cluster_compat: shellQuote peer dir name
- podman image: shellQuote (was %q backtick injection)
- nft TrustedProbes: net.ParseIP/CIDR validation + split v4/v6 sets
- sudoers: validate --proxmox-user/--proxmox-role ^[a-zA-Z_][a-zA-Z0-9_-]{0,31}$
fixed path /etc/sudoers.d/orca; shellQuote pveum/useradd; validateSudoers
checks actual file
- nft country block: validate ^[A-Z]{2}$ (was len==2 only)
New file: internal/cli/validate.go (shared validators + shellQuote)
All 38 Go test packages pass. go vet + gofmt clean.
---ci---
project: orca
phase: 2
milestone: v0.13
status: complete
requirements:
covered: [150]
---/ci---
143 lines
4.0 KiB
Go
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 %s %s", shellQuote(name), shellQuote(image), shellQuote(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", shellQuote(name))); err != nil {
|
|
return fmt.Errorf("podman: stop: %w", err)
|
|
}
|
|
if _, err := p.transport.Exec(ctx, alloc.Node, fmt.Sprintf("podman rm %s", shellQuote(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", shellQuote(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
|
|
}
|