/** * Services Manager for Onebox * * Orchestrates service deployment: Docker + Nginx + DNS + SSL */ import type { IService, IServiceDeployOptions } from '../types.ts'; import { logger } from '../logging.ts'; import { OneboxDatabase } from './database.ts'; import { OneboxDockerManager } from './docker.ts'; export class OneboxServicesManager { private oneboxRef: any; // Will be Onebox instance private database: OneboxDatabase; private docker: OneboxDockerManager; constructor(oneboxRef: any) { this.oneboxRef = oneboxRef; this.database = oneboxRef.database; this.docker = oneboxRef.docker; } /** * Deploy a new service (full workflow) */ async deployService(options: IServiceDeployOptions): Promise { try { logger.info(`Deploying service: ${options.name}`); // Check if service already exists const existing = this.database.getServiceByName(options.name); if (existing) { throw new Error(`Service already exists: ${options.name}`); } // Create service record in database const service = await this.database.createService({ name: options.name, image: options.image, registry: options.registry, envVars: options.envVars || {}, port: options.port, domain: options.domain, status: 'stopped', createdAt: Date.now(), updatedAt: Date.now(), }); // Pull image await this.docker.pullImage(options.image, options.registry); // Create container const containerID = await this.docker.createContainer(service); // Update service with container ID this.database.updateService(service.id!, { containerID, status: 'starting', }); // Start container await this.docker.startContainer(containerID); // Update status this.database.updateService(service.id!, { status: 'running' }); // If domain is specified, configure nginx, DNS, and SSL if (options.domain) { logger.info(`Configuring domain: ${options.domain}`); // Configure DNS (if autoDNS is enabled) if (options.autoDNS !== false) { try { await this.oneboxRef.dns.addDNSRecord(options.domain); } catch (error) { logger.warn(`Failed to configure DNS for ${options.domain}: ${error.message}`); } } // Configure reverse proxy try { await this.oneboxRef.reverseProxy.addRoute(service.id!, options.domain, options.port); } catch (error) { logger.warn(`Failed to configure reverse proxy for ${options.domain}: ${error.message}`); } // Configure SSL (if autoSSL is enabled) if (options.autoSSL !== false) { try { await this.oneboxRef.ssl.obtainCertificate(options.domain); await this.oneboxRef.reverseProxy.reloadCertificates(); } catch (error) { logger.warn(`Failed to obtain SSL certificate for ${options.domain}: ${error.message}`); } } } logger.success(`Service deployed successfully: ${options.name}`); return this.database.getServiceByName(options.name)!; } catch (error) { logger.error(`Failed to deploy service ${options.name}: ${error.message}`); throw error; } } /** * Start a service */ async startService(name: string): Promise { try { const service = this.database.getServiceByName(name); if (!service) { throw new Error(`Service not found: ${name}`); } if (!service.containerID) { throw new Error(`Service ${name} has no container ID`); } logger.info(`Starting service: ${name}`); this.database.updateService(service.id!, { status: 'starting' }); await this.docker.startContainer(service.containerID); this.database.updateService(service.id!, { status: 'running' }); logger.success(`Service started: ${name}`); } catch (error) { logger.error(`Failed to start service ${name}: ${error.message}`); this.database.updateService( this.database.getServiceByName(name)?.id!, { status: 'failed' } ); throw error; } } /** * Stop a service */ async stopService(name: string): Promise { try { const service = this.database.getServiceByName(name); if (!service) { throw new Error(`Service not found: ${name}`); } if (!service.containerID) { throw new Error(`Service ${name} has no container ID`); } logger.info(`Stopping service: ${name}`); this.database.updateService(service.id!, { status: 'stopping' }); await this.docker.stopContainer(service.containerID); this.database.updateService(service.id!, { status: 'stopped' }); logger.success(`Service stopped: ${name}`); } catch (error) { logger.error(`Failed to stop service ${name}: ${error.message}`); throw error; } } /** * Restart a service */ async restartService(name: string): Promise { try { const service = this.database.getServiceByName(name); if (!service) { throw new Error(`Service not found: ${name}`); } if (!service.containerID) { throw new Error(`Service ${name} has no container ID`); } logger.info(`Restarting service: ${name}`); await this.docker.restartContainer(service.containerID); this.database.updateService(service.id!, { status: 'running' }); logger.success(`Service restarted: ${name}`); } catch (error) { logger.error(`Failed to restart service ${name}: ${error.message}`); throw error; } } /** * Remove a service (full cleanup) */ async removeService(name: string): Promise { try { const service = this.database.getServiceByName(name); if (!service) { throw new Error(`Service not found: ${name}`); } logger.info(`Removing service: ${name}`); // Stop and remove container if (service.containerID) { try { await this.docker.removeContainer(service.containerID, true); } catch (error) { logger.warn(`Failed to remove container: ${error.message}`); } } // Remove reverse proxy route if (service.domain) { try { this.oneboxRef.reverseProxy.removeRoute(service.domain); } catch (error) { logger.warn(`Failed to remove reverse proxy route: ${error.message}`); } // Note: We don't remove DNS records or SSL certs automatically // as they might be used by other services or need manual cleanup } // Remove from database this.database.deleteService(service.id!); logger.success(`Service removed: ${name}`); } catch (error) { logger.error(`Failed to remove service ${name}: ${error.message}`); throw error; } } /** * List all services */ listServices(): IService[] { return this.database.getAllServices(); } /** * Get service by name */ getService(name: string): IService | null { return this.database.getServiceByName(name); } /** * Get service logs */ async getServiceLogs(name: string, tail = 100): Promise { try { const service = this.database.getServiceByName(name); if (!service) { throw new Error(`Service not found: ${name}`); } if (!service.containerID) { throw new Error(`Service ${name} has no container ID`); } const logs = await this.docker.getContainerLogs(service.containerID, tail); return `=== STDOUT ===\n${logs.stdout}\n\n=== STDERR ===\n${logs.stderr}`; } catch (error) { logger.error(`Failed to get logs for service ${name}: ${error.message}`); throw error; } } /** * Stream service logs (real-time) */ async streamServiceLogs( name: string, callback: (line: string, isError: boolean) => void ): Promise { try { const service = this.database.getServiceByName(name); if (!service) { throw new Error(`Service not found: ${name}`); } if (!service.containerID) { throw new Error(`Service ${name} has no container ID`); } await this.docker.streamContainerLogs(service.containerID, callback); } catch (error) { logger.error(`Failed to stream logs for service ${name}: ${error.message}`); throw error; } } /** * Get service metrics */ async getServiceMetrics(name: string) { try { const service = this.database.getServiceByName(name); if (!service) { throw new Error(`Service not found: ${name}`); } if (!service.containerID) { throw new Error(`Service ${name} has no container ID`); } const stats = await this.docker.getContainerStats(service.containerID); return stats; } catch (error) { logger.error(`Failed to get metrics for service ${name}: ${error.message}`); return null; } } /** * Get service status */ async getServiceStatus(name: string): Promise { try { const service = this.database.getServiceByName(name); if (!service) { return 'not-found'; } if (!service.containerID) { return service.status; } const status = await this.docker.getContainerStatus(service.containerID); return status; } catch (error) { logger.error(`Failed to get status for service ${name}: ${error.message}`); return 'unknown'; } } /** * Update service environment variables */ async updateServiceEnv(name: string, envVars: Record): Promise { try { const service = this.database.getServiceByName(name); if (!service) { throw new Error(`Service not found: ${name}`); } // Update database this.database.updateService(service.id!, { envVars }); // Note: Requires container restart to take effect logger.info(`Environment variables updated for ${name}. Restart service to apply changes.`); } catch (error) { logger.error(`Failed to update env vars for service ${name}: ${error.message}`); throw error; } } /** * Sync service status from Docker */ async syncServiceStatus(name: string): Promise { try { const service = this.database.getServiceByName(name); if (!service || !service.containerID) { return; } const status = await this.docker.getContainerStatus(service.containerID); // Map Docker status to our status let ourStatus: IService['status'] = 'stopped'; if (status === 'running') { ourStatus = 'running'; } else if (status === 'exited' || status === 'dead') { ourStatus = 'stopped'; } else if (status === 'created') { ourStatus = 'stopped'; } else if (status === 'restarting') { ourStatus = 'starting'; } // Only update and broadcast if status changed if (service.status !== ourStatus) { this.database.updateService(service.id!, { status: ourStatus }); // Broadcast status change via WebSocket if (this.oneboxRef.httpServer) { this.oneboxRef.httpServer.broadcastServiceStatus(name, ourStatus); } } } catch (error) { logger.debug(`Failed to sync status for service ${name}: ${error.message}`); } } /** * Sync all service statuses from Docker */ async syncAllServiceStatuses(): Promise { const services = this.listServices(); for (const service of services) { await this.syncServiceStatus(service.name); } } }