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
5 changes: 3 additions & 2 deletions src/spec-node/featuresCLI/testCommandImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { CLIHost } from '../../spec-common/cliHost';
import { launch, ProvisionOptions, createDockerParams } from '../devContainers';
import { doExec } from '../devContainersSpecCLI';
import { LaunchResult, staticExecParams, staticProvisionParams, testLibraryScript } from './utils';
import { DockerResolverParameters } from '../utils';
import { DockerResolverParameters, normalizeDevContainerLabelPath } from '../utils';
import { DevContainerConfig } from '../../spec-configuration/configuration';
import { FeaturesTestCommandInput } from './test';
import { cpDirectoryLocal, rmLocal } from '../../spec-utils/pfs';
Expand Down Expand Up @@ -546,7 +546,8 @@ async function launchProject(params: DockerResolverParameters, workspaceFolder:
const { common } = params;
let response = {} as LaunchResult;

const idLabels = [`devcontainer.local_folder=${workspaceFolder}`, `devcontainer.is_test_run=true`];
const normalizedWorkspaceFolder = normalizeDevContainerLabelPath(process.platform, workspaceFolder);
const idLabels = [`devcontainer.local_folder=${normalizedWorkspaceFolder}`, `devcontainer.is_test_run=true`];
const options: ProvisionOptions = {
...staticProvisionParams,
workspaceFolder,
Expand Down
90 changes: 83 additions & 7 deletions src/spec-node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { CommonDevContainerConfig, ContainerProperties, getContainerProperties,
import { Workspace } from '../spec-utils/workspaces';
import { URI } from 'vscode-uri';
import { ShellServer } from '../spec-common/shellServer';
import { inspectContainer, inspectImage, getEvents, 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 } 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 @@ -614,21 +614,99 @@ export function getEmptyContextFolder(common: ResolverParameters) {
return common.cliHost.path.join(common.persistedFolder, 'empty-folder');
}

export function normalizeDevContainerLabelPath(platform: NodeJS.Platform, value: string): string {
if (platform !== 'win32') {
return value;
}

// Normalize separators and dot segments, then explicitly lowercase the drive
// letter because devcontainer.local_folder / devcontainer.config_file labels
// should compare case-insensitively on Windows.
const normalized = path.win32.normalize(value);
if (normalized.length >= 2 && normalized[1] === ':') {
return normalized[0].toLowerCase() + normalized.slice(1);
}

return normalized;
}

async function findDevContainerByNormalizedLabels(params: DockerResolverParameters | DockerCLIParameters, normalizedWorkspaceFolder: string, normalizedConfigFile: string) {
if (process.platform !== 'win32') {
return undefined;
}

const ids = await listContainers(params, true, [hostFolderLabel]);
if (!ids.length) {
return undefined;
}

const details = await inspectContainers(params, ids);
return details
.filter(container => container.State.Status !== 'removing')
.find(container => {
const labels = container.Config.Labels || {};
const containerWorkspaceFolder = labels[hostFolderLabel];
if (!containerWorkspaceFolder || normalizeDevContainerLabelPath('win32', containerWorkspaceFolder) !== normalizedWorkspaceFolder) {
return false;
}

const containerConfigFile = labels[configFileLabel];
return !!containerConfigFile
&& normalizeDevContainerLabelPath('win32', containerConfigFile) === normalizedConfigFile;
});
}

async function findLegacyDevContainerByNormalizedWorkspaceFolder(params: DockerResolverParameters | DockerCLIParameters, normalizedWorkspaceFolder: string) {
if (process.platform !== 'win32') {
return undefined;
}

const ids = await listContainers(params, true, [hostFolderLabel]);
if (!ids.length) {
return undefined;
}

const details = await inspectContainers(params, ids);
return details
.filter(container => container.State.Status !== 'removing')
.find(container => {
const labels = container.Config.Labels || {};
const containerWorkspaceFolder = labels[hostFolderLabel];
if (!containerWorkspaceFolder) {
return false;
}

return normalizeDevContainerLabelPath('win32', containerWorkspaceFolder) === normalizedWorkspaceFolder;
});
}

export async function findContainerAndIdLabels(params: DockerResolverParameters | DockerCLIParameters, containerId: string | undefined, providedIdLabels: string[] | undefined, workspaceFolder: string | undefined, configFile: string | undefined, removeContainerWithOldLabels?: boolean | string) {
if (providedIdLabels) {
return {
container: containerId ? await inspectContainer(params, containerId) : await findDevContainer(params, providedIdLabels),
idLabels: providedIdLabels,
};
}

const normalizedWorkspaceFolder = workspaceFolder ? normalizeDevContainerLabelPath(process.platform, workspaceFolder) : workspaceFolder;
const normalizedConfigFile = configFile ? normalizeDevContainerLabelPath(process.platform, configFile) : configFile;
const newLabels = [`${hostFolderLabel}=${normalizedWorkspaceFolder}`, `${configFileLabel}=${normalizedConfigFile}`];
const oldLabels = [`${hostFolderLabel}=${normalizedWorkspaceFolder}`];

let container: ContainerDetails | undefined;
if (containerId) {
container = await inspectContainer(params, containerId);
} else if (workspaceFolder && configFile) {
container = await findDevContainer(params, [`${hostFolderLabel}=${workspaceFolder}`, `${configFileLabel}=${configFile}`]);
} else if (normalizedWorkspaceFolder && normalizedConfigFile) {
container = await findDevContainer(params, newLabels);
if (!container) {
container = await findDevContainerByNormalizedLabels(params, normalizedWorkspaceFolder, normalizedConfigFile);
}
if (!container) {
// Fall back to old labels.
container = await findDevContainer(params, [`${hostFolderLabel}=${workspaceFolder}`]);
container = await findDevContainer(params, oldLabels);
if (!container) {
container = await findLegacyDevContainerByNormalizedWorkspaceFolder(params, normalizedWorkspaceFolder);
}
if (container) {
if (container.Config.Labels?.[configFileLabel]) {
// But ignore containers with new labels.
Expand All @@ -645,9 +723,7 @@ export async function findContainerAndIdLabels(params: DockerResolverParameters
}
return {
container,
idLabels: !container || container.Config.Labels?.[configFileLabel] ?
[`${hostFolderLabel}=${workspaceFolder}`, `${configFileLabel}=${configFile}`] :
[`${hostFolderLabel}=${workspaceFolder}`],
idLabels: !container || container.Config.Labels?.[configFileLabel] ? newLabels : oldLabels,
};
}

Expand Down
29 changes: 29 additions & 0 deletions src/test/labelPathNormalization.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

import { assert } from 'chai';
import { normalizeDevContainerLabelPath } from '../spec-node/utils';

describe('normalizeDevContainerLabelPath', function () {
it('lowercases Windows drive letters', function () {
assert.equal(
normalizeDevContainerLabelPath('win32', 'C:\\CodeBlocks\\remill'),
'c:\\CodeBlocks\\remill'
);
});

it('normalizes Windows path separators', function () {
assert.equal(
normalizeDevContainerLabelPath('win32', 'C:/CodeBlocks/remill/.devcontainer/devcontainer.json'),
'c:\\CodeBlocks\\remill\\.devcontainer\\devcontainer.json'
);
});

it('leaves non-Windows paths unchanged', function () {
assert.equal(
normalizeDevContainerLabelPath('linux', '/workspaces/remill'),
'/workspaces/remill'
);
});
});