From c12b3ba995a425cc3ba375432a32f7a0a1a4b0b6 Mon Sep 17 00:00:00 2001 From: Anso Date: Sun, 10 May 2026 01:29:21 -0400 Subject: [PATCH] fix(mesh): apply D-1 pushed override on pilot deploys via file-presence fallback (#1019) When isMeshStackEnabled returns false, ensureStackOverride now checks for an override file already on disk before returning null. On pilot nodes the mesh_stacks table is intentionally empty (opt-in state lives on central per the C-3 design), so the DB gate blocked the override that central had already pushed via applyLocalOverride. File-presence is safe as the fallback because removeOverrideFromNode sends a DELETE to the pilot when a stack is opted out, so a stale file cannot survive past opt-out. Also aligns removeStackOverride to use path.basename consistently with all other override-path construction sites in the same file. Adds two tests: pilot node with a pushed file returns the path; pilot node with no file returns null. --- backend/src/__tests__/mesh-service.test.ts | 41 ++++++++++++++++++++++ backend/src/services/MeshService.ts | 19 ++++++++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/backend/src/__tests__/mesh-service.test.ts b/backend/src/__tests__/mesh-service.test.ts index 3cf012fa..8daf5f16 100644 --- a/backend/src/__tests__/mesh-service.test.ts +++ b/backend/src/__tests__/mesh-service.test.ts @@ -622,6 +622,47 @@ describe('MeshService.ensureStackOverride (BUG-1 fix)', () => { && /orphaned-stack/.test(e.message), )).toBe(true); }); + + it('returns a pushed override file on pilot nodes where isMeshStackEnabled is always false', async () => { + const svc = MeshService.getInstance(); + const db = DatabaseService.getInstance(); + const localNodeId = db.getNodes()[0].id; + + // Simulate the pilot scenario: no mesh_stacks row (isMeshStackEnabled → false), + // but the override file already exists on disk, pushed by central via D-1. + const dataDir = process.env.DATA_DIR as string; + const overrideDir = path.join(dataDir, 'mesh', 'overrides', String(localNodeId)); + fsSync.mkdirSync(overrideDir, { recursive: true }); + const overrideFile = path.join(overrideDir, 'pilot-stack.override.yml'); + fsSync.writeFileSync(overrideFile, [ + 'services:', + ' echo:', + ' networks:', + ' - sencho_mesh', + ' extra_hosts:', + ' - echo.pilot-stack.pilot.sencho:172.30.0.2', + 'networks:', + ' sencho_mesh:', + ' external: true', + ].join('\n'), 'utf8'); + + // No mesh_stacks row → isMeshStackEnabled returns false. + const result = await svc.ensureStackOverride(localNodeId, 'pilot-stack'); + expect(result).toBe(overrideFile); + + // Cleanup. + fsSync.unlinkSync(overrideFile); + }); + + it('returns null for pilot nodes when no pushed override file exists', async () => { + const svc = MeshService.getInstance(); + const db = DatabaseService.getInstance(); + const localNodeId = db.getNodes()[0].id; + + // No mesh_stacks row, no file on disk. + const result = await svc.ensureStackOverride(localNodeId, 'no-such-stack'); + expect(result).toBeNull(); + }); }); describe('MeshService tunnel-up regen (BUG-2)', () => { diff --git a/backend/src/services/MeshService.ts b/backend/src/services/MeshService.ts index e100b215..d848874f 100644 --- a/backend/src/services/MeshService.ts +++ b/backend/src/services/MeshService.ts @@ -592,13 +592,26 @@ export class MeshService extends EventEmitter implements MeshForwarderHost { public async ensureStackOverride(nodeId: number, stackName: string): Promise { if (!isValidStackName(stackName)) return null; const db = DatabaseService.getInstance(); - if (!db.isMeshStackEnabled(nodeId, stackName)) return null; + const dir = this.overrideDirFor(nodeId); + if (!db.isMeshStackEnabled(nodeId, stackName)) { + // Pilot nodes intentionally have no mesh_stacks rows (opt-in state + // lives on central per the C-3 design). Use file-presence as the + // fallback: if central pushed an override via applyLocalOverride, return + // that path so ComposeService picks it up on the next deploy. + const file = path.resolve(dir, `${path.basename(stackName)}.override.yml`); + if (!isPathWithinBase(file, dir)) return null; + try { + await fs.access(file); + return file; + } catch { + return null; + } + } if (!this.senchoIp) return null; const aliases: MeshAlias[] = Array.from(this.aliasCache.values()).map((a) => ({ host: a.host })); const serviceNames = await this.getDeclaredStackServiceNames(stackName, nodeId); - const dir = this.overrideDirFor(nodeId); await fs.mkdir(dir, { recursive: true }); // path.basename mirrors the applyLocalOverride pattern (and is // the form CodeQL's path-injection model recognizes). @@ -705,7 +718,7 @@ export class MeshService extends EventEmitter implements MeshForwarderHost { private async removeStackOverride(nodeId: number, stackName: string): Promise { if (!isValidStackName(stackName)) return; const dir = this.overrideDirFor(nodeId); - const file = path.resolve(dir, `${stackName}.override.yml`); + const file = path.resolve(dir, `${path.basename(stackName)}.override.yml`); if (!isPathWithinBase(file, dir)) return; try { await fs.unlink(file); } catch { /* ignore not-exist */ } }