Skip to content

Commit 13f5d03

Browse files
committed
refactor: avoid loading compose info unless needed
1 parent 25d5b56 commit 13f5d03

File tree

4 files changed

+57
-131
lines changed

4 files changed

+57
-131
lines changed

packages/testcontainers/src/container-runtime/clients/client.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { DockerImageClient } from "./image/docker-image-client";
1717
import { ImageClient } from "./image/image-client";
1818
import { DockerNetworkClient } from "./network/docker-network-client";
1919
import { NetworkClient } from "./network/network-client";
20-
import { ComposeInfo, ContainerRuntimeInfo, Info, NodeInfo } from "./types";
20+
import { ContainerRuntimeInfo, Info, NodeInfo } from "./types";
2121

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

9494
log.trace("Fetching Compose info...");
95-
const composeClient = await getComposeClient(result.composeEnvironment);
95+
const composeClient = getComposeClient(result.composeEnvironment);
9696

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

126-
const composeInfo: ComposeInfo = composeClient.info;
127-
128-
const info: Info = { node: nodeInfo, containerRuntime: containerRuntimeInfo, compose: composeInfo };
126+
const info: Info = { node: nodeInfo, containerRuntime: containerRuntimeInfo };
129127

130128
log.trace(`Container runtime info:\n${JSON.stringify(info, null, 2)}`);
131129
return new ContainerRuntimeClient(info, composeClient, containerClient, imageClient, networkClient);

packages/testcontainers/src/container-runtime/clients/compose/compose-client.ts

Lines changed: 51 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,135 +1,78 @@
1-
import { default as dockerComposeV1, default as v1, v2 as dockerComposeV2, v2 } from "docker-compose";
1+
import { default as v1, v2 } from "docker-compose";
22
import { log, pullLog } from "../../../common";
33
import { ComposeInfo } from "../types";
44
import { defaultComposeOptions } from "./default-compose-options";
55
import { ComposeDownOptions, ComposeOptions } from "./types";
66

77
export interface ComposeClient {
8-
info: ComposeInfo;
8+
getInfo(): Promise<ComposeInfo>;
99
up(options: ComposeOptions, services?: Array<string>): Promise<void>;
1010
pull(options: ComposeOptions, services?: Array<string>): Promise<void>;
1111
stop(options: ComposeOptions): Promise<void>;
1212
down(options: ComposeOptions, downOptions: ComposeDownOptions): Promise<void>;
1313
}
1414

