Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/test-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ permissions:
jobs:
tests-matrix:
name: Tests Matrix (Windows)
runs-on: windows-latest
runs-on: windows-2022
timeout-minutes: 15
strategy:
fail-fast: false
Expand Down
6 changes: 3 additions & 3 deletions src/spec-node/containerFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import * as path from 'path';

import { DevContainerConfig } from '../spec-configuration/configuration';
import { dockerCLI, dockerPtyCLI, ImageDetails, toExecParameters, toPtyExecParameters } from '../spec-shutdown/dockerUtils';
import { dockerCLI, dockerPtyCLI, ImageDetails, toExecParameters, toPtyExecParameters, CLIVariant } from '../spec-shutdown/dockerUtils';
import { LogLevel, makeLog } from '../spec-utils/log';
import { FeaturesConfig, getContainerFeaturesBaseDockerFile, getFeatureInstallWrapperScript, getFeatureLayers, getFeatureMainValue, getFeatureValueObject, generateFeaturesConfig, Feature, generateContainerEnvs } from '../spec-configuration/containerFeaturesConfiguration';
import { readLocalFile } from '../spec-utils/pfs';
Expand Down Expand Up @@ -364,7 +364,7 @@ async function isUsingSELinuxLabels(params: DockerResolverParameters): Promise<b
try {
const { common } = params;
const { cliHost, output } = common;
return params.isPodman && cliHost.platform === 'linux'
return params.cliVariant === CLIVariant.Podman && cliHost.platform === 'linux'
&& (await runCommandNoPty({
exec: cliHost.exec,
cmd: 'getenforce',
Expand Down Expand Up @@ -467,7 +467,7 @@ export async function updateRemoteUserUID(params: DockerResolverParameters, merg
'-f', destDockerfile,
'-t', fixedImageName,
...(platform ? ['--platform', platform] : []),
'--build-arg', `BASE_IMAGE=${params.isPodman && !hasRegistryHostname(imageName) ? 'localhost/' : ''}${imageName}`, // Podman: https://github.com/microsoft/vscode-remote-release/issues/9748
'--build-arg', `BASE_IMAGE=${params.cliVariant === CLIVariant.Podman && !hasRegistryHostname(imageName) ? 'localhost/' : ''}${imageName}`, // Podman: https://github.com/microsoft/vscode-remote-release/issues/9748
'--build-arg', `REMOTE_USER=${remoteUser}`,
'--build-arg', `NEW_UID=${await cliHost.getuid!()}`,
'--build-arg', `NEW_GID=${await cliHost.getgid!()}`,
Expand Down
8 changes: 5 additions & 3 deletions src/spec-node/devContainers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { LogLevel, LogDimensions, toErrorText, createCombinedLog, createTerminal
import { dockerComposeCLIConfig } from './dockerCompose';
import { Mount } from '../spec-configuration/containerFeaturesConfiguration';
import { getPackageConfig, PackageConfiguration } from '../spec-utils/product';
import { dockerBuildKitVersion, dockerEngineVersion, isPodman } from '../spec-shutdown/dockerUtils';
import { dockerBuildKitVersion, dockerEngineVersion, lookupCLIVariant, CLIVariant } from '../spec-shutdown/dockerUtils';
import { Event } from '../spec-utils/event';


Expand Down Expand Up @@ -213,6 +213,8 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
targetPlatformInfo
}));

const cliVariant = await lookupCLIVariant({ exec: cliHost.exec, cmd: dockerPath, env: cliHost.env, output });

const dockerEngineVer = await dockerEngineVersion({
cliHost,
dockerCLI: dockerPath,
Expand All @@ -221,13 +223,13 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
output,
buildPlatformInfo,
targetPlatformInfo
});
}, { useSimpleVersion: cliVariant === CLIVariant.Wslc });

return {
common,
parsedAuthority,
dockerCLI: dockerPath,
isPodman: await isPodman({ exec: cliHost.exec, cmd: dockerPath, env: cliHost.env, output }),
cliVariant,
dockerComposeCLI: dockerComposeCLI,
dockerEnv: cliHost.env,
workspaceMountConsistencyDefault: workspaceMountConsistency,
Expand Down
4 changes: 2 additions & 2 deletions src/spec-node/dockerCompose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ContainerProperties, setupInContainer, ResolverProgress } from '../spec
import { ContainerError } from '../spec-common/errors';
import { Workspace } from '../spec-utils/workspaces';
import { equalPaths, parseVersion, isEarlierVersion, CLIHost } from '../spec-common/commonUtils';
import { ContainerDetails, inspectContainer, listContainers, DockerCLIParameters, dockerComposeCLI, dockerComposePtyCLI, PartialExecParameters, DockerComposeCLI, ImageDetails, toExecParameters, toPtyExecParameters, removeContainer } from '../spec-shutdown/dockerUtils';
import { ContainerDetails, inspectContainer, listContainers, DockerCLIParameters, dockerComposeCLI, dockerComposePtyCLI, PartialExecParameters, DockerComposeCLI, ImageDetails, toExecParameters, toPtyExecParameters, removeContainer, CLIVariant } from '../spec-shutdown/dockerUtils';
import { DevContainerFromDockerComposeConfig, getDockerComposeFilePaths } from '../spec-configuration/configuration';
import { Log, LogLevel, makeLog, terminalEscapeSequences } from '../spec-utils/log';
import { getExtendImageBuildInfo, updateRemoteUserUID } from './containerFeatures';
Expand Down Expand Up @@ -188,7 +188,7 @@ export async function buildAndExtendDockerCompose(configWithRaw: SubstitutedConf

// determine whether we need to extend with features
const version = parseVersion((await params.dockerComposeCLI()).version);
const supportsAdditionalBuildContexts = !params.isPodman && version && !isEarlierVersion(version, [2, 17, 0]);
const supportsAdditionalBuildContexts = params.cliVariant !== CLIVariant.Podman && params.cliVariant !== CLIVariant.Wslc && version && !isEarlierVersion(version, [2, 17, 0]);
const optionalBuildKitParams = supportsAdditionalBuildContexts ? params : { ...params, buildKitVersion: undefined };
const extendImageBuildInfo = await getExtendImageBuildInfo(optionalBuildKitParams, configWithRaw, baseName, imageBuildInfo, composeService.user, additionalFeatures, canAddLabelsToContainer);

Expand Down
60 changes: 45 additions & 15 deletions src/spec-node/singleContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { createContainerProperties, startEventSeen, ResolverResult, getTunnelInformation, getDockerfilePath, getDockerContextPath, DockerResolverParameters, isDockerFileConfig, uriToWSLFsPath, WorkspaceConfiguration, getFolderImageName, inspectDockerImage, logUMask, SubstitutedConfig, checkDockerSupportForGPU, isBuildKitImagePolicyError, isBuildxCacheToInline } 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';
import { ContainerDetails, listContainers, DockerCLIParameters, inspectContainers, dockerCLI, dockerPtyCLI, toPtyExecParameters, ImageDetails, toExecParameters, removeContainer, CLIVariant } from '../spec-shutdown/dockerUtils';
import { DevContainerConfig, DevContainerFromDockerfileConfig, DevContainerFromImageConfig } from '../spec-configuration/configuration';
import { LogLevel, Log, makeLog } from '../spec-utils/log';
import { extendImage, getExtendImageBuildInfo, updateRemoteUserUID } from './containerFeatures';
Expand Down Expand Up @@ -347,8 +347,8 @@ export async function spawnDevContainer(params: DockerResolverParameters, config
const exposedPorts = typeof appPort === 'number' || typeof appPort === 'string' ? [appPort] : appPort || [];
const exposed = (<string[]>[]).concat(...exposedPorts.map(port => ['-p', typeof port === 'number' ? `127.0.0.1:${port}:${port}` : port]));

const cwdMount = workspaceMount ? ['--mount', workspaceMount] : [];
const additionalMount = additionalMountString ? ['--mount', additionalMountString] : [];
const cwdMount = workspaceMount ? (params.cliVariant === CLIVariant.Wslc ? convertMountToVolume(workspaceMount) : ['--mount', workspaceMount]) : [];
const additionalMount = additionalMountString ? (params.cliVariant === CLIVariant.Wslc ? convertMountToVolume(additionalMountString) : ['--mount', additionalMountString]) : [];

const envObj = mergedConfig.containerEnv || {};
const containerEnv = Object.keys(envObj)
Expand All @@ -360,24 +360,30 @@ export async function spawnDevContainer(params: DockerResolverParameters, config
const containerUserArgs = containerUser ? ['-u', containerUser] : [];

const featureArgs: string[] = [];
if (mergedConfig.init) {
// wslc does not support --init, --privileged, --cap-add, or --security-opt
if (mergedConfig.init && params.cliVariant !== CLIVariant.Wslc) {
featureArgs.push('--init');
}
if (mergedConfig.privileged) {
if (mergedConfig.privileged && params.cliVariant !== CLIVariant.Wslc) {
featureArgs.push('--privileged');
}
for (const cap of mergedConfig.capAdd || []) {
featureArgs.push('--cap-add', cap);
}
for (const securityOpt of mergedConfig.securityOpt || []) {
featureArgs.push('--security-opt', securityOpt);
if (params.cliVariant !== CLIVariant.Wslc) {
for (const cap of mergedConfig.capAdd || []) {
featureArgs.push('--cap-add', cap);
}
for (const securityOpt of mergedConfig.securityOpt || []) {
featureArgs.push('--security-opt', securityOpt);
}
}

const featureMounts = ([] as string[]).concat(
...[
...mergedConfig.mounts || [],
...params.additionalMounts,
].map(m => generateMountCommand(m))
].map(m => {
const mountArgs = generateMountCommand(m);
return params.cliVariant === CLIVariant.Wslc ? convertMountArgsToVolume(mountArgs) : mountArgs;
})
);

const customEntrypoints = mergedConfig.entrypoints || [];
Expand All @@ -396,9 +402,7 @@ while sleep 1 & wait $!; do :; done`, '-']; // `wait $!` allows for the `trap` t

const args = [
'run',
'--sig-proxy=false',
'-a', 'STDOUT',
'-a', 'STDERR',
...(params.cliVariant === CLIVariant.Wslc ? [] : ['--sig-proxy=false', '-a', 'STDOUT', '-a', 'STDERR']),
...exposed,
...cwdMount,
...additionalMount,
Expand Down Expand Up @@ -432,7 +436,7 @@ while sleep 1 & wait $!; do :; done`, '-']; // `wait $!` allows for the `trap` t
}

async function getPodmanArgs(params: DockerResolverParameters, config: DevContainerFromDockerfileConfig | DevContainerFromImageConfig, mergedConfig: MergedDevContainerConfig, imageDetails: () => Promise<ImageDetails>): Promise<string[]> {
if (params.isPodman && params.common.cliHost.platform === 'linux') {
if (params.cliVariant === CLIVariant.Podman && params.common.cliHost.platform === 'linux') {
const args = ['--security-opt', 'label=disable'];
const hasIdMapping = (config.runArgs || []).some(arg => /--[ug]idmap(=|$)/.test(arg));
if (!hasIdMapping) {
Expand All @@ -446,6 +450,32 @@ async function getPodmanArgs(params: DockerResolverParameters, config: DevContai
return [];
}

// Convert a --mount string (e.g., "type=bind,source=/a,target=/b,consistency=cached") to -v syntax for wslc.
function convertMountToVolume(mountStr: string): string[] {
const parts = new Map(mountStr.split(',').map(p => {
const eq = p.indexOf('=');
return eq === -1 ? [p, ''] : [p.substring(0, eq), p.substring(eq + 1)];
}));
const source = parts.get('source') || parts.get('src') || '';
const target = parts.get('target') || parts.get('dst') || parts.get('destination') || '';
if (source && target) {
return ['-v', `${source}:${target}`];
}
if (target) {
return ['-v', target];
}
// Fallback: pass as --mount and let the runtime handle it.
return ['--mount', mountStr];
}

// Convert --mount args array (e.g., ['--mount', 'type=bind,...']) to -v syntax for wslc.
function convertMountArgsToVolume(args: string[]): string[] {
if (args.length === 2 && args[0] === '--mount') {
return convertMountToVolume(args[1]);
}
return args;
}

function getLabels(labels: string[]): string[] {
let result: string[] = [];
labels.forEach(each => result.push('-l', each));
Expand Down
38 changes: 36 additions & 2 deletions src/spec-node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import * as os from 'os';
import { ContainerError, toErrorText } from '../spec-common/errors';
import { CLIHost, runCommandNoPty, runCommand, getLocalUsername, PlatformInfo } from '../spec-common/commonUtils';
import { Log, LogLevel, makeLog, nullLog } from '../spec-utils/log';
import { delay } from '../spec-common/async';

import { CommonDevContainerConfig, ContainerProperties, getContainerProperties, LifecycleCommand, ResolverParameters } from '../spec-common/injectHeadless';
import { Workspace } from '../spec-utils/workspaces';
import { URI } from 'vscode-uri';
import { ShellServer } from '../spec-common/shellServer';
import { inspectContainer, inspectContainers, inspectImage, getEvents, listContainers, ContainerDetails, DockerCLIParameters, dockerExecFunction, dockerPtyCLI, dockerPtyExecFunction, toDockerImageName, DockerComposeCLI, ImageDetails, dockerCLI, removeContainer } from '../spec-shutdown/dockerUtils';
import { inspectContainer, inspectContainers, inspectImage, getEvents, listContainers, ContainerDetails, DockerCLIParameters, dockerExecFunction, dockerPtyCLI, dockerPtyExecFunction, toDockerImageName, DockerComposeCLI, ImageDetails, dockerCLI, removeContainer, CLIVariant } from '../spec-shutdown/dockerUtils';
import { getRemoteWorkspaceFolder } from './dockerCompose';
import { findGitRootFolder } from '../spec-common/git';
import { parentURI, uriToFsPath } from '../spec-configuration/configurationCommonUtils';
Expand Down Expand Up @@ -108,7 +109,7 @@ export interface DockerResolverParameters {
common: ResolverParameters;
parsedAuthority: ParsedAuthority | undefined;
dockerCLI: string;
isPodman: boolean;
cliVariant: CLIVariant;
dockerComposeCLI: () => Promise<DockerComposeCLI>;
dockerEnv: NodeJS.ProcessEnv;
workspaceMountConsistencyDefault: BindMountConsistency;
Expand Down Expand Up @@ -170,6 +171,9 @@ export function addSubstitution<T extends DevContainerConfig | ImageMetadataEntr
}

export async function startEventSeen(params: DockerResolverParameters, labels: Record<string, string>, canceled: Promise<void>, output: Log, trace: boolean) {
if (params.cliVariant === CLIVariant.Wslc) {
return startEventSeenPolling(params, labels, canceled, output, trace);
}
const eventsProcess = await getEvents(params, { event: ['start'] });
return {
started: new Promise<void>((resolve, reject) => {
Expand Down Expand Up @@ -209,6 +213,36 @@ export async function startEventSeen(params: DockerResolverParameters, labels: R
};
}

// Polling-based fallback for runtimes that don't support `events` (e.g., wslc).
function startEventSeenPolling(params: DockerResolverParameters, labels: Record<string, string>, canceled: Promise<void>, output: Log, trace: boolean) {
let stopped = false;
canceled.catch(() => { stopped = true; });
const labelFilters = Object.entries(labels).map(([k, v]) => `${k}=${v}`);
return {
started: new Promise<void>((resolve, reject) => {
canceled.catch(reject);
const poll = async () => {
while (!stopped) {
try {
const containers = await listContainers(params, false, labelFilters);
if (trace) {
output.write(`Log: startEventSeenPolling found ${containers.length} container(s)\r\n`);
}
if (containers.length > 0) {
resolve();
return;
}
} catch (e) {
// Ignore transient errors during polling.
}
await delay(500);
}
};
poll();
})
};
}

async function hasLabels(params: DockerResolverParameters, info: any, expectedLabels: Record<string, string>) {
const actualLabels = info.Actor?.Attributes
// Docker uses 'id', Podman 'ID'.
Expand Down
37 changes: 27 additions & 10 deletions src/spec-shutdown/dockerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export interface PartialPtyExecParameters {

interface DockerResolverParameters {
dockerCLI: string;
isPodman: boolean;
cliVariant: CLIVariant;
dockerComposeCLI: () => Promise<DockerComposeCLI>;
dockerEnv: NodeJS.ProcessEnv;
common: {
Expand Down Expand Up @@ -171,6 +171,7 @@ export async function listContainers(params: DockerCLIParameters | PartialExecPa
}

export async function removeContainer(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, nameOrId: string) {
const useEvents = !('cliVariant' in params && params.cliVariant === CLIVariant.Wslc);
let eventsProcess: Exec | undefined;
let removedSeenP: Promise<void> | undefined;
try {
Expand All @@ -184,7 +185,7 @@ export async function removeContainer(params: DockerCLIParameters | PartialExecP
if (i === n - 1 || !stderr.includes('already in progress')) {
throw err;
}
if (!removedSeenP) {
if (useEvents && !removedSeenP) {
eventsProcess = await getEvents(params, {
container: [nameOrId],
event: ['destroy'],
Expand All @@ -197,7 +198,7 @@ export async function removeContainer(params: DockerCLIParameters | PartialExecP
});
});
}
await Promise.race([removedSeenP, delay(1000)]);
await Promise.race([removedSeenP || delay(1000), delay(1000)]);
}
}
} finally {
Expand All @@ -215,7 +216,7 @@ export async function getEvents(params: DockerCLIParameters | PartialExecParamet
filterArgs.push('--filter', `${filter}=${value}`);
}
}
const format = 'isPodman' in params && params.isPodman ? 'json' : '{{json .}}'; // https://github.com/containers/libpod/issues/5981
const format = 'cliVariant' in params && params.cliVariant === CLIVariant.Podman ? 'json' : '{{json .}}'; // https://github.com/containers/libpod/issues/5981
const combinedArgs = (args || []).concat(['events', '--format', format, ...filterArgs]);

const p = await exec({
Expand Down Expand Up @@ -260,13 +261,16 @@ export async function dockerBuildKitVersion(params: DockerCLIParameters | Partia
}
}

export async function dockerEngineVersion(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters): Promise<{ versionString: string; versionMatch?: string } | undefined> {
export async function dockerEngineVersion(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, options?: { useSimpleVersion?: boolean }): Promise<{ versionString: string; versionMatch?: string } | undefined> {
try {
const execParams = {
...toExecParameters(params),
print: true,
};
const result = await dockerCLI(execParams, 'version', '--format', '{{.Server.Version}}');
const args: string[] = options?.useSimpleVersion
? ['version']
: ['version', '--format', '{{.Server.Version}}'];
const result = await dockerCLI(execParams, ...args);
const versionString = result.stdout.toString().trim();
const versionMatch = versionString.match(/(?<major>[0-9]+)\.(?<minor>[0-9]+)\.(?<patch>[0-9]+)/);
if (!versionMatch) {
Expand All @@ -286,13 +290,26 @@ export async function dockerCLI(params: DockerCLIParameters | PartialExecParamet
});
}

export async function isPodman(params: PartialExecParameters) {
export enum CLIVariant {
Docker = 'docker',
Podman = 'podman',
Wslc = 'wslc',
}

export async function lookupCLIVariant(params: PartialExecParameters): Promise<CLIVariant> {
try {
const { stdout } = await dockerCLI(params, '-v');
return stdout.toString().toLowerCase().indexOf('podman') !== -1;
} catch (err) {
return false;
const lower = stdout.toString().toLowerCase();
if (lower.indexOf('wslc') !== -1) {
return CLIVariant.Wslc;
}
if (lower.indexOf('podman') !== -1) {
return CLIVariant.Podman;
}
} catch (_err) {
// fall through
}
return CLIVariant.Docker;
}

export async function dockerPtyCLI(params: PartialPtyExecParameters | DockerResolverParameters | DockerCLIParameters, ...args: string[]) {
Expand Down
Loading