/** * MinIO (S3-compatible) Platform Service Provider */ import { BasePlatformServiceProvider } from './base.ts'; import type { IService, IPlatformResource, IPlatformServiceConfig, IProvisionedResource, IEnvVarMapping, TPlatformServiceType, TPlatformResourceType, } from '../../../types.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 MinioProvider extends BasePlatformServiceProvider { readonly type: TPlatformServiceType = 'minio'; readonly displayName = 'S3 Storage (MinIO)'; readonly resourceTypes: TPlatformResourceType[] = ['bucket']; constructor(oneboxRef: Onebox) { super(oneboxRef); } getDefaultConfig(): IPlatformServiceConfig { return { image: 'minio/minio:latest', port: 9000, volumes: ['/var/lib/onebox/minio:/data'], command: 'server /data --console-address :9001', environment: { MINIO_ROOT_USER: 'admin', // Password will be generated and stored encrypted }, }; } getEnvVarMappings(): IEnvVarMapping[] { return [ { envVar: 'S3_ENDPOINT', credentialPath: 'endpoint' }, { envVar: 'S3_BUCKET', credentialPath: 'bucket' }, { envVar: 'S3_ACCESS_KEY', credentialPath: 'accessKey' }, { envVar: 'S3_SECRET_KEY', credentialPath: 'secretKey' }, { envVar: 'S3_REGION', credentialPath: 'region' }, // AWS SDK compatible names { envVar: 'AWS_ACCESS_KEY_ID', credentialPath: 'accessKey' }, { envVar: 'AWS_SECRET_ACCESS_KEY', credentialPath: 'secretKey' }, { envVar: 'AWS_ENDPOINT_URL', credentialPath: 'endpoint' }, { envVar: 'AWS_REGION', credentialPath: 'region' }, ]; } async deployContainer(): Promise { const config = this.getDefaultConfig(); const containerName = this.getContainerName(); const dataDir = '/var/lib/onebox/minio'; logger.info(`Deploying MinIO platform service as ${containerName}...`); // Check if we have existing data and stored credentials const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type); let adminCredentials: { username: string; password: string }; let dataExists = false; // Check if data directory has existing MinIO data // MinIO creates .minio.sys directory on first startup try { const stat = await Deno.stat(`${dataDir}/.minio.sys`); dataExists = stat.isDirectory; logger.info(`MinIO data directory exists with .minio.sys folder`); } catch { // .minio.sys doesn't exist, this is a fresh install dataExists = false; } if (dataExists && platformService?.adminCredentialsEncrypted) { // Reuse existing credentials from database logger.info('Reusing existing MinIO credentials (data directory already initialized)'); adminCredentials = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted); } else { // Generate new credentials for fresh deployment logger.info('Generating new MinIO admin credentials'); adminCredentials = { username: 'admin', password: credentialEncryption.generatePassword(32), }; // If data exists but we don't have credentials, we need to wipe the data if (dataExists) { logger.warn('MinIO data exists but no credentials in database - wiping data directory'); try { await Deno.remove(dataDir, { recursive: true }); } catch (e) { logger.error(`Failed to wipe MinIO data directory: ${getErrorMessage(e)}`); throw new Error('Cannot deploy MinIO: data directory exists without credentials'); } } } // Ensure data directory exists try { await Deno.mkdir(dataDir, { recursive: true }); } catch (e) { // Directory might already exist if (!(e instanceof Deno.errors.AlreadyExists)) { logger.warn(`Could not create MinIO data directory: ${getErrorMessage(e)}`); } } // Create container using Docker API const envVars = [ `MINIO_ROOT_USER=${adminCredentials.username}`, `MINIO_ROOT_PASSWORD=${adminCredentials.password}`, ]; const containerId = await this.oneboxRef.docker.createPlatformContainer({ name: containerName, image: config.image, port: config.port, env: envVars, volumes: config.volumes, network: this.getNetworkName(), command: config.command?.split(' '), exposePorts: [9000, 9001], // API and Console ports }); // Store encrypted admin credentials (only update if new or changed) const encryptedCreds = await credentialEncryption.encrypt(adminCredentials); if (platformService) { this.oneboxRef.database.updatePlatformService(platformService.id!, { containerId, adminCredentialsEncrypted: encryptedCreds, status: 'starting', }); } logger.success(`MinIO container created: ${containerId}`); return containerId; } async stopContainer(containerId: string): Promise { logger.info(`Stopping MinIO container ${containerId}...`); await this.oneboxRef.docker.stopContainer(containerId); logger.success('MinIO container stopped'); } async healthCheck(): Promise { try { logger.info('MinIO health check: starting...'); const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type); if (!platformService) { logger.info('MinIO health check: platform service not found in database'); return false; } if (!platformService.adminCredentialsEncrypted) { logger.info('MinIO health check: no admin credentials stored'); return false; } if (!platformService.containerId) { logger.info('MinIO health check: no container ID in database record'); return false; } logger.info(`MinIO health check: using container ID ${platformService.containerId.substring(0, 12)}...`); // Use docker exec to run health check inside the container // This avoids network issues with overlay networks const result = await this.oneboxRef.docker.execInContainer( platformService.containerId, ['curl', '-sf', 'http://localhost:9000/minio/health/live'] ); if (result.exitCode === 0) { logger.info('MinIO health check: success'); return true; } else { logger.info(`MinIO health check failed: exit code ${result.exitCode}, stderr: ${result.stderr.substring(0, 200)}`); return false; } } catch (error) { logger.info(`MinIO health check exception: ${getErrorMessage(error)}`); return false; } } async provisionResource(userService: IService): Promise { const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type); if (!platformService || !platformService.adminCredentialsEncrypted || !platformService.containerId) { throw new Error('MinIO platform service not found or not configured'); } const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted); const containerName = this.getContainerName(); // Generate bucket name const bucketName = this.generateBucketName(userService.name); logger.info(`Provisioning MinIO bucket '${bucketName}' for service '${userService.name}'...`); // Use docker exec with mc (MinIO Client) inside the container // First configure mc alias for local server await this.execMc(platformService.containerId, [ 'alias', 'set', 'local', 'http://localhost:9000', adminCreds.username, adminCreds.password, ]); // Create the bucket const mbResult = await this.execMc(platformService.containerId, [ 'mb', '--ignore-existing', `local/${bucketName}`, ]); logger.info(`Created MinIO bucket '${bucketName}'`); // Set bucket policy to allow public read/write (services on the same network use root creds) await this.execMc(platformService.containerId, [ 'anonymous', 'set', 'none', `local/${bucketName}`, ]); // Use container name for the endpoint in credentials (user services run in same network) const serviceEndpoint = `http://${containerName}:9000`; const credentials: Record = { endpoint: serviceEndpoint, bucket: bucketName, accessKey: adminCreds.username, secretKey: adminCreds.password, region: 'us-east-1', }; // Map credentials to env vars const envVars: Record = {}; for (const mapping of this.getEnvVarMappings()) { if (credentials[mapping.credentialPath]) { envVars[mapping.envVar] = credentials[mapping.credentialPath]; } } logger.success(`MinIO bucket '${bucketName}' provisioned`); return { type: 'bucket', name: bucketName, credentials, envVars, }; } async deprovisionResource(resource: IPlatformResource, credentials: Record): Promise { const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type); if (!platformService || !platformService.adminCredentialsEncrypted || !platformService.containerId) { throw new Error('MinIO platform service not found or not configured'); } const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted); logger.info(`Deprovisioning MinIO bucket '${resource.resourceName}'...`); // Configure mc alias await this.execMc(platformService.containerId, [ 'alias', 'set', 'local', 'http://localhost:9000', adminCreds.username, adminCreds.password, ]); try { // Remove all objects and the bucket await this.execMc(platformService.containerId, [ 'rb', '--force', `local/${resource.resourceName}`, ]); logger.success(`MinIO bucket '${resource.resourceName}' deleted`); } catch (e) { logger.error(`Failed to delete MinIO bucket: ${getErrorMessage(e)}`); throw e; } } /** * Execute mc (MinIO Client) command inside the container */ private async execMc( containerId: string, args: string[], ): Promise<{ stdout: string; stderr: string }> { const result = await this.oneboxRef.docker.execInContainer(containerId, ['mc', ...args]); if (result.exitCode !== 0) { throw new Error(`mc command failed (exit ${result.exitCode}): ${result.stderr.substring(0, 200)}`); } return result; } }