15-
export async function getComposeClient(environment: NodeJS.ProcessEnv): Promise<ComposeClient> {
16-
const info = await getComposeInfo();
17-
18-
switch (info?.compatability) {
19-
case undefined:
20-
return new MissingComposeClient();
21-
case "v1":
22-
return new ComposeV1Client(info, environment);
23-
case "v2":
24-
return new ComposeV2Client(info, environment);
25-
}
26-
}
27-
28-
async function getComposeInfo(): Promise<ComposeInfo | undefined> {
29-
try {
30-
return {
31-
version: (await dockerComposeV2.version()).data.version,
32-
compatability: "v2",
33-
};
34-
} catch (err) {
35-
try {
36-
return {
37-
version: (await dockerComposeV1.version()).data.version,
38-
compatability: "v1",
39-
};
40-
} catch {
41-
return undefined;
42-
}
43-
}
15+
export function getComposeClient(environment: NodeJS.ProcessEnv): ComposeClient {
16+
return new LazyComposeClient(environment);
4417
}
4518

46-
class ComposeV1Client implements ComposeClient {
47-
constructor(
48-
public readonly info: ComposeInfo,
49-
private readonly environment: NodeJS.ProcessEnv
50-
) {}
19+
class LazyComposeClient implements ComposeClient {
20+
private info: ComposeInfo | undefined = undefined;
21+
private client: typeof v1 | typeof v2 | undefined = undefined;
22+
constructor(private readonly environment: NodeJS.ProcessEnv) {}
5123

52-
async up(options: ComposeOptions, services: Array<string> | undefined): Promise<void> {
53-
try {
54-
if (services) {
55-
log.info(`Upping Compose environment services ${services.join(", ")}...`);
56-
await v1.upMany(services, await defaultComposeOptions(this.environment, options));
57-
} else {
58-
log.info(`Upping Compose environment...`);
59-
await v1.upAll(await defaultComposeOptions(this.environment, options));
60-
}
61-
log.info(`Upped Compose environment`);
62-
} catch (err) {
63-
await handleAndRethrow(err, async (error: Error) => {
64-
try {
65-
log.error(`Failed to up Compose environment: ${error.message}`);
66-
await this.down(options, { removeVolumes: true, timeout: 0 });
67-
} catch {
68-
log.error(`Failed to down Compose environment after failed up`);
69-
}
70-
});
24+
async getInfo(): Promise<ComposeInfo | undefined> {
25+
if (this.info !== undefined) {
26+
return this.info;
7127
}
72-
}
7328

74-
async pull(options: ComposeOptions, services: Array<string> | undefined): Promise<void> {
7529
try {
76-
if (services) {
77-
log.info(`Pulling Compose environment images "${services.join('", "')}"...`);
78-
await v1.pullMany(services, await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
79-
} else {
80-
log.info(`Pulling Compose environment images...`);
81-
await v1.pullAll(await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
82-
}
83-
log.info(`Pulled Compose environment`);
30+
this.info = {
31+
version: (await v2.version()).data.version,
32+
compatibility: "v2",
33+
};
8434
} catch (err) {
85-
await handleAndRethrow(err, async (error: Error) =>
86-
log.error(`Failed to pull Compose environment images: ${error.message}`)
87-
);
35+
try {
36+
this.info = {
37+
version: (await v1.version()).data.version,
38+
compatibility: "v1",
39+
};
40+
} catch {
41+
return undefined;
42+
}
8843
}
44+
45+
return this.info;
8946
}
9047

91-
async stop(options: ComposeOptions): Promise<void> {
92-
try {
93-
log.info(`Stopping Compose environment...`);
94-
await v1.stop(await defaultComposeOptions(this.environment, options));
95-
log.info(`Stopped Compose environment`);
96-
} catch (err) {
97-
await handleAndRethrow(err, async (error: Error) =>
98-
log.error(`Failed to stop Compose environment: ${error.message}`)
99-
);
48+
private async getClient(): Promise<typeof v1 | typeof v2> {
49+
if (this.client !== undefined) {
50+
return this.client;
10051
}
101-
}
10252

103-
async down(options: ComposeOptions, downOptions: ComposeDownOptions): Promise<void> {
104-
try {
105-
log.info(`Downing Compose environment...`);
106-
await v1.down({
107-
...(await defaultComposeOptions(this.environment, options)),
108-
commandOptions: composeDownCommandOptions(downOptions),
109-
});
110-
log.info(`Downed Compose environment`);
111-
} catch (err) {
112-
await handleAndRethrow(err, async (error: Error) =>
113-
log.error(`Failed to down Compose environment: ${error.message}`)
114-
);
53+
const info = await this.getInfo();
54+
switch (info?.compatibility) {
55+
case undefined:
56+
throw new Error("Compose is not installed");
57+
case "v1":
58+
this.client = v1;
59+
return v1;
60+
case "v2":
61+
this.client = v2;
62+
return v2;
11563
}
11664
}
117-
}
118-
119-
class ComposeV2Client implements ComposeClient {
120-
constructor(
121-
public readonly info: ComposeInfo,
122-
private readonly environment: NodeJS.ProcessEnv
123-
) {}
12465

12566
async up(options: ComposeOptions, services: Array<string> | undefined): Promise<void> {
67+
const client = await this.getClient();
68+
12669
try {
12770
if (services) {
12871
log.info(`Upping Compose environment services ${services.join(", ")}...`);
129-
await v2.upMany(services, await defaultComposeOptions(this.environment, options));
72+
await client.upMany(services, await defaultComposeOptions(this.environment, options));
13073
} else {
13174
log.info(`Upping Compose environment...`);
132-
await v2.upAll(await defaultComposeOptions(this.environment, options));
75+
await client.upAll(await defaultComposeOptions(this.environment, options));
13376
}
13477
log.info(`Upped Compose environment`);
13578
} catch (err) {
@@ -145,13 +88,15 @@ class ComposeV2Client implements ComposeClient {
14588
}
14689

14790
async pull(options: ComposeOptions, services: Array<string> | undefined): Promise<void> {
91+
const client = await this.getClient();
92+
14893
try {
14994
if (services) {
15095
log.info(`Pulling Compose environment images "${services.join('", "')}"...`);
151-
await v2.pullMany(services, await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
96+
await client.pullMany(services, await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
15297
} else {
15398
log.info(`Pulling Compose environment images...`);
154-
await v2.pullAll(await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
99+
await client.pullAll(await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
155100
}
156101
log.info(`Pulled Compose environment`);
157102
} catch (err) {
@@ -162,9 +107,11 @@ class ComposeV2Client implements ComposeClient {
162107
}
163108

164109
async stop(options: ComposeOptions): Promise<void> {
110+
const client = await this.getClient();
111+
165112
try {
166113
log.info(`Stopping Compose environment...`);
167-
await v2.stop(await defaultComposeOptions(this.environment, options));
114+
await client.stop(await defaultComposeOptions(this.environment, options));
168115
log.info(`Stopped Compose environment`);
169116
} catch (err) {
170117
await handleAndRethrow(err, async (error: Error) =>
@@ -174,9 +121,10 @@ class ComposeV2Client implements ComposeClient {
174121
}
175122

176123
async down(options: ComposeOptions, downOptions: ComposeDownOptions): Promise<void> {
124+
const client = await this.getClient();
177125
try {
178126
log.info(`Downing Compose environment...`);
179-
await v2.down({
127+
await client.down({
180128
...(await defaultComposeOptions(this.environment, options)),
181129
commandOptions: composeDownCommandOptions(downOptions),
182130
});
@@ -189,26 +137,6 @@ class ComposeV2Client implements ComposeClient {
189137
}
190138
}
191139

192-
class MissingComposeClient implements ComposeClient {
193-
public readonly info = undefined;
194-
195-
up(): Promise<void> {
196-
throw new Error("Compose is not installed");
197-
}
198-
199-
pull(): Promise<void> {
200-
throw new Error("Compose is not installed");
201-
}
202-
203-
stop(): Promise<void> {
204-
throw new Error("Compose is not installed");
205-
}
206-
207-
down(): Promise<void> {
208-
throw new Error("Compose is not installed");
209-
}
210-
}
211-
212140
// eslint-disable-next-line @typescript-eslint/no-explicit-any
213141
async function handleAndRethrow(err: any, handle: (error: Error) => Promise<void>): Promise<never> {
214142
const error = err instanceof Error ? err : new Error(err.err.trim());

packages/testcontainers/src/container-runtime/clients/types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
export type Info = {
22
node: NodeInfo;
33
containerRuntime: ContainerRuntimeInfo;
4-
compose: ComposeInfo;
54
};
65

76
export type NodeInfo = {
@@ -28,7 +27,7 @@ export type ContainerRuntimeInfo = {
2827
export type ComposeInfo =
2928
| {
3029
version: string;
31-
compatability: "v1" | "v2";
30+
compatibility: "v1" | "v2";
3231
}
3332
| undefined;
3433

packages/testcontainers/src/utils/test-helper.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ export const getVolumeNames = async (): Promise<string[]> => {
9292

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

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

0 commit comments

Comments
 (0)