Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/data-expander.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export function imageComplex (data: any) {
return {
name: typeof data === "string" ? data : data.name,
entrypoint: data.entrypoint,
...(data.docker?.user ? {docker: {user: data.docker?.user}} : {}),
...(data.docker?.user || data.docker?.platform ? {docker: {user: data.docker.user, platform: data.docker.platform}} : {}),
};
}

Expand Down
33 changes: 29 additions & 4 deletions src/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,7 @@ If you know what you're doing and would like to suppress this warning, use one o
this._dotenvVariables = await this.initProducerReportsDotenvVariables(writeStreams, Utils.expandVariables(this._variables));
const expanded = Utils.unscape$$Variables(Utils.expandVariables({...this._variables, ...this._dotenvVariables}));
const imageName = this.imageName(expanded);
const imagePlatform = this.imagePlatform(expanded);
const helperImageName = argv.helperImage;
const safeJobName = this.safeJobName;

Expand All @@ -682,7 +683,7 @@ If you know what you're doing and would like to suppress this warning, use one o
}

if (imageName) {
await this.pullImage(writeStreams, imageName);
await this.pullImage(writeStreams, imageName, imagePlatform);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

const buildVolumeName = this.buildVolumeName;
const tmpVolumeName = this.tmpVolumeName;
Expand Down Expand Up @@ -971,6 +972,11 @@ If you know what you're doing and would like to suppress this warning, use one o
dockerCmd += `--user ${imageUser} `;
}

const imagePlatform = this.imagePlatform(expanded);
if (imagePlatform) {
dockerCmd += `--platform ${imagePlatform} `;
}

if (this.argv.containerEmulate) {
const runnerName: string = this.argv.containerEmulate;

Expand Down Expand Up @@ -1207,6 +1213,13 @@ If you know what you're doing and would like to suppress this warning, use one o
return Utils.expandText(image["docker"]["user"], vars);
}

private imagePlatform (vars: {[key: string]: string} = {}): string | null {
const image = this.jobData["image"];
if (!image) return null;
if (!image["docker"]) return null;
return Utils.expandText(image["docker"]["platform"], vars);
}

get imageEntrypoint (): string[] | null {
const image = this.jobData["image"];

Expand Down Expand Up @@ -1235,21 +1248,33 @@ If you know what you're doing and would like to suppress this warning, use one o
}
}

private async pullImage (writeStreams: WriteStreams, imageToPull: string) {
private async pullImage (writeStreams: WriteStreams, imageToPull: string, imagePlatform: string | null = null) {
const pullPolicy = this.argv.pullPolicy;
const platformArgs = imagePlatform ? ["--platform", imagePlatform] : [];
const platformSuffix = imagePlatform ? ` (${imagePlatform})` : "";
const actualPull = async () => {
await this.validateCiDependencyProxyServerAuthentication(imageToPull);
const time = process.hrtime();
await Utils.spawn([this.argv.containerExecutable, "pull", imageToPull]);
await Utils.spawn([this.argv.containerExecutable, "pull", imageToPull, ...platformArgs]);
const endTime = process.hrtime(time);
writeStreams.stdout(chalk`${this.formattedJobName} {magentaBright pulled} ${imageToPull} in {magenta ${prettyHrtime(endTime)}}\n`);
writeStreams.stdout(chalk`${this.formattedJobName} {magentaBright pulled} ${imageToPull}${platformSuffix} in {magenta ${prettyHrtime(endTime)}}\n`);
this.refreshLongRunningSilentTimeout(writeStreams);
};

if (pullPolicy === "always") {
await actualPull();
return;
}
// The `image inspect` cache check is platform-agnostic — a different-arch
// variant of the same image name will satisfy it, causing the requested
// platform to silently differ from what's actually on disk. Force a pull
// when a specific platform was requested so the matching manifest is
// guaranteed locally. Pulls are idempotent: if the variant is already
// cached, `docker pull` short-circuits with "Image is up to date".
if (imagePlatform) {
await actualPull();
return;
}
try {
await Utils.spawn([this.argv.containerExecutable, "image", "inspect", imageToPull]);
} catch {
Expand Down
8 changes: 8 additions & 0 deletions tests/test-cases/image/.gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ image-user:
script:
- id -u

image-platform:
image:
name: alpine
docker:
platform: linux/amd64
script:
- uname -m

image-entrypoint-with-variables:
variables:
FOO: BAR
Expand Down
23 changes: 23 additions & 0 deletions tests/test-cases/image/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,29 @@ test.concurrent("image <image-user>", async () => {
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
});

test.concurrent("image <image-platform>", async () => {
const writeStreams = new WriteStreamsMock();

await handler({
cwd: "tests/test-cases/image",
job: ["image-platform"],
stateDir: ".gitlab-ci-local-image-platform",
}, writeStreams);

// The "pulled" log line includes the requested platform, proving --platform
// was forwarded to `docker pull` rather than silently dropped. Use `.*` to
// span the chalk ANSI escape codes between tokens. We deliberately don't
// pass `pullPolicy: "always"` — the platform-aware short-circuit inside
// pullImage forces a pull whenever a platform is set, regardless of whether
// a different-arch variant of the same image happens to be cached locally.
expect(writeStreams.stdoutLines.join("\n")).toMatch(/pulled.*alpine.*linux\/amd64/);

// The job runs successfully on the host (linux/amd64 matches the CI runner arch).
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining([
chalk`{blueBright image-platform} {greenBright >} x86_64`,
]));
});

test.concurrent("pull invalid image", async () => {
const jobs: Job[] = [];
const writeStreams = new WriteStreamsMock();
Expand Down