From 9d7f132f6d00ec3be596c492619fdbc58658c80f Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Wed, 26 Nov 2025 18:46:50 +0000 Subject: [PATCH] fix(platform-services/minio): Improve MinIO provider: reuse existing data and credentials, use host-bound port for provisioning, and safer provisioning/deprovisioning --- changelog.md | 10 ++ ts/00_commitinfo_data.ts | 2 +- .../platform-services/providers/minio.ts | 129 +++++++++++++----- 3 files changed, 104 insertions(+), 37 deletions(-) diff --git a/changelog.md b/changelog.md index 95c8804..79a5544 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2025-11-26 - 1.2.1 - fix(platform-services/minio) +Improve MinIO provider: reuse existing data and credentials, use host-bound port for provisioning, and safer provisioning/deprovisioning + +- MinIO provider now detects existing data directory and will reuse stored admin credentials when available instead of regenerating them. +- If data exists but no credentials are stored, MinIO deployment will wipe the data directory to avoid credential mismatch and fail early with a clear error if wiping fails. +- Provisioning and deprovisioning now connect to MinIO via the container's host-mapped port (127.0.0.1:) instead of relying on overlay network addresses; an error is thrown when the host port mapping cannot be determined. +- Bucket provisioning creates policies and returns environment variables using container network hostnames for in-network access; a warning notes that per-service MinIO accounts are TODO and root credentials are used for now. +- Added logging improvements around MinIO deploy/provision/deprovision steps for easier debugging. +- Added VSCode workspace files (extensions, launch, tasks) for the ui project to improve developer experience. + ## 2025-11-26 - 1.2.0 - feat(ui) Sync UI tab state with URL and update routes/links diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 0005f0b..beae010 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/onebox', - version: '1.2.0', + version: '1.2.1', description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers' } diff --git a/ts/classes/platform-services/providers/minio.ts b/ts/classes/platform-services/providers/minio.ts index 0c558d2..3f4114a 100644 --- a/ts/classes/platform-services/providers/minio.ts +++ b/ts/classes/platform-services/providers/minio.ts @@ -57,22 +57,55 @@ export class MinioProvider extends BasePlatformServiceProvider { 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, - }; + 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('/var/lib/onebox/minio', { recursive: true }); + 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)}`); } @@ -95,9 +128,8 @@ export class MinioProvider extends BasePlatformServiceProvider { exposePorts: [9000, 9001], // API and Console ports }); - // Store encrypted admin credentials + // Store encrypted admin credentials (only update if new or changed) const encryptedCreds = await credentialEncryption.encrypt(adminCredentials); - const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type); if (platformService) { this.oneboxRef.database.updatePlatformService(platformService.id!, { containerId, @@ -118,41 +150,58 @@ export class MinioProvider extends BasePlatformServiceProvider { async healthCheck(): Promise { try { + logger.info('MinIO health check: starting...'); const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type); - if (!platformService || !platformService.containerId) { + 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; } - // Get container IP for health check (hostname won't resolve from host) - const containerIP = await this.oneboxRef.docker.getContainerIP(platformService.containerId); - if (!containerIP) { - logger.debug('MinIO health check: could not get container IP'); + 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; } - - const endpoint = `http://${containerIP}: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: ${getErrorMessage(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) { + 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(); + // Get container host port for connection from host (overlay network IPs not accessible from host) + const hostPort = await this.oneboxRef.docker.getContainerHostPort(platformService.containerId, 9000); + if (!hostPort) { + throw new Error('Could not get MinIO container host port'); + } + // Generate bucket name and credentials const bucketName = this.generateBucketName(userService.name); const accessKey = credentialEncryption.generateAccessKey(20); @@ -160,14 +209,15 @@ export class MinioProvider extends BasePlatformServiceProvider { logger.info(`Provisioning MinIO bucket '${bucketName}' for service '${userService.name}'...`); - const endpoint = `http://${containerName}:9000`; + // Connect to MinIO via localhost and the mapped host port (for provisioning from host) + const provisioningEndpoint = `http://127.0.0.1:${hostPort}`; // Import AWS S3 client const { S3Client, CreateBucketCommand, PutBucketPolicyCommand } = await import('npm:@aws-sdk/client-s3@3'); - // Create S3 client with admin credentials + // Create S3 client with admin credentials - connect via host port const s3Client = new S3Client({ - endpoint, + endpoint: provisioningEndpoint, region: 'us-east-1', credentials: { accessKeyId: adminCreds.username, @@ -225,8 +275,11 @@ export class MinioProvider extends BasePlatformServiceProvider { // TODO: Implement MinIO service account creation logger.warn('Using root credentials for MinIO access. Consider implementing service accounts for production.'); + // Use container name for the endpoint in credentials (user services run in same network) + const serviceEndpoint = `http://${containerName}:9000`; + const credentials: Record = { - endpoint, + endpoint: serviceEndpoint, bucket: bucketName, accessKey: adminCreds.username, // Using root for now secretKey: adminCreds.password, @@ -253,20 +306,24 @@ export class MinioProvider extends BasePlatformServiceProvider { async deprovisionResource(resource: IPlatformResource, credentials: Record): Promise { const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type); - if (!platformService || !platformService.adminCredentialsEncrypted) { + 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(); - const endpoint = `http://${containerName}:9000`; + + // Get container host port for connection from host (overlay network IPs not accessible from host) + const hostPort = await this.oneboxRef.docker.getContainerHostPort(platformService.containerId, 9000); + if (!hostPort) { + throw new Error('Could not get MinIO container host port'); + } 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, + endpoint: `http://127.0.0.1:${hostPort}`, region: 'us-east-1', credentials: { accessKeyId: adminCreds.username,