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 });