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

378 lines 16.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GenericContainer = void 0; const archiver_1 = __importDefault(require("archiver")); const async_lock_1 = __importDefault(require("async-lock")); const common_1 = require("../common"); const container_runtime_1 = require("../container-runtime"); const types_1 = require("../container-runtime/clients/container/types"); const port_forwarder_1 = require("../port-forwarder/port-forwarder"); const reaper_1 = require("../reaper/reaper"); const bound_ports_1 = require("../utils/bound-ports"); const labels_1 = require("../utils/labels"); const map_inspect_result_1 = require("../utils/map-inspect-result"); const port_1 = require("../utils/port"); const pull_policy_1 = require("../utils/pull-policy"); const wait_1 = require("../wait-strategies/wait"); const wait_for_container_1 = require("../wait-strategies/wait-for-container"); const generic_container_builder_1 = require("./generic-container-builder"); const started_generic_container_1 = require("./started-generic-container"); const reusableContainerCreationLock = new async_lock_1.default(); class GenericContainer { static fromDockerfile(context, dockerfileName = "Dockerfile") { return new generic_container_builder_1.GenericContainerBuilder(context, dockerfileName); } createOpts; hostConfig; imageName; startupTimeout; waitStrategy = wait_1.Wait.forListeningPorts(); environment = {}; exposedPorts = []; reuse = false; autoRemove = true; networkMode; networkAliases = []; pullPolicy = pull_policy_1.PullPolicy.defaultPolicy(); logConsumer; filesToCopy = []; directoriesToCopy = []; contentsToCopy = []; archivesToCopy = []; healthCheck; constructor(image) { this.imageName = container_runtime_1.ImageName.fromString(image); this.createOpts = { Image: this.imageName.string }; this.hostConfig = { AutoRemove: this.imageName.string === reaper_1.REAPER_IMAGE }; } isHelperContainer() { return this.isReaper() || this.imageName.string === port_forwarder_1.SSHD_IMAGE; } isReaper() { return this.imageName.string === reaper_1.REAPER_IMAGE; } async start() { const client = await (0, container_runtime_1.getContainerRuntimeClient)(); await client.image.pull(this.imageName, { force: this.pullPolicy.shouldPull(), platform: this.createOpts.platform, }); if (this.beforeContainerCreated) { await this.beforeContainerCreated(); } if (!this.isHelperContainer() && port_forwarder_1.PortForwarderInstance.isRunning()) { const portForwarder = await port_forwarder_1.PortForwarderInstance.getInstance(); this.hostConfig.ExtraHosts = [ ...(this.hostConfig.ExtraHosts ?? []), `host.testcontainers.internal:${portForwarder.getIpAddress()}`, ]; } this.hostConfig.NetworkMode = this.networkAliases.length > 0 ? undefined : this.networkMode; this.createOpts.Labels = { ...(0, labels_1.createLabels)(), ...this.createOpts.Labels }; if (process.env.TESTCONTAINERS_REUSE_ENABLE !== "false" && this.reuse) { return this.reuseOrStartContainer(client); } if (!this.isReaper()) { const reaper = await (0, reaper_1.getReaper)(client); this.createOpts.Labels = { ...this.createOpts.Labels, [labels_1.LABEL_TESTCONTAINERS_SESSION_ID]: reaper.sessionId }; } return this.startContainer(client); } async reuseOrStartContainer(client) { const containerHash = (0, common_1.hash)(JSON.stringify(this.createOpts)); this.createOpts.Labels = { ...this.createOpts.Labels, [labels_1.LABEL_TESTCONTAINERS_CONTAINER_HASH]: containerHash }; common_1.log.debug(`Container reuse has been enabled with hash "${containerHash}"`); return reusableContainerCreationLock.acquire(containerHash, async () => { const container = await client.container.fetchByLabel(labels_1.LABEL_TESTCONTAINERS_CONTAINER_HASH, containerHash, { status: types_1.CONTAINER_STATUSES.filter((status) => status !== "removing" && status !== "dead" && status !== "restarting"), }); if (container !== undefined) { common_1.log.debug(`Found container to reuse with hash "${containerHash}"`, { containerId: container.id }); return this.reuseContainer(client, container); } common_1.log.debug("No container found to reuse"); return this.startContainer(client); }); } async reuseContainer(client, container) { let inspectResult = await client.container.inspect(container); if (!inspectResult.State.Running) { common_1.log.debug("Reused container is not running, attempting to start it"); await client.container.start(container); // Refetch the inspect result to get the updated state inspectResult = await client.container.inspect(container); } const mappedInspectResult = (0, map_inspect_result_1.mapInspectResult)(inspectResult); const boundPorts = bound_ports_1.BoundPorts.fromInspectResult(client.info.containerRuntime.hostIps, mappedInspectResult).filter(this.exposedPorts); if (this.startupTimeout !== undefined) { this.waitStrategy.withStartupTimeout(this.startupTimeout); } await (0, wait_for_container_1.waitForContainer)(client, container, this.waitStrategy, boundPorts); return new started_generic_container_1.StartedGenericContainer(container, client.info.containerRuntime.host, inspectResult, boundPorts, inspectResult.Name, this.waitStrategy, this.autoRemove); } async startContainer(client) { const container = await client.container.create({ ...this.createOpts, HostConfig: this.hostConfig }); if (!this.isHelperContainer() && port_forwarder_1.PortForwarderInstance.isRunning()) { await this.connectContainerToPortForwarder(client, container); } if (this.networkMode && this.networkAliases.length > 0) { const network = client.network.getById(this.networkMode); await client.container.connectToNetwork(container, network, this.networkAliases); } if (this.filesToCopy.length > 0 || this.directoriesToCopy.length > 0 || this.contentsToCopy.length > 0) { const archive = this.createArchiveToCopyToContainer(); archive.finalize(); await client.container.putArchive(container, archive, "/"); } for (const archive of this.archivesToCopy) { await client.container.putArchive(container, archive.tar, archive.target); } common_1.log.info(`Starting container for image "${this.createOpts.Image}"...`, { containerId: container.id }); if (this.containerCreated) { await this.containerCreated(container.id); } await client.container.start(container); common_1.log.info(`Started container for image "${this.createOpts.Image}"`, { containerId: container.id }); const inspectResult = await client.container.inspect(container); const mappedInspectResult = (0, map_inspect_result_1.mapInspectResult)(inspectResult); const boundPorts = bound_ports_1.BoundPorts.fromInspectResult(client.info.containerRuntime.hostIps, mappedInspectResult).filter(this.exposedPorts); if (this.startupTimeout !== undefined) { this.waitStrategy.withStartupTimeout(this.startupTimeout); } if (common_1.containerLog.enabled() || this.logConsumer !== undefined) { if (this.logConsumer !== undefined) { this.logConsumer(await client.container.logs(container)); } if (common_1.containerLog.enabled()) { (await client.container.logs(container)) .on("data", (data) => common_1.containerLog.trace(data.trim(), { containerId: container.id })) .on("err", (data) => common_1.containerLog.error(data.trim(), { containerId: container.id })); } } if (this.containerStarting) { await this.containerStarting(mappedInspectResult, false); } await (0, wait_for_container_1.waitForContainer)(client, container, this.waitStrategy, boundPorts); const startedContainer = new started_generic_container_1.StartedGenericContainer(container, client.info.containerRuntime.host, inspectResult, boundPorts, inspectResult.Name, this.waitStrategy, this.autoRemove); if (this.containerStarted) { await this.containerStarted(startedContainer, mappedInspectResult, false); } return startedContainer; } async connectContainerToPortForwarder(client, container) { const portForwarder = await port_forwarder_1.PortForwarderInstance.getInstance(); const portForwarderNetworkId = portForwarder.getNetworkId(); const excludedNetworks = [portForwarderNetworkId, "none", "host"]; if (!this.networkMode || !excludedNetworks.includes(this.networkMode)) { const network = client.network.getById(portForwarderNetworkId); await client.container.connectToNetwork(container, network, []); } } createArchiveToCopyToContainer() { const tar = (0, archiver_1.default)("tar"); for (const { source, target, mode } of this.filesToCopy) { tar.file(source, { name: target, mode }); } for (const { source, target, mode } of this.directoriesToCopy) { tar.directory(source, target, { mode }); } for (const { content, target, mode } of this.contentsToCopy) { tar.append(content, { name: target, mode }); } return tar; } withCommand(command) { this.createOpts.Cmd = command; return this; } withEntrypoint(entrypoint) { this.createOpts.Entrypoint = entrypoint; return this; } withName(name) { this.createOpts.name = name; return this; } withLabels(labels) { this.createOpts.Labels = { ...this.createOpts.Labels, ...labels }; return this; } withEnvironment(environment) { this.environment = { ...this.environment, ...environment }; this.createOpts.Env = [ ...(this.createOpts.Env ?? []), ...Object.entries(environment).map(([key, value]) => `${key}=${value}`), ]; return this; } withPlatform(platform) { this.createOpts.platform = platform; return this; } withTmpFs(tmpFs) { this.hostConfig.Tmpfs = { ...this.hostConfig.Tmpfs, ...tmpFs }; return this; } withUlimits(ulimits) { this.hostConfig.Ulimits = [ ...(this.hostConfig.Ulimits ?? []), ...Object.entries(ulimits).map(([key, value]) => ({ Name: key, Hard: value.hard, Soft: value.soft, })), ]; return this; } withAddedCapabilities(...capabilities) { this.hostConfig.CapAdd = [...(this.hostConfig.CapAdd ?? []), ...capabilities]; return this; } withDroppedCapabilities(...capabilities) { this.hostConfig.CapDrop = [...(this.hostConfig.CapDrop ?? []), ...capabilities]; return this; } withNetwork(network) { this.networkMode = network.getName(); return this; } withNetworkMode(networkMode) { this.networkMode = networkMode; return this; } withNetworkAliases(...networkAliases) { this.networkAliases = [...this.networkAliases, ...networkAliases]; return this; } withExtraHosts(extraHosts) { this.hostConfig.ExtraHosts = [ ...(this.hostConfig.ExtraHosts ?? []), ...extraHosts.map((extraHost) => `${extraHost.host}:${extraHost.ipAddress}`), ]; return this; } withExposedPorts(...ports) { const exposedPorts = {}; for (const exposedPort of ports) { exposedPorts[(0, port_1.getContainerPort)(exposedPort).toString()] = {}; } this.exposedPorts = [...this.exposedPorts, ...ports]; this.createOpts.ExposedPorts = { ...this.createOpts.ExposedPorts, ...exposedPorts, }; const portBindings = {}; for (const exposedPort of ports) { if ((0, port_1.hasHostBinding)(exposedPort)) { portBindings[exposedPort.container] = [{ HostPort: exposedPort.host.toString() }]; } else { portBindings[exposedPort] = [{ HostPort: "0" }]; } } this.hostConfig.PortBindings = { ...this.hostConfig.PortBindings, ...portBindings, }; return this; } withBindMounts(bindMounts) { this.hostConfig.Binds = bindMounts .map((bindMount) => ({ mode: "rw", ...bindMount })) .map(({ source, target, mode }) => `${source}:${target}:${mode}`); return this; } withHealthCheck(healthCheck) { const toNanos = (duration) => duration * 1e6; this.healthCheck = healthCheck; this.createOpts.Healthcheck = { Test: healthCheck.test, Interval: healthCheck.interval ? toNanos(healthCheck.interval) : 0, Timeout: healthCheck.timeout ? toNanos(healthCheck.timeout) : 0, Retries: healthCheck.retries ?? 0, StartPeriod: healthCheck.startPeriod ? toNanos(healthCheck.startPeriod) : 0, }; return this; } withStartupTimeout(startupTimeoutMs) { this.startupTimeout = startupTimeoutMs; return this; } withWaitStrategy(waitStrategy) { this.waitStrategy = waitStrategy; return this; } withDefaultLogDriver() { this.hostConfig.LogConfig = { Type: "json-file", Config: {}, }; return this; } withPrivilegedMode() { this.hostConfig.Privileged = true; return this; } withUser(user) { this.createOpts.User = user; return this; } withReuse() { this.reuse = true; return this; } withAutoRemove(autoRemove) { this.autoRemove = autoRemove; return this; } withPullPolicy(pullPolicy) { this.pullPolicy = pullPolicy; return this; } withIpcMode(ipcMode) { this.hostConfig.IpcMode = ipcMode; return this; } withCopyFilesToContainer(filesToCopy) { this.filesToCopy = [...this.filesToCopy, ...filesToCopy]; return this; } withCopyDirectoriesToContainer(directoriesToCopy) { this.directoriesToCopy = [...this.directoriesToCopy, ...directoriesToCopy]; return this; } withCopyContentToContainer(contentsToCopy) { this.contentsToCopy = [...this.contentsToCopy, ...contentsToCopy]; return this; } withCopyArchivesToContainer(archivesToCopy) { this.archivesToCopy = [...this.archivesToCopy, ...archivesToCopy]; return this; } withWorkingDir(workingDir) { this.createOpts.WorkingDir = workingDir; return this; } withResourcesQuota({ memory, cpu }) { this.hostConfig.Memory = memory !== undefined ? memory * 1024 ** 3 : undefined; this.hostConfig.NanoCpus = cpu !== undefined ? cpu * 10 ** 9 : undefined; return this; } withSharedMemorySize(bytes) { this.hostConfig.ShmSize = bytes; return this; } withLogConsumer(logConsumer) { this.logConsumer = logConsumer; return this; } withHostname(hostname) { this.createOpts.Hostname = hostname; return this; } } exports.GenericContainer = GenericContainer; //# sourceMappingURL=generic-container.js.map