diff --git a/docs/supported-container-runtimes.md b/docs/supported-container-runtimes.md index ffc477a46..7305dd28b 100644 --- a/docs/supported-container-runtimes.md +++ b/docs/supported-container-runtimes.md @@ -8,7 +8,8 @@ Works out of the box. ### Usage -MacOS: +#### MacOS: + ```bash {% raw %} export DOCKER_HOST=unix://$(podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}') @@ -16,10 +17,29 @@ export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock {% endraw %} ``` -Linux: -```bash -export DOCKER_HOST=unix://${XDG_RUNTIME_DIR}/podman/podman.sock -``` +#### Linux: + +1. Ensure the Podman socket is exposed: + + Rootless: + + ```bash + systemctl --user status podman.socket + ``` + + Rootful: + + ```bash + sudo systemctl enable --now podman.socket + ``` + +2. Export the `DOCKER_HOST`: + + ```bash + {% raw %} + export DOCKER_HOST="unix://$(podman info --format '{{.Host.RemoteSocket.Path}}')" + {% endraw %} + ``` ### Known issues @@ -71,10 +91,7 @@ You can use a composite wait strategy to additionally wait for a port to be boun const { GenericContainer, Wait } = require("testcontainers"); const container = await new GenericContainer("redis") - .withWaitStrategy(Wait.forAll([ - Wait.forListeningPorts(), - Wait.forLogMessage("Ready to accept connections") - ])) + .withWaitStrategy(Wait.forAll([Wait.forListeningPorts(), Wait.forLogMessage("Ready to accept connections")])) .start(); ``` diff --git a/packages/testcontainers/src/port-forwarder/port-forwarder.ts b/packages/testcontainers/src/port-forwarder/port-forwarder.ts index 93d03c09c..cdd0a834e 100644 --- a/packages/testcontainers/src/port-forwarder/port-forwarder.ts +++ b/packages/testcontainers/src/port-forwarder/port-forwarder.ts @@ -96,7 +96,13 @@ export class PortForwarderInstance { } log.debug(`Connecting to Port Forwarder on "${host}:${port}"...`); - const connection = await createSshConnection({ host, port, username: "root", password: "root" }); + const connection = await createSshConnection({ + host, + port, + username: "root", + password: "root", + readyTimeout: 100_000, + }); log.debug(`Connected to Port Forwarder on "${host}:${port}"`); connection.unref(); diff --git a/packages/testcontainers/src/reaper/reaper.test.ts b/packages/testcontainers/src/reaper/reaper.test.ts index e6e06b47a..357ace7c6 100644 --- a/packages/testcontainers/src/reaper/reaper.test.ts +++ b/packages/testcontainers/src/reaper/reaper.test.ts @@ -1,4 +1,5 @@ import { ContainerRuntimeClient, getContainerRuntimeClient } from "../container-runtime"; +import { RandomUniquePortGenerator } from "../utils/port-generator"; describe("Reaper", { timeout: 120_000 }, () => { let client: ContainerRuntimeClient; @@ -7,9 +8,64 @@ describe("Reaper", { timeout: 120_000 }, () => { beforeEach(async () => { vi.resetModules(); + vi.stubEnv("TESTCONTAINERS_RYUK_TEST_LABEL", "true"); client = await getContainerRuntimeClient(); }); + it("should create disabled reaper when TESTCONTAINERS_RYUK_DISABLED=true", async () => { + vi.stubEnv("TESTCONTAINERS_RYUK_DISABLED", "true"); + vi.spyOn(client.container, "list").mockResolvedValue([]); + + const reaper = await getReaper(); + + expect(() => reaper.addSession("test-session")).not.toThrow(); + expect(() => reaper.addComposeProject("test-project")).not.toThrow(); + }); + + it("should return cached reaper instance", async () => { + vi.spyOn(client.container, "list").mockResolvedValue([]); + + const reaper = await getReaper(); + const reaper2 = await getReaper(); + + expect(reaper2.containerId).toBe(reaper.containerId); + }); + + it("should create new reaper container if one is not running", async () => { + vi.spyOn(client.container, "list").mockResolvedValue([]); + const reaper = await getReaper(); + vi.resetModules(); + + const reaper2 = await getReaper(); + + expect(reaper2.containerId).not.toBe(reaper.containerId); + }); + + it("should reuse existing reaper container if one is already running", async () => { + const reaper = await getReaper(); + vi.resetModules(); + const reaperContainerInfo = (await client.container.list()).filter((c) => c.Id === reaper.containerId)[0]; + reaperContainerInfo.Labels["TESTCONTAINERS_RYUK_TEST_LABEL"] = "false"; + vi.spyOn(client.container, "list").mockResolvedValue([reaperContainerInfo]); + + const reaper2 = await getReaper(); + + expect(reaper2.containerId).toBe(reaper.containerId); + }); + + it("should use custom port when TESTCONTAINERS_RYUK_PORT is set", async () => { + const customPort = (await new RandomUniquePortGenerator().generatePort()).toString(); + vi.stubEnv("TESTCONTAINERS_RYUK_PORT", customPort); + vi.spyOn(client.container, "list").mockResolvedValue([]); + + const reaper = await getReaper(); + + const reaperContainer = client.container.getById(reaper.containerId); + const ports = (await reaperContainer.inspect()).HostConfig.PortBindings; + const port = ports["8080"] || ports["8080/tcp"]; + expect(port[0].HostPort).toBe(customPort); + }); + it("should create Reaper container without RYUK_VERBOSE env var by default", async () => { vi.spyOn(client.container, "list").mockResolvedValue([]); const reaper = await getReaper(); @@ -22,8 +78,8 @@ describe("Reaper", { timeout: 120_000 }, () => { it("should propagate TESTCONTAINERS_RYUK_VERBOSE into Reaper container", async () => { vi.stubEnv("TESTCONTAINERS_RYUK_VERBOSE", "true"); - vi.spyOn(client.container, "list").mockResolvedValue([]); + const reaper = await getReaper(); const reaperContainer = client.container.getById(reaper.containerId); diff --git a/packages/testcontainers/src/reaper/reaper.ts b/packages/testcontainers/src/reaper/reaper.ts index a2af584bc..1158838c6 100644 --- a/packages/testcontainers/src/reaper/reaper.ts +++ b/packages/testcontainers/src/reaper/reaper.ts @@ -48,7 +48,10 @@ export async function getReaper(client: ContainerRuntimeClient): Promise async function findReaperContainer(client: ContainerRuntimeClient): Promise { const containers = await client.container.list(); return containers.find( - (container) => container.State === "running" && container.Labels[LABEL_TESTCONTAINERS_RYUK] === "true" + (container) => + container.State === "running" && + container.Labels[LABEL_TESTCONTAINERS_RYUK] === "true" && + container.Labels["TESTCONTAINERS_RYUK_TEST_LABEL"] !== "true" ); } @@ -78,12 +81,15 @@ async function createNewReaper(sessionId: string, remoteSocketPath: string): Pro .withBindMounts([{ source: remoteSocketPath, target: "/var/run/docker.sock" }]) .withLabels({ [LABEL_TESTCONTAINERS_SESSION_ID]: sessionId }) .withWaitStrategy(Wait.forLogMessage(/.*Started.*/)); - if (process.env["TESTCONTAINERS_RYUK_VERBOSE"]) + if (process.env["TESTCONTAINERS_RYUK_VERBOSE"]) { container.withEnvironment({ RYUK_VERBOSE: process.env["TESTCONTAINERS_RYUK_VERBOSE"] }); - - if (process.env.TESTCONTAINERS_RYUK_PRIVILEGED === "true") { + } + if (process.env["TESTCONTAINERS_RYUK_PRIVILEGED"] === "true") { container.withPrivilegedMode(); } + if (process.env["TESTCONTAINERS_RYUK_TEST_LABEL"] === "true") { + container.withLabels({ TESTCONTAINERS_RYUK_TEST_LABEL: "true" }); + } const startedContainer = await container.start();