Skip to content

Commit 5732a4a

Browse files
authored
Implement withAutoRemove #905 (#939)
1 parent 6d79dfd commit 5732a4a

File tree

7 files changed

+74
-5
lines changed

7 files changed

+74
-5
lines changed

docs/features/containers.md

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

351+
Alternatively, you can disable automatic removal while configuring the container:
352+
353+
```javascript
354+
const container = await new GenericContainer("alpine")
355+
.withAutoRemove(false)
356+
.start();
357+
358+
await container.stop()
359+
```
360+
361+
The value specified to `.withAutoRemove()` can be overridden by `.stop()`:
362+
363+
```javascript
364+
const container = await new GenericContainer("alpine")
365+
.withAutoRemove(false)
366+
.start();
367+
368+
await container.stop({ remove: true }) // The container is stopped *AND* removed
369+
```
370+
351371
Volumes created by the container are removed when stopped. This is configurable:
352372

353373
```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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,34 @@ describe("GenericContainer", { timeout: 180_000 }, () => {
518518
expect(await getRunningContainerNames()).not.toContain(container.getName());
519519
});
520520

521+
it("should stop but not remove the container", async () => {
522+
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
523+
.withName(`container-${new RandomUuid().nextUuid()}`)
524+
.withAutoRemove(false)
525+
.start();
526+
527+
const stopped = await container.stop();
528+
const dockerode = (await getContainerRuntimeClient()).container.dockerode;
529+
expect(stopped.getId()).toBeTruthy();
530+
const lowerLevelContainer = dockerode.getContainer(stopped.getId());
531+
expect((await lowerLevelContainer.inspect()).State.Status).toEqual("exited");
532+
});
533+
534+
it("should stop and override .withAutoRemove", async () => {
535+
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
536+
.withName(`container-${new RandomUuid().nextUuid()}`)
537+
.withAutoRemove(false)
538+
.start();
539+
540+
await container.stop({ remove: true });
541+
542+
const stopped = await container.stop();
543+
const dockerode = (await getContainerRuntimeClient()).container.dockerode;
544+
expect(stopped.getId()).toBeTruthy();
545+
const lowerLevelContainer = dockerode.getContainer(stopped.getId());
546+
await expect(lowerLevelContainer.inspect()).rejects.toThrow(/404/); // Error: (HTTP code 404) no such container
547+
});
548+
521549
it("should build a target stage", async () => {
522550
const context = path.resolve(fixtures, "docker-multi-stage");
523551
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
@@ -51,6 +51,7 @@ export class GenericContainer implements TestContainer {
5151
protected environment: Record<string, string> = {};
5252
protected exposedPorts: PortWithOptionalBinding[] = [];
5353
protected reuse = false;
54+
protected autoRemove = true;
5455
protected networkMode?: string;
5556
protected networkAliases: string[] = [];
5657
protected pullPolicy: ImagePullPolicy = PullPolicy.defaultPolicy();
@@ -160,7 +161,8 @@ export class GenericContainer implements TestContainer {
160161
inspectResult,
161162
boundPorts,
162163
inspectResult.Name,
163-
this.waitStrategy
164+
this.waitStrategy,
165+
this.autoRemove
164166
);
165167
}
166168

@@ -228,7 +230,8 @@ export class GenericContainer implements TestContainer {
228230
inspectResult,
229231
boundPorts,
230232
inspectResult.Name,
231-
this.waitStrategy
233+
this.waitStrategy,
234+
this.autoRemove
232235
);
233236

234237
if (this.containerStarted) {
@@ -439,6 +442,11 @@ export class GenericContainer implements TestContainer {
439442
return this;
440443
}
441444

445+
public withAutoRemove(autoRemove: boolean): this {
446+
this.autoRemove = autoRemove;
447+
return this;
448+
}
449+
442450
public withPullPolicy(pullPolicy: ImagePullPolicy): this {
443451
this.pullPolicy = pullPolicy;
444452
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
@@ -24,7 +24,8 @@ export class StartedGenericContainer implements StartedTestContainer {
2424
private inspectResult: ContainerInspectInfo,
2525
private boundPorts: BoundPorts,
2626
private readonly name: string,
27-
private readonly waitStrategy: WaitStrategy
27+
private readonly waitStrategy: WaitStrategy,
28+
private readonly autoRemove: boolean
2829
) {}
2930

3031
protected containerIsStopping?(): Promise<void>;
@@ -105,7 +106,7 @@ export class StartedGenericContainer implements StartedTestContainer {
105106
await this.containerIsStopping();
106107
}
107108

108-
const resolvedOptions: StopOptions = { remove: true, timeout: 0, removeVolumes: true, ...options };
109+
const resolvedOptions: StopOptions = { remove: this.autoRemove, timeout: 0, removeVolumes: true, ...options };
109110
await client.container.stop(this.container, { timeout: resolvedOptions.timeout });
110111
if (resolvedOptions.remove) {
111112
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
@@ -42,6 +42,7 @@ export interface TestContainer {
4242
withUser(user: string): this;
4343
withPullPolicy(pullPolicy: ImagePullPolicy): this;
4444
withReuse(): this;
45+
withAutoRemove(autoRemove: boolean): this;
4546
withCopyFilesToContainer(filesToCopy: FileToCopy[]): this;
4647
withCopyDirectoriesToContainer(directoriesToCopy: DirectoryToCopy[]): this;
4748
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)