Skip to content

Commit 1947842

Browse files
authored
Add support for specifying image platform (#806)
1 parent 1891647 commit 1947842

File tree

7 files changed

+72
-33
lines changed

7 files changed

+72
-33
lines changed

docs/features/containers.md

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ const container = await new GenericContainer("alpine")
6868
.start();
6969
```
7070

71+
### With a platform
72+
73+
```javascript
74+
const container = await new GenericContainer("alpine")
75+
.withPlatform("linux/arm64") // similar to `--platform linux/arm64`
76+
.start();
77+
```
78+
7179
### With bind mounts
7280

7381
**Not recommended.**
@@ -76,9 +84,9 @@ Bind mounts are not portable. They do not work with Docker in Docker or in cases
7684

7785
```javascript
7886
const container = await new GenericContainer("alpine")
79-
.withBindMounts([{
80-
source: "/local/file.txt",
81-
target:"/remote/file.txt"
87+
.withBindMounts([{
88+
source: "/local/file.txt",
89+
target:"/remote/file.txt"
8290
}, {
8391
source: "/local/dir",
8492
target:"/remote/dir",
@@ -97,7 +105,7 @@ const container = await new GenericContainer("alpine")
97105

98106
### With a name
99107

100-
**Not recommended.**
108+
**Not recommended.**
101109

102110
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).
103111

@@ -113,15 +121,15 @@ Copy files/directories or content to a container before it starts:
113121

114122
```javascript
115123
const container = await new GenericContainer("alpine")
116-
.withCopyFilesToContainer([{
117-
source: "/local/file.txt",
124+
.withCopyFilesToContainer([{
125+
source: "/local/file.txt",
118126
target: "/remote/file1.txt"
119127
}])
120128
.withCopyDirectoriesToContainer([{
121129
source: "/localdir",
122130
target: "/some/nested/remotedir"
123131
}])
124-
.withCopyContentToContainer([{
132+
.withCopyContentToContainer([{
125133
content: "hello world",
126134
target: "/remote/file2.txt"
127135
}])
@@ -133,15 +141,15 @@ Or after it starts:
133141
```javascript
134142
const container = await new GenericContainer("alpine").start();
135143

136-
container.copyFilesToContainer([{
137-
source: "/local/file.txt",
144+
container.copyFilesToContainer([{
145+
source: "/local/file.txt",
138146
target: "/remote/file1.txt"
139147
}])
140148
container.copyDirectoriesToContainer([{
141149
source: "/localdir",
142150
target: "/some/nested/remotedir"
143151
}])
144-
container.copyContentToContainer([{
152+
container.copyContentToContainer([{
145153
content: "hello world",
146154
target: "/remote/file2.txt"
147155
}])
@@ -151,8 +159,8 @@ An optional `mode` can be specified in octal for setting file permissions:
151159

152160
```javascript
153161
const container = await new GenericContainer("alpine")
154-
.withCopyFilesToContainer([{
155-
source: "/local/file.txt",
162+
.withCopyFilesToContainer([{
163+
source: "/local/file.txt",
156164
target: "/remote/file1.txt",
157165
mode: parseInt("0644", 8)
158166
}])
@@ -161,7 +169,7 @@ const container = await new GenericContainer("alpine")
161169
target: "/some/nested/remotedir",
162170
mode: parseInt("0644", 8)
163171
}])
164-
.withCopyContentToContainer([{
172+
.withCopyContentToContainer([{
165173
content: "hello world",
166174
target: "/remote/file2.txt",
167175
mode: parseInt("0644", 8)
@@ -258,10 +266,10 @@ const container = await new GenericContainer("alpine")
258266

259267
```javascript
260268
const container = await new GenericContainer("aline")
261-
.withUlimits({
262-
memlock: {
263-
hard: -1,
264-
soft: -1
269+
.withUlimits({
270+
memlock: {
271+
hard: -1,
272+
soft: -1
265273
}
266274
})
267275
.start();
@@ -339,7 +347,7 @@ await container.restart();
339347

340348
## Reusing a container
341349

342-
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.
350+
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.
343351

344352
This is useful for example if you want to share a container across tests without global set up.
345353

@@ -403,29 +411,29 @@ const startedCustomContainer: StartedTestContainer = await customContainer.start
403411
Define your own lifecycle callbacks for better control over your custom containers:
404412

405413
```typescript
406-
import {
407-
GenericContainer,
408-
AbstractStartedContainer,
409-
StartedTestContainer,
410-
InspectResult
414+
import {
415+
GenericContainer,
416+
AbstractStartedContainer,
417+
StartedTestContainer,
418+
InspectResult
411419
} from "testcontainers";
412420

413421
class CustomContainer extends GenericContainer {
414422
protected override async beforeContainerCreated(): Promise<void> {
415423
// ...
416424
}
417-
425+
418426
protected override async containerCreated(containerId: string): Promise<void> {
419427
// ...
420428
}
421-
429+
422430
protected override async containerStarting(
423431
inspectResult: InspectResult,
424432
reused: boolean
425433
): Promise<void> {
426434
// ...
427435
}
428-
436+
429437
protected override async containerStarted(
430438
container: StartedTestContainer,
431439
inspectResult: InspectResult,
@@ -443,7 +451,7 @@ class CustomStartedContainer extends AbstractStartedContainer {
443451
protected override async containerStopping(): Promise<void> {
444452
// ...
445453
}
446-
454+
447455
protected override async containerStopped(): Promise<void> {
448456
// ...
449457
}
@@ -495,7 +503,7 @@ const container = await new GenericContainer("alpine")
495503

496504
## Running commands
497505

498-
To run a command inside an already started container use the `exec` method. The command will be run in the container's
506+
To run a command inside an already started container use the `exec` method. The command will be run in the container's
499507
working directory, returning the command output and exit code:
500508

501509
```javascript
@@ -555,7 +563,7 @@ const container = await new GenericContainer("alpine")
555563
.start();
556564
```
557565

558-
You can specify a point in time as a UNIX timestamp from which you want the logs to start:
566+
You can specify a point in time as a UNIX timestamp from which you want the logs to start:
559567

560568
```javascript
561569
const msInSec = 1000;

packages/testcontainers/src/container-runtime/clients/image/docker-image-client.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export class DockerImageClient implements ImageClient {
8787
});
8888
}
8989

90-
async pull(imageName: ImageName, opts?: { force: boolean }): Promise<void> {
90+
async pull(imageName: ImageName, opts?: { force: boolean; platform: string | undefined }): Promise<void> {
9191
try {
9292
if (!opts?.force && (await this.exists(imageName))) {
9393
log.debug(`Image "${imageName.string}" already exists`);
@@ -96,7 +96,10 @@ export class DockerImageClient implements ImageClient {
9696

9797
log.debug(`Pulling image "${imageName.string}"...`);
9898
const authconfig = await getAuthConfig(imageName.registry ?? this.indexServerAddress);
99-
const stream = await this.dockerode.pull(imageName.string, { authconfig });
99+
const stream = await this.dockerode.pull(imageName.string, {
100+
authconfig,
101+
platform: opts?.platform,
102+
});
100103
await new Promise<void>((resolve) => {
101104
byline(stream).on("data", (line) => {
102105
if (pullLog.enabled()) {

packages/testcontainers/src/container-runtime/clients/image/image-client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ import { ImageName } from "../../image-name";
33

44
export interface ImageClient {
55
build(context: string, opts: ImageBuildOptions): Promise<void>;
6-
pull(imageName: ImageName, opts?: { force: boolean }): Promise<void>;
6+
pull(imageName: ImageName, opts?: { force: boolean; platform: string | undefined }): Promise<void>;
77
exists(imageName: ImageName): Promise<boolean>;
88
}

packages/testcontainers/src/generic-container/generic-container-builder.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export class GenericContainerBuilder {
1818
private pullPolicy: ImagePullPolicy = PullPolicy.defaultPolicy();
1919
private cache = true;
2020
private target?: string;
21+
private platform?: string;
2122

2223
constructor(
2324
private readonly context: string,
@@ -40,6 +41,11 @@ export class GenericContainerBuilder {
4041
return this;
4142
}
4243

44+
public withPlatform(platform: string): this {
45+
this.platform = platform;
46+
return this;
47+
}
48+
4349
public withTarget(target: string): this {
4450
this.target = target;
4551
return this;
@@ -72,6 +78,7 @@ export class GenericContainerBuilder {
7278
registryconfig: registryConfig,
7379
labels,
7480
target: this.target,
81+
platform: this.platform,
7582
};
7683

7784
if (this.pullPolicy.shouldPull()) {

packages/testcontainers/src/generic-container/generic-container.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,18 @@ describe("GenericContainer", () => {
123123
expect(output).toEqual(expect.stringContaining("/tmp"));
124124
});
125125

126+
it("should set platform", async () => {
127+
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
128+
.withPullPolicy(PullPolicy.alwaysPull())
129+
.withCommand(["node", "../index.js"])
130+
.withPlatform("linux/amd64")
131+
.withExposedPorts(8080)
132+
.start();
133+
134+
const { output } = await container.exec(["arch"]);
135+
expect(output).toEqual(expect.stringContaining("x86_64"));
136+
});
137+
126138
it("should set entrypoint", async () => {
127139
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
128140
.withEntrypoint(["node"])

packages/testcontainers/src/generic-container/generic-container.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,10 @@ export class GenericContainer implements TestContainer {
7979

8080
public async start(): Promise<StartedTestContainer> {
8181
const client = await getContainerRuntimeClient();
82-
await client.image.pull(this.imageName, { force: this.pullPolicy.shouldPull() });
82+
await client.image.pull(this.imageName, {
83+
force: this.pullPolicy.shouldPull(),
84+
platform: this.createOpts.platform,
85+
});
8386

8487
if (this.beforeContainerCreated) {
8588
await this.beforeContainerCreated();
@@ -278,6 +281,11 @@ export class GenericContainer implements TestContainer {
278281
return this;
279282
}
280283

284+
public withPlatform(platform: string): this {
285+
this.createOpts.platform = platform;
286+
return this;
287+
}
288+
281289
public withTmpFs(tmpFs: TmpFs): this {
282290
this.hostConfig.Tmpfs = { ...this.hostConfig.Tmpfs, ...tmpFs };
283291
return this;

packages/testcontainers/src/test-container.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export interface TestContainer {
3636
withExtraHosts(extraHosts: ExtraHost[]): this;
3737
withDefaultLogDriver(): this;
3838
withPrivilegedMode(): this;
39+
withPlatform(platform: string): this;
3940
withUser(user: string): this;
4041
withPullPolicy(pullPolicy: ImagePullPolicy): this;
4142
withReuse(): this;

0 commit comments

Comments
 (0)