mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-09-04 14:45:41 +00:00
fix(fleet-actions): stop-by-label works on Community remote nodes (#1270)
* fix(fleet-actions): stop-by-label works on Community remote nodes Fleet-stop's remote leg fanned out to POST /api/labels/:id/action, which is gated to Skipper/Admiral, so on a Community fleet the control node stopped its own stacks but every remote node returned 403. Fleet-stop itself is admin-only and available on every license, so the remote leg contradicted the feature's own gate. Extract the label-match plus bulk-stop logic into a shared runLocalLabelStop helper and add an admin-only, every-license POST /api/fleet-actions/labels/local-stop receiver. The control now fans out to that receiver, so remote stacks stop on every tier. Each node runs under its own per-node bulk lock, so a fleet-stop and a per-label action still serialize cleanly instead of double-stopping containers. Also degrade the control's own leg per-node instead of failing the whole fan-out when its filesystem read throws, and gate fleet-stop and fleet-prune diagnostics behind developer_mode. Tests: local-stop auth, tier, validation, and behavior; a remote-leg routing guard that asserts the fan-out targets local-stop and never the paid route; local-leg graceful degradation; and the three Fleet Action card UIs. * fix(fleet-actions): honor the remote stop receiver's matched flag The control reached the remote leg only because its own mirror had the label, then hardcoded matched:true and trusted results without guarding its shape. A mirror-skewed control (mirror has the label, remote does not) then showed a remote mismatch as "matched, 0 stacks" instead of "no matching label", and a malformed 200 body could flow a non-array into the per-stack renderers. Honor the remote's own matched flag and coerce results to an array when the body is malformed. Add regression tests for the matched:false skew case and the non-array results case.
This commit is contained in:
@@ -4,6 +4,8 @@ import { authMiddleware } from '../middleware/auth';
|
||||
import { requireAdmin, requireBody } from '../middleware/tierGates';
|
||||
import { isValidStackName } from '../utils/validation';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { runLocalLabelStop, type LabelLocalStopResponse } from '../helpers/fleetLabelStop';
|
||||
|
||||
// Per-node fleet-action endpoints. Mounted under `/api/fleet-actions/`, which
|
||||
// is NOT in `PROXY_EXEMPT_PREFIXES`, so when `x-node-id` targets a remote node
|
||||
@@ -66,3 +68,35 @@ fleetActionsRouter.post(
|
||||
res.json({ results });
|
||||
},
|
||||
);
|
||||
|
||||
// Per-node label-matched stop. A control instance calls this on each remote
|
||||
// node during a fleet-wide stop-by-label so the destructive work runs under the
|
||||
// remote's own admin auth and per-node bulk lock. Admin-only and available on
|
||||
// every license, matching the rest of the Fleet Actions surface. The paid
|
||||
// label-driven action lives at `POST /api/labels/:id/action`; this receiver is
|
||||
// the fleet-plumbing equivalent the control fans out to, so a fleet-stop on a
|
||||
// Community fleet stops remote stacks instead of 403'ing on the remote leg.
|
||||
fleetActionsRouter.post(
|
||||
'/labels/local-stop',
|
||||
authMiddleware,
|
||||
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 };
|
||||
if (typeof labelName !== 'string' || labelName.trim().length === 0) {
|
||||
res.status(400).json({ error: 'labelName is required' });
|
||||
return;
|
||||
}
|
||||
const nodeId = req.nodeId ?? 0;
|
||||
const trimmedLabel = labelName.trim();
|
||||
try {
|
||||
const outcome = await runLocalLabelStop(nodeId, trimmedLabel, dryRun === true);
|
||||
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);
|
||||
} catch (err) {
|
||||
console.error('[FleetActions] local-stop error:', { nodeId, labelName: trimmedLabel }, err);
|
||||
res.status(500).json({ error: getErrorMessage(err, 'Failed to run local label stop') });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user