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