diff --git a/src/spec-node/configContainer.ts b/src/spec-node/configContainer.ts index b4e159657..c0b21eb82 100644 --- a/src/spec-node/configContainer.ts +++ b/src/spec-node/configContainer.ts @@ -60,7 +60,7 @@ async function resolveWithLocalFolder(params: DockerResolverParameters, parsedAu const { dockerCLI, dockerComposeCLI } = params; const { env } = common; - const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output, platformInfo: params.platformInfo }; + const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo }; await ensureNoDisallowedFeatures(cliParams, config, additionalFeatures, idLabels); await runInitializeCommand({ ...params, common: { ...common, output: common.lifecycleHook.output } }, config.initializeCommand, common.lifecycleHook.onDidInput); diff --git a/src/spec-node/devContainers.ts b/src/spec-node/devContainers.ts index 8a2e6b042..a856890c6 100644 --- a/src/spec-node/devContainers.ts +++ b/src/spec-node/devContainers.ts @@ -172,7 +172,12 @@ export async function createDockerParams(options: ProvisionOptions, disposables: output: common.output, }, dockerPath, dockerComposePath); - const platformInfo = (() => { + const buildPlatformInfo = { + os: mapNodeOSToGOOS(cliHost.platform), + arch: mapNodeArchitectureToGOARCH(cliHost.arch), + }; + + const targetPlatformInfo = (() => { if (common.buildxPlatform) { const slash1 = common.buildxPlatform.indexOf('/'); const slash2 = common.buildxPlatform.indexOf('/', slash1 + 1); @@ -204,7 +209,8 @@ export async function createDockerParams(options: ProvisionOptions, disposables: dockerComposeCLI, env: cliHost.env, output, - platformInfo + buildPlatformInfo, + targetPlatformInfo })); const dockerEngineVer = await dockerEngineVersion({ @@ -213,7 +219,8 @@ export async function createDockerParams(options: ProvisionOptions, disposables: dockerComposeCLI, env: cliHost.env, output, - platformInfo + buildPlatformInfo, + targetPlatformInfo }); return { @@ -246,7 +253,8 @@ export async function createDockerParams(options: ProvisionOptions, disposables: additionalLabels: options.additionalLabels, buildxOutput: common.buildxOutput, buildxCacheTo: common.buildxCacheTo, - platformInfo + buildPlatformInfo, + targetPlatformInfo }; } diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts index fbd501591..18c44136b 100644 --- a/src/spec-node/devContainersSpecCLI.ts +++ b/src/spec-node/devContainersSpecCLI.ts @@ -641,7 +641,7 @@ async function doBuild({ throw new ContainerError({ description: '--push true cannot be used with --output.' }); } - const buildParams: DockerCLIParameters = { cliHost, dockerCLI: params.dockerCLI, dockerComposeCLI, env, output, platformInfo: params.platformInfo }; + const buildParams: DockerCLIParameters = { cliHost, dockerCLI: params.dockerCLI, dockerComposeCLI, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo }; await ensureNoDisallowedFeatures(buildParams, config, additionalFeatures, undefined); // Support multiple use of `--image-name` @@ -1058,16 +1058,18 @@ async function readConfiguration({ env: cliHost.env, output, }, dockerCLI, dockerComposePath || 'docker-compose'); + const buildPlatformInfo = { + os: mapNodeOSToGOOS(cliHost.platform), + arch: mapNodeArchitectureToGOARCH(cliHost.arch), + }; const params: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env: cliHost.env, output, - platformInfo: { - os: mapNodeOSToGOOS(cliHost.platform), - arch: mapNodeArchitectureToGOARCH(cliHost.arch), - } + buildPlatformInfo, + targetPlatformInfo: buildPlatformInfo }; const { container, idLabels } = await findContainerAndIdLabels(params, containerId, providedIdLabels, workspaceFolder, configPath?.fsPath); if (container) { diff --git a/src/spec-node/dockerCompose.ts b/src/spec-node/dockerCompose.ts index 4c20ebd68..8093464cc 100644 --- a/src/spec-node/dockerCompose.ts +++ b/src/spec-node/dockerCompose.ts @@ -27,7 +27,7 @@ const serviceLabel = 'com.docker.compose.service'; export async function openDockerComposeDevContainer(params: DockerResolverParameters, workspace: Workspace, config: SubstitutedConfig, idLabels: string[], additionalFeatures: Record>): Promise { const { common, dockerCLI, dockerComposeCLI } = params; const { cliHost, env, output } = common; - const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output, platformInfo: params.platformInfo }; + const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo }; return _openDockerComposeDevContainer(params, buildParams, workspace, config, getRemoteWorkspaceFolder(config.config), idLabels, additionalFeatures); } @@ -155,7 +155,7 @@ export async function buildAndExtendDockerCompose(configWithRaw: SubstitutedConf const { cliHost, env, output } = common; const { config } = configWithRaw; - const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI: dockerComposeCLIFunc, env, output, platformInfo: params.platformInfo }; + const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI: dockerComposeCLIFunc, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo }; const composeConfig = await readDockerComposeConfig(cliParams, localComposeFiles, envFile); const composeService = composeConfig.services[config.service]; diff --git a/src/spec-node/dockerfileUtils.ts b/src/spec-node/dockerfileUtils.ts index 2a07023aa..b65833fe7 100644 --- a/src/spec-node/dockerfileUtils.ts +++ b/src/spec-node/dockerfileUtils.ts @@ -100,7 +100,7 @@ export function findUserStatement(dockerfile: Dockerfile, buildArgs: Record, target: string | undefined) { +export function findBaseImage(dockerfile: Dockerfile, buildArgs: Record, target: string | undefined, globalBuildxPlatformArgs: Record = {}) { let stage: Stage | undefined = target ? dockerfile.stagesByLabel[target] : dockerfile.stages[dockerfile.stages.length - 1]; const seen = new Set(); while (stage) { @@ -109,7 +109,7 @@ export function findBaseImage(dockerfile: Dockerfile, buildArgs: Record, targetStage: string | undefined, substitute: SubstituteConfig) { const { output } = 'output' in params ? params : params.common; const omitSyntaxDirective = 'common' in params ? !!params.common.omitSyntaxDirective : false; - return internalGetImageBuildInfoFromDockerfile(imageName => inspectDockerImage(params, imageName, true), dockerfile, dockerBuildArgs, targetStage, substitute, output, omitSyntaxDirective); + return internalGetImageBuildInfoFromDockerfile(imageName => inspectDockerImage(params, imageName, true), dockerfile, dockerBuildArgs, targetStage, substitute, output, omitSyntaxDirective, params.buildPlatformInfo, params.targetPlatformInfo); } -export async function internalGetImageBuildInfoFromDockerfile(inspectDockerImage: (imageName: string) => Promise, dockerfileText: string, dockerBuildArgs: Record, targetStage: string | undefined, substitute: SubstituteConfig, output: Log, omitSyntaxDirective: boolean): Promise { +export async function internalGetImageBuildInfoFromDockerfile(inspectDockerImage: (imageName: string) => Promise, dockerfileText: string, dockerBuildArgs: Record, targetStage: string | undefined, substitute: SubstituteConfig, output: Log, omitSyntaxDirective: boolean, buildPlatform: PlatformInfo, targetPlatform: PlatformInfo): Promise { const dockerfile = extractDockerfile(dockerfileText); if (dockerfile.preamble.directives.syntax && omitSyntaxDirective) { output.write(`Omitting syntax directive '${dockerfile.preamble.directives.syntax}' from Dockerfile.`, LogLevel.Trace); delete dockerfile.preamble.directives.syntax; } - const baseImage = findBaseImage(dockerfile, dockerBuildArgs, targetStage); + // https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#automatic-platform-args-in-the-global-scope + const globalBuildxPlatformArgs = { + // platform of the node performing the build. + BUILDPLATFORM: [buildPlatform.os, buildPlatform.arch, buildPlatform.variant].filter(Boolean).join("/"), + // OS component of BUILDPLATFORM + BUILDOS: buildPlatform.os, + // architecture component of BUILDPLATFORM + BUILDARCH: buildPlatform.arch, + // variant component of BUILDPLATFORM + BUILDVARIANT: buildPlatform.variant ?? "", + // platform of the build result. Eg linux/amd64, linux/arm/v7, windows/amd64. + TARGETPLATFORM: [targetPlatform.os, targetPlatform.arch, targetPlatform.variant].filter(Boolean).join("/"), + // OS component of TARGETPLATFORM + TARGETOS: targetPlatform.os, + // architecture component of TARGETPLATFORM + TARGETARCH: targetPlatform.arch, + // variant component of TARGETPLATFORM + TARGETVARIANT: targetPlatform.variant ?? "", + }; + const baseImage = findBaseImage(dockerfile, dockerBuildArgs, targetStage, globalBuildxPlatformArgs); const imageDetails = baseImage && await inspectDockerImage(baseImage) || undefined; - const dockerfileUser = findUserStatement(dockerfile, dockerBuildArgs, envListToObj(imageDetails?.Config.Env), targetStage); + const dockerfileUser = findUserStatement(dockerfile, dockerBuildArgs, { + ...envListToObj(imageDetails?.Config.Env), + ...globalBuildxPlatformArgs, + }, targetStage); const user = dockerfileUser || imageDetails?.Config.User || 'root'; const metadata = imageDetails ? getImageMetadata(imageDetails, substitute, output) : { config: [], raw: [], substitute }; return { diff --git a/src/spec-node/upgradeCommand.ts b/src/spec-node/upgradeCommand.ts index dfdf8e7e7..51410de07 100644 --- a/src/spec-node/upgradeCommand.ts +++ b/src/spec-node/upgradeCommand.ts @@ -86,16 +86,18 @@ async function featuresUpgrade({ env: cliHost.env, output, }, dockerPath, dockerComposePath); + const buildPlatformInfo = { + os: mapNodeOSToGOOS(cliHost.platform), + arch: mapNodeArchitectureToGOARCH(cliHost.arch), + }; const dockerParams: DockerCLIParameters = { cliHost, dockerCLI: dockerPath, dockerComposeCLI, env: cliHost.env, output, - platformInfo: { - os: mapNodeOSToGOOS(cliHost.platform), - arch: mapNodeArchitectureToGOARCH(cliHost.arch), - } + buildPlatformInfo, + targetPlatformInfo: buildPlatformInfo, }; const workspace = workspaceFromPath(cliHost.path, workspaceFolder); diff --git a/src/spec-node/utils.ts b/src/spec-node/utils.ts index bdf5289f0..74817d1a0 100644 --- a/src/spec-node/utils.ts +++ b/src/spec-node/utils.ts @@ -134,7 +134,8 @@ export interface DockerResolverParameters { additionalLabels: string[]; buildxOutput: string | undefined; buildxCacheTo: string | undefined; - platformInfo: PlatformInfo; + buildPlatformInfo: PlatformInfo; + targetPlatformInfo: PlatformInfo; } export interface ResolverResult { @@ -250,7 +251,7 @@ export async function inspectDockerImage(params: DockerResolverParameters | Dock throw inspectErr; } try { - return await inspectImageInRegistry(output, params.platformInfo, imageName); + return await inspectImageInRegistry(output, params.targetPlatformInfo, imageName); } catch (inspectErr2) { output.write(`Error fetching image details: ${inspectErr2?.message}`, LogLevel.Info); } diff --git a/src/spec-shutdown/dockerUtils.ts b/src/spec-shutdown/dockerUtils.ts index 3aa4d8166..9f0bce850 100644 --- a/src/spec-shutdown/dockerUtils.ts +++ b/src/spec-shutdown/dockerUtils.ts @@ -52,7 +52,8 @@ export interface DockerCLIParameters { dockerComposeCLI: () => Promise; env: NodeJS.ProcessEnv; output: Log; - platformInfo: PlatformInfo; + buildPlatformInfo: PlatformInfo; + targetPlatformInfo: PlatformInfo; } export interface PartialExecParameters { diff --git a/src/test/dockerfileUtils.test.ts b/src/test/dockerfileUtils.test.ts index 979375b91..4fe292168 100644 --- a/src/test/dockerfileUtils.test.ts +++ b/src/test/dockerfileUtils.test.ts @@ -178,7 +178,7 @@ FROM ubuntu:latest as dev const info = await internalGetImageBuildInfoFromDockerfile(async (imageName) => { assert.strictEqual(imageName, 'ubuntu:latest'); return details; - }, dockerfile, {}, undefined, testSubstitute, nullLog, false); + }, dockerfile, {}, undefined, testSubstitute, nullLog, false, {} as any, {} as any); assert.strictEqual(info.user, 'imageUser'); assert.strictEqual(info.metadata.config.length, 1); assert.strictEqual(info.metadata.config[0].id, 'testid-substituted'); @@ -206,11 +206,44 @@ USER dockerfileUserB const info = await internalGetImageBuildInfoFromDockerfile(async (imageName) => { assert.strictEqual(imageName, 'ubuntu:latest'); return details; - }, dockerfile, {}, undefined, testSubstitute, nullLog, false); + }, dockerfile, {}, undefined, testSubstitute, nullLog, false, {} as any, {} as any); assert.strictEqual(info.user, 'dockerfileUserB'); assert.strictEqual(info.metadata.config.length, 0); assert.strictEqual(info.metadata.raw.length, 0); }); + + it('for a USER in a multiarch image', async () => { + const dockerfile = ` +FROM ubuntu:latest as base-amd64 +USER amd64_user + +FROM ubuntu:latest as base-arm64 +USER arm64_user + +FROM base-\${TARGETARCH} + +ARG TARGETARCH +`; + const details: ImageDetails = { + Id: '123', + Config: { + User: 'imageUser', + Env: null, + Labels: null, + Entrypoint: null, + Cmd: null + }, + Os: 'linux', + Architecture: 'amd64' + }; + const info = await internalGetImageBuildInfoFromDockerfile(async (imageName) => { + assert.strictEqual(imageName, 'ubuntu:latest'); + return details; + }, dockerfile, {}, undefined, testSubstitute, nullLog, false, {} as any, { os: 'linux', arch: 'amd64' }); + assert.strictEqual(info.user, 'amd64_user'); + assert.strictEqual(info.metadata.config.length, 0); + assert.strictEqual(info.metadata.raw.length, 0); + }); }); describe('findBaseImage', () => { diff --git a/src/test/testUtils.ts b/src/test/testUtils.ts index cd48c5359..22597dce2 100644 --- a/src/test/testUtils.ts +++ b/src/test/testUtils.ts @@ -154,16 +154,18 @@ export async function createCLIParams(hostPath: string) { env: cliHost.env, output, }, 'docker', 'docker-compose'); + const buildPlatformInfo = { + os: mapNodeOSToGOOS(cliHost.platform), + arch: mapNodeArchitectureToGOARCH(cliHost.arch), + }; const cliParams: DockerCLIParameters = { cliHost, dockerCLI: 'docker', dockerComposeCLI, env: {}, output, - platformInfo: { - os: mapNodeOSToGOOS(cliHost.platform), - arch: mapNodeArchitectureToGOARCH(cliHost.arch), - } + buildPlatformInfo, + targetPlatformInfo: buildPlatformInfo, }; return cliParams; }