248 lines
7.5 KiB
TypeScript
248 lines
7.5 KiB
TypeScript
|
|
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`;
|
||
|
|
}
|
||
|
|
}
|