284 lines
10 KiB
TypeScript
284 lines
10 KiB
TypeScript
/**
|
|
* Redis 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 { getErrorMessage } from '../../../utils/error.ts';
|
|
import { credentialEncryption } from '../../encryption.ts';
|
|
import type { Onebox } from '../../onebox.ts';
|
|
|
|
export class RedisProvider extends BasePlatformServiceProvider {
|
|
readonly type: TPlatformServiceType = 'redis';
|
|
readonly displayName = 'Redis';
|
|
readonly resourceTypes: TPlatformResourceType[] = ['cache'];
|
|
|
|
constructor(oneboxRef: Onebox) {
|
|
super(oneboxRef);
|
|
}
|
|
|
|
getDefaultConfig(): IPlatformServiceConfig {
|
|
return {
|
|
image: 'redis:7-alpine',
|
|
port: 6379,
|
|
volumes: ['/var/lib/onebox/redis:/data'],
|
|
environment: {},
|
|
};
|
|
}
|
|
|
|
getEnvVarMappings(): IEnvVarMapping[] {
|
|
return [
|
|
{ envVar: 'REDIS_HOST', credentialPath: 'host' },
|
|
{ envVar: 'REDIS_PORT', credentialPath: 'port' },
|
|
{ envVar: 'REDIS_PASSWORD', credentialPath: 'password' },
|
|
{ envVar: 'REDIS_DB', credentialPath: 'db' },
|
|
{ envVar: 'REDIS_URL', credentialPath: 'connectionString' },
|
|
];
|
|
}
|
|
|
|
async deployContainer(): Promise<string> {
|
|
const config = this.getDefaultConfig();
|
|
const containerName = this.getContainerName();
|
|
const dataDir = '/var/lib/onebox/redis';
|
|
|
|
logger.info(`Deploying Redis 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 Redis data
|
|
try {
|
|
const stat = await Deno.stat(`${dataDir}/dump.rdb`);
|
|
dataExists = stat.isFile;
|
|
logger.info(`Redis data directory exists with dump.rdb file`);
|
|
} catch {
|
|
// Also check for appendonly file
|
|
try {
|
|
const stat = await Deno.stat(`${dataDir}/appendonly.aof`);
|
|
dataExists = stat.isFile;
|
|
logger.info(`Redis data directory exists with appendonly.aof file`);
|
|
} catch {
|
|
dataExists = false;
|
|
}
|
|
}
|
|
|
|
if (dataExists && platformService?.adminCredentialsEncrypted) {
|
|
// Reuse existing credentials from database
|
|
logger.info('Reusing existing Redis credentials (data directory already initialized)');
|
|
adminCredentials = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
|
|
} else {
|
|
// Generate new credentials for fresh deployment
|
|
logger.info('Generating new Redis admin credentials');
|
|
adminCredentials = {
|
|
username: 'default',
|
|
password: credentialEncryption.generatePassword(32),
|
|
};
|
|
|
|
// If data exists but we don't have credentials, we need to wipe the data
|
|
if (dataExists) {
|
|
logger.warn('Redis data exists but no credentials in database - wiping data directory');
|
|
try {
|
|
await Deno.remove(dataDir, { recursive: true });
|
|
} catch (e) {
|
|
logger.error(`Failed to wipe Redis data directory: ${getErrorMessage(e)}`);
|
|
throw new Error('Cannot deploy Redis: data directory exists without credentials');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ensure data directory exists
|
|
try {
|
|
await Deno.mkdir(dataDir, { recursive: true });
|
|
} catch (e) {
|
|
// Directory might already exist
|
|
if (!(e instanceof Deno.errors.AlreadyExists)) {
|
|
logger.warn(`Could not create Redis data directory: ${getErrorMessage(e)}`);
|
|
}
|
|
}
|
|
|
|
// Redis uses command args for password, not env vars
|
|
const containerId = await this.oneboxRef.docker.createPlatformContainer({
|
|
name: containerName,
|
|
image: config.image,
|
|
port: config.port,
|
|
env: [],
|
|
volumes: config.volumes,
|
|
network: this.getNetworkName(),
|
|
command: ['redis-server', '--requirepass', adminCredentials.password, '--appendonly', 'yes'],
|
|
});
|
|
|
|
// Store encrypted admin credentials (only update if new or changed)
|
|
const encryptedCreds = await credentialEncryption.encrypt(adminCredentials);
|
|
if (platformService) {
|
|
this.oneboxRef.database.updatePlatformService(platformService.id!, {
|
|
containerId,
|
|
adminCredentialsEncrypted: encryptedCreds,
|
|
status: 'starting',
|
|
});
|
|
}
|
|
|
|
logger.success(`Redis container created: ${containerId}`);
|
|
return containerId;
|
|
}
|
|
|
|
async stopContainer(containerId: string): Promise<void> {
|
|
logger.info(`Stopping Redis container ${containerId}...`);
|
|
await this.oneboxRef.docker.stopContainer(containerId);
|
|
logger.success('Redis container stopped');
|
|
}
|
|
|
|
async healthCheck(): Promise<boolean> {
|
|
try {
|
|
logger.info('Redis health check: starting...');
|
|
const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type);
|
|
if (!platformService) {
|
|
logger.info('Redis health check: platform service not found in database');
|
|
return false;
|
|
}
|
|
if (!platformService.adminCredentialsEncrypted) {
|
|
logger.info('Redis health check: no admin credentials stored');
|
|
return false;
|
|
}
|
|
if (!platformService.containerId) {
|
|
logger.info('Redis health check: no container ID in database record');
|
|
return false;
|
|
}
|
|
|
|
logger.info(`Redis health check: using container ID ${platformService.containerId.substring(0, 12)}...`);
|
|
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
|
|
|
|
// Use docker exec to run health check inside the container
|
|
const result = await this.oneboxRef.docker.execInContainer(
|
|
platformService.containerId,
|
|
['redis-cli', '-a', adminCreds.password, 'ping']
|
|
);
|
|
|
|
if (result.exitCode === 0 && result.stdout.includes('PONG')) {
|
|
logger.info('Redis health check: success');
|
|
return true;
|
|
} else {
|
|
logger.info(`Redis health check failed: exit code ${result.exitCode}, stdout: ${result.stdout.substring(0, 200)}`);
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
logger.info(`Redis 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) {
|
|
throw new Error('Redis platform service not found or not configured');
|
|
}
|
|
|
|
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
|
|
const containerName = this.getContainerName();
|
|
|
|
// Determine the next available DB index (1-15, reserving 0 for admin)
|
|
const existingResources = this.oneboxRef.database.getPlatformResourcesByPlatformService(platformService.id!);
|
|
const usedIndexes = new Set<number>();
|
|
|
|
for (const resource of existingResources) {
|
|
try {
|
|
const creds = await credentialEncryption.decrypt(resource.credentialsEncrypted);
|
|
if (creds.db) {
|
|
usedIndexes.add(parseInt(creds.db, 10));
|
|
}
|
|
} catch {
|
|
// Skip resources with corrupt credentials
|
|
}
|
|
}
|
|
|
|
let dbIndex = -1;
|
|
for (let i = 1; i <= 15; i++) {
|
|
if (!usedIndexes.has(i)) {
|
|
dbIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (dbIndex === -1) {
|
|
throw new Error('No available Redis database indexes (max 15 services per Redis instance)');
|
|
}
|
|
|
|
const resourceName = this.generateResourceName(userService.name);
|
|
|
|
logger.info(`Provisioning Redis database index ${dbIndex} for service '${userService.name}'...`);
|
|
|
|
// No server-side creation needed - Redis DB indexes exist implicitly
|
|
// Just verify connectivity
|
|
if (platformService.containerId) {
|
|
const result = await this.oneboxRef.docker.execInContainer(
|
|
platformService.containerId,
|
|
['redis-cli', '-a', adminCreds.password, '-n', String(dbIndex), 'ping']
|
|
);
|
|
|
|
if (result.exitCode !== 0 || !result.stdout.includes('PONG')) {
|
|
throw new Error(`Failed to verify Redis database ${dbIndex}: exit code ${result.exitCode}`);
|
|
}
|
|
}
|
|
|
|
logger.success(`Redis database index ${dbIndex} provisioned for service '${userService.name}'`);
|
|
|
|
// Build the credentials and env vars
|
|
const credentials: Record<string, string> = {
|
|
host: containerName,
|
|
port: '6379',
|
|
password: adminCreds.password,
|
|
db: String(dbIndex),
|
|
connectionString: `redis://:${adminCreds.password}@${containerName}:6379/${dbIndex}`,
|
|
};
|
|
|
|
// 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: 'cache',
|
|
name: resourceName,
|
|
credentials,
|
|
envVars,
|
|
};
|
|
}
|
|
|
|
async deprovisionResource(resource: IPlatformResource, credentials: Record<string, string>): Promise<void> {
|
|
const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type);
|
|
if (!platformService || !platformService.adminCredentialsEncrypted || !platformService.containerId) {
|
|
throw new Error('Redis platform service not found or not configured');
|
|
}
|
|
|
|
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
|
|
const dbIndex = credentials.db || '0';
|
|
|
|
logger.info(`Deprovisioning Redis database index ${dbIndex} for resource '${resource.resourceName}'...`);
|
|
|
|
// Flush the specific database
|
|
const result = await this.oneboxRef.docker.execInContainer(
|
|
platformService.containerId,
|
|
['redis-cli', '-a', adminCreds.password, '-n', dbIndex, 'FLUSHDB']
|
|
);
|
|
|
|
if (result.exitCode !== 0) {
|
|
logger.warn(`Redis deprovision returned exit code ${result.exitCode}: ${result.stderr.substring(0, 200)}`);
|
|
}
|
|
|
|
logger.success(`Redis database index ${dbIndex} flushed for resource '${resource.resourceName}'`);
|
|
}
|
|
}
|