Skip to content

Commit dd7cf51

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

File tree

4 files changed

+62
-141
lines changed

4 files changed

+62
-141
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: 56 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +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-
}
15+
export function getComposeClient(environment: NodeJS.ProcessEnv): ComposeClient {
16+
return new LazyComposeClient(environment);
2617
}
2718

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) {
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) {}
23+
24+
async getInfo(): Promise<ComposeInfo | undefined> {
25+
if (this.info !== undefined) {
26+
return this.info;
27+
}
28+
3529
try {
36-
return {
37-
version: (await dockerComposeV1.version()).data.version,
38-
compatability: "v1",
30+
this.info = {
31+
version: (await v2.version()).data.version,
32+
compatibility: "v2",
3933
};
40-
} catch {
41-
return undefined;
34+
} catch (err) {
35+
try {
36+
this.info = {
37+
version: (await v1.version()).data.version,
38+
compatibility: "v1",
39+
};
40+
} catch {
41+
return undefined;
42+
}
4243
}
44+
45+
return this.info;
4346
}
44-
}
4547

46-
class ComposeV1Client implements ComposeClient {
47-
constructor(
48-
public readonly info: ComposeInfo,
49-
private readonly environment: NodeJS.ProcessEnv
50-
) {}
48+
private async getClient(): Promise<typeof v1 | typeof v2> {
49+
if (this.client !== undefined) {
50+
return this.client;
51+
}
5152

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-
});
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;
7163
}
7264
}
7365

74-
async pull(options: ComposeOptions, services: Array<string> | undefined): Promise<void> {
66+
async up(options: ComposeOptions, services: Array<string> | undefined): Promise<void> {
67+
const client = await this.getClient();
68+
7569
try {
7670
if (services) {
7771
log.info(`Pulling Compose environment images "${services.join('", "')}"...`);
78-
await v1.pullMany(services, await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
72+
await client.pullMany(services, await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
7973
} else {
8074
log.info(`Pulling Compose environment images...`);
81-
await v1.pullAll(await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
75+
await client.pullAll(await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
8276
}
8377
log.info(`Pulled Compose environment`);
8478
} catch (err) {
@@ -88,70 +82,16 @@ class ComposeV1Client implements ComposeClient {
8882
}
8983
}
9084

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-
);
100-
}
101-
}
102-
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-
);
115-
}
116-
}
117-
}
118-
119-
class ComposeV2Client implements ComposeClient {
120-
constructor(
121-
public readonly info: ComposeInfo,
122-
private readonly environment: NodeJS.ProcessEnv
123-
) {}
124-
125-
async up(options: ComposeOptions, services: Array<string> | undefined): Promise<void> {
126-
try {
127-
if (services) {
128-
log.info(`Upping Compose environment services ${services.join(", ")}...`);
129-
await v2.upMany(services, await defaultComposeOptions(this.environment, options));
130-
} else {
131-
log.info(`Upping Compose environment...`);
132-
await v2.upAll(await defaultComposeOptions(this.environment, options));
133-
}
134-
log.info(`Upped Compose environment`);
135-
} catch (err) {
136-
await handleAndRethrow(err, async (error: Error) => {
137-
try {
138-
log.error(`Failed to up Compose environment: ${error.message}`);
139-
await this.down(options, { removeVolumes: true, timeout: 0 });
140-
} catch {
141-
log.error(`Failed to down Compose environment after failed up`);
142-
}
143-
});
144-
}
145-
}
146-
14785
async pull(options: ComposeOptions, services: Array<string> | undefined): Promise<void> {
86+
const client = await this.getClient();
87+
14888
try {
14989
if (services) {
15090
log.info(`Pulling Compose environment images "${services.join('", "')}"...`);
151-
await v2.pullMany(services, await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
91+
await client.pullMany(services, await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
15292
} else {
15393
log.info(`Pulling Compose environment images...`);
154-
await v2.pullAll(await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
94+
await client.pullAll(await defaultComposeOptions(this.environment, { ...options, logger: pullLog }));
15595
}
15696
log.info(`Pulled Compose environment`);
15797
} catch (err) {
@@ -162,9 +102,11 @@ class ComposeV2Client implements ComposeClient {
162102
}
163103

164104
async stop(options: ComposeOptions): Promise<void> {
105+
const client = await this.getClient();
106+
165107
try {
166108
log.info(`Stopping Compose environment...`);
167-
await v2.stop(await defaultComposeOptions(this.environment, options));
109+
await client.stop(await defaultComposeOptions(this.environment, options));
168110
log.info(`Stopped Compose environment`);
169111
} catch (err) {
170112
await handleAndRethrow(err, async (error: Error) =>
@@ -174,9 +116,10 @@ class ComposeV2Client implements ComposeClient {
174116
}
175117

176118
async down(options: ComposeOptions, downOptions: ComposeDownOptions): Promise<void> {
119+
const client = await this.getClient();
177120
try {
178121
log.info(`Downing Compose environment...`);
179-
await v2.down({
122+
await client.down({
180123
...(await defaultComposeOptions(this.environment, options)),
181124
commandOptions: composeDownCommandOptions(downOptions),
182125
});
@@ -189,26 +132,6 @@ class ComposeV2Client implements ComposeClient {
189132
}
190133
}
191134

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-
212135
// eslint-disable-next-line @typescript-eslint/no-explicit-any
213136
async function handleAndRethrow(err: any, handle: (error: Error) => Promise<void>): Promise<never> {
214137
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)