Skip to content

Commit

Permalink
Add support for specifying image platform (#806)
Browse files Browse the repository at this point in the history
  • Loading branch information
weyert committed Aug 16, 2024
1 parent 1891647 commit 1947842
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 33 deletions.
66 changes: 37 additions & 29 deletions docs/features/containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
Expand All @@ -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",
Expand All @@ -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).

Expand All @@ -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"
}])
Expand All @@ -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"
}])
Expand All @@ -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)
}])
Expand All @@ -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)
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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.

Expand Down Expand 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<void> {
// ...
}

protected override async containerCreated(containerId: string): Promise<void> {
// ...
}

protected override async containerStarting(
inspectResult: InspectResult,
reused: boolean
): Promise<void> {
// ...
}

protected override async containerStarted(
container: StartedTestContainer,
inspectResult: InspectResult,
Expand All @@ -443,7 +451,7 @@ class CustomStartedContainer extends AbstractStartedContainer {
protected override async containerStopping(): Promise<void> {
// ...
}

protected override async containerStopped(): Promise<void> {
// ...
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export class DockerImageClient implements ImageClient {
});
}

async pull(imageName: ImageName, opts?: { force: boolean }): Promise<void> {
async pull(imageName: ImageName, opts?: { force: boolean; platform: string | undefined }): Promise<void> {
try {
if (!opts?.force && (await this.exists(imageName))) {
log.debug(`Image "${imageName.string}" already exists`);
Expand All @@ -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<void>((resolve) => {
byline(stream).on("data", (line) => {
if (pullLog.enabled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { ImageName } from "../../image-name";

export interface ImageClient {
build(context: string, opts: ImageBuildOptions): Promise<void>;
pull(imageName: ImageName, opts?: { force: boolean }): Promise<void>;
pull(imageName: ImageName, opts?: { force: boolean; platform: string | undefined }): Promise<void>;
exists(imageName: ImageName): Promise<boolean>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -72,6 +78,7 @@ export class GenericContainerBuilder {
registryconfig: registryConfig,
labels,
target: this.target,
platform: this.platform,
};

if (this.pullPolicy.shouldPull()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ export class GenericContainer implements TestContainer {

public async start(): Promise<StartedTestContainer> {
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();
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/testcontainers/src/test-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 1947842

Please sign in to comment.