From 2a807d7671b53626fef4d5df7f73de298edbd11f Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar Date: Mon, 18 May 2026 22:37:35 +0200 Subject: [PATCH] feat(dev): local docker-compose stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds dev/docker-compose.yml + dev/keycloak/realm-export.json + dev/README.md and Makefile targets so a developer can: make dev-up and get Keycloak 26 on :8080 with the breakpilot-dev realm pre-imported, plus pg-app (:5432), Redis (:6379), Mongo (:27017), and MinIO (:9000 + :9001). Seed users: test@breakpilot.dev / test — IT_ADMIN of tenant 'acme' admin@breakpilot.dev / admin — BREAKPILOT_ADMIN (platform staff) Realm includes a dev-portal public PKCE client (redirect URIs cover http://localhost:3000/* and http://*.localhost:3000/* so subdomain routing works in dev) and a dev-tenant-registry bearer-only client. Protocol mappers project tenant_id, tenant_slug, org_roles, products, plan, and tenant_status into every issued JWT — the contract portal + tenant-registry expect in prod, fronted by Keycloak attributes today. dev/ lives in orca-platform because this repo already documents the production topology that this compose mirrors. INFRASTRUCTURE.md §1 sets dev as 'docker-compose on developer laptops' — this is that compose. Refs: M0.1+ (precondition for local-dev work on tenant-registry / portal) --- CHANGELOG.md | 1 + Makefile | 30 ++++- README.md | 14 +++ dev/README.md | 62 ++++++++++ dev/docker-compose.yml | 139 +++++++++++++++++++++++ dev/keycloak/realm-export.json | 199 +++++++++++++++++++++++++++++++++ 6 files changed, 444 insertions(+), 1 deletion(-) create mode 100644 dev/README.md create mode 100644 dev/docker-compose.yml create mode 100644 dev/keycloak/realm-export.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 1db7ac2..14b17af 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(dev): local docker-compose stack (Keycloak + Postgres + Redis + Mongo + MinIO) with pre-imported breakpilot-dev realm and seed users - feat(iac): scaffold orca-platform — manifests/, overlays/, dns/, scripts/, Makefile (M1.1) - diff --git a/Makefile b/Makefile index 81b6c33..a92f19d 100644 --- a/Makefile +++ b/Makefile @@ -5,11 +5,18 @@ # make apply ENV= (M1.2+) push resolved set to Orca controller # make diff ENV= alias for plan # make clean remove .orca-out/ +# +# Local dev stack (dev/docker-compose.yml): +# make dev-up bring up Keycloak + Postgres + Redis + Mongo + MinIO +# make dev-down stop, keep volumes +# make dev-reset stop, wipe volumes, fresh start +# make dev-logs tail logs from every service -.PHONY: help validate plan apply diff clean +.PHONY: help validate plan apply diff clean dev-up dev-down dev-reset dev-logs ENV ?= ORCA_API_URL ?= +COMPOSE = docker compose -f dev/docker-compose.yml help: @echo "orca-platform targets:" @@ -18,6 +25,12 @@ help: @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/" + @echo "" + @echo "Local dev stack:" + @echo " make dev-up bring up the dev compose stack" + @echo " make dev-down stop, keep volumes" + @echo " make dev-reset stop, wipe volumes, fresh start" + @echo " make dev-logs tail logs" validate: @./scripts/validate.sh @@ -33,3 +46,18 @@ apply: clean: @rm -rf .orca-out @echo "removed .orca-out/" + +dev-up: + @$(COMPOSE) up -d + @echo "Keycloak: http://localhost:8080 (admin / admin-dev-pass on master realm)" + @echo " realm 'breakpilot-dev', user test@breakpilot.dev / test" + +dev-down: + @$(COMPOSE) down + +dev-reset: + @$(COMPOSE) down -v + @$(COMPOSE) up -d + +dev-logs: + @$(COMPOSE) logs -f diff --git a/README.md b/README.md index 2387f84..264fb87 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ The single source of truth for which container runs on which VM in which environ ## Run locally +### IaC validation + ```bash make validate # check all manifests parse + have required fields make plan ENV=stage # resolve manifests for stage → .orca-out/stage/ @@ -51,6 +53,18 @@ make apply ENV=stage # no-op until M1.2 stands up the Orca controller `make validate` runs in CI on every PR. +### Dev stack + +`platform/orca-platform` doubles as the home for the local-dev compose stack so a developer can clone this repo, run `make dev-up`, and immediately work against a real Keycloak realm + Postgres / Redis / Mongo / MinIO. See [`dev/README.md`](./dev/README.md) for the full picture. + +```bash +make dev-up # Keycloak (:8080) + Postgres (:5432) + Redis (:6379) + Mongo (:27017) + MinIO (:9000) +make dev-down # stop, keep volumes +make dev-reset # stop, wipe, fresh +``` + +Seed user: `test@breakpilot.dev` / `test` (tenant `acme`, products `certifai` + `compliance`). + ## Per-milestone fill-in schedule Each stub manifest in `manifests/` carries a header comment naming the milestone that finalises its real values. Summary: diff --git a/dev/README.md b/dev/README.md new file mode 100644 index 0000000..0eb12f9 --- /dev/null +++ b/dev/README.md @@ -0,0 +1,62 @@ +# Local dev stack + +Docker-compose that brings up just enough infrastructure to run `platform/tenant-registry` and `platform/portal` locally with a real Keycloak realm + seed user. + +## What's running + +| Service | Port | Purpose | +|---|---|---| +| Keycloak 26 | `:8080` | OIDC provider for portal + tenant-registry | +| pg-keycloak | (internal only) | Keycloak's backing Postgres | +| pg-app | `:5432` | Tenant Registry's Postgres (`platform/platform/platform-dev-pass`) | +| Redis | `:6379` | Session cache for portal | +| Mongo | `:27017` | (Future) CERTifAI data store | +| MinIO | `:9000` / `:9001` console | (Future) Compliance evidence object store | + +Stack is loopback only — no public exposure, no DNS, no TLS. Tenant-registry and portal run on the **host**, not in this compose. + +## Run + +From the repo root: + +```bash +make dev-up # bring everything up; first start takes ~30s for Keycloak realm import +make dev-down # stop, keep volumes +make dev-reset # stop, wipe volumes, fresh start +make dev-logs # tail logs from every service +``` + +## First login + +The realm `breakpilot-dev` ships pre-imported with two users: + +| Email | Password | Role | +|---|---|---| +| `test@breakpilot.dev` | `test` | IT_ADMIN of tenant `acme`, products: certifai + compliance | +| `admin@breakpilot.dev`| `admin` | BREAKPILOT_ADMIN (platform staff, backstage access) | + +Verify Keycloak is up: → click "Administration Console" → log in as `admin`/`admin-dev-pass` (master admin) → switch realm to `breakpilot-dev`. + +## Realm contents + +- **Clients:** + - `dev-portal` — public PKCE client used by `platform/portal`. Redirect URIs cover `http://localhost:3000/*` and `http://*.localhost:3000/*` so subdomain routing works in dev. + - `dev-tenant-registry` — bearer-only client used by the Go service to validate JWTs. +- **Protocol mappers on `dev-portal`** put `tenant_id`, `tenant_slug`, `org_roles`, `products`, `plan`, and `tenant_status` claims into every issued token. The portal middleware reads these to resolve the tenant + render the dashboard. The `test@breakpilot.dev` user has these claims set as user attributes — edit the user in Keycloak admin to flip flags during dev. +- **Realm roles** (platform-staff): `BREAKPILOT_ADMIN`, `SUPPORT_ENGINEER`, `SALES_REP`. + +## Reset just Keycloak + +If you mess up the realm and want to reimport the JSON: + +```bash +docker compose -f dev/docker-compose.yml down keycloak pg-keycloak +docker volume rm breakpilot-dev_pg-keycloak-data +make dev-up +``` + +The other services' data survives. + +## Why this is in `orca-platform` + +This is the only repo that already knows the whole topology — manifests in `manifests/` reference the same images this compose runs. Keeping the dev stack here means a developer clones `orca-platform`, runs `make dev-up`, and is ready to clone the service repo they actually want to work on. Per `INFRASTRUCTURE.md §1` dev runs entirely on developer laptops via docker-compose; this is that compose file. diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml new file mode 100644 index 0000000..a74aa1e --- /dev/null +++ b/dev/docker-compose.yml @@ -0,0 +1,139 @@ +# Local-dev stack for the Breakpilot Platform. +# +# Brings up the dependencies a developer needs to run tenant-registry + +# portal against a real Keycloak realm with seed data — no SysEleven VMs, +# no PowerDNS, no Stalwart. Loopback only. +# +# make dev-up bring this stack up +# make dev-down stop it, keep volumes +# make dev-reset stop, wipe volumes, bring up fresh +# +# Networking: every service joins the `breakpilot-dev` bridge network so +# tenant-registry and portal (running on the host, not in this compose) +# can reach Postgres etc. via `localhost:`. Inter-service traffic +# inside the compose (Keycloak → pg-keycloak) uses the service name. + +name: breakpilot-dev + +networks: + breakpilot-dev: + driver: bridge + +volumes: + pg-keycloak-data: + pg-app-data: + mongo-data: + minio-data: + +services: + # ─── Identity ────────────────────────────────────────────────────────── + pg-keycloak: + image: postgres:16-alpine + restart: unless-stopped + networks: [breakpilot-dev] + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: keycloak + POSTGRES_PASSWORD: keycloak-dev-pass + volumes: + - pg-keycloak-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"] + interval: 5s + timeout: 3s + retries: 10 + + keycloak: + image: quay.io/keycloak/keycloak:26.0 + restart: unless-stopped + depends_on: + pg-keycloak: + condition: service_healthy + networks: [breakpilot-dev] + ports: + - "8080:8080" + environment: + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://pg-keycloak:5432/keycloak + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: keycloak-dev-pass + KC_HEALTH_ENABLED: "true" + KC_HOSTNAME: localhost + KC_HTTP_ENABLED: "true" + KC_PROXY_HEADERS: xforwarded + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin-dev-pass + command: + - start-dev + - --import-realm + volumes: + - ./keycloak/realm-export.json:/opt/keycloak/data/import/breakpilot-dev.json:ro + healthcheck: + test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/8080 && echo -e 'GET /health/ready HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n' >&3 && grep -q UP <&3"] + interval: 10s + timeout: 5s + retries: 20 + start_period: 30s + + # ─── App data ────────────────────────────────────────────────────────── + pg-app: + image: postgres:16-alpine + restart: unless-stopped + networks: [breakpilot-dev] + ports: + - "5432:5432" + environment: + POSTGRES_DB: platform + POSTGRES_USER: platform + POSTGRES_PASSWORD: platform-dev-pass + volumes: + - pg-app-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U platform -d platform"] + interval: 5s + timeout: 3s + retries: 10 + + # ─── Caches + object stores ──────────────────────────────────────────── + redis: + image: redis:7-alpine + restart: unless-stopped + networks: [breakpilot-dev] + ports: + - "6379:6379" + + mongo: + image: mongo:7 + restart: unless-stopped + networks: [breakpilot-dev] + ports: + - "27017:27017" + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: mongo-dev-pass + volumes: + - mongo-data:/data/db + healthcheck: + test: ["CMD-SHELL", "echo 'db.runCommand({ping:1})' | mongosh --quiet"] + interval: 10s + timeout: 3s + retries: 6 + + minio: + image: minio/minio:latest + restart: unless-stopped + networks: [breakpilot-dev] + ports: + - "9000:9000" + - "9001:9001" + environment: + MINIO_ROOT_USER: minio-dev + MINIO_ROOT_PASSWORD: minio-dev-pass + command: server /data --console-address ":9001" + volumes: + - minio-data:/data + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:9000/minio/health/live"] + interval: 10s + timeout: 3s + retries: 5 diff --git a/dev/keycloak/realm-export.json b/dev/keycloak/realm-export.json new file mode 100644 index 0000000..216f86c --- /dev/null +++ b/dev/keycloak/realm-export.json @@ -0,0 +1,199 @@ +{ + "realm": "breakpilot-dev", + "enabled": true, + "displayName": "Breakpilot (dev)", + "registrationAllowed": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": true, + "editUsernameAllowed": false, + "bruteForceProtected": true, + "accessTokenLifespan": 900, + "ssoSessionIdleTimeout": 3600, + "ssoSessionMaxLifespan": 36000, + "internationalizationEnabled": false, + "defaultSignatureAlgorithm": "RS256", + "organizationsEnabled": true, + + "roles": { + "realm": [ + { "name": "BREAKPILOT_ADMIN", "description": "Platform staff: full backstage access" }, + { "name": "SUPPORT_ENGINEER", "description": "Platform staff: tenant impersonation + read" }, + { "name": "SALES_REP", "description": "Platform staff: demo-tenant access only" } + ] + }, + + "groups": [ + { + "name": "IT_ADMIN", + "path": "/IT_ADMIN", + "attributes": { "org_role": ["IT_ADMIN"] } + }, + { + "name": "CXO", + "path": "/CXO", + "attributes": { "org_role": ["CXO"] } + }, + { + "name": "USER", + "path": "/USER", + "attributes": { "org_role": ["USER"] } + } + ], + + "users": [ + { + "username": "test@breakpilot.dev", + "email": "test@breakpilot.dev", + "emailVerified": true, + "firstName": "Test", + "lastName": "Acme", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "test", + "temporary": false + } + ], + "attributes": { + "tenant_id": ["00000000-0000-0000-0000-000000000001"], + "tenant_slug": ["acme"], + "org_roles": ["IT_ADMIN"], + "products": ["certifai", "compliance"], + "plan": ["professional"], + "tenant_status": ["active"] + }, + "groups": ["/IT_ADMIN"] + }, + { + "username": "admin@breakpilot.dev", + "email": "admin@breakpilot.dev", + "emailVerified": true, + "firstName": "Platform", + "lastName": "Admin", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "admin", + "temporary": false + } + ], + "realmRoles": ["BREAKPILOT_ADMIN"] + } + ], + + "clients": [ + { + "clientId": "dev-portal", + "name": "Customer Portal (dev)", + "enabled": true, + "publicClient": true, + "standardFlowEnabled": true, + "directAccessGrantsEnabled": false, + "implicitFlowEnabled": false, + "serviceAccountsEnabled": false, + "rootUrl": "http://localhost:3000", + "baseUrl": "http://localhost:3000", + "redirectUris": [ + "http://localhost:3000/*", + "http://acme.localhost:3000/*", + "http://demo.localhost:3000/*" + ], + "webOrigins": ["+"], + "attributes": { + "pkce.code.challenge.method": "S256", + "post.logout.redirect.uris": "http://localhost:3000/*##http://*.localhost:3000/*" + }, + "protocolMappers": [ + { + "name": "tenant_id-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "config": { + "user.attribute": "tenant_id", + "claim.name": "tenant_id", + "jsonType.label": "String", + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "name": "tenant_slug-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "config": { + "user.attribute": "tenant_slug", + "claim.name": "tenant_slug", + "jsonType.label": "String", + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "name": "org_roles-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "config": { + "user.attribute": "org_roles", + "claim.name": "org_roles", + "jsonType.label": "String", + "multivalued": "true", + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "name": "products-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "config": { + "user.attribute": "products", + "claim.name": "products", + "jsonType.label": "String", + "multivalued": "true", + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "name": "plan-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "config": { + "user.attribute": "plan", + "claim.name": "plan", + "jsonType.label": "String", + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "name": "tenant_status-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "config": { + "user.attribute": "tenant_status", + "claim.name": "tenant_status", + "jsonType.label": "String", + "id.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "clientId": "dev-tenant-registry", + "name": "Tenant Registry (dev)", + "enabled": true, + "bearerOnly": true, + "publicClient": false, + "standardFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false + } + ] +}