arc-frp/node/src/services/dockerManager.ts

248 lines
7.5 KiB
TypeScript
Raw Normal View History

2025-07-03 15:50:13 -04:00
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<string, GameServerConfig>;
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<any[]> {
try {
const containers = await this.docker.listContainers();
return containers;
} catch (error) {
logger.error('Error listing containers:', error);
return [];
}
}
async getServerStatus(serviceName: string): Promise<ServerStatus> {
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<any> {
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`;
}
}