Skip to content

Commit 217ae9c

Browse files
authored
Inline buildx global build and target platform envvars when resolving base image and user (#1169)
1 parent 71fb140 commit 217ae9c

File tree

11 files changed

+122
-55
lines changed

11 files changed

+122
-55
lines changed

src/spec-node/configContainer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ async function resolveWithLocalFolder(params: DockerResolverParameters, parsedAu
6060

6161
const { dockerCLI, dockerComposeCLI } = params;
6262
const { env } = common;
63-
const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output, platformInfo: params.platformInfo };
63+
const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo };
6464
await ensureNoDisallowedFeatures(cliParams, config, additionalFeatures, idLabels);
6565

6666
await runInitializeCommand({ ...params, common: { ...common, output: common.lifecycleHook.output } }, config.initializeCommand, common.lifecycleHook.onDidInput);

src/spec-node/devContainers.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,12 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
172172
output: common.output,
173173
}, dockerPath, dockerComposePath);
174174

175-
const platformInfo = (() => {
175+
const buildPlatformInfo = {
176+
os: mapNodeOSToGOOS(cliHost.platform),
177+
arch: mapNodeArchitectureToGOARCH(cliHost.arch),
178+
};
179+
180+
const targetPlatformInfo = (() => {
176181
if (common.buildxPlatform) {
177182
const slash1 = common.buildxPlatform.indexOf('/');
178183
const slash2 = common.buildxPlatform.indexOf('/', slash1 + 1);
@@ -204,7 +209,8 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
204209
dockerComposeCLI,
205210
env: cliHost.env,
206211
output,
207-
platformInfo
212+
buildPlatformInfo,
213+
targetPlatformInfo
208214
}));
209215

210216
const dockerEngineVer = await dockerEngineVersion({
@@ -213,7 +219,8 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
213219
dockerComposeCLI,
214220
env: cliHost.env,
215221
output,
216-
platformInfo
222+
buildPlatformInfo,
223+
targetPlatformInfo
217224
});
218225

219226
return {
@@ -246,7 +253,8 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
246253
additionalLabels: options.additionalLabels,
247254
buildxOutput: common.buildxOutput,
248255
buildxCacheTo: common.buildxCacheTo,
249-
platformInfo
256+
buildPlatformInfo,
257+
targetPlatformInfo
250258
};
251259
}
252260

src/spec-node/devContainersSpecCLI.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,7 @@ async function doBuild({
641641
throw new ContainerError({ description: '--push true cannot be used with --output.' });
642642
}
643643

644-
const buildParams: DockerCLIParameters = { cliHost, dockerCLI: params.dockerCLI, dockerComposeCLI, env, output, platformInfo: params.platformInfo };
644+
const buildParams: DockerCLIParameters = { cliHost, dockerCLI: params.dockerCLI, dockerComposeCLI, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo };
645645
await ensureNoDisallowedFeatures(buildParams, config, additionalFeatures, undefined);
646646

647647
// Support multiple use of `--image-name`
@@ -1058,16 +1058,18 @@ async function readConfiguration({
10581058
env: cliHost.env,
10591059
output,
10601060
}, dockerCLI, dockerComposePath || 'docker-compose');
1061+
const buildPlatformInfo = {
1062+
os: mapNodeOSToGOOS(cliHost.platform),
1063+
arch: mapNodeArchitectureToGOARCH(cliHost.arch),
1064+
};
10611065
const params: DockerCLIParameters = {
10621066
cliHost,
10631067
dockerCLI,
10641068
dockerComposeCLI,
10651069
env: cliHost.env,
10661070
output,
1067-
platformInfo: {
1068-
os: mapNodeOSToGOOS(cliHost.platform),
1069-
arch: mapNodeArchitectureToGOARCH(cliHost.arch),
1070-
}
1071+
buildPlatformInfo,
1072+
targetPlatformInfo: buildPlatformInfo
10711073
};
10721074
const { container, idLabels } = await findContainerAndIdLabels(params, containerId, providedIdLabels, workspaceFolder, configPath?.fsPath);
10731075
if (container) {

src/spec-node/dockerCompose.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const serviceLabel = 'com.docker.compose.service';
2727
export async function openDockerComposeDevContainer(params: DockerResolverParameters, workspace: Workspace, config: SubstitutedConfig<DevContainerFromDockerComposeConfig>, idLabels: string[], additionalFeatures: Record<string, string | boolean | Record<string, string | boolean>>): Promise<ResolverResult> {
2828
const { common, dockerCLI, dockerComposeCLI } = params;
2929
const { cliHost, env, output } = common;
30-
const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output, platformInfo: params.platformInfo };
30+
const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo };
3131
return _openDockerComposeDevContainer(params, buildParams, workspace, config, getRemoteWorkspaceFolder(config.config), idLabels, additionalFeatures);
3232
}
3333

@@ -155,7 +155,7 @@ export async function buildAndExtendDockerCompose(configWithRaw: SubstitutedConf
155155
const { cliHost, env, output } = common;
156156
const { config } = configWithRaw;
157157

158-
const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI: dockerComposeCLIFunc, env, output, platformInfo: params.platformInfo };
158+
const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI: dockerComposeCLIFunc, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo };
159159
const composeConfig = await readDockerComposeConfig(cliParams, localComposeFiles, envFile);
160160
const composeService = composeConfig.services[config.service];
161161

