feat: Implement platform service providers for MinIO and MongoDB

- Added base interface and abstract class for platform service providers.
- Created MinIOProvider class for S3-compatible storage with deployment, provisioning, and deprovisioning functionalities.
- Implemented MongoDBProvider class for MongoDB service with similar capabilities.
- Introduced error handling utilities for better error management.
- Developed TokensComponent for managing registry tokens in the UI, including creation, deletion, and display of tokens.
This commit is contained in:
2025-11-25 04:20:19 +00:00
parent 9aa6906ca5
commit 8ebd677478
28 changed files with 3462 additions and 490 deletions

View File

@@ -0,0 +1,10 @@
/**
* Platform Services Module
* Exports all platform service related classes and types
*/
export { PlatformServicesManager } from './manager.ts';
export type { IPlatformServiceProvider } from './providers/base.ts';
export { BasePlatformServiceProvider } from './providers/base.ts';
export { MongoDBProvider } from './providers/mongodb.ts';
export { MinioProvider } from './providers/minio.ts';

View File

@@ -0,0 +1,361 @@
/**
* Platform Services Manager
* Orchestrates platform services (MongoDB, MinIO) and their resources
*/
import type {
IService,
IPlatformService,
IPlatformResource,
IPlatformRequirements,
IProvisionedResource,
TPlatformServiceType,
} from '../../types.ts';
import type { IPlatformServiceProvider } from './providers/base.ts';
import { MongoDBProvider } from './providers/mongodb.ts';
import { MinioProvider } from './providers/minio.ts';
import { logger } from '../../logging.ts';
import { credentialEncryption } from '../encryption.ts';
import type { Onebox } from '../onebox.ts';
export class PlatformServicesManager {
private oneboxRef: Onebox;
private providers = new Map<TPlatformServiceType, IPlatformServiceProvider>();
constructor(oneboxRef: Onebox) {
this.oneboxRef = oneboxRef;
}
/**
* Initialize the platform services manager
*/
async init(): Promise<void> {
// Initialize encryption
await credentialEncryption.init();
// Register providers
this.registerProvider(new MongoDBProvider(this.oneboxRef));
this.registerProvider(new MinioProvider(this.oneboxRef));
logger.info(`Platform services manager initialized with ${this.providers.size} providers`);
}
/**
* Register a platform service provider
*/
registerProvider(provider: IPlatformServiceProvider): void {
this.providers.set(provider.type, provider);
logger.debug(`Registered platform service provider: ${provider.displayName}`);
}
/**
* Get a provider by type
*/
getProvider(type: TPlatformServiceType): IPlatformServiceProvider | undefined {
return this.providers.get(type);
}
/**
* Get all registered providers
*/
getAllProviders(): IPlatformServiceProvider[] {
return Array.from(this.providers.values());
}
/**
* Ensure a platform service is running, deploying it if necessary
*/
async ensureRunning(type: TPlatformServiceType): Promise<IPlatformService> {
const provider = this.providers.get(type);
if (!provider) {
throw new Error(`Unknown platform service type: ${type}`);
}
// Check if platform service exists in database
let platformService = this.oneboxRef.database.getPlatformServiceByType(type);
if (!platformService) {
// Create platform service record
logger.info(`Creating new ${provider.displayName} platform service...`);
const config = provider.getDefaultConfig();
platformService = this.oneboxRef.database.createPlatformService({
name: `onebox-${type}`,
type,
status: 'stopped',
config,
createdAt: Date.now(),
updatedAt: Date.now(),
});
}
// Check if already running
if (platformService.status === 'running') {
// Verify it's actually healthy
const isHealthy = await provider.healthCheck();
if (isHealthy) {
logger.debug(`${provider.displayName} is already running and healthy`);
return platformService;
}
logger.warn(`${provider.displayName} reports running but health check failed, restarting...`);
}
// Deploy if not running
if (platformService.status !== 'running') {
logger.info(`Starting ${provider.displayName} platform service...`);
try {
this.oneboxRef.database.updatePlatformService(platformService.id!, { status: 'starting' });
const containerId = await provider.deployContainer();
// Wait for health check to pass
const healthy = await this.waitForHealthy(type, 60000); // 60 second timeout
if (healthy) {
this.oneboxRef.database.updatePlatformService(platformService.id!, {
status: 'running',
containerId,
});
logger.success(`${provider.displayName} platform service is now running`);
} else {
this.oneboxRef.database.updatePlatformService(platformService.id!, { status: 'failed' });
throw new Error(`${provider.displayName} failed to start within timeout`);
}
// Refresh platform service from database
platformService = this.oneboxRef.database.getPlatformServiceByType(type)!;
} catch (error) {
logger.error(`Failed to start ${provider.displayName}: ${error.message}`);
this.oneboxRef.database.updatePlatformService(platformService.id!, { status: 'failed' });
throw error;
}
}
return platformService;
}
/**
* Wait for a platform service to become healthy
*/
private async waitForHealthy(type: TPlatformServiceType, timeoutMs: number): Promise<boolean> {
const provider = this.providers.get(type);
if (!provider) return false;
const startTime = Date.now();
const checkInterval = 2000; // Check every 2 seconds
while (Date.now() - startTime < timeoutMs) {
const isHealthy = await provider.healthCheck();
if (isHealthy) {
return true;
}
await new Promise((resolve) => setTimeout(resolve, checkInterval));
}
return false;
}
/**
* Stop a platform service
*/
async stopPlatformService(type: TPlatformServiceType): Promise<void> {
const provider = this.providers.get(type);
if (!provider) {
throw new Error(`Unknown platform service type: ${type}`);
}
const platformService = this.oneboxRef.database.getPlatformServiceByType(type);
if (!platformService) {
logger.warn(`Platform service ${type} not found`);
return;
}
if (!platformService.containerId) {
logger.warn(`Platform service ${type} has no container ID`);
return;
}
logger.info(`Stopping ${provider.displayName} platform service...`);
this.oneboxRef.database.updatePlatformService(platformService.id!, { status: 'stopping' });
try {
await provider.stopContainer(platformService.containerId);
this.oneboxRef.database.updatePlatformService(platformService.id!, {
status: 'stopped',
containerId: undefined,
});
logger.success(`${provider.displayName} platform service stopped`);
} catch (error) {
logger.error(`Failed to stop ${provider.displayName}: ${error.message}`);
this.oneboxRef.database.updatePlatformService(platformService.id!, { status: 'failed' });
throw error;
}
}
/**
* Provision platform resources for a user service based on its requirements
*/
async provisionForService(service: IService): Promise<Record<string, string>> {
const requirements = service.platformRequirements;
if (!requirements) {
return {};
}
const allEnvVars: Record<string, string> = {};
// Provision MongoDB if requested
if (requirements.mongodb) {
logger.info(`Provisioning MongoDB for service '${service.name}'...`);
// Ensure MongoDB is running
const mongoService = await this.ensureRunning('mongodb');
const provider = this.providers.get('mongodb')!;
// Provision database
const result = await provider.provisionResource(service);
// Store resource record
const encryptedCreds = await credentialEncryption.encrypt(result.credentials);
this.oneboxRef.database.createPlatformResource({
platformServiceId: mongoService.id!,
serviceId: service.id!,
resourceType: result.type,
resourceName: result.name,
credentialsEncrypted: encryptedCreds,
createdAt: Date.now(),
});
// Merge env vars
Object.assign(allEnvVars, result.envVars);
logger.success(`MongoDB provisioned for service '${service.name}'`);
}
// Provision S3/MinIO if requested
if (requirements.s3) {
logger.info(`Provisioning S3 storage for service '${service.name}'...`);
// Ensure MinIO is running
const minioService = await this.ensureRunning('minio');
const provider = this.providers.get('minio')!;
// Provision bucket
const result = await provider.provisionResource(service);
// Store resource record
const encryptedCreds = await credentialEncryption.encrypt(result.credentials);
this.oneboxRef.database.createPlatformResource({
platformServiceId: minioService.id!,
serviceId: service.id!,
resourceType: result.type,
resourceName: result.name,
credentialsEncrypted: encryptedCreds,
createdAt: Date.now(),
});
// Merge env vars
Object.assign(allEnvVars, result.envVars);
logger.success(`S3 storage provisioned for service '${service.name}'`);
}
return allEnvVars;
}
/**
* Cleanup platform resources when a user service is deleted
*/
async cleanupForService(serviceId: number): Promise<void> {
const resources = this.oneboxRef.database.getPlatformResourcesByService(serviceId);
for (const resource of resources) {
try {
const platformService = this.oneboxRef.database.getPlatformServiceById(resource.platformServiceId);
if (!platformService) {
logger.warn(`Platform service not found for resource ${resource.id}`);
continue;
}
const provider = this.providers.get(platformService.type);
if (!provider) {
logger.warn(`Provider not found for type ${platformService.type}`);
continue;
}
// Decrypt credentials
const credentials = await credentialEncryption.decrypt(resource.credentialsEncrypted);
// Deprovision the resource
logger.info(`Cleaning up ${resource.resourceType} '${resource.resourceName}'...`);
await provider.deprovisionResource(resource, credentials);
// Delete resource record
this.oneboxRef.database.deletePlatformResource(resource.id!);
logger.success(`Cleaned up ${resource.resourceType} '${resource.resourceName}'`);
} catch (error) {
logger.error(`Failed to cleanup resource ${resource.id}: ${error.message}`);
// Continue with other resources even if one fails
}
}
}
/**
* Get injected environment variables for a service
*/
async getInjectedEnvVars(serviceId: number): Promise<Record<string, string>> {
const resources = this.oneboxRef.database.getPlatformResourcesByService(serviceId);
const allEnvVars: Record<string, string> = {};
for (const resource of resources) {
const platformService = this.oneboxRef.database.getPlatformServiceById(resource.platformServiceId);
if (!platformService) continue;
const provider = this.providers.get(platformService.type);
if (!provider) continue;
const credentials = await credentialEncryption.decrypt(resource.credentialsEncrypted);
const mappings = provider.getEnvVarMappings();
for (const mapping of mappings) {
if (credentials[mapping.credentialPath]) {
allEnvVars[mapping.envVar] = credentials[mapping.credentialPath];
}
}
}
return allEnvVars;
}
/**
* Get all platform services with their status
*/
getAllPlatformServices(): IPlatformService[] {
return this.oneboxRef.database.getAllPlatformServices();
}
/**
* Get resources for a specific user service
*/
async getResourcesForService(serviceId: number): Promise<Array<{
resource: IPlatformResource;
platformService: IPlatformService;
credentials: Record<string, string>;
}>> {
const resources = this.oneboxRef.database.getPlatformResourcesByService(serviceId);
const result = [];
for (const resource of resources) {
const platformService = this.oneboxRef.database.getPlatformServiceById(resource.platformServiceId);
if (!platformService) continue;
const credentials = await credentialEncryption.decrypt(resource.credentialsEncrypted);
result.push({
resource,
platformService,
credentials,
});
}
return result;
}
}

