diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 52bd7d6..7989554 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -1,5 +1,6 @@ -# CI for orca-platform (IaC). `shared` always runs; `validate` activates -# when at least one Orca manifest lands. +# CI for orca-platform (IaC). +# `shared` always runs (commitlint + gitleaks + trivy fs). +# `validate` always runs (parses every manifest + overlay + vm spec). name: ci on: @@ -53,18 +54,18 @@ jobs: TRIVY_VERSION=0.70.0 curl -fsSL "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" \ | 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: runs-on: docker - if: hashFiles('**/*.orca.yaml','**/*.orca.yml','manifests/**') != '' steps: - uses: actions/checkout@v4 - - name: install orca + - name: setup python + shell: bash run: | - curl -fsSL https://orca.meghsakha.com/install.sh | sh - orca version + which python3 + python3 --version - - name: orca validate - run: orca validate ./ + - name: make validate + run: make validate diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index 80c7348..58a629a 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -11,7 +11,7 @@ jobs: runs-on: docker environment: name: production # Gitea Environments — requires sign-off per branch protection - url: https://yourplatform.com + url: https://breakpilot.com steps: - uses: actions/checkout@v4 with: { fetch-depth: 0 } @@ -22,7 +22,7 @@ jobs: - name: verify stage soak (>= 24h on this image) run: | - IMG=registry.yourplatform.com/${{ github.event.repository.name }}:env-stage + IMG=registry.breakpilot.com/${{ github.event.repository.name }}:env-stage SOAK_SECONDS=$(orca image-age --env=stage --image $IMG) if [ "$SOAK_SECONDS" -lt 86400 ]; then echo "Stage soak only $SOAK_SECONDS s, < 24h. Aborting." @@ -34,12 +34,12 @@ jobs: - name: re-tag image as semver + env-prod uses: docker/login-action@v3 with: - registry: registry.yourplatform.com + registry: registry.breakpilot.com username: ${{ secrets.REGISTRY_USER }} password: ${{ secrets.REGISTRY_PASS }} - run: | - IMG=registry.yourplatform.com/${{ github.event.repository.name }} + IMG=registry.breakpilot.com/${{ github.event.repository.name }} docker pull $IMG:env-stage docker tag $IMG:env-stage $IMG:v${{ steps.v.outputs.version }} docker tag $IMG:env-stage $IMG:env-prod diff --git a/.gitignore b/.gitignore index 376d6c0..70d32e5 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ vendor/ # Rust **/target/ +.orca-out/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7804e26..1db7ac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Generated section is appended on release tag via `git-cliff` (see `.gitea/workfl ## [Unreleased] ### Added +- feat(iac): scaffold orca-platform — manifests/, overlays/, dns/, scripts/, Makefile (M1.1) - ### Changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d54d1a5..3ad8852 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,4 +86,4 @@ When reviewing, check in this order: ## Questions -`#engineering` channel · `oncall@yourplatform.com` · or open a PR with a `[WIP]` prefix and ask in the description. +`#engineering` channel · `oncall@breakpilot.com` · or open a PR with a `[WIP]` prefix and ask in the description. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..81b6c33 --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +# orca-platform — IaC for the Breakpilot Platform. +# +# make validate parse + structural sanity check every manifest +# make plan ENV= resolve manifests + overlay into .orca-out// +# make apply ENV= (M1.2+) push resolved set to Orca controller +# make diff 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= resolve manifests for env (dev/stage/prod)" + @echo " make apply ENV= push resolved set (no-op until M1.2)" + @echo " make diff 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/" diff --git a/README.md b/README.md index 5ecfcef..2387f84 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,105 @@ # 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): > [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) · -> [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) ## 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 **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) +## 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 +├── dns/ +│ └── breakpilot.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=` → .orca-out// +│ ├── apply.sh # `make apply ENV=` (no-op until M1.2) +│ └── restore-drill.sh.template M1.3 placeholder +└── Makefile # validate / plan / apply / diff / clean +``` + ## Run locally ```bash -# prerequisites: see CONTRIBUTING.md for tooling once code lands -make dev # starts dependencies + this service on http://localhost:3000 -make test # unit + integration -make e2e # only if this repo ships user-facing flows +make validate # check all manifests parse + have required fields +make plan ENV=stage # resolve manifests for stage → .orca-out/stage/ +make plan ENV=prod # same for prod +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 (Terraform/OpenStack in a separate repo); 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 -{{For services: list the top-level routes or commands. -For libraries: list the public API entry points. -For IaC: list the make targets.}} +| Target | What it does | +|---|---| +| `make validate` | Parse + structural check (no cluster contact) | +| `make plan ENV=` | Resolve manifests + overlay → `.orca-out//` | +| `make apply ENV=` | Push to Orca controller at `$ORCA_API_URL` (M1.2 brings this online) | +| `make diff ENV=` | Alias for `plan` | +| `make clean` | Remove `.orca-out/` | ## Deployment -| Env | URL | How | +| Env | Apply path | Trigger | |---|---|---| -| dev | `http://localhost:3000` | `make dev` | -| stage | `https://orca-platform.stage.yourplatform.com` | auto on merge to `main` | -| prod | `https://orca-platform.yourplatform.com` | manual: tag `vX.Y.Z` + sign-off | +| dev | `docker-compose` in each product repo | dev's machine | +| stage | `make apply ENV=stage` against the stage Orca controller | CI on merge to main + image build | +| 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 -- Traces, logs, metrics: [SigNoz](https://signoz.meghsakha.com) — service name `orca-platform` -- Audit events: Tenant Registry `/audit` (Retraced-shape schema) -- On-call: `oncall@yourplatform.com` · runbook at `platform/docs/runbooks/orca-platform.md` +- Traces, logs, metrics: [SigNoz](https://signoz.meghsakha.com) — service name per individual container +- On-call: `oncall@breakpilot.com` · runbooks at `platform/docs/runbooks/` ## 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 diff --git a/cluster.toml.tmpl b/cluster.toml.tmpl new file mode 100644 index 0000000..420f73a --- /dev/null +++ b/cluster.toml.tmpl @@ -0,0 +1,29 @@ +# Cluster-level config rendered per env. +# Real values get substituted by `make plan ENV=` from environment + +# overlays//overlay.toml. +# +# Schema mirrors ~/workspace/orca-infra/cluster.toml (Orca-native). + +[cluster] +name = "breakpilot-${ENV}" +domain = "${DOMAIN}" +acme_email = "oncall@breakpilot.com" + +[ai] +provider = "litellm" +endpoint = "https://llm.breakpilot.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}" diff --git a/dns/yourplatform.com.zone.template b/dns/yourplatform.com.zone.template new file mode 100644 index 0000000..f67b1bd --- /dev/null +++ b/dns/yourplatform.com.zone.template @@ -0,0 +1,20 @@ +; PowerDNS authoritative zone for breakpilot.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 breakpilot.com. +$TTL 60 + +@ IN SOA ns1.breakpilot.com. oncall.breakpilot.com. ( + ; serial — bumped by CI on every commit + 2026051800 + 3600 ; refresh + 600 ; retry + 604800 ; expire + 60 ; minimum TTL +) + +@ IN NS ns1.breakpilot.com. +@ IN NS ns2.breakpilot.com. + +; A records, wildcards, mail/spf/dkim/dmarc land in M0.3 diff --git a/manifests/README.md b/manifests/README.md new file mode 100644 index 0000000..9888bae --- /dev/null +++ b/manifests/README.md @@ -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 `/.toml` following the shape of an existing stub. +3. Set `placement.node = ""`, `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=` merges the base manifest with the matching overlay in `overlays//` and prints the resulting service definitions. It is a no-op until matching overlays exist for the env. + +`make apply ENV=` is gated on a real Orca controller URL — refuses to run until `ORCA_API_URL` is set (lands in M1.2). diff --git a/manifests/stage/admin-compliance.toml b/manifests/stage/admin-compliance.toml new file mode 100644 index 0000000..d7c2ea8 --- /dev/null +++ b/manifests/stage/admin-compliance.toml @@ -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.breakpilot.com/admin-compliance:env-stage" +port = 3002 + +[service.placement] +node = "stage" + +[service.resources] +memory = "256Mi" +cpu = 0.25 diff --git a/manifests/stage/ai-compliance-sdk.toml b/manifests/stage/ai-compliance-sdk.toml new file mode 100644 index 0000000..4a9b721 --- /dev/null +++ b/manifests/stage/ai-compliance-sdk.toml @@ -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.breakpilot.com/ai-compliance-sdk:env-stage" +port = 3001 + +[service.placement] +node = "stage" + +[service.resources] +memory = "512Mi" +cpu = 0.25 diff --git a/manifests/stage/backend-compliance.toml b/manifests/stage/backend-compliance.toml new file mode 100644 index 0000000..011a3dc --- /dev/null +++ b/manifests/stage/backend-compliance.toml @@ -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.breakpilot.com/backend-compliance:env-stage" +port = 3000 + +[service.placement] +node = "stage" + +[service.resources] +memory = "512Mi" +cpu = 0.25 diff --git a/manifests/stage/certifai-dashboard.toml b/manifests/stage/certifai-dashboard.toml new file mode 100644 index 0000000..087fc64 --- /dev/null +++ b/manifests/stage/certifai-dashboard.toml @@ -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.breakpilot.com/certifai:env-stage" +port = 3000 + +[service.placement] +node = "stage" + +[service.resources] +memory = "1Gi" +cpu = 0.5 diff --git a/manifests/stage/customer-portal.toml b/manifests/stage/customer-portal.toml new file mode 100644 index 0000000..7a50c31 --- /dev/null +++ b/manifests/stage/customer-portal.toml @@ -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.breakpilot.com/portal:env-stage" +port = 3000 +domain = "*.stage.breakpilot.com" + +[service.placement] +node = "stage" + +[service.resources] +memory = "1Gi" +cpu = 0.5 diff --git a/manifests/stage/litellm.toml b/manifests/stage/litellm.toml new file mode 100644 index 0000000..8f8be4b --- /dev/null +++ b/manifests/stage/litellm.toml @@ -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 diff --git a/manifests/stage/mongodb-stage.toml b/manifests/stage/mongodb-stage.toml new file mode 100644 index 0000000..6088716 --- /dev/null +++ b/manifests/stage/mongodb-stage.toml @@ -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 diff --git a/manifests/stage/orca-proxy.toml b/manifests/stage/orca-proxy.toml new file mode 100644 index 0000000..6442b7d --- /dev/null +++ b/manifests/stage/orca-proxy.toml @@ -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 diff --git a/manifests/stage/pg-app-stage.toml b/manifests/stage/pg-app-stage.toml new file mode 100644 index 0000000..cbd2159 --- /dev/null +++ b/manifests/stage/pg-app-stage.toml @@ -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 diff --git a/manifests/stage/qdrant-stage.toml b/manifests/stage/qdrant-stage.toml new file mode 100644 index 0000000..b94d268 --- /dev/null +++ b/manifests/stage/qdrant-stage.toml @@ -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 diff --git a/manifests/stage/tenant-registry.toml b/manifests/stage/tenant-registry.toml new file mode 100644 index 0000000..a521b3d --- /dev/null +++ b/manifests/stage/tenant-registry.toml @@ -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.breakpilot.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.breakpilot.com/realms/breakpilot-prod" diff --git a/manifests/vm-control/customer-portal.toml b/manifests/vm-control/customer-portal.toml new file mode 100644 index 0000000..cd86700 --- /dev/null +++ b/manifests/vm-control/customer-portal.toml @@ -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.breakpilot.com/portal:placeholder" +port = 3000 +domain = "*.breakpilot.com" +depends_on = ["tenant-registry"] + +[service.placement] +node = "vm-control" + +[service.resources] +memory = "1Gi" +cpu = 1.0 + +[service.env] +KEYCLOAK_ISSUER = "https://auth.breakpilot.com/realms/breakpilot-prod" +TENANT_REGISTRY_URL = "http://tenant-registry:8080" diff --git a/manifests/vm-control/erpnext.toml b/manifests/vm-control/erpnext.toml new file mode 100644 index 0000000..505151e --- /dev/null +++ b/manifests/vm-control/erpnext.toml @@ -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.breakpilot.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}" diff --git a/manifests/vm-control/frappe-hd.toml b/manifests/vm-control/frappe-hd.toml new file mode 100644 index 0000000..f11c9ae --- /dev/null +++ b/manifests/vm-control/frappe-hd.toml @@ -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 diff --git a/manifests/vm-control/mariadb.toml b/manifests/vm-control/mariadb.toml new file mode 100644 index 0000000..321d128 --- /dev/null +++ b/manifests/vm-control/mariadb.toml @@ -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}" diff --git a/manifests/vm-control/redis-erpnext.toml b/manifests/vm-control/redis-erpnext.toml new file mode 100644 index 0000000..057f478 --- /dev/null +++ b/manifests/vm-control/redis-erpnext.toml @@ -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 diff --git a/manifests/vm-control/stalwart.toml b/manifests/vm-control/stalwart.toml new file mode 100644 index 0000000..fdd060d --- /dev/null +++ b/manifests/vm-control/stalwart.toml @@ -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.breakpilot.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.breakpilot.com" diff --git a/manifests/vm-control/tenant-registry.toml b/manifests/vm-control/tenant-registry.toml new file mode 100644 index 0000000..edb55f2 --- /dev/null +++ b/manifests/vm-control/tenant-registry.toml @@ -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.breakpilot.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.breakpilot.com/realms/breakpilot-prod" +KEYCLOAK_ADMIN_USER = "${secrets.KEYCLOAK_ADMIN_USER}" +KEYCLOAK_ADMIN_PASS = "${secrets.KEYCLOAK_ADMIN_PASS}" diff --git a/manifests/vm-data/admin-compliance.toml b/manifests/vm-data/admin-compliance.toml new file mode 100644 index 0000000..1306f60 --- /dev/null +++ b/manifests/vm-data/admin-compliance.toml @@ -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.breakpilot.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 diff --git a/manifests/vm-data/ai-compliance-sdk.toml b/manifests/vm-data/ai-compliance-sdk.toml new file mode 100644 index 0000000..fd4047c --- /dev/null +++ b/manifests/vm-data/ai-compliance-sdk.toml @@ -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.breakpilot.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 diff --git a/manifests/vm-data/backend-compliance.toml b/manifests/vm-data/backend-compliance.toml new file mode 100644 index 0000000..b332e38 --- /dev/null +++ b/manifests/vm-data/backend-compliance.toml @@ -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.breakpilot.com/backend-compliance:placeholder" +port = 3000 +depends_on = ["pg-app", "minio"] + +[service.placement] +node = "vm-data" + +[service.resources] +memory = "1Gi" +cpu = 0.5 diff --git a/manifests/vm-data/certifai-dashboard.toml b/manifests/vm-data/certifai-dashboard.toml new file mode 100644 index 0000000..635c2a4 --- /dev/null +++ b/manifests/vm-data/certifai-dashboard.toml @@ -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.breakpilot.com/certifai:placeholder" +port = 3000 +depends_on = ["mongodb", "litellm"] + +[service.placement] +node = "vm-data" + +[service.resources] +memory = "1Gi" +cpu = 0.5 diff --git a/manifests/vm-data/litellm.toml b/manifests/vm-data/litellm.toml new file mode 100644 index 0000000..43ff1a8 --- /dev/null +++ b/manifests/vm-data/litellm.toml @@ -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}" diff --git a/manifests/vm-data/minio.toml b/manifests/vm-data/minio.toml new file mode 100644 index 0000000..4d37535 --- /dev/null +++ b/manifests/vm-data/minio.toml @@ -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}" diff --git a/manifests/vm-data/mongodb.toml b/manifests/vm-data/mongodb.toml new file mode 100644 index 0000000..49ffa23 --- /dev/null +++ b/manifests/vm-data/mongodb.toml @@ -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}" diff --git a/manifests/vm-data/pg-app.toml b/manifests/vm-data/pg-app.toml new file mode 100644 index 0000000..2c5a564 --- /dev/null +++ b/manifests/vm-data/pg-app.toml @@ -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}" diff --git a/manifests/vm-data/qdrant.toml b/manifests/vm-data/qdrant.toml new file mode 100644 index 0000000..e72d8cd --- /dev/null +++ b/manifests/vm-data/qdrant.toml @@ -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" diff --git a/manifests/vm-edge/gitea.toml b/manifests/vm-edge/gitea.toml new file mode 100644 index 0000000..a0a3106 --- /dev/null +++ b/manifests/vm-edge/gitea.toml @@ -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.breakpilot.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.breakpilot.com" diff --git a/manifests/vm-edge/infisical.toml b/manifests/vm-edge/infisical.toml new file mode 100644 index 0000000..d5d5db6 --- /dev/null +++ b/manifests/vm-edge/infisical.toml @@ -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.breakpilot.com" diff --git a/manifests/vm-edge/keycloak.toml b/manifests/vm-edge/keycloak.toml new file mode 100644 index 0000000..04816c3 --- /dev/null +++ b/manifests/vm-edge/keycloak.toml @@ -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.breakpilot.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.breakpilot.com" +KC_PROXY_HEADERS = "xforwarded" +KC_HEALTH_ENABLED = "true" +JAVA_OPTS_APPEND = "-Xms1g -Xmx1500m" diff --git a/manifests/vm-edge/orca-proxy.toml b/manifests/vm-edge/orca-proxy.toml new file mode 100644 index 0000000..3b2263f --- /dev/null +++ b/manifests/vm-edge/orca-proxy.toml @@ -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 diff --git a/manifests/vm-edge/pg-infisical.toml b/manifests/vm-edge/pg-infisical.toml new file mode 100644 index 0000000..2239a3e --- /dev/null +++ b/manifests/vm-edge/pg-infisical.toml @@ -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}" diff --git a/manifests/vm-edge/pg-keycloak.toml b/manifests/vm-edge/pg-keycloak.toml new file mode 100644 index 0000000..c02b732 --- /dev/null +++ b/manifests/vm-edge/pg-keycloak.toml @@ -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}" diff --git a/manifests/vm-edge/powerdns-auth.toml b/manifests/vm-edge/powerdns-auth.toml new file mode 100644 index 0000000..a17c4e6 --- /dev/null +++ b/manifests/vm-edge/powerdns-auth.toml @@ -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 diff --git a/manifests/vm-edge/redis-infisical.toml b/manifests/vm-edge/redis-infisical.toml new file mode 100644 index 0000000..82e25a7 --- /dev/null +++ b/manifests/vm-edge/redis-infisical.toml @@ -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 diff --git a/overlays/README.md b/overlays/README.md new file mode 100644 index 0000000..b3599ff --- /dev/null +++ b/overlays/README.md @@ -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//`. + +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). diff --git a/overlays/dev/overlay.toml b/overlays/dev/overlay.toml new file mode 100644 index 0000000..afd32d4 --- /dev/null +++ b/overlays/dev/overlay.toml @@ -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 diff --git a/overlays/prod/overlay.toml b/overlays/prod/overlay.toml new file mode 100644 index 0000000..95c31f3 --- /dev/null +++ b/overlays/prod/overlay.toml @@ -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" diff --git a/overlays/stage/overlay.toml b/overlays/stage/overlay.toml new file mode 100644 index 0000000..0409857 --- /dev/null +++ b/overlays/stage/overlay.toml @@ -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" diff --git a/scripts/apply.sh b/scripts/apply.sh new file mode 100755 index 0000000..12b3463 --- /dev/null +++ b/scripts/apply.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# `make apply ENV=` — push the resolved manifest set to an Orca controller. +# +# Refuses to run unless ORCA_API_URL is set (or read from overlays/). +# 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=}" +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" diff --git a/scripts/plan.sh b/scripts/plan.sh new file mode 100755 index 0000000..185a2ff --- /dev/null +++ b/scripts/plan.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# `make plan ENV=` — show what would be deployed for the given env. +# +# Merges manifests/ with overlays//overlay.toml and writes the resolved +# service set to .orca-out//. 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=}" +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= make apply ENV=$ENV" diff --git a/scripts/restore-drill.sh.template b/scripts/restore-drill.sh.template new file mode 100644 index 0000000..cbbfada --- /dev/null +++ b/scripts/restore-drill.sh.template @@ -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 diff --git a/scripts/validate.sh b/scripts/validate.sh new file mode 100755 index 0000000..7f06eae --- /dev/null +++ b/scripts/validate.sh @@ -0,0 +1,56 @@ +#!/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}') +print(f'checked {count} files') +for e in errs: + print(' ', e) +sys.exit(1 if errs else 0) +PY