diff --git a/docs/features/containers.md b/docs/features/containers.md index ed04e68fe..288fe3700 100644 --- a/docs/features/containers.md +++ b/docs/features/containers.md @@ -348,6 +348,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 e7664a957..83dd0a2cb 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 2618cafed..6c8544b03 100644 --- a/packages/testcontainers/src/generic-container/generic-container.test.ts +++ b/packages/testcontainers/src/generic-container/generic-container.test.ts @@ -7,6 +7,7 @@ import { checkContainerIsHealthy, getDockerEventStream, getRunningContainerNames, + getStoppedContainerNames, waitForDockerEvent, } from "../utils/test-helper"; import { GenericContainer } from "./generic-container"; @@ -518,6 +519,30 @@ describe("GenericContainer", { timeout: 180_000 }, () => { 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 5655884ea..5b50bc91c 100644 --- a/packages/testcontainers/src/generic-container/generic-container.ts +++ b/packages/testcontainers/src/generic-container/generic-container.ts @@ -51,6 +51,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(); @@ -160,7 +161,8 @@ export class GenericContainer implements TestContainer { inspectResult, boundPorts, inspectResult.Name, - this.waitStrategy + this.waitStrategy, + this.autoRemove ); } @@ -228,7 +230,8 @@ export class GenericContainer implements TestContainer { inspectResult, boundPorts, inspectResult.Name, - this.waitStrategy + this.waitStrategy, + this.autoRemove ); if (this.containerStarted) { @@ -439,6 +442,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 b0bc42cd6..e97d8aae6 100644 --- a/packages/testcontainers/src/generic-container/started-generic-container.ts +++ b/packages/testcontainers/src/generic-container/started-generic-container.ts @@ -24,7 +24,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; @@ -105,7 +106,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 84e9633de..f4154ca78 100644 --- a/packages/testcontainers/src/test-container.ts +++ b/packages/testcontainers/src/test-container.ts @@ -42,6 +42,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 d99a55358..16210782f 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 });