/** * 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 { 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(); // Generate admin credentials const adminUser = 'admin'; const adminPassword = credentialEncryption.generatePassword(32); const adminCredentials = { username: adminUser, password: adminPassword, }; logger.info(`Deploying MinIO platform service as ${containerName}...`); // Ensure data directory exists try { await Deno.mkdir('/var/lib/onebox/minio', { recursive: true }); } catch (e) { if (!(e instanceof Deno.errors.AlreadyExists)) { logger.warn(`Could not create MinIO data directory: ${e.message}`); } } // 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 const encryptedCreds = await credentialEncryption.encrypt(adminCredentials); const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type); 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 { const containerName = this.getContainerName(); const endpoint = `http://${containerName}:9000/minio/health/live`; const response = await fetch(endpoint, { method: 'GET', signal: AbortSignal.timeout(5000), }); return response.ok; } catch (error) { logger.debug(`MinIO health check failed: ${error.message}`); return false; } } async provisionResource(userService: IService): Promise { const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type); if (!platformService || !platformService.adminCredentialsEncrypted) { 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 and credentials const bucketName = this.generateBucketName(userService.name); const accessKey = credentialEncryption.generateAccessKey(20); const secretKey = credentialEncryption.generateSecretKey(40); logger.info(`Provisioning MinIO bucket '${bucketName}' for service '${userService.name}'...`); const endpoint = `http://${containerName}:9000`; // Import AWS S3 client const { S3Client, CreateBucketCommand, PutBucketPolicyCommand } = await import('npm:@aws-sdk/client-s3@3'); // Create S3 client with admin credentials const s3Client = new S3Client({ endpoint, region: 'us-east-1', credentials: { accessKeyId: adminCreds.username, secretAccessKey: adminCreds.password, }, forcePathStyle: true, }); // Create the bucket try { await s3Client.send(new CreateBucketCommand({ Bucket: bucketName, })); logger.info(`Created MinIO bucket '${bucketName}'`); } catch (e: any) { if (e.name !== 'BucketAlreadyOwnedByYou' && e.name !== 'BucketAlreadyExists') { throw e; } logger.warn(`Bucket '${bucketName}' already exists`); } // Create service account/access key using MinIO Admin API // MinIO Admin API requires mc client or direct API calls // For simplicity, we'll use root credentials and bucket policy isolation // In production, you'd use MinIO's Admin API to create service accounts // Set bucket policy to allow access only with this bucket's credentials const bucketPolicy = { Version: '2012-10-17', Statement: [ { Effect: 'Allow', Principal: { AWS: ['*'] }, Action: ['s3:GetObject', 's3:PutObject', 's3:DeleteObject', 's3:ListBucket'], Resource: [ `arn:aws:s3:::${bucketName}`, `arn:aws:s3:::${bucketName}/*`, ], }, ], }; try { await s3Client.send(new PutBucketPolicyCommand({ Bucket: bucketName, Policy: JSON.stringify(bucketPolicy), })); logger.info(`Set bucket policy for '${bucketName}'`); } catch (e) { logger.warn(`Could not set bucket policy: ${e.message}`); } // Note: For proper per-service credentials, MinIO Admin API should be used // For now, we're providing the bucket with root access // TODO: Implement MinIO service account creation logger.warn('Using root credentials for MinIO access. Consider implementing service accounts for production.'); const credentials: Record = { endpoint, bucket: bucketName, accessKey: adminCreds.username, // Using root for now 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) { throw new Error('MinIO platform service not found or not configured'); } const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted); const containerName = this.getContainerName(); const endpoint = `http://${containerName}:9000`; logger.info(`Deprovisioning MinIO bucket '${resource.resourceName}'...`); const { S3Client, DeleteBucketCommand, ListObjectsV2Command, DeleteObjectsCommand } = await import('npm:@aws-sdk/client-s3@3'); const s3Client = new S3Client({ endpoint, region: 'us-east-1', credentials: { accessKeyId: adminCreds.username, secretAccessKey: adminCreds.password, }, forcePathStyle: true, }); try { // First, delete all objects in the bucket let continuationToken: string | undefined; do { const listResponse = await s3Client.send(new ListObjectsV2Command({ Bucket: resource.resourceName, ContinuationToken: continuationToken, })); if (listResponse.Contents && listResponse.Contents.length > 0) { await s3Client.send(new DeleteObjectsCommand({ Bucket: resource.resourceName, Delete: { Objects: listResponse.Contents.map(obj => ({ Key: obj.Key! })), }, })); logger.info(`Deleted ${listResponse.Contents.length} objects from bucket`); } continuationToken = listResponse.IsTruncated ? listResponse.NextContinuationToken : undefined; } while (continuationToken); // Now delete the bucket await s3Client.send(new DeleteBucketCommand({ Bucket: resource.resourceName, })); logger.success(`MinIO bucket '${resource.resourceName}' deleted`); } catch (e) { logger.error(`Failed to delete MinIO bucket: ${e.message}`); throw e; } } }