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
34 changes: 8 additions & 26 deletions src/spec-node/devContainers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import * as crypto from 'crypto';
import * as os from 'os';

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

const targetPlatformInfo = (() => {
if (common.buildxPlatform) {
const slash1 = common.buildxPlatform.indexOf('/');
const slash2 = common.buildxPlatform.indexOf('/', slash1 + 1);
// `--platform linux/amd64/v3` `--platform linux/arm64/v8`
if (slash2 !== -1) {
return {
os: <GoOS> common.buildxPlatform.slice(0, slash1),
arch: <GoARCH> common.buildxPlatform.slice(slash1 + 1, slash2),
variant: common.buildxPlatform.slice(slash2 + 1),
};
}
// `--platform linux/amd64` and `--platform linux/arm64`
return {
os: <GoOS> common.buildxPlatform.slice(0, slash1),
arch: <GoARCH> common.buildxPlatform.slice(slash1 + 1),
};
} else {
const targetPlatformInfo = common.buildxPlatform ?
platformInfoFromBuildxPlatform(common.buildxPlatform) :
{
// `--platform` omitted
return {
os: mapNodeOSToGOOS(cliHost.platform),
arch: mapNodeArchitectureToGOARCH(cliHost.arch),
};
}
})();
os: mapNodeOSToGOOS(cliHost.platform),
arch: mapNodeArchitectureToGOARCH(cliHost.arch),
};

