Skip to content

Commit e0b0406

Browse files
committed
Add --platform argument to devcontainer up
1 parent 65f98a5 commit e0b0406

5 files changed

Lines changed: 139 additions & 32 deletions

File tree

src/spec-node/devContainers.ts

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import * as crypto from 'crypto';
88
import * as os from 'os';
99

1010
import { mapNodeOSToGOOS, mapNodeArchitectureToGOARCH } from '../spec-configuration/containerCollectionsOCI';
11-
import { DockerResolverParameters, DevContainerAuthority, UpdateRemoteUserUIDDefault, BindMountConsistency, getCacheFolder, GPUAvailability } from './utils';
11+
import { DockerResolverParameters, DevContainerAuthority, UpdateRemoteUserUIDDefault, BindMountConsistency, getCacheFolder, GPUAvailability, platformInfoFromBuildxPlatform } from './utils';
1212
import { createNullLifecycleHook, finishBackgroundTasks, ResolverParameters, UserEnvProbe } from '../spec-common/injectHeadless';
13-
import { GoARCH, GoOS, getCLIHost, loadNativeModule } from '../spec-common/commonUtils';
13+
import { getCLIHost, loadNativeModule } from '../spec-common/commonUtils';
1414
import { resolve } from './configContainer';
1515
import { URI } from 'vscode-uri';
1616
import { LogLevel, LogDimensions, toErrorText, createCombinedLog, createTerminalLog, Log, makeLog, LogFormat, createJSONLog, createPlainLog, LogHandler, replaceAllLog } from '../spec-utils/log';
@@ -177,31 +177,13 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
177177
arch: mapNodeArchitectureToGOARCH(cliHost.arch),
178178
};
179179

180-
const targetPlatformInfo = (() => {
181-
if (common.buildxPlatform) {
182-
const slash1 = common.buildxPlatform.indexOf('/');
183-
const slash2 = common.buildxPlatform.indexOf('/', slash1 + 1);
184-
// `--platform linux/amd64/v3` `--platform linux/arm64/v8`
185-
if (slash2 !== -1) {
186-
return {
187-
os: <GoOS> common.buildxPlatform.slice(0, slash1),
188-
arch: <GoARCH> common.buildxPlatform.slice(slash1 + 1, slash2),
189-
variant: common.buildxPlatform.slice(slash2 + 1),
190-
};
191-
}
192-
// `--platform linux/amd64` and `--platform linux/arm64`
193-
return {
194-
os: <GoOS> common.buildxPlatform.slice(0, slash1),
195-
arch: <GoARCH> common.buildxPlatform.slice(slash1 + 1),
196-
};
197-
} else {
180+
const targetPlatformInfo = common.buildxPlatform ?
181+
platformInfoFromBuildxPlatform(common.buildxPlatform) :
182+
{
198183
// `--platform` omitted
199-
return {
200-
os: mapNodeOSToGOOS(cliHost.platform),
201-
arch: mapNodeArchitectureToGOARCH(cliHost.arch),
202-
};
203-
}
204-
})();
184+
os: mapNodeOSToGOOS(cliHost.platform),
185+
arch: mapNodeArchitectureToGOARCH(cliHost.arch),
186+
};
205187

