Skip to content
Closed
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
166 changes: 114 additions & 52 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 4 additions & 5 deletions packages/modules/azurite/src/azurite-container.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { BlobServiceClient, StorageSharedKeyCredential } from "@azure/storage-bl
import { QueueServiceClient } from "@azure/storage-queue";
import fs from "node:fs";
import path from "node:path";
import { RandomPortGenerator } from "testcontainers";
import { getRandomPort } from "testcontainers";
import { getImage } from "../../../testcontainers/src/utils/test-helper";
import { AzuriteContainer } from "./azurite-container";
import { createOAuthToken, createTokenCredential, getTlsPipelineOptions } from "./azurite-test-utils";
Expand Down Expand Up @@ -99,10 +99,9 @@ describe("AzuriteContainer", { timeout: 240_000 }, () => {

it("should be able to specify custom ports", async () => {
// customPorts {
const portGenerator = new RandomPortGenerator();
const blobPort = await portGenerator.generatePort();
const queuePort = await portGenerator.generatePort();
const tablePort = await portGenerator.generatePort();
const blobPort = await getRandomPort();
const queuePort = await getRandomPort();
const tablePort = await getRandomPort();

await using container = await new AzuriteContainer(IMAGE)
.withSkipApiVersionCheck()
Expand Down
3 changes: 1 addition & 2 deletions packages/modules/couchbase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"testcontainers": "^12.0.3"
},
"devDependencies": {
"couchbase": "^4.7.1",
"get-port": "^7.2.0"
"couchbase": "^4.7.1"
}
}
4 changes: 2 additions & 2 deletions packages/modules/couchbase/src/couchbase-container.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import couchbase, { Bucket, Cluster } from "couchbase";
import getPort from "get-port";
import { getRandomPort } from "testcontainers";
import { getImage } from "../../../testcontainers/src/utils/test-helper";
import { BucketDefinition } from "./bucket-definition";
import { CouchbaseContainer } from "./couchbase-container";
Expand Down Expand Up @@ -130,7 +130,7 @@ describe("CouchbaseContainer", { timeout: 180_000 }, () => {
});

it("should preserve fixed host port binding", async () => {
const hostPort = await getPort();
const hostPort = await getRandomPort();
await using container = await new CouchbaseContainer(COMMUNITY_IMAGE)
.withEnabledServices(CouchbaseService.KV)
.withExposedPorts({ container: PORTS.MGMT_PORT, host: hostPort })
Expand Down
5 changes: 2 additions & 3 deletions packages/testcontainers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,28 +32,27 @@
"dependencies": {
"@balena/dockerignore": "^1.0.2",
"@types/dockerode": "^4.0.1",
"archiver": "^7.0.1",
"async-lock": "^1.4.1",
"byline": "^5.0.0",
"debug": "^4.4.3",
"docker-compose": "^1.4.2",
"dockerode": "^5.0.0",
"get-port": "^7.2.0",
"proper-lockfile": "^4.1.2",
"properties-reader": "^3.0.1",
"ssh-remote-port-forward": "^1.0.4",
"tar-fs": "^3.1.2",
"tar-stream": "^3.2.0",
"tmp": "^0.2.7",
"undici": "^8.3.0"
},
"devDependencies": {
"@types/archiver": "^7.0.0",
"@types/async-lock": "^1.4.2",
"@types/byline": "^4.2.36",
"@types/debug": "^4.1.13",
"@types/proper-lockfile": "^4.1.4",
"@types/properties-reader": "^2.1.3",
"@types/tar-fs": "^2.0.4",
"@types/tar-stream": "^3.1.4",
"@types/tmp": "^0.2.6"
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import archiver from "archiver";
import getPort from "get-port";
import path from "path";
import * as tarStream from "tar-stream";
import { RandomUuid } from "../common";
import { getContainerRuntimeClient } from "../container-runtime";
import { getRandomPort } from "../utils/port-generator";
import { PullPolicy } from "../utils/pull-policy";
import {
checkContainerIsHealthy,
Expand Down Expand Up @@ -38,7 +38,7 @@ describe("GenericContainer", { timeout: 180_000 }, () => {
});

it("should bind to specified host port", async () => {
const hostPort = await getPort();
const hostPort = await getRandomPort();
await using container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withExposedPorts({
container: 8080,
Expand All @@ -51,7 +51,7 @@ describe("GenericContainer", { timeout: 180_000 }, () => {
});

it("should bind to specified host port with a different protocol", async () => {
const hostPort = await getPort();
const hostPort = await getRandomPort();
await using container = await new GenericContainer("mendhak/udp-listener")
.withWaitStrategy(Wait.forLogMessage("Listening on UDP port 5005"))
.withExposedPorts({
Expand Down Expand Up @@ -473,6 +473,19 @@ describe("GenericContainer", { timeout: 180_000 }, () => {
expect((await container.exec("cat /tmp/test.txt")).output).toEqual(expect.stringContaining("hello world"));
});

it("should copy directory to started container with permissions", async () => {
const source = path.resolve(fixtures, "docker");
const target = "/tmp/started-newdir";
const mode = parseInt("0777", 8);
await using container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withExposedPorts(8080)
.start();

await container.copyDirectoriesToContainer([{ source, target, mode }]);

expect((await container.exec(`stat -c "%a %n" /tmp/started-newdir/test.txt`)).output).toContain("777");
});

it("should copy content to container", async () => {
const content = "hello world";
const target = "/tmp/test.txt";
Expand Down Expand Up @@ -521,9 +534,7 @@ describe("GenericContainer", { timeout: 180_000 }, () => {
.withExposedPorts(8080)
.start();

const tar = archiver("tar");
tar.append("hello world", { name: targetWithCopyOwnership.slice(1), uid, gid } as archiver.EntryData);
tar.finalize();
const tar = createArchiveWithOwnership(targetWithCopyOwnership, uid, gid);

await container.copyArchiveToContainer(tar, "/", { copyUIDGID: true });

Expand All @@ -536,9 +547,7 @@ describe("GenericContainer", { timeout: 180_000 }, () => {
const uid = 4242;
const gid = 4343;
const targetWithCopyOwnership = "/tmp/with-copy-archives-copyuidgid.txt";
const tar = archiver("tar");
tar.append("hello world", { name: targetWithCopyOwnership.slice(1), uid, gid } as archiver.EntryData);
tar.finalize();
const tar = createArchiveWithOwnership(targetWithCopyOwnership, uid, gid);

await using containerWithCopyOwnership = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withCopyArchivesToContainer([
Expand Down Expand Up @@ -672,3 +681,27 @@ describe("GenericContainer", { timeout: 180_000 }, () => {
await GenericContainer.fromDockerfile(context).withTarget("invalid").build();
});
});

const createArchiveWithOwnership = (target: string, uid: number, gid: number) => {
const content = "hello world";
const tar = tarStream.pack();

tar.entry(
{
name: target.slice(1),
uid,
gid,
size: Buffer.byteLength(content),
},
content,
(err) => {
if (err) {
tar.destroy(err);
} else {
tar.finalize();
}
}
);

return tar;
};
32 changes: 6 additions & 26 deletions packages/testcontainers/src/generic-container/generic-container.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import archiver from "archiver";
import AsyncLock from "async-lock";
import { Container, ContainerCreateOptions, ContainerInspectInfo, HostConfig } from "dockerode";
import { promises as fs } from "fs";
import { Readable } from "stream";
import { containerLog, hash, log, toNanos } from "../common";
import { ContainerRuntimeClient, getContainerRuntimeClient, ImageName } from "../container-runtime";
Expand Down Expand Up @@ -31,6 +29,7 @@ import { createLabels, LABEL_TESTCONTAINERS_CONTAINER_HASH, LABEL_TESTCONTAINERS
import { mapInspectResult } from "../utils/map-inspect-result";
import { getContainerPort, getProtocol, hasHostBinding, PortWithOptionalBinding } from "../utils/port";
import { ImagePullPolicy, PullPolicy } from "../utils/pull-policy";
import { createTarArchive } from "../utils/tar-archive";
import { selectWaitStrategy } from "../wait-strategies/utils/wait-strategy-selector";
import { waitForContainer } from "../wait-strategies/wait-for-container";
import { WaitStrategy } from "../wait-strategies/wait-strategy";
Expand Down Expand Up @@ -198,8 +197,11 @@ export class GenericContainer implements TestContainer {
}

if (this.filesToCopy.length > 0 || this.directoriesToCopy.length > 0 || this.contentsToCopy.length > 0) {
const archive = await this.createArchiveToCopyToContainer();
archive.finalize();
const archive = await createTarArchive({
filesToCopy: this.filesToCopy,
directoriesToCopy: this.directoriesToCopy,
contentsToCopy: this.contentsToCopy,
});
await client.container.putArchive(container, archive, "/", this.copyToContainerOptions);
}

Expand Down Expand Up @@ -275,28 +277,6 @@ export class GenericContainer implements TestContainer {
}
}

private async createArchiveToCopyToContainer(): Promise<archiver.Archiver> {
const tar = archiver("tar");
const filesToCopyWithStats = await Promise.all(
this.filesToCopy.map(async (fileToCopy) => ({
...fileToCopy,
stats: await fs.stat(fileToCopy.source),
}))
);

for (const { source, target, mode, stats } of filesToCopyWithStats) {
tar.file(source, { name: target, mode, stats });
}
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;
}

protected containerStarted?(
container: StartedTestContainer,
inspectResult: InspectResult,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import archiver from "archiver";
import AsyncLock from "async-lock";
import Dockerode, { ContainerInspectInfo } from "dockerode";
import { promises as fs } from "fs";
import { Readable } from "stream";
import { containerLog, log } from "../common";
import { ContainerRuntimeClient, getContainerRuntimeClient } from "../container-runtime";
Expand All @@ -21,6 +19,7 @@ import { BoundPorts } from "../utils/bound-ports";
import { LABEL_TESTCONTAINERS_SESSION_ID } from "../utils/labels";
import { mapInspectResult } from "../utils/map-inspect-result";
import { PortWithOptionalBinding } from "../utils/port";
import { createTarArchive } from "../utils/tar-archive";
import { waitForContainer } from "../wait-strategies/wait-for-container";
import { WaitStrategy } from "../wait-strategies/wait-strategy";
import { inspectContainerUntilPortsExposed } from "./inspect-container-util-ports-exposed";
Expand Down Expand Up @@ -192,35 +191,23 @@ export class StartedGenericContainer implements StartedTestContainer {
public async copyFilesToContainer(filesToCopy: FileToCopy[]): Promise<void> {
log.debug(`Copying files to container...`, { containerId: this.container.id });
const client = await getContainerRuntimeClient();
const tar = archiver("tar");
const filesToCopyWithStats = await Promise.all(
filesToCopy.map(async (fileToCopy) => ({
...fileToCopy,
stats: await fs.stat(fileToCopy.source),
}))
);
filesToCopyWithStats.forEach(({ source, target, mode, stats }) => tar.file(source, { name: target, mode, stats }));
tar.finalize();
const tar = await createTarArchive({ filesToCopy });
await client.container.putArchive(this.container, tar, "/");
log.debug(`Copied files to container`, { containerId: this.container.id });
}

public async copyDirectoriesToContainer(directoriesToCopy: DirectoryToCopy[]): Promise<void> {
log.debug(`Copying directories to container...`, { containerId: this.container.id });
const client = await getContainerRuntimeClient();
const tar = archiver("tar");
directoriesToCopy.forEach(({ source, target }) => tar.directory(source, target));
tar.finalize();
const tar = await createTarArchive({ directoriesToCopy });
await client.container.putArchive(this.container, tar, "/");
log.debug(`Copied directories to container`, { containerId: this.container.id });
}

public async copyContentToContainer(contentsToCopy: ContentToCopy[]): Promise<void> {
log.debug(`Copying content to container...`, { containerId: this.container.id });
const client = await getContainerRuntimeClient();
const tar = archiver("tar");
contentsToCopy.forEach(({ content, target, mode }) => tar.append(content, { name: target, mode: mode }));
tar.finalize();
const tar = await createTarArchive({ contentsToCopy });
await client.container.putArchive(this.container, tar, "/");
log.debug(`Copied content to container`, { containerId: this.container.id });
}
Expand Down
2 changes: 1 addition & 1 deletion packages/testcontainers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export { CommitOptions, Content, CopyToContainerOptions, ExecOptions, ExecResult
export { BoundPorts } from "./utils/bound-ports";
export { LABEL_TESTCONTAINERS_SESSION_ID } from "./utils/labels";
export { PortWithBinding, PortWithOptionalBinding, getContainerPort, hasHostBinding } from "./utils/port";
export { PortGenerator, RandomPortGenerator } from "./utils/port-generator";
export { PortGenerator, RandomPortGenerator, getRandomPort } from "./utils/port-generator";
export { ImagePullPolicy, PullPolicy } from "./utils/pull-policy";
export { HttpWaitStrategyOptions } from "./wait-strategies/http-wait-strategy";
export { StartupCheckStrategy, StartupStatus } from "./wait-strategies/startup-check-strategy";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import { GenericContainer } from "../generic-container/generic-container";
import { RandomPortGenerator } from "../utils/port-generator";
import { getRandomPort } from "../utils/port-generator";
import { createTestServer } from "../utils/test-helper";

describe.sequential("Port Forwarder reuse", { timeout: 180_000 }, () => {
const portGen = new RandomPortGenerator();

it("should expose additional ports", async () => {
const { TestContainers: TC1 } = await import("../test-containers.js");
const { PortForwarderInstance: PFI1 } = await import("../port-forwarder/port-forwarder.js");
const port1 = await portGen.generatePort();
const port1 = await getRandomPort();
const server1 = await createTestServer(port1);
await TC1.exposeHostPorts(port1);
const portForwarder1ContainerId = (await PFI1.getInstance()).getContainerId();

vi.resetModules();
const { TestContainers: TC2 } = await import("../test-containers.js");
const { PortForwarderInstance: PFI2 } = await import("../port-forwarder/port-forwarder.js");
const port2 = await portGen.generatePort();
const port2 = await getRandomPort();
const server2 = await createTestServer(port2);
await TC2.exposeHostPorts(port2);
const portForwarder2ContainerId = (await PFI2.getInstance()).getContainerId();
Expand All @@ -34,7 +32,7 @@ describe.sequential("Port Forwarder reuse", { timeout: 180_000 }, () => {
});

it("should reuse same ports", async () => {
const port = await portGen.generatePort();
const port = await getRandomPort();
const server = await createTestServer(port);

const { TestContainers: TC1 } = await import("../test-containers.js");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { GenericContainer } from "../generic-container/generic-container";
import { Network } from "../network/network";
import { TestContainers } from "../test-containers";
import { RandomPortGenerator } from "../utils/port-generator";
import { getRandomPort } from "../utils/port-generator";
import { createTestServer } from "../utils/test-helper";

describe("PortForwarder", { timeout: 180_000 }, () => {
const portGen = new RandomPortGenerator();

it("should expose host ports to the container", async () => {
const randomPort = await portGen.generatePort();
const randomPort = await getRandomPort();
await using _ = await createTestServer(randomPort);
await TestContainers.exposeHostPorts(randomPort);

Expand All @@ -19,7 +17,7 @@ describe("PortForwarder", { timeout: 180_000 }, () => {
});

it("should expose host ports to the container with custom network", async () => {
const randomPort = await portGen.generatePort();
const randomPort = await getRandomPort();
await using _ = await createTestServer(randomPort);
await TestContainers.exposeHostPorts(randomPort);

Expand All @@ -33,7 +31,7 @@ describe("PortForwarder", { timeout: 180_000 }, () => {
});

it("should expose host ports to the container with custom network and network alias", async () => {
const randomPort = await portGen.generatePort();
const randomPort = await getRandomPort();
await using _ = await createTestServer(randomPort);
await TestContainers.exposeHostPorts(randomPort);

Expand Down
6 changes: 3 additions & 3 deletions packages/testcontainers/src/reaper/reaper.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ContainerRuntimeClient, getContainerRuntimeClient } from "../container-runtime";
import { RandomPortGenerator } from "../utils/port-generator";
import { getRandomPort } from "../utils/port-generator";

describe.sequential("Reaper", { timeout: 120_000 }, () => {
let client: ContainerRuntimeClient;
Expand Down Expand Up @@ -56,7 +56,7 @@ describe.sequential("Reaper", { timeout: 120_000 }, () => {
it("should create new reaper container when existing reaper cannot be reached", async () => {
const reaper = await getReaper();
vi.resetModules();
const unreachablePort = await new RandomPortGenerator().generatePort();
const unreachablePort = await getRandomPort();
const reaperContainerInfo = (await client.container.list()).filter((c) => c.Id === reaper.containerId)[0];
reaperContainerInfo.Labels["TESTCONTAINERS_RYUK_TEST_LABEL"] = "false";
const reaperPort = reaperContainerInfo.Ports.find((port) => port.PrivatePort == 8080);
Expand All @@ -72,7 +72,7 @@ describe.sequential("Reaper", { timeout: 120_000 }, () => {
});

it("should use custom port when TESTCONTAINERS_RYUK_PORT is set", async () => {
const customPort = (await new RandomPortGenerator().generatePort()).toString();
const customPort = (await getRandomPort()).toString();
vi.stubEnv("TESTCONTAINERS_RYUK_PORT", customPort);
vi.spyOn(client.container, "list").mockResolvedValue([]);

Expand Down
Loading
Loading