update
This commit is contained in:
405
ts/classes/services.ts
Normal file
405
ts/classes/services.ts
Normal file
@@ -0,0 +1,405 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user