Initial commit: Onebox v1.0.0

- Complete Deno-based architecture following nupst/spark patterns
- SQLite database with full schema
- Docker container management
- Service orchestration (Docker + Nginx + DNS + SSL)
- Registry authentication
- Nginx reverse proxy configuration
- Cloudflare DNS integration
- Let's Encrypt SSL automation
- Background daemon with metrics collection
- HTTP API server
- Comprehensive CLI
- Cross-platform compilation setup
- NPM distribution wrapper
- Shell installer script

Core features:
- Deploy containers with single command
- Automatic domain configuration
- Automatic SSL certificates
- Multi-registry support
- Metrics and logging
- Systemd integration

Ready for Angular UI implementation and testing.
This commit is contained in:
2025-10-28 13:05:42 +00:00
commit 246a6073e0
29 changed files with 5227 additions and 0 deletions

489
ts/onebox.classes.docker.ts Normal file
View File

@@ -0,0 +1,489 @@
/**
* Docker Manager for Onebox
*
* Handles all Docker operations: containers, images, networks, volumes
*/
import * as plugins from './onebox.plugins.ts';
import type { IService, IContainerStats } from './onebox.types.ts';
import { logger } from './onebox.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',
});
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!.listNetworks();
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!.listContainers({
all: true,
filters: {
label: ['managed-by=onebox'],
},
});
return containers;
} 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;
}
}
}