Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,12 @@ jobs:
run: npm prune --omit=dev --workspace packages/testcontainers
- name: Run CommonJS module smoke test
run: node packages/testcontainers/smoke-test.js
env:
DEBUG: "testcontainers*"
- name: Run ES module smoke test
run: node packages/testcontainers/smoke-test.mjs
env:
DEBUG: "testcontainers*"

test:
if: ${{ needs.detect-modules.outputs.modules_count > 0 }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,7 @@ export class GenericContainer implements TestContainer {
if (!inspectResult.State.Running) {
log.debug("Reused container is not running, attempting to start it");
await client.container.start(container);
inspectResult = (
await inspectContainerUntilPortsExposed(
() => client.container.inspect(container),
this.exposedPorts,
container.id
)
).inspectResult;
inspectResult = await inspectContainerUntilPortsExposed(() => client.container.inspect(container), container.id);
}

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

const { inspectResult, mappedInspectResult } = await inspectContainerUntilPortsExposed(
const inspectResult = await inspectContainerUntilPortsExposed(
() => client.container.inspect(container),
this.exposedPorts,
container.id
);
const mappedInspectResult = mapInspectResult(inspectResult);
const boundPorts = BoundPorts.fromInspectResult(client.info.containerRuntime.hostIps, mappedInspectResult).filter(
this.exposedPorts
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,72 +1,74 @@
import { ContainerInspectInfo } from "dockerode";
import { mapInspectResult } from "../utils/map-inspect-result";
import { inspectContainerUntilPortsExposed } from "./inspect-container-util-ports-exposed";

function mockInspectResult(ports: ContainerInspectInfo["NetworkSettings"]["Ports"]) {
const date = new Date();

const inspectResult: ContainerInspectInfo = {
Name: "container-id",
Config: {
Hostname: "hostname",
Labels: {},
},
State: {
Health: {
Status: "healthy",
},
Status: "running",
Running: true,
StartedAt: date.toISOString(),
FinishedAt: date.toISOString(),
function mockInspectResult(
portBindings: ContainerInspectInfo["HostConfig"]["PortBindings"],
ports: ContainerInspectInfo["NetworkSettings"]["Ports"]
): ContainerInspectInfo {
return {
HostConfig: {
PortBindings: portBindings,
},
NetworkSettings: {
Ports: ports,
Networks: {},
},
} as unknown as ContainerInspectInfo;

return { inspectResult, mappedInspectResult: mapInspectResult(inspectResult) };
} as ContainerInspectInfo;
}

test("returns the inspect results when all ports are exposed", async () => {
const data = mockInspectResult({ "8080/tcp": [{ HostIp: "0.0.0.0", HostPort: "45000" }] });
const inspectFn = vi.fn().mockResolvedValueOnce(data.inspectResult);
describe.sequential("inspectContainerUntilPortsExposed", () => {
it("returns the inspect result when all ports are exposed", async () => {
const data = mockInspectResult({ "8080/tcp": [] }, { "8080/tcp": [{ HostIp: "0.0.0.0", HostPort: "45000" }] });
const inspectFn = vi.fn().mockResolvedValueOnce(data);

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

expect(result).toEqual(data);
});
expect(result).toEqual(data);
});

test("retries the inspect if ports are not yet exposed", async () => {
const data1 = mockInspectResult({ "8080/tcp": [] });
const data2 = mockInspectResult({ "8080/tcp": [{ HostIp: "0.0.0.0", HostPort: "45000" }] });
const inspectFn = vi
.fn()
.mockResolvedValueOnce(data1.inspectResult)
.mockResolvedValueOnce(data1.inspectResult)
.mockResolvedValueOnce(data2.inspectResult);
it("returns the inspect result when no ports are exposed", async () => {
const data = mockInspectResult({}, {});
const inspectFn = vi.fn().mockResolvedValueOnce(data);

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

expect(result).toEqual(data2);
expect(inspectFn).toHaveBeenCalledTimes(3);
});
expect(result).toEqual(data);
});

test("throws an error when host ports are not exposed within timeout", async () => {
const data = mockInspectResult({ "8080/tcp": [] });
const inspectFn = vi.fn().mockResolvedValue(data.inspectResult);
it("returns the inspect result if host config port bindings are null", async () => {
const data = mockInspectResult(null, {});
const inspectFn = vi.fn().mockResolvedValueOnce(data);

await expect(inspectContainerUntilPortsExposed(inspectFn, [8080], "container-id", 0)).rejects.toThrow(
"Container did not expose all ports after starting"
);
});
const result = await inspectContainerUntilPortsExposed(inspectFn, "container-id");

expect(result).toEqual(data);
});

it("retries the inspect if ports are not yet exposed", async () => {
const data1 = mockInspectResult({ "8080/tcp": [] }, { "8080/tcp": [] });
const data2 = mockInspectResult({ "8080/tcp": [] }, { "8080/tcp": [{ HostIp: "0.0.0.0", HostPort: "45000" }] });
const inspectFn = vi.fn().mockResolvedValueOnce(data1).mockResolvedValueOnce(data1).mockResolvedValueOnce(data2);

const result = await inspectContainerUntilPortsExposed(inspectFn, "container-id");

expect(result).toEqual(data2);
expect(inspectFn).toHaveBeenCalledTimes(3);
});

it("throws an error when host ports are not exposed within timeout", async () => {
const data = mockInspectResult({ "8080/tcp": [] }, { "8080/tcp": [] });
const inspectFn = vi.fn().mockResolvedValue(data);

await expect(inspectContainerUntilPortsExposed(inspectFn, "container-id", 0)).rejects.toThrow(
"Timed out after 0ms while waiting for container ports to be bound to the host"
);
});

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

await expect(inspectContainerUntilPortsExposed(inspectFn, [8080], "container-id", 0)).rejects.toThrow(
"Container did not expose all ports after starting"
);
await expect(inspectContainerUntilPortsExposed(inspectFn, "container-id", 0)).rejects.toThrow(
"Timed out after 0ms while waiting for container ports to be bound to the host"
);
});
});
Original file line number Diff line number Diff line change
@@ -1,32 +1,21 @@
import { ContainerInspectInfo } from "dockerode";
import { IntervalRetry, log } from "../common";
import { InspectResult } from "../types";
import { mapInspectResult } from "../utils/map-inspect-result";
import { getContainerPort, PortWithOptionalBinding } from "../utils/port";

type Result = {
inspectResult: ContainerInspectInfo;
mappedInspectResult: InspectResult;
};

export async function inspectContainerUntilPortsExposed(
inspectFn: () => Promise<ContainerInspectInfo>,
ports: PortWithOptionalBinding[],
containerId: string,
timeout = 10_000
): Promise<Result> {
const result = await new IntervalRetry<Result, Error>(250).retryUntil(
async () => {
const inspectResult = await inspectFn();
const mappedInspectResult = mapInspectResult(inspectResult);
return { inspectResult, mappedInspectResult };
): Promise<ContainerInspectInfo> {
const result = await new IntervalRetry<ContainerInspectInfo, Error>(250).retryUntil(
() => inspectFn(),
(inspectResult) => {
const portBindings = inspectResult?.HostConfig?.PortBindings;
if (!portBindings) return true;
const expectedlyBoundPorts = Object.keys(portBindings);
return expectedlyBoundPorts.every((exposedPort) => inspectResult.NetworkSettings.Ports[exposedPort]?.length > 0);
},
({ mappedInspectResult }) =>
ports
.map((exposedPort) => getContainerPort(exposedPort))
.every((exposedPort) => mappedInspectResult.ports[exposedPort]?.length > 0),
() => {
const message = `Container did not expose all ports after starting`;
const message = `Timed out after ${timeout}ms while waiting for container ports to be bound to the host`;
log.error(message, { containerId });
return new Error(message);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { LABEL_TESTCONTAINERS_SESSION_ID } from "../utils/labels";
import { mapInspectResult } from "../utils/map-inspect-result";
import { waitForContainer } from "../wait-strategies/wait-for-container";
import { WaitStrategy } from "../wait-strategies/wait-strategy";
import { inspectContainerUntilPortsExposed } from "./inspect-container-util-ports-exposed";
import { StoppedGenericContainer } from "./stopped-generic-container";

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

this.inspectResult = await client.container.inspect(this.container);
this.inspectResult = await inspectContainerUntilPortsExposed(
() => client.container.inspect(this.container),
this.container.id
);
const mappedInspectResult = mapInspectResult(this.inspectResult);
const startTime = new Date(this.inspectResult.State.StartedAt);

Expand Down
Loading