Files
onebox/ts/classes/platform-services/manager.ts
Juergen Kunz 8ebd677478 feat: Implement platform service providers for MinIO and MongoDB
- Added base interface and abstract class for platform service providers.
- Created MinIOProvider class for S3-compatible storage with deployment, provisioning, and deprovisioning functionalities.
- Implemented MongoDBProvider class for MongoDB service with similar capabilities.
- Introduced error handling utilities for better error management.
- Developed TokensComponent for managing registry tokens in the UI, including creation, deletion, and display of tokens.
2025-11-25 04:20:19 +00:00

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;
}
}