277 lines
6.9 KiB
TypeScript
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`);
|
|
}
|