src/spec-node/dockerfileUtils.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export function extractDockerfile(dockerfile: string): Dockerfile {
8181
} as Dockerfile;
8282
}
8383

84-
export function findUserStatement(dockerfile: Dockerfile, buildArgs: Record<string, string>, baseImageEnv: Record<string, string>, target: string | undefined) {
84+
export function findUserStatement(dockerfile: Dockerfile, buildArgs: Record<string, string>, baseImageEnv: Record<string, string>, globalBuildxPlatformArgs: Record<string, string> = {}, target: string | undefined) {
8585
let stage: Stage | undefined = target ? dockerfile.stagesByLabel[target] : dockerfile.stages[dockerfile.stages.length - 1];
8686
const seen = new Set<Stage>();
8787
while (stage) {
@@ -92,15 +92,15 @@ export function findUserStatement(dockerfile: Dockerfile, buildArgs: Record<stri
9292

9393
const i = findLastIndex(stage.instructions, i => i.instruction === 'USER');
9494
if (i !== -1) {
95-
return replaceVariables(dockerfile, buildArgs, baseImageEnv, stage.instructions[i].name, stage, i) || undefined;
95+
return replaceVariables(dockerfile, buildArgs, baseImageEnv, globalBuildxPlatformArgs, stage.instructions[i].name, stage, i) || undefined;
9696
}
97-
const image = replaceVariables(dockerfile, buildArgs, baseImageEnv, stage.from.image, dockerfile.preamble, dockerfile.preamble.instructions.length);
97+
const image = replaceVariables(dockerfile, buildArgs, baseImageEnv, globalBuildxPlatformArgs, stage.from.image, dockerfile.preamble, dockerfile.preamble.instructions.length);
9898
stage = dockerfile.stagesByLabel[image];
9999
}
100100
return undefined;
101101
}
102102

103-
export function findBaseImage(dockerfile: Dockerfile, buildArgs: Record<string, string>, target: string | undefined) {
103+
export function findBaseImage(dockerfile: Dockerfile, buildArgs: Record<string, string>, target: string | undefined, globalBuildxPlatformArgs: Record<string, string> = {}) {
104104
let stage: Stage | undefined = target ? dockerfile.stagesByLabel[target] : dockerfile.stages[dockerfile.stages.length - 1];
105105
const seen = new Set<Stage>();
106106
while (stage) {
@@ -109,7 +109,7 @@ export function findBaseImage(dockerfile: Dockerfile, buildArgs: Record<string,
109109
}
110110
seen.add(stage);
111111

112-
const image = replaceVariables(dockerfile, buildArgs, /* not available in FROM instruction */ {}, stage.from.image, dockerfile.preamble, dockerfile.preamble.instructions.length);
112+
const image = replaceVariables(dockerfile, buildArgs, /* not available in FROM instruction */ {}, globalBuildxPlatformArgs, stage.from.image, dockerfile.preamble, dockerfile.preamble.instructions.length);
113113
const nextStage = dockerfile.stagesByLabel[image];
114114
if (!nextStage) {
115115
return image;
@@ -155,12 +155,12 @@ function getExpressionValue(option: string, isSet: boolean, word: string, value:
155155
return operations[option](isSet, word, value).replace(/^['"]|['"]$/g, ''); // remove quotes from start and end of the string
156156
}
157157

158-
function replaceVariables(dockerfile: Dockerfile, buildArgs: Record<string, string>, baseImageEnv: Record<string, string>, str: string, stage: { from?: From; instructions: Instruction[] }, beforeInstructionIndex: number) {
158+
function replaceVariables(dockerfile: Dockerfile, buildArgs: Record<string, string>, baseImageEnv: Record<string, string>, globalBuildxPlatformArgs: Record<string, string> = {}, str: string, stage: { from?: From; instructions: Instruction[] }, beforeInstructionIndex: number) {
159159
return [...str.matchAll(argumentExpression)]
160160
.map(match => {
161161
const variable = match.groups!.variable;
162162
const isVarExp = match.groups!.isVarExp ? true : false;
163-
let value = findValue(dockerfile, buildArgs, baseImageEnv, variable, stage, beforeInstructionIndex) || '';
163+
let value = findValue(dockerfile, buildArgs, baseImageEnv, globalBuildxPlatformArgs, variable, stage, beforeInstructionIndex) || '';
164164
if (isVarExp) {
165165
// Handle replacing variable expressions (${var:+word}) if they exist
166166
const option = match.groups!.option;
@@ -178,7 +178,7 @@ function replaceVariables(dockerfile: Dockerfile, buildArgs: Record<string, stri
178178
.reduce((str, { begin, end, value }) => str.substring(0, begin) + value + str.substring(end), str);
179179
}
180180

181-
function findValue(dockerfile: Dockerfile, buildArgs: Record<string, string>, baseImageEnv: Record<string, string>, variable: string, stage: { from?: From; instructions: Instruction[] }, beforeInstructionIndex: number): string | undefined {
181+
function findValue(dockerfile: Dockerfile, buildArgs: Record<string, string>, baseImageEnv: Record<string, string>, globalBuildxPlatformArgs: Record<string, string> = {}, variable: string, stage: { from?: From; instructions: Instruction[] }, beforeInstructionIndex: number): string | undefined {
182182
let considerArg = true;
183183
const seen = new Set<typeof stage>();
184184
while (true) {
@@ -191,22 +191,22 @@ function findValue(dockerfile: Dockerfile, buildArgs: Record<string, string>, ba
191191
if (i !== -1) {
192192
const instruction = stage.instructions[i];
193193
if (instruction.instruction === 'ENV') {
194-
return replaceVariables(dockerfile, buildArgs, baseImageEnv, instruction.value!, stage, i);
194+
return replaceVariables(dockerfile, buildArgs, baseImageEnv, globalBuildxPlatformArgs, instruction.value!, stage, i);
195195
}
196196
if (instruction.instruction === 'ARG') {
197-
return replaceVariables(dockerfile, buildArgs, baseImageEnv, buildArgs[instruction.name] ?? instruction.value, stage, i);
197+
return replaceVariables(dockerfile, buildArgs, baseImageEnv, globalBuildxPlatformArgs, buildArgs[instruction.name] ?? instruction.value, stage, i);
198198
}
199199
}
200200

201201
if (!stage.from) {
202-
const value = baseImageEnv[variable];
202+
const value = baseImageEnv[variable] ?? globalBuildxPlatformArgs[variable];
203203
if (typeof value === 'string') {
204204
return value;
205205
}
206206
return undefined;
207207
}
208208

209-
const image = replaceVariables(dockerfile, buildArgs, baseImageEnv, stage.from.image, dockerfile.preamble, dockerfile.preamble.instructions.length);
209+
const image = replaceVariables(dockerfile, buildArgs, baseImageEnv, globalBuildxPlatformArgs, stage.from.image, dockerfile.preamble, dockerfile.preamble.instructions.length);
210210
stage = dockerfile.stagesByLabel[image] || dockerfile.preamble;
211211
beforeInstructionIndex = stage.instructions.length;
212212
considerArg = stage === dockerfile.preamble;

src/spec-node/imageMetadata.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { ContainerError } from '../spec-common/errors';
7+
import { PlatformInfo } from '../spec-common/commonUtils';
78
import { LifecycleCommand, LifecycleHooksInstallMap } from '../spec-common/injectHeadless';
89
import { DevContainerConfig, DevContainerConfigCommand, DevContainerFromDockerComposeConfig, DevContainerFromDockerfileConfig, DevContainerFromImageConfig, getDockerComposeFilePaths, getDockerfilePath, HostGPURequirements, HostRequirements, isDockerFileConfig, PortAttributes, UserEnvProbe } from '../spec-configuration/configuration';
910
import { Feature, FeaturesConfig, Mount, parseMount, SchemaFeatureLifecycleHooks } from '../spec-configuration/containerFeaturesConfiguration';
@@ -349,7 +350,7 @@ export async function getImageBuildInfo(params: DockerResolverParameters | Docke
349350
const cwdEnvFile = cliHost.path.join(cliHost.cwd, '.env');
350351
const envFile = Array.isArray(config.dockerComposeFile) && config.dockerComposeFile.length === 0 && await cliHost.isFile(cwdEnvFile) ? cwdEnvFile : undefined;
351352
const composeFiles = await getDockerComposeFilePaths(cliHost, config, cliHost.env, cliHost.cwd);
352-
const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env: cliHost.env, output, platformInfo: params.platformInfo };
353+
const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env: cliHost.env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo };
353354

354355
const composeConfig = await readDockerComposeConfig(buildParams, composeFiles, envFile);
355356
const services = Object.keys(composeConfig.services || {});
@@ -394,18 +395,37 @@ export async function getImageBuildInfoFromImage(params: DockerResolverParameter
394395
export async function getImageBuildInfoFromDockerfile(params: DockerResolverParameters | DockerCLIParameters, dockerfile: string, dockerBuildArgs: Record<string, string>, targetStage: string | undefined, substitute: SubstituteConfig) {
395396
const { output } = 'output' in params ? params : params.common;
396397
const omitSyntaxDirective = 'common' in params ? !!params.common.omitSyntaxDirective : false;
397-
return internalGetImageBuildInfoFromDockerfile(imageName => inspectDockerImage(params, imageName, true), dockerfile, dockerBuildArgs, targetStage, substitute, output, omitSyntaxDirective);
398+
return internalGetImageBuildInfoFromDockerfile(imageName => inspectDockerImage(params, imageName, true), dockerfile, dockerBuildArgs, targetStage, substitute, output, omitSyntaxDirective, params.buildPlatformInfo, params.targetPlatformInfo);
398399
}
399400

400-
export async function internalGetImageBuildInfoFromDockerfile(inspectDockerImage: (imageName: string) => Promise<ImageDetails>, dockerfileText: string, dockerBuildArgs: Record<string, string>, targetStage: string | undefined, substitute: SubstituteConfig, output: Log, omitSyntaxDirective: boolean): Promise<ImageBuildInfo> {
401+
export async function internalGetImageBuildInfoFromDockerfile(inspectDockerImage: (imageName: string) => Promise<ImageDetails>, dockerfileText: string, dockerBuildArgs: Record<string, string>, targetStage: string | undefined, substitute: SubstituteConfig, output: Log, omitSyntaxDirective: boolean, buildPlatform: PlatformInfo, targetPlatform: PlatformInfo): Promise<ImageBuildInfo> {
401402
const dockerfile = extractDockerfile(dockerfileText);
402403
if (dockerfile.preamble.directives.syntax && omitSyntaxDirective) {
403404
output.write(`Omitting syntax directive '${dockerfile.preamble.directives.syntax}' from Dockerfile.`, LogLevel.Trace);
404405
delete dockerfile.preamble.directives.syntax;
405406
}
406-
const baseImage = findBaseImage(dockerfile, dockerBuildArgs, targetStage);
407+
// https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#automatic-platform-args-in-the-global-scope
408+
const globalBuildxPlatformArgs = {
409+
// platform of the node performing the build.
410+
BUILDPLATFORM: [buildPlatform.os, buildPlatform.arch, buildPlatform.variant].filter(Boolean).join("/"),
411+
// OS component of BUILDPLATFORM
412+
BUILDOS: buildPlatform.os,
413+
// architecture component of BUILDPLATFORM
414+
BUILDARCH: buildPlatform.arch,
415+
// variant component of BUILDPLATFORM
416+
BUILDVARIANT: buildPlatform.variant ?? "",
417+
// platform of the build result. Eg linux/amd64, linux/arm/v7, windows/amd64.
418+
TARGETPLATFORM: [targetPlatform.os, targetPlatform.arch, targetPlatform.variant].filter(Boolean).join("/"),
419+
// OS component of TARGETPLATFORM
420+
TARGETOS: targetPlatform.os,
421+
// architecture component of TARGETPLATFORM
422+
TARGETARCH: targetPlatform.arch,
423+
// variant component of TARGETPLATFORM
424+
TARGETVARIANT: targetPlatform.variant ?? "",
425+
};
426+
const baseImage = findBaseImage(dockerfile, dockerBuildArgs, targetStage, globalBuildxPlatformArgs);
407427
const imageDetails = baseImage && await inspectDockerImage(baseImage) || undefined;
408-
const dockerfileUser = findUserStatement(dockerfile, dockerBuildArgs, envListToObj(imageDetails?.Config.Env), targetStage);
428+
const dockerfileUser = findUserStatement(dockerfile, dockerBuildArgs, envListToObj(imageDetails?.Config.Env), globalBuildxPlatformArgs, targetStage);
409429
const user = dockerfileUser || imageDetails?.Config.User || 'root';
410430
const metadata = imageDetails ? getImageMetadata(imageDetails, substitute, output) : { config: [], raw: [], substitute };
411431
return {

src/spec-node/upgradeCommand.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,16 +86,18 @@ async function featuresUpgrade({
8686
env: cliHost.env,
8787
output,
8888
}, dockerPath, dockerComposePath);
89+
const buildPlatformInfo = {
90+
os: mapNodeOSToGOOS(cliHost.platform),
91+
arch: mapNodeArchitectureToGOARCH(cliHost.arch),
92+
};
8993
const dockerParams: DockerCLIParameters = {
9094
cliHost,
9195
dockerCLI: dockerPath,
9296
dockerComposeCLI,
9397
env: cliHost.env,
9498
output,
95-
platformInfo: {
96-
os: mapNodeOSToGOOS(cliHost.platform),
97-
arch: mapNodeArchitectureToGOARCH(cliHost.arch),
98-
}
99+
buildPlatformInfo,
100+
targetPlatformInfo: buildPlatformInfo,
99101
};
100102

101103
const workspace = workspaceFromPath(cliHost.path, workspaceFolder);

src/spec-node/utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,8 @@ export interface DockerResolverParameters {
134134
additionalLabels: string[];
135135
buildxOutput: string | undefined;
136136
buildxCacheTo: string | undefined;
137-
platformInfo: PlatformInfo;
137+
buildPlatformInfo: PlatformInfo;
138+
targetPlatformInfo: PlatformInfo;
138139
}
139140

140141
export interface ResolverResult {
@@ -250,7 +251,7 @@ export async function inspectDockerImage(params: DockerResolverParameters | Dock
250251
throw inspectErr;
251252
}
252253
try {
253-
return await inspectImageInRegistry(output, params.platformInfo, imageName);
254+
return await inspectImageInRegistry(output, params.targetPlatformInfo, imageName);
254255
} catch (inspectErr2) {
255256
output.write(`Error fetching image details: ${inspectErr2?.message}`, LogLevel.Info);
256257
}

src/spec-shutdown/dockerUtils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ export interface DockerCLIParameters {
5252
dockerComposeCLI: () => Promise<DockerComposeCLI>;
5353
env: NodeJS.ProcessEnv;
5454
output: Log;
55-
platformInfo: PlatformInfo;
55+
buildPlatformInfo: PlatformInfo;
56+
targetPlatformInfo: PlatformInfo;
5657
}
5758

5859
export interface PartialExecParameters {

0 commit comments

Comments
 (0)