mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-08 09:54:26 +00:00
fix(mesh): address Codex audit findings on F-1 PR (#1158)
- docs(sencho-mesh): split subnet_overlap troubleshooting into env-set vs env-unset paths; rewrite the "Customising the mesh subnet" intro to describe the candidate list and the adopt-existing behavior. - backend(MeshService): preserve idempotent 409 handling in the explicit-env path. On createNetwork 409 (TOCTOU race against another process), re-inspect and treat the race-winner as success when its subnet matches the operator's request; subnet_mismatch otherwise. - frontend(MeshDataPlaneBanner): trim the card variant to a true one-line strip (headline only, truncate min-w-0). Full recovery hint stays on the Routing tab variant and in docs. - tests(mesh): add five cases covering the previously untested branches — candidate-loop non-overlap bail, adopt-existing with unparseable subnet, explicit-env generic createNetwork failure, TOCTOU 409 race-winner match, TOCTOU 409 race-winner mismatch. Architecture map (gitignored per Directive 11) updated locally with the new useMeshDataPlane hook node and the mesh.dashboardBanner flow so the local interactive viewer stays accurate.
This commit is contained in:
@@ -327,4 +327,117 @@ describe('MeshService.setupMeshNetwork subnet auto-fallback', () => {
|
||||
expect(status.subnet).toBe('10.42.0.0/24');
|
||||
expect(createNetwork).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('bails the candidate loop on a non-overlap createNetwork failure', async () => {
|
||||
delete process.env.SENCHO_MESH_SUBNET;
|
||||
process.env.HOSTNAME = 'sencho';
|
||||
const overlap = Object.assign(
|
||||
new Error('Pool overlaps with other one on this address space'),
|
||||
{ statusCode: 500 },
|
||||
);
|
||||
const daemonErr = Object.assign(
|
||||
new Error('daemon attach error: out of inodes'),
|
||||
{ statusCode: 500 },
|
||||
);
|
||||
// First candidate overlaps (recoverable, advance), second hits a
|
||||
// generic daemon error (unrecoverable, bail).
|
||||
const createNetwork = vi.fn()
|
||||
.mockRejectedValueOnce(overlap)
|
||||
.mockRejectedValueOnce(daemonErr);
|
||||
const inspectNetwork = vi.fn().mockRejectedValue({ statusCode: 404, message: 'no such network' });
|
||||
mockDocker({ createNetwork, inspectNetwork });
|
||||
|
||||
const svc = MeshService.getInstance();
|
||||
await callSetup(svc);
|
||||
|
||||
const status = svc.getDataPlaneStatus();
|
||||
expect(status.ok).toBe(false);
|
||||
expect(status.reason).toBe('attach_failed');
|
||||
// Stops at the second candidate; does not advance to the third.
|
||||
expect(createNetwork).toHaveBeenCalledTimes(2);
|
||||
expect(status.subnet).toBe('172.31.0.0/24');
|
||||
});
|
||||
|
||||
it('classifies an unparseable existing subnet as subnet_invalid on the adopt path', async () => {
|
||||
delete process.env.SENCHO_MESH_SUBNET;
|
||||
process.env.HOSTNAME = 'sencho';
|
||||
const createNetwork = vi.fn();
|
||||
const inspectNetwork = vi.fn().mockResolvedValue({
|
||||
IPAM: { Config: [{ Subnet: 'not-a-cidr' }] },
|
||||
});
|
||||
mockDocker({ createNetwork, inspectNetwork });
|
||||
|
||||
const svc = MeshService.getInstance();
|
||||
await callSetup(svc);
|
||||
|
||||
const status = svc.getDataPlaneStatus();
|
||||
expect(status.ok).toBe(false);
|
||||
expect(status.reason).toBe('subnet_invalid');
|
||||
expect(status.subnet).toBe('not-a-cidr');
|
||||
expect(createNetwork).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('classifies a generic explicit-env createNetwork failure as attach_failed', async () => {
|
||||
process.env.SENCHO_MESH_SUBNET = '10.42.0.0/24';
|
||||
process.env.HOSTNAME = 'sencho';
|
||||
const daemonErr = Object.assign(
|
||||
new Error('daemon attach error: out of inodes'),
|
||||
{ statusCode: 500 },
|
||||
);
|
||||
const createNetwork = vi.fn().mockRejectedValue(daemonErr);
|
||||
const inspectNetwork = vi.fn().mockRejectedValue({ statusCode: 404, message: 'no such network' });
|
||||
mockDocker({ createNetwork, inspectNetwork });
|
||||
|
||||
const svc = MeshService.getInstance();
|
||||
await callSetup(svc);
|
||||
|
||||
const status = svc.getDataPlaneStatus();
|
||||
expect(status.ok).toBe(false);
|
||||
expect(status.reason).toBe('attach_failed');
|
||||
expect(status.subnet).toBe('10.42.0.0/24');
|
||||
});
|
||||
|
||||
it('treats a TOCTOU 409 in the explicit-env path as idempotent when the race-winner subnet matches', async () => {
|
||||
process.env.SENCHO_MESH_SUBNET = '10.42.0.0/24';
|
||||
process.env.HOSTNAME = 'sencho';
|
||||
const conflict = Object.assign(
|
||||
new Error('network with name sencho_mesh already exists'),
|
||||
{ statusCode: 409 },
|
||||
);
|
||||
const createNetwork = vi.fn().mockRejectedValue(conflict);
|
||||
// First inspect (pre-create probe): 404. Second inspect (post-409
|
||||
// race-winner check): the matching subnet.
|
||||
const inspectNetwork = vi.fn()
|
||||
.mockRejectedValueOnce({ statusCode: 404, message: 'no such network' })
|
||||
.mockResolvedValueOnce({ IPAM: { Config: [{ Subnet: '10.42.0.0/24' }] } });
|
||||
mockDocker({ createNetwork, inspectNetwork });
|
||||
|
||||
const svc = MeshService.getInstance();
|
||||
await callSetup(svc);
|
||||
|
||||
const status = svc.getDataPlaneStatus();
|
||||
expect(status.ok).toBe(true);
|
||||
expect(status.subnet).toBe('10.42.0.0/24');
|
||||
});
|
||||
|
||||
it('treats a TOCTOU 409 in the explicit-env path as subnet_mismatch when the race-winner differs', async () => {
|
||||
process.env.SENCHO_MESH_SUBNET = '10.42.0.0/24';
|
||||
process.env.HOSTNAME = 'sencho';
|
||||
const conflict = Object.assign(
|
||||
new Error('network with name sencho_mesh already exists'),
|
||||
{ statusCode: 409 },
|
||||
);
|
||||
const createNetwork = vi.fn().mockRejectedValue(conflict);
|
||||
const inspectNetwork = vi.fn()
|
||||
.mockRejectedValueOnce({ statusCode: 404, message: 'no such network' })
|
||||
.mockResolvedValueOnce({ IPAM: { Config: [{ Subnet: '172.30.0.0/24' }] } });
|
||||
mockDocker({ createNetwork, inspectNetwork });
|
||||
|
||||
const svc = MeshService.getInstance();
|
||||
await callSetup(svc);
|
||||
|
||||
const status = svc.getDataPlaneStatus();
|
||||
expect(status.ok).toBe(false);
|
||||
expect(status.reason).toBe('subnet_mismatch');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -662,13 +662,43 @@ export class MeshService extends EventEmitter implements MeshForwarderHost {
|
||||
try {
|
||||
await this.createMeshNetwork(envSubnet);
|
||||
} catch (err) {
|
||||
this.recordSetupFailure(
|
||||
this.classifyMeshNetworkError(err),
|
||||
err,
|
||||
'error',
|
||||
envSubnet,
|
||||
);
|
||||
return;
|
||||
// TOCTOU: another process may have created `sencho_mesh`
|
||||
// between our inspect (returned null) and our create
|
||||
// (rejected with 409). Re-inspect; if the existing
|
||||
// subnet matches what the operator requested, treat
|
||||
// this as idempotent success (matches the prior
|
||||
// ensureMeshNetwork 409-then-inspect behavior). Any
|
||||
// other error or a mismatch reverts to the typed
|
||||
// failure path.
|
||||
const dockerErr = err as { statusCode?: number };
|
||||
if (dockerErr?.statusCode === 409) {
|
||||
const raceWinner = await this.inspectExistingMeshSubnet().catch(() => null);
|
||||
if (raceWinner === envSubnet) {
|
||||
// Adopt the race-winner's network; proceed to attach.
|
||||
} else if (raceWinner) {
|
||||
this.recordSetupFailure(
|
||||
'subnet_mismatch',
|
||||
new Error(
|
||||
`${SENCHO_MESH_NETWORK} exists with subnet ${raceWinner}, ` +
|
||||
`expected ${envSubnet}. Remove the network or set SENCHO_MESH_SUBNET to match.`,
|
||||
),
|
||||
'error',
|
||||
envSubnet,
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
this.recordSetupFailure('attach_failed', err, 'error', envSubnet);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.recordSetupFailure(
|
||||
this.classifyMeshNetworkError(err),
|
||||
err,
|
||||
'error',
|
||||
envSubnet,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (existingSubnet) {
|
||||
|
||||
Reference in New Issue
Block a user