362 lines
12 KiB
TypeScript
362 lines
12 KiB
TypeScript
|
|
/**
|
||
|
|
* 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 { credentialEncryption } from '../encryption.ts';
|
||
|
|
import type { Onebox } from '../onebox.ts';
|
||
|
|
|
||
|
|
export class PlatformServicesManager {
|
||
|
|
private oneboxRef: Onebox;
|
||
|
|
private providers = new Map<TPlatformServiceType, IPlatformServiceProvider>();
|
||
|
|
|
||
|
|
constructor(oneboxRef: Onebox) {
|
||
|
|
this.oneboxRef = oneboxRef;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize the platform services manager
|
||
|
|
*/
|
||
|
|
async init(): Promise<void> {
|
||
|
|
// 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<IPlatformService> {
|
||
|
|
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}: ${error.message}`);
|
||
|
|
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<boolean> {
|
||
|
|
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<void> {
|
||
|
|
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}: ${error.message}`);
|
||
|
|
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<Record<string, string>> {
|
||
|
|
const requirements = service.platformRequirements;
|
||
|
|
if (!requirements) {
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
|
||
|
|
const allEnvVars: Record<string, string> = {};
|
||
|
|
|
||
|
|
// 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<void> {
|
||
|
|
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}: ${error.message}`);
|
||
|
|
// Continue with other resources even if one fails
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get injected environment variables for a service
|
||
|
|
*/
|
||
|
|
async getInjectedEnvVars(serviceId: number): Promise<Record<string, string>> {
|
||
|
|
const resources = this.oneboxRef.database.getPlatformResourcesByService(serviceId);
|
||
|
|
const allEnvVars: Record<string, string> = {};
|
||
|
|
|
||
|
|
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<Array<{
|
||
|
|
resource: IPlatformResource;
|
||
|
|
platformService: IPlatformService;
|
||
|
|
credentials: Record<string, string>;
|
||
|
|
}>> {
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|