mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-27 18:57:09 +00:00
fix(mesh): inspect remote stacks via HTTP proxy not Dockerode (#992)
MeshService.inspectStackServices used DockerController.getInstance(nodeId)
to enumerate compose-labeled containers, but NodeRegistry.getDocker
explicitly throws for any node with type='remote' by design. The throw
was silently caught and an empty array returned, so opt-in for any
remote node failed with a misleading "no running services" error and
refreshAliasCache only ever populated LOCAL aliases. Cross-node mesh
routing was therefore impossible end-to-end regardless of pilot-tunnel
state.
Split the inspector. inspectLocalStackServices keeps the Dockerode
listContainers path and always queries the local Docker daemon; it is
public so the new route can call it. inspectStackServices is now a
dispatcher: local nodes fall through to the Dockerode path, remote
nodes (proxy mode and pilot-agent) HTTP-fetch
/api/mesh/local-services/:stackName against the URL resolved by
NodeRegistry.getProxyTarget, with the persisted node_proxy Bearer
token and license tier headers attached. The remote's MeshService
enumerates its own LOCAL Docker daemon and returns the
{service, ports[]} envelope.
refreshAliasCache now inspects every opted-in stack in parallel via
Promise.allSettled so a slow or unreachable remote does not stall the
60-second loop. The new /api/mesh/local-services/:stackName route is
gated by requireAdmiral plus isValidStackName and always queries the
caller's own Sencho instance.
Together with PR #989 (proxy-side bridge dispatch) and PR #990
(agent-side loopback auth), this makes the central, bridge, agent,
local-Sencho mesh control plane functional end-to-end for both
proxy-mode and pilot-agent remotes.
This commit is contained in:
@@ -4,6 +4,7 @@ import { NodeRegistry } from '../services/NodeRegistry';
|
||||
import { MeshError, MeshService } from '../services/MeshService';
|
||||
import { requireAdmin, requireAdmiral } from '../middleware/tierGates';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
import { isValidStackName } from '../utils/validation';
|
||||
|
||||
export const meshRouter = Router();
|
||||
|
||||
@@ -49,6 +50,26 @@ meshRouter.post('/nodes/:nodeId/disable', async (req: Request, res: Response): P
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the LOCAL Docker daemon's services for a stack with their listening
|
||||
* ports. Always queries this Sencho instance's own Dockerode regardless of
|
||||
* `x-node-id`. Central calls this endpoint against each remote node via the
|
||||
* existing proxy chain (`NodeRegistry.getProxyTarget`) so it can build the
|
||||
* cross-fleet alias cache without violating the local-only Dockerode rule.
|
||||
*/
|
||||
meshRouter.get('/local-services/:stackName', async (req: Request, res: Response): Promise<void> => {
|
||||
if (!requireAdmiral(req, res)) return;
|
||||
const stackName = req.params.stackName as string;
|
||||
if (!isValidStackName(stackName)) { res.status(400).json({ error: 'Invalid stack name' }); return; }
|
||||
try {
|
||||
const services = await MeshService.getInstance().inspectLocalStackServices(stackName);
|
||||
res.json({ services });
|
||||
} catch (err) {
|
||||
console.warn('[mesh] /local-services failed:', sanitizeForLog((err as Error).message));
|
||||
res.status(500).json({ error: 'Failed to list local services' });
|
||||
}
|
||||
});
|
||||
|
||||
meshRouter.get('/nodes/:nodeId/stacks', async (req: Request, res: Response): Promise<void> => {
|
||||
if (!requireAdmiral(req, res)) return;
|
||||
const nodeId = Number.parseInt(req.params.nodeId as string, 10);
|
||||
|
||||
Reference in New Issue
Block a user