Skip to content

Commit d62d9c2

Browse files
Add support for docker-compose pull policy (#530)
1 parent ee31dbb commit d62d9c2

File tree

7 files changed

+104
-45
lines changed

7 files changed

+104
-45
lines changed

docs/features/compose.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ const environment = await new DockerComposeEnvironment(composeFilePath, composeF
4141
.up();
4242
```
4343

44+
### With a pull policy
45+
46+
```javascript
47+
const { DockerComposeEnvironment, AlwaysPullPolicy } = require("testcontainers");
48+
49+
const environment = await new DockerComposeEnvironment(composeFilePath, composeFile)
50+
.withPullPolicy(new AlwaysPullPolicy())
51+
.up();
52+
```
53+
4454
### With rebuild
4555

4656
```javascript

src/docker-compose-environment/docker-compose-environment.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {
77
composeContainerName,
88
getRunningContainerNames,
99
getVolumeNames,
10+
waitForDockerEvent,
1011
} from "../test-helper";
12+
import { AlwaysPullPolicy } from "../pull-policy";
1113

1214
describe("DockerComposeEnvironment", () => {
1315
jest.setTimeout(180_000);
@@ -38,6 +40,30 @@ describe("DockerComposeEnvironment", () => {
3840
await startedEnvironment.down();
3941
});
4042

43+
it("should use pull policy", async () => {
44+
const env = new DockerComposeEnvironment(fixtures, "docker-compose-with-many-services.yml");
45+
46+
const startedEnv1 = await env.up();
47+
const dockerPullEventPromise = waitForDockerEvent("pull", 2);
48+
const startedEnv2 = await env.withPullPolicy(new AlwaysPullPolicy()).up();
49+
await dockerPullEventPromise;
50+
51+
await startedEnv1.stop();
52+
await startedEnv2.stop();
53+
});
54+
55+
it("should use pull policy for specific service", async () => {
56+
const env = new DockerComposeEnvironment(fixtures, "docker-compose-with-many-services.yml");
57+
58+
const startedEnv1 = await env.up(["service_2"]);
59+
const dockerPullEventPromise = waitForDockerEvent("pull");
60+
const startedEnv2 = await env.withPullPolicy(new AlwaysPullPolicy()).up(["service_2"]);
61+
await dockerPullEventPromise;
62+
63+
await startedEnv1.stop();
64+
await startedEnv2.stop();
65+
});
66+
4167
it("should start environment with multiple compose files", async () => {
4268
const overrideFixtures = path.resolve(fixtures, "docker-compose-with-override");
4369

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import { dockerComposeDown } from "../docker-compose/functions/docker-compose-do
1717
import { dockerComposeUp } from "../docker-compose/functions/docker-compose-up";
1818
import { waitForContainer } from "../wait-for-container";
1919
import { defaultWaitStrategy } from "../wait-strategy/default-wait-strategy";
20+
import { DefaultPullPolicy, PullPolicy } from "../pull-policy";
21+
import { dockerComposePull } from "../docker-compose/functions/docker-compose-pull";
2022

2123
export class DockerComposeEnvironment {
2224
private readonly composeFilePath: string;
@@ -28,6 +30,7 @@ export class DockerComposeEnvironment {
2830
private environmentFile = "";
2931
private profiles: string[] = [];
3032
private environment: Environment = {};
33+
private pullPolicy: PullPolicy = new DefaultPullPolicy();
3134
private waitStrategy: { [containerName: string]: WaitStrategy } = {};
3235
private startupTimeout = 60_000;
3336

@@ -63,6 +66,11 @@ export class DockerComposeEnvironment {
6366
return this;
6467
}
6568

69+
public withPullPolicy(pullPolicy: PullPolicy): this {
70+
this.pullPolicy = pullPolicy;
71+
return this;
72+
}
73+
6674
public withWaitStrategy(containerName: string, waitStrategy: WaitStrategy): this {
6775
this.waitStrategy[containerName] = waitStrategy;
6876
return this;
@@ -98,6 +106,9 @@ export class DockerComposeEnvironment {
98106
}
99107
this.profiles.forEach((profile) => composeOptions.push("--profile", profile));
100108

109+
if (this.pullPolicy.shouldPull()) {
110+
await dockerComposePull(options, services);
111+
}
101112
await dockerComposeUp({ ...options, commandOptions, composeOptions, environment: this.environment }, services);
102113

103114
const startedContainers = (await listContainers()).filter(
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { log } from "../../logger";
2+
import { pullAll, pullMany } from "docker-compose";
3+
import { defaultDockerComposeOptions } from "../default-docker-compose-options";
4+
import { DockerComposeOptions } from "../docker-compose-options";
5+
6+
export const dockerComposePull = async (options: DockerComposeOptions, services?: Array<string>): Promise<void> => {
7+
try {
8+
if (services) {
9+
log.info(`Pulling DockerCompose environment images for ${services.join(", ")}`);
10+
await pullMany(services, await defaultDockerComposeOptions(options));
11+
} else {
12+
log.info(`Pulling DockerCompose environment images`);
13+
await pullAll(await defaultDockerComposeOptions(options));
14+
}
15+
log.info(`Pulled DockerCompose environment`);
16+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
17+
} catch (err: any) {
18+
const errorMessage = err.err || err.message || err || "";
19+
log.error(`Failed to pull DockerCompose environment images: ${errorMessage.trim()}`);
20+
throw new Error(errorMessage);
21+
}
22+
};

src/generic-container/generic-container-dockerfile.test.ts

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import path from "path";
22
import { GenericContainer } from "./generic-container";
33
import { AlwaysPullPolicy } from "../pull-policy";
44
import { Wait } from "../wait-strategy/wait";
5-
import { checkContainerIsHealthy, getEvents } from "../test-helper";
5+
import { checkContainerIsHealthy, waitForDockerEvent } from "../test-helper";
66

77
describe("GenericContainer Dockerfile", () => {
88
jest.setTimeout(180_000);
@@ -22,29 +22,13 @@ describe("GenericContainer Dockerfile", () => {
2222
// https://github.com/containers/podman/issues/17779
2323
if (!process.env["CI_PODMAN"]) {
2424
it("should use pull policy", async () => {
25-
const containerSpec = GenericContainer.fromDockerfile(path.resolve(fixtures, "docker")).withPullPolicy(
26-
new AlwaysPullPolicy()
27-
);
28-
await containerSpec.build();
25+
const dockerfile = path.resolve(fixtures, "docker");
26+
const containerSpec = GenericContainer.fromDockerfile(dockerfile).withPullPolicy(new AlwaysPullPolicy());
2927

30-
const events = await getEvents();
31-
const pullPromise = new Promise<void>((resolve) => {
32-
events.on("data", (data) => {
33-
try {
34-
const { status } = JSON.parse(data);
35-
if (status === "pull") {
36-
resolve();
37-
}
38-
} catch {
39-
// ignored
40-
}
41-
});
42-
});
4328
await containerSpec.build();
44-
45-
await pullPromise;
46-
47-
events.destroy();
29+
const dockerPullEventPromise = waitForDockerEvent("pull");
30+
await containerSpec.build();
31+
await dockerPullEventPromise;
4832
});
4933
}
5034

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

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from "path";
33
import getPort from "get-port";
44
import { GenericContainer } from "./generic-container";
55
import { AlwaysPullPolicy } from "../pull-policy";
6-
import { checkContainerIsHealthy, getEvents } from "../test-helper";
6+
import { checkContainerIsHealthy, waitForDockerEvent } from "../test-helper";
77
import { getContainerById } from "../docker/functions/container/get-container";
88

99
describe("GenericContainer", () => {
@@ -220,30 +220,15 @@ describe("GenericContainer", () => {
220220
});
221221

222222
it("should use pull policy", async () => {
223-
const container1 = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").withExposedPorts(8080).start();
224-
const events = await getEvents();
225-
const pullPromise = new Promise<void>((resolve, reject) => {
226-
events.on("data", (data) => {
227-
try {
228-
if (JSON.parse(data).status === "pull") {
229-
resolve();
230-
}
231-
} catch (err) {
232-
reject(`Unexpected err: ${err}`);
233-
}
234-
});
235-
});
236-
237-
const container2 = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
238-
.withPullPolicy(new AlwaysPullPolicy())
239-
.withExposedPorts(8080)
240-
.start();
223+
const container = new GenericContainer("cristianrgreco/testcontainer:1.1.14").withExposedPorts(8080);
241224

242-
await pullPromise;
225+
const startedContainer1 = await container.start();
226+
const dockerPullEventPromise = waitForDockerEvent("pull");
227+
const startedContainer2 = await container.withPullPolicy(new AlwaysPullPolicy()).start();
228+
await dockerPullEventPromise;
243229

244-
events.destroy();
245-
await container1.stop();
246-
await container2.stop();
230+
await startedContainer1.stop();
231+
await startedContainer2.stop();
247232
});
248233

249234
it("should set the IPC mode", async () => {

src/test-helper.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,24 @@ export const composeContainerName = async (serviceName: string, index = 1): Prom
7676
const { dockerComposeInfo } = await getSystemInfo(dockerode);
7777
return dockerComposeInfo?.version.startsWith("1.") ? `${serviceName}_${index}` : `${serviceName}-${index}`;
7878
};
79+
80+
export const waitForDockerEvent = async (event: string, times = 1) => {
81+
const events = await getEvents();
82+
83+
let currentTimes = 0;
84+
return new Promise<void>((resolve, reject) => {
85+
events.on("data", (data) => {
86+
try {
87+
if (JSON.parse(data).status === event) {
88+
if (++currentTimes === times) {
89+
resolve();
90+
events.destroy();
91+
}
92+
}
93+
} catch (err) {
94+
reject(`Unexpected err: ${err}`);
95+
events.destroy();
96+
}
97+
});
98+
});
99+
};

0 commit comments

Comments
 (0)