Skip to content

Commit ce55fba

Browse files
Refactor + add tests
1 parent 212f7f9 commit ce55fba

File tree

3 files changed

+151
-46
lines changed

3 files changed

+151
-46
lines changed

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

Lines changed: 15 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import archiver from "archiver";
22
import AsyncLock from "async-lock";
3-
import { Container, ContainerCreateOptions, ContainerInspectInfo, HostConfig } from "dockerode";
3+
import { Container, ContainerCreateOptions, HostConfig } from "dockerode";
44
import { Readable } from "stream";
5-
import { containerLog, hash, IntervalRetry, log, toNanos } from "../common";
5+
import { containerLog, hash, log, toNanos } from "../common";
66
import { ContainerRuntimeClient, getContainerRuntimeClient, ImageName } from "../container-runtime";
77
import { CONTAINER_STATUSES } from "../container-runtime/clients/container/types";
88
import { StartedNetwork } from "../network/network";
@@ -33,6 +33,7 @@ import { Wait } from "../wait-strategies/wait";
3333
import { waitForContainer } from "../wait-strategies/wait-for-container";
3434
import { WaitStrategy } from "../wait-strategies/wait-strategy";
3535
import { GenericContainerBuilder } from "./generic-container-builder";
36+
import { inspectContainerUntilPortsExposed } from "./inspect-container-util-ports-exposed";
3637
import { StartedGenericContainer } from "./started-generic-container";
3738

