mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-19 14:56:27 +00:00
fix(mesh): enumerate stacks on remote pilot nodes for opt-in sheet (#1025)
The mesh opt-in sheet showed "No stacks deployed on this node yet" for every remote pilot because GET /api/mesh/nodes/:nodeId/stacks called FileSystemService.getInstance(nodeId).getStacks() unconditionally, which always reads central's own filesystem regardless of which node the operator targeted. Fix dispatches through the proxy chain when the targeted node is remote, mirroring the C-3 pattern that already governs every other mesh endpoint needing data from a remote Sencho: - New endpoint GET /api/mesh/local-stacks (Admiral-gated) returns the bare stacks list from THIS Sencho's own filesystem. Mirrors the precedent set by /api/mesh/local-services/:stackName. - New MeshService.listLocalStacks() and MeshService.listStacksOnNode() helpers; the latter dispatches local vs remote and degrades to an empty list on transport failure (no proxy target, non-2xx response, malformed body). - Refactored route delegates the stack-name list to listStacksOnNode; central's mesh_stacks DB is still authoritative for the opt-in flag set per C-3. Tests cover the local path, the remote-OK path with header forwarding, non-2xx, no-target (pilot tunnel down), malformed bodies, and unknown node ids.
This commit is contained in:
@@ -1031,6 +1031,44 @@ export class MeshService extends EventEmitter implements MeshForwarderHost {
|
||||
}
|
||||
}
|
||||
|
||||
public async listLocalStacks(): Promise<string[]> {
|
||||
const localNodeId = NodeRegistry.getInstance().getDefaultNodeId();
|
||||
return FileSystemService.getInstance(localNodeId).getStacks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Local nodes read the filesystem; remote nodes fetch their own
|
||||
* Sencho's `/api/mesh/local-stacks` because the remote's compose
|
||||
* directory is not visible from central (pilot's filesystem lives
|
||||
* on a different host).
|
||||
*/
|
||||
public async listStacksOnNode(nodeId: number): Promise<string[]> {
|
||||
const node = DatabaseService.getInstance().getNode(nodeId);
|
||||
if (!node) return [];
|
||||
if (node.type !== 'remote') return this.listLocalStacks();
|
||||
|
||||
try {
|
||||
const res = await this.proxyFetch(nodeId, 'GET', '/api/mesh/local-stacks', undefined, 5_000);
|
||||
if (!res.ok) {
|
||||
console.error(`[MeshService] listStacksOnNode: HTTP ${res.status} from node ${nodeId} (${sanitizeForLog(node.name)})`);
|
||||
return [];
|
||||
}
|
||||
const body = await res.json() as { stacks?: unknown };
|
||||
if (!Array.isArray(body.stacks)) return [];
|
||||
return body.stacks.filter((s): s is string => typeof s === 'string');
|
||||
} catch (err) {
|
||||
// proxyFetch throws MeshError('push_failed') when getProxyTarget
|
||||
// returns null (e.g. pilot tunnel offline). Treat the same as a
|
||||
// non-OK response: empty list.
|
||||
if (err instanceof MeshError && err.code === 'push_failed') {
|
||||
console.warn(`[MeshService] listStacksOnNode: no proxy target for node ${nodeId} (${sanitizeForLog(node.name)})`);
|
||||
return [];
|
||||
}
|
||||
console.error('[MeshService] listStacksOnNode remote unreachable:', sanitizeForLog((err as Error).message));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a `fetch` against a remote Sencho's API with the bearer token
|
||||
* and the proxy tier/variant headers in place. Centralizes the header
|
||||
|
||||
Reference in New Issue
Block a user