feat(iac): scaffold orca-platform layout (M1.1)
ci / shared (pull_request) Successful in 4s
ci / validate (pull_request) Successful in 3s

Lands the per-VM × per-service manifest tree, per-env overlays, VM specs
for SysEleven provisioning, DNS zone placeholder, plan/apply/validate
scripts, and a Makefile.

Structure (per INFRASTRUCTURE.md §2 + IMPLEMENTATION_PLAN.md M1.1):
- manifests/{vm-edge,vm-control,vm-data,stage}/<service>.toml — 35 stubs
- overlays/{dev,stage,prod}/overlay.toml — env-selection rules
- vms/{vm-edge,vm-control,vm-data,stage}.toml — OpenStack flavor/IP/firewall
- dns/yourplatform.com.zone.template — PowerDNS zone (body lands in M0.3)
- cluster.toml.tmpl — cluster-level config rendered per env
- scripts/validate.sh — TOML parse + structural sanity
- scripts/plan.sh — merge manifests + overlay → .orca-out/<env>/
- scripts/apply.sh — push to Orca controller (no-op until M1.2)
- Makefile — validate / plan / apply / diff / clean

Each manifest header names the milestone that finalises its real values;
images today are 'placeholder' for services that need their own repo to
exist first. make validate stays green; apply gates on ORCA_API_URL.

CI workflow swapped from the broken 'orca validate' to 'make validate',
which calls a Python TOML parser plus structural checks (placement.node
matches vm dir, resources.memory present, no mis-nested keys).

