fix(platform-services/minio): Improve MinIO provider: reuse existing data and credentials, use host-bound port for provisioning, and safer provisioning/deprovisioning

This commit is contained in:
2025-11-26 18:46:50 +00:00
parent 3574a94e38
commit 9d7f132f6d
3 changed files with 104 additions and 37 deletions

View File

@@ -57,22 +57,55 @@ export class MinioProvider extends BasePlatformServiceProvider {
async deployContainer(): Promise<string> {
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<boolean> {
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<IProvisionedResource> {
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<string, string> = {
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<string, string>): Promise<void> {
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,