update
This commit is contained in:
489
ts/classes/docker.ts
Normal file
489
ts/classes/docker.ts
Normal file
@@ -0,0 +1,489 @@
|
||||
/**
|
||||
* Docker Manager for Onebox
|
||||
*
|
||||
* Handles all Docker operations: containers, images, networks, volumes
|
||||
*/
|
||||
|
||||
import * as plugins from '../plugins.ts';
|
||||
import type { IService, IContainerStats } from '../types.ts';
|
||||
import { logger } from '../logging.ts';
|
||||
|
||||
export class OneboxDockerManager {
|
||||
private dockerClient: plugins.docker.Docker | null = null;
|
||||
private networkName = 'onebox-network';
|
||||
|
||||
/**
|
||||
* Initialize Docker client and create onebox network
|
||||
*/
|
||||
async init(): Promise<void> {
|
||||
try {
|
||||
// Initialize Docker client (connects to /var/run/docker.sock by default)
|
||||
this.dockerClient = new plugins.docker.Docker({
|
||||
socketPath: '/var/run/docker.sock',
|
||||
});
|
||||
|
||||
// Start the Docker client
|
||||
await this.dockerClient.start();
|
||||
|
||||
logger.info('Docker client initialized');
|
||||
|
||||
// Ensure onebox network exists
|
||||
await this.ensureNetwork();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to initialize Docker client: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure onebox network exists
|
||||
*/
|
||||
private async ensureNetwork(): Promise<void> {
|
||||
try {
|
||||
const networks = await this.dockerClient!.getNetworks();
|
||||
const existingNetwork = networks.find((n: any) => n.name === this.networkName);
|
||||
|
||||
if (!existingNetwork) {
|
||||
logger.info(`Creating Docker network: ${this.networkName}`);
|
||||
await this.dockerClient!.createNetwork({
|
||||
Name: this.networkName,
|
||||
Driver: 'bridge',
|
||||
Labels: {
|
||||
'managed-by': 'onebox',
|
||||
},
|
||||
});
|
||||
logger.success(`Docker network created: ${this.networkName}`);
|
||||
} else {
|
||||
logger.debug(`Docker network already exists: ${this.networkName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to create Docker network: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull an image from a registry
|
||||
*/
|
||||
async pullImage(image: string, registry?: string): Promise<void> {
|
||||
try {
|
||||
logger.info(`Pulling Docker image: ${image}`);
|
||||
|
||||
const fullImage = registry ? `${registry}/${image}` : image;
|
||||
|
||||
await this.dockerClient!.pull(fullImage, (error: any, stream: any) => {
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Follow progress
|
||||
this.dockerClient!.modem.followProgress(stream, (err: any, output: any) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
logger.debug('Pull complete:', output);
|
||||
});
|
||||
});
|
||||
|
||||
logger.success(`Image pulled successfully: ${fullImage}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to pull image ${image}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and start a container
|
||||
*/
|
||||
async createContainer(service: IService): Promise<string> {
|
||||
try {
|
||||
logger.info(`Creating container for service: ${service.name}`);
|
||||
|
||||
const fullImage = service.registry
|
||||
? `${service.registry}/${service.image}`
|
||||
: service.image;
|
||||
|
||||
// Prepare environment variables
|
||||
const env: string[] = [];
|
||||
for (const [key, value] of Object.entries(service.envVars)) {
|
||||
env.push(`${key}=${value}`);
|
||||
}
|
||||
|
||||
// Create container
|
||||
const container = await this.dockerClient!.createContainer({
|
||||
Image: fullImage,
|
||||
name: `onebox-${service.name}`,
|
||||
Env: env,
|
||||
Labels: {
|
||||
'managed-by': 'onebox',
|
||||
'onebox-service': service.name,
|
||||
},
|
||||
ExposedPorts: {
|
||||
[`${service.port}/tcp`]: {},
|
||||
},
|
||||
HostConfig: {
|
||||
NetworkMode: this.networkName,
|
||||
RestartPolicy: {
|
||||
Name: 'unless-stopped',
|
||||
},
|
||||
PortBindings: {
|
||||
// Don't bind to host ports - nginx will proxy
|
||||
[`${service.port}/tcp`]: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const containerID = container.id;
|
||||
logger.success(`Container created: ${containerID}`);
|
||||
|
||||
return containerID;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to create container for ${service.name}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a container by ID
|
||||
*/
|
||||
async startContainer(containerID: string): Promise<void> {
|
||||
try {
|
||||
logger.info(`Starting container: ${containerID}`);
|
||||
|
||||
const container = this.dockerClient!.getContainer(containerID);
|
||||
await container.start();
|
||||
|
||||
logger.success(`Container started: ${containerID}`);
|
||||
} catch (error) {
|
||||
// Ignore "already started" errors
|
||||
if (error.message.includes('already started')) {
|
||||
logger.debug(`Container already running: ${containerID}`);
|
||||
return;
|
||||
}
|
||||
logger.error(`Failed to start container ${containerID}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a container by ID
|
||||
*/
|
||||
async stopContainer(containerID: string): Promise<void> {
|
||||
try {
|
||||
logger.info(`Stopping container: ${containerID}`);
|
||||
|
||||
const container = this.dockerClient!.getContainer(containerID);
|
||||
await container.stop();
|
||||
|
||||
logger.success(`Container stopped: ${containerID}`);
|
||||
} catch (error) {
|
||||
// Ignore "already stopped" errors
|
||||
if (error.message.includes('already stopped') || error.statusCode === 304) {
|
||||
logger.debug(`Container already stopped: ${containerID}`);
|
||||
return;
|
||||
}
|
||||
logger.error(`Failed to stop container ${containerID}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart a container by ID
|
||||
*/
|
||||
async restartContainer(containerID: string): Promise<void> {
|
||||
try {
|
||||
logger.info(`Restarting container: ${containerID}`);
|
||||
|
||||
const container = this.dockerClient!.getContainer(containerID);
|
||||
await container.restart();
|
||||
|
||||
logger.success(`Container restarted: ${containerID}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to restart container ${containerID}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a container by ID
|
||||
*/
|
||||
async removeContainer(containerID: string, force = false): Promise<void> {
|
||||
try {
|
||||
logger.info(`Removing container: ${containerID}`);
|
||||
|
||||
const container = this.dockerClient!.getContainer(containerID);
|
||||
|
||||
// Stop first if not forced
|
||||
if (!force) {
|
||||
try {
|
||||
await this.stopContainer(containerID);
|
||||
} catch (error) {
|
||||
// Ignore stop errors
|
||||
logger.debug(`Error stopping container before removal: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
await container.remove({ force });
|
||||
|
||||
logger.success(`Container removed: ${containerID}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to remove container ${containerID}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get container status
|
||||
*/
|
||||
async getContainerStatus(containerID: string): Promise<string> {
|
||||
try {
|
||||
const container = this.dockerClient!.getContainer(containerID);
|
||||
const info = await container.inspect();
|
||||
|
||||
return info.State.Status;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get container status ${containerID}: ${error.message}`);
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get container stats (CPU, memory, network)
|
||||
*/
|
||||
async getContainerStats(containerID: string): Promise<IContainerStats | null> {
|
||||
try {
|
||||
const container = this.dockerClient!.getContainer(containerID);
|
||||
const stats = await container.stats({ stream: false });
|
||||
|
||||
// Calculate CPU percentage
|
||||
const cpuDelta =
|
||||
stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
|
||||
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
|
||||
const cpuPercent =
|
||||
systemDelta > 0 ? (cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100 : 0;
|
||||
|
||||
// Memory stats
|
||||
const memoryUsed = stats.memory_stats.usage || 0;
|
||||
const memoryLimit = stats.memory_stats.limit || 0;
|
||||
const memoryPercent = memoryLimit > 0 ? (memoryUsed / memoryLimit) * 100 : 0;
|
||||
|
||||
// Network stats
|
||||
let networkRx = 0;
|
||||
let networkTx = 0;
|
||||
if (stats.networks) {
|
||||
for (const network of Object.values(stats.networks)) {
|
||||
networkRx += (network as any).rx_bytes || 0;
|
||||
networkTx += (network as any).tx_bytes || 0;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
cpuPercent,
|
||||
memoryUsed,
|
||||
memoryLimit,
|
||||
memoryPercent,
|
||||
networkRx,
|
||||
networkTx,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get container stats ${containerID}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get container logs
|
||||
*/
|
||||
async getContainerLogs(
|
||||
containerID: string,
|
||||
tail = 100
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
try {
|
||||
const container = this.dockerClient!.getContainer(containerID);
|
||||
const logs = await container.logs({
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
tail,
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
// Parse logs (Docker returns them in a special format)
|
||||
const stdout: string[] = [];
|
||||
const stderr: string[] = [];
|
||||
|
||||
const lines = logs.toString().split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.length === 0) continue;
|
||||
|
||||
// Docker log format: first byte indicates stream (1=stdout, 2=stderr)
|
||||
const streamType = line.charCodeAt(0);
|
||||
const content = line.slice(8); // Skip header (8 bytes)
|
||||
|
||||
if (streamType === 1) {
|
||||
stdout.push(content);
|
||||
} else if (streamType === 2) {
|
||||
stderr.push(content);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
stdout: stdout.join('\n'),
|
||||
stderr: stderr.join('\n'),
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get container logs ${containerID}: ${error.message}`);
|
||||
return { stdout: '', stderr: '' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream container logs (real-time)
|
||||
*/
|
||||
async streamContainerLogs(
|
||||
containerID: string,
|
||||
callback: (line: string, isError: boolean) => void
|
||||
): Promise<void> {
|
||||
try {
|
||||
const container = this.dockerClient!.getContainer(containerID);
|
||||
const stream = await container.logs({
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
follow: true,
|
||||
tail: 0,
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
stream.on('data', (chunk: Buffer) => {
|
||||
const streamType = chunk[0];
|
||||
const content = chunk.slice(8).toString();
|
||||
callback(content, streamType === 2);
|
||||
});
|
||||
|
||||
stream.on('error', (error: Error) => {
|
||||
logger.error(`Log stream error for ${containerID}: ${error.message}`);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to stream container logs ${containerID}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all onebox-managed containers
|
||||
*/
|
||||
async listContainers(): Promise<any[]> {
|
||||
try {
|
||||
const containers = await this.dockerClient!.getContainers();
|
||||
// Filter for onebox-managed containers
|
||||
return containers.filter((c: any) =>
|
||||
c.labels && c.labels['managed-by'] === 'onebox'
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to list containers: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Docker is running
|
||||
*/
|
||||
async isDockerRunning(): Promise<boolean> {
|
||||
try {
|
||||
await this.dockerClient!.ping();
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Docker version info
|
||||
*/
|
||||
async getDockerVersion(): Promise<any> {
|
||||
try {
|
||||
return await this.dockerClient!.version();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get Docker version: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prune unused images
|
||||
*/
|
||||
async pruneImages(): Promise<void> {
|
||||
try {
|
||||
logger.info('Pruning unused Docker images...');
|
||||
await this.dockerClient!.pruneImages();
|
||||
logger.success('Unused images pruned successfully');
|
||||
} catch (error) {
|
||||
logger.error(`Failed to prune images: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get container IP address in onebox network
|
||||
*/
|
||||
async getContainerIP(containerID: string): Promise<string | null> {
|
||||
try {
|
||||
const container = this.dockerClient!.getContainer(containerID);
|
||||
const info = await container.inspect();
|
||||
|
||||
const networks = info.NetworkSettings.Networks;
|
||||
if (networks && networks[this.networkName]) {
|
||||
return networks[this.networkName].IPAddress;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get container IP ${containerID}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command in a running container
|
||||
*/
|
||||
async execInContainer(
|
||||
containerID: string,
|
||||
cmd: string[]
|
||||
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
||||
try {
|
||||
const container = this.dockerClient!.getContainer(containerID);
|
||||
|
||||
const exec = await container.exec({
|
||||
Cmd: cmd,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
});
|
||||
|
||||
const stream = await exec.start({ Detach: false });
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
stream.on('data', (chunk: Buffer) => {
|
||||
const streamType = chunk[0];
|
||||
const content = chunk.slice(8).toString();
|
||||
|
||||
if (streamType === 1) {
|
||||
stdout += content;
|
||||
} else if (streamType === 2) {
|
||||
stderr += content;
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for completion
|
||||
await new Promise((resolve) => stream.on('end', resolve));
|
||||
|
||||
const inspect = await exec.inspect();
|
||||
const exitCode = inspect.ExitCode || 0;
|
||||
|
||||
return { stdout, stderr, exitCode };
|
||||
} catch (error) {
|
||||
logger.error(`Failed to exec in container ${containerID}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user