View File

@@ -0,0 +1,123 @@
/**
* Base interface and types for platform service providers
*/
import type {
IService,
IPlatformService,
IPlatformResource,
IPlatformServiceConfig,
IProvisionedResource,
IEnvVarMapping,
TPlatformServiceType,
TPlatformResourceType,
} from '../../../types.ts';
import type { Onebox } from '../../onebox.ts';
/**
* Interface that all platform service providers must implement
*/
export interface IPlatformServiceProvider {
/** Unique identifier for this provider type */
readonly type: TPlatformServiceType;
/** Human-readable display name */
readonly displayName: string;
/** Resource types this provider can provision */
readonly resourceTypes: TPlatformResourceType[];
/**
* Get the default configuration for this platform service
*/
getDefaultConfig(): IPlatformServiceConfig;
/**
* Deploy the platform service container
* @returns The container ID
*/
deployContainer(): Promise<string>;
/**
* Stop the platform service container
*/
stopContainer(containerId: string): Promise<void>;
/**
* Check if the platform service is healthy and ready to accept connections
*/
healthCheck(): Promise<boolean>;
/**
* Provision a resource for a user service (e.g., create database, bucket)
* @param userService The user service requesting the resource
* @returns Provisioned resource with credentials and env var mappings
*/
provisionResource(userService: IService): Promise<IProvisionedResource>;
/**
* Deprovision a resource (e.g., drop database, delete bucket)
* @param resource The resource to deprovision
*/
deprovisionResource(resource: IPlatformResource, credentials: Record<string, string>): Promise<void>;
/**
* Get the environment variable mappings for this provider
*/
getEnvVarMappings(): IEnvVarMapping[];
}
/**
* Base class for platform service providers with common functionality
*/
export abstract class BasePlatformServiceProvider implements IPlatformServiceProvider {
abstract readonly type: TPlatformServiceType;
abstract readonly displayName: string;
abstract readonly resourceTypes: TPlatformResourceType[];
protected oneboxRef: Onebox;
constructor(oneboxRef: Onebox) {
this.oneboxRef = oneboxRef;
}
abstract getDefaultConfig(): IPlatformServiceConfig;
abstract deployContainer(): Promise<string>;
abstract stopContainer(containerId: string): Promise<void>;
abstract healthCheck(): Promise<boolean>;
abstract provisionResource(userService: IService): Promise<IProvisionedResource>;
abstract deprovisionResource(resource: IPlatformResource, credentials: Record<string, string>): Promise<void>;
abstract getEnvVarMappings(): IEnvVarMapping[];
/**
* Get the internal Docker network name for platform services
*/
protected getNetworkName(): string {
return 'onebox-network';
}
/**
* Get the container name for this platform service
*/
protected getContainerName(): string {
return `onebox-${this.type}`;
}
/**
* Generate a resource name from a user service name
*/
protected generateResourceName(serviceName: string, prefix: string = 'onebox'): string {
// Replace dashes with underscores for database compatibility
const sanitized = serviceName.replace(/-/g, '_');
return `${prefix}_${sanitized}`;
}
/**
* Generate a bucket name from a user service name
*/
protected generateBucketName(serviceName: string, prefix: string = 'onebox'): string {
// Buckets use dashes, lowercase
const sanitized = serviceName.toLowerCase().replace(/_/g, '-');
return `${prefix}-${sanitized}`;
}
}

