UNPKG

testcontainers

Version:

Testcontainers is a NodeJS library that supports tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container

160 lines (159 loc) 7.19 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getReaperImage = getReaperImage; exports.getReaper = getReaper; const net_1 = require("net"); const os_1 = require("os"); const common_1 = require("../common"); const container_runtime_1 = require("../container-runtime"); const generic_container_1 = require("../generic-container/generic-container"); const labels_1 = require("../utils/labels"); const wait_1 = require("../wait-strategies/wait"); /** * Resolve the Ryuk reaper image name. Read lazily so that callers (and tests) * can set `process.env.RYUK_CONTAINER_IMAGE` _after_ this module is imported — * including via `.env` files loaded by `dotenv` at runtime. * * See https://github.com/testcontainers/testcontainers-node/issues/1310. */ function getReaperImage() { return process.env["RYUK_CONTAINER_IMAGE"] ? container_runtime_1.ImageName.fromString(process.env["RYUK_CONTAINER_IMAGE"]).string : container_runtime_1.ImageName.fromString("testcontainers/ryuk:0.14.0").string; } let reaper; let sessionId; async function getReaper(client) { if (reaper) { return reaper; } const userId = (0, os_1.userInfo)().uid; reaper = await (0, common_1.withFileLock)(`testcontainers-node-${userId}.lock`, async () => { const reaperContainers = await findReaperContainers(client); if (process.env.TESTCONTAINERS_RYUK_DISABLED === "true") { sessionId = new common_1.RandomUuid().nextUuid(); return new DisabledReaper(sessionId, ""); } for (const reaperContainer of reaperContainers) { const existingSessionId = reaperContainer.Labels[labels_1.LABEL_TESTCONTAINERS_SESSION_ID] ?? new common_1.RandomUuid().nextUuid(); try { sessionId = existingSessionId; return await useExistingReaper(reaperContainer, sessionId, client.info.containerRuntime.host); } catch (error) { const message = error instanceof Error ? error.message : String(error); common_1.log.warn(`Failed to reuse existing Reaper: ${message}. Trying another Reaper...`, { containerId: reaperContainer.Id, }); } } sessionId = new common_1.RandomUuid().nextUuid(); return await createNewReaper(sessionId, client.info.containerRuntime.remoteSocketPath); }); reaper.addSession(sessionId); return reaper; } async function findReaperContainers(client) { const containers = await client.container.list(); return containers .filter((container) => container.State === "running" && container.Labels[labels_1.LABEL_TESTCONTAINERS_RYUK] === "true" && container.Labels["TESTCONTAINERS_RYUK_TEST_LABEL"] !== "true") .sort((a, b) => b.Created - a.Created); } async function useExistingReaper(reaperContainer, sessionId, host) { common_1.log.debug(`Reusing existing Reaper for session "${sessionId}"...`); const reaperPort = reaperContainer.Ports.find((port) => port.PrivatePort == 8080)?.PublicPort; if (!reaperPort) { throw new Error("Expected Reaper to map exposed port 8080"); } const socket = await connectToReaperSocket(host, reaperPort, reaperContainer.Id); return new RyukReaper(sessionId, reaperContainer.Id, socket); } async function createNewReaper(sessionId, remoteSocketPath) { common_1.log.debug(`Creating new Reaper for session "${sessionId}" with socket path "${remoteSocketPath}"...`); const container = new generic_container_1.GenericContainer(getReaperImage()) .withName(`testcontainers-ryuk-${sessionId}`) .withExposedPorts(process.env["TESTCONTAINERS_RYUK_PORT"] ? { container: 8080, host: parseInt(process.env["TESTCONTAINERS_RYUK_PORT"]) } : 8080) .withBindMounts([{ source: remoteSocketPath, target: "/var/run/docker.sock" }]) .withLabels({ [labels_1.LABEL_TESTCONTAINERS_SESSION_ID]: sessionId }) .withWaitStrategy(wait_1.Wait.forLogMessage(/.*Started.*/)); if (process.env["TESTCONTAINERS_RYUK_VERBOSE"]) { container.withEnvironment({ RYUK_VERBOSE: process.env["TESTCONTAINERS_RYUK_VERBOSE"] }); } if (process.env["TESTCONTAINERS_RYUK_RECONNECTION_TIMEOUT"]) { container.withEnvironment({ RYUK_RECONNECTION_TIMEOUT: process.env["TESTCONTAINERS_RYUK_RECONNECTION_TIMEOUT"] }); } 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(); const socket = await connectToReaperSocket(startedContainer.getHost(), startedContainer.getMappedPort(8080), startedContainer.getId()); return new RyukReaper(sessionId, startedContainer.getId(), socket); } async function connectToReaperSocket(host, port, containerId) { const retryResult = await new common_1.IntervalRetry(1000).retryUntil((attempt) => { return new Promise((resolve) => { common_1.log.debug(`Connecting to Reaper (attempt ${attempt + 1}) on "${host}:${port}"...`, { containerId }); const socket = new net_1.Socket(); socket .unref() .on("timeout", () => common_1.log.error(`Reaper ${containerId} socket timed out`)) .on("error", (err) => common_1.log.error(`Reaper ${containerId} socket error: ${err}`)) .on("close", (hadError) => { if (hadError) { common_1.log.error(`Connection to Reaper closed with error`, { containerId }); } else { common_1.log.warn(`Connection to Reaper closed`, { containerId }); } resolve(undefined); }) .connect(port, host, () => { common_1.log.debug(`Connected to Reaper`, { containerId }); resolve(socket); }); }); }, (result) => result !== undefined, () => { const message = `Failed to connect to Reaper`; common_1.log.error(message, { containerId }); return new Error(message); }, 4000); if (retryResult instanceof net_1.Socket) { return retryResult; } else { throw retryResult; } } class RyukReaper { sessionId; containerId; socket; constructor(sessionId, containerId, socket) { this.sessionId = sessionId; this.containerId = containerId; this.socket = socket; } addComposeProject(projectName) { this.socket.write(`label=com.docker.compose.project=${projectName}\r\n`); } addSession(sessionId) { this.socket.write(`label=${labels_1.LABEL_TESTCONTAINERS_SESSION_ID}=${sessionId}\r\n`); } } class DisabledReaper { sessionId; containerId; constructor(sessionId, containerId) { this.sessionId = sessionId; this.containerId = containerId; } addComposeProject() { } addSession() { } }