Refs: M1.1
This commit is contained in:
2026-05-18 22:02:11 +02:00
parent c196f5e801
commit 6cd1a1546c
56 changed files with 1122 additions and 30 deletions
+10 -9
View File
@@ -1,5 +1,6 @@
# CI for orca-platform (IaC). `shared` always runs; `validate` activates # CI for orca-platform (IaC).
# when at least one Orca manifest lands. # `shared` always runs (commitlint + gitleaks + trivy fs).
# `validate` always runs (parses every manifest + overlay + vm spec).
name: ci name: ci
on: on:
@@ -53,18 +54,18 @@ jobs:
TRIVY_VERSION=0.70.0 TRIVY_VERSION=0.70.0
curl -fsSL "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" \ curl -fsSL "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" \
| tar -xz -C /tmp trivy | tar -xz -C /tmp trivy
/tmp/trivy fs --severity HIGH,CRITICAL --exit-code 1 --no-progress --skip-dirs node_modules,target,dist . /tmp/trivy fs --severity HIGH,CRITICAL --exit-code 1 --no-progress --skip-dirs node_modules,target,dist,.orca-out .
validate: validate:
runs-on: docker runs-on: docker
if: hashFiles('**/*.orca.yaml','**/*.orca.yml','manifests/**') != ''
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install orca - name: setup python
shell: bash
run: | run: |
curl -fsSL https://orca.meghsakha.com/install.sh | sh which python3
orca version python3 --version
- name: orca validate - name: make validate
run: orca validate ./ run: make validate
+1
View File
@@ -35,3 +35,4 @@ vendor/
# Rust # Rust
**/target/ **/target/
.orca-out/
+1
View File
@@ -6,6 +6,7 @@ Generated section is appended on release tag via `git-cliff` (see `.gitea/workfl
## [Unreleased] ## [Unreleased]
### Added ### Added
- feat(iac): scaffold orca-platform — manifests/, overlays/, vms/, dns/, scripts/, Makefile (M1.1)
- -
### Changed ### Changed
+35
View File
@@ -0,0 +1,35 @@
# orca-platform — IaC for the Breakpilot Platform.
#
# make validate parse + structural sanity check every manifest
# make plan ENV=<env> resolve manifests + overlay into .orca-out/<env>/
# make apply ENV=<env> (M1.2+) push resolved set to Orca controller
# make diff ENV=<env> alias for plan
# make clean remove .orca-out/
.PHONY: help validate plan apply diff clean
ENV ?=
ORCA_API_URL ?=
help:
@echo "orca-platform targets:"
@echo " make validate syntax + structural check (all manifests)"
@echo " make plan ENV=<env> resolve manifests for env (dev/stage/prod)"
@echo " make apply ENV=<env> push resolved set (no-op until M1.2)"
@echo " make diff ENV=<env> alias for plan"
@echo " make clean remove .orca-out/"
validate:
@./scripts/validate.sh
plan:
@./scripts/plan.sh
diff: plan
apply:
@./scripts/apply.sh
clean:
@rm -rf .orca-out
@echo "removed .orca-out/"
+73 -21
View File
@@ -1,58 +1,110 @@
# orca-platform # orca-platform
IaC for VMs, Orca manifests, DNS, TLS, backups. IaC for the Breakpilot Platform: per-VM Orca service manifests, per-env overlays, DNS zones, backup/restore tooling, and the `make plan`/`make apply` wrappers.
> Part of the **Breakpilot Platform**. For the big picture see [`platform/docs`](https://gitea.meghsakha.com/platform/docs): > Part of the **Breakpilot Platform**. For the big picture see [`platform/docs`](https://gitea.meghsakha.com/platform/docs):
> [Architecture](https://gitea.meghsakha.com/platform/docs/src/branch/main/PLATFORM_ARCHITECTURE.md) · > [Architecture](https://gitea.meghsakha.com/platform/docs/src/branch/main/PLATFORM_ARCHITECTURE.md) ·
> [Infrastructure](https://gitea.meghsakha.com/platform/docs/src/branch/main/INFRASTRUCTURE.md) · > [Infrastructure](https://gitea.meghsakha.com/platform/docs/src/branch/main/INFRASTRUCTURE.md) ·
> [Product Integration Spec](https://gitea.meghsakha.com/platform/docs/src/branch/main/PRODUCT_INTEGRATION_SPEC.md) ·
> [Implementation Plan](https://gitea.meghsakha.com/platform/docs/src/branch/main/IMPLEMENTATION_PLAN.md) > [Implementation Plan](https://gitea.meghsakha.com/platform/docs/src/branch/main/IMPLEMENTATION_PLAN.md)
## What this is ## What this is
IaC for VMs, Orca manifests, DNS, TLS, backups. Scaffolded under milestone M1.1. See [`platform/docs`](https://gitea.meghsakha.com/platform/docs) for the full architecture context. The single source of truth for which container runs on which VM in which environment. Every change to prod infrastructure should flow through this repo — never through `orca deploy` from a laptop.
**Plane:** Infra **Plane:** Infra
**Owner:** @sharang **Owner:** @sharang
**Status:** pre-alpha **Status:** pre-alpha (M1.1 — layout only; real values land per the per-milestone schedule below)
**Linked milestone:** [M1.1](https://gitea.meghsakha.com/platform/docs/src/branch/main/IMPLEMENTATION_PLAN.md) **Linked milestone:** [M1.1](https://gitea.meghsakha.com/platform/docs/src/branch/main/IMPLEMENTATION_PLAN.md)
## Directory layout
```
.
├── manifests/ # Base service.toml per VM × service (35 stubs)
│ ├── vm-edge/ Identity + Infra plane services
│ ├── vm-control/ Control plane services
│ ├── vm-data/ Data plane services
│ └── stage/ Stage (app plane only)
├── overlays/ # Per-env sparse deltas applied on top of manifests/
│ ├── dev/overlay.toml no-op; dev runs docker-compose per-service
│ ├── stage/overlay.toml include manifests/stage/, image_tag=env-stage
│ └── prod/overlay.toml include vm-{edge,control,data}, image_tag=env-prod
├── vms/ # OpenStack VM specs (consumed by M1.2 provisioner)
│ ├── vm-edge.toml m2.small, public IP, identity+infra
│ ├── vm-control.toml m2.medium, private, control plane
│ ├── vm-data.toml m2.medium, private, data plane (scale driver)
│ └── stage.toml m2.small, public IP, ephemeral
├── dns/
│ └── yourplatform.com.zone.template PowerDNS zone — body lands in M0.3
├── cluster.toml.tmpl # Cluster-level config (acme_email, backup, ai); rendered per env
├── scripts/
│ ├── validate.sh # `make validate`
│ ├── plan.sh # `make plan ENV=<env>` → .orca-out/<env>/
│ ├── apply.sh # `make apply ENV=<env>` (no-op until M1.2)
│ └── restore-drill.sh.template M1.3 placeholder
└── Makefile # validate / plan / apply / diff / clean
```
## Run locally ## Run locally
```bash ```bash
# prerequisites: see CONTRIBUTING.md for tooling once code lands make validate # check all manifests parse + have required fields
make dev # starts dependencies + this service on http://localhost:3000 make plan ENV=stage # resolve manifests for stage → .orca-out/stage/
make test # unit + integration make plan ENV=prod # same for prod
make e2e # only if this repo ships user-facing flows make apply ENV=stage # no-op until M1.2 stands up the Orca controller
``` ```
Local secrets come from `.env.local` (gitignored). Template at `.env.example`. `make validate` runs in CI on every PR.
## Per-milestone fill-in schedule
Each stub manifest in `manifests/` carries a header comment naming the milestone that finalises its real values. Summary:
| Milestone | What it fills in |
|---|---|
| **M0.3** | `vm-edge/powerdns-auth.toml`, DNS zone body, orca-proxy routes |
| **M1.2** | VM provisioning (consumes `vms/*.toml`); brings `make apply` online |
| **M1.3** | Backup cron services + `scripts/restore-drill.sh` |
| **M2.1** | `vm-edge/keycloak.toml` + `pg-keycloak.toml` |
| **M3.1** | `vm-edge/infisical.toml` + `pg-infisical.toml` + `redis-infisical.toml` |
| **M3.2** | `vm-control/stalwart.toml` |
| **M4.1** | `vm-control/tenant-registry.toml` + `vm-data/pg-app.toml` |
| **M5.1** | `vm-control/customer-portal.toml` + stage equivalents |
| **M6.x** | `vm-data/certifai-dashboard.toml`, `mongodb.toml`, `litellm.toml` |
| **M7.x** | compliance services on vm-data + stage |
| **M8.1** | `vm-control/erpnext.toml`, `mariadb.toml`, `redis-erpnext.toml` |
| **M9.1** | `vm-control/frappe-hd.toml` |
Until the milestone PR lands, the stub still parses and `make validate` stays green — but `apply` will refuse a stub that hasn't replaced its `placeholder` image tag (gate to be added with the first real image).
## Endpoints / surface ## Endpoints / surface
{{For services: list the top-level routes or commands. | Target | What it does |
For libraries: list the public API entry points. |---|---|
For IaC: list the make targets.}} | `make validate` | Parse + structural check (no cluster contact) |
| `make plan ENV=<env>` | Resolve manifests + overlay → `.orca-out/<env>/` |
| `make apply ENV=<env>` | Push to Orca controller at `$ORCA_API_URL` (M1.2 brings this online) |
| `make diff ENV=<env>` | Alias for `plan` |
| `make clean` | Remove `.orca-out/` |
## Deployment ## Deployment
| Env | URL | How | | Env | Apply path | Trigger |
|---|---|---| |---|---|---|
| dev | `http://localhost:3000` | `make dev` | | dev | `docker-compose` in each product repo | dev's machine |
| stage | `https://orca-platform.stage.yourplatform.com` | auto on merge to `main` | | stage | `make apply ENV=stage` against the stage Orca controller | CI on merge to main + image build |
| prod | `https://orca-platform.yourplatform.com` | manual: tag `vX.Y.Z` + sign-off | | prod | `make apply ENV=prod` against the prod Orca controller | release tag `vX.Y.Z` + sign-off |
Rollback: `orca rollout undo orca-platform --env={{env}}`. `apply` for prod will be gated by the production-promotion gate (24h stage soak + manual sign-off) per `IMPLEMENTATION_PLAN.md §1.6`. Wiring lands in M1.2.
## Observability ## Observability
- Traces, logs, metrics: [SigNoz](https://signoz.meghsakha.com) — service name `orca-platform` - Traces, logs, metrics: [SigNoz](https://signoz.meghsakha.com) — service name per individual container
- Audit events: Tenant Registry `/audit` (Retraced-shape schema) - On-call: `oncall@yourplatform.com` · runbooks at `platform/docs/runbooks/`
- On-call: `oncall@yourplatform.com` · runbook at `platform/docs/runbooks/orca-platform.md`
## Contributing ## Contributing
See [`CONTRIBUTING.md`](./CONTRIBUTING.md). TL;DR: branch from main, open a PR, 1 review + green CI, squash-merge. See [`CONTRIBUTING.md`](./CONTRIBUTING.md). Every PR touching `manifests/` MUST keep `make validate` green; CI enforces it.
## License ## License
+29
View File
@@ -0,0 +1,29 @@
# Cluster-level config rendered per env.
# Real values get substituted by `make plan ENV=<env>` from environment +
# overlays/<env>/overlay.toml.
#
# Schema mirrors ~/workspace/orca-infra/cluster.toml (Orca-native).
[cluster]
name = "breakpilot-${ENV}"
domain = "${DOMAIN}"
acme_email = "oncall@yourplatform.com"
[ai]
provider = "litellm"
endpoint = "https://llm.yourplatform.com"
model = "gpt-oss-120b"
api_key = "${secrets.LITELLM_API_KEY}"
[backup]
enabled = true
schedule = "0 0 3 * * *"
retention_days = 30
[[backup.targets]]
type = "s3"
endpoint = "https://s3.dus2.cloud.syseleven.net"
bucket = "platform-${ENV}-backups"
region = "dus2"
access_key = "${secrets.S3_ACCESS_KEY}"
secret_key = "${secrets.S3_SECRET_KEY}"
+20
View File
@@ -0,0 +1,20 @@
; PowerDNS authoritative zone for yourplatform.com.
; Source-of-truth: this file. Synced into PowerDNS by M0.3 deploy step.
; Real records (apex, wildcards, A, MX, SPF/DKIM/DMARC) land with M0.3.
$ORIGIN yourplatform.com.
$TTL 60
@ IN SOA ns1.yourplatform.com. oncall.yourplatform.com. (
; serial — bumped by CI on every commit
2026051800
3600 ; refresh
600 ; retry
604800 ; expire
60 ; minimum TTL
)
@ IN NS ns1.yourplatform.com.
@ IN NS ns2.yourplatform.com.
; A records, wildcards, mail/spf/dkim/dmarc land in M0.3
+28
View File
@@ -0,0 +1,28 @@
# Manifests
One `service.toml` per service, grouped by host VM, per `INFRASTRUCTURE.md §2`.
| Directory | VM | Plane(s) | Owner milestone of "real" config |
|---|---|---|---|
| `vm-edge/` | vm-edge | Identity + Infra | M2.1 (Keycloak), M3.1 (Infisical), M0.3 (PowerDNS), M2.x (Gitea), M1.2 (proxy) |
| `vm-control/` | vm-control | Control | M5.1 (portal), M4.1 (tenant-registry), M8.1 (ERPNext), M3.2 (Stalwart) |
| `vm-data/` | vm-data | Data | M6.x (CERTifAI), M7.x (compliance), M4.1 (pg-app) |
| `stage/` | stage | App plane only | promotion target of stage builds |
Each file in this directory is currently a **shape-only stub** — fields are set but image references and env wiring will be finalised by the milestone listed in the file header.
## Adding a new service
1. Pick the owning VM per `INFRASTRUCTURE.md §2`.
2. Create `<vm-name>/<service-name>.toml` following the shape of an existing stub.
3. Set `placement.node = "<vm-name>"`, `resources.memory`/`cpu` per the co-tenant budget in `INFRASTRUCTURE.md §6`.
4. Reference secrets as `${secrets.NAME}` — Infisical resolves these. No plaintext values except the Keycloak bootstrap DB URI exception (`INFRASTRUCTURE.md §8 rule 3`).
5. Run `make validate` before pushing.
## Validation
`make validate` parses every TOML and checks required fields (`name`, image OR build OR module, `placement.node`, `resources.memory`). It does NOT contact a running cluster.
`make plan ENV=<env>` merges the base manifest with the matching overlay in `overlays/<env>/` and prints the resulting service definitions. It is a no-op until matching overlays exist for the env.
`make apply ENV=<env>` is gated on a real Orca controller URL — refuses to run until `ORCA_API_URL` is set (lands in M1.2).
+14
View File
@@ -0,0 +1,14 @@
# admin-compliance stub — full config lands in M7.x.
# Host: stage. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "admin-compliance"
image = "registry.yourplatform.com/admin-compliance:env-stage"
port = 3002
[service.placement]
node = "stage"
[service.resources]
memory = "256Mi"
cpu = 0.25
+14
View File
@@ -0,0 +1,14 @@
# ai-compliance-sdk stub — full config lands in M7.x.
# Host: stage. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "ai-compliance-sdk"
image = "registry.yourplatform.com/ai-compliance-sdk:env-stage"
port = 3001
[service.placement]
node = "stage"
[service.resources]
memory = "512Mi"
cpu = 0.25
+14
View File
@@ -0,0 +1,14 @@
# backend-compliance stub — full config lands in M7.x.
# Host: stage. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "backend-compliance"
image = "registry.yourplatform.com/backend-compliance:env-stage"
port = 3000
[service.placement]
node = "stage"
[service.resources]
memory = "512Mi"
cpu = 0.25
+14
View File
@@ -0,0 +1,14 @@
# certifai-dashboard stub — full config lands in M6.x.
# Host: stage. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "certifai-dashboard"
image = "registry.yourplatform.com/certifai:env-stage"
port = 3000
[service.placement]
node = "stage"
[service.resources]
memory = "1Gi"
cpu = 0.5
+15
View File
@@ -0,0 +1,15 @@
# customer-portal stub — full config lands in M5.1.
# Host: stage. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "customer-portal"
image = "registry.yourplatform.com/portal:env-stage"
port = 3000
domain = "*.stage.yourplatform.com"
[service.placement]
node = "stage"
[service.resources]
memory = "1Gi"
cpu = 0.5
+14
View File
@@ -0,0 +1,14 @@
# litellm stub — full config lands in M6.x.
# Host: stage. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "litellm"
image = "ghcr.io/berriai/litellm:main-stable"
port = 4000
[service.placement]
node = "stage"
[service.resources]
memory = "512Mi"
cpu = 0.25
+15
View File
@@ -0,0 +1,15 @@
# mongodb-stage stub — full config lands in M6.x.
# Host: stage. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
# Ephemeral.
[[service]]
name = "mongodb-stage"
image = "mongo:7"
port = 27017
[service.placement]
node = "stage"
[service.resources]
memory = "512Mi"
cpu = 0.25
+15
View File
@@ -0,0 +1,15 @@
# orca-proxy stub — full config lands in M1.2.
# Host: stage. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
# Stage proxy only routes to stage app containers.
[[service]]
name = "orca-proxy"
image = "orca-managed/orca-proxy:placeholder"
port = 443
[service.placement]
node = "stage"
[service.resources]
memory = "256Mi"
cpu = 0.5
+15
View File
@@ -0,0 +1,15 @@
# pg-app-stage stub — full config lands in M4.1.
# Host: stage. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
# Ephemeral; no backup, no volume; reset on each release.
[[service]]
name = "pg-app-stage"
image = "postgres:16-alpine"
port = 5432
[service.placement]
node = "stage"
[service.resources]
memory = "1Gi"
cpu = 0.5
+15
View File
@@ -0,0 +1,15 @@
# qdrant-stage stub — full config lands in M7.x.
# Host: stage. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
# Ephemeral, tiny corpus.
[[service]]
name = "qdrant-stage"
image = "qdrant/qdrant:v1.10.0"
port = 6333
[service.placement]
node = "stage"
[service.resources]
memory = "512Mi"
cpu = 0.25
+19
View File
@@ -0,0 +1,19 @@
# tenant-registry stub — full config lands in M4.1.
# Host: stage. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
# Calls PROD Keycloak per §2 "Calls OUT to prod"; audience is stage_client_id.
[[service]]
name = "tenant-registry"
image = "registry.yourplatform.com/tenant-registry:env-stage"
port = 8080
depends_on = ["pg-app-stage"]
[service.placement]
node = "stage"
[service.resources]
memory = "512Mi"
cpu = 0.25
[service.env]
KEYCLOAK_ISSUER = "https://auth.yourplatform.com/realms/breakpilot-prod"
+20
View File
@@ -0,0 +1,20 @@
# customer-portal stub — full config lands in M5.1.
# Host: vm-control. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "customer-portal"
image = "registry.yourplatform.com/portal:placeholder"
port = 3000
domain = "*.yourplatform.com"
depends_on = ["tenant-registry"]
[service.placement]
node = "vm-control"
[service.resources]
memory = "1Gi"
cpu = 1.0
[service.env]
KEYCLOAK_ISSUER = "https://auth.yourplatform.com/realms/breakpilot-prod"
TENANT_REGISTRY_URL = "http://tenant-registry:8080"
+25
View File
@@ -0,0 +1,25 @@
# erpnext stub — full config lands in M8.1.
# Host: vm-control. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "erpnext"
image = "frappe/erpnext:v15"
port = 8000
domain = "erp.yourplatform.com"
depends_on = ["mariadb", "redis-erpnext"]
[service.placement]
node = "vm-control"
[service.resources]
memory = "6Gi"
cpu = 2.0
[service.volume]
path = "/home/frappe/frappe-bench/sites"
[service.env]
DB_HOST = "mariadb"
REDIS_QUEUE = "redis://redis-erpnext:6379/0"
REDIS_CACHE = "redis://redis-erpnext:6379/1"
ADMIN_PASSWORD = "${secrets.ERPNEXT_ADMIN_PASSWORD}"
+15
View File
@@ -0,0 +1,15 @@
# frappe-hd stub — full config lands in M9.1.
# Host: vm-control. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "frappe-hd"
image = "frappe/helpdesk:v1"
port = 8001
depends_on = ["mariadb", "redis-erpnext"]
[service.placement]
node = "vm-control"
[service.resources]
memory = "1Gi"
cpu = 0.5
+20
View File
@@ -0,0 +1,20 @@
# mariadb stub — full config lands in M8.1.
# Host: vm-control. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "mariadb"
image = "mariadb:11"
port = 3306
[service.placement]
node = "vm-control"
[service.resources]
memory = "3Gi"
cpu = 1.0
[service.volume]
path = "/var/lib/mysql"
[service.env]
MARIADB_ROOT_PASSWORD = "${secrets.MARIADB_ROOT_PASSWORD}"
+14
View File
@@ -0,0 +1,14 @@
# redis-erpnext stub — full config lands in M8.1.
# Host: vm-control. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "redis-erpnext"
image = "redis:7-alpine"
port = 6379
[service.placement]
node = "vm-control"
[service.resources]
memory = "256Mi"
cpu = 0.25
+22
View File
@@ -0,0 +1,22 @@
# stalwart stub — full config lands in M3.2.
# Host: vm-control. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "stalwart"
image = "stalwartlabs/mail-server:latest"
port = 587
domain = "mail.yourplatform.com"
extra_ports = ["25:25", "465:465", "587:587", "993:993"]
[service.placement]
node = "vm-control"
[service.resources]
memory = "1Gi"
cpu = 0.5
[service.volume]
path = "/opt/stalwart-mail"
[service.env]
STALWART__SERVER__HOSTNAME = "mail.yourplatform.com"
+20
View File
@@ -0,0 +1,20 @@
# tenant-registry stub — full config lands in M4.1.
# Host: vm-control. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "tenant-registry"
image = "registry.yourplatform.com/tenant-registry:placeholder"
port = 8080
[service.placement]
node = "vm-control"
[service.resources]
memory = "512Mi"
cpu = 0.5
[service.env]
DATABASE_URL = "${secrets.TENANT_REGISTRY_DB_URL}"
KEYCLOAK_ISSUER = "https://auth.yourplatform.com/realms/breakpilot-prod"
KEYCLOAK_ADMIN_USER = "${secrets.KEYCLOAK_ADMIN_USER}"
KEYCLOAK_ADMIN_PASS = "${secrets.KEYCLOAK_ADMIN_PASS}"
+15
View File
@@ -0,0 +1,15 @@
# admin-compliance stub — full config lands in M7.x.
# Host: vm-data. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "admin-compliance"
image = "registry.yourplatform.com/admin-compliance:placeholder"
port = 3002
depends_on = ["backend-compliance", "ai-compliance-sdk"]
[service.placement]
node = "vm-data"
[service.resources]
memory = "512Mi"
cpu = 0.25
+15
View File
@@ -0,0 +1,15 @@
# ai-compliance-sdk stub — full config lands in M7.x.
# Host: vm-data. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "ai-compliance-sdk"
image = "registry.yourplatform.com/ai-compliance-sdk:placeholder"
port = 3001
depends_on = ["pg-app", "qdrant", "litellm"]
[service.placement]
node = "vm-data"
[service.resources]
memory = "1Gi"
cpu = 0.5
+15
View File
@@ -0,0 +1,15 @@
# backend-compliance stub — full config lands in M7.x.
# Host: vm-data. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "backend-compliance"
image = "registry.yourplatform.com/backend-compliance:placeholder"
port = 3000
depends_on = ["pg-app", "minio"]
[service.placement]
node = "vm-data"
[service.resources]
memory = "1Gi"
cpu = 0.5
+15
View File
@@ -0,0 +1,15 @@
# certifai-dashboard stub — full config lands in M6.x.
# Host: vm-data. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "certifai-dashboard"
image = "registry.yourplatform.com/certifai:placeholder"
port = 3000
depends_on = ["mongodb", "litellm"]
[service.placement]
node = "vm-data"
[service.resources]
memory = "1Gi"
cpu = 0.5
+18
View File
@@ -0,0 +1,18 @@
# litellm stub — full config lands in M6.x.
# Host: vm-data. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "litellm"
image = "ghcr.io/berriai/litellm:main-stable"
port = 4000
[service.placement]
node = "vm-data"
[service.resources]
memory = "1Gi"
cpu = 0.5
[service.env]
LITELLM_MASTER_KEY = "${secrets.LITELLM_MASTER_KEY}"
LITELLM_SALT_KEY = "${secrets.LITELLM_SALT_KEY}"
+23
View File
@@ -0,0 +1,23 @@
# minio stub — full config lands in M7.x.
# Host: vm-data. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "minio"
image = "minio/minio:latest"
port = 9000
extra_ports = ["9001:9001"]
cmd = ["server", "/data", "--console-address", ":9001"]
[service.placement]
node = "vm-data"
[service.resources]
memory = "1Gi"
cpu = 0.5
[service.volume]
path = "/data"
[service.env]
MINIO_ROOT_USER = "${secrets.MINIO_ROOT_USER}"
MINIO_ROOT_PASSWORD = "${secrets.MINIO_ROOT_PASSWORD}"
+21
View File
@@ -0,0 +1,21 @@
# mongodb stub — full config lands in M6.x.
# Host: vm-data. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "mongodb"
image = "mongo:7"
port = 27017
[service.placement]
node = "vm-data"
[service.resources]
memory = "2Gi"
cpu = 1.0
[service.volume]
path = "/data/db"
[service.env]
MONGO_INITDB_ROOT_USERNAME = "${secrets.MONGO_ADMIN_USER}"
MONGO_INITDB_ROOT_PASSWORD = "${secrets.MONGO_ADMIN_PASSWORD}"
+23
View File
@@ -0,0 +1,23 @@
# pg-app stub — full config lands in M4.1.
# Host: vm-data. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
# RISK-1 (§12): single instance owns tenant_registry + compliance schemas. Split into pg-registry + pg-compliance at Tier B.
[[service]]
name = "pg-app"
image = "postgres:16-alpine"
port = 5432
[service.placement]
node = "vm-data"
[service.resources]
memory = "3Gi"
cpu = 1.0
[service.volume]
path = "/var/lib/postgresql/data"
[service.env]
POSTGRES_DB = "platform"
POSTGRES_USER = "platform"
POSTGRES_PASSWORD = "${secrets.PG_APP_PASSWORD}"
+17
View File
@@ -0,0 +1,17 @@
# qdrant stub — full config lands in M7.x.
# Host: vm-data. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "qdrant"
image = "qdrant/qdrant:v1.10.0"
port = 6333
[service.placement]
node = "vm-data"
[service.resources]
memory = "1Gi"
cpu = 0.5
[service.volume]
path = "/qdrant/storage"
+25
View File
@@ -0,0 +1,25 @@
# gitea stub — full config lands in M3.x.
# Host: vm-edge. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "gitea"
image = "gitea/gitea:1.22"
port = 3000
domain = "git.yourplatform.com"
[service.placement]
node = "vm-edge"
[service.resources]
memory = "512Mi"
cpu = 0.5
[service.volume]
path = "/data"
[service.env]
USER_UID = "1000"
USER_GID = "1000"
GITEA__database__DB_TYPE = "sqlite3"
GITEA__database__PATH = "/data/gitea/gitea.db"
GITEA__server__ROOT_URL = "https://git.yourplatform.com"
+22
View File
@@ -0,0 +1,22 @@
# infisical stub — full config lands in M3.1.
# Host: vm-edge. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "infisical"
image = "infisical/infisical:latest"
port = 8080
depends_on = ["pg-infisical", "redis-infisical"]
[service.placement]
node = "vm-edge"
[service.resources]
memory = "512Mi"
cpu = 0.5
[service.env]
DB_CONNECTION_URI = "${secrets.INFISICAL_DB_URI}"
REDIS_URL = "redis://redis-infisical:6379"
ENCRYPTION_KEY = "${secrets.INFISICAL_ENCRYPTION_KEY}"
AUTH_SECRET = "${secrets.INFISICAL_AUTH_SECRET}"
SITE_URL = "https://infisical.yourplatform.com"
+25
View File
@@ -0,0 +1,25 @@
# keycloak stub — full config lands in M2.1.
# Host: vm-edge. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
# Bootstrap exception per §8 rule 3: KC_DB_URL lives in Orca env, not Infisical (Infisical runs on same VM).
[[service]]
name = "keycloak"
image = "quay.io/keycloak/keycloak:26.0"
port = 8443
domain = "auth.yourplatform.com"
depends_on = ["pg-keycloak"]
[service.placement]
node = "vm-edge"
[service.resources]
memory = "2Gi"
cpu = 1.0
[service.env]
KC_DB = "postgres"
KC_DB_URL = "${secrets.KC_DB_URL}"
KC_HOSTNAME = "auth.yourplatform.com"
KC_PROXY_HEADERS = "xforwarded"
KC_HEALTH_ENABLED = "true"
JAVA_OPTS_APPEND = "-Xms1g -Xmx1500m"
+15
View File
@@ -0,0 +1,15 @@
# orca-proxy stub — full config lands in M1.2/M0.3.
# Host: vm-edge. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
# Wildcard TLS terminator; routing rules land with M0.3.
[[service]]
name = "orca-proxy"
image = "orca-managed/orca-proxy:placeholder"
port = 443
[service.placement]
node = "vm-edge"
[service.resources]
memory = "256Mi"
cpu = 0.5
+22
View File
@@ -0,0 +1,22 @@
# pg-infisical stub — full config lands in M3.1.
# Host: vm-edge. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "pg-infisical"
image = "postgres:16-alpine"
port = 5432
[service.placement]
node = "vm-edge"
[service.resources]
memory = "256Mi"
cpu = 0.25
[service.volume]
path = "/var/lib/postgresql/data"
[service.env]
POSTGRES_DB = "infisical"
POSTGRES_USER = "infisical"
POSTGRES_PASSWORD = "${secrets.PG_INFISICAL_PASSWORD}"
+22
View File
@@ -0,0 +1,22 @@
# pg-keycloak stub — full config lands in M2.1.
# Host: vm-edge. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "pg-keycloak"
image = "postgres:16-alpine"
port = 5432
[service.placement]
node = "vm-edge"
[service.resources]
memory = "512Mi"
cpu = 0.5
[service.volume]
path = "/var/lib/postgresql/data"
[service.env]
POSTGRES_DB = "keycloak"
POSTGRES_USER = "keycloak"
POSTGRES_PASSWORD = "${secrets.PG_KEYCLOAK_PASSWORD}"
+15
View File
@@ -0,0 +1,15 @@
# powerdns-auth stub — full config lands in M0.3.
# Host: vm-edge. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
[[service]]
name = "powerdns-auth"
image = "powerdns/pdns-auth:4.9"
port = 53
extra_ports = ["53:53/udp", "53:53/tcp"]
[service.placement]
node = "vm-edge"
[service.resources]
memory = "256Mi"
cpu = 0.25
+15
View File
@@ -0,0 +1,15 @@
# redis-infisical stub — full config lands in M3.1.
# Host: vm-edge. Resource budget per INFRASTRUCTURE.md §6 co-tenant notes.
# Ephemeral cache; no volume.
[[service]]
name = "redis-infisical"
image = "redis:7-alpine"
port = 6379
[service.placement]
node = "vm-edge"
[service.resources]
memory = "128Mi"
cpu = 0.1
+9
View File
@@ -0,0 +1,9 @@
# Overlays
Per-env *sparse* deltas applied on top of `manifests/`. Concept: each overlay
file may set just the fields that differ from the base manifest. The merge
script in `scripts/plan.sh` produces the final per-env service set at
`.orca-out/<env>/`.
For now the overlays are placeholder structures — concrete deltas land with
the milestones that introduce real images and replica counts (M4.1, M5.1, M6.x).
+11
View File
@@ -0,0 +1,11 @@
# Dev overlay — placeholder.
#
# Dev runs everything in docker-compose on the developer's laptop, not via
# Orca. This overlay exists so `make plan ENV=dev` is symmetric with stage/
# prod, but it does not yet point at real images.
#
# Real dev wiring lives in the per-service repos' `make dev` target.
[env]
name = "dev"
api_url = "" # no orca controller; apply is a no-op
+15
View File
@@ -0,0 +1,15 @@
# Prod overlay.
#
# Selects manifests under vm-edge / vm-control / vm-data. Stage manifests
# (manifests/stage/) are excluded from prod apply.
[env]
name = "prod"
api_url = "${ORCA_PROD_API_URL}"
[deploy]
include_dirs = ["manifests/vm-edge", "manifests/vm-control", "manifests/vm-data"]
[image]
# Default tag for prod; release.yaml retags `env-stage` → `v$VERSION` + `env-prod`.
default_tag = "env-prod"
+16
View File
@@ -0,0 +1,16 @@
# Stage overlay.
#
# Stage maps to the single 'stage' VM, app plane only. Selects only the
# services under manifests/stage/.
[env]
name = "stage"
api_url = "${ORCA_STAGE_API_URL}"
# Service filter: only deploy manifests under this directory.
[deploy]
include_dirs = ["manifests/stage"]
[image]
# Default image tag for stage builds. Per-service overrides may land later.
default_tag = "env-stage"
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# `make apply ENV=<env>` — push the resolved manifest set to an Orca controller.
#
# Refuses to run unless ORCA_API_URL is set (or read from overlays/<env>).
# In M1.1 this is a guard; the real call lands once vm-edge has an Orca
# controller (M1.2).
set -euo pipefail
ENV="${ENV:?usage: make apply ENV=<dev|stage|prod>}"
case "$ENV" in dev|stage|prod) ;; *) echo "unknown env: $ENV" >&2; exit 2;; esac
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
OUT="$ROOT/.orca-out/$ENV"
if [ ! -d "$OUT" ]; then
echo "no resolved manifests at $OUT — run \`make plan ENV=$ENV\` first" >&2
exit 1
fi
if [ -z "${ORCA_API_URL:-}" ]; then
echo "ORCA_API_URL not set." >&2
echo "M1.2 will provision the controller; until then \`make apply\` is a no-op." >&2
echo "Want to dry-run? Use \`make plan ENV=$ENV\` and inspect .orca-out/$ENV/." >&2
exit 0 # exit 0 — no-op is the expected M1.1 behaviour
fi
# Real apply once a controller exists. orca CLI deploys a directory of TOMLs.
echo "=== apply ENV=$ENV against $ORCA_API_URL ==="
orca --api "$ORCA_API_URL" deploy --file "$OUT"
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# `make plan ENV=<env>` — show what would be deployed for the given env.
#
# Merges manifests/ with overlays/<env>/overlay.toml and writes the resolved
# service set to .orca-out/<env>/. Does NOT contact a cluster.
#
# Behavior in M1.1: the merge is a passthrough (overlays are placeholders).
# A real merge that resolves per-env image tags and replica counts will land
# alongside the first env-specific delta (M1.2 or later).
set -euo pipefail
ENV="${ENV:?usage: make plan ENV=<dev|stage|prod>}"
case "$ENV" in dev|stage|prod) ;; *) echo "unknown env: $ENV" >&2; exit 2;; esac
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
OUT="$ROOT/.orca-out/$ENV"
rm -rf "$OUT"
mkdir -p "$OUT"
OVERLAY="overlays/$ENV/overlay.toml"
[ -f "$OVERLAY" ] || { echo "missing $OVERLAY" >&2; exit 1; }
# Read include_dirs from overlay; fall back to all VMs (dev default).
INCLUDE_DIRS=$(python3 - "$OVERLAY" <<'PY'
import sys, tomllib
data = tomllib.load(open(sys.argv[1], 'rb'))
dirs = data.get('deploy', {}).get('include_dirs')
if dirs is None:
dirs = ['manifests/vm-edge', 'manifests/vm-control', 'manifests/vm-data', 'manifests/stage']
print('\n'.join(dirs))
PY
)
echo "=== plan ENV=$ENV ==="
echo "overlay: $OVERLAY"
echo "include_dirs:"
echo "$INCLUDE_DIRS" | sed 's/^/ /'
echo
count=0
while IFS= read -r dir; do
[ -d "$dir" ] || continue
for tml in "$dir"/*.toml; do
[ -e "$tml" ] || continue
rel="${tml#manifests/}"
dest="$OUT/$rel"
mkdir -p "$(dirname "$dest")"
cp "$tml" "$dest"
count=$((count+1))
done
done <<< "$INCLUDE_DIRS"
echo "→ wrote $count resolved manifests to .orca-out/$ENV/"
echo "→ apply with: ORCA_API_URL=<url> make apply ENV=$ENV"
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Stub — M1.3 fills this in.
# Per INFRASTRUCTURE.md §10 Scenario F, a quarterly cold-restore drill is
# required: pull latest pg_dump from S3, restore into a scratch Postgres,
# verify row counts, post result to oncall.
#
# Concrete steps land with M1.3 (Backups, monitoring, on-call).
echo "restore drill not implemented yet — see M1.3" >&2
exit 1
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# TOML syntax + structural sanity for every manifest in this repo.
# Used by `make validate` and by .gitea/workflows/ci.yaml.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
python3 - "$ROOT" <<'PY'
import sys, tomllib, pathlib
root = pathlib.Path(sys.argv[1])
errs = []
count = 0
for p in sorted(root.glob('manifests/**/*.toml')):
count += 1
try:
data = tomllib.load(open(p, 'rb'))
except Exception as e:
errs.append(f'{p}: TOML parse: {e}')
continue
svcs = data.get('service')
if not svcs:
errs.append(f'{p}: no [[service]] block')
continue
for svc in svcs:
for required in ('name', 'image'):
if required not in svc:
errs.append(f'{p}: service missing required field "{required}"')
# forbidden nesting bugs
for sub in ('placement', 'resources', 'env', 'volume'):
if isinstance(svc.get(sub), dict):
for fb in ('depends_on', 'extra_ports', 'cmd', 'mounts'):
if fb in svc[sub]:
errs.append(f'{p}: "{fb}" nested under [service.{sub}] — must be at [[service]] level')
# placement.node must match parent vm directory
node = (svc.get('placement') or {}).get('node')
vm_dir = p.parent.name
if node and node != vm_dir:
errs.append(f'{p}: placement.node "{node}" mismatches dir "{vm_dir}"')
if not node:
errs.append(f'{p}: missing placement.node')
mem = (svc.get('resources') or {}).get('memory')
if not mem:
errs.append(f'{p}: missing resources.memory (mandatory per §8 rule 5)')
# Validate overlays parse too
for p in sorted(root.glob('overlays/*/overlay.toml')):
count += 1
try:
tomllib.load(open(p, 'rb'))
except Exception as e:
errs.append(f'{p}: TOML parse: {e}')
# Validate VMs parse too
for p in sorted(root.glob('vms/*.toml')):
count += 1
try:
tomllib.load(open(p, 'rb'))
except Exception as e:
errs.append(f'{p}: TOML parse: {e}')
print(f'checked {count} files')
for e in errs:
print(' ', e)
sys.exit(1 if errs else 0)
PY
+6
View File
@@ -0,0 +1,6 @@
# VM specs
Source of truth for the four billable VMs in `INFRASTRUCTURE.md §1`. These
files are consumed by the M1.2 provisioning step (Terraform/OpenStack against
SysEleven DUS2). Orca itself ignores this directory — it deals with services,
not infrastructure.
+20
View File
@@ -0,0 +1,20 @@
# stage — single-VM stage environment. App plane only, ephemeral DBs.
# Calls OUT to prod Keycloak + prod Stalwart per §2 isolation rules.
[vm]
name = "stage"
env = "stage"
flavor = "m2.small" # 2 vCPU, 8 GB RAM
public_ip = true # tester access without VPN
region = "DUS2"
private_network = "platform-stage"
private_ip_cidr = "10.1.1.0/24"
[vm.disk]
block_volume_gb = 50 # pg-stage + mongo-stage + qdrant-stage; reset per release
[vm.firewall]
ingress_public = [
{ proto = "tcp", ports = [80, 443], source = "0.0.0.0/0", purpose = "stage portal" },
]
ingress_private = [] # stage does not accept private traffic from prod
+20
View File
@@ -0,0 +1,20 @@
# vm-control — Control plane (portal, tenant-registry, ERPNext, Stalwart).
# See INFRASTRUCTURE.md §1, §2, §6.
[vm]
name = "vm-control"
env = "prod"
flavor = "m2.medium" # 4 vCPU, 16 GB RAM
public_ip = false # only reachable via vm-edge orca-proxy
region = "DUS2"
private_network = "platform-prod"
private_ip_cidr = "10.0.2.0/24"
[vm.disk]
block_volume_gb = 250 # MariaDB (ERPNext) + Stalwart mail spool, medium growth
[vm.firewall]
ingress_public = [] # no public ingress
ingress_private = [
{ proto = "tcp", ports = "all", source = "10.0.0.0/16", purpose = "intra-platform" },
]
+20
View File
@@ -0,0 +1,20 @@
# vm-data — Data plane (CERTifAI, compliance, pg-app, MongoDB, MinIO).
# Scale driver — bump flavor at Tier B/C per §13.
[vm]
name = "vm-data"
env = "prod"
flavor = "m2.medium" # 4 vCPU, 16 GB RAM (Tier B bump: m2.large)
public_ip = false
region = "DUS2"
private_network = "platform-prod"
private_ip_cidr = "10.0.3.0/24"
[vm.disk]
block_volume_gb = 500 # MongoDB + pg-app + Qdrant + MinIO, fast growth (scales with N customers)
[vm.firewall]
ingress_public = []
ingress_private = [
{ proto = "tcp", ports = "all", source = "10.0.0.0/16", purpose = "intra-platform" },
]
+26
View File
@@ -0,0 +1,26 @@
# vm-edge — Identity + Infra plane, public IP, root auth dependency.
# See INFRASTRUCTURE.md §1, §2.
[vm]
name = "vm-edge"
env = "prod"
flavor = "m2.small" # 2 vCPU, 8 GB RAM
public_ip = true
region = "DUS2"
private_network = "platform-prod"
private_ip_cidr = "10.0.1.0/24"
[vm.disk]
block_volume_gb = 50 # pg-keycloak + pg-infisical + Gitea repos (slow growth)
[vm.firewall]
# vm-edge is the only host accepting public traffic. Everything else is
# behind the private network.
ingress_public = [
{ proto = "tcp", ports = [80, 443], source = "0.0.0.0/0", purpose = "orca-proxy HTTP/HTTPS" },
{ proto = "tcp", ports = [53], source = "0.0.0.0/0", purpose = "PowerDNS (TCP)" },
{ proto = "udp", ports = [53], source = "0.0.0.0/0", purpose = "PowerDNS (UDP)" },
]
ingress_private = [
{ proto = "tcp", ports = "all", source = "10.0.0.0/16", purpose = "intra-platform" },
]