--type linux (REQ-161): - internal/linux/bootstrap.go: SSH bootstrap for generic Linux workers (orcas pubkey, system user, drift-events dir; no PVE role/sudoers) - internal/cli/node.go: joinLinux function + --type linux dispatch - peer-setup kept as documented fallback UAT plan (REQ-162): - docs/uat.md: 3-host topology (lead Ubuntu + pve01 Proxmox + worker01 Ubuntu), 22 step-by-step commands, 35-claim matrix, Proxmox prerequisite + alternative 3xUbuntu path (C-48), signoff procedure UAT signoff script (REQ-163, C-47): - scripts/uat-signoff.sh: 35 idempotent read-only assertions, exit 0 iff all pass. Includes 4 critical-path assertions: job deploys to remote, ACL deny-by-default, seal/unseal round-trip, OIDC health - scripts/uat-smoke.sh: 13 CI-tested pure-CLI assertions for .coreci.yml Tests: node join --type linux test, fingerprint test updated, smoke test all 13 pass. ---ci--- project: orca phase: 12 milestone: v0.13 status: complete requirements: covered: [161, 162, 163] ---/ci---
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.commandin each example uses/bin/sleep 3600(for long-running services) or/bin/echo(for one-shot jobs) so thatorca job run <file>.mdsucceeds 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 versionworks) - 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 node join --type proxmox --host 192.168.1.101 --ssh-key ~/.ssh/orca_ed25519
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: Serviceimplies 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.1opts 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: 0per 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 theschedule:frontmatter block (v0.9 parser gap). Thelog-shipperexample useskind: Servicewithcount: 1and anode.roleconstraint as a workaround. - Secret resolution:
env: { KEY: { from: "secret:..." } }is parsed but not resolved toEnvironmentFile=/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.