3839
const reusableContainerCreationLock = new AsyncLock();
@@ -141,7 +142,13 @@ export class GenericContainer implements TestContainer {
141142
if (!inspectResult.State.Running) {
142143
log.debug("Reused container is not running, attempting to start it");
143144
await client.container.start(container);
144-
inspectResult = (await this.inspectContainer(client, container)).inspectResult;
145+
inspectResult = (
146+
await inspectContainerUntilPortsExposed(
147+
() => client.container.inspect(container),
148+
this.exposedPorts,
149+
container.id
150+
)
151+
).inspectResult;
145152
}
146153

147154
const mappedInspectResult = mapInspectResult(inspectResult);
@@ -195,7 +202,11 @@ export class GenericContainer implements TestContainer {
195202
await client.container.start(container);
196203
log.info(`Started container for image "${this.createOpts.Image}"`, { containerId: container.id });
197204

198-
const { inspectResult, mappedInspectResult } = await this.inspectContainer(client, container);
205+
const { inspectResult, mappedInspectResult } = await inspectContainerUntilPortsExposed(
206+
() => client.container.inspect(container),
207+
this.exposedPorts,
208+
container.id
209+
);
199210
const boundPorts = BoundPorts.fromInspectResult(client.info.containerRuntime.hostIps, mappedInspectResult).filter(
200211
this.exposedPorts
201212
);
@@ -239,48 +250,6 @@ export class GenericContainer implements TestContainer {
239250
return startedContainer;
240251
}
241252

242-
private async inspectContainer(
243-
client: ContainerRuntimeClient,
244-
container: Container
245-
): Promise<{
246-
inspectResult: ContainerInspectInfo;
247-
mappedInspectResult: InspectResult;
248-
}> {
249-
const containerInspectRetry = await new IntervalRetry<
250-
{
251-
inspectResult: ContainerInspectInfo;
252-
mappedInspectResult: InspectResult;
253-
},
254-
Error
255-
>(100).retryUntil(
256-
async () => {
257-
const inspectResult = await client.container.inspect(container);
258-
const mappedInspectResult = mapInspectResult(inspectResult);
259-
return { inspectResult, mappedInspectResult };
260-
},
261-
({ mappedInspectResult }) =>
262-
this.exposedPorts
263-
.map((exposedPort) => getContainerPort(exposedPort))
264-
.every(
265-
(exposedPort) =>
266-
mappedInspectResult.ports[exposedPort].length > 0 &&
267-
mappedInspectResult.ports[exposedPort].every(({ hostPort }) => hostPort !== undefined)
268-
),
269-
() => {
270-
const message = `Container did not expose all ports after starting`;
271-
log.error(message, { containerId: container.id });
272-
return new Error(message);
273-
},
274-
3000
275-
);
276-
277-
if (containerInspectRetry instanceof Error) {
278-
throw containerInspectRetry;
279-
}
280-
281-
return containerInspectRetry;
282-
}
283-
284253
private async connectContainerToPortForwarder(client: ContainerRuntimeClient, container: Container) {
285254
const portForwarder = await PortForwarderInstance.getInstance();
286255
const portForwarderNetworkId = portForwarder.getNetworkId();
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { ContainerInspectInfo } from "dockerode";
2+
import { InspectResult } from "../types";
3+
import { mapInspectResult } from "../utils/map-inspect-result";
4+
import { inspectContainerUntilPortsExposed } from "./inspect-container-util-ports-exposed";
5+
6+
function mockExposed(): { inspectResult: ContainerInspectInfo; mappedInspectResult: InspectResult } {
7+
const date = new Date();
8+
9+
const inspectResult: ContainerInspectInfo = {
10+
Name: "container-id",
11+
Config: {
12+
Hostname: "hostname",
13+
Labels: {},
14+
},
15+
State: {
16+
Health: {
17+
Status: "healthy",
18+
},
19+
Status: "running",
20+
Running: true,
21+
StartedAt: date.toISOString(),
22+
FinishedAt: date.toISOString(),
23+
},
24+
NetworkSettings: {
25+
Ports: { "8080/tcp": [{ HostIp: "0.0.0.0", HostPort: "45000" }] },
26+
Networks: {},
27+
},
28+
} as unknown as ContainerInspectInfo;
29+
30+
return { inspectResult, mappedInspectResult: mapInspectResult(inspectResult) };
31+
}
32+
33+
function mockNotExposed(): { inspectResult: ContainerInspectInfo; mappedInspectResult: InspectResult } {
34+
const date = new Date();
35+
36+
const inspectResult: ContainerInspectInfo = {
37+
Name: "container-id",
38+
Config: {
39+
Hostname: "hostname",
40+
Labels: {},
41+
},
42+
State: {
43+
Health: {
44+
Status: "healthy",
45+
},
46+
Status: "running",
47+
Running: true,
48+
StartedAt: date.toISOString(),
49+
FinishedAt: date.toISOString(),
50+
},
51+
NetworkSettings: {
52+
Ports: { "8080/tcp": [] },
53+
Networks: {},
54+
},
55+
} as unknown as ContainerInspectInfo;
56+
57+
return { inspectResult, mappedInspectResult: mapInspectResult(inspectResult) };
58+
}
59+
60+
test("returns the inspect results when all ports are exposed", async () => {
61+
const data = mockExposed();
62+
const inspectFn = vi.fn().mockResolvedValueOnce(data.inspectResult);
63+
64+
const result = await inspectContainerUntilPortsExposed(inspectFn, [8080], "container-id");
65+
66+
expect(result).toEqual(data);
67+
});
68+
69+
test("retries the inspect if ports are not yet exposed", async () => {
70+
const data1 = mockNotExposed();
71+
const data2 = mockExposed();
72+
const inspectFn = vi
73+
.fn()
74+
.mockResolvedValueOnce(data1.inspectResult)
75+
.mockResolvedValueOnce(data1.inspectResult)
76+
.mockResolvedValueOnce(data2.inspectResult);
77+
78+
const result = await inspectContainerUntilPortsExposed(inspectFn, [8080], "container-id");
79+
80+
expect(result).toEqual(data2);
81+
expect(inspectFn).toHaveBeenCalledTimes(3);
82+
});
83+
84+
test("throws an error ", async () => {
85+
const data = mockNotExposed();
86+
const inspectFn = vi.fn().mockResolvedValue(data.inspectResult);
87+
88+
await expect(inspectContainerUntilPortsExposed(inspectFn, [8080], "container-id", 0)).rejects.toThrow(
89+
"Container did not expose all ports after starting"
90+
);
91+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { ContainerInspectInfo } from "dockerode";
2+
import { IntervalRetry, log } from "../common";
3+
import { InspectResult } from "../types";
4+
import { mapInspectResult } from "../utils/map-inspect-result";
5+
import { getContainerPort, PortWithOptionalBinding } from "../utils/port";
6+
7+
type Result = {
8+
inspectResult: ContainerInspectInfo;
9+
mappedInspectResult: InspectResult;
10+
};
11+
12+
export async function inspectContainerUntilPortsExposed(
13+
inspectFn: () => Promise<ContainerInspectInfo>,
14+
ports: PortWithOptionalBinding[],
15+
containerId: string,
16+
timeout = 5000
17+
): Promise<Result> {
18+
const result = await new IntervalRetry<Result, Error>(100).retryUntil(
19+
async () => {
20+
const inspectResult = await inspectFn();
21+
const mappedInspectResult = mapInspectResult(inspectResult);
22+
return { inspectResult, mappedInspectResult };
23+
},
24+
({ mappedInspectResult }) =>
25+
ports
26+
.map((exposedPort) => getContainerPort(exposedPort))
27+
.every(
28+
(exposedPort) =>
29+
mappedInspectResult.ports[exposedPort].length > 0 &&
30+
mappedInspectResult.ports[exposedPort].every(({ hostPort }) => hostPort !== undefined)
31+
),
32+
() => {
33+
const message = `Container did not expose all ports after starting`;
34+
log.error(message, { containerId });
35+
return new Error(message);
36+
},
37+
timeout
38+
);
39+
40+
if (result instanceof Error) {
41+
throw result;
42+
}
43+
44+
return result;
45+
}

0 commit comments

Comments
 (0)