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
35 changes: 26 additions & 9 deletions docs/supported-container-runtimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,38 @@ Works out of the box.

### Usage

MacOS:
#### MacOS:

```bash
{% raw %}
export DOCKER_HOST=unix://$(podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}')
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

Expand Down Expand Up @@ -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();
```

Expand Down
8 changes: 7 additions & 1 deletion packages/testcontainers/src/port-forwarder/port-forwarder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
58 changes: 57 additions & 1 deletion packages/testcontainers/src/reaper/reaper.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ContainerRuntimeClient, getContainerRuntimeClient } from "../container-runtime";
import { RandomUniquePortGenerator } from "../utils/port-generator";

describe("Reaper", { timeout: 120_000 }, () => {
let client: ContainerRuntimeClient;
Expand All @@ -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();
Expand All @@ -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);
Expand Down
14 changes: 10 additions & 4 deletions packages/testcontainers/src/reaper/reaper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ export async function getReaper(client: ContainerRuntimeClient): Promise<Reaper>
async function findReaperContainer(client: ContainerRuntimeClient): Promise<ContainerInfo | undefined> {
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"
);
}

Expand Down Expand Up @@ -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();

Expand Down