Compare commits

...

10 Commits

Author SHA1 Message Date
Jon Chery a20cdb294c docs(debug): update checkpoint to v0.9.6 (post-hoc fix)
---ci---
project: orca
phase: 99
milestone: v0.10
status: complete
---/ci---
2026-08-05 21:24:08 +00:00
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
Jon Chery 8839781539 docs(milestone): complete v0.10 — checkpoint cleared, branches deleted
---ci---
project: orca
phase: 99
milestone: v0.10
status: complete
requirements:
  covered: [REQ-091,REQ-092,REQ-093,REQ-094,REQ-095,REQ-096,REQ-097,REQ-098]
  partial: []
---/ci---
2026-08-05 21:03:28 +00:00
Jon Chery f0b9910bf1 docs(milestone): complete v0.10 — docs & install hardening
Milestone v0.10 — Docs & Install Hardening — COMPLETE.

6 tagged phases (v0.9.0..v0.9.5). 8 REQs (091-098) all Complete:
- REQ-091: docs/cli.md (CLI reference)
- REQ-092: docs/jobspec.md (jobspec reference)
- REQ-093: docs/ingress.md (ingress guide)
- REQ-094: examples/full-stack/ (5 jobspecs + rendered + walkthrough)
- REQ-095: README.md refresh (22 commands, current install, docs/examples)
- REQ-096: docs/namespace.md v0.9 multi-namespace layout
- REQ-097: release.sh cross-build amd64 + asset verification (C-21)
- REQ-098: install.sh fallback walk + --check dry-run

3 binding conditions cleared (C-20, C-21, C-22).
7 decisions (D-188..D-194). 31 files changed, 2654 insertions.

Root cause of v0.4.5 install fixed: v0.8.x releases shipped with zero
binary assets; release.sh now cross-builds amd64 + verifies the asset
post-create; install.sh walks back through releases if the latest lacks
an asset. v0.9.1 is the first correctly-asseted release.

---ci---
project: orca
phase: 5
milestone: v0.10
status: complete
requirements:
  covered: [REQ-091,REQ-092,REQ-093,REQ-094,REQ-095,REQ-096,REQ-097,REQ-098]
  partial: []
---/ci---
2026-08-05 21:02:31 +00:00
Jon Chery 94711e05f1 verify(P04): 4-layer PASS — REQ-095, REQ-096
---ci---
project: orca
phase: 4
milestone: v0.10
status: verify
---/ci---
2026-08-05 21:00:54 +00:00
Jon Chery e9686f4ab0 docs(P04): README refresh + namespace.md v0.9 layout update
P04 — README and namespace.md refresh (REQ-095, REQ-096).

README.md (REQ-095):
- Status line updated (v0.9 complete, v0.10 in progress).
- Install --version example updated to v0.9.1 (current).
- Added --check dry-run example.
- Update-in-place example updated to v0.8.15 -> v0.9.1.
- Subcommand table expanded to all 22 commands with Since column and
  deprecation markers (daemon, cert, status marked deprecated).
- Development section complete (verify-reqs, security-scan, test-race,
  changelog, release).
