feat(fleet): cross-node bulk label assign with authoritative label discovery (#1389)

* feat(fleet): cross-node bulk label assign with authoritative label discovery

Make Fleet Actions > Bulk label assign work across the fleet. Pick a stack
label that exists anywhere in the fleet, select stacks on one or more nodes,
and the control orchestrates: each target node resolves the label by name,
creating it with the same name and color if missing, then adds it to the
selected stacks while preserving their existing labels. The local node runs
in process; each remote runs its own admin-only local-assign receiver over
the node proxy. Per-node failures (unknown node, no proxy target, unreachable,
mixed-version remote) degrade that node only and are reported per node in the
result. Assignment writes use a transactional INSERT OR IGNORE so the
add-preserve path is idempotent and race-free.

Also make the shared fleet label discovery authoritative: suggestions,
match-preview, and the fleet-stop remote leg now read each node's labels live
over the proxy instead of the control database, which does not mirror remote
labels. A propagated label therefore appears in, and is stoppable by,
Stop-by-label across the fleet, and unreachable nodes are surfaced rather than
silently dropped.

Fleet Actions runs against the unfiltered node list, so overview filters no
longer narrow its scope. The previous node-scoped, replace-by-id bulk-assign
endpoint is removed.

* fix(fleet): treat malformed remote label responses as per-node failures

A 200 response from a remote node whose body is not the expected shape was
treated as a benign empty result, so a malformed remote could read as a clean
zero-stack assign or a "matched, nothing to stop" no-op and even surface a
success toast. Validate the wire shape in the bulk-assign and fleet-stop remote
legs and in the authoritative label discovery fan-out; on a malformed body,
report the node as a per-node failure with the error attributed to its stacks
instead of silently dropping it.

* chore: drop accidentally committed temp file
This commit is contained in:
Anso
2026-06-20 11:52:28 -04:00
committed by GitHub
parent cd9247db1e
commit d26ab58189
13 changed files with 1209 additions and 338 deletions
+27
View File
@@ -5343,6 +5343,33 @@ export class DatabaseService {
txn();
}
/**
* Add labels to a stack without disturbing its existing assignments. Unlike
* `setStackLabels` (replace), this is additive: validation and
* `INSERT OR IGNORE` run in one transaction and the
* `(label_id, stack_name, node_id)` primary key makes re-adds idempotent, so
* two additive writes to the same stack never drop each other's labels. A
* concurrent `setStackLabels` (replace) still wins last-writer, which is the
* intended replace semantics, not a lost update.
*/
public addStackLabels(stackName: string, nodeId: number, labelIds: number[]): void {
if (labelIds.length === 0) return;
const txn = this.db.transaction(() => {
const placeholders = labelIds.map(() => '?').join(',');
const validCount = this.db.prepare(
`SELECT COUNT(*) as cnt FROM stack_labels WHERE id IN (${placeholders}) AND node_id = ?`
).get(...labelIds, nodeId) as { cnt: number };
if (validCount.cnt !== labelIds.length) {
throw new Error('One or more label IDs are invalid for this node');
}
const insert = this.db.prepare('INSERT OR IGNORE INTO stack_label_assignments (label_id, stack_name, node_id) VALUES (?, ?, ?)');
for (const labelId of labelIds) {
insert.run(labelId, stackName, nodeId);
}
});
txn();
}
public getLabelsForStacks(nodeId: number): Record<string, Label[]> {
const rows = this.db.prepare(`
SELECT a.stack_name, l.id, l.node_id, l.name, l.color