- Updated multiple classes to replace direct error message access with getErrorMessage function for improved error handling and consistency. - Modified error logging in CertRequirementManager, CloudflareDomainSync, OneboxDnsManager, OneboxDockerManager, OneboxHttpServer, Onebox, OneboxRegistriesManager, and OneboxServicesManager. - Ensured that all error messages logged provide a standardized format and improved clarity.
754 lines
24 KiB
TypeScript
754 lines
24 KiB
TypeScript
/**
|
|
* Services Manager for Onebox
|
|
*
|
|
* Orchestrates service deployment: Docker + Nginx + DNS + SSL
|
|
*/
|
|
|
|
import type { IService, IServiceDeployOptions, IPlatformRequirements } from '../types.ts';
|
|
import { logger } from '../logging.ts';
|
|
import { getErrorMessage } from '../utils/error.ts';
|
|
import { OneboxDatabase } from './database.ts';
|
|
import { OneboxDockerManager } from './docker.ts';
|
|
import type { PlatformServicesManager } from './platform-services/index.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}`);
|
|
}
|
|
|
|
// Handle Onebox Registry setup
|
|
let imageToPull: string;
|
|
|
|
if (options.useOneboxRegistry) {
|
|
// Use onebox registry image name
|
|
const tag = options.registryImageTag || 'latest';
|
|
imageToPull = this.oneboxRef.registry.getImageName(options.name, tag);
|
|
} else {
|
|
// Use external image
|
|
imageToPull = options.image;
|
|
}
|
|
|
|
// Build platform requirements
|
|
const platformRequirements: IPlatformRequirements | undefined =
|
|
(options.enableMongoDB || options.enableS3)
|
|
? {
|
|
mongodb: options.enableMongoDB,
|
|
s3: options.enableS3,
|
|
}
|
|
: undefined;
|
|
|
|
// Create service record in database
|
|
const service = await this.database.createService({
|
|
name: options.name,
|
|
image: options.useOneboxRegistry ? imageToPull : options.image,
|
|
registry: options.registry,
|
|
envVars: options.envVars || {},
|
|
port: options.port,
|
|
domain: options.domain,
|
|
status: 'stopped',
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
// Onebox Registry fields
|
|
useOneboxRegistry: options.useOneboxRegistry,
|
|
registryRepository: options.useOneboxRegistry ? options.name : undefined,
|
|
registryImageTag: options.registryImageTag || 'latest',
|
|
autoUpdateOnPush: options.autoUpdateOnPush,
|
|
// Platform requirements
|
|
platformRequirements,
|
|
});
|
|
|
|
// Provision platform resources if needed
|
|
let platformEnvVars: Record<string, string> = {};
|
|
if (platformRequirements) {
|
|
try {
|
|
logger.info(`Provisioning platform resources for service '${options.name}'...`);
|
|
const platformServices = this.oneboxRef.platformServices as PlatformServicesManager;
|
|
platformEnvVars = await platformServices.provisionForService(service);
|
|
logger.success(`Platform resources provisioned for service '${options.name}'`);
|
|
} catch (error) {
|
|
logger.error(`Failed to provision platform resources: ${getErrorMessage(error)}`);
|
|
// Clean up the service record on failure
|
|
this.database.deleteService(service.id!);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Merge platform env vars with user-specified env vars (user vars take precedence)
|
|
const mergedEnvVars = { ...platformEnvVars, ...(options.envVars || {}) };
|
|
|
|
// Update service with merged env vars
|
|
if (Object.keys(platformEnvVars).length > 0) {
|
|
this.database.updateService(service.id!, { envVars: mergedEnvVars });
|
|
}
|
|
|
|
// Get updated service with merged env vars
|
|
const serviceWithEnvVars = this.database.getServiceByName(options.name)!;
|
|
|
|
// Pull image (skip if using onebox registry - image might not exist yet)
|
|
if (!options.useOneboxRegistry) {
|
|
await this.docker.pullImage(imageToPull, options.registry);
|
|
}
|
|
|
|
// Create container (uses the updated service with merged env vars)
|
|
const containerID = await this.docker.createContainer(serviceWithEnvVars);
|
|
|
|
// 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}`);
|
|
|
|
// Validate domain and create CertRequirement
|
|
try {
|
|
// Extract base domain (e.g., "api.example.com" -> "example.com")
|
|
const domainParts = options.domain.split('.');
|
|
const baseDomain = domainParts.slice(-2).join('.');
|
|
const subdomain = domainParts.length > 2 ? domainParts.slice(0, -2).join('.') : '';
|
|
|
|
// Check if base domain exists in Domain table
|
|
const domainRecord = this.database.getDomainByName(baseDomain);
|
|
|
|
if (!domainRecord) {
|
|
logger.warn(
|
|
`Domain ${baseDomain} not found in Domain table. ` +
|
|
`Service will deploy but certificate management may not work. ` +
|
|
`Run Cloudflare domain sync or manually add the domain.`
|
|
);
|
|
} else if (domainRecord.isObsolete) {
|
|
logger.warn(
|
|
`Domain ${baseDomain} is marked as obsolete. ` +
|
|
`Certificate management may not work properly.`
|
|
);
|
|
} else {
|
|
// Create CertRequirement for automatic certificate management
|
|
const now = Date.now();
|
|
this.database.createCertRequirement({
|
|
serviceId: service.id!,
|
|
domainId: domainRecord.id!,
|
|
subdomain: subdomain,
|
|
status: 'pending',
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
});
|
|
logger.info(
|
|
`Created certificate requirement for ${options.domain} ` +
|
|
`(domain: ${baseDomain}, subdomain: ${subdomain || 'none'})`
|
|
);
|
|
}
|
|
} catch (error) {
|
|
logger.warn(`Failed to create certificate requirement: ${getErrorMessage(error)}`);
|
|
}
|
|
|
|
// 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}: ${getErrorMessage(error)}`);
|
|
}
|
|
}
|
|
|
|
// 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}: ${getErrorMessage(error)}`);
|
|
}
|
|
|
|
// Configure SSL (if autoSSL is enabled)
|
|
// Note: With CertRequirement system, certificates are managed automatically
|
|
// but we still support the old direct obtainCertificate for backward compatibility
|
|
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}: ${getErrorMessage(error)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
logger.success(`Service deployed successfully: ${options.name}`);
|
|
|
|
return this.database.getServiceByName(options.name)!;
|
|
} catch (error) {
|
|
logger.error(`Failed to deploy service ${options.name}: ${getErrorMessage(error)}`);
|
|
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}: ${getErrorMessage(error)}`);
|
|
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}: ${getErrorMessage(error)}`);
|
|
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}: ${getErrorMessage(error)}`);
|
|
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: ${getErrorMessage(error)}`);
|
|
}
|
|
}
|
|
|
|
// Remove reverse proxy route
|
|
if (service.domain) {
|
|
try {
|
|
this.oneboxRef.reverseProxy.removeRoute(service.domain);
|
|
} catch (error) {
|
|
logger.warn(`Failed to remove reverse proxy route: ${getErrorMessage(error)}`);
|
|
}
|
|
|
|
// Note: We don't remove DNS records or SSL certs automatically
|
|
// as they might be used by other services or need manual cleanup
|
|
}
|
|
|
|
// Cleanup platform resources (MongoDB databases, S3 buckets, etc.)
|
|
if (service.platformRequirements) {
|
|
try {
|
|
logger.info(`Cleaning up platform resources for service '${name}'...`);
|
|
const platformServices = this.oneboxRef.platformServices as PlatformServicesManager;
|
|
await platformServices.cleanupForService(service.id!);
|
|
logger.success(`Platform resources cleaned up for service '${name}'`);
|
|
} catch (error) {
|
|
logger.warn(`Failed to cleanup platform resources: ${getErrorMessage(error)}`);
|
|
// Continue with service deletion even if cleanup fails
|
|
}
|
|
}
|
|
|
|
// Remove from database
|
|
this.database.deleteService(service.id!);
|
|
|
|
logger.success(`Service removed: ${name}`);
|
|
} catch (error) {
|
|
logger.error(`Failed to remove service ${name}: ${getErrorMessage(error)}`);
|
|
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);
|
|
|
|
// Debug: check what we got
|
|
logger.log(`getServiceLogs: logs type = ${typeof logs}, constructor = ${logs?.constructor?.name}`);
|
|
logger.log(`getServiceLogs: logs.stdout type = ${typeof logs.stdout}`);
|
|
logger.log(`getServiceLogs: logs.stdout value = ${String(logs.stdout).slice(0, 100)}`);
|
|
|
|
// v5 API returns combined stdout/stderr with proper formatting
|
|
return logs.stdout;
|
|
} catch (error) {
|
|
logger.error(`Failed to get logs for service ${name}: ${getErrorMessage(error)}`);
|
|
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}: ${getErrorMessage(error)}`);
|
|
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}: ${getErrorMessage(error)}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get platform resources for a service
|
|
*/
|
|
async getServicePlatformResources(name: string) {
|
|
try {
|
|
const service = this.database.getServiceByName(name);
|
|
if (!service) {
|
|
throw new Error(`Service not found: ${name}`);
|
|
}
|
|
|
|
if (!service.platformRequirements) {
|
|
return [];
|
|
}
|
|
|
|
const platformServices = this.oneboxRef.platformServices as PlatformServicesManager;
|
|
return await platformServices.getResourcesForService(service.id!);
|
|
} catch (error) {
|
|
logger.error(`Failed to get platform resources for service ${name}: ${getErrorMessage(error)}`);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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}: ${getErrorMessage(error)}`);
|
|
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}: ${getErrorMessage(error)}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update service configuration (image, port, domain, env vars)
|
|
* Recreates the container with new configuration and auto-restarts
|
|
*/
|
|
async updateService(
|
|
name: string,
|
|
updates: {
|
|
image?: string;
|
|
registry?: string;
|
|
port?: number;
|
|
domain?: string;
|
|
envVars?: Record<string, string>;
|
|
}
|
|
): Promise<IService> {
|
|
try {
|
|
const service = this.database.getServiceByName(name);
|
|
if (!service) {
|
|
throw new Error(`Service not found: ${name}`);
|
|
}
|
|
|
|
logger.info(`Updating service: ${name}`);
|
|
const wasRunning = service.status === 'running';
|
|
const oldContainerID = service.containerID;
|
|
const oldDomain = service.domain;
|
|
|
|
// Stop the container if running
|
|
if (wasRunning && oldContainerID) {
|
|
logger.info(`Stopping service ${name} for updates...`);
|
|
try {
|
|
await this.docker.stopContainer(oldContainerID);
|
|
} catch (error) {
|
|
logger.warn(`Failed to stop container: ${getErrorMessage(error)}`);
|
|
}
|
|
}
|
|
|
|
// Pull new image if changed
|
|
if (updates.image && updates.image !== service.image) {
|
|
logger.info(`Pulling new image: ${updates.image}`);
|
|
await this.docker.pullImage(updates.image, updates.registry || service.registry);
|
|
}
|
|
|
|
// Update service in database
|
|
const updateData: any = {
|
|
updatedAt: Date.now(),
|
|
};
|
|
if (updates.image !== undefined) updateData.image = updates.image;
|
|
if (updates.registry !== undefined) updateData.registry = updates.registry;
|
|
if (updates.port !== undefined) updateData.port = updates.port;
|
|
if (updates.domain !== undefined) updateData.domain = updates.domain;
|
|
if (updates.envVars !== undefined) updateData.envVars = updates.envVars;
|
|
|
|
this.database.updateService(service.id!, updateData);
|
|
|
|
// Get updated service
|
|
const updatedService = this.database.getServiceByName(name)!;
|
|
|
|
// Remove old container
|
|
if (oldContainerID) {
|
|
try {
|
|
await this.docker.removeContainer(oldContainerID, true);
|
|
logger.info(`Removed old container for ${name}`);
|
|
} catch (error) {
|
|
logger.warn(`Failed to remove old container: ${getErrorMessage(error)}`);
|
|
}
|
|
}
|
|
|
|
// Create new container with updated config
|
|
logger.info(`Creating new container for ${name}...`);
|
|
const containerID = await this.docker.createContainer(updatedService);
|
|
this.database.updateService(service.id!, { containerID });
|
|
|
|
// Update reverse proxy if domain changed
|
|
if (updates.domain !== undefined && updates.domain !== oldDomain) {
|
|
// Remove old route if it existed
|
|
if (oldDomain) {
|
|
try {
|
|
this.oneboxRef.reverseProxy.removeRoute(oldDomain);
|
|
} catch (error) {
|
|
logger.warn(`Failed to remove old reverse proxy route: ${getErrorMessage(error)}`);
|
|
}
|
|
}
|
|
|
|
// Add new route if domain specified
|
|
if (updates.domain) {
|
|
try {
|
|
await this.oneboxRef.reverseProxy.addRoute(
|
|
service.id!,
|
|
updates.domain,
|
|
updates.port || service.port
|
|
);
|
|
} catch (error) {
|
|
logger.warn(`Failed to configure reverse proxy: ${getErrorMessage(error)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Restart the container if it was running
|
|
if (wasRunning) {
|
|
logger.info(`Starting updated service ${name}...`);
|
|
this.database.updateService(service.id!, { status: 'starting' });
|
|
await this.docker.startContainer(containerID);
|
|
this.database.updateService(service.id!, { status: 'running' });
|
|
logger.success(`Service ${name} updated and restarted`);
|
|
} else {
|
|
this.database.updateService(service.id!, { status: 'stopped' });
|
|
logger.success(`Service ${name} updated (not started)`);
|
|
}
|
|
|
|
return this.database.getServiceByName(name)!;
|
|
} catch (error) {
|
|
logger.error(`Failed to update service ${name}: ${getErrorMessage(error)}`);
|
|
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';
|
|
}
|
|
|
|
// 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}: ${getErrorMessage(error)}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync all service statuses from Docker
|
|
*/
|
|
async syncAllServiceStatuses(): Promise<void> {
|
|
const services = this.listServices();
|
|
|
|
for (const service of services) {
|
|
await this.syncServiceStatus(service.name);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start auto-update monitoring for registry services
|
|
* Polls every 30 seconds for digest changes and restarts services if needed
|
|
*/
|
|
startAutoUpdateMonitoring(): void {
|
|
// Check every 30 seconds
|
|
setInterval(async () => {
|
|
try {
|
|
await this.checkForRegistryUpdates();
|
|
} catch (error) {
|
|
logger.error(`Auto-update check failed: ${getErrorMessage(error)}`);
|
|
}
|
|
}, 30000);
|
|
|
|
logger.info('Auto-update monitoring started (30s interval)');
|
|
}
|
|
|
|
/**
|
|
* Check all services using onebox registry for updates
|
|
*/
|
|
private async checkForRegistryUpdates(): Promise<void> {
|
|
const services = this.listServices();
|
|
|
|
for (const service of services) {
|
|
// Skip if not using onebox registry or auto-update is disabled
|
|
if (!service.useOneboxRegistry || !service.autoUpdateOnPush) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
// Get current digest from registry
|
|
const currentDigest = await this.oneboxRef.registry.getImageDigest(
|
|
service.registryRepository!,
|
|
service.registryImageTag || 'latest'
|
|
);
|
|
|
|
// Skip if no digest found (image might not exist yet)
|
|
if (!currentDigest) {
|
|
continue;
|
|
}
|
|
|
|
// Check if digest has changed
|
|
if (service.imageDigest && service.imageDigest !== currentDigest) {
|
|
logger.info(
|
|
`Digest changed for ${service.name}: ${service.imageDigest} -> ${currentDigest}`
|
|
);
|
|
|
|
// Update digest in database
|
|
this.database.updateService(service.id!, {
|
|
imageDigest: currentDigest,
|
|
});
|
|
|
|
// Pull new image
|
|
const imageName = this.oneboxRef.registry.getImageName(
|
|
service.registryRepository!,
|
|
service.registryImageTag || 'latest'
|
|
);
|
|
|
|
logger.info(`Pulling updated image: ${imageName}`);
|
|
await this.docker.pullImage(imageName);
|
|
|
|
// Restart service
|
|
logger.info(`Auto-restarting service: ${service.name}`);
|
|
await this.restartService(service.name);
|
|
|
|
// Broadcast update via WebSocket
|
|
this.oneboxRef.httpServer.broadcastServiceUpdate({
|
|
action: 'updated',
|
|
service: this.database.getServiceByName(service.name)!,
|
|
});
|
|
} else if (!service.imageDigest) {
|
|
// First time - just store the digest
|
|
this.database.updateService(service.id!, {
|
|
imageDigest: currentDigest,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
logger.error(`Failed to check updates for ${service.name}: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
}
|