- New Documentation section linking all 7 docs/*.md.
- New Examples section linking examples/full-stack/.

docs/namespace.md (REQ-096):
- Replaced v0.8 flat path table with v0.9 multi-namespace layout
  (cluster/, _defaults/, per-ns db/jobs/alloc/ns.md, orca_cache.db).
- Full path reference table from internal/paths/paths.go.
- Namespace root resolution (ORCA_HOME/--system/~/.orca).
- orca ns subcommand cross-link to docs/cli.md.
- Namespace inheritance (_defaults implicit root, D-159/D-185/D-187).
- v0.8 flat layout flagged deprecated with callout box.

All README links verified to resolve. make verify-reqs: 98 consistent.

---ci---
project: orca
phase: 4
milestone: v0.10
status: execute
---/ci---
2026-08-05 21:00:46 +00:00
Jon Chery 76967b5145 verify(P03): 4-layer PASS — REQ-094; gate C-20 cleared
---ci---
project: orca
phase: 3
milestone: v0.10
status: verify
---/ci---
2026-08-05 20:59:41 +00:00
Jon Chery 2c26a6d54f docs(P03): full-stack examples — 5 jobspecs + rendered artifacts + walkthrough
P03 — full-stack example with ingress configured (REQ-094; gate C-20).

examples/full-stack/:
- web-app.md: kind Service, process runtime, 3 replicas, Unix socket
  (default R-007), rolling update, constraints (node.role==web),
  affinity (zone==a weight 80), lifecycle hooks (post_start/pre_stop).
- api.md: kind Service, process runtime, 2 replicas, TCP opt-in
  (service.bind: 127.0.0.1, R-007), canary update with manual promote,
  constraints (node.role==api, node.cpus>=2), env vars.
- worker.md: kind Job, process runtime, one-shot, timeout 300s, env
  vars, lifecycle hooks (register/drain).
- log-shipper.md: kind Service (DaemonSet workaround — parser gap),
  process runtime, constraints (node.role==logs), env vars. Notes the
  v0.9 parser gap (schedule: block not wired) in a callout.
- postgres.md: kind Service, process runtime, 1 replica, blue-green
  update, volumes with replication (replicate:peer-b,peer-c via
  Syncthing), constraints (node.role==db, node.cpus>=4, node.memory>=8192).
- rendered/: Traefik dynamic YAML (web-app, api) + systemd units
  (web-app, api, log-shipper) showing what Orca generates on target nodes.
- README.md: end-to-end walkthrough (init -> node join -> capacity set
  -> ns create -> job run -> list --watch -> inspect rendered -> verify
  ingress -> drain/rollback). Cross-links to docs/ingress.md.
- examples_test.go: Go test that parses + validates all 5 jobspecs
  against the current parser and schema validators (gate C-20).

All 5 jobspecs pass jobspec.ParseFile + schema.ValidatorFor(kind).

---ci---
project: orca
phase: 3
milestone: v0.10
status: execute
---/ci---
2026-08-05 20:59:33 +00:00
Jon Chery 4e019ab51e verify(P02): 4-layer PASS — REQ-091, REQ-092, REQ-093; gate C-22 cleared
---ci---
project: orca
phase: 2
milestone: v0.10
status: verify
---/ci---
2026-08-05 20:57:16 +00:00
Jon Chery 289e5cf6e1 docs(P02): CLI reference + jobspec reference + ingress guide
P02 — operator-facing documentation (REQ-091, REQ-092, REQ-093; gate C-22).

docs/cli.md (REQ-091):
- Full CLI command/flag reference: every command/subcommand with synopsis,
  flag tables (name/type/default/description), one-line examples.
- Global flags (--json, --system, --config, --no-deprecation-warnings).
- Output modes (text/json/watch), env vars, exit codes.
- Deprecated surface callout boxes (daemon, cert, node-join-mTLS, HCL
  jobspec) pointing to v0.11 removal.

docs/jobspec.md (REQ-092):
- Markdown frontmatter schema reference: all top-level keys, block
  reference (runtime/ports/env-secrets/volumes/restart/update/service/
  health/lifecycle/constraints/affinity/tasks), kinds matrix
  (Job/Service/DaemonSet required vs allowed), CEL subset grammar, body
  byte-exact preservation (R-015), deprecated HCL callout.

docs/ingress.md (REQ-093):
- Traefik ingress reference: service->Traefik mapping (D-175), R-007
  socket-vs-TCP-bind semantics, generated YAML shape (routers/services/
  healthCheck), atomic reload (C-10), drain (weight:0), TLS (certResolver,
  trust domain, step-ca), worked-example pointer to examples/full-stack/,
  v0.11 forward limitations.

All factual claims grounded in live codebase (gate C-22). Cross-links
verified to resolve.

---ci---
project: orca
phase: 2
milestone: v0.10
status: execute
---/ci---
2026-08-05 20:57:05 +00:00
22 changed files with 2126 additions and 120 deletions
+12 -11
View File
@@ -1,24 +1,25 @@
{
"phase": 1,
"stage": "verify",
"phase": 5,
"stage": "complete",
"milestone": "v0.10",
"milestone_slug": "docs-cli-examples",
"phase_role": "execution",
"phase_role": "final",
"attempts": 0,
"updated_at": "2026-08-05T20:30:00Z",
"milestone_complete": false,
"updated_at": "2026-08-05T21:30:00Z",
"milestone_complete": true,
"next_milestone": "v0.11",
"ship": {
"tag": "v0.9.0",
"merged_to_main": false,
"milestone_branch_deleted": false,
"tag": "v0.9.6",
"merged_to_main": true,
"milestone_branch_deleted": true,
"all_phase_branches_deleted": true
},
"requirements": {
"covered": [],
"covered": [91,92,93,94,95,96,97,98],
"partial": []
},
"gates": {
"cleared": ["C-21"],
"pending": ["C-20", "C-22"]
"cleared": ["C-20", "C-21", "C-22"],
"deferred_v0_11": []
}
}
+8 -8
View File
@@ -193,11 +193,11 @@ that guarantees every Gitea release carries a Linux binary asset.
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-091 | `docs/cli.md` comprehensive CLI reference: every command/subcommand with synopsis, flags (name/type/default/description), and one-line example; global flags (`--json`, `--system`, `--config`, `--no-deprecation-warnings`); output modes (text vs `--json`, `--watch` table vs NDJSON); exit codes; deprecated surface (`orca daemon`, `orca cert`, `orca node join` mTLS path, legacy `.hcl` jobspec) flagged with callout boxes pointing to v0.10 removal | High | **v0.10 P2** | Pending |
| REQ-092 | `docs/jobspec.md` markdown frontmatter schema reference: all top-level keys, block reference (runtime, ports, env/secrets, volumes, restart, update, service, health, lifecycle, constraints, affinity, tasks), kinds matrix (Job/Service/DaemonSet required vs allowed), CEL subset grammar, body byte-exact preservation (R-015), deprecated HCL form callout | High | **v0.10 P2** | Pending |
| REQ-093 | `docs/ingress.md` Traefik ingress reference: `kind: Service` implies Traefik route (D-175), R-007 socket-vs-TCP-bind semantics, generated Traefik YAML shape (routers/services/healthCheck), atomic reload (C-10), drain (`weight: 0`), TLS (certResolver, trust domain, step-ca), worked-example pointer to `examples/full-stack/`, v0.10 forward limitations (socket activation, transactional update) | High | **v0.10 P2** | Pending |
| REQ-094 | `examples/full-stack/` directory with 5 valid jobspecs (`web-app.md`, `api.md`, `worker.md`, `log-shipper.md`, `postgres.md`) exercising ports/service/health/restart/update/constraints/affinity/lifecycle/task-groups/volumes/replication/DaemonSet; `rendered/` subdir showing the Traefik dynamic YAML + systemd units orca generates; `README.md` walkthrough (init → node join → capacity set → ns create → job run → list --watch → inspect rendered) | High | **v0.10 P3** | Pending |
| REQ-095 | README.md refresh: status line (v0.9 complete, v0.10 in progress), install `--version` example updated to current tag, subcommand table expanded to all commands with deprecation markers, update-in-place example updated, development targets complete (`verify-reqs`, `security-scan`, `test-race`, `changelog`), new Documentation + Examples sections linking all `docs/*.md` and `examples/` | High | **v0.10 P4** | Pending |
| REQ-096 | `docs/namespace.md` v0.9 multi-namespace layout update: replace v0.8 flat path table with v0.9 layout (`cluster/`, `_defaults/`, per-ns `db/jobs/alloc/ns.md`), `ORCA_HOME`/`--system` resolution, `orca ns` subcommand cross-link, v0.8 flat layout flagged deprecated | Medium | **v0.10 P4** | Pending |
| REQ-097 | `scripts/release.sh` release pipeline fix: cross-build `linux-amd64` tarball regardless of host arch (`GOOS=linux GOARCH=amd64 go build`); post-create asset verification (query `/releases/tags/$VERSION`, assert the tarball in attachments, retry/fail loudly if missing). Guarantees every Gitea release carries the Linux binary asset (root cause of v0.4.5 install) | High | **v0.10 P1** | Pending |
| REQ-098 | `scripts/install.sh` asset fallback walk: if the latest/pinned release lacks the matching `orca-<ver>-<os>-<arch>.tar.gz`, walk backward through `/releases?limit=20` to the most recent release that has it, with a clear warning. Keeps pulling from releases (not main). Optional `--check` dry-run mode | High | **v0.10 P1** | Pending |
| REQ-091 | `docs/cli.md` comprehensive CLI reference: every command/subcommand with synopsis, flags (name/type/default/description), and one-line example; global flags (`--json`, `--system`, `--config`, `--no-deprecation-warnings`); output modes (text vs `--json`, `--watch` table vs NDJSON); exit codes; deprecated surface (`orca daemon`, `orca cert`, `orca node join` mTLS path, legacy `.hcl` jobspec) flagged with callout boxes pointing to v0.10 removal | High | **v0.10 P2** | **Complete** |
| REQ-092 | `docs/jobspec.md` markdown frontmatter schema reference: all top-level keys, block reference (runtime, ports, env/secrets, volumes, restart, update, service, health, lifecycle, constraints, affinity, tasks), kinds matrix (Job/Service/DaemonSet required vs allowed), CEL subset grammar, body byte-exact preservation (R-015), deprecated HCL form callout | High | **v0.10 P2** | **Complete** |
| REQ-093 | `docs/ingress.md` Traefik ingress reference: `kind: Service` implies Traefik route (D-175), R-007 socket-vs-TCP-bind semantics, generated Traefik YAML shape (routers/services/healthCheck), atomic reload (C-10), drain (`weight: 0`), TLS (certResolver, trust domain, step-ca), worked-example pointer to `examples/full-stack/`, v0.10 forward limitations (socket activation, transactional update) | High | **v0.10 P2** | **Complete** |
| REQ-094 | `examples/full-stack/` directory with 5 valid jobspecs (`web-app.md`, `api.md`, `worker.md`, `log-shipper.md`, `postgres.md`) exercising ports/service/health/restart/update/constraints/affinity/lifecycle/task-groups/volumes/replication/DaemonSet; `rendered/` subdir showing the Traefik dynamic YAML + systemd units orca generates; `README.md` walkthrough (init → node join → capacity set → ns create → job run → list --watch → inspect rendered) | High | **v0.10 P3** | **Complete** |
| REQ-095 | README.md refresh: status line (v0.9 complete, v0.10 in progress), install `--version` example updated to current tag, subcommand table expanded to all commands with deprecation markers, update-in-place example updated, development targets complete (`verify-reqs`, `security-scan`, `test-race`, `changelog`), new Documentation + Examples sections linking all `docs/*.md` and `examples/` | High | **v0.10 P4** | **Complete** |
| REQ-096 | `docs/namespace.md` v0.9 multi-namespace layout update: replace v0.8 flat path table with v0.9 layout (`cluster/`, `_defaults/`, per-ns `db/jobs/alloc/ns.md`), `ORCA_HOME`/`--system` resolution, `orca ns` subcommand cross-link, v0.8 flat layout flagged deprecated | Medium | **v0.10 P4** | **Complete** |
| REQ-097 | `scripts/release.sh` release pipeline fix: cross-build `linux-amd64` tarball regardless of host arch (`GOOS=linux GOARCH=amd64 go build`); post-create asset verification (query `/releases/tags/$VERSION`, assert the tarball in attachments, retry/fail loudly if missing). Guarantees every Gitea release carries the Linux binary asset (root cause of v0.4.5 install) | High | **v0.10 P1** | **Complete** |
| REQ-098 | `scripts/install.sh` asset fallback walk: if the latest/pinned release lacks the matching `orca-<ver>-<os>-<arch>.tar.gz`, walk backward through `/releases?limit=20` to the most recent release that has it, with a clear warning. Keeps pulling from releases (not main). Optional `--check` dry-run mode | High | **v0.10 P1** | **Complete** |
+7 -7
View File
@@ -249,7 +249,7 @@ HCL-canonical, single-namespace, no-container-runtime, no-SPIFFE). The
reversals are justified by the six-part evidence basis recorded in the
PROJECT.md Supersession Table.
## Milestone v0.10: Docs & Install Hardening — **IN PROGRESS**
## Milestone v0.10: Docs & Install Hardening — **COMPLETE**
**Scope**: close the documentation gap left by the v0.9 re-architecture
and fix the release/install pipeline bug that caused `install.sh` to
@@ -265,12 +265,12 @@ carries a Linux binary asset.
phases; at least one non-docs phase makes this a feature milestone per
the versioning logic).
- [ ] Phase 0: Pre-execution (specify → clarify → research → ideate → plan → grill) — tag `v0.9.0`
- [ ] Phase P1: release.sh + install.sh fix (REQ-097, REQ-098) — tag `v0.9.1`
- [ ] Phase P2: docs/cli.md + docs/jobspec.md + docs/ingress.md (REQ-091, REQ-092, REQ-093) — tag `v0.9.2`
- [ ] Phase P3: examples/full-stack/ (REQ-094) — tag `v0.9.3`
- [ ] Phase P4: README.md + docs/namespace.md refresh (REQ-095, REQ-096) — tag `v0.9.4`
- [ ] Phase P5: Final review + ship + audit (milestone release) — tag `v0.9.5` = v0.10.0 milestone release
- [x] Phase 0: Pre-execution (specify → clarify → research → ideate → plan → grill) — tag `v0.9.0`
- [x] Phase P1: release.sh + install.sh fix (REQ-097, REQ-098) — tag `v0.9.1`
- [x] Phase P2: docs/cli.md + docs/jobspec.md + docs/ingress.md (REQ-091, REQ-092, REQ-093) — tag `v0.9.2`
- [x] Phase P3: examples/full-stack/ (REQ-094) — tag `v0.9.3`
- [x] Phase P4: README.md + docs/namespace.md refresh (REQ-095, REQ-096) — tag `v0.9.4`
- [x] Phase P5: Final review + ship + audit (milestone release) — tag `v0.9.5` = v0.10.0 milestone release
**Milestone tag**: `v0.9.5` (final phase patch = milestone release per
feature-milestone progressive-patch rule). Per-phase tags: `v0.9.0``v0.9.5`.
+61 -16
View File
@@ -4,7 +4,9 @@ Offline/CLI-first orchestration engine inspired by HashiCorp Nomad, far simpler
## Status
**v0.1: Foundation** — see [.ciagent/ROADMAP.md](.ciagent/ROADMAP.md) for the 6-phase plan.
**v0.9: Re-architecture Foundation — COMPLETE** | **v0.10: Docs & Install Hardening — IN PROGRESS**
See [.ciagent/ROADMAP.md](.ciagent/ROADMAP.md) for the full roadmap.
## Pillars
@@ -28,7 +30,10 @@ curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install
curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install.sh | sudo bash -s -- --system
# Pin a specific version
curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install.sh | bash -s -- --version v0.4.2
curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install.sh | bash -s -- --version v0.9.1
# Dry-run: check what would be installed without writing
curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install.sh | bash -s -- --check
```
Then initialize local state and verify:
@@ -54,27 +59,67 @@ config, database, and certificates in the namespace dir:
```bash
curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install.sh | bash
# → "updated orca from v0.4.1 to v0.4.2"
# → "updated orca from v0.8.15 to v0.9.1"
```
## Subcommands
| Command | Description | Status |
|---------|-------------|--------|
| `orca version` | Print version info | ✅ Phase 1 |
| `orca init` | Initialize local orca state | ✅ Phase 1 (stub) |
| `orca status` | Show orca daemon status | ✅ Phase 1 (stub) |
| `orca node` | Node management (`join`, `leave`, `list`) | Phase 2 |
| `orca job` | Job management (`run`, `list`, `stop`, `logs`) | Phase 3 |
| Command | Description | Since |
|---------|-------------|-------|
| `orca init` | Initialize local orca state (full bootstrap) | v0.6 |
| `orca version` | Print version info | v0.1 |
| `orca status` | Show orca daemon status (**deprecated** v0.9) | v0.1 |
| `orca job run` | Run a job from a spec file (`.md`/`.yaml`/`.hcl`) | v0.1 |
| `orca job list` | List all jobs (`--watch` for streaming) | v0.1 |
| `orca job stop` | Stop a running job | v0.1 |
| `orca job logs` | Show task output for a job | v0.1 |
| `orca node join` | Join a node (`--type proxmox` for SSH-push) | v0.2 |
| `orca node leave` | Remove a node from the registry | v0.2 |
| `orca node list` | List all nodes (`--watch` for streaming) | v0.2 |
| `orca node key-reset` | Reset SSH known_hosts entry for a node | v0.8 |
| `orca node capacity` | Manage node capacity (show/set/list) | v0.2 |
| `orca ns list` | List all namespaces | v0.9 |
| `orca ns create` | Create a namespace directory + ns.md | v0.9 |
| `orca ns delete` | Remove an empty namespace | v0.9 |
| `orca ns inspect` | Print effective chain, merged env, constraints | v0.9 |
| `orca ns validate` | Run cycle + missing-parent + schema checks | v0.9 |
| `orca doctor` | Run self-checks (cert/network/db/os/proxmox) | v0.2 |
| `orca audit list` | View audit log entries | v0.1 |
| `orca daemon` | Run the daemon (**deprecated** v0.9) | v0.1 |
| `orca cert` | Manage certificates (**deprecated** v0.9) | v0.2 |
See [docs/cli.md](docs/cli.md) for the full CLI reference with all flags and examples.
## Documentation
| Document | Description |
|----------|-------------|
| [docs/cli.md](docs/cli.md) | CLI reference — every command, flag, and example |
| [docs/jobspec.md](docs/jobspec.md) | Jobspec reference — markdown frontmatter schema |
| [docs/ingress.md](docs/ingress.md) | Ingress guide — Traefik configuration |
| [docs/namespace.md](docs/namespace.md) | Namespace and path layout |
| [docs/install.md](docs/install.md) | Installation guide |
| [docs/docker.md](docs/docker.md) | Docker image guide |
| [docs/security-scanning.md](docs/security-scanning.md) | Security scanning tools |
## Examples
| Example | Description |
|---------|-------------|
| [examples/full-stack/](examples/full-stack/) | Full-stack deployment with ingress (5 services + rendered artifacts) |
## Development
```bash
make build # Build binary to ./bin/orca
make test # Run tests with race detection
make lint # Run golangci-lint
make fmt # Format code
make release # Build + create Gitea release (Phase 6)
make build # Build binary to ./bin/orca
make test # Run tests
make test-race # Run tests with race detection
make lint # Run gofmt + go vet + shellcheck
make fmt # Format code
make security-scan # Run gosec + govulncheck + gitleaks
make verify-reqs # Assert ROADMAP ↔ REQUIREMENTS consistency
make changelog # Generate CHANGELOG.md from ---ci--- blocks
make release # Build + create Gitea release (VERSION required)
```
## Architecture
@@ -83,4 +128,4 @@ See [.ciagent/ARCHITECTURE.md](.ciagent/ARCHITECTURE.md) for full architecture d
## License
MIT — see [LICENSE](LICENSE).
MIT — see [LICENSE](LICENSE).
+522
View File
@@ -0,0 +1,522 @@
# Orca CLI Reference
This document is the complete reference for the `orca` command-line
interface. Every command, subcommand, and flag is documented here.
> **Canonical path (v0.9)**: The v0.9 re-architecture introduced the
> SSH-push deployment model, markdown jobspec, multi-namespace layout,
> and CLI-side scheduler. Commands marked **deprecated** below are from
> the v0.8 daemon/mTLS model and will be removed in v0.11. Use the
> v0.9 canonical path for all new work.
## Global flags
These flags are available on every `orca` command.
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--json` | bool | `false` | Output in JSON format (machine-readable) |
| `--system` | bool | `false` | Use system-level namespace root (`/root/.orca`) instead of user-level (`~/.orca`). Errors if `ORCA_HOME` is already set to a conflicting value. |
| `--config` | string | `""` | Path to config file (overrides `~/.orca/config.hcl`). Supports `.hcl` (legacy) and `.md` (v0.9 canonical) formats. |
| `--no-deprecation-warnings` | bool | `false` | Suppress v0.9 deprecation warnings. Use during `orca upgrade` migrations. |
### Output modes
- **Text** (default): human-readable tables and messages.
- **JSON** (`--json`): structured JSON output for machine consumption
and AI agents.
- **Watch** (`--watch` on list commands): table refresh (text default)
or NDJSON streaming (`--json`), one line per event until Ctrl-C.
### Environment variables
| Variable | Description |
|----------|-------------|
| `ORCA_HOME` | Namespace root directory (default `~/.orca`). Overrides all on-disk paths. |
| `ORCA_DB` | Fine-grained database path override. |
| `ORCA_PROXMOX_PASSWORD` | SSH password for `orca node join --type proxmox` (never persisted). |
| `ORCA_LISTEN_ADDR` | Daemon listen address (deprecated). |
| `ORCA_CA_PATH` | CA certificate path override. |
| `ORCA_SERVER_CERT_PATH` | Server certificate path override. |
| `ORCA_SERVER_KEY_PATH` | Server key path override. |
| `ORCA_NODE_CPU` | Node CPU capacity override (millicores). |
| `ORCA_NODE_MEMORY_MB` | Node memory capacity override (MiB). |
### Exit codes
| Code | Meaning |
|------|---------|
| `0` | Success |
| `1` | Error (printed to stderr) |
---
## `orca init`
Initialize local orca state with full bootstrap.
```
orca init
```
Performs a 6-step idempotent bootstrap:
1. Create the namespace directory (honors `$ORCA_HOME`; defaults to `~/.orca`)
2. Open and migrate the SQLite database (migrations 00010006)
3. Bootstrap the internal CA (`ca.crt` + `ca.key`) if not already present
4. Generate the server cert (`server.crt` + `server.key`) if not already present
5. Auto-detect the local OS via `/etc/os-release`
6. Register a localhost node (kind=localhost, os=\<detected\>)
Re-running `orca init` is safe — it refreshes `last_seen` and `os` on
the localhost node without regenerating certs or changing the node ID.
**Flags**: none.
**Example**:
```bash
orca init
orca --system init # system-level bootstrap at /root/.orca
```
---
## `orca job`
Manage orca jobs — run, list, stop, and inspect.
### `orca job run`
Run a job from a spec file.
```
orca job run <spec> [flags]
```
Dispatches by file extension:
- `.md` → Markdown frontmatter parser (v0.9 canonical)
- `.yaml` / `.yml` → YAML frontmatter parser
- `.hcl` → Legacy HCL adapter (deprecated, see callout below)
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--target` | string | `""` | Pin job to a specific node ID (overrides bin-packing scheduler) |
| `--idempotency-key` | string | `""` | Idempotency key for cross-node dispatch dedupe |
**Examples**:
```bash
orca job run web-app.md
orca job run api.yaml --target node-abc-123
orca job run worker.md --idempotency-key deploy-2026-08-05
```
> **Deprecated**: `orca job run <spec.hcl>` (legacy HCL jobspec) still
> works via the adapter but emits a deprecation warning. Migrate `.hcl`
> specs to `.md` (see [docs/jobspec.md](jobspec.md)). Removed in v0.11.
### `orca job list`
List all jobs.
```
orca job list [flags]
```
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--watch` | bool | `false` | Stream jobs until Ctrl-C (table refresh or `--json` per-event) |
**Output columns**: `ID NAME STATUS EXIT`
**Examples**:
```bash
orca job list
orca job list --watch # table refresh
orca job list --watch --json # NDJSON: {"event":"update","job":{...}}
```
### `orca job stop`
Stop a running job (soft stop).
```
orca job stop [job-id] [flags]
```
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--id` | string | `""` | Job ID (alternative to positional argument) |
**Example**:
```bash
orca job stop abc-123-def
orca job stop --id abc-123-def
```
### `orca job logs`
Show task output for a job.
```
orca job logs [job-id] [flags]
```
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--id` | string | `""` | Job ID (alternative to positional argument) |
**Example**:
```bash
orca job logs abc-123-def
```
---
## `orca node`
Manage orca nodes — join, leave, or list nodes in the registry.
### `orca node join`
Join a node to the orca registry.
```
orca node join [flags]
```
Node types (via `--type`):
- `localhost` (default): register a local or Linux node
- `proxmox`: SSH-bootstrap a remote Proxmox VE 8/9 host (deploys orca
pubkey, creates orca user + PVE role + sudoers allowlist; requires
`--host` + `--password`)
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--name` | string | `""` | Node name (required for `--type localhost`) |
| `--addr` | string | `""` | Node address (default `localhost:8443`) |
| `--ca-fingerprint` | string | `""` | Pin CA cert SHA-256 (fails if on-disk CA doesn't match) |
| `--type` | string | `"localhost"` | Node type: `localhost` or `proxmox` |
| `--host` | string | `""` | Proxmox host address (IP/hostname; required for `--type proxmox`) |
| `--ssh-user` | string | `"root"` | SSH username for proxmox bootstrap |
| `--password` | string | `""` | SSH password for proxmox bootstrap (never persisted; prefer `$ORCA_PROXMOX_PASSWORD`) |
| `--ssh-port` | int | `22` | SSH port for proxmox bootstrap |
| `--proxmox-user` | string | `"orca"` | Linux system user to create on the proxmox host |
| `--proxmox-role` | string | `"OrcaOperator"` | PVE custom role to create |
| `--host-key-fingerprint` | string | `""` | SSH host key `SHA256:base64` fingerprint (pre-pin; supersedes TOFU for `--type proxmox`) |
**Examples**:
```bash
# Localhost (deprecated mTLS path)
orca node join --name my-node
# Proxmox (v0.9 canonical SSH-push path)
orca node join --type proxmox --host 192.168.1.100 --ssh-user root
ORCA_PROXMOX_PASSWORD=secret orca node join --type proxmox --host 192.168.1.100
# Proxmox with pre-pinned host key
orca node join --type proxmox --host 192.168.1.100 --host-key-fingerprint SHA256:abc123...
```
> **Deprecated**: `orca node join` without `--type proxmox` (the
> localhost mTLS join path) is deprecated in v0.9. The v0.9 canonical
> path is SSH-push (`--type proxmox`) or local execution (no join
> needed). Removed in v0.11.
### `orca node leave`
Remove a node from the orca registry.
```
orca node leave [node-id] [flags]
```
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--id` | string | `""` | Node ID (alternative to positional argument) |
### `orca node list`
List all nodes in the orca registry.
```
orca node list [flags]
```
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--watch` | bool | `false` | Stream nodes until Ctrl-C (table refresh or `--json` per-event) |
**Output columns**: `ID NAME ADDRESS STATE`
### `orca node key-reset`
Reset the SSH known_hosts entry for a node.
```
orca node key-reset <node>
```
Removes the pinned SSH host key for `<node>` from the local
`known_hosts` file. The next connect re-pins the key via TOFU or
`--host-key-fingerprint`. Local only — does not touch the remote
host's `authorized_keys`.
`<node>` is the node name (for proxmox nodes, this is the host address).
**Example**:
```bash
orca node key-reset 192.168.1.100
```
### `orca node capacity`
Manage node capacity declarations (bin-packing scheduler input).
```
orca node capacity <subcommand>
```
#### `orca node capacity show`
Show capacity for a node (defaults to `self`).
```
orca node capacity show [node-id] [flags]
```
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--node` | string | `""` | Node ID (defaults to `self`) |
**Output**: `Node:`, `CPU:` (millicores), `Memory:` (MiB), `Disk:` (MiB), `Updated:`
#### `orca node capacity set`
Declare capacity for a node.
```
orca node capacity set [flags]
```
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--cpu` | int64 | `0` | CPU capacity in millicores (1000 = 1 vCPU) |
| `--memory` | int64 | `0` | Memory capacity in MiB |
| `--disk` | int64 | `0` | Disk capacity in MiB |
| `--node` | string | `""` | Node ID (defaults to `self`) |
**Example**:
```bash
orca node capacity set --cpu 4000 --memory 8192 --disk 100000
orca node capacity set --cpu 2000 --memory 4096 --node web-1
```
#### `orca node capacity list`
List all node capacity declarations.
```
orca node capacity list
```
**Output columns**: `NODE CPU(mc) MEM(MiB) DISK(MiB) UPDATED`
---
## `orca ns`
Manage orca namespaces under `ORCA_HOME` (R-002).
Each namespace is a directory with `ns.md`, `.env`, `.env.secrets`,
`db/`, `jobs/`, `alloc/`. The implicit root namespace `_defaults`
always exists; every namespace inherits from `_defaults` and cannot
opt out.
### `orca ns list`
List all namespaces under `ORCA_HOME`.
```
orca ns list
```
**Output columns**: `NAME DEFAULT PATH` (`_defaults` marked `*`)
### `orca ns create`
Create a namespace directory + `ns.md`.
```
orca ns create <name> [flags]
```
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--parent` | string | `""` | Parent namespace (default `_defaults`; implicit root always appended last) |
| `--inherits-env` | bool | `true` | Inherit env from parents |
| `--inherits-secrets` | bool | `true` | Inherit secrets from parents |
**Example**:
```bash
orca ns create prod --parent _defaults
orca ns create staging --parent prod
```
### `orca ns delete`
Remove an empty namespace directory.
```
orca ns delete <name>
```
Refuses if `jobs/` or `alloc/` contain files. The implicit root
`_defaults` cannot be deleted.
### `orca ns inspect`
Print the effective inheritance chain, merged env, and constraints.
```
orca ns inspect <name>
```
**Output**: `Namespace:`, `Chain:` (e.g., `prod -> _defaults`), `Env:`
(sorted keys), `Constraints:` (unioned CEL expressions).
### `orca ns validate`
Run cycle + missing-parent + schema checks on a namespace.
```
orca ns validate <name>
```
Exits 0 if valid, 1 on error. Runs over ALL namespaces under
`ORCA_HOME` (parsing + resolving validates cycles and missing parents
across the set).
---
## `orca doctor`
Run self-checks on the orca installation.
```
orca doctor [subcommand]
```
Without a subcommand, runs all checks and prints a PASS/WARN/FAIL
report per check.
### Subcommands
| Command | Description |
|---------|-------------|
| `orca doctor cert` | CA, server cert, expiry, fingerprint checks |
| `orca doctor network` | Network reachability via mTLS `/healthz` probe |
| `orca doctor db` | Database integrity (`PRAGMA integrity_check` + migration version) |
| `orca doctor os` | OS detection self-check (verifies `/etc/os-release` matches stored node) |
| `orca doctor proxmox` | Proxmox node reachability via SSH `pveversion`/`pvecmd status` probe |
**Example**:
```bash
orca doctor
orca doctor cert
orca doctor proxmox --json
```
---
## `orca audit`
View orca audit log (security-first observability).
### `orca audit list`
List recent audit log entries.
```
orca audit list [flags]
```
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--limit` | int | `50` | Max entries to show |
**Output columns**: `TIMESTAMP ACTOR ACTION RESOURCE RESULT`
---
## `orca version`
Print version information.
```
orca version
```
**Output**:
```
orca version v0.9.1
git commit: abc1234
build time: 2026-08-05T20:30:00Z
```
---
## `orca status`
Show orca daemon status.
```
orca status
```
> **Deprecated**: The daemon model is deprecated in v0.9 (replaced by
> SSH-push, R-001). This command returns a stub status. Removed in
> v0.11.
---
## Deprecated commands
The following commands are from the v0.8 daemon/mTLS model and are
**deprecated in v0.9**. They still work during the dual-write window
but emit `slog.Warn` deprecation warnings. They will be **removed in
v0.11**.
> **`orca daemon`** — Run the orca daemon (HTTP API + health checks).
> The v0.9 re-architecture replaces the daemon with SSH-push (R-001).
> The daemon is repurposed to `drain-and-stop` in v0.11-P05 and deleted
> in v0.11-P14. Flags: `--addr` (default `:8080`), `--pprof` (pprof
> endpoint, default disabled).
> **`orca cert`** — Manage orca certificates (CA, server, rotation).
> The v0.9 re-architecture replaces the internal CA with step-ca
> (D-101). Subcommands: `ca-init`, `gen`, `show`, `renew`,
> `fingerprint`. Removed in v0.11.
> **`orca node join` (mTLS path)** — The localhost mTLS join path
> (without `--type proxmox`) is deprecated. The v0.9 canonical path is
> SSH-push (`--type proxmox`) or local execution (no join needed).
> **`orca job run <spec.hcl>`** — Legacy HCL jobspec. Migrate to `.md`
> (see [docs/jobspec.md](jobspec.md)). The HCL adapter preserves
> `orca job run old-spec.hcl` during the migration window.
To suppress deprecation warnings during migration, use
`--no-deprecation-warnings`:
```bash
orca --no-deprecation-warnings daemon
```
---
## See also
- [docs/jobspec.md](jobspec.md) — Markdown frontmatter jobspec reference
- [docs/ingress.md](ingress.md) — Traefik ingress configuration guide
- [docs/namespace.md](namespace.md) — Namespace and path layout
- [docs/install.md](install.md) — Installation guide
- [examples/full-stack/](../examples/full-stack/) — Full-stack example with ingress
+209
View File
@@ -0,0 +1,209 @@
# Orca Ingress & Traefik Guide
This document explains how Orca configures ingress via Traefik dynamic
configuration. It covers the service→Traefik mapping, the R-007
socket-vs-TCP-bind model, atomic reload, drain, TLS, and a worked
example.
> **Canonical path (v0.9)**: Orca generates Traefik dynamic
> configuration files via the `TraefikEmitter`. The `kind: Service`
> workload implies a Traefik route. The emitter renders one YAML file
> per Service; Traefik watches the dynamic config directory and reloads
> atomically on change.
## The model
A `kind: Service` jobspec **implies** a Traefik route (D-175). `Job`
and `DaemonSet` do **not** carry a Traefik route by default — a
`service:` block on a `Job` is rejected by the validator.
When `orca job run` submits a `kind: Service` workload, the
`TraefikEmitter` renders a Traefik dynamic config file at:
```
/etc/traefik/dynamic/orca-<service-name>.yaml
```
This file contains:
- One **router** (`orca-<name>`) with a `PathPrefix` rule and TLS config.
- One **service** (`orca-<name>`) as a `loadBalancer` with one **server**
per port, pointing at the workload's Unix socket (or TCP port).
- A **healthCheck** stanza when the `health:` block is present.
Traefik watches `/etc/traefik/dynamic/` via `fsnotify` and reloads
whenever a file changes. Orca writes config atomically (write-tmp +
rename) so Traefik sees a single `IN_MOVED_TO` event and never observes
a half-written file.
## R-007: socket vs TCP bind
Orca workloads bind to a **Unix socket** by default, not a TCP port.
This is the R-007 security model: loopback-only by default, no network
exposure.
### Default: Unix socket
When `service.bind` is empty (default), the workload binds a Unix
socket at:
```
/run/orca/alloc-<alloc-id>/port-<port-name>.sock
```
systemd creates `/run/orca/alloc-<alloc-id>/` via
`RuntimeDirectory=orca/alloc-<alloc-id>` (mode 0750, owned by
`orca:orca`). The Traefik backend server URL is:
```yaml
servers:
- url: "unix:///run/orca/alloc-<alloc-id>/port-<port-name>.sock"
```
### TCP opt-in: `service.bind: 127.0.0.1`
When `service.bind: 127.0.0.1` is set, the workload binds a TCP port
directly (loopback only). The emitter adds an `ExecStartPre` marker to
the systemd unit so the bind mode is visible:
```ini
ExecStartPre=/bin/echo orca: bind 127.0.0.1 port <name> (tcp, R-007 opt-in)
```
`service.bind` must be a valid IP address. Empty (socket default) or
`127.0.0.1` (TCP opt-in) are the documented values; any other valid IP
is accepted but the bind happens in the process, not the emitter.
## Generated Traefik YAML
For a Service named `web` with port `http`:
```yaml
http:
routers:
orca-web:
rule: PathPrefix("/web")
service: orca-web
tls:
certResolver: orca
domains:
- main: "cluster.orca.local"
services:
orca-web:
loadBalancer:
servers:
- url: "unix:///run/orca/alloc-<alloc-id>/port-http.sock"
healthCheck:
path: /healthz
interval: 5s
timeout: 1s
```
- One router per Service, named `orca-<service-name>`.
- Router rule: `PathPrefix("/<service-name>")`.
- TLS: `certResolver: orca`, trust domain `cluster.orca.local`
(placeholder; step-ca provisioner overrides in v0.11).
- One service per Service, named `orca-<service-name>`.
- One server per port, URL is `unix://<socket-path>`.
- `healthCheck` stanza present when `health:` block is set (required
for Service). Path is `/healthz`; interval and timeout come from the
`health:` block.
## Atomic reload (gate C-10)
Orca writes Traefik config atomically to avoid Traefik observing a
half-written file:
1. Write to `<path>.tmp` via `WriteFileIdempotent` (write + fsync).
2. `mv -f <path>.tmp <path>` (atomic POSIX rename).
Traefik's `fsnotify` watcher sees a single `IN_MOVED_TO` event and
reloads. If the new config is malformed, Traefik logs an error and
**holds last-good config** — the cluster keeps serving traffic on the
previous config.
## Drain
`RenderDrain` produces the same Traefik YAML with `weight: 0` on every
server in the load balancer:
```yaml
servers:
- url: "unix:///run/orca/alloc-<alloc-id>/port-http.sock"
weight: 0
```
Traefik stops sending traffic to the drained backend. The workload
keeps running; drain is reversible (re-submit the normal config to
restore traffic).
## TLS
- **certResolver**: `orca` (references the Traefik ACME/step-ca
certificate resolver configured in Traefik's static config).
- **Trust domain**: `cluster.orca.local` (placeholder in v0.9; step-ca
provisioner in v0.11 overrides with the real cluster trust domain).
- **SPIFFE SVIDs**: workload identity via SPIFFE SVIDs minted at submit
time via step-ca (v0.11-P01.5, gate C-08). The SVID is a URI SAN in
the workload's X.509 cert.
## Health checks
The `health:` block (required for `Service`) maps to the Traefik
`healthCheck` stanza:
```yaml
health:
check_type: http
interval: 5s
timeout: 1s
unhealthy_threshold: 2
```
```yaml
healthCheck:
path: /healthz
interval: 5s
timeout: 1s
```
Traefik polls each backend's `/healthz` at the configured interval. An
unhealthy backend is removed from the load balancer pool until it
passes the health check again.
## Worked example
See [examples/full-stack/](../examples/full-stack/) for a complete
multi-service stack with ingress configured:
- `web-app.md` — frontend Service (socket bind, PathPrefix route)
- `api.md` — backend API Service (TCP opt-in, `127.0.0.1` bind)
- `examples/full-stack/rendered/traefik-dynamic-web-app.yaml` — the
Traefik config Orca generates
## v0.11 forward (limitations)
The following are not yet implemented in v0.9 and will land in v0.11:
- **`service.host` / `service.route_id`**: stored on the `ServiceBlock`
but not yet consumed by the `TraefikEmitter`. The router rule is
hardcoded `PathPrefix("/<name>")`. Custom host-based routing lands in
v0.11.
- **Socket activation**: real socket-activation (socket unit files, fd
passing) lands in v0.11-P08. The current emitter renders the
`RuntimeDirectory` + socket path comments but does not create socket
units.
- **Transactional update execution**: the `update:` block's rolling/
canary/blue-green plan is computed by the emitter but not yet
executed transactionally. Transactional execution lands in
v0.11-P10.
- **SPIFFE SVID minting**: workload identity via step-ca SVIDs lands in
v0.11-P01.5 (gate C-08).
- **Secrets in env**: `env: { KEY: { from: "secret:..." } }` resolution
to `EnvironmentFile=`/`LoadCredential=` lands in v0.11-P03.
## See also
- [docs/cli.md](cli.md) — CLI reference
- [docs/jobspec.md](jobspec.md) — Jobspec reference (`service:`, `health:`, `ports:` blocks)
- [examples/full-stack/](../examples/full-stack/) — Full-stack example with ingress
+442
View File
@@ -0,0 +1,442 @@
# Orca Jobspec Reference
This document is the complete reference for the Orca jobspec format —
the Markdown-with-frontmatter specification that describes workloads.
> **Canonical format (v0.9)**: Orca uses Markdown with YAML frontmatter
> as the canonical jobspec format (R-013/R-014). The legacy HCL format
> is supported via an adapter during the migration window but is
> deprecated (see [HCL jobspec](#deprecated-hcl-jobspec) below).
## File formats
The `orca job run` command dispatches by file extension:
| Extension | Parser | Body |
|-----------|--------|------|
| `.md` | `ParseMarkdown` (canonical) | Verbatim after closing `---` (R-015 byte-exact) |
| `.yaml` / `.yml` | `parseYAMLFile` | Whole file as frontmatter; body empty |
| `.hcl` | `ParseHCL` (legacy adapter) | Empty (deprecated) |
## Minimal example
```yaml
---
kind: Job
name: my-job
runtime:
one_of: process
command: /bin/echo hello
---
# My Job
This body is preserved byte-exact and carried to the target node.
```
## Top-level keys
| Key | Type | Default | Required | Notes |
|-----|------|---------|----------|-------|
| `orca-spec-version` | string | `""` | no | Free-form version tag (e.g. `"1"`) |
| `kind` | enum | — | **yes** | One of `Job`, `Service`, `DaemonSet` |
| `name` | string | — | **yes** | Workload name (trimmed, non-empty) |
| `count` | int | `1` | no | Job: must be 1; Service: ≥1; DaemonSet: not allowed |
| `runtime` | block | nil | see kinds | Runtime block (or per-task runtimes in a task group) |
| `ports` | block list | nil | Service: **yes** | Array of port mappings |
| `env` | block map | nil | no | Environment variables |
| `secrets` | inline/block list | nil | no | Secret names (resolution in v0.11) |
| `volumes` | block list | nil | no | Volume mounts |
| `restart` | block | nil | Service/DaemonSet: **yes** | Restart policy |
| `update` | block | nil | Service: **yes** | Update strategy |
| `service` | block | nil | no | Traefik route definition (implied for Service; not allowed for Job/DaemonSet) |
| `health` | block | nil | Service: **yes** | Health check |
| `lifecycle` | block | nil | no | Pre-stop / post-start hooks |
| `constraints` | list | nil | no | CEL expressions (node selection) |
| `affinity` | block list | nil | no | Co-location / anti-affinity rules |
| `tasks` | block list | nil | no | Task group (multi-process alloc) |
| `timeout` | duration string | `""` | no | Job timeout |
| `schedule` | block | nil | DaemonSet: **yes** | Schedule mode |
## Kinds
### `Job`
A one-shot batch task. Runs once and exits.
- `count` must be 1 (or unset). Use `Service` for replicas.
- `service` block is **not allowed** (no Traefik route for Jobs).
- `restart` optional (defaults to `never` / `on-failure`).
- `timeout` optional.
**Example**:
```yaml
---
kind: Job
name: data-migration
runtime:
one_of: process
command: /usr/bin/python3 migrate.py
timeout: 300s
env:
DB_URL: postgres://localhost/mydb
---
```
### `Service`
A long-running, load-balanced workload with a Traefik route.
- `count` ≥ 1 (number of replicas).
- `ports` required (at least one).
- `restart` required; `mode` one of `service`, `on-failure`, `never`.
- `update` required; `strategy` one of `rolling`, `canary`, `blue-green`.
- `runtime` required (or a task group with per-task runtimes).
- `health` required (Traefik routing requires health checks).
- `service` block optional (implied for Service; use for `bind` override).
- `service.bind` if present must be a valid IP (`127.0.0.1` = TCP opt-in;
default = Unix socket).
**Example**:
```yaml
---
kind: Service
name: web
count: 3
runtime:
one_of: process
command: /usr/bin/httpd
ports:
- name: http
port: 8080
restart:
mode: service
attempts: 5
delay: 2s
update:
strategy: rolling
max_parallel: 1
health:
check_type: http
interval: 5s
timeout: 1s
unhealthy_threshold: 2
constraints:
- node.role == "web"
---
```
### `DaemonSet`
A workload that runs on every matching node.
- `schedule` required; `mode` one of `every-node`, `matching`, `mandatory`.
- `ports` **not allowed** (no Traefik route by default).
- `count` **not allowed** (implicit = matching nodes).
- `restart` required.
**Example**:
```yaml
---
kind: DaemonSet
name: log-shipper
schedule:
mode: every-node
runtime:
one_of: process
command: /usr/bin/fluent-bit
restart:
mode: service
---
```
## Block reference
### `runtime`
The runtime backend for the workload.
| Field | Key | Type | Default | Notes |
|-------|-----|------|---------|-------|
| `one_of` | `one_of` | string | — | Runtime type (see below) |
| `image` | `image` | string | `""` | Container image (for `podman`) |
| `command` | `command` | string | — | ExecStart command |
**Supported runtime types** (`one_of`):
| Type | Description | Requires |
|------|-------------|----------|
| `process` | Direct process execution via systemd (default) | systemd on target |
| `wasm` / `wasmtime` | WASM via wasmtime CLI (apt-installed on peer, SSH exec) | wasmtime on target |
| `podman` | Container via podman | podman on target |
| `pve-vm` | Proxmox VM via `qm` | Proxmox node |
| `pve-ct` | Proxmox container via `pct` | Proxmox node |
| `proxmox` | Alias for Proxmox runtime | Proxmox node |
An empty/missing `Runtime` or `OneOf` is runtime-agnostic (always fits
the runtime axis in the scheduler).
### `ports`
Array of port mappings. Required for `Service`.
| Field | Key | Type | Default | Notes |
|-------|-----|------|---------|-------|
| `name` | `name` | string | — | Port name (used in socket path) |
| `port` | `port` | int | — | Container port |
| `host_port` | `host_port` | int | `0` | Host port |
| `protocol` | `protocol` | string | `""` | Protocol (e.g. `tcp`) |
| `host_ip` | `host_ip` | string | `""` | Host IP |
**Example**:
```yaml
ports:
- name: http
port: 8080
host_port: 80
protocol: tcp
- name: https
port: 8443
host_port: 443
```
### `env`
Environment variables. Scalar values or secret references.
```yaml
env:
FOO: bar
BAZ: "qux"
SECRET_REF:
from: "secret:db-password"
INLINE: {from: "secret:token"}
```
> Secret resolution (`from: "secret:..."`) lands in v0.11-P03. The
> parser stores the reference; the emitter will emit
> `EnvironmentFile=`/`LoadCredential=` in v0.11.
### `secrets`
List of secret names. Inline array or block list.
```yaml
secrets: ["db-password", "api-token"]
# or
secrets:
- db-password
- api-token
```
### `volumes`
Array of volume mounts.
| Field | Key | Type | Default | Notes |
|-------|-----|------|---------|-------|
| `name` | `name` | string | — | Volume name |
| `type` | `type` | string | — | Volume type (e.g. `host`) |
| `source` | `source` | string | — | Source path (or `replicate:<peer>,<peer>` for Syncthing) |
| `target` | `target` | string | — | Mount target |
| `read_only` | `read_only` | bool | `false` | Read-only mount (`true`/`yes`/`on`/`1`) |
**Example**:
```yaml
volumes:
- name: data
type: host
source: /data
target: /data
read_only: true
```
### `restart`
Restart policy.
| Field | Key | Type | Default | Notes |
|-------|-----|------|---------|-------|
| `mode` | `mode` | enum | — | `never`, `on-failure`, `service` |
| `attempts` / `max_retries` | `attempts` or `max_retries` | int | `0` | Max retries (both keys accepted) |
| `delay` | `delay` | duration string | `""` | Retry delay (e.g. `2s`) |
### `update`
Update strategy. Required for `Service`.
| Field | Key | Type | Default | Notes |
|-------|-----|------|---------|-------|
| `strategy` | `strategy` | enum | — | `rolling`, `canary`, `blue-green` |
| `max_surge` | `max_surge` | int | `0` | Max surge |
| `max_parallel` | `max_parallel` | int | `1` (clamped to `count`) | Max parallel updates |
| `min_healthy_time` | `min_healthy_time` | duration | `""` | Min time healthy before next batch |
| `healthy_deadline` | `healthy_deadline` | duration | `""` | Deadline for health |
| `canary` | `canary` | int or `"<n>%"` | — | Canary size (int count or percentage) |
| `auto_promote` | `auto_promote` | bool | `false` | Auto-promote canary (`true`/`yes`/`on`/`1`) |
**Strategies**:
- **rolling**: batches of `max_parallel`, each batch waits for healthy.
- **canary**: canary batch first, then `promote` (manual or `auto_promote`), then remaining in `max_parallel` batches.
- **blue-green**: all new allocs start in parallel, wait healthy, then `cutover`.
> Transactional update execution lands in v0.11-P10. The current
> emitter computes the plan; execution is a v0.11 deliverable.
### `service`
Traefik route definition. Implied for `Service`; not allowed for
`Job`/`DaemonSet`. See [docs/ingress.md](ingress.md) for details.
| Field | Key | Type | Default | Notes |
|-------|-----|------|---------|-------|
| `name` | `name` | string | — | Service name |
| `port` | `port` | int | — | Service port |
| `bind` | `bind` | string (IP) | `""` | Bind mode: empty = Unix socket (default); `127.0.0.1` = TCP opt-in (R-007) |
| `host` | `host` | string | `""` | Host (stored, not yet consumed by emitter) |
| `route_id` | `route_id` | string | `""` | Route ID (stored, not yet consumed by emitter) |
### `health`
Health check. Required for `Service`.
| Field | Key | Type | Default | Notes |
|-------|-----|------|---------|-------|
| `check_type` | `check_type` | string | — | Check type (e.g. `http`) |
| `interval` | `interval` | duration string | — | Check interval (e.g. `5s`) |
| `timeout` | `timeout` | duration string | — | Check timeout |
| `unhealthy_threshold` | `unhealthy_threshold` | int | `0` | Failures before unhealthy |
Maps to Traefik `healthCheck` stanza (`path: /healthz`).
### `lifecycle`
Lifecycle hooks. Maps to systemd `ExecStartPost` / `ExecStop`.
| Field | Key | Type | Default | systemd mapping |
|-------|-----|------|---------|-----------------|
| `post_start` | `post_start` | string list | nil | `ExecStartPost=` (runs after main starts) |
| `pre_stop` | `pre_stop` | string list | nil | `ExecStop=` (runs before kill) |
**Example**:
```yaml
lifecycle:
pre_stop:
- /bin/sh -c 'sleep 5'
- /usr/local/bin/drain.sh
post_start:
- /usr/local/bin/warm-cache.sh
```
### `constraints`
CEL-subset expressions for node selection. Inline array or block list.
```yaml
constraints:
- node.role == "web"
- region == "us"
# or inline
constraints: ['node.role == "web"', 'region == "us"']
```
**CEL subset grammar** (hand-rolled, no CEL dependency):
- Node attributes: `node.hostname`, `node.kind`, `node.cpus`,
`node.memory`, `node.tags`, `node.runtimes`
- Bare identifiers: equivalent to `node.<name>`
- Literals: string (`"..."`), int
- Comparisons: `==`, `!=`, `>=`, `<=`, `>`, `<`
- Membership: `in`, `not in`
- Boolean: `and`, `or`, `not`, parentheses
- Anything outside the subset returns an error (node skipped, not
silently mis-evaluated)
### `affinity`
Co-location / anti-affinity rules.
```yaml
affinity:
- target: zone == "a"
weight: 80
- target: web
weight: -50 # anti-affinity (negative weight)
```
- `target`: CEL expression or bare workload name (for name-based
co-location).
- `weight`: positive = co-locate, negative = anti-affinity.
- Affinity is a **hint** (not a gate); evaluation failures are ignored.
### `tasks` (task group)
Multi-process alloc (P06). When `tasks` is non-empty, the alloc runs
multiple processes, each as its own systemd unit, grouped under a
systemd target.
```yaml
tasks:
- name: app
runtime:
one_of: process
command: /usr/bin/httpd -f
env:
LOG_LEVEL: debug
- name: sidecar
runtime:
one_of: wasm
command: /bin/wasm-runner sidecar.wasm
```
- A task with no `runtime:` inherits the top-level `spec.Runtime`.
- Each task can have its own `env:` overlay.
- `command` falls back: `task.Command``task.Runtime.Command`
`spec.Runtime.Command`.
- Task names must be unique within the group.
## Kinds matrix
| Feature | Job | Service | DaemonSet |
|---------|-----|---------|-----------|
| `count` | must be 1 | ≥ 1 | not allowed |
| `ports` | optional | **required** | not allowed |
| `service` block | not allowed | optional (implied) | not allowed |
| `restart` | optional | **required** | **required** |
| `update` | optional | **required** | optional |
| `health` | optional | **required** | optional |
| `runtime` | optional | **required** (or task group) | optional |
| `schedule` | optional | optional | **required** |
| `tasks` | optional | optional | optional |
| Traefik route | no | yes (implied) | no (by default) |
## Body semantics
The body after the closing `---` is preserved **byte-exact** (R-015) —
including trailing newlines, CRLF, BOM in body, and `---` inside code
fences. The body is carried verbatim to the target node. It is not
interpreted as commands/scripts by the parser today.
## Deprecated: HCL jobspec
The legacy HCL jobspec format is supported via an adapter during the
migration window. It is deprecated in v0.9 and will be removed in
v0.11.
```hcl
job "hello-orca" {
}
task "greet" {
command = "/bin/echo"
args = ["hello", "from", "orca"]
}
```
The adapter converts this to a `*WorkloadSpec{Kind: "Job", Name:
"hello-orca", Count: 1, Runtime: {OneOf: "process", Command:
"/bin/echo"}}`. Use `.md` for all new jobspecs.
## See also
- [docs/cli.md](cli.md) — CLI reference (including `orca job run`)
- [docs/ingress.md](ingress.md) — Traefik ingress configuration
- [examples/full-stack/](../examples/full-stack/) — Full-stack example jobspecs
+158 -77
View File
@@ -1,96 +1,177 @@
# Namespace and Paths
Orca stores all on-disk state (SQLite database, CA certs, server certs,
config) under a single **namespace root** directory. This document
describes how that root is resolved and how to override it.
Orca stores all on-disk state under a single **namespace root**
directory. The v0.9 re-architecture introduced a multi-namespace
layout (R-002) where each namespace is a self-contained directory tree
with its own database, jobs, allocs, env, and secrets. A `cluster/`
directory holds cluster-wide artifacts shared across namespaces.
## Default: User-Level (`~/.orca`)
> **v0.9 layout (canonical)**: This document describes the v0.9
> multi-namespace layout. The v0.8 flat layout (`orca.db`, `ca.crt`,
> `server.crt` at the root) is deprecated and will be removed in
> v0.11. See [v0.8 flat layout](#deprecated-v08-flat-layout) below.
By default, the namespace root is `~/.orca` (i.e., `$HOME/.orca`).
All orca state lives under this directory:
## Namespace root resolution
| Path | Contents |
|------|----------|
| `~/.orca/orca.db` | SQLite database (jobs, nodes, tasks, audit log, capacity) |
| `~/.orca/ca.crt` | CA certificate (PEM, mode 0644) |
| `~/.orca/ca.key` | CA private key (PEM, mode 0600) |
| `~/.orca/server.crt` | Server certificate (PEM, mode 0644) |
| `~/.orca/server.key` | Server private key (PEM, mode 0600) |
## Override: `ORCA_HOME` Environment Variable (REQ-041)
Set the `ORCA_HOME` environment variable to change the namespace root
for **all** orca components (database, certs, init, daemon):
```bash
export ORCA_HOME=/var/lib/orca
orca init # creates /var/lib/orca/
orca daemon # reads /var/lib/orca/orca.db
orca cert ca-init # writes CA to /var/lib/orca/
```
This is the single source of truth for the namespace root. Every
component that reads or writes on-disk state resolves the root via
`ORCA_HOME` (falling back to `~/.orca` when unset).
### Use cases
- **Testing**: point `ORCA_HOME` at a temp directory.
- **Multi-instance**: run multiple orca daemons on the same host with
different `ORCA_HOME` values.
- **Custom layout**: store state on a mounted volume
(`ORCA_HOME=/mnt/orca-data`).
## System-Level: `--system` Flag (REQ-042)
The `--system` persistent flag selects the system-level namespace root
`/root/.orca`. This is intended for root-owned system deployments
(where orca runs as a system service under root):
```bash
sudo orca --system init # creates /root/.orca/
sudo orca --system daemon # reads /root/.orca/orca.db
sudo orca --system cert ca-init # writes CA to /root/.orca/
```
The `--system` flag is equivalent to setting `ORCA_HOME=/root/.orca`,
but it is a CLI convenience that does not require exporting an env var.
If `ORCA_HOME` is already set to a different value, `--system` returns
an error (to avoid silent namespace mismatches).
### Path layout
System-level uses the same directory shape as user-level, just under
`/root/.orca` instead of `~/.orca`:
| Path | Contents |
|------|----------|
| `/root/.orca/orca.db` | SQLite database |
| `/root/.orca/ca.crt` | CA certificate |
| `/root/.orca/ca.key` | CA private key |
| `/root/.orca/server.crt` | Server certificate |
| `/root/.orca/server.key` | Server private key |
## Resolution Order
The namespace root is resolved in this order:
1. If `--system` flag is passed → root is `/root/.orca` (errors if
`ORCA_HOME` is set to a conflicting value).
2. Else if `ORCA_HOME` is set → root is `$ORCA_HOME`.
3. Else → root is `~/.orca` (`$HOME/.orca`).
## `ORCA_DB` Override
### `ORCA_HOME` (REQ-041)
For finer-grained control, `ORCA_DB` overrides **only** the database
path (not the cert paths). This is primarily a testing affordance. When
`ORCA_DB` is set, certs still resolve under `ORCA_HOME` (or `~/.orca`).
Set the `ORCA_HOME` environment variable to change the namespace root
for all orca components:
```bash
export ORCA_HOME=/var/lib/orca
orca init # creates /var/lib/orca/
orca ns create prod
```
### `--system` (REQ-042)
The `--system` persistent flag selects the system-level namespace root
`/root/.orca`:
```bash
sudo orca --system init # creates /root/.orca/
sudo orca --system ns list
```
If `ORCA_HOME` is already set to a different value, `--system` returns
an error (to avoid silent namespace mismatches).
## v0.9 multi-namespace layout (R-002)
```
$ORCA_HOME/
├── cluster/ # cluster-wide (NOT a workload namespace)
│ ├── ca.crt, ca.key # step-ca root (R-006, D-101)
│ ├── master.key # AES-256-GCM root (R-011, mode 0600)
│ ├── config.md # Markdown frontmatter config (R-014)
│ ├── known_hosts # SSH known_hosts (D-035)
│ ├── orca_ssh_key # orca SSH private key (D-037)
│ ├── orca_ssh_key.pub # orca SSH public key
│ ├── peers/<host>/ # per-peer directory
│ ├── txns/ # cluster transaction log (R-016)
│ └── state/ # cluster state
├── _defaults/ # implicit root namespace (always exists)
│ ├── ns.md # namespace frontmatter (kind: Namespace)
│ ├── .env # per-namespace env
│ ├── .env.secrets # encrypted secrets
│ ├── db/orca.db # per-namespace SQLite database
│ ├── jobs/ # submitted jobspecs
│ └── alloc/ # allocation state
├── <explicit-namespace>/ # operator-created (e.g., prod, staging)
│ ├── ns.md
│ ├── .env, .env.secrets
│ ├── db/orca.db
│ ├── jobs/, alloc/
│ └── syncthing/ # Syncthing config (if replicated volumes)
└── orca_cache.db # CLI-side cache (R-008)
```
### Key points
- **`_defaults/`** is the implicit root namespace (D-159). It always
exists. Every namespace inherits from `_defaults` and cannot opt out
(D-185, D-187).
- **`cluster/`** is NOT a workload namespace — it holds cluster-wide
artifacts (CA, master key, SSH keys, known_hosts, peers, txns).
- **Per-namespace DBs**: each namespace has its own
`db/orca.db` (R-002). No namespace column in SQLite.
- **Namespace inheritance**: child namespaces inherit env and
constraints from parents (via `ns.md` frontmatter `parents:` field).
`_defaults` is always appended last in the inheritance chain.
- **`orca ns` subcommands**: `list`, `create`, `delete`, `inspect`,
`validate` — see [docs/cli.md](cli.md#orca-ns).
### Path reference (`internal/paths/`)
| Function | Path | Contents |
|----------|------|----------|
| `Root()` | `$ORCA_HOME` | Namespace root |
| `ClusterDir()` | `Root()/cluster` | Cluster-wide artifacts |
| `NamespaceDir(ns)` | `Root()/ns` | Per-namespace directory |
| `NSDb(ns)` | `Root()/ns/db/orca.db` | Per-namespace SQLite DB |
| `NSEnv(ns)` | `Root()/ns/.env` | Per-namespace env |
| `NSSecrets(ns)` | `Root()/ns/.env.secrets` | Encrypted secrets |
| `NSJobs(ns)` | `Root()/ns/jobs` | Jobs dir |
| `NSAlloc(ns)` | `Root()/ns/alloc` | Alloc dir |
| `NSMd(ns)` | `Root()/ns/ns.md` | Namespace frontmatter |
| `DefaultNamespace()` | `_defaults` | Implicit root (D-159) |
| `CACertPath()` | `ClusterDir()/ca.crt` | step-ca root (D-101) |
| `MasterKeyPath()` | `ClusterDir()/master.key` | AES-256-GCM root key |
| `KnownHostsPath()` | `ClusterDir()/known_hosts` | SSH known_hosts |
| `SSHKeyPath()` | `ClusterDir()/orca_ssh_key` | orca SSH private key |
| `ConfigPath()` | `ClusterDir()/config.md` | Markdown config (R-014) |
| `CacheDB()` | `Root()/orca_cache.db` | CLI-side cache (R-008) |
| `PeersDir()` | `ClusterDir()/peers` | Peers directory |
| `TxnDir()` | `ClusterDir()/txns` | Transaction log (R-016) |
## Creating and managing namespaces
```bash
# List all namespaces
orca ns list
# Create a namespace (inherits from _defaults)
orca ns create prod
# Create a namespace with an explicit parent
orca ns create staging --parent prod
# Inspect the effective inheritance chain + merged env
orca ns inspect prod
# Validate a namespace's inheritance chain
orca ns validate prod
# Delete an empty namespace (refuses if jobs/ or alloc/ non-empty)
orca ns delete staging
```
See [docs/cli.md](cli.md#orca-ns) for the full `orca ns` reference.
## `ORCA_DB` override
For finer-grained control, `ORCA_DB` overrides only the database path
(not the cert/namespace paths). This is primarily a testing affordance.
```bash
export ORCA_DB=/tmp/test.db
orca daemon # uses /tmp/test.db for the DB, ~/.orca/ for certs
orca init # uses /tmp/test.db for the DB, ~/.orca/ for everything else
```
## See Also
## Deprecated: v0.8 flat layout
> **Deprecated in v0.9**: The v0.8 flat layout (`orca.db`, `ca.crt`,
> `ca.key`, `server.crt`, `server.key` at the namespace root) is
> superseded by the v0.9 multi-namespace layout (R-002). The v0.8
> layout is supported during the dual-write window via
> `internal/certpaths` (a thin shim) and will be removed in v0.11.
The v0.8 flat layout stored all state at the namespace root:
| Path | Contents |
|------|----------|
| `~/.orca/orca.db` | SQLite database |
| `~/.orca/ca.crt` | CA certificate |
| `~/.orca/ca.key` | CA private key |
| `~/.orca/server.crt` | Server certificate |
| `~/.orca/server.key` | Server private key |
The v0.9 re-architecture moved these to `cluster/` (CA, SSH keys) and
per-namespace `db/` (SQLite) to support multi-tenancy (R-002). The
`orca doctor --legacy-paths` command (v0.11-P14c) will detect v0.8
residue and recommend migration.
## See also
- [Install Guide](install.md) — 1-liner install with `install.sh`.
- [Docker Guide](docker.md) — running orca in a container (uses
`ORCA_HOME=/var/lib/orca` inside the image).
- [Docker Guide](docker.md) — running orca in a container.
- [CLI Reference](cli.md) — `orca ns` subcommands.
- [Jobspec Reference](jobspec.md) — markdown frontmatter schema.
+191
View File
@@ -0,0 +1,191 @@
# 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
```bash
# 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
```bash
# 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:
```bash
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
```bash
orca ns create prod --parent _defaults
```
This creates `~/.orca/prod/` with `db/`, `jobs/`, `alloc/`, and `ns.md`.
### Step 5: Submit the stack
```bash
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
```bash
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):
```bash
# 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](../../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:
```bash
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.
+46
View File
@@ -0,0 +1,46 @@
---
kind: Service
name: api
count: 2
runtime:
one_of: process
command: /bin/sleep 3600
ports:
- name: api
port: 9090
restart:
mode: service
attempts: 3
delay: 5s
update:
strategy: canary
canary: 1
max_parallel: 1
auto_promote: false
min_healthy_time: 30s
healthy_deadline: 5m
service:
name: api
port: 9090
bind: 127.0.0.1
health:
check_type: http
interval: 10s
timeout: 2s
unhealthy_threshold: 3
constraints:
- node.role == "api"
- node.cpus >= 2
env:
DB_HOST: postgres
DB_PORT: "5432"
LOG_LEVEL: info
---
# API Server
Backend API service binding to 127.0.0.1:9090 (TCP opt-in, R-007).
Canary update strategy with manual promote. Two replicas with CPU
constraint (>= 2 vCPUs) and API-role node selection.
> **Production substitution**: replace `runtime.command` with your
> actual API binary, e.g. `/usr/bin/api-server --listen 127.0.0.1:9090`.
+60
View File
@@ -0,0 +1,60 @@
package fullstack_test
import (
"os"
"path/filepath"
"testing"
"git.cloudinit.dev/coreci/orca/internal/jobspec"
"git.cloudinit.dev/coreci/orca/internal/spec/schema"
)
// TestExamplesValidate parses and validates every jobspec in
// examples/full-stack/ against the current parser and schema validators
// (gate C-20, REQ-094). This ensures the example jobspecs use only
// fields that exist in the current WorkloadSpec struct and pass the
// per-kind validators.
func TestExamplesValidate(t *testing.T) {
dir := filepath.Join("..", "..", "examples", "full-stack")
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("read examples dir: %v", err)
}
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
// Skip README.md and other non-jobspec markdown files.
if name == "README.md" {
continue
}
ext := filepath.Ext(name)
if ext != ".md" && ext != ".yaml" && ext != ".yml" {
continue
}
t.Run(name, func(t *testing.T) {
path := filepath.Join(dir, name)
spec, err := jobspec.ParseFile(path)
if err != nil {
t.Fatalf("ParseFile %s: %v", name, err)
}
if spec == nil {
t.Fatalf("ParseFile %s: spec is nil", name)
}
if spec.Kind == "" {
t.Fatalf("ParseFile %s: kind is empty", name)
}
if spec.Name == "" {
t.Fatalf("ParseFile %s: name is empty", name)
}
validator, err := schema.ValidatorFor(spec.Kind)
if err != nil {
t.Fatalf("ValidatorFor %s (kind %s): %v", name, spec.Kind, err)
}
if err := validator.Validate(spec); err != nil {
t.Fatalf("Validate %s: %v", name, err)
}
})
}
}
+43
View File
@@ -0,0 +1,43 @@
---
kind: Service
name: log-shipper
count: 1
runtime:
one_of: process
command: /bin/sleep 3600
ports:
- name: metrics
port: 2024
restart:
mode: service
attempts: 3
delay: 10s
update:
strategy: rolling
max_parallel: 1
health:
check_type: http
interval: 30s
timeout: 5s
unhealthy_threshold: 3
constraints:
- node.role == "logs"
env:
LOG_LEVEL: warn
OUTPUT: unix:///run/orca/alloc-log-collector/ingest.sock
---
# Log Shipper
Log shipper service (fluent-bit) running on a dedicated logs-role node.
Exposes a metrics port for health checking. Ships logs to a central
collector via Unix socket.
> **Production substitution**: replace `runtime.command` with your
> actual log shipper binary, e.g.
> `/usr/bin/fluent-bit -c /etc/orca/log-shipper/fluent-bit.conf`.
> **Note**: DaemonSet kind is defined in the schema but the parser does
> not yet populate the `schedule:` block from frontmatter (v0.9 parser
> gap). This example uses `kind: Service` with `count: 1` and a
> `node.role == "logs"` constraint to achieve single-node placement
> until the parser gains `schedule:` support (v0.11).
+52
View File
@@ -0,0 +1,52 @@
---
kind: Service
name: postgres
count: 1
runtime:
one_of: process
command: /bin/sleep 3600
ports:
- name: pg
port: 5432
restart:
mode: service
attempts: 5
delay: 10s
update:
strategy: blue-green
min_healthy_time: 60s
healthy_deadline: 10m
service:
name: postgres
port: 5432
health:
check_type: http
interval: 15s
timeout: 5s
unhealthy_threshold: 3
volumes:
- name: data
type: host
source: replicate:peer-b,peer-c
target: /var/lib/postgresql/data
read_only: false
constraints:
- node.role == "db"
- node.cpus >= 4
- node.memory >= 8192
env:
POSTGRES_DB: appdb
POSTGRES_USER: orca
PGDATA: /var/lib/postgresql/data
---
# PostgreSQL
Database service with a single replica, blue-green update strategy,
and volume replication via Syncthing (replicate:peer-b,peer-c). The
data volume is replicated to two peers for fault tolerance. Health
check on port 5432. Constraints require DB-role nodes with >= 4 vCPUs
and >= 8 GiB memory.
> **Production substitution**: replace `runtime.command` with your
> actual postgres binary, e.g.
> `/usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/data`.
@@ -0,0 +1,9 @@
# Systemd unit for orca api service (alloc api-0)
# Generated by SystemdEmitter (internal/emitter/systemd.go)
# Path on target node: /etc/systemd/system/orca-v1-api.service
# service.bind: 127.0.0.1 (TCP opt-in, R-007)
[Service]
ExecStart=/bin/sleep 3600
RuntimeDirectory=orca/alloc-api-0
# socket: /run/orca/alloc-api-0/port-api.sock
ExecStartPre=/bin/echo orca: bind 127.0.0.1 port api (tcp, R-007 opt-in)
@@ -0,0 +1,7 @@
# Systemd unit for orca log-shipper service
# Generated by SystemdEmitter (internal/emitter/systemd.go)
# Path on target node: /etc/systemd/system/orca-v1-log-shipper.service
[Service]
ExecStart=/bin/sleep 3600
RuntimeDirectory=orca/alloc-log-shipper-0
# socket: /run/orca/alloc-log-shipper-0/port-metrics.sock
@@ -0,0 +1,11 @@
# Systemd unit for orca web-app service (alloc web-app-0)
# Generated by SystemdEmitter (internal/emitter/systemd.go)
# Path on target node: /etc/systemd/system/orca-v1-web-app.service
# Unit name prefix orca-v1- (dual-write window, REQ-090)
[Service]
ExecStart=/bin/sleep 3600
ExecStartPost=/bin/echo cache warmed
ExecStop=/bin/sleep 5
ExecStop=/bin/echo draining web-app
RuntimeDirectory=orca/alloc-web-app-0
# socket: /run/orca/alloc-web-app-0/port-http.sock
@@ -0,0 +1,23 @@
# Traefik dynamic config for orca api service
# Generated by TraefikEmitter (internal/emitter/traefik.go)
# Path on target node: /etc/traefik/dynamic/orca-api.yaml
# service.bind: 127.0.0.1 (TCP opt-in, R-007)
http:
routers:
orca-api:
rule: PathPrefix("/api")
service: orca-api
tls:
certResolver: orca
domains:
- main: "cluster.orca.local"
services:
orca-api:
loadBalancer:
servers:
- url: "http://127.0.0.1:9090"
- url: "http://127.0.0.1:9090"
healthCheck:
path: /healthz
interval: 10s
timeout: 2s
@@ -0,0 +1,24 @@
# Traefik dynamic config for orca web-app service
# Generated by TraefikEmitter (internal/emitter/traefik.go)
# Path on target node: /etc/traefik/dynamic/orca-web-app.yaml
# Atomic reload: write to .tmp + mv (gate C-10)
http:
routers:
orca-web-app:
rule: PathPrefix("/web-app")
service: orca-web-app
tls:
certResolver: orca
domains:
- main: "cluster.orca.local"
services:
orca-web-app:
loadBalancer:
servers:
- url: "unix:///run/orca/alloc-web-app-0/port-http.sock"
- url: "unix:///run/orca/alloc-web-app-1/port-http.sock"
- url: "unix:///run/orca/alloc-web-app-2/port-http.sock"
healthCheck:
path: /healthz
interval: 5s
timeout: 1s
+51
View File
@@ -0,0 +1,51 @@
---
kind: Service
name: web-app
count: 3
runtime:
one_of: process
command: /bin/sleep 3600
ports:
- name: http
port: 8080
restart:
mode: service
attempts: 5
delay: 2s
update:
strategy: rolling
max_parallel: 1
min_healthy_time: 10s
healthy_deadline: 2m
service:
name: web-app
port: 8080
health:
check_type: http
interval: 5s
timeout: 1s
unhealthy_threshold: 2
constraints:
- node.role == "web"
affinity:
- target: zone == "a"
weight: 80
lifecycle:
post_start:
- /bin/sh -c 'echo cache warmed'
pre_stop:
- /bin/sh -c 'sleep 5'
- /bin/sh -c 'echo draining web-app'
---
# Web App
Frontend web application serving HTTP on port 8080 via Unix socket.
Three replicas with rolling updates, anti-affinity for zone spreading,
and lifecycle hooks for cache warm-up and graceful drain.
> **Production substitution**: this example uses `/bin/sh -c 'echo ...
> sleep 3600'` so it runs out-of-the-box on any Linux machine. In a
> real deployment, replace the `runtime.command` with your actual
> binary, e.g. `/usr/bin/httpd -f /etc/orca/web-app/httpd.conf`, and
> replace the lifecycle hooks with your real scripts
> (`/usr/local/bin/warm-cache.sh`, `/usr/local/bin/drain.sh`).
+27
View File
@@ -0,0 +1,27 @@
---
kind: Job
name: worker
runtime:
one_of: process
command: /bin/echo worker processing batch
timeout: 300s
env:
QUEUE_URL: unix:///run/orca/alloc-worker/queue.sock
BATCH_SIZE: "100"
LOG_LEVEL: debug
lifecycle:
post_start:
- /bin/sh -c 'echo worker registered'
pre_stop:
- /bin/sh -c 'echo draining worker queue'
---
# Worker
One-shot batch worker that processes items from a queue. Runs once,
exits on completion or after 300s timeout. Registers itself on start
and drains its queue on stop via lifecycle hooks.
> **Production substitution**: replace `runtime.command` with your
> actual worker binary, e.g. `/usr/bin/python3 /opt/orca/jobs/worker.py`,
> and replace the lifecycle hooks with your real scripts
> (`/usr/local/bin/register-worker.sh`, `/usr/local/bin/drain-queue.sh`).
+22 -1
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
@@ -337,6 +338,12 @@ func toTaskSpecs(in []jobspec.TaskSpec) []engine.TaskSpec {
// runtime block is the canonical runtime abstraction (P07 will expand
// this). When Runtime is nil we emit a single no-op task to preserve
// the legacy "at least one task" invariant.
//
// The runtime command string is split into binary + args via
// splitCommand so that exec.Command receives the binary path and the
// args as separate elements. Without this split, a command like
// "/usr/bin/httpd -f /etc/orca/web-app/httpd.conf" is treated as a
// single file path and fork/exec fails with "no such file or directory".
func workloadToTaskSpecs(spec *jobspec.WorkloadSpec) []engine.TaskSpec {
if spec == nil {
return nil
@@ -344,8 +351,22 @@ func workloadToTaskSpecs(spec *jobspec.WorkloadSpec) []engine.TaskSpec {
if spec.Runtime == nil {
return []engine.TaskSpec{{Name: spec.Name, Command: "/bin/true"}}
}
bin, args := splitCommand(spec.Runtime.Command)
return []engine.TaskSpec{{
Name: spec.Name,
Command: spec.Runtime.Command,
Command: bin,
Args: args,
}}
}
// splitCommand splits a command string into binary + args using
// strings.Fields (handles multiple spaces/tabs). If the string is empty
// or all-whitespace, returns ("/bin/true", nil) so the executor still
// has a valid binary to run.
func splitCommand(s string) (string, []string) {
parts := strings.Fields(s)
if len(parts) == 0 {
return "/bin/true", nil
}
return parts[0], parts[1:]
}
+141
View File
@@ -0,0 +1,141 @@
package cli
import (
"testing"
"git.cloudinit.dev/coreci/orca/internal/jobspec"
)
func TestSplitCommand(t *testing.T) {
tests := []struct {
name string
input string
wantBin string
wantArgs []string
}{
{
name: "single binary",
input: "/bin/true",
wantBin: "/bin/true",
wantArgs: nil,
},
{
name: "binary with one arg",
input: "/bin/echo hello",
wantBin: "/bin/echo",
wantArgs: []string{"hello"},
},
{
name: "binary with multiple args",
input: "/usr/bin/httpd -f /etc/orca/web-app/httpd.conf",
wantBin: "/usr/bin/httpd",
wantArgs: []string{"-f", "/etc/orca/web-app/httpd.conf"},
},
{
name: "binary with sh -c and quoted string",
input: "/bin/sh -c 'echo hello world'",
wantBin: "/bin/sh",
wantArgs: []string{"-c", "'echo", "hello", "world'"},
},
{
name: "empty command falls back to /bin/true",
input: "",
wantBin: "/bin/true",
wantArgs: nil,
},
{
name: "all-whitespace command falls back to /bin/true",
input: " \t ",
wantBin: "/bin/true",
wantArgs: nil,
},
{
name: "multiple spaces between args",
input: "/bin/echo hello world",
wantBin: "/bin/echo",
wantArgs: []string{"hello", "world"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotBin, gotArgs := splitCommand(tt.input)
if gotBin != tt.wantBin {
t.Errorf("splitCommand(%q) bin = %q, want %q", tt.input, gotBin, tt.wantBin)
}
if len(gotArgs) != len(tt.wantArgs) {
t.Errorf("splitCommand(%q) args len = %d, want %d (got %v, want %v)",
tt.input, len(gotArgs), len(tt.wantArgs), gotArgs, tt.wantArgs)
return
}
for i, a := range gotArgs {
if a != tt.wantArgs[i] {
t.Errorf("splitCommand(%q) args[%d] = %q, want %q",
tt.input, i, a, tt.wantArgs[i])
}
}
})
}
}
func TestWorkloadToTaskSpecs_SplitsCommand(t *testing.T) {
spec := &jobspec.WorkloadSpec{
Name: "web-app",
Runtime: &jobspec.RuntimeBlock{
OneOf: "process",
Command: "/usr/bin/httpd -f /etc/orca/web-app/httpd.conf",
},
}
tasks := workloadToTaskSpecs(spec)
if len(tasks) != 1 {
t.Fatalf("expected 1 task, got %d", len(tasks))
}
if tasks[0].Command != "/usr/bin/httpd" {
t.Errorf("expected Command=/usr/bin/httpd, got %q", tasks[0].Command)
}
if len(tasks[0].Args) != 2 {
t.Fatalf("expected 2 args, got %d (%v)", len(tasks[0].Args), tasks[0].Args)
}
if tasks[0].Args[0] != "-f" || tasks[0].Args[1] != "/etc/orca/web-app/httpd.conf" {
t.Errorf("expected args [-f /etc/orca/web-app/httpd.conf], got %v", tasks[0].Args)
}
}
func TestWorkloadToTaskSpecs_NilRuntimeUsesBinTrue(t *testing.T) {
spec := &jobspec.WorkloadSpec{
Name: "noop",
}
tasks := workloadToTaskSpecs(spec)
if len(tasks) != 1 {
t.Fatalf("expected 1 task, got %d", len(tasks))
}
if tasks[0].Command != "/bin/true" {
t.Errorf("expected Command=/bin/true, got %q", tasks[0].Command)
}
if len(tasks[0].Args) != 0 {
t.Errorf("expected 0 args, got %d (%v)", len(tasks[0].Args), tasks[0].Args)
}
}
func TestWorkloadToTaskSpecs_EmptyCommandUsesBinTrue(t *testing.T) {
spec := &jobspec.WorkloadSpec{
Name: "empty",
Runtime: &jobspec.RuntimeBlock{
OneOf: "process",
Command: "",
},
}
tasks := workloadToTaskSpecs(spec)
if len(tasks) != 1 {
t.Fatalf("expected 1 task, got %d", len(tasks))
}
if tasks[0].Command != "/bin/true" {
t.Errorf("expected Command=/bin/true, got %q", tasks[0].Command)
}
}
func TestWorkloadToTaskSpecs_NilSpecReturnsNil(t *testing.T) {
tasks := workloadToTaskSpecs(nil)
if tasks != nil {
t.Errorf("expected nil, got %v", tasks)
}
}