Skip to content

Commit 212f7f9

Browse files
Retry inspecting container until exposed ports are mapped
1 parent 282d3fe commit 212f7f9

1 file changed

Lines changed: 49 additions & 9 deletions

File tree

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

Lines changed: 49 additions & 9 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, HostConfig } from "dockerode";
3+
import { Container, ContainerCreateOptions, ContainerInspectInfo, HostConfig } from "dockerode";
44
import { Readable } from "stream";
5-
import { containerLog, hash, log, toNanos } from "../common";
5+
import { containerLog, hash, IntervalRetry, 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";
@@ -141,8 +141,7 @@ export class GenericContainer implements TestContainer {
141141
if (!inspectResult.State.Running) {
142142
log.debug("Reused container is not running, attempting to start it");
143143
await client.container.start(container);
144-
// Refetch the inspect result to get the updated state
145-
inspectResult = await client.container.inspect(container);
144+
inspectResult = (await this.inspectContainer(client, container)).inspectResult;
146145
}
147146

148147
const mappedInspectResult = mapInspectResult(inspectResult);
@@ -196,8 +195,7 @@ export class GenericContainer implements TestContainer {
196195
await client.container.start(container);
197196
log.info(`Started container for image "${this.createOpts.Image}"`, { containerId: container.id });
198197

199-
const inspectResult = await client.container.inspect(container);
200-
const mappedInspectResult = mapInspectResult(inspectResult);
198+
const { inspectResult, mappedInspectResult } = await this.inspectContainer(client, container);
201199
const boundPorts = BoundPorts.fromInspectResult(client.info.containerRuntime.hostIps, mappedInspectResult).filter(
202200
this.exposedPorts
203201
);
@@ -241,6 +239,48 @@ export class GenericContainer implements TestContainer {
241239
return startedContainer;
242240
}
243241

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+
244284
private async connectContainerToPortForwarder(client: ContainerRuntimeClient, container: Container) {
245285
const portForwarder = await PortForwarderInstance.getInstance();
246286
const portForwarderNetworkId = portForwarder.getNetworkId();
@@ -361,7 +401,7 @@ export class GenericContainer implements TestContainer {
361401
public withExposedPorts(...ports: PortWithOptionalBinding[]): this {
362402
const exposedPorts: { [port: string]: Record<string, never> } = {};
363403
for (const exposedPort of ports) {
364-
exposedPorts[getContainerPort(exposedPort).toString()] = {};
404+
exposedPorts[`${getContainerPort(exposedPort).toString()}/tcp`] = {};
365405
}
366406

367407
this.exposedPorts = [...this.exposedPorts, ...ports];
@@ -373,9 +413,9 @@ export class GenericContainer implements TestContainer {
373413
const portBindings: Record<string, Array<Record<string, string>>> = {};
374414
for (const exposedPort of ports) {
375415
if (hasHostBinding(exposedPort)) {
376-
portBindings[exposedPort.container] = [{ HostPort: exposedPort.host.toString() }];
416+
portBindings[`${exposedPort.container}/tcp`] = [{ HostPort: exposedPort.host.toString() }];
377417
} else {
378-
portBindings[exposedPort] = [{ HostPort: "0" }];
418+
portBindings[`${exposedPort}/tcp`] = [{ HostPort: "0" }];
379419
}
380420
}
381421

0 commit comments

Comments
 (0)