Skip to content

Commit 52a9050

Browse files
Wait for ports to be bound when container restarts
1 parent 1afb781 commit 52a9050

File tree

4 files changed

+44
-67
lines changed

4 files changed

+44
-67
lines changed

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

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,7 @@ export class GenericContainer implements TestContainer {
142142
if (!inspectResult.State.Running) {
143143
log.debug("Reused container is not running, attempting to start it");
144144
await client.container.start(container);
145-
inspectResult = (
146-
await inspectContainerUntilPortsExposed(
147-
() => client.container.inspect(container),
148-
this.exposedPorts,
149-
container.id
150-
)
151-
).inspectResult;
145+
inspectResult = await inspectContainerUntilPortsExposed(() => client.container.inspect(container), container.id);
152146
}
153147

154148
const mappedInspectResult = mapInspectResult(inspectResult);
@@ -202,11 +196,11 @@ export class GenericContainer implements TestContainer {
202196
await client.container.start(container);
203197
log.info(`Started container for image "${this.createOpts.Image}"`, { containerId: container.id });
204198

205-
const { inspectResult, mappedInspectResult } = await inspectContainerUntilPortsExposed(
199+
const inspectResult = await inspectContainerUntilPortsExposed(
206200
() => client.container.inspect(container),
207-
this.exposedPorts,
208201
container.id
209202
);
203+
const mappedInspectResult = mapInspectResult(inspectResult);
210204
const boundPorts = BoundPorts.fromInspectResult(client.info.containerRuntime.hostIps, mappedInspectResult).filter(
211205
this.exposedPorts
212206
);
Lines changed: 30 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,64 @@
11
import { ContainerInspectInfo } from "dockerode";
2-
import { mapInspectResult } from "../utils/map-inspect-result";
32
import { inspectContainerUntilPortsExposed } from "./inspect-container-util-ports-exposed";
43

5-
function mockInspectResult(ports: ContainerInspectInfo["NetworkSettings"]["Ports"]) {
6-
const date = new Date();
7-
8-
const inspectResult: ContainerInspectInfo = {
9-
Name: "container-id",
10-
Config: {
11-
Hostname: "hostname",
12-
Labels: {},
13-
},
14-
State: {
15-
Health: {
16-
Status: "healthy",
17-
},
18-
Status: "running",
19-
Running: true,
20-
StartedAt: date.toISOString(),
21-
FinishedAt: date.toISOString(),
4+
function mockInspectResult(
5+
portBindings: ContainerInspectInfo["HostConfig"]["PortBindings"],
6+
ports: ContainerInspectInfo["NetworkSettings"]["Ports"]
7+
): ContainerInspectInfo {
8+
return {
9+
HostConfig: {
10+
PortBindings: portBindings,
2211
},
2312
NetworkSettings: {
2413
Ports: ports,
2514
Networks: {},
2615
},
2716
} as unknown as ContainerInspectInfo;
28-
29-
return { inspectResult, mappedInspectResult: mapInspectResult(inspectResult) };
3017
}
3118

32-
test("returns the inspect results when all ports are exposed", async () => {
33-
const data = mockInspectResult({ "8080/tcp": [{ HostIp: "0.0.0.0", HostPort: "45000" }] });
34-
const inspectFn = vi.fn().mockResolvedValueOnce(data.inspectResult);
19+
test("returns the inspect result when all ports are exposed", async () => {
20+
const data = mockInspectResult({ "8080/tcp": [] }, { "8080/tcp": [{ HostIp: "0.0.0.0", HostPort: "45000" }] });
21+
const inspectFn = vi.fn().mockResolvedValueOnce(data);
22+
23+
const result = await inspectContainerUntilPortsExposed(inspectFn, "container-id");
24+
25+
expect(result).toEqual(data);
26+
});
27+
28+
test("returns the inspect result when no ports are exposed", async () => {
29+
const data = mockInspectResult({}, {});
30+
const inspectFn = vi.fn().mockResolvedValueOnce(data);
3531

36-
const result = await inspectContainerUntilPortsExposed(inspectFn, [8080], "container-id");
32+
const result = await inspectContainerUntilPortsExposed(inspectFn, "container-id");
3733

3834
expect(result).toEqual(data);
3935
});
4036

4137
test("retries the inspect if ports are not yet exposed", async () => {
42-
const data1 = mockInspectResult({ "8080/tcp": [] });
43-
const data2 = mockInspectResult({ "8080/tcp": [{ HostIp: "0.0.0.0", HostPort: "45000" }] });
44-
const inspectFn = vi
45-
.fn()
46-
.mockResolvedValueOnce(data1.inspectResult)
47-
.mockResolvedValueOnce(data1.inspectResult)
48-
.mockResolvedValueOnce(data2.inspectResult);
38+
const data1 = mockInspectResult({ "8080/tcp": [] }, { "8080/tcp": [] });
39+
const data2 = mockInspectResult({ "8080/tcp": [] }, { "8080/tcp": [{ HostIp: "0.0.0.0", HostPort: "45000" }] });
40+
const inspectFn = vi.fn().mockResolvedValueOnce(data1).mockResolvedValueOnce(data1).mockResolvedValueOnce(data2);
4941

50-
const result = await inspectContainerUntilPortsExposed(inspectFn, [8080], "container-id");
42+
const result = await inspectContainerUntilPortsExposed(inspectFn, "container-id");
5143

5244
expect(result).toEqual(data2);
5345
expect(inspectFn).toHaveBeenCalledTimes(3);
5446
});
5547

5648
test("throws an error when host ports are not exposed within timeout", async () => {
57-
const data = mockInspectResult({ "8080/tcp": [] });
58-
const inspectFn = vi.fn().mockResolvedValue(data.inspectResult);
49+
const data = mockInspectResult({ "8080/tcp": [] }, { "8080/tcp": [] });
50+
const inspectFn = vi.fn().mockResolvedValue(data);
5951

60-
await expect(inspectContainerUntilPortsExposed(inspectFn, [8080], "container-id", 0)).rejects.toThrow(
52+
await expect(inspectContainerUntilPortsExposed(inspectFn, "container-id", 0)).rejects.toThrow(
6153
"Container did not expose all ports after starting"
6254
);
6355
});
6456

6557
test("throws an error when container ports not exposed within timeout", async () => {
66-
const data = mockInspectResult({});
67-
const inspectFn = vi.fn().mockResolvedValue(data.inspectResult);
58+
const data = mockInspectResult({ "8080/tcp": [] }, {});
59+
const inspectFn = vi.fn().mockResolvedValue(data);
6860

69-
await expect(inspectContainerUntilPortsExposed(inspectFn, [8080], "container-id", 0)).rejects.toThrow(
61+
await expect(inspectContainerUntilPortsExposed(inspectFn, "container-id", 0)).rejects.toThrow(
7062
"Container did not expose all ports after starting"
7163
);
7264
});

packages/testcontainers/src/generic-container/inspect-container-util-ports-exposed.ts

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,17 @@
11
import { ContainerInspectInfo } from "dockerode";
22
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-
};
113

124
export async function inspectContainerUntilPortsExposed(
135
inspectFn: () => Promise<ContainerInspectInfo>,
14-
ports: PortWithOptionalBinding[],
156
containerId: string,
167
timeout = 10_000
17-
): Promise<Result> {
18-
const result = await new IntervalRetry<Result, Error>(250).retryUntil(
19-
async () => {
20-
const inspectResult = await inspectFn();
21-
const mappedInspectResult = mapInspectResult(inspectResult);
22-
return { inspectResult, mappedInspectResult };
8+
): Promise<ContainerInspectInfo> {
9+
const result = await new IntervalRetry<ContainerInspectInfo, Error>(250).retryUntil(
10+
() => inspectFn(),
11+
(inspectResult) => {
12+
const exposedPorts = Object.keys(inspectResult.HostConfig.PortBindings);
13+
return exposedPorts.every((exposedPort) => inspectResult.NetworkSettings.Ports[exposedPort]?.length > 0);
2314
},
24-
({ mappedInspectResult }) =>
25-
ports
26-
.map((exposedPort) => getContainerPort(exposedPort))
27-
.every((exposedPort) => mappedInspectResult.ports[exposedPort]?.length > 0),
2815
() => {
2916
const message = `Container did not expose all ports after starting`;
3017
log.error(message, { containerId });

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { LABEL_TESTCONTAINERS_SESSION_ID } from "../utils/labels";
1212
import { mapInspectResult } from "../utils/map-inspect-result";
1313
import { waitForContainer } from "../wait-strategies/wait-for-container";
1414
import { WaitStrategy } from "../wait-strategies/wait-strategy";
15+
import { inspectContainerUntilPortsExposed } from "./inspect-container-util-ports-exposed";
1516
import { StoppedGenericContainer } from "./stopped-generic-container";
1617

1718
export class StartedGenericContainer implements StartedTestContainer {
@@ -80,7 +81,10 @@ export class StartedGenericContainer implements StartedTestContainer {
8081
const resolvedOptions: RestartOptions = { timeout: 0, ...options };
8182
await client.container.restart(this.container, resolvedOptions);
8283

83-
this.inspectResult = await client.container.inspect(this.container);
84+
this.inspectResult = await inspectContainerUntilPortsExposed(
85+
() => client.container.inspect(this.container),
86+
this.container.id
87+
);
8488
const mappedInspectResult = mapInspectResult(this.inspectResult);
8589
const startTime = new Date(this.inspectResult.State.StartedAt);
8690

0 commit comments

Comments
 (0)