Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for specifying image platform #806

Merged
merged 9 commits into from
Aug 16, 2024
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
Loading