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 express from 'express';
|
||||||
|
import http from 'http';
|
||||||
import pool from '../db/pool.js';
|
import pool from '../db/pool.js';
|
||||||
import { requireAuth } from '../middleware/auth.js';
|
import { requireAuth } from '../middleware/auth.js';
|
||||||
|
|
||||||
|
|
@ -18,6 +19,30 @@ function pickIp(reportedIp, reqIp) {
|
||||||
return reportedIp;
|
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
|
// GET / – list all registered cluster nodes with online status
|
||||||
router.get('/', async (req, res, next) => {
|
router.get('/', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -34,6 +59,45 @@ router.get('/', async (req, res, next) => {
|
||||||
} catch (err) { next(err); }
|
} 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)
|
// POST /heartbeat – upsert this node's registration (includes hardware capabilities)
|
||||||
router.post('/heartbeat', async (req, res, next) => {
|
router.post('/heartbeat', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue