Skip to content

Commit 93fd586

Browse files
committed
feat: implement .withAutoRemove
1 parent f68ffbd commit 93fd586

File tree

7 files changed

+71
-5
lines changed

7 files changed

+71
-5
lines changed

docs/features/containers.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,26 @@ const container = await new GenericContainer("alpine").start();
331331
await container.stop({ remove: false });
332332
```
333333

334+
Alternatively, you can disable automatic removal while configuring the container:
335+
336+
```javascript
337+
const container = await new GenericContainer("alpine")
338+
.withAutoRemove(false)
339+
.start();
340+
341+
await container.stop()
342+
```
343+
344+
The value specified to `.withAutoRemove()` can be overridden by `.stop()`:
345+
346+
```javascript
347+
const container = await new GenericContainer("alpine")
348+
.withAutoRemove(false)
349+
.start();
350+
351+
await container.stop({ remove: true }) // The container is stopped *AND* removed
352+
```
353+
334354
Volumes created by the container are removed when stopped. This is configurable:
335355

336356
```javascript

packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ export class DockerComposeEnvironment {
168168
inspectResult,
169169
boundPorts,
170170
containerName,
171-
waitStrategy
171+
waitStrategy,
172+
true
172173
);
173174
})
174175
)

packages/testcontainers/src/generic-container/generic-container.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
checkContainerIsHealthy,
77
getDockerEventStream,
88
getRunningContainerNames,
9+
getStoppedContainerNames,
910
waitForDockerEvent,
1011
} from "../utils/test-helper";
1112
import { getContainerRuntimeClient } from "../container-runtime";
@@ -519,6 +520,30 @@ describe("GenericContainer", () => {
519520
expect(await getRunningContainerNames()).not.toContain(container.getName());
520521
});
521522

523+
it("should stop but not remove the container", async () => {
524+
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
525+
.withName(`container-${new RandomUuid().nextUuid()}`)
526+
.withAutoRemove(false)
527+
.start();
528+
529+
await container.stop();
530+
531+
expect(await getRunningContainerNames()).not.toContain(container.getName().replace("/", ""));
532+
expect(await getStoppedContainerNames()).toContain(container.getName().replace("/", ""));
533+
});
534+
535+
it("should stop and override .withAutoRemove", async () => {
536+
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
537+
.withName(`container-${new RandomUuid().nextUuid()}`)
538+
.withAutoRemove(false)
539+
.start();
540+
541+
await container.stop({ remove: true });
542+
543+
expect(await getRunningContainerNames()).not.toContain(container.getName().replace("/", ""));
544+
expect(await getStoppedContainerNames()).not.toContain(container.getName().replace("/", ""));
545+
});
546+
522547
it("should build a target stage", async () => {
523548
const context = path.resolve(fixtures, "docker-multi-stage");
524549
const firstContainer = await GenericContainer.fromDockerfile(context).withTarget("first").build();

packages/testcontainers/src/generic-container/generic-container.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export class GenericContainer implements TestContainer {
5050
protected environment: Record<string, string> = {};
5151
protected exposedPorts: PortWithOptionalBinding[] = [];
5252
protected reuse = false;
53+
protected autoRemove = true;
5354
protected networkMode?: string;
5455
protected networkAliases: string[] = [];
5556
protected pullPolicy: ImagePullPolicy = PullPolicy.defaultPolicy();
@@ -157,7 +158,8 @@ export class GenericContainer implements TestContainer {
157158
inspectResult,
158159
boundPorts,
159160
inspectResult.Name,
160-
this.waitStrategy
161+
this.waitStrategy,
162+
this.autoRemove
161163
);
162164
}
163165

@@ -221,7 +223,8 @@ export class GenericContainer implements TestContainer {
221223
inspectResult,
222224
boundPorts,
223225
inspectResult.Name,
224-
this.waitStrategy
226+
this.waitStrategy,
227+
this.autoRemove
225228
);
226229

227230
if (this.containerStarted) {
@@ -430,6 +433,11 @@ export class GenericContainer implements TestContainer {
430433
return this;
431434
}
432435

436+
public withAutoRemove(autoRemove: boolean): this {
437+
this.autoRemove = autoRemove;
438+
return this;
439+
}
440+
433441
public withPullPolicy(pullPolicy: ImagePullPolicy): this {
434442
this.pullPolicy = pullPolicy;
435443
return this;

packages/testcontainers/src/generic-container/started-generic-container.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ export class StartedGenericContainer implements StartedTestContainer {
2222
private inspectResult: ContainerInspectInfo,
2323
private boundPorts: BoundPorts,
2424
private readonly name: string,
25-
private readonly waitStrategy: WaitStrategy
25+
private readonly waitStrategy: WaitStrategy,
26+
private readonly autoRemove: boolean
2627
) {}
2728

2829
protected containerIsStopping?(): Promise<void>;
@@ -71,7 +72,7 @@ export class StartedGenericContainer implements StartedTestContainer {
7172
await this.containerIsStopping();
7273
}
7374

74-
const resolvedOptions: StopOptions = { remove: true, timeout: 0, removeVolumes: true, ...options };
75+
const resolvedOptions: StopOptions = { remove: this.autoRemove, timeout: 0, removeVolumes: true, ...options };
7576
await client.container.stop(this.container, { timeout: resolvedOptions.timeout });
7677
if (resolvedOptions.remove) {
7778
await client.container.remove(this.container, { removeVolumes: resolvedOptions.removeVolumes });

packages/testcontainers/src/test-container.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface TestContainer {
4040
withUser(user: string): this;
4141
withPullPolicy(pullPolicy: ImagePullPolicy): this;
4242
withReuse(): this;
43+
withAutoRemove(autoRemove: boolean): this;
4344
withCopyFilesToContainer(filesToCopy: FileToCopy[]): this;
4445
withCopyDirectoriesToContainer(directoriesToCopy: DirectoryToCopy[]): this;
4546
withCopyContentToContainer(contentsToCopy: ContentToCopy[]): this;

packages/testcontainers/src/utils/test-helper.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ export const getRunningContainerNames = async (): Promise<string[]> => {
4444
.map((containerName) => containerName.replace("/", ""));
4545
};
4646

47+
export const getStoppedContainerNames = async (): Promise<string[]> => {
48+
const dockerode = (await getContainerRuntimeClient()).container.dockerode;
49+
const containers = await dockerode.listContainers({ all: true });
50+
return containers
51+
.filter((container) => container.State === "exited")
52+
.map((container) => container.Names)
53+
.reduce((result, containerNames) => [...result, ...containerNames], [])
54+
.map((containerName) => containerName.replace("/", ""));
55+
};
56+
4757
export const getContainerIds = async (): Promise<string[]> => {
4858
const dockerode = (await getContainerRuntimeClient()).container.dockerode;
4959
const containers = await dockerode.listContainers({ all: true });

0 commit comments

Comments
 (0)