diff --git a/frontend/e2e/utils/docker.ts b/frontend/e2e/utils/docker.ts new file mode 100644 index 00000000..e72e2bda --- /dev/null +++ b/frontend/e2e/utils/docker.ts @@ -0,0 +1,276 @@ +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`); +}