/** * SSH Stats API Endpoint * * GET /api/stats/:host/:container - Get real-time docker stats for a container */ import { Router } from 'express'; import { Client } from 'ssh2'; import { readFileSync } from 'fs'; import { homedir } from 'os'; import { getHostConfigs } from '../config'; import { StatsResponse } from '../types'; const router = Router(); interface SSHConnectionConfig { host: string; port?: number; username: string; privateKey?: Buffer; } async function connectSSH(config: SSHConnectionConfig, timeout: number = 30000): Promise { return new Promise((resolve, reject) => { const conn = new Client(); const timeoutId = setTimeout(() => { conn.end(); reject(new Error('Connection timeout')); }, timeout); conn.on('ready', () => { clearTimeout(timeoutId); resolve(conn); }); conn.on('error', (err) => { clearTimeout(timeoutId); reject(err); }); conn.connect({ host: config.host, port: config.port || 22, username: config.username, privateKey: config.privateKey, readyTimeout: timeout, }); }); } async function execSSH(conn: Client, command: string): Promise { return new Promise((resolve, reject) => { conn.exec(command, (err, stream) => { if (err) { reject(err); return; } let output = ''; stream.on('close', () => resolve(output)); stream.on('data', (data: Buffer) => { output += data.toString(); }); stream.stderr.on('data', (data: Buffer) => { output += data.toString(); }); }); }); } async function getContainerStats( hostIp: string, containerName: string, sshUser: string, sshKeyPath?: string, sshPort?: number ): Promise { // Load SSH key const keyPath = sshKeyPath || `${homedir()}/.ssh/id_ed25519`; let privateKey: Buffer | undefined; try { privateKey = readFileSync(keyPath); } catch { // If key file doesn't exist, try without it } const sshConfig: SSHConnectionConfig = { host: hostIp, port: sshPort || 22, username: sshUser, privateKey, }; const conn = await connectSSH(sshConfig, 30000); // Run docker stats with JSON format const dockerStatsCmd = `docker stats ${containerName} --no-stream --format '{"cpu":"{{.CPUPerc}}","mem":"{{.MemPerc}}","net":"{{.NetIO}}"}'`; const output = await execSSH(conn, dockerStatsCmd); conn.end(); // Parse the output try { const parsed = JSON.parse(output.trim()); // Parse CPU (remove % and convert to number) const cpu = parsed.cpu ? parseFloat(parsed.cpu.replace('%', '')) : 0; // Parse Memory (remove % and convert to number) const memory = parsed.mem ? parseFloat(parsed.mem.replace('%', '')) : 0; // Parse Network I/O (format: "1.2MB / 800kB" -> rx: "1.2MB", tx: "800kB") const netParts = parsed.net ? parsed.net.split(' / ') : ['0B', '0B']; const rx = netParts[0]?.trim() || '0B'; const tx = netParts[1]?.trim() || '0B'; return { cpu, memory, network: { rx, tx }, }; } catch (parseError) { return { cpu: 0, memory: 0, network: { rx: '0B', tx: '0B' }, error: 'Failed to parse stats output', }; } } // GET /api/stats/:host/:container router.get('/stats/:host/:container', async (req, res) => { try { const { host, container } = req.params; // Find host config by name const hosts = await getHostConfigs(); const hostConfig = hosts.find(h => h.name.toLowerCase() === host.toLowerCase()); if (!hostConfig) { const response: StatsResponse = { cpu: 0, memory: 0, network: { rx: '0B', tx: '0B' }, error: `Host '${host}' not found`, }; return res.status(404).json(response); } const stats = await getContainerStats( hostConfig.ip, container, hostConfig.sshUser, hostConfig.sshKeyPath, hostConfig.sshPort ); res.json(stats); } catch (error: any) { const response: StatsResponse = { cpu: 0, memory: 0, network: { rx: '0B', tx: '0B' }, error: error.message || 'Failed to get stats', }; res.status(500).json(response); } }); export default router;