From 93fd5864f8232bb14b27ad1423d25975711bac1d Mon Sep 17 00:00:00 2001 From: Victor Date: Mon, 27 Jan 2025 10:45:41 +0100 Subject: [PATCH 1/3] feat: implement `.withAutoRemove` --- docs/features/containers.md | 20 +++++++++++++++ .../docker-compose-environment.ts | 3 ++- .../generic-container.test.ts | 25 +++++++++++++++++++ .../generic-container/generic-container.ts | 12 +++++++-- .../started-generic-container.ts | 5 ++-- packages/testcontainers/src/test-container.ts | 1 + .../testcontainers/src/utils/test-helper.ts | 10 ++++++++ 7 files changed, 71 insertions(+), 5 deletions(-) diff --git a/docs/features/containers.md b/docs/features/containers.md index 1f5501e3c..e26e759e8 100644 --- a/docs/features/containers.md +++ b/docs/features/containers.md @@ -331,6 +331,26 @@ const container = await new GenericContainer("alpine").start(); await container.stop({ remove: false }); ``` +Alternatively, you can disable automatic removal while configuring the container: + +```javascript +const container = await new GenericContainer("alpine") + .withAutoRemove(false) + .start(); + +await container.stop() +``` + +The value specified to `.withAutoRemove()` can be overridden by `.stop()`: + +```javascript +const container = await new GenericContainer("alpine") + .withAutoRemove(false) + .start(); + +await container.stop({ remove: true }) // The container is stopped *AND* removed +``` + Volumes created by the container are removed when stopped. This is configurable: ```javascript diff --git a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts index 48043fc7a..3af3c9464 100644 --- a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts +++ b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts @@ -168,7 +168,8 @@ export class DockerComposeEnvironment { inspectResult, boundPorts, containerName, - waitStrategy + waitStrategy, + true ); }) ) diff --git a/packages/testcontainers/src/generic-container/generic-container.test.ts b/packages/testcontainers/src/generic-container/generic-container.test.ts index 9eff27f52..1e5715ab3 100644 --- a/packages/testcontainers/src/generic-container/generic-container.test.ts +++ b/packages/testcontainers/src/generic-container/generic-container.test.ts @@ -6,6 +6,7 @@ import { checkContainerIsHealthy, getDockerEventStream, getRunningContainerNames, + getStoppedContainerNames, waitForDockerEvent, } from "../utils/test-helper"; import { getContainerRuntimeClient } from "../container-runtime"; @@ -519,6 +520,30 @@ describe("GenericContainer", () => { expect(await getRunningContainerNames()).not.toContain(container.getName()); }); + it("should stop but not remove the container", async () => { + const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14") + .withName(`container-${new RandomUuid().nextUuid()}`) + .withAutoRemove(false) + .start(); + + await container.stop(); + + expect(await getRunningContainerNames()).not.toContain(container.getName().replace("/", "")); + expect(await getStoppedContainerNames()).toContain(container.getName().replace("/", "")); + }); + + it("should stop and override .withAutoRemove", async () => { + const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14") + .withName(`container-${new RandomUuid().nextUuid()}`) + .withAutoRemove(false) + .start(); + + await container.stop({ remove: true }); + + expect(await getRunningContainerNames()).not.toContain(container.getName().replace("/", "")); + expect(await getStoppedContainerNames()).not.toContain(container.getName().replace("/", "")); + }); + it("should build a target stage", async () => { const context = path.resolve(fixtures, "docker-multi-stage"); const firstContainer = await GenericContainer.fromDockerfile(context).withTarget("first").build(); diff --git a/packages/testcontainers/src/generic-container/generic-container.ts b/packages/testcontainers/src/generic-container/generic-container.ts index 4ebe2899f..e07976281 100644 --- a/packages/testcontainers/src/generic-container/generic-container.ts +++ b/packages/testcontainers/src/generic-container/generic-container.ts @@ -50,6 +50,7 @@ export class GenericContainer implements TestContainer { protected environment: Record = {}; protected exposedPorts: PortWithOptionalBinding[] = []; protected reuse = false; + protected autoRemove = true; protected networkMode?: string; protected networkAliases: string[] = []; protected pullPolicy: ImagePullPolicy = PullPolicy.defaultPolicy(); @@ -157,7 +158,8 @@ export class GenericContainer implements TestContainer { inspectResult, boundPorts, inspectResult.Name, - this.waitStrategy + this.waitStrategy, + this.autoRemove ); } @@ -221,7 +223,8 @@ export class GenericContainer implements TestContainer { inspectResult, boundPorts, inspectResult.Name, - this.waitStrategy + this.waitStrategy, + this.autoRemove ); if (this.containerStarted) { @@ -430,6 +433,11 @@ export class GenericContainer implements TestContainer { return this; } + public withAutoRemove(autoRemove: boolean): this { + this.autoRemove = autoRemove; + return this; + } + public withPullPolicy(pullPolicy: ImagePullPolicy): this { this.pullPolicy = pullPolicy; return this; diff --git a/packages/testcontainers/src/generic-container/started-generic-container.ts b/packages/testcontainers/src/generic-container/started-generic-container.ts index c5ff8865a..38aeb106f 100644 --- a/packages/testcontainers/src/generic-container/started-generic-container.ts +++ b/packages/testcontainers/src/generic-container/started-generic-container.ts @@ -22,7 +22,8 @@ export class StartedGenericContainer implements StartedTestContainer { private inspectResult: ContainerInspectInfo, private boundPorts: BoundPorts, private readonly name: string, - private readonly waitStrategy: WaitStrategy + private readonly waitStrategy: WaitStrategy, + private readonly autoRemove: boolean ) {} protected containerIsStopping?(): Promise; @@ -71,7 +72,7 @@ export class StartedGenericContainer implements StartedTestContainer { await this.containerIsStopping(); } - const resolvedOptions: StopOptions = { remove: true, timeout: 0, removeVolumes: true, ...options }; + const resolvedOptions: StopOptions = { remove: this.autoRemove, timeout: 0, removeVolumes: true, ...options }; await client.container.stop(this.container, { timeout: resolvedOptions.timeout }); if (resolvedOptions.remove) { await client.container.remove(this.container, { removeVolumes: resolvedOptions.removeVolumes }); diff --git a/packages/testcontainers/src/test-container.ts b/packages/testcontainers/src/test-container.ts index 056921ac8..b40b3347f 100644 --- a/packages/testcontainers/src/test-container.ts +++ b/packages/testcontainers/src/test-container.ts @@ -40,6 +40,7 @@ export interface TestContainer { withUser(user: string): this; withPullPolicy(pullPolicy: ImagePullPolicy): this; withReuse(): this; + withAutoRemove(autoRemove: boolean): this; withCopyFilesToContainer(filesToCopy: FileToCopy[]): this; withCopyDirectoriesToContainer(directoriesToCopy: DirectoryToCopy[]): this; withCopyContentToContainer(contentsToCopy: ContentToCopy[]): this; diff --git a/packages/testcontainers/src/utils/test-helper.ts b/packages/testcontainers/src/utils/test-helper.ts index 47640dbe4..aa7ff5b3f 100644 --- a/packages/testcontainers/src/utils/test-helper.ts +++ b/packages/testcontainers/src/utils/test-helper.ts @@ -44,6 +44,16 @@ export const getRunningContainerNames = async (): Promise => { .map((containerName) => containerName.replace("/", "")); }; +export const getStoppedContainerNames = async (): Promise => { + const dockerode = (await getContainerRuntimeClient()).container.dockerode; + const containers = await dockerode.listContainers({ all: true }); + return containers + .filter((container) => container.State === "exited") + .map((container) => container.Names) + .reduce((result, containerNames) => [...result, ...containerNames], []) + .map((containerName) => containerName.replace("/", "")); +}; + export const getContainerIds = async (): Promise => { const dockerode = (await getContainerRuntimeClient()).container.dockerode; const containers = await dockerode.listContainers({ all: true }); From 037f27e773887ff6b2ef38a45e6839c24b5487c8 Mon Sep 17 00:00:00 2001 From: digital88 Date: Tue, 18 Mar 2025 21:51:58 +0300 Subject: [PATCH 2/3] fix tests for pr #905 --- .../generic-container/generic-container.test.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/testcontainers/src/generic-container/generic-container.test.ts b/packages/testcontainers/src/generic-container/generic-container.test.ts index 6c8544b03..89cb8ba06 100644 --- a/packages/testcontainers/src/generic-container/generic-container.test.ts +++ b/packages/testcontainers/src/generic-container/generic-container.test.ts @@ -525,10 +525,11 @@ describe("GenericContainer", { timeout: 180_000 }, () => { .withAutoRemove(false) .start(); - await container.stop(); - - expect(await getRunningContainerNames()).not.toContain(container.getName().replace("/", "")); - expect(await getStoppedContainerNames()).toContain(container.getName().replace("/", "")); + const stopped = await container.stop(); + const dockerode = (await getContainerRuntimeClient()).container.dockerode; + expect(stopped.getId()).toBeTruthy(); + const lowerLevelContainer = dockerode.getContainer(stopped.getId()); + expect((await lowerLevelContainer.inspect()).State.Status).toEqual("exited"); }); it("should stop and override .withAutoRemove", async () => { @@ -539,8 +540,11 @@ describe("GenericContainer", { timeout: 180_000 }, () => { await container.stop({ remove: true }); - expect(await getRunningContainerNames()).not.toContain(container.getName().replace("/", "")); - expect(await getStoppedContainerNames()).not.toContain(container.getName().replace("/", "")); + const stopped = await container.stop(); + const dockerode = (await getContainerRuntimeClient()).container.dockerode; + expect(stopped.getId()).toBeTruthy(); + const lowerLevelContainer = dockerode.getContainer(stopped.getId()); + await expect(lowerLevelContainer.inspect()).rejects.toThrow(/404/) // Error: (HTTP code 404) no such container }); it("should build a target stage", async () => { From 7a2ab37b7399b3d5645a452d46f57b087cd80690 Mon Sep 17 00:00:00 2001 From: digital88 Date: Tue, 18 Mar 2025 22:05:09 +0300 Subject: [PATCH 3/3] forgot to lint --- .../src/generic-container/generic-container.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/testcontainers/src/generic-container/generic-container.test.ts b/packages/testcontainers/src/generic-container/generic-container.test.ts index 89cb8ba06..b96cc5d90 100644 --- a/packages/testcontainers/src/generic-container/generic-container.test.ts +++ b/packages/testcontainers/src/generic-container/generic-container.test.ts @@ -7,7 +7,6 @@ import { checkContainerIsHealthy, getDockerEventStream, getRunningContainerNames, - getStoppedContainerNames, waitForDockerEvent, } from "../utils/test-helper"; import { GenericContainer } from "./generic-container"; @@ -544,7 +543,7 @@ describe("GenericContainer", { timeout: 180_000 }, () => { const dockerode = (await getContainerRuntimeClient()).container.dockerode; expect(stopped.getId()).toBeTruthy(); const lowerLevelContainer = dockerode.getContainer(stopped.getId()); - await expect(lowerLevelContainer.inspect()).rejects.toThrow(/404/) // Error: (HTTP code 404) no such container + await expect(lowerLevelContainer.inspect()).rejects.toThrow(/404/); // Error: (HTTP code 404) no such container }); it("should build a target stage", async () => {