feat: add /cluster/containers endpoint via Docker socket
Lists all containers on the local host; supports POST /containers/:id/restart. Falls back to [] gracefully if Docker socket is unavailable.
This commit is contained in:
parent
6f2de45819
commit
4afd0c7b21
1 changed files with 64 additions and 0 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import express from 'express';
|
||||
import http from 'http';
|
||||
import pool from '../db/pool.js';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
|
||||
|
|
@ -18,6 +19,30 @@ function pickIp(reportedIp, reqIp) {
|
|||
return reportedIp;
|
||||
}
|
||||
|
||||
function dockerRequest(path, method = 'GET', body = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const opts = {
|
||||
socketPath: '/var/run/docker.sock',
|
||||
path: `/v1.41${path}`,
|
||||
method,
|
||||
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
|
||||
};
|
||||
const req = http.request(opts, (res) => {
|
||||
let data = '';
|
||||
res.on('data', d => { data += d; });
|
||||
res.on('end', () => {
|
||||
if (!data.trim()) return resolve(null);
|
||||
try { resolve(JSON.parse(data)); }
|
||||
catch (e) { resolve(null); }
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.setTimeout(5000, () => { req.destroy(); reject(new Error('Docker socket timeout')); });
|
||||
if (body) req.write(JSON.stringify(body));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// GET / – list all registered cluster nodes with online status
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
|
|
@ -34,6 +59,45 @@ router.get('/', async (req, res, next) => {
|
|||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
// GET /containers – list all containers on the local Docker host
|
||||
router.get('/containers', async (req, res, next) => {
|
||||
try {
|
||||
const containers = await dockerRequest('/containers/json?all=true');
|
||||
if (!Array.isArray(containers)) return res.json([]);
|
||||
const out = containers.map(c => {
|
||||
const rawName = (c.Names[0] || '').replace(/^\//, '');
|
||||
const name = rawName.replace(/^wild-dragon-/, '').replace(/-\d+$/, '');
|
||||
const ports = (c.Ports || [])
|
||||
.filter(p => p.PublicPort)
|
||||
.map(p => `${p.PublicPort}→${p.PrivatePort}`)
|
||||
.join(', ');
|
||||
return {
|
||||
id: c.Id.slice(0, 12),
|
||||
name,
|
||||
image: (c.Image || '').replace(/^sha256:/, '').slice(0, 40),
|
||||
state: c.State,
|
||||
uptime: (c.Status || '').replace(/\s*\(.*\)/, '').trim(),
|
||||
healthy: (c.Status || '').includes('healthy'),
|
||||
ports,
|
||||
cpu: 0,
|
||||
mem: 0,
|
||||
};
|
||||
});
|
||||
res.json(out);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT' || err.code === 'EACCES') return res.json([]);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// POST /containers/:nameOrId/restart
|
||||
router.post('/containers/:nameOrId/restart', async (req, res, next) => {
|
||||
try {
|
||||
await dockerRequest(`/containers/${encodeURIComponent(req.params.nameOrId)}/restart`, 'POST');
|
||||
res.json({ ok: true });
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
// POST /heartbeat – upsert this node's registration (includes hardware capabilities)
|
||||
router.post('/heartbeat', async (req, res, next) => {
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in a new issue