Files
sso-manager-node/docker-compose.multisite-e2e.yml
wmantly dae0361e82 fix(multi-site): promotion no longer orphans the demoted old master's LDAP replication
Found while auditing the new LDAP MMR auto-config for gaps: neither
POST /site-promote nor POST /demote ever touched SiteSpoke. Two real
problems:

1. The demoted old master got a fresh masterJoinKey but was never
   registered as a spoke of the new master -- no SiteSpoke row, no
   ldapServerId, invisible to GET /ldap-peers's peer list. It also
   structurally could not self-heal: POST /join refuses re-join for a
   node that's already a spoke, and separately requires a fresh
   install (siteIsFresh()) -- neither true for a former master with
   real users/agents. Fixed: /demote now registers itself with the new
   master immediately (POST /spokes), the same way a real join does,
   deriving its own endpoint from stack.selfUrl (override) or
   https://stack.ssoHost (the normal case).

2. The promoted node's live OpenLDAP ServerID doesn't change --
   GET /ldap-replication-config starts advertising 1 for it
   immediately (derived purely from cfg.isMaster), but nothing
   restarts slapd with that value (OpenLDAP's static slapd.conf only
   reloads at process start, and this app has no safe way to restart
   its own container). Can't be fixed in-process; surfaced instead --
   /site-promote's response now includes ldapReplicationNote telling
   the operator to re-run setup.sh promptly.

Verified against real running containers (docker-compose.multisite-e2e.yml):
after promotion, the demoted old master correctly appears in the new
master's LDAP peer list with a real assigned ldapServerId.
2026-08-10 23:21:59 -04:00

76 lines
2.6 KiB
YAML

# End-to-end test of the real, shipped multi-site join flow (docs/site-join.md).
#
# Spins up two full all-in-one instances (app + bundled slapd each, like
# docker-compose.repl-test.yml) — "master" and "spoke" — plus a client that
# drives the actual HTTP API a human/operator would use: mint a site join key
# on master, join from spoke, verify the spoke adopted the catalog, went
# read-only, and reports live WAN health.
#
# docker compose -f docker-compose.multisite-e2e.yml up --build --abort-on-container-exit
# # exit code 0 = MULTISITE E2E PASS
#
# slapcat (used by POST /api/site/export) only sees the LDAP data of the
# container it runs in, so this MUST use the all-in-one image (master and
# spoke each carry their own slapd) — the split ldap+redis+app harness used
# by docker-compose.test.yml/e2e.yml won't exercise export/join at all.
services:
master:
build:
context: .
dockerfile: Dockerfile.openldap
container_name: multisite_e2e_master
environment:
- LDAP_BASE_DN=dc=master,dc=test
- LDAP_ADMIN_PASS=secret
- ORG_NAME=E2E Master
- app_oauth__jwtSecret=e2e-multisite-master-jwt-secret
# POST /demote's self-registration (routes/api_site.js) needs a real
# reachable endpoint for this container; stack.selfUrl overrides the
# normal https://<stack.ssoHost> derivation, which isn't reachable
# here (plain HTTP, no TLS/proxy in front, non-443 port).
- app_stack__selfUrl=http://master:3001
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3001/health >/dev/null 2>&1"]
interval: 2s
timeout: 3s
retries: 40
start_period: 5s
spoke:
build:
context: .
dockerfile: Dockerfile.openldap
container_name: multisite_e2e_spoke
environment:
- LDAP_BASE_DN=dc=spoke,dc=test
- LDAP_ADMIN_PASS=secret
- ORG_NAME=E2E Spoke
- app_oauth__jwtSecret=e2e-multisite-spoke-jwt-secret
- app_stack__selfUrl=http://spoke:3001
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3001/health >/dev/null 2>&1"]
interval: 2s
timeout: 3s
retries: 40
start_period: 5s
client:
build:
context: .
dockerfile: Dockerfile.test-runner
command: ["sh", "-c", "node test/multisite_join_e2e.js"]
environment:
- MASTER_URL=http://master:3001
- SPOKE_URL=http://spoke:3001
- MASTER_LDAP_HOST=master
- MASTER_BASE_DN=dc=master,dc=test
- SPOKE_LDAP_HOST=spoke
- SPOKE_BASE_DN=dc=spoke,dc=test
- LDAP_ADMIN_PASS=secret
depends_on:
master:
condition: service_healthy
spoke:
condition: service_healthy