Files
onebox/ts/classes/services.ts
2025-11-18 00:03:24 +00:00

406 lines
11 KiB
TypeScript

/**
* 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<IService> {
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<void> {
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<void> {
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<void> {
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<void> {
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<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 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<void> {
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<string> {
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<string, string>): Promise<void> {
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<void> {
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';
}
this.database.updateService(service.id!, { status: ourStatus });
} catch (error) {
logger.debug(`Failed to sync status for service ${name}: ${error.message}`);
}
}
/**
* Sync all service statuses from Docker
*/
async syncAllServiceStatuses(): Promise<void> {
const services = this.listServices();
for (const service of services) {
await this.syncServiceStatus(service.name);
}
}
}