import { execSync } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; /** * Docker container lifecycle management for E2E tests */ export interface DockerComposeConfig { projectName: string; composePath: string; envVars: Record; } /** * Start Docker Compose services for a workflow */ export async function startDockerCompose(config: DockerComposeConfig): Promise { const { projectName, composePath, envVars } = config; // Prepare environment variables const envString = Object.entries(envVars) .map(([key, value]) => `${key}="${value}"`) .join(' '); try { // Use docker-compose up execSync( `cd ${path.dirname(composePath)} && ${envString} docker-compose -f ${path.basename(composePath)} -p ${projectName} up -d`, { stdio: 'inherit' } ); console.log(`Docker Compose services started for project: ${projectName}`); } catch (error) { throw new Error(`Failed to start Docker Compose: ${error}`); } } /** * Stop and remove Docker Compose services */ export async function stopDockerCompose(projectName: string, composePath: string): Promise { try { execSync( `cd ${path.dirname(composePath)} && docker-compose -f ${path.basename(composePath)} -p ${projectName} down`, { stdio: 'inherit' } ); console.log(`Docker Compose services stopped for project: ${projectName}`); } catch (error) { console.warn(`Warning stopping Docker Compose: ${error}`); } } /** * Wait for a service to be healthy */ export async function waitForServiceHealthy( projectName: string, serviceName: string, maxRetries: number = 30 ): Promise { let retries = 0; while (retries < maxRetries) { try { const health = execSync( `docker inspect --format='{{json .State.Health}}' ${projectName}_${serviceName}_1 2>/dev/null || echo '{"Status":"none"}'`, { encoding: 'utf-8' } ).trim(); const healthStatus = JSON.parse(health || '{"Status":"none"}'); if (healthStatus.Status === 'healthy') { console.log(`Service ${serviceName} is healthy`); return; } retries++; await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second } catch (error) { retries++; await new Promise((resolve) => setTimeout(resolve, 1000)); } } throw new Error(`Service ${serviceName} did not become healthy after ${maxRetries} seconds`); } /** * Get container logs */ export async function getContainerLogs( projectName: string, serviceName: string, tail: number = 50 ): Promise { try { return execSync(`docker-compose -p ${projectName} logs --tail=${tail} ${serviceName}`, { encoding: 'utf-8', }); } catch (error) { return `Error getting logs: ${error}`; } } /** * Check if service is running */ export async function isServiceRunning( projectName: string, serviceName: string ): Promise { try { execSync(`docker-compose -p ${projectName} ps ${serviceName} | grep -q "Up"`, { stdio: 'ignore' }); return true; } catch { return false; } } /** * Restart a service */ export async function restartService(projectName: string, serviceName: string): Promise { try { execSync(`docker-compose -p ${projectName} restart ${serviceName}`, { stdio: 'inherit' }); console.log(`Service ${serviceName} restarted`); } catch (error) { throw new Error(`Failed to restart service ${serviceName}: ${error}`); } } /** * Get service port mapping */ export async function getServicePort( projectName: string, serviceName: string, containerPort: number ): Promise { try { const output = execSync( `docker-compose -p ${projectName} port ${serviceName} ${containerPort}`, { encoding: 'utf-8' } ).trim(); const match = output.match(/:(\d+)$/); return match ? parseInt(match[1], 10) : null; } catch { return null; } } /** * Execute command inside container */ export async function execInContainer( projectName: string, serviceName: string, command: string[] ): Promise { try { const cmd = `docker-compose -p ${projectName} exec -T ${serviceName} ${command.join(' ')}`; return execSync(cmd, { encoding: 'utf-8' }); } catch (error) { throw new Error(`Failed to execute command in container: ${error}`); } } /** * Get service status */ export async function getServiceStatus( projectName: string, serviceName: string ): Promise { try { const output = execSync(`docker-compose -p ${projectName} ps ${serviceName}`, { encoding: 'utf-8', }); const lines = output.split('\n'); if (lines.length > 1) { const statusLine = lines[1]; const status = statusLine.split(/\s{2,}/)[4] || 'Unknown'; return status; } return 'Unknown'; } catch { return 'Not found'; } } /** * Clean up all Docker resources for a project */ export async function cleanupProject(projectName: string): Promise { try { // Stop containers execSync(`docker-compose -p ${projectName} down --remove-orphans`, { stdio: 'ignore', }); // Remove volumes execSync(`docker volume prune -f`, { stdio: 'ignore' }); console.log(`Cleanup complete for project: ${projectName}`); } catch (error) { console.warn(`Cleanup warning: ${error}`); } } /** * Wait for multiple services */ export async function waitForAllServices( projectName: string, services: string[], maxRetries: number = 30 ): Promise { const promises = services.map((service) => waitForServiceHealthy(projectName, service, maxRetries)); try { await Promise.all(promises); console.log(`All services are healthy for project: ${projectName}`); } catch (error) { throw new Error(`Failed to wait for services: ${error}`); } } /** * Generate compose file with specific ports and env vars */ export function generateComposeEnvVars( workflowId: string, ports: { backend: number; frontend: number; ldap: number }, ldapEnabled: boolean = true ): Record { return { WORKFLOW_ID: workflowId, BACKEND_PORT: ports.backend.toString(), FRONTEND_PORT: ports.frontend.toString(), LDAP_PORT: ports.ldap.toString(), LDAP_ENABLED: ldapEnabled ? 'true' : 'false', LDAP_SERVER: `localhost:${ports.ldap}`, LDAP_BASE_DN: 'dc=ainventory,dc=local', AI_PROVIDER: 'gemini', LOG_LEVEL: 'INFO', }; } /** * Wait for port to be available */ export async function waitForPort( port: number, host: string = 'localhost', timeout: number = 30000 ): Promise { const startTime = Date.now(); while (Date.now() - startTime < timeout) { try { execSync(`curl -s http://${host}:${port}/health >/dev/null 2>&1 || false`, { stdio: 'ignore', }); return; // Port is available } catch { await new Promise((resolve) => setTimeout(resolve, 500)); } } throw new Error(`Port ${port} did not become available after ${timeout}ms`); }