189 lines
4.9 KiB
TypeScript
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}`;
|
|
}
|
|
}
|
|
} |