feat(directory): LDAP replication status + per-spoke detail on the Multi-Site modal

The modal previously showed zero LDAP replication status -- no
ServerID, no MMR active/inactive indicator, nothing -- and only
aggregate counts, never per-spoke detail (no-inbound flag, relay
note, assigned ldapServerId). An operator had no way to tell whether
replication was actually configured/working without SSHing in.

Added utils/ldap_replication.js's currentSlapdServerId(), which reads
the ACTUAL running ServerID straight from this node's own slapd.conf
-- distinct from what GET /ldap-peers / /ldap-replication-config
currently ADVERTISE for it, which can genuinely disagree right after a
promotion or a new spoke joining (OpenLDAP's static config only
reloads at process start). GET /directory-admin/site-status now
surfaces both plus a `stale` flag, and a full spokes list (not just a
count) with each one's endpoint, LDAP ServerID, and relay path.

directory.ejs renders this as an "LDAP Replication (MMR)" status row
(ServerID, peer count, a "needs setup.sh re-run" warning when stale)
and a Registered Spokes table.

Verified two ways: real running containers via
docker-compose.multisite-e2e.yml (new site-status assertions), and an
actual browser session against the promoted node -- screenshotted the
rendered modal showing the real registered spoke with its assigned
ldapServerId and the correctly-surfaced "not configured (standalone)"
MMR state (this test node never ran site-ldap-register.js against it,
so the mismatch itself is the expected, documented behavior).
This commit is contained in:
2026-08-10 23:48:20 -04:00
parent bd2205f1a9
commit 4542c055bb
4 changed files with 118 additions and 5 deletions
+13
View File
@@ -229,6 +229,19 @@ async function main() {
const selfInOwnPeerList = (spokeLdapCfg.peers || []).some(p => p.ldapServerId === spokeLdapCfg.ldapServerId);
if (selfInOwnPeerList) fail(`spoke's own peer list should not include itself, got ${JSON.stringify(spokeLdapCfg.peers)}`);
step('Verifying the master\'s own site-status surfaces LDAP status + per-spoke detail (Multi-Site modal data)');
const { body: masterStatus } = await api(MASTER_URL, '/api/directory-admin/site-status', { token: masterToken });
if (!masterStatus.ldap || masterStatus.ldap.advertisedServerId !== 1) {
fail(`master's site-status should report ldap.advertisedServerId 1, got ${JSON.stringify(masterStatus.ldap)}`);
}
if (masterStatus.ldap.peersCount !== 1) {
fail(`master's site-status should report exactly 1 LDAP peer (the spoke), got ${JSON.stringify(masterStatus.ldap)}`);
}
const statusSpokeEntry = (masterStatus.spokes || []).find(s => s.endpoint === 'http://spoke:3001');
if (!statusSpokeEntry || typeof statusSpokeEntry.ldapServerId !== 'number') {
fail(`master's site-status spokes list should include the spoke with an ldapServerId, got ${JSON.stringify(masterStatus.spokes)}`);
}
step('Verifying the spoke adopted the master\'s pre-join catalog');
const spokeResources = await api(SPOKE_URL, '/api/directory-admin/resources', { token: spokeToken });
const adopted = (spokeResources.body.results || spokeResources.body.resources || spokeResources.body || []);