Files
gridpilot.gg/tests/integration/harness/docker-manager.ts
2026-01-08 16:52:37 +01:00

189 lines
4.9 KiB
TypeScript

/**
* Docker Manager for Integration Tests
* Manages Docker Compose services for integration testing
*/
import { execSync, spawn } from 'child_process';
import { setTimeout } from 'timers/promises';
export interface DockerServiceConfig {
name: string;
port: number;
healthCheck: string;
timeout?: number;
}
export class DockerManager {
private static instance: DockerManager;
private services: Map<string, boolean> = new Map();
private composeProject = 'gridpilot-test';
private composeFile = 'docker-compose.test.yml';
private constructor() {}
static getInstance(): DockerManager {
if (!DockerManager.instance) {
DockerManager.instance = new DockerManager();
}
return DockerManager.instance;
}
/**
* Check if Docker services are already running
*/
isRunning(): boolean {
try {
const output = execSync(
`docker-compose -p ${this.composeProject} -f ${this.composeFile} ps -q 2>/dev/null || true`,
{ encoding: 'utf8' }
).trim();
return output.length > 0;
} catch {
return false;
}
}
/**
* Start Docker services with dependency checking
*/
async start(): Promise<void> {
console.log('[DockerManager] Starting test environment...');
if (this.isRunning()) {
console.log('[DockerManager] Services already running, checking health...');
await this.waitForServices();
return;
}
// Start services
execSync(
`COMPOSE_PARALLEL_LIMIT=1 docker-compose -p ${this.composeProject} -f ${this.composeFile} up -d ready api`,
{ stdio: 'inherit' }
);
console.log('[DockerManager] Services starting, waiting for health...');
await this.waitForServices();
}
/**
* Wait for all services to be healthy using polling
*/
async waitForServices(): Promise<void> {
const services: DockerServiceConfig[] = [
{
name: 'db',
port: 5433,
healthCheck: 'pg_isready -U gridpilot_test_user -d gridpilot_test',
timeout: 60000
},
{
name: 'api',
port: 3101,
healthCheck: 'curl -f http://localhost:3101/health',
timeout: 90000
}
];
for (const service of services) {
await this.waitForService(service);
}
}
/**
* Wait for a single service to be healthy
*/
async waitForService(config: DockerServiceConfig): Promise<void> {
const timeout = config.timeout || 30000;
const startTime = Date.now();
console.log(`[DockerManager] Waiting for ${config.name}...`);
while (Date.now() - startTime < timeout) {
try {
// Try health check command
if (config.name === 'db') {
// For DB, check if it's ready to accept connections
try {
execSync(
`docker exec ${this.composeProject}-${config.name}-1 ${config.healthCheck} 2>/dev/null`,
{ stdio: 'pipe' }
);
console.log(`[DockerManager] ✓ ${config.name} is healthy`);
return;
} catch {}
} else {
// For API, check HTTP endpoint
const response = await fetch(`http://localhost:${config.port}/health`);
if (response.ok) {
console.log(`[DockerManager] ✓ ${config.name} is healthy`);
return;
}
}
} catch (error) {
// Service not ready yet, continue waiting
}
await setTimeout(1000);
}
throw new Error(`[DockerManager] ${config.name} failed to become healthy within ${timeout}ms`);
}
/**
* Stop Docker services
*/
stop(): void {
console.log('[DockerManager] Stopping test environment...');
try {
execSync(
`docker-compose -p ${this.composeProject} -f ${this.composeFile} down --remove-orphans`,
{ stdio: 'inherit' }
);
} catch (error) {
console.warn('[DockerManager] Warning: Failed to stop services cleanly:', error);
}
}
/**
* Clean up volumes and containers
*/
clean(): void {
console.log('[DockerManager] Cleaning up test environment...');
try {
execSync(
`docker-compose -p ${this.composeProject} -f ${this.composeFile} down -v --remove-orphans --volumes`,
{ stdio: 'inherit' }
);
} catch (error) {
console.warn('[DockerManager] Warning: Failed to clean up cleanly:', error);
}
}
/**
* Execute a command in a service container
*/
execInService(service: string, command: string): string {
try {
return execSync(
`docker exec ${this.composeProject}-${service}-1 ${command}`,
{ encoding: 'utf8' }
);
} catch (error) {
throw new Error(`Failed to execute command in ${service}: ${error}`);
}
}
/**
* Get service logs
*/
getLogs(service: string): string {
try {
return execSync(
`docker logs ${this.composeProject}-${service}-1 --tail 100`,
{ encoding: 'utf8' }
);
} catch (error) {
return `Failed to get logs: ${error}`;
}
}
}