feat: create docker container management utility for e2e tests
This commit is contained in:
276
frontend/e2e/utils/docker.ts
Normal file
276
frontend/e2e/utils/docker.ts
Normal file
@@ -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<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Docker Compose services for a workflow
|
||||
*/
|
||||
export async function startDockerCompose(config: DockerComposeConfig): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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<number | null> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string, string> {
|
||||
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<void> {
|
||||
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`);
|
||||
}
|
||||
Reference in New Issue
Block a user