You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
108 lines
4.0 KiB
108 lines
4.0 KiB
import Docker from 'dockerode';
|
|
import type {Container} from 'dockerode';
|
|
import logging from '@tryghost/logging';
|
|
import baseDebug from '@tryghost/debug';
|
|
import {execSync} from 'child_process';
|
|
|
|
const debug = baseDebug('e2e:DockerCompose');
|
|
|
|
export class DockerCompose {
|
|
private readonly composeFilePath: string;
|
|
private readonly projectName: string;
|
|
private readonly docker: Docker;
|
|
|
|
constructor(options: {composeFilePath: string; projectName: string; docker: Docker}) {
|
|
this.composeFilePath = options.composeFilePath;
|
|
this.projectName = options.projectName;
|
|
this.docker = options.docker;
|
|
}
|
|
|
|
/**
|
|
* Bring up services and wait for specific one-shot services to complete.
|
|
*/
|
|
upAndWaitFor(servicesToWait: string[] = []): void {
|
|
try {
|
|
logging.info('Starting docker compose services...');
|
|
execSync(`docker compose -f ${this.composeFilePath} up -d`, {stdio: 'inherit'});
|
|
if (servicesToWait.length > 0) {
|
|
// NOTE: `docker compose up -d --wait` will fail if one-shot services are included
|
|
execSync(`docker compose -f ${this.composeFilePath} wait ${servicesToWait.join(' ')}`);
|
|
}
|
|
logging.info('Docker compose services are up');
|
|
} catch (error) {
|
|
logging.error('Failed to start docker compose services:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop and remove all services for the project (including volumes).
|
|
*/
|
|
down(): void {
|
|
try {
|
|
execSync(`docker compose -f ${this.composeFilePath} down -v`, {stdio: 'inherit'});
|
|
} catch (error) {
|
|
logging.error('Failed to stop docker compose services:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read a file from a service container using `docker compose run`.
|
|
*/
|
|
readFileFromService(service: string, filePath: string): string {
|
|
const cmd = `docker compose -f ${this.composeFilePath} run --rm -T --entrypoint sh ${service} -c "cat ${filePath}"`;
|
|
debug('readFileFromService running:', cmd);
|
|
const output = execSync(cmd, {encoding: 'utf-8'}).toString();
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Find the container for a compose service by label.
|
|
*/
|
|
async getContainerForService(service: string): Promise<Container> {
|
|
debug('getContainerForService called for service:', service);
|
|
const containers = await this.docker.listContainers({
|
|
all: true,
|
|
filters: {
|
|
label: [
|
|
`com.docker.compose.project=${this.projectName}`,
|
|
`com.docker.compose.service=${service}`
|
|
]
|
|
}
|
|
});
|
|
if (containers.length === 0) {
|
|
throw new Error(`No container found for service: ${service}`);
|
|
}
|
|
if (containers.length > 1) {
|
|
throw new Error(`Multiple containers found for service: ${service}`);
|
|
}
|
|
const container = this.docker.getContainer(containers[0].Id);
|
|
debug('getContainerForService returning container:', container.id);
|
|
return container;
|
|
}
|
|
|
|
/**
|
|
* Get the Docker network for the compose project.
|
|
*/
|
|
async getNetwork(): Promise<Docker.Network> {
|
|
debug('getNetwork called');
|
|
const networks = await this.docker.listNetworks({
|
|
filters: {label: [`com.docker.compose.project=${this.projectName}`]}
|
|
});
|
|
debug('getNetwork found networks:', networks.map(n => n.Id));
|
|
if (networks.length === 0) {
|
|
throw new Error('No Docker network found for the Compose project');
|
|
}
|
|
if (networks.length > 1) {
|
|
throw new Error('Multiple Docker networks found for the Compose project');
|
|
}
|
|
const networkId = networks[0].Id;
|
|
debug('getNetwork returning network ID:', networkId);
|
|
const network = this.docker.getNetwork(networkId);
|
|
debug('getNetwork returning network:', network.id);
|
|
return network;
|
|
}
|
|
}
|
|
|