View File

@@ -0,0 +1,299 @@
/**
* 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<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,
};
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<void> {
logger.info(`Stopping MinIO container ${containerId}...`);
await this.oneboxRef.docker.stopContainer(containerId);
logger.success('MinIO container stopped');
}
async healthCheck(): Promise<boolean> {
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<IProvisionedResource> {
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<string, string> = {
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<string, string> = {};
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<string, string>): Promise<void> {
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;
}
}
}

View File

@@ -0,0 +1,246 @@
/**
* MongoDB 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 MongoDBProvider extends BasePlatformServiceProvider {
readonly type: TPlatformServiceType = 'mongodb';
readonly displayName = 'MongoDB';
readonly resourceTypes: TPlatformResourceType[] = ['database'];
constructor(oneboxRef: Onebox) {
super(oneboxRef);
}
getDefaultConfig(): IPlatformServiceConfig {
return {
image: 'mongo:7',
port: 27017,
volumes: ['/var/lib/onebox/mongodb:/data/db'],
environment: {
MONGO_INITDB_ROOT_USERNAME: 'admin',
// Password will be generated and stored encrypted
},
};
}
getEnvVarMappings(): IEnvVarMapping[] {
return [
{ envVar: 'MONGODB_URI', credentialPath: 'connectionString' },
{ envVar: 'MONGODB_HOST', credentialPath: 'host' },
{ envVar: 'MONGODB_PORT', credentialPath: 'port' },
{ envVar: 'MONGODB_DATABASE', credentialPath: 'database' },
{ envVar: 'MONGODB_USERNAME', credentialPath: 'username' },
{ envVar: 'MONGODB_PASSWORD', credentialPath: 'password' },
];
}
async deployContainer(): Promise<string> {
const config = this.getDefaultConfig();
const containerName = this.getContainerName();
// Generate admin password
const adminPassword = credentialEncryption.generatePassword(32);
// Store admin credentials encrypted in the platform service record
const adminCredentials = {
username: 'admin',
password: adminPassword,
};
logger.info(`Deploying MongoDB platform service as ${containerName}...`);
// Ensure data directory exists
try {
await Deno.mkdir('/var/lib/onebox/mongodb', { recursive: true });
} catch (e) {
// Directory might already exist
if (!(e instanceof Deno.errors.AlreadyExists)) {
logger.warn(`Could not create MongoDB data directory: ${e.message}`);
}
}
// Create container using Docker API
const envVars = [
`MONGO_INITDB_ROOT_USERNAME=${adminCredentials.username}`,
`MONGO_INITDB_ROOT_PASSWORD=${adminCredentials.password}`,
];
// Use Docker to create the container
const containerId = await this.oneboxRef.docker.createPlatformContainer({
name: containerName,
image: config.image,
port: config.port,
env: envVars,
volumes: config.volumes,
network: this.getNetworkName(),
});
// 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(`MongoDB container created: ${containerId}`);
return containerId;
}
async stopContainer(containerId: string): Promise<void> {
logger.info(`Stopping MongoDB container ${containerId}...`);
await this.oneboxRef.docker.stopContainer(containerId);
logger.success('MongoDB container stopped');
}
async healthCheck(): Promise<boolean> {
try {
const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type);
if (!platformService || !platformService.adminCredentialsEncrypted) {
return false;
}
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
const containerName = this.getContainerName();
// Try to connect to MongoDB using mongosh ping
const { MongoClient } = await import('npm:mongodb@6');
const uri = `mongodb://${adminCreds.username}:${adminCreds.password}@${containerName}:27017/?authSource=admin`;
const client = new MongoClient(uri, {
serverSelectionTimeoutMS: 5000,
connectTimeoutMS: 5000,
});
await client.connect();
await client.db('admin').command({ ping: 1 });
await client.close();
return true;
} catch (error) {
logger.debug(`MongoDB health check failed: ${error.message}`);
return false;
}
}
async provisionResource(userService: IService): Promise<IProvisionedResource> {
const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type);
if (!platformService || !platformService.adminCredentialsEncrypted) {
throw new Error('MongoDB platform service not found or not configured');
}
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
const containerName = this.getContainerName();
// Generate resource names and credentials
const dbName = this.generateResourceName(userService.name);
const username = this.generateResourceName(userService.name);
const password = credentialEncryption.generatePassword(32);
logger.info(`Provisioning MongoDB database '${dbName}' for service '${userService.name}'...`);
// Connect to MongoDB and create database/user
const { MongoClient } = await import('npm:mongodb@6');
const adminUri = `mongodb://${adminCreds.username}:${adminCreds.password}@${containerName}:27017/?authSource=admin`;
const client = new MongoClient(adminUri);
await client.connect();
try {
// Create the database by switching to it (MongoDB creates on first write)
const db = client.db(dbName);
// Create a collection to ensure the database exists
await db.createCollection('_onebox_init');
// Create user with readWrite access to this database
await db.command({
createUser: username,
pwd: password,
roles: [{ role: 'readWrite', db: dbName }],
});
logger.success(`MongoDB database '${dbName}' provisioned with user '${username}'`);
} finally {
await client.close();
}
// Build the credentials and env vars
const credentials: Record<string, string> = {
host: containerName,
port: '27017',
database: dbName,
username,
password,
connectionString: `mongodb://${username}:${password}@${containerName}:27017/${dbName}?authSource=${dbName}`,
};
// Map credentials to env vars
const envVars: Record<string, string> = {};
for (const mapping of this.getEnvVarMappings()) {
if (credentials[mapping.credentialPath]) {
envVars[mapping.envVar] = credentials[mapping.credentialPath];
}
}
return {
type: 'database',
name: dbName,
credentials,
envVars,
};
}
async deprovisionResource(resource: IPlatformResource, credentials: Record<string, string>): Promise<void> {
const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type);
if (!platformService || !platformService.adminCredentialsEncrypted) {
throw new Error('MongoDB platform service not found or not configured');
}
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
const containerName = this.getContainerName();
logger.info(`Deprovisioning MongoDB database '${resource.resourceName}'...`);
const { MongoClient } = await import('npm:mongodb@6');
const adminUri = `mongodb://${adminCreds.username}:${adminCreds.password}@${containerName}:27017/?authSource=admin`;
const client = new MongoClient(adminUri);
await client.connect();
try {
const db = client.db(resource.resourceName);
// Drop the user
try {
await db.command({ dropUser: credentials.username });
logger.info(`Dropped MongoDB user '${credentials.username}'`);
} catch (e) {
logger.warn(`Could not drop MongoDB user: ${e.message}`);
}
// Drop the database
await db.dropDatabase();
logger.success(`MongoDB database '${resource.resourceName}' dropped`);
} finally {
await client.close();
}
}
}