dae0361e82
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.
76 lines
2.6 KiB
YAML
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
|