Files
tfm_ainventory/frontend/e2e/utils/docker.ts

277 lines
6.9 KiB
TypeScript

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`);
}