Skip to content

Commit ff3f044

Browse files
committed
inline buildx global build and target platform envvars when resolving base image and user
1 parent 9ee8cfb commit ff3f044

File tree

11 files changed

+104
-32
lines changed

11 files changed

+104
-32
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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export function findUserStatement(dockerfile: Dockerfile, buildArgs: Record<stri
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, globalBuildxPlatformArgs, stage.from.image, dockerfile.preamble, dockerfile.preamble.instructions.length);
113113
const nextStage = dockerfile.stagesByLabel[image];
114114
if (!nextStage) {
115115
return image;

src/spec-node/imageMetadata.ts

Lines changed: 28 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,40 @@ 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, {
429+
...envListToObj(imageDetails?.Config.Env),
430+
...globalBuildxPlatformArgs,
431+
}, targetStage);
409432
const user = dockerfileUser || imageDetails?.Config.User || 'root';
410433
const metadata = imageDetails ? getImageMetadata(imageDetails, substitute, output) : { config: [], raw: [], substitute };
411434
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 {

src/test/dockerfileUtils.test.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ FROM ubuntu:latest as dev
178178
const info = await internalGetImageBuildInfoFromDockerfile(async (imageName) => {
179179
assert.strictEqual(imageName, 'ubuntu:latest');
180180
return details;
181-
}, dockerfile, {}, undefined, testSubstitute, nullLog, false);
181+
}, dockerfile, {}, undefined, testSubstitute, nullLog, false, {} as any, {} as any);
182182
assert.strictEqual(info.user, 'imageUser');
183183
assert.strictEqual(info.metadata.config.length, 1);
184184
assert.strictEqual(info.metadata.config[0].id, 'testid-substituted');
@@ -206,11 +206,44 @@ USER dockerfileUserB
206206
const info = await internalGetImageBuildInfoFromDockerfile(async (imageName) => {
207207
assert.strictEqual(imageName, 'ubuntu:latest');
208208
return details;
209-
}, dockerfile, {}, undefined, testSubstitute, nullLog, false);
209+
}, dockerfile, {}, undefined, testSubstitute, nullLog, false, {} as any, {} as any);
210210
assert.strictEqual(info.user, 'dockerfileUserB');
211211
assert.strictEqual(info.metadata.config.length, 0);
212212
assert.strictEqual(info.metadata.raw.length, 0);
213213
});
214+
215+
it('for a USER in a multiarch image', async () => {
216+
const dockerfile = `
217+
FROM ubuntu:latest as base-amd64
218+
USER amd64_user
219+
220+
FROM ubuntu:latest as base-arm64
221+
USER arm64_user
222+
223+
FROM base-\${TARGETARCH}
224+
225+
ARG TARGETARCH
226+
`;
227+
const details: ImageDetails = {
228+
Id: '123',
229+
Config: {
230+
User: 'imageUser',
231+
Env: null,
232+
Labels: null,
233+
Entrypoint: null,
234+
Cmd: null
235+
},
236+
Os: 'linux',
237+
Architecture: 'amd64'
238+
};
239+
const info = await internalGetImageBuildInfoFromDockerfile(async (imageName) => {
240+
assert.strictEqual(imageName, 'ubuntu:latest');
241+
return details;
242+
}, dockerfile, {}, undefined, testSubstitute, nullLog, false, {} as any, { os: 'linux', arch: 'amd64' });
243+
assert.strictEqual(info.user, 'amd64_user');
244+
assert.strictEqual(info.metadata.config.length, 0);
245+
assert.strictEqual(info.metadata.raw.length, 0);
246+
});
214247
});
215248

216249
describe('findBaseImage', () => {

0 commit comments

Comments
 (0)