Live Google Workspace delivery validation surfaced a real, self-resolving Google-side condition: freshly-authorized domain-wide delegation can take up to about a minute to propagate across Google's auth infrastructure, producing intermittent unauthorized_client on the OAuth token exchange even with correct configuration. gws-provider.js now classifies unauthorized_client as a temporary (retryable) error instead of permanent, so the existing bounded-backoff worker absorbs it automatically. Also documents the alias-vs-delegated-user env passthrough in docker-compose.dev.yml, and records both real findings (this one and the GOOGLE_DELEGATED_USER alias distinction) plus the full live-delivery validation outcome in the runbook and Phase 5C doc. Validated live: isolated gws-email-test.js send (DKIM/SPF passing, delivered) and a full real lifecycle test (create student, enrollment, real activation email, activate, login, see program, real password-reset email, reset, login with new password) via [email protected].
116 lines
5.6 KiB
YAML
116 lines
5.6 KiB
YAML
# Phase 2B full-stack local dev/test — Postgres + egress-proxy + Provisioner + the lightweight
|
|
# Platform API + the Gateway, all on one machine (the documented Phase 1/2A/2B dev exception;
|
|
# production is the two-VM Proxmox split, see docs/LAB_ARCHITECTURE.md §1).
|
|
#
|
|
# `api` (platform/api) replaces Phase 2A's "identity" service — that was a full, --privileged
|
|
# Lab image booting an unused k3s cluster just to serve roster/curriculum/progress APIs. This
|
|
# is a plain Node process: no k3s, no --privileged, no terminal runtime. It owns
|
|
# students/cohorts/enrollments/curriculum/progress/scenario_attempts in Postgres and is the
|
|
# only service that talks to the Provisioner (student launch/reset AND instructor/admin
|
|
# actions) — see docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md.
|
|
#
|
|
# gateway, provisioner, and api's egress-adjacent needs aside, `api` does NOT need
|
|
# network_mode: host — it never touches Docker networking or firewall rules itself, it only
|
|
# calls the Provisioner's HTTP API and Postgres. gateway and provisioner keep network_mode:
|
|
# host because they need to reach ephemeral per-student Lab containers by internal IP /
|
|
# manage firewall rules on the host network namespace.
|
|
|
|
services:
|
|
postgres:
|
|
image: postgres:16-alpine
|
|
environment:
|
|
POSTGRES_USER: postgres
|
|
POSTGRES_PASSWORD: postgres
|
|
POSTGRES_DB: rootnode_platform
|
|
ports:
|
|
- "5433:5432"
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 10
|
|
|
|
egress-proxy:
|
|
build: ./egress-proxy
|
|
network_mode: host
|
|
|
|
provisioner:
|
|
build: ./provisioner
|
|
network_mode: host
|
|
cap_add:
|
|
- NET_ADMIN
|
|
- NET_RAW
|
|
volumes:
|
|
- /var/run/docker.sock:/var/run/docker.sock
|
|
environment:
|
|
PLATFORM_DB_URL: postgres://postgres:postgres@localhost:5433/rootnode_platform
|
|
PROVISIONER_SHARED_SECRET: ${PROVISIONER_SHARED_SECRET:?set a real secret, do not use a default}
|
|
PROVISIONER_PORT: "4100"
|
|
LAB_IMAGE_REF: ${LAB_IMAGE_REF:-rootnode-academy:test}
|
|
EGRESS_PROXY_PORT: "3128"
|
|
DNS_RESOLVER: "127.0.0.11"
|
|
NO_PROXY_CIDRS: "10.42.0.0/16,10.43.0.0/16"
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
|
|
api:
|
|
build:
|
|
context: ..
|
|
dockerfile: platform/api/Dockerfile
|
|
network_mode: host # needs to reach the Provisioner at localhost:4100 (host-networked);
|
|
# port 4300 is directly the host's port under host networking, no
|
|
# `ports:` mapping needed (and compose rejects one with this mode).
|
|
environment:
|
|
# localhost:5433, not the "postgres" compose hostname — network_mode: host means this
|
|
# container is on the host's network namespace, not the compose bridge network, so
|
|
# compose's internal DNS doesn't resolve here (same reason Provisioner uses localhost too).
|
|
PLATFORM_DB_URL: postgres://postgres:postgres@localhost:5433/rootnode_platform
|
|
PROVISIONER_URL: "http://localhost:4100"
|
|
PROVISIONER_SHARED_SECRET: ${PROVISIONER_SHARED_SECRET:?set a real secret, do not use a default}
|
|
# Phase 5B — the website/commerce integration boundary's shared secret (see
|
|
# docs/WEBSITE_ENROLLMENT_INTEGRATION.md). Optional in dev: unset means
|
|
# POST /api/integrations/enrollments refuses every request (fails closed), which is fine
|
|
# for anyone not exercising that boundary locally.
|
|
INTEGRATION_SHARED_SECRET: ${INTEGRATION_SHARED_SECRET:-}
|
|
# Phase 5C — activation/reset email links are built from this origin (see
|
|
# docs/PHASE5C_ENROLLMENT_COMMUNICATIONS.md §7). The Gateway's own dev URL, since that's
|
|
# what a browser actually hits locally. EMAIL_PROVIDER is deliberately left unset here —
|
|
# it defaults to the dev provider (email/dev-provider.js), which never makes a real
|
|
# network call no matter what recipient it's given.
|
|
PUBLIC_APP_ORIGIN: ${PUBLIC_APP_ORIGIN:-http://localhost:4200}
|
|
EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-RootNode Academy}
|
|
# Empty/unset by default (fail-safe: dev provider, no real email — see email/provider.js).
|
|
# Set all four in platform/.env only for a deliberate one-off live-delivery test; unset
|
|
# again afterward so routine dev/test runs never risk a real send.
|
|
EMAIL_PROVIDER: ${EMAIL_PROVIDER:-}
|
|
GOOGLE_SERVICE_ACCOUNT_JSON: ${GOOGLE_SERVICE_ACCOUNT_JSON:-}
|
|
EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-}
|
|
GOOGLE_DELEGATED_USER: ${GOOGLE_DELEGATED_USER:-}
|
|
PLATFORM_API_PORT: "4300"
|
|
# No ADMIN_PASSWORD anymore (Phase 3A) — instructor accounts live in Postgres, created via
|
|
# `npm run bootstrap-owner` for the first owner and the admin portal after that.
|
|
# Empty by default — unset/empty means the exact same dev behavior as before this was
|
|
# parametrized (dev_reset_token echoed, dev-preview route reachable). Phase 5C's
|
|
# e2e-phase5c.js sets this to "1" via .env for one test to prove production gating for
|
|
# real, then unsets it again — see that test's own comment.
|
|
COOKIE_SECURE: ${COOKIE_SECURE:-}
|
|
# TRUST_PROXY: "1" # set once fronted by a trusted reverse proxy
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
provisioner:
|
|
condition: service_started
|
|
|
|
gateway:
|
|
build: ./gateway
|
|
network_mode: host
|
|
environment:
|
|
GATEWAY_PORT: "4200"
|
|
PROVISIONER_URL: "http://localhost:4100"
|
|
PROVISIONER_SHARED_SECRET: ${PROVISIONER_SHARED_SECRET:?set a real secret, do not use a default}
|
|
PLATFORM_API_URL: "http://localhost:4300"
|
|
depends_on:
|
|
- provisioner
|
|
- api
|