diff --git a/docs/features/containers.md b/docs/features/containers.md index 8f664b6df..43bbbb0ce 100644 --- a/docs/features/containers.md +++ b/docs/features/containers.md @@ -68,6 +68,14 @@ const container = await new GenericContainer("alpine") .start(); ``` +### With a platform + +```javascript +const container = await new GenericContainer("alpine") + .withPlatform("linux/arm64") // similar to `--platform linux/arm64` + .start(); +``` + ### With bind mounts **Not recommended.** @@ -76,9 +84,9 @@ Bind mounts are not portable. They do not work with Docker in Docker or in cases ```javascript const container = await new GenericContainer("alpine") - .withBindMounts([{ - source: "/local/file.txt", - target:"/remote/file.txt" + .withBindMounts([{ + source: "/local/file.txt", + target:"/remote/file.txt" }, { source: "/local/dir", target:"/remote/dir", @@ -97,7 +105,7 @@ const container = await new GenericContainer("alpine") ### With a name -**Not recommended.** +**Not recommended.** If a container with the same name already exists, Docker will raise a conflict. If you are specifying a name to enable container to container communication, look into creating a network and using [network aliases](../networking#network-aliases). @@ -113,15 +121,15 @@ Copy files/directories or content to a container before it starts: ```javascript const container = await new GenericContainer("alpine") - .withCopyFilesToContainer([{ - source: "/local/file.txt", + .withCopyFilesToContainer([{ + source: "/local/file.txt", target: "/remote/file1.txt" }]) .withCopyDirectoriesToContainer([{ source: "/localdir", target: "/some/nested/remotedir" }]) - .withCopyContentToContainer([{ + .withCopyContentToContainer([{ content: "hello world", target: "/remote/file2.txt" }]) @@ -133,15 +141,15 @@ Or after it starts: ```javascript const container = await new GenericContainer("alpine").start(); -container.copyFilesToContainer([{ - source: "/local/file.txt", +container.copyFilesToContainer([{ + source: "/local/file.txt", target: "/remote/file1.txt" }]) container.copyDirectoriesToContainer([{ source: "/localdir", target: "/some/nested/remotedir" }]) -container.copyContentToContainer([{ +container.copyContentToContainer([{ content: "hello world", target: "/remote/file2.txt" }]) @@ -151,8 +159,8 @@ An optional `mode` can be specified in octal for setting file permissions: ```javascript const container = await new GenericContainer("alpine") - .withCopyFilesToContainer([{ - source: "/local/file.txt", + .withCopyFilesToContainer([{ + source: "/local/file.txt", target: "/remote/file1.txt", mode: parseInt("0644", 8) }]) @@ -161,7 +169,7 @@ const container = await new GenericContainer("alpine") target: "/some/nested/remotedir", mode: parseInt("0644", 8) }]) - .withCopyContentToContainer([{ + .withCopyContentToContainer([{ content: "hello world", target: "/remote/file2.txt", mode: parseInt("0644", 8) @@ -258,10 +266,10 @@ const container = await new GenericContainer("alpine") ```javascript const container = await new GenericContainer("aline") - .withUlimits({ - memlock: { - hard: -1, - soft: -1 + .withUlimits({ + memlock: { + hard: -1, + soft: -1 } }) .start(); @@ -339,7 +347,7 @@ await container.restart(); ## Reusing a container -Enabling container re-use means that Testcontainers will not start a new container if a Testcontainers managed container with the same configuration is already running. +Enabling container re-use means that Testcontainers will not start a new container if a Testcontainers managed container with the same configuration is already running. This is useful for example if you want to share a container across tests without global set up. @@ -403,29 +411,29 @@ const startedCustomContainer: StartedTestContainer = await customContainer.start Define your own lifecycle callbacks for better control over your custom containers: ```typescript -import { - GenericContainer, - AbstractStartedContainer, - StartedTestContainer, - InspectResult +import { + GenericContainer, + AbstractStartedContainer, + StartedTestContainer, + InspectResult } from "testcontainers"; class CustomContainer extends GenericContainer { protected override async beforeContainerCreated(): Promise { // ... } - + protected override async containerCreated(containerId: string): Promise { // ... } - + protected override async containerStarting( inspectResult: InspectResult, reused: boolean ): Promise { // ... } - + protected override async containerStarted( container: StartedTestContainer, inspectResult: InspectResult, @@ -443,7 +451,7 @@ class CustomStartedContainer extends AbstractStartedContainer { protected override async containerStopping(): Promise { // ... } - + protected override async containerStopped(): Promise { // ... } @@ -495,7 +503,7 @@ const container = await new GenericContainer("alpine") ## Running commands -To run a command inside an already started container use the `exec` method. The command will be run in the container's +To run a command inside an already started container use the `exec` method. The command will be run in the container's working directory, returning the command output and exit code: ```javascript @@ -555,7 +563,7 @@ const container = await new GenericContainer("alpine") .start(); ``` -You can specify a point in time as a UNIX timestamp from which you want the logs to start: +You can specify a point in time as a UNIX timestamp from which you want the logs to start: ```javascript const msInSec = 1000; diff --git a/packages/testcontainers/src/container-runtime/clients/image/docker-image-client.ts b/packages/testcontainers/src/container-runtime/clients/image/docker-image-client.ts index 0c57fce34..47f2fb799 100644 --- a/packages/testcontainers/src/container-runtime/clients/image/docker-image-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/image/docker-image-client.ts @@ -87,7 +87,7 @@ export class DockerImageClient implements ImageClient { }); } - async pull(imageName: ImageName, opts?: { force: boolean }): Promise { + async pull(imageName: ImageName, opts?: { force: boolean; platform: string | undefined }): Promise { try { if (!opts?.force && (await this.exists(imageName))) { log.debug(`Image "${imageName.string}" already exists`); @@ -96,7 +96,10 @@ export class DockerImageClient implements ImageClient { log.debug(`Pulling image "${imageName.string}"...`); const authconfig = await getAuthConfig(imageName.registry ?? this.indexServerAddress); - const stream = await this.dockerode.pull(imageName.string, { authconfig }); + const stream = await this.dockerode.pull(imageName.string, { + authconfig, + platform: opts?.platform, + }); await new Promise((resolve) => { byline(stream).on("data", (line) => { if (pullLog.enabled()) { diff --git a/packages/testcontainers/src/container-runtime/clients/image/image-client.ts b/packages/testcontainers/src/container-runtime/clients/image/image-client.ts index 15a1eed9b..a4b92f2db 100644 --- a/packages/testcontainers/src/container-runtime/clients/image/image-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/image/image-client.ts @@ -3,6 +3,6 @@ import { ImageName } from "../../image-name"; export interface ImageClient { build(context: string, opts: ImageBuildOptions): Promise; - pull(imageName: ImageName, opts?: { force: boolean }): Promise; + pull(imageName: ImageName, opts?: { force: boolean; platform: string | undefined }): Promise; exists(imageName: ImageName): Promise; } diff --git a/packages/testcontainers/src/generic-container/generic-container-builder.ts b/packages/testcontainers/src/generic-container/generic-container-builder.ts index 74d254b2f..25ee2c329 100644 --- a/packages/testcontainers/src/generic-container/generic-container-builder.ts +++ b/packages/testcontainers/src/generic-container/generic-container-builder.ts @@ -18,6 +18,7 @@ export class GenericContainerBuilder { private pullPolicy: ImagePullPolicy = PullPolicy.defaultPolicy(); private cache = true; private target?: string; + private platform?: string; constructor( private readonly context: string, @@ -40,6 +41,11 @@ export class GenericContainerBuilder { return this; } + public withPlatform(platform: string): this { + this.platform = platform; + return this; + } + public withTarget(target: string): this { this.target = target; return this; @@ -72,6 +78,7 @@ export class GenericContainerBuilder { registryconfig: registryConfig, labels, target: this.target, + platform: this.platform, }; if (this.pullPolicy.shouldPull()) { diff --git a/packages/testcontainers/src/generic-container/generic-container.test.ts b/packages/testcontainers/src/generic-container/generic-container.test.ts index fa02099d7..6484c0ef6 100644 --- a/packages/testcontainers/src/generic-container/generic-container.test.ts +++ b/packages/testcontainers/src/generic-container/generic-container.test.ts @@ -123,6 +123,18 @@ describe("GenericContainer", () => { expect(output).toEqual(expect.stringContaining("/tmp")); }); + it("should set platform", async () => { + const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14") + .withPullPolicy(PullPolicy.alwaysPull()) + .withCommand(["node", "../index.js"]) + .withPlatform("linux/amd64") + .withExposedPorts(8080) + .start(); + + const { output } = await container.exec(["arch"]); + expect(output).toEqual(expect.stringContaining("x86_64")); + }); + it("should set entrypoint", async () => { const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14") .withEntrypoint(["node"]) diff --git a/packages/testcontainers/src/generic-container/generic-container.ts b/packages/testcontainers/src/generic-container/generic-container.ts index ef50853cf..373f31ce5 100644 --- a/packages/testcontainers/src/generic-container/generic-container.ts +++ b/packages/testcontainers/src/generic-container/generic-container.ts @@ -79,7 +79,10 @@ export class GenericContainer implements TestContainer { public async start(): Promise { const client = await getContainerRuntimeClient(); - await client.image.pull(this.imageName, { force: this.pullPolicy.shouldPull() }); + await client.image.pull(this.imageName, { + force: this.pullPolicy.shouldPull(), + platform: this.createOpts.platform, + }); if (this.beforeContainerCreated) { await this.beforeContainerCreated(); @@ -278,6 +281,11 @@ export class GenericContainer implements TestContainer { return this; } + public withPlatform(platform: string): this { + this.createOpts.platform = platform; + return this; + } + public withTmpFs(tmpFs: TmpFs): this { this.hostConfig.Tmpfs = { ...this.hostConfig.Tmpfs, ...tmpFs }; return this; diff --git a/packages/testcontainers/src/test-container.ts b/packages/testcontainers/src/test-container.ts index bfcf9d128..b2410024c 100644 --- a/packages/testcontainers/src/test-container.ts +++ b/packages/testcontainers/src/test-container.ts @@ -36,6 +36,7 @@ export interface TestContainer { withExtraHosts(extraHosts: ExtraHost[]): this; withDefaultLogDriver(): this; withPrivilegedMode(): this; + withPlatform(platform: string): this; withUser(user: string): this; withPullPolicy(pullPolicy: ImagePullPolicy): this; withReuse(): this;