feat(multi-site): route replication traffic over the mesh when available
Cross-component routing TODO item: a spoke's resync push now prefers its WG mesh IP (reported via the noInbound/meshIp fields added for the relay automation) over the public endpoint, falling back to the public endpoint if the mesh attempt fails for any reason (tunnel not actually up between these two particular gateways yet, transient failure, etc.) -- a mesh-routing preference must never turn into "spoke stops getting updates." Plain HTTP over the mesh IP, not HTTPS: the WG tunnel is already encrypted, same reasoning already applied to the no-inbound relay terminating at the master. A spoke with no meshIp on file behaves exactly as before (public endpoint only) -- this is additive, not a behavior change for spokes that haven't opted into mesh registration.
This commit is contained in:
@@ -55,6 +55,45 @@ describe('site_replicate', () => {
|
||||
expect(JSON.parse(optsA.body).reason).toBe('catalog-changed');
|
||||
});
|
||||
|
||||
test('prefers the mesh IP over the public endpoint when the spoke reported one', async () => {
|
||||
SiteSpoke._seed([
|
||||
{ endpoint: 'https://spoke-a.example.com:8443', pushToken: 'token-a', meshIp: '172.24.5.1' }
|
||||
]);
|
||||
|
||||
await siteReplicate.replicateToSpokes('catalog-changed');
|
||||
await new Promise((r) => setImmediate(r));
|
||||
|
||||
expect(mockFetchCalls.length).toBe(1);
|
||||
expect(mockFetchCalls[0][0]).toBe('http://172.24.5.1:8443/api/site/resync');
|
||||
});
|
||||
|
||||
test('falls back to the public endpoint if the mesh attempt fails', async () => {
|
||||
SiteSpoke._seed([
|
||||
{ endpoint: 'https://spoke-a.example.com', pushToken: 'token-a', meshIp: '172.24.5.1' }
|
||||
]);
|
||||
mockFetchImpl = async (url) => {
|
||||
if (url.startsWith('http://172.24.5.1')) throw new Error('mesh unreachable');
|
||||
return { ok: true, status: 200 };
|
||||
};
|
||||
|
||||
await siteReplicate.replicateToSpokes('catalog-changed');
|
||||
await new Promise((r) => setImmediate(r));
|
||||
|
||||
expect(mockFetchCalls.length).toBe(2);
|
||||
expect(mockFetchCalls[0][0]).toMatch(/^http:\/\/172\.24\.5\.1/);
|
||||
expect(mockFetchCalls[1][0]).toBe('https://spoke-a.example.com/api/site/resync');
|
||||
});
|
||||
|
||||
test('a spoke with no meshIp only ever tries the public endpoint', async () => {
|
||||
SiteSpoke._seed([{ endpoint: 'https://spoke-a.example.com', pushToken: 'token-a' }]);
|
||||
|
||||
await siteReplicate.replicateToSpokes('catalog-changed');
|
||||
await new Promise((r) => setImmediate(r));
|
||||
|
||||
expect(mockFetchCalls.length).toBe(1);
|
||||
expect(mockFetchCalls[0][0]).toBe('https://spoke-a.example.com/api/site/resync');
|
||||
});
|
||||
|
||||
test('no known spokes: resolves cleanly, no fetch calls', async () => {
|
||||
await siteReplicate.replicateToSpokes('catalog-changed');
|
||||
await new Promise((r) => setImmediate(r));
|
||||
|
||||
Reference in New Issue
Block a user