const buildKitVersion = options.useBuildKit === 'never' ? undefined : (await dockerBuildKitVersion({
cliHost,
Expand Down
4 changes: 3 additions & 1 deletion src/spec-node/devContainersSpecCLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ function provisionOptions(y: Argv) {
'cache-from': { type: 'string', description: 'Additional image to use as potential layer cache during image building' },
'cache-to': { type: 'string', description: 'Additional image to use as potential layer cache during image building' },
'buildkit': { choices: ['auto' as 'auto', 'never' as 'never'], default: 'auto' as 'auto', description: 'Control whether BuildKit should be used' },
'platform': { type: 'string', description: 'Set target platform (e.g. linux/amd64). Used to resolve, pull and build the base image.' },
'additional-features': { type: 'string', description: 'Additional features to apply to the dev container (JSON as per "features" section in devcontainer.json)' },
'skip-feature-auto-mapping': { type: 'boolean', default: false, hidden: true, description: 'Temporary option for testing.' },
'skip-post-attach': { type: 'boolean', default: false, description: 'Do not run postAttachCommand.' },
Expand Down Expand Up @@ -213,6 +214,7 @@ async function provision({
'cache-from': addCacheFrom,
'cache-to': addCacheTo,
'buildkit': buildkit,
'platform': buildxPlatform,
'additional-features': additionalFeaturesJson,
'skip-feature-auto-mapping': skipFeatureAutoMapping,
'skip-post-attach': skipPostAttach,
Expand Down Expand Up @@ -287,7 +289,7 @@ async function provision({
secretsP,
additionalCacheFroms: addCacheFroms,
useBuildKit: buildkit,
buildxPlatform: undefined,
buildxPlatform,
buildxPush: false,
additionalLabels: [],
buildxOutput: undefined,
Expand Down
49 changes: 47 additions & 2 deletions src/spec-node/singleContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/


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

// Image-based dev containers have no `build.options` to carry a platform, so when the --platform flag is
// not set, fall back to the platform from runArgs (e.g. ["--platform=linux/amd64"]) to resolve the image
// for the same platform it will run on. Only the resolve platform is affected here; runArgs already pins
// `docker run`. Dockerfile builds are left untouched (they specify platform via build.options or --platform).
if (!params.buildxPlatform && !isDockerFileConfig(config)) {
const runArgsPlatform = findPlatformArg(config.runArgs);
if (runArgsPlatform) {
params.targetPlatformInfo = platformInfoFromBuildxPlatform(runArgsPlatform);
}
}

let container: ContainerDetails | undefined;
let containerProperties: ContainerProperties | undefined;

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

export function findPlatformArg(runArgs: string[] = []) {
for (let i = runArgs.length - 1; i >= 0; i--) {
const runArg = runArgs[i];
if (runArg === '--platform' && i + 1 < runArgs.length) {
return runArgs[i + 1];
}
if (runArg.startsWith('--platform=')) {
return runArg.slice(runArg.indexOf('=') + 1);
}
}
return undefined;
}

export function removePlatformArg(runArgs: string[] = []) {
const result: string[] = [];
for (let i = 0; i < runArgs.length; i++) {
const runArg = runArgs[i];
if (runArg === '--platform') {
i++; // Skip the following value as well.
continue;
}
if (runArg.startsWith('--platform=')) {
continue;
}
result.push(runArg);
}
return result;
}

export async function findExistingContainer(params: DockerResolverParameters, labels: string[]) {
const { common } = params;
let container = await findDevContainer(params, labels);
Expand Down Expand Up @@ -359,6 +399,11 @@ export async function spawnDevContainer(params: DockerResolverParameters, config

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

// The --platform flag (params.buildxPlatform) takes precedence over any --platform in runArgs.
const runArgs = params.buildxPlatform ?
['--platform', params.buildxPlatform, ...removePlatformArg(config.runArgs)] :
(config.runArgs || []);

const featureArgs: string[] = [];
if (mergedConfig.init) {
featureArgs.push('--init');
Expand Down Expand Up @@ -407,7 +452,7 @@ while sleep 1 & wait $!; do :; done`, '-']; // `wait $!` allows for the `trap` t
...containerEnv,
...containerUserArgs,
...await getPodmanArgs(params, config, mergedConfig, imageDetails),
...(config.runArgs || []),
...runArgs,
...(await extraRunArgs(common, params, config) || []),
...featureArgs,
...entrypoint,
Expand Down
22 changes: 20 additions & 2 deletions src/spec-node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as crypto from 'crypto';
import * as os from 'os';

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

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

// Parses a buildx/docker platform string (e.g. `linux/amd64` or `linux/arm64/v8`) into PlatformInfo.
export function platformInfoFromBuildxPlatform(buildxPlatform: string): PlatformInfo {
const slash1 = buildxPlatform.indexOf('/');
const slash2 = buildxPlatform.indexOf('/', slash1 + 1);
if (slash2 !== -1) {
return {
os: <GoOS>buildxPlatform.slice(0, slash1),
arch: <GoARCH>buildxPlatform.slice(slash1 + 1, slash2),
variant: buildxPlatform.slice(slash2 + 1),
};
}
return {
os: <GoOS>buildxPlatform.slice(0, slash1),
arch: <GoARCH>buildxPlatform.slice(slash1 + 1),
};
}

export async function inspectDockerImage(params: DockerResolverParameters | DockerCLIParameters, imageName: string, pullImageOnError: boolean) {
try {
return await inspectImage(params, imageName);
Expand All @@ -256,7 +273,8 @@ export async function inspectDockerImage(params: DockerResolverParameters | Dock
output.write(`Error fetching image details: ${inspectErr2?.message}`, LogLevel.Info);
}
try {
await retry(async () => dockerPtyCLI(params, 'pull', imageName), { maxRetries: 5, retryIntervalMilliseconds: 1000, output });
const platformArgs = 'buildxPlatform' in params && params.buildxPlatform ? ['--platform', params.buildxPlatform] : [];
await retry(async () => dockerPtyCLI(params, 'pull', ...platformArgs, imageName), { maxRetries: 5, retryIntervalMilliseconds: 1000, output });
} catch (pullErr) {
logErrorStdoutStderr(inspectErr, output);
logErrorStdoutStderr(pullErr, output);
Expand Down
62 changes: 61 additions & 1 deletion src/test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

import * as assert from 'assert';

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

describe('Utils', function () {
describe('isBuildxCacheToInline', function () {
Expand All @@ -26,4 +27,63 @@ describe('Utils', function () {
assert.strictEqual(isBuildxCacheToInline('inline'), false);
});
});

describe('platformInfoFromBuildxPlatform', function () {
it('parses os/arch without a variant', () => {
assert.deepStrictEqual(platformInfoFromBuildxPlatform('linux/amd64'), { os: 'linux', arch: 'amd64' });
assert.deepStrictEqual(platformInfoFromBuildxPlatform('windows/amd64'), { os: 'windows', arch: 'amd64' });
});

it('parses os/arch/variant', () => {
assert.deepStrictEqual(platformInfoFromBuildxPlatform('linux/arm64/v8'), { os: 'linux', arch: 'arm64', variant: 'v8' });
assert.deepStrictEqual(platformInfoFromBuildxPlatform('linux/amd64/v3'), { os: 'linux', arch: 'amd64', variant: 'v3' });
});
});

describe('findPlatformArg', function () {
it('returns undefined when runArgs is missing or has no --platform', () => {
assert.strictEqual(findPlatformArg(), undefined);
assert.strictEqual(findPlatformArg([]), undefined);
assert.strictEqual(findPlatformArg(['--user=foo', '--rm']), undefined);
});

it('parses the --platform=value form', () => {
assert.strictEqual(findPlatformArg(['--platform=linux/amd64']), 'linux/amd64');
});

it('parses the separate --platform value form', () => {
assert.strictEqual(findPlatformArg(['--rm', '--platform', 'linux/arm64/v8', '-it']), 'linux/arm64/v8');
});

it('returns the last occurrence when --platform is repeated', () => {
assert.strictEqual(findPlatformArg(['--platform=linux/amd64', '--platform', 'linux/arm64']), 'linux/arm64');
});

it('ignores a trailing --platform with no value', () => {
assert.strictEqual(findPlatformArg(['--foo', '--platform']), undefined);
});
});

describe('removePlatformArg', function () {
it('returns an empty array for missing or empty runArgs', () => {
assert.deepStrictEqual(removePlatformArg(), []);
assert.deepStrictEqual(removePlatformArg([]), []);
});

it('leaves runArgs without --platform untouched', () => {
assert.deepStrictEqual(removePlatformArg(['--rm', '--user=foo']), ['--rm', '--user=foo']);
});

it('removes the --platform=value form', () => {
assert.deepStrictEqual(removePlatformArg(['--rm', '--platform=linux/amd64', '-it']), ['--rm', '-it']);
});

it('removes the separate --platform value form including its value', () => {
assert.deepStrictEqual(removePlatformArg(['--rm', '--platform', 'linux/arm64/v8', '-it']), ['--rm', '-it']);
});

it('removes all occurrences', () => {
assert.deepStrictEqual(removePlatformArg(['--platform=linux/amd64', '--rm', '--platform', 'linux/arm64']), ['--rm']);
});
});
});