206188
const buildKitVersion = options.useBuildKit === 'never' ? undefined : (await dockerBuildKitVersion({
207189
cliHost,

src/spec-node/devContainersSpecCLI.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ function provisionOptions(y: Argv) {
129129
'cache-from': { type: 'string', description: 'Additional image to use as potential layer cache during image building' },
130130
'cache-to': { type: 'string', description: 'Additional image to use as potential layer cache during image building' },
131131
'buildkit': { choices: ['auto' as 'auto', 'never' as 'never'], default: 'auto' as 'auto', description: 'Control whether BuildKit should be used' },
132+
'platform': { type: 'string', description: 'Set target platform (e.g. linux/amd64). Used to resolve, pull and build the base image.' },
132133
'additional-features': { type: 'string', description: 'Additional features to apply to the dev container (JSON as per "features" section in devcontainer.json)' },
133134
'skip-feature-auto-mapping': { type: 'boolean', default: false, hidden: true, description: 'Temporary option for testing.' },
134135
'skip-post-attach': { type: 'boolean', default: false, description: 'Do not run postAttachCommand.' },
@@ -213,6 +214,7 @@ async function provision({
213214
'cache-from': addCacheFrom,
214215
'cache-to': addCacheTo,
215216
'buildkit': buildkit,
217+
'platform': buildxPlatform,
216218
'additional-features': additionalFeaturesJson,
217219
'skip-feature-auto-mapping': skipFeatureAutoMapping,
218220
'skip-post-attach': skipPostAttach,
@@ -287,7 +289,7 @@ async function provision({
287289
secretsP,
288290
additionalCacheFroms: addCacheFroms,
289291
useBuildKit: buildkit,
290-
buildxPlatform: undefined,
292+
buildxPlatform,
291293
buildxPush: false,
292294
additionalLabels: [],
293295
buildxOutput: undefined,

src/spec-node/singleContainer.ts

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

66

7-
import { createContainerProperties, startEventSeen, ResolverResult, getTunnelInformation, getDockerfilePath, getDockerContextPath, DockerResolverParameters, isDockerFileConfig, uriToWSLFsPath, WorkspaceConfiguration, getFolderImageName, inspectDockerImage, logUMask, SubstitutedConfig, checkDockerSupportForGPU, isBuildKitImagePolicyError, isBuildxCacheToInline } from './utils';
7+
import { createContainerProperties, startEventSeen, ResolverResult, getTunnelInformation, getDockerfilePath, getDockerContextPath, DockerResolverParameters, isDockerFileConfig, uriToWSLFsPath, WorkspaceConfiguration, getFolderImageName, inspectDockerImage, logUMask, SubstitutedConfig, checkDockerSupportForGPU, isBuildKitImagePolicyError, isBuildxCacheToInline, platformInfoFromBuildxPlatform } from './utils';
88
import { ContainerProperties, setupInContainer, ResolverProgress, ResolverParameters } from '../spec-common/injectHeadless';
99
import { ContainerError, toErrorText } from '../spec-common/errors';
1010
import { ContainerDetails, listContainers, DockerCLIParameters, inspectContainers, dockerCLI, dockerPtyCLI, toPtyExecParameters, ImageDetails, toExecParameters, removeContainer } from '../spec-shutdown/dockerUtils';
@@ -21,6 +21,17 @@ export async function openDockerfileDevContainer(params: DockerResolverParameter
2121
const { common } = params;
2222
const { config } = configWithRaw;
2323

24+
// Image-based dev containers have no `build.options` to carry a platform, so when the --platform flag is
25+
// not set, fall back to the platform from runArgs (e.g. ["--platform=linux/amd64"]) to resolve the image
26+
// for the same platform it will run on. Only the resolve platform is affected here; runArgs already pins
27+
// `docker run`. Dockerfile builds are left untouched (they specify platform via build.options or --platform).
28+
if (!params.buildxPlatform && !isDockerFileConfig(config)) {
29+
const runArgsPlatform = findPlatformArg(config.runArgs);
30+
if (runArgsPlatform) {
31+
params.targetPlatformInfo = platformInfoFromBuildxPlatform(runArgsPlatform);
32+
}
33+
}
34+
2435
let container: ContainerDetails | undefined;
2536
let containerProperties: ContainerProperties | undefined;
2637

@@ -285,6 +296,35 @@ export function findUserArg(runArgs: string[] = []) {
285296
return undefined;
286297
}
287298

299+
export function findPlatformArg(runArgs: string[] = []) {
300+
for (let i = runArgs.length - 1; i >= 0; i--) {
301+
const runArg = runArgs[i];
302+
if (runArg === '--platform' && i + 1 < runArgs.length) {
303+
return runArgs[i + 1];
304+
}
305+
if (runArg.startsWith('--platform=')) {
306+
return runArg.slice(runArg.indexOf('=') + 1);
307+
}
308+
}
309+
return undefined;
310+
}
311+
312+
export function removePlatformArg(runArgs: string[] = []) {
313+
const result: string[] = [];
314+
for (let i = 0; i < runArgs.length; i++) {
315+
const runArg = runArgs[i];
316+
if (runArg === '--platform') {
317+
i++; // Skip the following value as well.
318+
continue;
319+
}
320+
if (runArg.startsWith('--platform=')) {
321+
continue;
322+
}
323+
result.push(runArg);
324+
}
325+
return result;
326+
}
327+
288328
export async function findExistingContainer(params: DockerResolverParameters, labels: string[]) {
289329
const { common } = params;
290330
let container = await findDevContainer(params, labels);
@@ -359,6 +399,11 @@ export async function spawnDevContainer(params: DockerResolverParameters, config
359399

360400
const containerUserArgs = containerUser ? ['-u', containerUser] : [];
361401

402+
// The --platform flag (params.buildxPlatform) takes precedence over any --platform in runArgs.
403+
const runArgs = params.buildxPlatform ?
404+
['--platform', params.buildxPlatform, ...removePlatformArg(config.runArgs)] :
405+
(config.runArgs || []);
406+
362407
const featureArgs: string[] = [];
363408
if (mergedConfig.init) {
364409
featureArgs.push('--init');
@@ -407,7 +452,7 @@ while sleep 1 & wait $!; do :; done`, '-']; // `wait $!` allows for the `trap` t
407452
...containerEnv,
408453
...containerUserArgs,
409454
...await getPodmanArgs(params, config, mergedConfig, imageDetails),
410-
...(config.runArgs || []),
455+
...runArgs,
411456
...(await extraRunArgs(common, params, config) || []),
412457
...featureArgs,
413458
...entrypoint,

src/spec-node/utils.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as crypto from 'crypto';
88
import * as os from 'os';
99

1010
import { ContainerError, toErrorText } from '../spec-common/errors';
11-
import { CLIHost, runCommandNoPty, runCommand, getLocalUsername, PlatformInfo } from '../spec-common/commonUtils';
11+
import { CLIHost, runCommandNoPty, runCommand, getLocalUsername, PlatformInfo, GoOS, GoARCH } from '../spec-common/commonUtils';
1212
import { Log, LogLevel, makeLog, nullLog } from '../spec-utils/log';
1313

1414
import { CommonDevContainerConfig, ContainerProperties, getContainerProperties, LifecycleCommand, ResolverParameters } from '../spec-common/injectHeadless';
@@ -241,6 +241,23 @@ export function isBuildKitImagePolicyError(err: any): boolean {
241241
|| (errStderr && typeof errStderr === 'string' && (errStderr.includes(imagePolicyErrorString) || errStderr.includes(sourceDeniedString)));
242242
}
243243

244+
// Parses a buildx/docker platform string (e.g. `linux/amd64` or `linux/arm64/v8`) into PlatformInfo.
245+
export function platformInfoFromBuildxPlatform(buildxPlatform: string): PlatformInfo {
246+
const slash1 = buildxPlatform.indexOf('/');
247+
const slash2 = buildxPlatform.indexOf('/', slash1 + 1);
248+
if (slash2 !== -1) {
249+
return {
250+
os: <GoOS>buildxPlatform.slice(0, slash1),
251+
arch: <GoARCH>buildxPlatform.slice(slash1 + 1, slash2),
252+
variant: buildxPlatform.slice(slash2 + 1),
253+
};
254+
}
255+
return {
256+
os: <GoOS>buildxPlatform.slice(0, slash1),
257+
arch: <GoARCH>buildxPlatform.slice(slash1 + 1),
258+
};
259+
}
260+
244261
export async function inspectDockerImage(params: DockerResolverParameters | DockerCLIParameters, imageName: string, pullImageOnError: boolean) {
245262
try {
246263
return await inspectImage(params, imageName);
@@ -256,7 +273,8 @@ export async function inspectDockerImage(params: DockerResolverParameters | Dock
256273
output.write(`Error fetching image details: ${inspectErr2?.message}`, LogLevel.Info);
257274
}
258275
try {
259-
await retry(async () => dockerPtyCLI(params, 'pull', imageName), { maxRetries: 5, retryIntervalMilliseconds: 1000, output });
276+
const platformArgs = 'buildxPlatform' in params && params.buildxPlatform ? ['--platform', params.buildxPlatform] : [];
277+
await retry(async () => dockerPtyCLI(params, 'pull', ...platformArgs, imageName), { maxRetries: 5, retryIntervalMilliseconds: 1000, output });
260278
} catch (pullErr) {
261279
logErrorStdoutStderr(inspectErr, output);
262280
logErrorStdoutStderr(pullErr, output);

src/test/utils.test.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
import * as assert from 'assert';
66

7-
import { isBuildxCacheToInline } from '../spec-node/utils';
7+
import { isBuildxCacheToInline, platformInfoFromBuildxPlatform } from '../spec-node/utils';
8+
import { findPlatformArg, removePlatformArg } from '../spec-node/singleContainer';
89

910
describe('Utils', function () {
1011
describe('isBuildxCacheToInline', function () {
@@ -26,4 +27,63 @@ describe('Utils', function () {
2627
assert.strictEqual(isBuildxCacheToInline('inline'), false);
2728
});
2829
});
30+
31+
describe('platformInfoFromBuildxPlatform', function () {
32+
it('parses os/arch without a variant', () => {
33+
assert.deepStrictEqual(platformInfoFromBuildxPlatform('linux/amd64'), { os: 'linux', arch: 'amd64' });
34+
assert.deepStrictEqual(platformInfoFromBuildxPlatform('windows/amd64'), { os: 'windows', arch: 'amd64' });
35+
});
36+
37+
it('parses os/arch/variant', () => {
38+
assert.deepStrictEqual(platformInfoFromBuildxPlatform('linux/arm64/v8'), { os: 'linux', arch: 'arm64', variant: 'v8' });
39+
assert.deepStrictEqual(platformInfoFromBuildxPlatform('linux/amd64/v3'), { os: 'linux', arch: 'amd64', variant: 'v3' });
40+
});
41+
});
42+
43+
describe('findPlatformArg', function () {
44+
it('returns undefined when runArgs is missing or has no --platform', () => {
45+
assert.strictEqual(findPlatformArg(), undefined);
46+
assert.strictEqual(findPlatformArg([]), undefined);
47+
assert.strictEqual(findPlatformArg(['--user=foo', '--rm']), undefined);
48+
});
49+
50+
it('parses the --platform=value form', () => {
51+
assert.strictEqual(findPlatformArg(['--platform=linux/amd64']), 'linux/amd64');
52+
});
53+
54+
it('parses the separate --platform value form', () => {
55+
assert.strictEqual(findPlatformArg(['--rm', '--platform', 'linux/arm64/v8', '-it']), 'linux/arm64/v8');
56+
});
57+
58+
it('returns the last occurrence when --platform is repeated', () => {
59+
assert.strictEqual(findPlatformArg(['--platform=linux/amd64', '--platform', 'linux/arm64']), 'linux/arm64');
60+
});
61+
62+
it('ignores a trailing --platform with no value', () => {
63+
assert.strictEqual(findPlatformArg(['--foo', '--platform']), undefined);
64+
});
65+
});
66+
67+
describe('removePlatformArg', function () {
68+
it('returns an empty array for missing or empty runArgs', () => {
69+
assert.deepStrictEqual(removePlatformArg(), []);
70+
assert.deepStrictEqual(removePlatformArg([]), []);
71+
});
72+
73+
it('leaves runArgs without --platform untouched', () => {
74+
assert.deepStrictEqual(removePlatformArg(['--rm', '--user=foo']), ['--rm', '--user=foo']);
75+
});
76+
77+
it('removes the --platform=value form', () => {
78+
assert.deepStrictEqual(removePlatformArg(['--rm', '--platform=linux/amd64', '-it']), ['--rm', '-it']);
79+
});
80+
81+
it('removes the separate --platform value form including its value', () => {
82+
assert.deepStrictEqual(removePlatformArg(['--rm', '--platform', 'linux/arm64/v8', '-it']), ['--rm', '-it']);
83+
});
84+
85+
it('removes all occurrences', () => {
86+
assert.deepStrictEqual(removePlatformArg(['--platform=linux/amd64', '--rm', '--platform', 'linux/arm64']), ['--rm']);
87+
});
88+
});
2989
});

0 commit comments

Comments
 (0)