/** * SSH Config API Endpoint * * GET /api/config/:host/:container - Get docker-compose config for a specific container */ import { Router } from 'express'; import { Client } from 'ssh2'; import { readFileSync } from 'fs'; import { homedir } from 'os'; import { getHostConfigs } from '../config'; import { ConfigResponse } 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(); }); }); }); } // Find docker-compose.yml files on the remote host async function findDockerComposeFiles(conn: Client): Promise { const command = `find /home -name "docker-compose.yml" 2>/dev/null | head -10`; const output = await execSSH(conn, command); const files = output .split('\n') .map(f => f.trim()) .filter(f => f.length > 0); // Also check common locations const commonPaths = [ `${homedir()}/docker-compose.yml`, '/opt/docker-compose.yml', '/root/docker-compose.yml' ]; return [...new Set([...files, ...commonPaths])]; } // Extract a specific service from docker-compose.yml function extractServiceFromYaml(fullYaml: string, serviceName: string): string { const lines = fullYaml.split('\n'); let inService = false; let serviceLines: string[] = []; let indentLevel = 0; for (const line of lines) { // Check if we're starting the target service if (line.match(new RegExp(`^\\s+${serviceName}:`)) || line.match(new RegExp(`^${serviceName}:`))) { inService = true; indentLevel = line.match(/^(\s*)/)![1].length; serviceLines.push(line); continue; } if (inService) { const currentIndent = line.match(/^(\s*)/)![1].length; // If we've dedented back to or past the service level, we're done if (line.trim() && currentIndent <= indentLevel) { // Check if this is another top-level key (services, volumes, networks) if (line.match(/^(services|volumes|networks):/)) { break; } // If this is another service, we're done if (!line.startsWith(' ')) { break; } } serviceLines.push(line); } } if (serviceLines.length === 0) { return `# Service "${serviceName}" not found in docker-compose.yml`; } return serviceLines.join('\n'); } // Read docker-compose.yml from remote host and extract service async function getContainerConfig(conn: Client, containerName: string): Promise { const files = await findDockerComposeFiles(conn); for (const filePath of files) { try { const command = `cat "${filePath}"`; const content = await execSSH(conn, command); if (content && content.includes(containerName)) { const serviceConfig = extractServiceFromYaml(content, containerName); return { yaml: serviceConfig, path: filePath }; } } catch (err) { console.error(`Error reading ${filePath}:`, err); continue; } } return { yaml: '', path: '', error: `No docker-compose.yml found with service "${containerName}"` }; } // GET /api/config/:host/:container router.get('/config/:host/:container', async (req, res) => { const { host, container } = req.params; console.log(`Fetching config for ${container} on ${host}`); try { // Find host config const hostConfigs = getHostConfigs(); const hostConfig = hostConfigs.find(h => h.name === host); if (!hostConfig) { return res.status(404).json({ yaml: '', path: '', error: `Host "${host}" not found in configuration` }); } // Read SSH key let privateKey: Buffer | undefined; if (hostConfig.sshKeyPath) { const keyPath = hostConfig.sshKeyPath.replace(/^~/, homedir()); try { privateKey = readFileSync(keyPath); } catch (err) { return res.status(500).json({ yaml: '', path: '', error: `Failed to read SSH key: ${err}` }); } } // Connect to host via SSH const conn = await connectSSH({ host: hostConfig.ip, port: hostConfig.sshPort || 22, username: hostConfig.sshUser, privateKey }, 30000); // Get container config const config = await getContainerConfig(conn, container); conn.end(); res.json(config); } catch (err: any) { console.error('Error fetching config:', err); res.status(500).json({ yaml: '', path: '', error: err.message || 'Failed to fetch config' }); } }); export default router;