diff --git a/docs/configuration.md b/docs/configuration.md index 0974f3692..33dbf2e6d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -32,15 +32,16 @@ Configuration of the Docker daemon: Configuration of Testcontainers and its behaviours: -| Variable | Example | Description | -| ------------------------------------- | -------------------------- | ---------------------------------------- | -| TESTCONTAINERS_HOST_OVERRIDE | tcp://docker:2375 | Docker's host on which ports are exposed | -| TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE | /var/run/docker.sock | Path to Docker's socket used by ryuk | -| TESTCONTAINERS_RYUK_PRIVILEGED | true | Run ryuk as a privileged container | -| TESTCONTAINERS_RYUK_DISABLED | true | Disable ryuk | -| TESTCONTAINERS_RYUK_PORT | 65515 | Set ryuk host port (not recommended) | -| TESTCONTAINERS_SSHD_PORT | 65515 | Set SSHd host port (not recommended) | -| TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX | mycompany.com/registry | Set default image registry | -| RYUK_CONTAINER_IMAGE | testcontainers/ryuk:0.11.0 | Custom image for ryuk | -| SSHD_CONTAINER_IMAGE | testcontainers/sshd:1.1.0 | Custom image for SSHd | -| TESTCONTAINERS_REUSE_ENABLE | true | Enable reusable containers | +| Variable | Example | Description | +| ------------------------------------- | -------------------------- | -------------------------------------------- | +| TESTCONTAINERS_HOST_OVERRIDE | tcp://docker:2375 | Docker's host on which ports are exposed | +| TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE | /var/run/docker.sock | Path to Docker's socket used by ryuk | +| TESTCONTAINERS_RYUK_PRIVILEGED | true | Run ryuk as a privileged container | +| TESTCONTAINERS_RYUK_DISABLED | true | Disable ryuk | +| TESTCONTAINERS_RYUK_PORT | 65515 | Set ryuk host port (not recommended) | +| TESTCONTAINERS_SSHD_PORT | 65515 | Set SSHd host port (not recommended) | +| TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX | mycompany.com/registry | Set default image registry | +| RYUK_CONTAINER_IMAGE | testcontainers/ryuk:0.11.0 | Custom image for ryuk | +| SSHD_CONTAINER_IMAGE | testcontainers/sshd:1.1.0 | Custom image for SSHd | +| TESTCONTAINERS_REUSE_ENABLE | true | Enable reusable containers | +| TESTCONTAINERS_RYUK_VERBOSE | true | Sets RYUK_VERBOSE env var in ryuk container | diff --git a/packages/testcontainers/src/reaper/reaper.test.ts b/packages/testcontainers/src/reaper/reaper.test.ts new file mode 100644 index 000000000..4421c4c56 --- /dev/null +++ b/packages/testcontainers/src/reaper/reaper.test.ts @@ -0,0 +1,34 @@ +import { ContainerRuntimeClient, getContainerRuntimeClient } from "../container-runtime"; + +describe("Reaper", { timeout: 120_000 }, () => { + let client: ContainerRuntimeClient; + + const getReaper = async () => await (await import("./reaper")).getReaper(client); + + beforeEach(async () => { + vi.resetModules(); + vitest.unstubAllEnvs(); + + client = await getContainerRuntimeClient(); + }); + + it("should create Reaper container without RYUK_VERBOSE env var by default", async () => { + vi.spyOn(client.container, "list").mockResolvedValue([]); + const reaper = await getReaper(); + + const reaperContainer = client.container.getById(reaper.containerId); + const reaperContainerEnv = (await reaperContainer.inspect()).Config.Env; + expect(reaperContainerEnv).not.toContain("RYUK_VERBOSE=true"); + expect(reaperContainerEnv).not.toContain("RYUK_VERBOSE=false"); + }); + + it("should propagate TESTCONTAINERS_RYUK_VERBOSE into Reaper container", async () => { + vitest.stubEnv("TESTCONTAINERS_RYUK_VERBOSE", "true"); + + vi.spyOn(client.container, "list").mockResolvedValue([]); + const reaper = await getReaper(); + + const reaperContainer = client.container.getById(reaper.containerId); + expect((await reaperContainer.inspect()).Config.Env).toContain("RYUK_VERBOSE=true"); + }); +}); diff --git a/packages/testcontainers/src/reaper/reaper.ts b/packages/testcontainers/src/reaper/reaper.ts index 0405c133b..a2af584bc 100644 --- a/packages/testcontainers/src/reaper/reaper.ts +++ b/packages/testcontainers/src/reaper/reaper.ts @@ -3,7 +3,7 @@ import { Socket } from "net"; import { IntervalRetry, log, RandomUuid, withFileLock } from "../common"; import { ContainerRuntimeClient, ImageName } from "../container-runtime"; import { GenericContainer } from "../generic-container/generic-container"; -import { LABEL_TESTCONTAINERS_SESSION_ID } from "../utils/labels"; +import { LABEL_TESTCONTAINERS_RYUK, LABEL_TESTCONTAINERS_SESSION_ID } from "../utils/labels"; import { Wait } from "../wait-strategies/wait"; export const REAPER_IMAGE = process.env["RYUK_CONTAINER_IMAGE"] @@ -13,6 +13,8 @@ export const REAPER_IMAGE = process.env["RYUK_CONTAINER_IMAGE"] export interface Reaper { sessionId: string; + containerId: string; + addSession(sessionId: string): void; addComposeProject(projectName: string): void; @@ -28,10 +30,10 @@ export async function getReaper(client: ContainerRuntimeClient): Promise reaper = await withFileLock("testcontainers-node.lock", async () => { const reaperContainer = await findReaperContainer(client); - sessionId = reaperContainer?.Labels["org.testcontainers.session-id"] ?? new RandomUuid().nextUuid(); + sessionId = reaperContainer?.Labels[LABEL_TESTCONTAINERS_SESSION_ID] ?? new RandomUuid().nextUuid(); if (process.env.TESTCONTAINERS_RYUK_DISABLED === "true") { - return new DisabledReaper(sessionId); + return new DisabledReaper(sessionId, ""); } else if (reaperContainer) { return await useExistingReaper(reaperContainer, sessionId, client.info.containerRuntime.host); } else { @@ -46,7 +48,7 @@ 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["org.testcontainers.ryuk"] === "true" + (container) => container.State === "running" && container.Labels[LABEL_TESTCONTAINERS_RYUK] === "true" ); } @@ -60,7 +62,7 @@ async function useExistingReaper(reaperContainer: ContainerInfo, sessionId: stri const socket = await connectToReaperSocket(host, reaperPort, reaperContainer.Id); - return new RyukReaper(sessionId, socket); + return new RyukReaper(sessionId, reaperContainer.Id, socket); } async function createNewReaper(sessionId: string, remoteSocketPath: string): Promise { @@ -76,6 +78,8 @@ 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"]) + container.withEnvironment({ RYUK_VERBOSE: process.env["TESTCONTAINERS_RYUK_VERBOSE"] }); if (process.env.TESTCONTAINERS_RYUK_PRIVILEGED === "true") { container.withPrivilegedMode(); @@ -89,7 +93,7 @@ async function createNewReaper(sessionId: string, remoteSocketPath: string): Pro startedContainer.getId() ); - return new RyukReaper(sessionId, socket); + return new RyukReaper(sessionId, startedContainer.getId(), socket); } async function connectToReaperSocket(host: string, port: number, containerId: string): Promise { @@ -135,6 +139,7 @@ async function connectToReaperSocket(host: string, port: number, containerId: st class RyukReaper implements Reaper { constructor( public readonly sessionId: string, + public readonly containerId: string, private readonly socket: Socket ) {} @@ -148,7 +153,10 @@ class RyukReaper implements Reaper { } class DisabledReaper implements Reaper { - constructor(public readonly sessionId: string) {} + constructor( + public readonly sessionId: string, + public readonly containerId: string + ) {} addComposeProject(): void {} diff --git a/packages/testcontainers/src/utils/labels.ts b/packages/testcontainers/src/utils/labels.ts index 7a2b73e87..b2ef06e3c 100644 --- a/packages/testcontainers/src/utils/labels.ts +++ b/packages/testcontainers/src/utils/labels.ts @@ -6,6 +6,7 @@ export const LABEL_TESTCONTAINERS_VERSION = "org.testcontainers.version"; export const LABEL_TESTCONTAINERS_SESSION_ID = "org.testcontainers.session-id"; export const LABEL_TESTCONTAINERS_SSHD = "org.testcontainers.sshd"; export const LABEL_TESTCONTAINERS_CONTAINER_HASH = "org.testcontainers.container-hash"; +export const LABEL_TESTCONTAINERS_RYUK = "org.testcontainers.ryuk"; export function createLabels(): Record { return {