Skip to content

Avoid loading compose info unless needed #1001

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { DockerImageClient } from "./image/docker-image-client";
import { ImageClient } from "./image/image-client";
import { DockerNetworkClient } from "./network/docker-network-client";
import { NetworkClient } from "./network/network-client";
import { ComposeInfo, ContainerRuntimeInfo, Info, NodeInfo } from "./types";
import { ContainerRuntimeInfo, Info, NodeInfo } from "./types";

export class ContainerRuntimeClient {
constructor(
Expand Down Expand Up @@ -92,7 +92,7 @@ async function initStrategy(strategy: ContainerRuntimeClientStrategy): Promise<C
const host = await resolveHost(dockerode, result, indexServerAddress);

log.trace("Fetching Compose info...");
const composeClient = await getComposeClient(result.composeEnvironment);
const composeClient = getComposeClient(result.composeEnvironment);

const nodeInfo: NodeInfo = {
version: process.version,
Expand Down Expand Up @@ -123,9 +123,7 @@ async function initStrategy(strategy: ContainerRuntimeClientStrategy): Promise<C
labels: dockerodeInfo.Labels ? dockerodeInfo.Labels : [],
};

const composeInfo: ComposeInfo = composeClient.info;

const info: Info = { node: nodeInfo, containerRuntime: containerRuntimeInfo, compose: composeInfo };
const info: Info = { node: nodeInfo, containerRuntime: containerRuntimeInfo };

log.trace(`Container runtime info:\n${JSON.stringify(info, null, 2)}`);
return new ContainerRuntimeClient(info, composeClient, containerClient, imageClient, networkClient);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,135 +1,78 @@
import { default as dockerComposeV1, default as v1, v2 as dockerComposeV2, v2 } from "docker-compose";
import { default as v1, v2 } from "docker-compose";
import { log, pullLog } from "../../../common";
import { ComposeInfo } from "../types";
import { defaultComposeOptions } from "./default-compose-options";
import { ComposeDownOptions, ComposeOptions } from "./types";

export interface ComposeClient {
info: ComposeInfo;
getInfo(): Promise<ComposeInfo>;
up(options: ComposeOptions, services?: Array<string>): Promise<void>;
pull(options: ComposeOptions, services?: Array<string>): Promise<void>;
stop(options: ComposeOptions): Promise<void>;
down(options: ComposeOptions, downOptions: ComposeDownOptions): Promise<void>;
}

export async function getComposeClient(environment: NodeJS.ProcessEnv): Promise<ComposeClient> {
const info = await getComposeInfo();

switch (info?.compatability) {
case undefined:
return new MissingComposeClient();
case "v1":
return new ComposeV1Client(info, environment);
case "v2":
return new ComposeV2Client(info, environment);
}
}

async function getComposeInfo(): Promise<ComposeInfo | undefined> {
try {
return {
version: (await dockerComposeV2.version()).data.version,
compatability: "v2",
};
} catch (err) {
try {
return {
version: (await dockerComposeV1.version()).data.version,
compatability: "v1",
};
} catch {
return undefined;
}
}
export function getComposeClient(environment: NodeJS.ProcessEnv): ComposeClient {
return new LazyComposeClient(environment);
}

class ComposeV1Client implements ComposeClient {
constructor(
public readonly info: ComposeInfo,
private readonly environment: NodeJS.ProcessEnv
) {}
class LazyComposeClient implements ComposeClient {
private info: ComposeInfo | undefined = undefined;
private client: typeof v1 | typeof v2 | undefined = undefined;
constructor(private readonly environment: NodeJS.ProcessEnv) {}

async up(options: ComposeOptions, services: Array<string> | undefined): Promise<void> {
try {
if (services) {
log.info(`Upping Compose environment services ${services.join(", ")}...`);
await v1.upMany(services, await defaultComposeOptions(this.environment, options));
} else {
log.info(`Upping Compose environment...`);
await v1.upAll(await defaultComposeOptions(this.environment, options));
}
log.info(`Upped Compose environment`);
} catch (err) {
await handleAndRethrow(err, async (error: Error) => {
try {
log.error(`Failed to up Compose environment: ${error.message}`);
await this.down(options, { removeVolumes: true, timeout: 0 });
} catch {
log.error(`Failed to down Compose environment after failed up`);
}
});
async getInfo(): Promise<ComposeInfo | undefined> {
if (this.info !== undefined) {
return this.info;
}
}

async pull(options: ComposeOptions, services: Array<string> | undefined): Promise<void> {
try {
if (services) {
log.info(`Pulling Compose environment images "${services.join('", "')}"...`);
await v1.pullMany(services, await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
} else {
log.info(`Pulling Compose environment images...`);
await v1.pullAll(await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
}
log.info(`Pulled Compose environment`);
this.info = {
version: (await v2.version()).data.version,
compatibility: "v2",
};
} catch (err) {
await handleAndRethrow(err, async (error: Error) =>
log.error(`Failed to pull Compose environment images: ${error.message}`)
);
try {
this.info = {
version: (await v1.version()).data.version,
compatibility: "v1",
};
} catch {
return undefined;
}
}

return this.info;
}

async stop(options: ComposeOptions): Promise<void> {
try {
log.info(`Stopping Compose environment...`);
await v1.stop(await defaultComposeOptions(this.environment, options));
log.info(`Stopped Compose environment`);
} catch (err) {
await handleAndRethrow(err, async (error: Error) =>
log.error(`Failed to stop Compose environment: ${error.message}`)
);
private async getClient(): Promise<typeof v1 | typeof v2> {
if (this.client !== undefined) {
return this.client;
}
}

async down(options: ComposeOptions, downOptions: ComposeDownOptions): Promise<void> {
try {
log.info(`Downing Compose environment...`);
await v1.down({
...(await defaultComposeOptions(this.environment, options)),
commandOptions: composeDownCommandOptions(downOptions),
});
log.info(`Downed Compose environment`);
} catch (err) {
await handleAndRethrow(err, async (error: Error) =>
log.error(`Failed to down Compose environment: ${error.message}`)
);
const info = await this.getInfo();
switch (info?.compatibility) {
case undefined:
throw new Error("Compose is not installed");
case "v1":
this.client = v1;
return v1;
case "v2":
this.client = v2;
return v2;
}
}
}

class ComposeV2Client implements ComposeClient {
constructor(
public readonly info: ComposeInfo,
private readonly environment: NodeJS.ProcessEnv
) {}

async up(options: ComposeOptions, services: Array<string> | undefined): Promise<void> {
const client = await this.getClient();

try {
if (services) {
log.info(`Upping Compose environment services ${services.join(", ")}...`);
await v2.upMany(services, await defaultComposeOptions(this.environment, options));
await client.upMany(services, await defaultComposeOptions(this.environment, options));
} else {
log.info(`Upping Compose environment...`);
await v2.upAll(await defaultComposeOptions(this.environment, options));
await client.upAll(await defaultComposeOptions(this.environment, options));
}
log.info(`Upped Compose environment`);
} catch (err) {
Expand All @@ -145,13 +88,15 @@ class ComposeV2Client implements ComposeClient {
}

async pull(options: ComposeOptions, services: Array<string> | undefined): Promise<void> {
const client = await this.getClient();

try {
if (services) {
log.info(`Pulling Compose environment images "${services.join('", "')}"...`);
await v2.pullMany(services, await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
await client.pullMany(services, await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
} else {
log.info(`Pulling Compose environment images...`);
await v2.pullAll(await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
await client.pullAll(await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
}
log.info(`Pulled Compose environment`);
} catch (err) {
Expand All @@ -162,9 +107,11 @@ class ComposeV2Client implements ComposeClient {
}

async stop(options: ComposeOptions): Promise<void> {
const client = await this.getClient();

try {
log.info(`Stopping Compose environment...`);
await v2.stop(await defaultComposeOptions(this.environment, options));
await client.stop(await defaultComposeOptions(this.environment, options));
log.info(`Stopped Compose environment`);
} catch (err) {
await handleAndRethrow(err, async (error: Error) =>
Expand All @@ -174,9 +121,10 @@ class ComposeV2Client implements ComposeClient {
}

async down(options: ComposeOptions, downOptions: ComposeDownOptions): Promise<void> {
const client = await this.getClient();
try {
log.info(`Downing Compose environment...`);
await v2.down({
await client.down({
...(await defaultComposeOptions(this.environment, options)),
commandOptions: composeDownCommandOptions(downOptions),
});
Expand All @@ -189,26 +137,6 @@ class ComposeV2Client implements ComposeClient {
}
}

class MissingComposeClient implements ComposeClient {
public readonly info = undefined;

up(): Promise<void> {
throw new Error("Compose is not installed");
}

pull(): Promise<void> {
throw new Error("Compose is not installed");
}

stop(): Promise<void> {
throw new Error("Compose is not installed");
}

down(): Promise<void> {
throw new Error("Compose is not installed");
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function handleAndRethrow(err: any, handle: (error: Error) => Promise<void>): Promise<never> {
const error = err instanceof Error ? err : new Error(err.err.trim());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export type Info = {
node: NodeInfo;
containerRuntime: ContainerRuntimeInfo;
compose: ComposeInfo;
};

export type NodeInfo = {
Expand All @@ -28,7 +27,7 @@ export type ContainerRuntimeInfo = {
export type ComposeInfo =
| {
version: string;
compatability: "v1" | "v2";
compatibility: "v1" | "v2";
}
| undefined;

Expand Down
3 changes: 2 additions & 1 deletion packages/testcontainers/src/utils/test-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export const getVolumeNames = async (): Promise<string[]> => {

export const composeContainerName = async (serviceName: string, index = 1): Promise<string> => {
const client = await getContainerRuntimeClient();
return client.info.compose?.version.startsWith("1.") ? `${serviceName}_${index}` : `${serviceName}-${index}`;
const info = await client.compose.getInfo();
return info?.version.startsWith("1.") ? `${serviceName}_${index}` : `${serviceName}-${index}`;
};

export const waitForDockerEvent = async (eventStream: Readable, eventName: string, times = 1) => {
Expand Down