fix: bind fleet stop-by-label to the exact confirmed nodes and stacks (#1506)

A fleet stop re-matched stacks by label name at execution, so a stack that
gained the label between the operator's preview and confirmation could be
stopped even though it never appeared in the confirmation. A confirmed node
that was deleted after the preview also vanished from the results, letting the
remaining successes read as a clean stop.

The confirm flow now sends the exact node and stack list resolved in the
preview. Each node's stop is bound to that set: only stacks that are still
label-matched and confirmed are stopped, and a confirmed node missing from the
registry is reported as an explicit failure rather than dropped.
This commit is contained in:
Anso
2026-06-28 16:33:01 -04:00
committed by GitHub
parent 78a742fb44
commit 1dc12f7da8
7 changed files with 333 additions and 59 deletions
+13 -2
View File
@@ -27,15 +27,26 @@ fleetActionsRouter.post(
async (req: Request, res: Response): Promise<void> => {
if (!requireAdmin(req, res)) return;
if (!requireBody(req, res)) return;
const { labelName, dryRun } = req.body as { labelName?: unknown; dryRun?: unknown };
const { labelName, dryRun, stackNames } = req.body as { labelName?: unknown; dryRun?: unknown; stackNames?: unknown };
if (typeof labelName !== 'string' || labelName.trim().length === 0) {
res.status(400).json({ error: 'labelName is required' });
return;
}
// Optional allowlist binding the stop to the stacks the control confirmed in
// its preview, so a stack labelled on this node after the preview is not
// stopped. Absent (e.g. a dry run) stops every currently label-matched stack.
let allowedStacks: Set<string> | undefined;
if (stackNames !== undefined) {
if (!Array.isArray(stackNames) || !stackNames.every(s => typeof s === 'string')) {
res.status(400).json({ error: 'stackNames must be an array of strings' });
return;
}
allowedStacks = new Set(stackNames as string[]);
}
const nodeId = req.nodeId ?? 0;
const trimmedLabel = labelName.trim();
try {
const outcome = await runLocalLabelStop(nodeId, trimmedLabel, dryRun === true);
const outcome = await runLocalLabelStop(nodeId, trimmedLabel, dryRun === true, allowedStacks);
if (isDebugEnabled()) console.debug('[FleetActions:debug] local-stop:', { nodeId, dryRun: dryRun === true, matched: outcome.matched, stacks: outcome.stackResults.length });
const body: LabelLocalStopResponse = { matched: outcome.matched, results: outcome.stackResults };
res.json(body);