fix(mesh): cascade opt-out when a stack is deleted (F-1 / F-14) (#1096)

DELETE /api/stacks/:name now calls MeshService.optOutStack after the
DB cleanup so the mesh_stacks row, the override file under
<DATA_DIR>/mesh/overrides/<nodeId>/, and any derived aliases do not
outlive the deleted stack. Pre-fix, those artifacts leaked and the
reconcile loop logged "No compose file found for stack" every tick.

optOutStack is idempotent (early return when the stack was never
opted in) and already cascades override-regen plus recompose across
the rest of the fleet, so peers' /etc/hosts drop the dropped alias.
The new cascade call is best-effort relative to the delete itself:
mesh cleanup failures warn but never regress the delete contract.

New regression test asserts three contracts: cascade on delete,
no-op on never-meshed, and 200 + warn when the cascade rejects.
This commit is contained in:
Anso
2026-05-18 00:14:10 -04:00
committed by GitHub
parent d9b4f71e0d
commit 9f238e187c
2 changed files with 154 additions and 0 deletions
+19
View File
@@ -6,6 +6,7 @@ import { FileSystemService } from '../services/FileSystemService';
import { ComposeService, getComposeRollbackInfo } from '../services/ComposeService';
import DockerController from '../services/DockerController';
import { DatabaseService } from '../services/DatabaseService';
import { MeshService } from '../services/MeshService';
import { CacheService } from '../services/CacheService';
import { UpdatePreviewService } from '../services/UpdatePreviewService';
import { GitSourceService, GitSourceError, repoHost as gitRepoHost } from '../services/GitSourceService';
@@ -562,6 +563,24 @@ stacksRouter.delete('/:stackName', async (req: Request, res: Response) => {
DatabaseService.getInstance().deleteRoleAssignmentsByResource('stack', stackName);
DatabaseService.getInstance().deleteGitSource(stackName);
// Cascade a mesh opt-out so the mesh_stacks row, override file on disk,
// and derived aliases do not outlive the stack. optOutStack is idempotent
// (no-op when the stack was never opted in). Best-effort: a mesh cleanup
// failure must not regress the delete itself.
try {
await MeshService.getInstance().optOutStack(
req.nodeId,
stackName,
req.user?.username ?? 'system',
);
} catch (meshErr) {
console.warn(
'[Stacks] Mesh opt-out cascade failed for %s, continuing delete:',
sanitizeForLog(stackName),
meshErr,
);
}
if (fsErr) throw fsErr;
invalidateNodeCaches(req.nodeId);