Files
orca/examples/full-stack/README.md
T
Jon Chery 4c2e59cf3f fix(P06): workloadToTaskSpecs command split + runnable examples
Root cause: orca job run <example>.md failed with fork/exec: no such
file or directory on every example. Two compounding problems:

1. workloadToTaskSpecs (internal/cli/job.go:340) passed the entire
   runtime.command string (e.g. "/usr/bin/httpd -f /etc/orca/web-app/
   httpd.conf") as a single binary path to exec.Command, which then
   looked for a file literally named "/usr/bin/httpd -f ..." and
   failed. The v0.9 markdown parser stores command: as a raw string;
   the legacy HCL path had separate command+args fields. Fix: add
   splitCommand helper that splits on strings.Fields into binary+args,
   with /bin/true fallback for empty commands.

2. The example commands referenced binaries that don't exist on a bare
   Linux machine (/usr/bin/httpd, postgres, api-server, fluent-bit).
   Fix: rewrite the 5 example runtime.command values to use /bin/sleep
   3600 (long-running services) or /bin/echo (one-shot job) so they
   run out-of-the-box. Each file has a Production substitution note
   showing the real binary to use in deployment.

Verified: orca job run examples/full-stack/worker.md now succeeds
(exit 0). All 4 services (web-app, api, log-shipper, postgres) start
correctly (task started, pid assigned). 12 new unit tests pass
(splitCommand: 7 cases, workloadToTaskSpecs: 5 cases). All 5 example
jobspecs still parse + validate (gate C-20). make lint clean.

---ci---
project: orca
phase: 6
milestone: v0.10
status: execute
decisions:
  - id: D-195
    decision: split command string via strings.Fields in workloadToTaskSpecs
    rationale: exec.Command expects binary path + args as separate elements;
      the v0.9 markdown parser stores command: as a single string with no
      args field (unlike legacy HCL). strings.Fields is dep-free and handles
      multiple spaces/tabs. Shell quoting (single/double quotes inside the
      command) is not handled — examples avoid sh -c with quoted strings.
    confidence: 0.95
    alternatives: [shellquote.Split from mvdan/sh (adds dependency)]
lessons:
  - The v0.9 markdown jobspec path needs the same command+args split that
    the legacy HCL path had via separate command/args fields. The parser
    stores command: as a raw string; the CLI must split it before passing
    to exec.Command.
  - Example jobspecs should use /bin/sleep and /bin/echo (binaries that
    exist on every Linux machine) so they run out-of-the-box. Descriptive
    production commands belong in a comment block, not in runtime.command.
---/ci---
2026-08-05 21:23:32 +00:00

6.5 KiB

Full-Stack Example with Ingress

This directory contains a complete multi-service stack deployed with Orca, including Traefik ingress configuration. Each file is a valid Orca jobspec (.md frontmatter) that passes the v0.9 parser and schema validators.

Runnable out-of-the-box: The runtime.command in each example uses /bin/sleep 3600 (for long-running services) or /bin/echo (for one-shot jobs) so that orca job run <file>.md succeeds on any Linux machine without installing any software. Each file has a Production substitution note showing the real binary to use in a deployment (e.g. /usr/bin/httpd, /usr/lib/postgresql/16/bin/postgres).

Stack overview

File Kind Runtime Ingress Description
web-app.md Service process Unix socket (default) Frontend HTTP server, 3 replicas, rolling update
api.md Service process TCP 127.0.0.1:9090 (R-007 opt-in) Backend API, 2 replicas, canary update
worker.md Job process none One-shot batch worker with lifecycle hooks
log-shipper.md Service process Unix socket (metrics) Log shipper on a dedicated node
postgres.md Service process Unix socket Database with volume replication, blue-green update

Rendered artifacts

The rendered/ directory shows what Orca generates on the target nodes when you submit these jobspecs:

File Description
traefik-dynamic-web-app.yaml Traefik dynamic config for the web-app Service
traefik-dynamic-api.yaml Traefik dynamic config for the api Service (TCP bind)
systemd-web-app.service Systemd unit for the web-app alloc
systemd-api.service Systemd unit for the api alloc (with TCP bind marker)
systemd-log-shipper.service Systemd unit for the log-shipper alloc

Walkthrough

Prerequisites

  • Orca installed (orca version works)
  • 2+ Linux nodes reachable over SSH (for multi-node scheduling)
  • Traefik installed on the lead node (watches /etc/traefik/dynamic/)

Step 1: Initialize the cluster

# On the operator laptop
orca init

This creates ~/.orca/ (or /root/.orca with --system), bootstraps the CA, generates the server cert, auto-detects the OS, and registers a localhost node.

Step 2: Join remote nodes

# Join a Proxmox node (v0.9 canonical SSH-push path)
orca node join --type proxmox --host 192.168.1.100 --ssh-user root

# Join a second node
ORCA_PROXMOX_PASSWORD=secret orca node join --type proxmox --host 192.168.1.101

Step 3: Declare node capacity

The CLI-side scheduler uses capacity declarations for bin-packing:

orca node capacity set --cpu 4000 --memory 8192 --disk 100000 --node 192.168.1.100
orca node capacity set --cpu 4000 --memory 8192 --disk 100000 --node 192.168.1.101

Step 4: Create a namespace

orca ns create prod --parent _defaults

This creates ~/.orca/prod/ with db/, jobs/, alloc/, and ns.md.

Step 5: Submit the stack

orca job run web-app.md
orca job run api.md
orca job run worker.md
orca job run log-shipper.md
orca job run postgres.md

Each orca job run parses the .md jobspec, validates it against the schema, schedules it via the CLI-side bin-packing scheduler, and generates the systemd + Traefik artifacts on the target node via SSH-push.

Step 6: Observe placements

orca job list --watch

# Output:
# ID                                   NAME          STATUS   EXIT
# abc-123...                           web-app       running  0
# def-456...                           api           running  0
# ghi-789...                           worker        complete 0
# jkl-012...                           log-shipper   running  0
# mno-345...                           postgres      running  0

Step 7: Inspect rendered artifacts

After submission, the target nodes have:

/etc/systemd/system/orca-v1-web-app.service       # systemd unit
/etc/systemd/system/orca-v1-api.service           # systemd unit (TCP bind)
/etc/traefik/dynamic/orca-web-app.yaml            # Traefik dynamic config
/etc/traefik/dynamic/orca-api.yaml                # Traefik dynamic config
/run/orca/alloc-web-app-0/port-http.sock          # Unix socket (R-007 default)

See the rendered/ directory in this example for the exact file contents.

Step 8: Verify ingress

Traefik watches /etc/traefik/dynamic/ and atomically reloads when a file changes (write-tmp + rename, gate C-10). The web-app is reachable at https://<cluster-domain>/web-app and the API at https://<cluster-domain>/api.

Health checks (/healthz on each backend) ensure Traefik only routes to healthy instances.

Step 9: Drain and rollback

To drain a service (stop traffic, keep the workload running):

# Orca writes a Traefik config with weight:0 on every backend
# (RenderDrain). Traefik stops sending traffic.

To roll back, re-submit the normal jobspec — Orca writes the non-drained Traefik config and Traefik resumes routing.

Ingress model

See docs/ingress.md for the full Traefik ingress reference. Key points:

  • kind: Service implies a Traefik route (D-175).
  • Default bind is a Unix socket at /run/orca/alloc-<id>/port-<name>.sock (R-007).
  • service.bind: 127.0.0.1 opts in to TCP (loopback only).
  • One Traefik dynamic file per Service at /etc/traefik/dynamic/orca-<name>.yaml.
  • Atomic reload via write-tmp + rename (gate C-10).
  • Drain sets weight: 0 per backend.

Validation

All jobspecs in this directory are validated by a Go test:

go test ./examples/full-stack/ -v -run TestExamplesValidate

This test parses each .md file with jobspec.ParseFile and validates it against schema.ValidatorFor(kind) — ensuring every field used in the examples exists in the current WorkloadSpec struct and passes the per-kind validators (gate C-20).

v0.11 forward

The following are not yet implemented in v0.9 and will land in v0.11:

  • DaemonSet schedule: block: the parser does not yet populate the schedule: frontmatter block (v0.9 parser gap). The log-shipper example uses kind: Service with count: 1 and a node.role constraint as a workaround.
  • Secret resolution: env: { KEY: { from: "secret:..." } } is parsed but not resolved to EnvironmentFile=/LoadCredential= until v0.11-P03.
  • Transactional update execution: the update: block's plan is computed but not executed transactionally until v0.11-P10.
  • Socket activation: real socket unit files land in v0.11-P08.