From 13f5d03338973b950097aa9b656ee390b0b46c2c Mon Sep 17 00:00:00 2001 From: Henri Normak Date: Tue, 13 May 2025 15:10:21 +0300 Subject: [PATCH] refactor: avoid loading compose info unless needed --- .../src/container-runtime/clients/client.ts | 8 +- .../clients/compose/compose-client.ts | 174 +++++------------- .../src/container-runtime/clients/types.ts | 3 +- .../testcontainers/src/utils/test-helper.ts | 3 +- 4 files changed, 57 insertions(+), 131 deletions(-) diff --git a/packages/testcontainers/src/container-runtime/clients/client.ts b/packages/testcontainers/src/container-runtime/clients/client.ts index 7a048d24a..1c6a51ba1 100644 --- a/packages/testcontainers/src/container-runtime/clients/client.ts +++ b/packages/testcontainers/src/container-runtime/clients/client.ts @@ -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( @@ -92,7 +92,7 @@ async function initStrategy(strategy: ContainerRuntimeClientStrategy): Promise; up(options: ComposeOptions, services?: Array): Promise; pull(options: ComposeOptions, services?: Array): Promise; stop(options: ComposeOptions): Promise; down(options: ComposeOptions, downOptions: ComposeDownOptions): Promise; } -export async function getComposeClient(environment: NodeJS.ProcessEnv): Promise { - 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 { - 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 | undefined): Promise { - 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 { + if (this.info !== undefined) { + return this.info; } - } - async pull(options: ComposeOptions, services: Array | undefined): Promise { 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 { - 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 { + if (this.client !== undefined) { + return this.client; } - } - async down(options: ComposeOptions, downOptions: ComposeDownOptions): Promise { - 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 | undefined): Promise { + 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) { @@ -145,13 +88,15 @@ class ComposeV2Client implements ComposeClient { } async pull(options: ComposeOptions, services: Array | undefined): Promise { + 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) { @@ -162,9 +107,11 @@ class ComposeV2Client implements ComposeClient { } async stop(options: ComposeOptions): Promise { + 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) => @@ -174,9 +121,10 @@ class ComposeV2Client implements ComposeClient { } async down(options: ComposeOptions, downOptions: ComposeDownOptions): Promise { + 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), }); @@ -189,26 +137,6 @@ class ComposeV2Client implements ComposeClient { } } -class MissingComposeClient implements ComposeClient { - public readonly info = undefined; - - up(): Promise { - throw new Error("Compose is not installed"); - } - - pull(): Promise { - throw new Error("Compose is not installed"); - } - - stop(): Promise { - throw new Error("Compose is not installed"); - } - - down(): Promise { - 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): Promise { const error = err instanceof Error ? err : new Error(err.err.trim()); diff --git a/packages/testcontainers/src/container-runtime/clients/types.ts b/packages/testcontainers/src/container-runtime/clients/types.ts index 4d90d25c7..d7d34f4f6 100644 --- a/packages/testcontainers/src/container-runtime/clients/types.ts +++ b/packages/testcontainers/src/container-runtime/clients/types.ts @@ -1,7 +1,6 @@ export type Info = { node: NodeInfo; containerRuntime: ContainerRuntimeInfo; - compose: ComposeInfo; }; export type NodeInfo = { @@ -28,7 +27,7 @@ export type ContainerRuntimeInfo = { export type ComposeInfo = | { version: string; - compatability: "v1" | "v2"; + compatibility: "v1" | "v2"; } | undefined; diff --git a/packages/testcontainers/src/utils/test-helper.ts b/packages/testcontainers/src/utils/test-helper.ts index dc1ff07b9..c6773c6d9 100644 --- a/packages/testcontainers/src/utils/test-helper.ts +++ b/packages/testcontainers/src/utils/test-helper.ts @@ -92,7 +92,8 @@ export const getVolumeNames = async (): Promise => { export const composeContainerName = async (serviceName: string, index = 1): Promise => { 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) => {