Skip to content

Commit 493628f

Browse files
Reaper should clean up images (#194)
1 parent c5623d5 commit 493628f

File tree

7 files changed

+111
-34
lines changed

7 files changed

+111
-34
lines changed

src/docker-compose-environment.test.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ import fetch from "node-fetch";
22
import path from "path";
33
import { DockerComposeEnvironment } from "./docker-compose-environment";
44
import { Wait } from "./wait";
5-
import Dockerode from "dockerode";
6-
import { getRunningContainerNames } from "./test-helper";
5+
import { getRunningContainerNames, getVolumeNames } from "./test-helper";
76

87
describe("DockerComposeEnvironment", () => {
98
jest.setTimeout(60000);
109

1110
const fixtures = path.resolve(__dirname, "..", "fixtures", "docker-compose");
12-
const dockerodeClient = new Dockerode();
1311

1412
it("should throw error when compose file is malformed", async () => {
1513
await expect(new DockerComposeEnvironment(fixtures, "docker-compose-malformed.yml").up()).rejects.toThrowError(
@@ -83,7 +81,7 @@ describe("DockerComposeEnvironment", () => {
8381
.up()
8482
).rejects.toThrowError(`Log message "unexpected" not received after 0ms`);
8583

86-
expect(await getRunningContainerNames(dockerodeClient)).not.toContain("custom_container_name");
84+
expect(await getRunningContainerNames()).not.toContain("custom_container_name");
8785
});
8886

8987
it("should stop the container when the health check wait strategy times out", async () => {
@@ -94,7 +92,7 @@ describe("DockerComposeEnvironment", () => {
9492
.up()
9593
).rejects.toThrowError(`Health check not healthy after 0ms`);
9694

97-
expect(await getRunningContainerNames(dockerodeClient)).not.toContain("container_1");
95+
expect(await getRunningContainerNames()).not.toContain("container_1");
9896
});
9997

10098
it("should support health check wait strategy", async () => {
@@ -125,10 +123,7 @@ describe("DockerComposeEnvironment", () => {
125123

126124
await environment.down();
127125

128-
const testVolumes = (await dockerodeClient.listVolumes()).Volumes.map(
129-
(volume) => volume.Name
130-
).filter((volumeName) => volumeName.includes("test-volume"));
131-
126+
const testVolumes = (await getVolumeNames()).filter((volumeName) => volumeName.includes("test-volume"));
132127
expect(testVolumes).toHaveLength(0);
133128
});
134129

src/generic-container.test.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import Dockerode from "dockerode";
21
import fetch from "node-fetch";
32
import path from "path";
43
import { createServer, Server } from "http";
@@ -9,19 +8,13 @@ import { Readable } from "stream";
98
import { RandomUuid } from "./uuid";
109
import { TestContainers } from "./test-containers";
1110
import { RandomPortClient } from "./port-client";
12-
import { getRunningContainerNames } from "./test-helper";
11+
import { getContainerById, getEvents, getRunningContainerNames } from "./test-helper";
1312

1413
describe("GenericContainer", () => {
1514
jest.setTimeout(180_000);
1615

1716
const fixtures = path.resolve(__dirname, "..", "fixtures", "docker");
1817

19-
let dockerodeClient: Dockerode;
20-
21-
beforeEach(() => {
22-
dockerodeClient = new Dockerode();
23-
});
24-
2518
it("should wait for port", async () => {
2619
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.12").withExposedPorts(8080).start();
2720

@@ -95,7 +88,7 @@ describe("GenericContainer", () => {
9588

9689
it("should set network mode", async () => {
9790
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.12").withNetworkMode("host").start();
98-
const dockerContainer = dockerodeClient.getContainer(container.getId());
91+
const dockerContainer = getContainerById(container.getId());
9992

10093
const containerInfo = await dockerContainer.inspect();
10194

@@ -177,7 +170,7 @@ describe("GenericContainer", () => {
177170

178171
it("should set default log driver", async () => {
179172
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.12").withDefaultLogDriver().start();
180-
const dockerContainer = dockerodeClient.getContainer(container.getId());
173+
const dockerContainer = getContainerById(container.getId());
181174

182175
const containerInfo = await dockerContainer.inspect();
183176

@@ -193,7 +186,7 @@ describe("GenericContainer", () => {
193186
.withPrivilegedMode()
194187
.withExposedPorts(8080)
195188
.start();
196-
const dockerContainer = dockerodeClient.getContainer(container.getId());
189+
const dockerContainer = getContainerById(container.getId());
197190
const containerInfo = await dockerContainer.inspect();
198191
expect(containerInfo.HostConfig.Privileged).toBe(true);
199192

@@ -207,7 +200,7 @@ describe("GenericContainer", () => {
207200
it("should use pull policy", async () => {
208201
const container1 = await new GenericContainer("cristianrgreco/testcontainer:1.1.12").withExposedPorts(8080).start();
209202

210-
const events = (await dockerodeClient.getEvents()).setEncoding("utf-8") as Readable;
203+
const events = await getEvents();
211204

212205
const container2 = await new GenericContainer("cristianrgreco/testcontainer:1.1.12")
213206
.withPullPolicy(new AlwaysPullPolicy())
@@ -277,7 +270,7 @@ describe("GenericContainer", () => {
277270
.start()
278271
).rejects.toThrowError("Port 8081 not bound after 0ms");
279272

280-
expect(await getRunningContainerNames(dockerodeClient)).not.toContain(containerName);
273+
expect(await getRunningContainerNames()).not.toContain(containerName);
281274
});
282275

283276
it("should stop the container when the log message wait strategy times out", async () => {
@@ -292,7 +285,7 @@ describe("GenericContainer", () => {
292285
.start()
293286
).rejects.toThrowError(`Log message "unexpected" not received after 0ms`);
294287

295-
expect(await getRunningContainerNames(dockerodeClient)).not.toContain(containerName);
288+
expect(await getRunningContainerNames()).not.toContain(containerName);
296289
});
297290

298291
it("should stop the container when the health check wait strategy times out", async () => {
@@ -309,7 +302,7 @@ describe("GenericContainer", () => {
309302
.start()
310303
).rejects.toThrowError("Health check not healthy after 0ms");
311304

312-
expect(await getRunningContainerNames(dockerodeClient)).not.toContain(containerName);
305+
expect(await getRunningContainerNames()).not.toContain(containerName);
313306
});
314307

315308
it("should stop the container when the health check fails", async () => {
@@ -326,7 +319,7 @@ describe("GenericContainer", () => {
326319
.start()
327320
).rejects.toThrowError("Health check failed");
328321

329-
expect(await getRunningContainerNames(dockerodeClient)).not.toContain(containerName);
322+
expect(await getRunningContainerNames()).not.toContain(containerName);
330323
});
331324

332325
it("should honour .dockerignore file", async () => {

src/generic-container.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ export class GenericContainerBuilder {
5555
return this;
5656
}
5757

58-
public async build(): Promise<GenericContainer> {
59-
const dockerImageName = new DockerImageName(undefined, this.uuid.nextUuid(), this.uuid.nextUuid());
58+
public async build(image = `${this.uuid.nextUuid()}:${this.uuid.nextUuid()}`): Promise<GenericContainer> {
59+
const dockerImageName = DockerImageName.fromString(image);
6060
const dockerClient = await DockerClientInstance.getInstance();
61+
await ReaperInstance.getInstance(dockerClient);
6162
await dockerClient.buildImage(dockerImageName, this.context, this.dockerfileName, this.buildArgs);
6263
const container = new GenericContainer(dockerImageName.toString());
6364

src/network.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import Dockerode from "dockerode";
21
import { GenericContainer } from "./generic-container";
32
import { Network } from "./network";
3+
import { getContainerById } from "./test-helper";
44

55
describe("Network", () => {
66
jest.setTimeout(180_000);
@@ -12,9 +12,7 @@ describe("Network", () => {
1212
.withNetworkMode(network.getName())
1313
.start();
1414

15-
const dockerodeClient = new Dockerode();
16-
17-
const dockerContainer = dockerodeClient.getContainer(container.getId());
15+
const dockerContainer = getContainerById(container.getId());
1816
const containerInfo = await dockerContainer.inspect();
1917
expect(containerInfo.HostConfig.NetworkMode).toBe(network.getName());
2018

src/reaper.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { GenericContainer } from "./generic-container";
2+
import { ReaperInstance } from "./reaper";
3+
import { getImagesRepoTags, getRunningContainerIds, getRunningNetworkIds } from "./test-helper";
4+
import { Network } from "./network";
5+
import path from "path";
6+
import { RandomUuid } from "./uuid";
7+
8+
describe("Reaper", () => {
9+
jest.setTimeout(60_000);
10+
11+
it("should remove containers", async () => {
12+
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.12").start();
13+
14+
await ReaperInstance.stopInstance();
15+
16+
expect(await getRunningContainerIds()).toContain(container.getId());
17+
await new Promise((resolve) => setTimeout(resolve, 15_000));
18+
expect(await getRunningContainerIds()).not.toContain(container.getId());
19+
});
20+
21+
it("should remove networks", async () => {
22+
const network = await new Network().start();
23+
24+
await ReaperInstance.stopInstance();
25+
26+
expect(await getRunningNetworkIds()).toContain(network.getId());
27+
await new Promise((resolve) => setTimeout(resolve, 15_000));
28+
expect(await getRunningNetworkIds()).not.toContain(network.getId());
29+
});
30+
31+
it("should remove images", async () => {
32+
const imageId = `${new RandomUuid().nextUuid()}:${new RandomUuid().nextUuid()}`;
33+
const context = path.resolve(__dirname, "..", "fixtures", "docker", "docker");
34+
await GenericContainer.fromDockerfile(context).build(imageId);
35+
36+
await ReaperInstance.stopInstance();
37+
38+
expect(await getImagesRepoTags()).toContain(imageId);
39+
await new Promise((resolve) => setTimeout(resolve, 15_000));
40+
expect(await getImagesRepoTags()).not.toContain(imageId);
41+
});
42+
});

src/reaper.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { DockerClient } from "./docker-client";
88

99
export interface Reaper {
1010
addProject(projectName: string): void;
11+
stop(): void;
1112
}
1213

1314
class RealReaper implements Reaper {
@@ -20,18 +21,26 @@ class RealReaper implements Reaper {
2021
public addProject(projectName: string): void {
2122
this.socket.write(`label=com.docker.compose.project=${projectName}\r\n`);
2223
}
24+
25+
public stop(): void {
26+
this.socket.end();
27+
}
2328
}
2429

2530
class DisabledReaper implements Reaper {
2631
public addProject(): void {
2732
// noop
2833
}
34+
35+
public stop(): void {
36+
// noop
37+
}
2938
}
3039

3140
export class ReaperInstance {
32-
private static DEFAULT_IMAGE = "testcontainers/ryuk:0.3.0";
41+
private static DEFAULT_IMAGE = "cristianrgreco/ryuk:0.4.0";
3342

34-
private static instance: Promise<Reaper>;
43+
private static instance?: Promise<Reaper>;
3544

3645
public static async getInstance(dockerClient: DockerClient): Promise<Reaper> {
3746
if (!this.instance) {
@@ -45,6 +54,14 @@ export class ReaperInstance {
4554
return this.instance;
4655
}
4756

57+
public static async stopInstance(): Promise<void> {
58+
if (this.instance) {
59+
const reaper = await this.instance;
60+
reaper.stop();
61+
this.instance = undefined;
62+
}
63+
}
64+
4865
public static getImage(): string {
4966
return process.env.RYUK_CONTAINER_IMAGE === undefined ? this.DEFAULT_IMAGE : process.env.RYUK_CONTAINER_IMAGE;
5067
}

src/test-helper.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,40 @@
11
import Dockerode from "dockerode";
2+
import { Readable } from "stream";
23

3-
export const getRunningContainerNames = async (dockerode: Dockerode): Promise<string[]> => {
4+
export const dockerode = new Dockerode();
5+
6+
export const getContainerById = (id: string): Dockerode.Container => dockerode.getContainer(id);
7+
8+
export const getEvents = async (): Promise<Readable> => {
9+
const events = (await dockerode.getEvents()) as Readable;
10+
events.setEncoding("utf-8");
11+
return events;
12+
};
13+
14+
export const getRunningContainerNames = async (): Promise<string[]> => {
415
const containers = await dockerode.listContainers();
516
return containers
617
.map((container) => container.Names)
718
.reduce((result, containerNames) => [...result, ...containerNames], [])
819
.map((containerName) => containerName.replace("/", ""));
920
};
21+
22+
export const getRunningContainerIds = async (): Promise<string[]> => {
23+
const containers = await dockerode.listContainers();
24+
return containers.map((container) => container.Id);
25+
};
26+
27+
export const getRunningNetworkIds = async (): Promise<string[]> => {
28+
const networks = await dockerode.listNetworks();
29+
return networks.map((network) => network.Id);
30+
};
31+
32+
export const getImagesRepoTags = async (): Promise<string[]> => {
33+
const images = await dockerode.listImages();
34+
return images.map((image) => image.RepoTags[0]);
35+
};
36+
37+
export const getVolumeNames = async (): Promise<string[]> => {
38+
const { Volumes: volumes } = await dockerode.listVolumes();
39+
return volumes.map((volume) => volume.Name);
40+
};

0 commit comments

Comments
 (0)