|
|
|
@@ -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,
|
|
|
|
|