Repository navigation
CLI: Add --mount support for create and run - #41337
Conversation
Add a Docker-style --mount option to `wslc container run` and `wslc container create`. The flag accepts comma-separated key=value pairs (type=bind|volume|tmpfs, source/src, target/destination/dst, readonly/ro) and is routed into the existing volume/tmpfs plumbing. - Parse --mount into a ParsedMount (ArgumentValidation) - Register the Mount argument for run/create - Wire parsed mounts into ContainerOptions (ContainerTasks) - Add localization strings (MountArgDescription, InvalidMountError) - Add e2e tests (tmpfs, named volume, readonly-via-inspect, invalid type) and update run/create help-text expectations Co-authored-by: Copilot <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR adds Docker-compatible --mount parsing and plumbing for wslc container run / wslc container create, translating validated mount specs into the existing bind/volume/tmpfs execution paths and adding unit + E2E coverage plus localized error strings.
Changes:
- Introduces a common
mount::Specmodel and Docker-grammar--mountparser undersrc/windows/common/. - Wires
--mountinto argument validation and container option construction, including duplicate-destination rejection across--mount/--volume/--tmpfs. - Adds table-driven unit tests and new E2E scenarios for
--mount.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/windows/wslc/WSLCCLIMountParserUnitTests.cpp | Adds table-driven unit tests for --mount parsing and destination de-duplication behavior. |
| test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp | Adds E2E coverage for --mount tmpfs/volume/readonly and failure cases. |
| src/windows/wslc/tasks/ContainerTasks.cpp | Plumbs parsed mount specs from CLI args into ContainerOptions and validates uniqueness. |
| src/windows/wslc/services/ContainerService.cpp | Translates mount::Spec into launcher calls for bind/volume/tmpfs. |
| src/windows/wslc/services/ContainerModel.h | Extends ContainerOptions with Mounts and declares destination uniqueness validation. |
| src/windows/wslc/services/ContainerModel.cpp | Reuses named-volume validation from common parser and implements duplicate-destination detection. |
| src/windows/wslc/commands/ContainerRunCommand.cpp | Adds --mount to container run arguments. |
| src/windows/wslc/commands/ContainerCreateCommand.cpp | Adds --mount to container create arguments. |
| src/windows/wslc/arguments/SpecParsing.cpp | Adds a standard header include used by parsing utilities. |
| src/windows/wslc/arguments/ArgumentValidation.cpp | Validates/parses --mount and surfaces localized user-facing errors. |
| src/windows/wslc/arguments/ArgumentDefinitions.h | Declares the new --mount argument in the X-macro table. |
| src/windows/wslc/arguments/ArgumentConvertedTypes.h | Adds the converted type alias mapping for parsed mount specs. |
| src/windows/common/MountSpecParsing.h | Declares the mount grammar version, spec model, and parsing/normalization helpers. |
| src/windows/common/MountSpecParsing.cpp | Implements Docker-compatible --mount parsing and tmpfs option formatting. |
| src/windows/common/CMakeLists.txt | Adds the new common parser sources/headers to the build. |
| localization/strings/en-US/Resources.resw | Adds localized strings for invalid mount syntax and duplicate mount destinations. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/windows/common/MountSpecParsing.cpp:583
FormatTmpfsOptionsalso omits an explicitly providedtmpfs-size=0by skipping size when the parsed value is 0. If the user passestmpfs-size=0, that intent should be preserved and forwarded (and kept consistent with existing--tmpfsbehavior, which can passsize=0).
if (mount.TmpfsSizeBytes.has_value() && mount.TmpfsSizeBytes.value() != 0)
{
options.emplace_back(std::format("size={}", FormatDockerTmpfsSize(mount.TmpfsSizeBytes.value())));
}
src/windows/common/MountSpecParsing.cpp:579
FormatTmpfsOptionsdrops an explicitly providedtmpfs-mode=0000because it omits the mode option when the parsed value is 0. That changes user-requested semantics (and differs from--tmpfs, which forwards options verbatim), sincemode=0is a meaningful tmpfs setting.
This issue also appears on line 580 of the same file.
if (mount.TmpfsMode.has_value() && mount.TmpfsMode.value() != 0)
{
options.emplace_back(std::format("mode={:o}", mount.TmpfsMode.value()));
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/windows/common/MountSpecParsing.cpp:14
- New files should use the repository’s single-line copyright header format (
// Copyright (C) Microsoft Corporation. All rights reserved.). This file currently uses the older block header style.
/*++
Copyright (c) Microsoft. All rights reserved.
src/windows/common/MountSpecParsing.h:14
- New files should use the repository’s single-line copyright header format (
// Copyright (C) Microsoft Corporation. All rights reserved.). This header currently uses the older block header style.
/*++
Copyright (c) Microsoft. All rights reserved.
test/windows/wslc/WSLCCLIMountParserUnitTests.cpp:14
- New files should use the repository’s single-line copyright header format (
// Copyright (C) Microsoft Corporation. All rights reserved.). This test file currently uses the older block header style.
/*++
Copyright (c) Microsoft. All rights reserved.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/windows/common/MountSpecParsing.cpp:159
- Minor typo in the digit set passed to find_last_of: it includes an extra '0' ("01234567890. "), which is confusing to readers even though it likely doesn’t change behavior.
const auto separator = input.find_last_of("01234567890. ");
src/windows/wslc/arguments/ArgumentValidation.cpp:234
- The mount parser’s ValidationException::Reason() is composed of hard-coded English strings (from MountSpecParsing.cpp) and is surfaced directly to users via WSLCCLI_InvalidMountError. This means a significant portion of the user-facing error text is not localizable, which conflicts with the PR’s stated goal of localized errors for unsupported/invalid mount specs.
catch (const mount::ValidationException& ex)
{
throw ArgumentException(Localization::WSLCCLI_InvalidMountError(value, ex.Reason()));
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.
Suppressed comments (4)
test/windows/wslc/WSLCCLIMountParserUnitTests.cpp:222
- This mount spec (
type=volume,...,bind-recursive=enabled) is listed as a valid case here, but the same exact input is also listed as an invalid (bind-* family mismatch) case later inc_invalidMountCases(lines ~350-351). With the current parser, bind-recursive is a bind-only option, so this entry will make the valid-case loop fail.
{L"type=volume,source=data-volume,target=/data,bind-recursive=enabled",
mount::Type::Volume,
L"data-volume",
"/data",
false,
{},
{},
""},
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp:802
- This test is in
WSLCE2EContainerCreateTests.cppand is namedContainer_Create_*, but it invokescontainer run. Becauserunhas additional behavior (and can exercise different code paths thancreate), this doesn’t reliably validatecontainer createrejecting missing bind sources.
auto result = RunWslc(std::format(
L"container run --name {} --mount \"type=bind,source={},target=/data\" {} true",
WslcContainerName,
source.wstring(),
AlpineImage.NameAndTag()));
result.Verify({.Stdout = L"", .Stderr = FormatWslcError(Localization::MessageWslcBindSourcePathNotFound(source.wstring())), .ExitCode = 1});
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp:820
- This
Container_Create_*test also runscontainer runinstead ofcontainer create, so it’s not specifically exercising the create path’s behavior around creating missing bind-source directories. Either switch tocontainer create(and clean up the created container), or rename/move the test so it’s clear it’s validatingrun.
auto result = RunWslc(std::format(
L"container run --name {} --volume \"{}:/data\" {} true", WslcContainerName, source.wstring(), AlpineImage.NameAndTag()));
result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(std::filesystem::is_directory(source));
EnsureContainerDoesNotExist(WslcContainerName);
src/windows/common/MountSpecParsing.cpp:585
- PR description says anonymous volumes are rejected, but
ValidateMountSpeccurrently allowstype=volumewith an empty source (i.e., anonymous volume) as long as the name is either empty or a valid named volume. Either update the PR description to match the implemented behavior, or enforcesourcefortype=volumeif anonymous volumes truly aren’t supported.
case Type::Volume:
if (!mount.Source.empty() && !IsValidNamedVolumeName(mount.Source))
{
ThrowValidation(Localization::WSLCCLI_MountVolumeSourceInvalidError());
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.
Suppressed comments (6)
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp:817
- This test is in the ContainerCreate suite but the command uses
container run, so it’s not actually validatingcontainer createbehavior when a host--volumesource is missing.
auto result = RunWslc(std::format(
L"container run --name {} --volume \"{}:/data\" {} true", WslcContainerName, source.wstring(), AlpineImage.NameAndTag()));
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp:801
- These two E2E tests are in the ContainerCreate suite and are named
..._Container_Create_..., but the command under test usescontainer run. That means the test is exercising theruncode path (which may have additional behaviors) instead ofcreate, so it won’t reliably catch regressions specific tocontainer create.
This issue also appears on line 816 of the same file.
auto result = RunWslc(std::format(
L"container run --name {} --mount \"type=bind,source={},target=/data\" {} true",
WslcContainerName,
source.wstring(),
AlpineImage.NameAndTag()));
src/windows/wslc/services/ContainerService.cpp:32
namespace mount = wsl::windows::common::mount;is declared but never used in this translation unit (there are nomount::references). This can trigger unused-alias warnings depending on compiler settings and adds noise.
namespace mount = wsl::windows::common::mount;
test/windows/wslc/e2e/WSLCE2EHelpers.h:29
FormatWslcErrorusesstd::formatandstd::wstring_viewbut this header doesn’t include<format>/<string_view>. Relying on transitive includes makes the header brittle and can break if include order changes.
inline std::wstring FormatWslcError(const std::wstring& message, std::wstring_view errorCode = L"E_INVALIDARG")
{
return std::format(L"{}\r\nError code: {}\r\n", message, errorCode);
}
src/windows/wslcsession/WSLCContainer.cpp:643
- If a mount destination pointer is unexpectedly null (e.g.,
Volumes[i].ContainerPath,NamedVolumes[i].ContainerPath, orTmpfs[i].Destination), this currently throwsE_INVALIDARGwithout any context. Using the_MSGform here makes failures much easier to diagnose.
const auto addDestination = [&](const char* destination) {
THROW_HR_IF_NULL(E_INVALIDARG, destination);
src/windows/wslcsession/WSLCContainer.cpp:1859
- There’s a nearby TODO saying “Return mounts once --mount is implemented”, but
HostConfig.Mountsis already being used here to enrich inspect output. Updating/removing that TODO would avoid misleading future readers.
wslc_schema::InspectMount mountInfo{};
mountInfo.Type = volume.Type;
mountInfo.Name = volume.Name;
const auto structuredMount = std::ranges::find_if(dockerInspect.HostConfig.Mounts, [&](const auto& mount) {
return mount.Type == "volume" && mount.Target == volume.Destination;
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 24 out of 24 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/windows/wslc/services/ContainerModel.cpp:290
ValidateUniqueMountDestinationsrethrowsmount::MountExceptionfor non-duplicate errors. If this function is ever called with a mount spec that bypassed arg validation (or new validation is added later), that would propagate a non-WIL exception through the CLI path and produce inconsistent error handling.
Consider translating all mount::MountException instances to a user-facing HRESULT here (similar to other validation helpers).
void ValidateUniqueMountDestinations(const ContainerOptions& options)
{
try
{
mount::ValidateMountCollection(options.Mounts);
}
catch (const mount::MountException& ex)
{
if (ex.Error() == mount::ValidationError::DuplicateDestination)
{
THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_DuplicateMountDestinationError(MultiByteToWide(ex.Destination())));
}
throw;
}
src/windows/common/MountSpecParsing.cpp:566
- The PR description says anonymous volumes are rejected, but
ValidateMountSpeccurrently allowsType::Volumemounts with an emptySource(which enables anonymous volumes, and is also exercised by the added e2e coverage).
Please align behavior with the stated intent by either rejecting empty volume sources here, or updating the PR description/notes to reflect that anonymous volumes are supported.
case Type::Volume:
if (!mount.Source.empty() && !IsValidNamedVolumeName(mount.Source))
{
ThrowValidation(Localization::WSLCCLI_MountVolumeSourceInvalidError());
}
break;
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/windows/wslc/services/ContainerService.cpp:34
- Unused namespace alias
mountwas introduced but is never referenced in this file. This can trigger unused-declaration warnings (and adds noise).
namespace wsl::windows::wslc::services {
namespace mount = wsl::windows::common::mount;
test/windows/wslc/WSLCCLIMountParserUnitTests.cpp:66
- PR description says anonymous volumes are rejected, but the parser/tests explicitly accept
type=volume,target=...(empty source) which results in an anonymous volume. Please either update the PR description to match the implemented behavior, or change validation to reject empty volume sources if that was the intent.
constexpr ValidMountCase c_validMountCases[] = {
{L"type=volume,target=/data", mount::Type::Volume, L"", "/data", false, {}, {}, ""},
{L"source=data-volume,target=/data", mount::Type::Volume, L"data-volume", "/data", false, {}, {}, ""},
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/windows/wslcsession/WSLCContainer.cpp:695
- ConvertAndValidateMounts() checks NamedVolumes[].Name for null but can pass a null NamedVolumes[].ContainerPath into addDestination(), which produces an unhelpful generic failure. Add an explicit null check with index context before addDestination().
THROW_HR_IF(E_INVALIDARG, containerOptions.NamedVolumesCount > 0 && containerOptions.NamedVolumes == nullptr);
for (ULONG i = 0; i < containerOptions.NamedVolumesCount; ++i)
{
THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.NamedVolumes[i].Name, "NamedVolume at index %lu has null Name", i);
addDestination(containerOptions.NamedVolumes[i].ContainerPath);
}
src/windows/wslcsession/WSLCContainer.cpp:701
- ConvertAndValidateMounts() can pass a null Tmpfs[*].Destination into addDestination(), which fails without any index context. Add an explicit null check with a message before calling addDestination().
THROW_HR_IF(E_INVALIDARG, containerOptions.TmpfsCount > 0 && containerOptions.Tmpfs == nullptr);
for (ULONG i = 0; i < containerOptions.TmpfsCount; ++i)
{
addDestination(containerOptions.Tmpfs[i].Destination);
}
src/windows/wslcsession/WSLCContainer.cpp:688
- ConvertAndValidateMounts() validates Volumes[].HostPath but can pass a null Volumes[].ContainerPath into addDestination(), which then fails with a generic E_INVALIDARG and no index context. Add an explicit null check with an index-specific message before calling addDestination().
This issue also appears in the following locations of the same file:
- line 690
- line 697
for (ULONG i = 0; i < containerOptions.VolumesCount; ++i)
{
THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.Volumes[i].HostPath, "Volumes[%lu].HostPath is null", i);
addDestination(containerOptions.Volumes[i].ContainerPath);
}
src/windows/wslcsession/WSLCContainer.cpp:747
- PrepareBindMount() calls std::filesystem::is_regular_file(hostPath) without the error_code overload. On access/permission issues this can throw std::filesystem::filesystem_error, bypassing the HRESULT/user-error flow and potentially failing the operation with an unhandled exception. Use the error_code overload and convert failures into a localized user error.
std::error_code ec;
hostPath = std::filesystem::canonical(hostPath, ec);
if (!ec)
{
if (std::filesystem::is_regular_file(hostPath))
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/windows/wslcsession/WSLCContainer.cpp:695
- ConvertAndValidateMounts() does not emit an indexed diagnostic if NamedVolumes[i].ContainerPath is null (it falls back to addDestination()'s generic null check). Adding an explicit THROW_HR_IF_NULL_MSG here would make failures actionable.
for (ULONG i = 0; i < containerOptions.NamedVolumesCount; ++i)
{
THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.NamedVolumes[i].Name, "NamedVolume at index %lu has null Name", i);
addDestination(containerOptions.NamedVolumes[i].ContainerPath);
}
src/windows/wslcsession/WSLCContainer.cpp:688
- ConvertAndValidateMounts() relies on addDestination()'s generic null check for Volumes[i].ContainerPath. This loses the index/context in the failure, and it’s inconsistent with the explicit HostPath null check above.
This issue also appears on line 691 of the same file.
for (ULONG i = 0; i < containerOptions.VolumesCount; ++i)
{
THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.Volumes[i].HostPath, "Volumes[%lu].HostPath is null", i);
addDestination(containerOptions.Volumes[i].ContainerPath);
}
src/windows/wslcsession/WSLCContainer.cpp:701
- ConvertAndValidateMounts() should report which tmpfs entry is invalid when Tmpfs[i].Destination is null (currently it throws E_INVALIDARG without context via addDestination()).
for (ULONG i = 0; i < containerOptions.TmpfsCount; ++i)
{
addDestination(containerOptions.Tmpfs[i].Destination);
}
| if (GetFileAttributesW(source.c_str()) == INVALID_FILE_ATTRIBUTES && GetLastError() == ERROR_INVALID_NAME) | ||
| { | ||
| ThrowParse(Localization::WSLCCLI_VolumeHostPathInvalid(value, rawSource)); | ||
| } |
There was a problem hiding this comment.
nit: I recommend using wsl::windows::common::filesystem::GetCanonicalPath() since it's what we use in other places
|
|
||
| std::string NormalizeDestination(std::string destination) | ||
| { | ||
| std::replace(destination.begin(), destination.end(), '\\', '/'); |
There was a problem hiding this comment.
If I'm following correctly, this is the container path, and in Linux '\' is actually valid in a path so we shouldn't translate it to /
| const auto component = destination.substr(start, end - start); | ||
| if (!component.empty() && component != ".") | ||
| { | ||
| if (component == "..") |
There was a problem hiding this comment.
Do we actually need to do this ? I would expect that the docker API does that for us
|
|
||
| namespace wsl::windows::common::mount { | ||
|
|
||
| inline constexpr std::string_view c_dockerCliMountGrammarVersion = "25.0.3"; |
| { | ||
| result.m_containerPath = value; | ||
| return result; | ||
| mount::ValidateMountCollection(options.Mounts); |
There was a problem hiding this comment.
I don't think we need to do this in the client side, I would expect the docker API to give us an error if this happens
| const auto sourceExists = std::filesystem::exists(mount.Source, error); | ||
| if (error) | ||
| { | ||
| throw mount::MountValidationException(Localization::MessageWslcBindSourcePathError(mount.Source, error.message())); |
There was a problem hiding this comment.
Same here, I would recommend throwing the user error directly to make things a bit easier to follow
| GUID volumeId; | ||
| THROW_IF_FAILED(CoCreateGuid(&volumeId)); | ||
|
|
||
| auto parentVMPath = std::format("/mnt/{}", wsl::shared::string::GuidToString<char>(volumeId)); |
There was a problem hiding this comment.
nit: This could be constructed later since we don't use it until line 749
| std::wstring sourceFilename; | ||
| { | ||
| std::error_code ec; | ||
| hostPath = std::filesystem::canonical(hostPath, ec); |
There was a problem hiding this comment.
nit: I recommend using wsl::windows::common::filesystem::GetCanonicalPath() here (probably didn't exist when you started working on this change)
| WSLCMountSpecFlagsTmpfsOptions = 8, | ||
| } WSLCMountSpecFlags; | ||
|
|
||
| cpp_quote("#define WSLCMountSpecFlagsValid (WSLCMountSpecFlagsTmpfsSize | WSLCMountSpecFlagsTmpfsMode | WSLCMountSpecFlagsCreateSourceIfMissing | WSLCMountSpecFlagsTmpfsOptions)") |
There was a problem hiding this comment.
Do we need WSLCMountSpecFlagsTmpfsOptions or could we just set TmpfsOptions to null when unset ?
| WSLCMountSpec mount{}; | ||
| switch (Mount.MountType) | ||
| { | ||
| case mount::Type::Bind: |
There was a problem hiding this comment.
nit: I recommend using the same enum here as well so we don't need to map
Summary of the Pull Request
Adds Docker-compatible
--mountsupport towslc container runandwslc container create, and consolidates all CLI mount forms into one typed mount pipeline.--mount,--volume, and--tmpfsnow producemount::Specvalues during argument validation. A single mount collection performs validation and duplicate-destination detection beforeWSLCContainerLauncher::AddMounttranslates each specification for the backend.PR Checklist
Implementation
mount::Specmodel and Docker-compatible parsers insrc/windows/common.--mounttargets because that syntax uses comma-separated key/value fields.ParseStorageSizeimplementation for tmpfs sizes.--tmpfsoptions such asuid,gid,nr_inodes,nosuid,nodev, andnoexec.AddVolume,AddNamedVolume, andAddTmpfsas compatibility wrappers that delegate toAddMount.--mount type=bindrequires the source to exist.--volume host:targetcreates a missing host directory.--tmpfsentries in the Docker request and inspect output.--mount,--volume, and--tmpfsusing one collection.Why WSLC Parses
--mountDocker CLI converts the textual
--mountgrammar into typed mount objects before calling Docker Engine. WSLC must do the same because its launcher and backend boundary are structured, and bind mounts require additional Windows-host and utility-VM preparation.WSLC also needs to reject options that its current transport cannot represent faithfully. Parsing and capability validation before the backend request prevents silent semantic loss and produces consistent CLI errors.
Validation
WSLCCLIMountParserUnitTests: passed.WSLCCLITmpfsParserUnitTests: 2/2 passed.WSLCVolumeMountUnitTests: passed through the common volume parser.--volumecreate E2E suite: 9/9 passed.--mount type=volumeE2E coverage passed.--mount,--volume, and--tmpfs.--mountfails without creating a directory while--volumesucceeds and creates it.