import Docker from 'dockerode'; import { logger } from '../utils/logger.js'; export interface GameServerConfig { name: string; image: string; ports: { [key: string]: number }; environment?: { [key: string]: string }; volumes?: string[]; restart?: string; } export interface ServerStatus { name: string; status: 'running' | 'stopped' | 'not-found'; containerId?: string; ports?: { [key: string]: number }; uptime?: string; } export class DockerManager { private docker: Docker; private gameServers: Map; constructor() { this.docker = new Docker(); this.gameServers = new Map(); this.initializeGameServers(); } private initializeGameServers() { // Define available game servers const servers: GameServerConfig[] = [ { name: 'minecraft', image: 'itzg/minecraft-server:latest', ports: { '25565': 25565 }, environment: { EULA: 'TRUE', TYPE: 'VANILLA', MEMORY: '2G' }, volumes: ['/data/minecraft:/data'], restart: 'unless-stopped' }, { name: 'valheim', image: 'lloesche/valheim-server:latest', ports: { '2456': 2456, '2457': 2457 }, environment: { SERVER_NAME: 'My Valheim Server', WORLD_NAME: 'MyWorld', SERVER_PASS: 'secret123' }, volumes: ['/data/valheim:/config'], restart: 'unless-stopped' }, { name: 'terraria', image: 'ryshe/terraria:latest', ports: { '7777': 7777 }, environment: { WORLD: 'MyWorld', PASSWORD: 'secret123' }, volumes: ['/data/terraria:/world'], restart: 'unless-stopped' } ]; servers.forEach(server => { this.gameServers.set(server.name, server); }); } async getRunningContainers(): Promise { try { const containers = await this.docker.listContainers(); return containers; } catch (error) { logger.error('Error listing containers:', error); return []; } } async getServerStatus(serviceName: string): Promise { try { const containers = await this.docker.listContainers({ all: true }); const container = containers.find(c => c.Names.some(name => name.includes(serviceName)) || c.Labels?.['game-server'] === serviceName ); if (!container) { return { name: serviceName, status: 'not-found' }; } const status = container.State === 'running' ? 'running' : 'stopped'; const ports: { [key: string]: number } = {}; if (container.Ports) { container.Ports.forEach(port => { if (port.PublicPort) { ports[port.PrivatePort.toString()] = port.PublicPort; } }); } return { name: serviceName, status, containerId: container.Id, ports, uptime: status === 'running' ? this.calculateUptime(container.Created) : undefined }; } catch (error) { logger.error(`Error getting status for ${serviceName}:`, error); return { name: serviceName, status: 'not-found' }; } } async startServer(serviceName: string): Promise<{ success: boolean; message: string }> { try { const config = this.gameServers.get(serviceName); if (!config) { return { success: false, message: `Unknown game server: ${serviceName}` }; } // Check if container already exists const status = await this.getServerStatus(serviceName); if (status.status === 'running') { return { success: false, message: `${serviceName} is already running` }; } if (status.containerId) { // Container exists but is stopped, start it const container = this.docker.getContainer(status.containerId); await container.start(); logger.info(`Started existing container for ${serviceName}`); } else { // Create new container const containerOptions = { Image: config.image, name: `gameserver-${serviceName}`, Labels: { 'game-server': serviceName }, Env: config.environment ? Object.entries(config.environment).map(([k, v]) => `${k}=${v}`) : [], ExposedPorts: Object.keys(config.ports).reduce((acc, port) => { acc[`${port}/tcp`] = {}; return acc; }, {} as any), HostConfig: { PortBindings: Object.entries(config.ports).reduce((acc, [privatePort, publicPort]) => { acc[`${privatePort}/tcp`] = [{ HostPort: publicPort.toString() }]; return acc; }, {} as any), Binds: config.volumes || [], RestartPolicy: { Name: config.restart || 'unless-stopped' } } }; const container = await this.docker.createContainer(containerOptions); await container.start(); logger.info(`Created and started new container for ${serviceName}`); } return { success: true, message: `${serviceName} started successfully` }; } catch (error) { logger.error(`Error starting ${serviceName}:`, error); return { success: false, message: `Failed to start ${serviceName}: ${error}` }; } } async stopServer(serviceName: string): Promise<{ success: boolean; message: string }> { try { const status = await this.getServerStatus(serviceName); if (status.status !== 'running') { return { success: false, message: `${serviceName} is not running` }; } if (status.containerId) { const container = this.docker.getContainer(status.containerId); await container.stop(); logger.info(`Stopped container for ${serviceName}`); return { success: true, message: `${serviceName} stopped successfully` }; } return { success: false, message: `Could not find container for ${serviceName}` }; } catch (error) { logger.error(`Error stopping ${serviceName}:`, error); return { success: false, message: `Failed to stop ${serviceName}: ${error}` }; } } async listGameServers(): Promise<{ available: string[]; running: ServerStatus[] }> { const available = Array.from(this.gameServers.keys()); const running: ServerStatus[] = []; for (const serverName of available) { const status = await this.getServerStatus(serverName); if (status.status === 'running') { running.push(status); } } return { available, running }; } async getSystemStats(): Promise { try { const info = await this.docker.info(); const version = await this.docker.version(); return { dockerVersion: version.Version, containers: info.Containers, containersRunning: info.ContainersRunning, containersPaused: info.ContainersPaused, containersStopped: info.ContainersStopped, images: info.Images, memoryLimit: info.MemoryLimit, swapLimit: info.SwapLimit, cpus: info.NCPU, architecture: info.Architecture }; } catch (error) { logger.error('Error getting system stats:', error); return null; } } private calculateUptime(created: number): string { const now = Date.now(); const uptime = now - (created * 1000); const hours = Math.floor(uptime / (1000 * 60 * 60)); const minutes = Math.floor((uptime % (1000 * 60 * 60)) / (1000 * 60)); return `${hours}h ${minutes}m`; } }