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:
10
changelog.md
10
changelog.md
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# 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:<hostPort>) 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)
|
## 2025-11-26 - 1.2.0 - feat(ui)
|
||||||
Sync UI tab state with URL and update routes/links
|
Sync UI tab state with URL and update routes/links
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/onebox',
|
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'
|
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,22 +57,55 @@ export class MinioProvider extends BasePlatformServiceProvider {
|
|||||||
async deployContainer(): Promise<string> {
|
async deployContainer(): Promise<string> {
|
||||||
const config = this.getDefaultConfig();
|
const config = this.getDefaultConfig();
|
||||||
const containerName = this.getContainerName();
|
const containerName = this.getContainerName();
|
||||||
|
const dataDir = '/var/lib/onebox/minio';
|
||||||
// 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}...`);
|
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
|
// Ensure data directory exists
|
||||||
try {
|
try {
|
||||||
await Deno.mkdir('/var/lib/onebox/minio', { recursive: true });
|
await Deno.mkdir(dataDir, { recursive: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// Directory might already exist
|
||||||
if (!(e instanceof Deno.errors.AlreadyExists)) {
|
if (!(e instanceof Deno.errors.AlreadyExists)) {
|
||||||
logger.warn(`Could not create MinIO data directory: ${getErrorMessage(e)}`);
|
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
|
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 encryptedCreds = await credentialEncryption.encrypt(adminCredentials);
|
||||||
const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type);
|
|
||||||
if (platformService) {
|
if (platformService) {
|
||||||
this.oneboxRef.database.updatePlatformService(platformService.id!, {
|
this.oneboxRef.database.updatePlatformService(platformService.id!, {
|
||||||
containerId,
|
containerId,
|
||||||
@@ -118,41 +150,58 @@ export class MinioProvider extends BasePlatformServiceProvider {
|
|||||||
|
|
||||||
async healthCheck(): Promise<boolean> {
|
async healthCheck(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
|
logger.info('MinIO health check: starting...');
|
||||||
const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get container IP for health check (hostname won't resolve from host)
|
logger.info(`MinIO health check: using container ID ${platformService.containerId.substring(0, 12)}...`);
|
||||||
const containerIP = await this.oneboxRef.docker.getContainerIP(platformService.containerId);
|
|
||||||
if (!containerIP) {
|
// Use docker exec to run health check inside the container
|
||||||
logger.debug('MinIO health check: could not get container IP');
|
// 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;
|
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) {
|
} catch (error) {
|
||||||
logger.debug(`MinIO health check failed: ${getErrorMessage(error)}`);
|
logger.info(`MinIO health check exception: ${getErrorMessage(error)}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async provisionResource(userService: IService): Promise<IProvisionedResource> {
|
async provisionResource(userService: IService): Promise<IProvisionedResource> {
|
||||||
const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type);
|
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');
|
throw new Error('MinIO platform service not found or not configured');
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
|
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
|
||||||
const containerName = this.getContainerName();
|
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
|
// Generate bucket name and credentials
|
||||||
const bucketName = this.generateBucketName(userService.name);
|
const bucketName = this.generateBucketName(userService.name);
|
||||||
const accessKey = credentialEncryption.generateAccessKey(20);
|
const accessKey = credentialEncryption.generateAccessKey(20);
|
||||||
@@ -160,14 +209,15 @@ export class MinioProvider extends BasePlatformServiceProvider {
|
|||||||
|
|
||||||
logger.info(`Provisioning MinIO bucket '${bucketName}' for service '${userService.name}'...`);
|
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
|
// Import AWS S3 client
|
||||||
const { S3Client, CreateBucketCommand, PutBucketPolicyCommand } = await import('npm:@aws-sdk/client-s3@3');
|
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({
|
const s3Client = new S3Client({
|
||||||
endpoint,
|
endpoint: provisioningEndpoint,
|
||||||
region: 'us-east-1',
|
region: 'us-east-1',
|
||||||
credentials: {
|
credentials: {
|
||||||
accessKeyId: adminCreds.username,
|
accessKeyId: adminCreds.username,
|
||||||
@@ -225,8 +275,11 @@ export class MinioProvider extends BasePlatformServiceProvider {
|
|||||||
// TODO: Implement MinIO service account creation
|
// TODO: Implement MinIO service account creation
|
||||||
logger.warn('Using root credentials for MinIO access. Consider implementing service accounts for production.');
|
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> = {
|
const credentials: Record<string, string> = {
|
||||||
endpoint,
|
endpoint: serviceEndpoint,
|
||||||
bucket: bucketName,
|
bucket: bucketName,
|
||||||
accessKey: adminCreds.username, // Using root for now
|
accessKey: adminCreds.username, // Using root for now
|
||||||
secretKey: adminCreds.password,
|
secretKey: adminCreds.password,
|
||||||
@@ -253,20 +306,24 @@ export class MinioProvider extends BasePlatformServiceProvider {
|
|||||||
|
|
||||||
async deprovisionResource(resource: IPlatformResource, credentials: Record<string, string>): Promise<void> {
|
async deprovisionResource(resource: IPlatformResource, credentials: Record<string, string>): Promise<void> {
|
||||||
const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type);
|
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');
|
throw new Error('MinIO platform service not found or not configured');
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
|
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}'...`);
|
logger.info(`Deprovisioning MinIO bucket '${resource.resourceName}'...`);
|
||||||
|
|
||||||
const { S3Client, DeleteBucketCommand, ListObjectsV2Command, DeleteObjectsCommand } = await import('npm:@aws-sdk/client-s3@3');
|
const { S3Client, DeleteBucketCommand, ListObjectsV2Command, DeleteObjectsCommand } = await import('npm:@aws-sdk/client-s3@3');
|
||||||
|
|
||||||
const s3Client = new S3Client({
|
const s3Client = new S3Client({
|
||||||
endpoint,
|
endpoint: `http://127.0.0.1:${hostPort}`,
|
||||||
region: 'us-east-1',
|
region: 'us-east-1',
|
||||||
credentials: {
|
credentials: {
|
||||||
accessKeyId: adminCreds.username,
|
accessKeyId: adminCreds.username,
|
||||||
|
|||||||
Reference in New Issue
Block a user