/** * Platform Services Manager * Orchestrates platform services (MongoDB, MinIO) and their resources */ import type { IService, IPlatformService, IPlatformResource, IPlatformRequirements, IProvisionedResource, TPlatformServiceType, } from '../../types.ts'; import type { IPlatformServiceProvider } from './providers/base.ts'; import { MongoDBProvider } from './providers/mongodb.ts'; import { MinioProvider } from './providers/minio.ts'; import { logger } from '../../logging.ts'; import { getErrorMessage } from '../../utils/error.ts'; import { credentialEncryption } from '../encryption.ts'; import type { Onebox } from '../onebox.ts'; export class PlatformServicesManager { private oneboxRef: Onebox; private providers = new Map(); constructor(oneboxRef: Onebox) { this.oneboxRef = oneboxRef; } /** * Initialize the platform services manager */ async init(): Promise { // Initialize encryption await credentialEncryption.init(); // Register providers this.registerProvider(new MongoDBProvider(this.oneboxRef)); this.registerProvider(new MinioProvider(this.oneboxRef)); logger.info(`Platform services manager initialized with ${this.providers.size} providers`); } /** * Register a platform service provider */ registerProvider(provider: IPlatformServiceProvider): void { this.providers.set(provider.type, provider); logger.debug(`Registered platform service provider: ${provider.displayName}`); } /** * Get a provider by type */ getProvider(type: TPlatformServiceType): IPlatformServiceProvider | undefined { return this.providers.get(type); } /** * Get all registered providers */ getAllProviders(): IPlatformServiceProvider[] { return Array.from(this.providers.values()); } /** * Ensure a platform service is running, deploying it if necessary */ async ensureRunning(type: TPlatformServiceType): Promise { const provider = this.providers.get(type); if (!provider) { throw new Error(`Unknown platform service type: ${type}`); } // Check if platform service exists in database let platformService = this.oneboxRef.database.getPlatformServiceByType(type); if (!platformService) { // Create platform service record logger.info(`Creating new ${provider.displayName} platform service...`); const config = provider.getDefaultConfig(); platformService = this.oneboxRef.database.createPlatformService({ name: `onebox-${type}`, type, status: 'stopped', config, createdAt: Date.now(), updatedAt: Date.now(), }); } // Check if already running if (platformService.status === 'running') { // Verify it's actually healthy const isHealthy = await provider.healthCheck(); if (isHealthy) { logger.debug(`${provider.displayName} is already running and healthy`); return platformService; } logger.warn(`${provider.displayName} reports running but health check failed, restarting...`); } // Deploy if not running if (platformService.status !== 'running') { logger.info(`Starting ${provider.displayName} platform service...`); try { this.oneboxRef.database.updatePlatformService(platformService.id!, { status: 'starting' }); const containerId = await provider.deployContainer(); // Wait for health check to pass const healthy = await this.waitForHealthy(type, 60000); // 60 second timeout if (healthy) { this.oneboxRef.database.updatePlatformService(platformService.id!, { status: 'running', containerId, }); logger.success(`${provider.displayName} platform service is now running`); } else { this.oneboxRef.database.updatePlatformService(platformService.id!, { status: 'failed' }); throw new Error(`${provider.displayName} failed to start within timeout`); } // Refresh platform service from database platformService = this.oneboxRef.database.getPlatformServiceByType(type)!; } catch (error) { logger.error(`Failed to start ${provider.displayName}: ${getErrorMessage(error)}`); this.oneboxRef.database.updatePlatformService(platformService.id!, { status: 'failed' }); throw error; } } return platformService; } /** * Wait for a platform service to become healthy */ private async waitForHealthy(type: TPlatformServiceType, timeoutMs: number): Promise { const provider = this.providers.get(type); if (!provider) return false; const startTime = Date.now(); const checkInterval = 2000; // Check every 2 seconds while (Date.now() - startTime < timeoutMs) { const isHealthy = await provider.healthCheck(); if (isHealthy) { return true; } await new Promise((resolve) => setTimeout(resolve, checkInterval)); } return false; } /** * Stop a platform service */ async stopPlatformService(type: TPlatformServiceType): Promise { const provider = this.providers.get(type); if (!provider) { throw new Error(`Unknown platform service type: ${type}`); } const platformService = this.oneboxRef.database.getPlatformServiceByType(type); if (!platformService) { logger.warn(`Platform service ${type} not found`); return; } if (!platformService.containerId) { logger.warn(`Platform service ${type} has no container ID`); return; } logger.info(`Stopping ${provider.displayName} platform service...`); this.oneboxRef.database.updatePlatformService(platformService.id!, { status: 'stopping' }); try { await provider.stopContainer(platformService.containerId); this.oneboxRef.database.updatePlatformService(platformService.id!, { status: 'stopped', containerId: undefined, }); logger.success(`${provider.displayName} platform service stopped`); } catch (error) { logger.error(`Failed to stop ${provider.displayName}: ${getErrorMessage(error)}`); this.oneboxRef.database.updatePlatformService(platformService.id!, { status: 'failed' }); throw error; } } /** * Provision platform resources for a user service based on its requirements */ async provisionForService(service: IService): Promise> { const requirements = service.platformRequirements; if (!requirements) { return {}; } const allEnvVars: Record = {}; // Provision MongoDB if requested if (requirements.mongodb) { logger.info(`Provisioning MongoDB for service '${service.name}'...`); // Ensure MongoDB is running const mongoService = await this.ensureRunning('mongodb'); const provider = this.providers.get('mongodb')!; // Provision database const result = await provider.provisionResource(service); // Store resource record const encryptedCreds = await credentialEncryption.encrypt(result.credentials); this.oneboxRef.database.createPlatformResource({ platformServiceId: mongoService.id!, serviceId: service.id!, resourceType: result.type, resourceName: result.name, credentialsEncrypted: encryptedCreds, createdAt: Date.now(), }); // Merge env vars Object.assign(allEnvVars, result.envVars); logger.success(`MongoDB provisioned for service '${service.name}'`); } // Provision S3/MinIO if requested if (requirements.s3) { logger.info(`Provisioning S3 storage for service '${service.name}'...`); // Ensure MinIO is running const minioService = await this.ensureRunning('minio'); const provider = this.providers.get('minio')!; // Provision bucket const result = await provider.provisionResource(service); // Store resource record const encryptedCreds = await credentialEncryption.encrypt(result.credentials); this.oneboxRef.database.createPlatformResource({ platformServiceId: minioService.id!, serviceId: service.id!, resourceType: result.type, resourceName: result.name, credentialsEncrypted: encryptedCreds, createdAt: Date.now(), }); // Merge env vars Object.assign(allEnvVars, result.envVars); logger.success(`S3 storage provisioned for service '${service.name}'`); } return allEnvVars; } /** * Cleanup platform resources when a user service is deleted */ async cleanupForService(serviceId: number): Promise { const resources = this.oneboxRef.database.getPlatformResourcesByService(serviceId); for (const resource of resources) { try { const platformService = this.oneboxRef.database.getPlatformServiceById(resource.platformServiceId); if (!platformService) { logger.warn(`Platform service not found for resource ${resource.id}`); continue; } const provider = this.providers.get(platformService.type); if (!provider) { logger.warn(`Provider not found for type ${platformService.type}`); continue; } // Decrypt credentials const credentials = await credentialEncryption.decrypt(resource.credentialsEncrypted); // Deprovision the resource logger.info(`Cleaning up ${resource.resourceType} '${resource.resourceName}'...`); await provider.deprovisionResource(resource, credentials); // Delete resource record this.oneboxRef.database.deletePlatformResource(resource.id!); logger.success(`Cleaned up ${resource.resourceType} '${resource.resourceName}'`); } catch (error) { logger.error(`Failed to cleanup resource ${resource.id}: ${getErrorMessage(error)}`); // Continue with other resources even if one fails } } } /** * Get injected environment variables for a service */ async getInjectedEnvVars(serviceId: number): Promise> { const resources = this.oneboxRef.database.getPlatformResourcesByService(serviceId); const allEnvVars: Record = {}; for (const resource of resources) { const platformService = this.oneboxRef.database.getPlatformServiceById(resource.platformServiceId); if (!platformService) continue; const provider = this.providers.get(platformService.type); if (!provider) continue; const credentials = await credentialEncryption.decrypt(resource.credentialsEncrypted); const mappings = provider.getEnvVarMappings(); for (const mapping of mappings) { if (credentials[mapping.credentialPath]) { allEnvVars[mapping.envVar] = credentials[mapping.credentialPath]; } } } return allEnvVars; } /** * Get all platform services with their status */ getAllPlatformServices(): IPlatformService[] { return this.oneboxRef.database.getAllPlatformServices(); } /** * Get resources for a specific user service */ async getResourcesForService(serviceId: number): Promise; }>> { const resources = this.oneboxRef.database.getPlatformResourcesByService(serviceId); const result = []; for (const resource of resources) { const platformService = this.oneboxRef.database.getPlatformServiceById(resource.platformServiceId); if (!platformService) continue; const credentials = await credentialEncryption.decrypt(resource.credentialsEncrypted); result.push({ resource, platformService, credentials, }); } return result; } }