molecule-core/tests/harness/compose.yml
claude-ceo-assistant (Claude Opus 4.7 on Hongming's MacBook) 7eb348536b fix(harness): bake cf-proxy nginx.conf at build time, not via configs:
The previous configs:-based fix (87b971a2) didn't actually fix the DinD
issue — Compose v2 falls back to bind mounts for `configs:` when swarm
mode is not active, so the resulting runc invocation still tries to
mount /workspace/.../cf-proxy/nginx.conf from the OUTER host filesystem
that the act_runner-vs-host-docker socket-mount can't see. Same
"not a directory" error returned.

Switch to a thin Dockerfile (cf-proxy/Dockerfile) that COPYs nginx.conf
into nginx:1.27-alpine. The build context is uploaded to the daemon as
a tarball, not bind-mounted from the host filesystem, so the path
translation gap doesn't apply. Verified locally: `docker build` +
`docker run cf-proxy nginx -T` reproduces the baked config end-to-end.

Trade-off: ~2-3s build cost on every harness up. Acceptable for the
Gitea CI gate; local-dev re-builds the image only when nginx.conf
changes (Docker layer cache).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 17:09:08 -07:00

199 lines
7.3 KiB
YAML

# Production-shape harness for local E2E. Multi-tenant.
#
# Reproduces the SaaS tenant topology on localhost using the SAME
# images that ship to production:
#
# client → cf-proxy (nginx, mimics CF tunnel headers, routes by Host)
# ├─ Host: harness-tenant-alpha.localhost → tenant-alpha
# │ ↓ (CP_UPSTREAM_URL=http://cp-stub:9090)
# │ tenant-alpha (workspace-server/Dockerfile.tenant)
# │ ↓
# │ postgres-alpha (per-tenant DB, matches prod)
# ├─ Host: harness-tenant-beta.localhost → tenant-beta
# │ ↓
# │ tenant-beta + postgres-beta
# └─ cp-stub + redis (shared infra; CP is Railway-singleton in prod,
# redis is shared cluster)
#
# The two-tenant topology catches:
# - TenantGuard cross-tenant escape (alpha-org token shouldn't see
# beta-tenant data even with a valid bearer)
# - cf-proxy Host-header routing correctness
# - Per-tenant DB isolation (workspaces table, activity_logs)
# - Concurrent multi-tenant operation (no shared mutable state)
#
# Quickstart (no /etc/hosts edits — see README):
# cd tests/harness && ./up.sh && ./seed.sh
# ./replays/peer-discovery-404.sh
# ./run-all-replays.sh
#
# Env config:
# GIT_SHA — passed to BOTH tenant builds for /buildinfo verification.
# CP_STUB_PEERS_MODE — peers failure mode for replay scripts.
services:
# ─── Shared infra (matches prod: CP is Railway-singleton, redis shared) ───
redis:
image: redis:7-alpine
networks: [harness-net]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 2s
timeout: 5s
retries: 10
cp-stub:
build:
context: ./cp-stub
environment:
PORT: "9090"
CP_STUB_PEERS_MODE: "${CP_STUB_PEERS_MODE:-}"
networks: [harness-net]
healthcheck:
test: ["CMD-SHELL", "wget -q -O- http://localhost:9090/healthz || exit 1"]
interval: 2s
timeout: 5s
retries: 10
# ─── Tenant alpha: postgres + workspace-server ────────────────────────
postgres-alpha:
image: postgres:16-alpine
environment:
POSTGRES_USER: harness
POSTGRES_PASSWORD: harness
POSTGRES_DB: molecule
networks: [harness-net]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U harness"]
interval: 2s
timeout: 5s
retries: 10
tenant-alpha:
build:
context: ../..
dockerfile: workspace-server/Dockerfile.tenant
args:
GIT_SHA: "${GIT_SHA:-harness}"
depends_on:
postgres-alpha:
condition: service_healthy
redis:
condition: service_healthy
cp-stub:
condition: service_healthy
environment:
DATABASE_URL: "postgres://harness:harness@postgres-alpha:5432/molecule?sslmode=disable"
REDIS_URL: "redis://redis:6379"
PORT: "8080"
PLATFORM_URL: "http://tenant-alpha:8080"
MOLECULE_ENV: "production"
SECRETS_ENCRYPTION_KEY: "${SECRETS_ENCRYPTION_KEY:?must be set — run via tests/harness/up.sh, which generates one per run}"
ADMIN_TOKEN: "harness-admin-token-alpha"
MOLECULE_ORG_ID: "harness-org-alpha"
CP_UPSTREAM_URL: "http://cp-stub:9090"
RATE_LIMIT: "1000"
CANVAS_PROXY_URL: "http://localhost:3000"
# Memory v2 sidecar (PR #2906) bundles the plugin into the
# tenant image and starts it before the main server. The plugin
# runs `CREATE EXTENSION vector` on first boot, which fails on
# the harness's plain postgres:15-alpine (no pgvector). The
# harness doesn't exercise memory features, so disable the
# sidecar via the entrypoint's documented escape hatch.
MEMORY_PLUGIN_DISABLE: "1"
networks: [harness-net]
healthcheck:
test: ["CMD-SHELL", "wget -q -O- http://localhost:8080/health || exit 1"]
interval: 5s
timeout: 5s
retries: 20
# ─── Tenant beta: postgres + workspace-server (parallel to alpha) ─────
postgres-beta:
image: postgres:16-alpine
environment:
POSTGRES_USER: harness
POSTGRES_PASSWORD: harness
POSTGRES_DB: molecule
networks: [harness-net]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U harness"]
interval: 2s
timeout: 5s
retries: 10
tenant-beta:
build:
context: ../..
dockerfile: workspace-server/Dockerfile.tenant
args:
GIT_SHA: "${GIT_SHA:-harness}"
depends_on:
postgres-beta:
condition: service_healthy
redis:
condition: service_healthy
cp-stub:
condition: service_healthy
environment:
DATABASE_URL: "postgres://harness:harness@postgres-beta:5432/molecule?sslmode=disable"
REDIS_URL: "redis://redis:6379"
PORT: "8080"
PLATFORM_URL: "http://tenant-beta:8080"
MOLECULE_ENV: "production"
SECRETS_ENCRYPTION_KEY: "${SECRETS_ENCRYPTION_KEY:?must be set — run via tests/harness/up.sh, which generates one per run}"
# Distinct ADMIN_TOKEN — replays use this to verify TenantGuard
# blocks alpha-token presented at beta's URL.
ADMIN_TOKEN: "harness-admin-token-beta"
MOLECULE_ORG_ID: "harness-org-beta"
CP_UPSTREAM_URL: "http://cp-stub:9090"
RATE_LIMIT: "1000"
CANVAS_PROXY_URL: "http://localhost:3000"
# Memory v2 sidecar (PR #2906) bundles the plugin into the
# tenant image and starts it before the main server. The plugin
# runs `CREATE EXTENSION vector` on first boot, which fails on
# the harness's plain postgres:15-alpine (no pgvector). The
# harness doesn't exercise memory features, so disable the
# sidecar via the entrypoint's documented escape hatch.
MEMORY_PLUGIN_DISABLE: "1"
networks: [harness-net]
healthcheck:
test: ["CMD-SHELL", "wget -q -O- http://localhost:8080/health || exit 1"]
interval: 5s
timeout: 5s
retries: 20
# ─── cf-proxy: routes by Host to the right tenant container ───────────
# Production shape: same single CF tunnel front-doors every tenant
# subdomain — the Host header carries the tenant identity, not the
# routing destination. Local cf-proxy mirrors this exactly.
#
# nginx.conf delivery: built into a custom image via cf-proxy/Dockerfile
# (a thin nginx:1.27-alpine + COPY). NOT a bind mount and NOT a
# compose `configs:` block, both of which break under Gitea's
# act_runner: the runner talks to the OUTER docker daemon over the
# host socket, and runc resolves bind sources on the outer host
# filesystem, where `/workspace/.../tests/harness/cf-proxy/nginx.conf`
# is invisible. Compose `configs:` falls back to bind mounts without
# swarm, so it hits the same gap. A build context, by contrast, is
# uploaded to the daemon as a tarball at build time — no bind. See
# issue #88 item 2.
cf-proxy:
build:
context: ./cf-proxy
dockerfile: Dockerfile
depends_on:
tenant-alpha:
condition: service_healthy
tenant-beta:
condition: service_healthy
# Bind to 127.0.0.1 only — hardcoded ADMIN_TOKENs make 0.0.0.0
# exposure unsafe even on a local network.
ports:
- "127.0.0.1:8080:8080"
networks: [harness-net]
networks:
harness-net:
name: molecule-harness-net