diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw
index b94a8d126..8e8ddaf6f 100644
--- a/localization/strings/en-US/Resources.resw
+++ b/localization/strings/en-US/Resources.resw
@@ -2496,6 +2496,14 @@ For privacy information about this product please visit https://aka.ms/privacy.<
Failed to create volume '{}': {}
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+ Bind source path does not exist: '{}'
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+
+ Failed to access bind source path '{}': {}
+ {FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
Too many volumes have been mounted (limit: {}). Restart the session to mount more volumes. This will be fixed in a future release.
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
@@ -3005,6 +3013,92 @@ On first run, creates the file with all settings commented out at their defaults
Invalid value "{}" for the '-f, --filter' option: bad format of filter (expected name=value)
{FixedPlaceholder="{}"}{Locked="--filter'"}Command line arguments, file names and string inserts should not be translated
+
+ Invalid argument "{}" for '--mount' option: {}
+ {FixedPlaceholder="{}"}{FixedPlaceholder="{}"}{Locked="--mount'"}Command line arguments, file names and string inserts should not be translated
+
+
+ Argument "{}" for '--mount' option uses an unsupported feature: {}
+ {FixedPlaceholder="{}"}{FixedPlaceholder="{}"}{Locked="--mount'"}Command line arguments, file names and string inserts should not be translated
+
+
+ Duplicate mount point: {}
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+
+ The CSV input is malformed.
+ {Locked="CSV"}Command line arguments should not be translated
+
+
+ The field '{}' must be a key=value pair.
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+
+ The key '{}' is unexpected in '{}'.
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+
+ The value for '{}' is invalid: '{}'.
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+
+ The value for '{}' is invalid: '{}'. Valid values are "enabled", "disabled", "writable", and "readonly".
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+
+ The mount type is required.
+
+
+ Options matching '{}' cannot be used with mount type '{}'.
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+
+ Option '{}' requires the 'readonly' option.
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+
+ Option 'bind-recursive=readonly' requires the 'bind-propagation=rprivate' option.
+ {Locked="bind-recursive=readonly"}{Locked="bind-propagation=rprivate"}Command line arguments should not be translated
+
+
+ Mount type '{}' is not supported.
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+
+ Option '{}' is not supported.
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+
+ The mount target is required.
+
+
+ The mount target path must be absolute.
+
+
+ The tmpfs options are supported only for tmpfs mounts.
+ {Locked="tmpfs"}Command line arguments should not be translated
+
+
+ The tmpfs size cannot be negative.
+ {Locked="tmpfs"}Command line arguments should not be translated
+
+
+ The mount source is required.
+
+
+ The bind source path must be absolute.
+ {Locked="bind"}Command line arguments should not be translated
+
+
+ The volume source must be a valid named volume.
+
+
+ The source option is not supported for tmpfs mounts.
+ {Locked="tmpfs"}Command line arguments should not be translated
+
+
+ The mount type is not supported.
+
Follow log output
@@ -3073,6 +3167,9 @@ On first run, creates the file with all settings commented out at their defaults
Memory limit (e.g. 512M, 1G)
{Locked="512M"}{Locked="1G"}Command line argument example values should not be translated
+
+ Attach a filesystem mount to the container
+
Container host name
diff --git a/src/windows/common/CMakeLists.txt b/src/windows/common/CMakeLists.txt
index 008eafab1..97fb2dfd8 100644
--- a/src/windows/common/CMakeLists.txt
+++ b/src/windows/common/CMakeLists.txt
@@ -26,6 +26,7 @@ set(SOURCES
LxssMessagePort.cpp
LxssSecurity.cpp
LxssServerPort.cpp
+ MountSpecParsing.cpp
NatNetworking.cpp
notifications.cpp
Redirector.cpp
@@ -111,6 +112,7 @@ set(HEADERS
LxssPort.h
LxssSecurity.h
LxssServerPort.h
+ MountSpecParsing.h
NatNetworking.h
notifications.h
precomp.h
diff --git a/src/windows/common/MountSpecParsing.cpp b/src/windows/common/MountSpecParsing.cpp
new file mode 100644
index 000000000..1a1079829
--- /dev/null
+++ b/src/windows/common/MountSpecParsing.cpp
@@ -0,0 +1,690 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ MountSpecParsing.cpp
+
+Abstract:
+
+ Docker-compatible mount specification parsing.
+
+--*/
+
+#include "precomp.h"
+#include "MountSpecParsing.h"
+#include "string.hpp"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace wsl::shared;
+using namespace wsl::shared::string;
+
+namespace wsl::windows::common::mount {
+
+namespace {
+
+ enum class Field
+ {
+ Type,
+ Source,
+ Target,
+ ReadOnly,
+ Consistency,
+ BindPropagation,
+ BindNonRecursive,
+ BindRecursive,
+ VolumeNoCopy,
+ VolumeLabel,
+ VolumeDriver,
+ VolumeOption,
+ TmpfsSize,
+ TmpfsMode,
+ };
+
+ enum class Family
+ {
+ General,
+ Bind,
+ Volume,
+ Tmpfs,
+ };
+
+ enum class Support
+ {
+ Supported,
+ Unsupported,
+ ValueDependent,
+ };
+
+ struct FieldDefinition
+ {
+ std::wstring_view Name;
+ Field Id;
+ Family OptionFamily;
+ bool AllowsBareForm;
+ Support SupportLevel;
+ };
+
+ // Keep this table aligned with docker/cli v25.0.3 opts/mount.go. It is the
+ // authoritative list of accepted fields, aliases, option families, and WSLC support.
+ constexpr std::array c_fieldDefinitions{
+ FieldDefinition{L"type", Field::Type, Family::General, false, Support::ValueDependent},
+ FieldDefinition{L"source", Field::Source, Family::General, false, Support::Supported},
+ FieldDefinition{L"src", Field::Source, Family::General, false, Support::Supported},
+ FieldDefinition{L"target", Field::Target, Family::General, false, Support::Supported},
+ FieldDefinition{L"dst", Field::Target, Family::General, false, Support::Supported},
+ FieldDefinition{L"destination", Field::Target, Family::General, false, Support::Supported},
+ FieldDefinition{L"readonly", Field::ReadOnly, Family::General, true, Support::Supported},
+ FieldDefinition{L"ro", Field::ReadOnly, Family::General, true, Support::Supported},
+ FieldDefinition{L"consistency", Field::Consistency, Family::General, false, Support::Unsupported},
+ FieldDefinition{L"bind-propagation", Field::BindPropagation, Family::Bind, false, Support::Unsupported},
+ FieldDefinition{L"bind-nonrecursive", Field::BindNonRecursive, Family::Bind, true, Support::Unsupported},
+ FieldDefinition{L"bind-recursive", Field::BindRecursive, Family::Bind, false, Support::ValueDependent},
+ FieldDefinition{L"volume-nocopy", Field::VolumeNoCopy, Family::Volume, true, Support::Unsupported},
+ FieldDefinition{L"volume-label", Field::VolumeLabel, Family::Volume, false, Support::Unsupported},
+ FieldDefinition{L"volume-driver", Field::VolumeDriver, Family::Volume, false, Support::Unsupported},
+ FieldDefinition{L"volume-opt", Field::VolumeOption, Family::Volume, false, Support::Unsupported},
+ FieldDefinition{L"tmpfs-size", Field::TmpfsSize, Family::Tmpfs, false, Support::Supported},
+ FieldDefinition{L"tmpfs-mode", Field::TmpfsMode, Family::Tmpfs, false, Support::Supported},
+ };
+
+ struct DockerMountSpec
+ {
+ std::wstring Type = L"volume";
+ std::wstring Source;
+ std::wstring Target;
+ bool ReadOnly = false;
+ bool HasVolumeOptions = false;
+ bool HasBindOptions = false;
+ bool HasTmpfsOptions = false;
+ bool BindReadOnlyNonRecursive = false;
+ bool BindReadOnlyForceRecursive = false;
+ std::wstring BindPropagation;
+ std::optional TmpfsSizeBytes;
+ std::optional TmpfsMode;
+ std::optional UnsupportedOption;
+ };
+
+ struct KeyValue
+ {
+ std::wstring Key;
+ std::wstring Value;
+ bool HadSeparator;
+ };
+
+ [[noreturn]] void ThrowValidation(std::wstring reason)
+ {
+ throw MountValidationException(std::move(reason));
+ }
+
+ [[noreturn]] void ThrowParse(std::wstring reason)
+ {
+ throw MountParseException(std::move(reason));
+ }
+
+ [[noreturn]] void ThrowUnsupported(std::wstring reason)
+ {
+ throw MountUnsupportedException(std::move(reason));
+ }
+
+ KeyValue SplitKeyValue(const std::wstring& value)
+ {
+ const auto position = value.find(L'=');
+ if (position == std::wstring::npos)
+ {
+ return {.Key = value, .HadSeparator = false};
+ }
+
+ return {.Key = value.substr(0, position), .Value = value.substr(position + 1), .HadSeparator = true};
+ }
+
+ const FieldDefinition* FindField(std::wstring_view name)
+ {
+ const auto found = std::ranges::find_if(c_fieldDefinitions, [&](const auto& definition) { return definition.Name == name; });
+ return found == c_fieldDefinitions.end() ? nullptr : &*found;
+ }
+
+ void RecordUnsupportedOption(DockerMountSpec& mount, std::wstring_view option)
+ {
+ if (!mount.UnsupportedOption.has_value())
+ {
+ mount.UnsupportedOption = option;
+ }
+ }
+
+ std::optional ParseDockerRamInBytes(const std::wstring& value)
+ {
+ const auto parsed = wsl::windows::common::string::ParseStorageSize(value, wsl::windows::common::string::StorageSizeUnit::Binary);
+ if (!parsed.has_value() || parsed.value() > static_cast(std::numeric_limits::max()))
+ {
+ return std::nullopt;
+ }
+
+ return static_cast(parsed.value());
+ }
+
+ std::optional ParseDockerTmpfsMode(const std::wstring& value)
+ {
+ if (value.empty() || value.front() == L'-')
+ {
+ return std::nullopt;
+ }
+
+ size_t position = value.front() == L'+' ? 1 : 0;
+ if (position == value.size())
+ {
+ return std::nullopt;
+ }
+
+ uint64_t result = 0;
+ for (; position < value.size(); ++position)
+ {
+ const auto digit = value[position];
+ if (digit < L'0' || digit > L'7')
+ {
+ return std::nullopt;
+ }
+
+ result = (result * 8) + static_cast(digit - L'0');
+ if (result > std::numeric_limits::max())
+ {
+ return std::nullopt;
+ }
+ }
+
+ return static_cast(result);
+ }
+
+ std::string FormatDockerTmpfsSize(int64_t sizeBytes)
+ {
+ for (const auto& [suffix, divisor] : std::array, 3>{{{'g', 1LL << 30}, {'m', 1LL << 20}, {'k', 1LL << 10}}})
+ {
+ if ((sizeBytes % divisor) == 0)
+ {
+ return std::format("{}{}", sizeBytes / divisor, suffix);
+ }
+ }
+
+ return std::to_string(sizeBytes);
+ }
+
+} // namespace
+
+Spec ParseDockerMountString(const std::wstring& value)
+{
+ const auto fields = SplitCsvFields(value);
+ if (!fields.has_value())
+ {
+ ThrowParse(Localization::WSLCCLI_MountMalformedCsvError());
+ }
+
+ DockerMountSpec mount;
+
+ for (const auto& field : *fields)
+ {
+ const auto keyValue = SplitKeyValue(field);
+ const auto key = AsciiToLower(std::wstring_view(keyValue.Key));
+ const auto definition = FindField(key);
+ if (definition == nullptr)
+ {
+ if (!keyValue.HadSeparator)
+ {
+ ThrowParse(Localization::WSLCCLI_MountFieldKeyValueRequiredError(field));
+ }
+
+ ThrowParse(Localization::WSLCCLI_MountUnexpectedKeyError(key, field));
+ }
+
+ if (!keyValue.HadSeparator && !definition->AllowsBareForm)
+ {
+ ThrowParse(Localization::WSLCCLI_MountFieldKeyValueRequiredError(field));
+ }
+
+ switch (definition->OptionFamily)
+ {
+ case Family::General:
+ break;
+ case Family::Bind:
+ mount.HasBindOptions = true;
+ break;
+ case Family::Volume:
+ mount.HasVolumeOptions = true;
+ break;
+ case Family::Tmpfs:
+ mount.HasTmpfsOptions = true;
+ break;
+ }
+
+ switch (definition->Id)
+ {
+ case Field::Type:
+ mount.Type = AsciiToLower(std::wstring_view(keyValue.Value));
+ break;
+
+ case Field::Source:
+ mount.Source = keyValue.Value;
+ if (mount.Source == L"." || mount.Source.starts_with(L".\\"))
+ {
+ std::error_code error;
+ auto absolutePath = std::filesystem::absolute(mount.Source, error);
+ if (!error)
+ {
+ mount.Source = absolutePath.lexically_normal().wstring();
+ }
+ }
+ break;
+
+ case Field::Target:
+ mount.Target = keyValue.Value;
+ break;
+
+ case Field::ReadOnly:
+ if (!keyValue.HadSeparator)
+ {
+ mount.ReadOnly = true;
+ break;
+ }
+
+ if (const auto parsed = ParseBool(keyValue.Value.c_str(), true); parsed.has_value())
+ {
+ mount.ReadOnly = parsed.value();
+ }
+ else
+ {
+ ThrowParse(Localization::WSLCCLI_MountInvalidValueError(key, keyValue.Value));
+ }
+ break;
+
+ case Field::Consistency:
+ break;
+
+ case Field::BindPropagation:
+ mount.BindPropagation = AsciiToLower(std::wstring_view(keyValue.Value));
+ break;
+
+ case Field::BindNonRecursive:
+ if (keyValue.HadSeparator && !ParseBool(keyValue.Value.c_str(), true).has_value())
+ {
+ ThrowParse(Localization::WSLCCLI_MountInvalidValueError(key, keyValue.Value));
+ }
+
+ break;
+
+ case Field::BindRecursive:
+ if (keyValue.Value == L"enabled")
+ {
+ break;
+ }
+
+ RecordUnsupportedOption(mount, key);
+ if (keyValue.Value == L"disabled")
+ {
+ break;
+ }
+ if (keyValue.Value == L"writable")
+ {
+ mount.BindReadOnlyNonRecursive = true;
+ break;
+ }
+ if (keyValue.Value == L"readonly")
+ {
+ mount.BindReadOnlyForceRecursive = true;
+ break;
+ }
+
+ ThrowParse(Localization::WSLCCLI_MountInvalidBindRecursiveValueError(key, keyValue.Value));
+
+ case Field::VolumeNoCopy:
+ if (keyValue.HadSeparator && !ParseBool(keyValue.Value.c_str(), true).has_value())
+ {
+ ThrowParse(Localization::WSLCCLI_MountInvalidValueError(L"volume-nocopy", keyValue.Value));
+ }
+
+ break;
+
+ case Field::VolumeLabel:
+ case Field::VolumeDriver:
+ case Field::VolumeOption:
+ break;
+
+ case Field::TmpfsSize:
+ mount.TmpfsSizeBytes = ParseDockerRamInBytes(keyValue.Value);
+ if (!mount.TmpfsSizeBytes.has_value())
+ {
+ ThrowParse(Localization::WSLCCLI_MountInvalidValueError(key, keyValue.Value));
+ }
+
+ break;
+
+ case Field::TmpfsMode:
+ mount.TmpfsMode = ParseDockerTmpfsMode(keyValue.Value);
+ if (!mount.TmpfsMode.has_value())
+ {
+ ThrowParse(Localization::WSLCCLI_MountInvalidValueError(key, keyValue.Value));
+ }
+
+ break;
+ }
+
+ if (definition->SupportLevel == Support::Unsupported)
+ {
+ RecordUnsupportedOption(mount, key);
+ }
+ }
+
+ if (mount.Type.empty())
+ {
+ ThrowParse(Localization::WSLCCLI_MountTypeRequiredError());
+ }
+
+ if (mount.HasVolumeOptions && mount.Type != L"volume")
+ {
+ ThrowParse(Localization::WSLCCLI_MountOptionFamilyMismatchError(L"volume-*", mount.Type));
+ }
+ if (mount.HasBindOptions && mount.Type != L"bind")
+ {
+ ThrowParse(Localization::WSLCCLI_MountOptionFamilyMismatchError(L"bind-*", mount.Type));
+ }
+ if (mount.HasTmpfsOptions && mount.Type != L"tmpfs")
+ {
+ ThrowParse(Localization::WSLCCLI_MountOptionFamilyMismatchError(L"tmpfs-*", mount.Type));
+ }
+
+ if (mount.BindReadOnlyNonRecursive && !mount.ReadOnly)
+ {
+ ThrowParse(Localization::WSLCCLI_MountOptionRequiresReadonlyError(L"bind-recursive=writable"));
+ }
+ if (mount.BindReadOnlyForceRecursive)
+ {
+ if (!mount.ReadOnly)
+ {
+ ThrowParse(Localization::WSLCCLI_MountOptionRequiresReadonlyError(L"bind-recursive=readonly"));
+ }
+ if (mount.BindPropagation != L"rprivate")
+ {
+ ThrowParse(Localization::WSLCCLI_MountBindRecursiveReadonlyRequiresPropagationError());
+ }
+ }
+
+ Type type;
+ if (mount.Type == L"bind")
+ {
+ type = Type::Bind;
+ }
+ else if (mount.Type == L"volume")
+ {
+ type = Type::Volume;
+ }
+ else if (mount.Type == L"tmpfs")
+ {
+ type = Type::Tmpfs;
+ }
+ else
+ {
+ ThrowUnsupported(Localization::WSLCCLI_MountTypeUnsupportedError(mount.Type));
+ }
+
+ if (mount.UnsupportedOption.has_value())
+ {
+ ThrowUnsupported(Localization::WSLCCLI_MountOptionUnsupportedError(mount.UnsupportedOption.value()));
+ }
+
+ return {
+ .MountType = type,
+ .Source = std::move(mount.Source),
+ .Target = WideToMultiByte(mount.Target),
+ .ReadOnly = mount.ReadOnly,
+ .TmpfsSizeBytes = mount.TmpfsSizeBytes,
+ .TmpfsMode = mount.TmpfsMode,
+ };
+}
+
+Spec ParseDockerVolumeString(const std::wstring& value)
+{
+ const auto formatUsage = Localization::WSLCCLI_VolumeFormatUsage();
+ const auto lastColon = value.rfind(L':');
+ if (lastColon == std::wstring::npos)
+ {
+ ThrowParse(Localization::WSLCCLI_VolumeInvalidSpec(value, formatUsage));
+ }
+
+ auto splitColon = lastColon;
+ bool readOnly = false;
+ std::wstring_view lastToken{value.data() + lastColon + 1, value.size() - lastColon - 1};
+ if (lastToken == L"ro" || lastToken == L"rw")
+ {
+ readOnly = lastToken == L"ro";
+ if (lastColon == 0)
+ {
+ ThrowParse(Localization::WSLCCLI_VolumeInvalidSpec(value, formatUsage));
+ }
+
+ splitColon = value.rfind(L':', lastColon - 1);
+ if (splitColon == std::wstring::npos)
+ {
+ ThrowParse(Localization::WSLCCLI_VolumeInvalidSpec(value, formatUsage));
+ }
+ }
+
+ const auto targetEnd = lastToken == L"ro" || lastToken == L"rw" ? lastColon : value.size();
+ const auto target = value.substr(splitColon + 1, targetEnd - splitColon - 1);
+ if (target.empty())
+ {
+ ThrowParse(Localization::WSLCCLI_VolumeContainerPathEmpty(value, formatUsage));
+ }
+
+ if (target.front() != L'/')
+ {
+ ThrowParse(Localization::WSLCCLI_VolumeContainerPathNotAbsolute(value, formatUsage));
+ }
+
+ const auto rawSource = value.substr(0, splitColon);
+ if (rawSource.empty())
+ {
+ ThrowParse(Localization::WSLCCLI_VolumeHostPathEmpty(value, formatUsage));
+ }
+
+ if (IsValidNamedVolumeName(rawSource))
+ {
+ return {
+ .MountType = Type::Volume,
+ .Source = rawSource,
+ .Target = WideToMultiByte(target),
+ .ReadOnly = readOnly,
+ };
+ }
+
+ std::wstring source;
+ if (FAILED(wil::GetFullPathNameW(rawSource.c_str(), source)))
+ {
+ ThrowParse(Localization::WSLCCLI_VolumeHostPathInvalid(value, rawSource));
+ }
+
+ if (GetFileAttributesW(source.c_str()) == INVALID_FILE_ATTRIBUTES && GetLastError() == ERROR_INVALID_NAME)
+ {
+ ThrowParse(Localization::WSLCCLI_VolumeHostPathInvalid(value, rawSource));
+ }
+
+ return {
+ .MountType = Type::Bind,
+ .Source = std::move(source),
+ .Target = WideToMultiByte(target),
+ .ReadOnly = readOnly,
+ .BindSource = BindSourcePolicy::CreateIfMissing,
+ };
+}
+
+Spec ParseDockerTmpfsString(const std::wstring& value)
+{
+ const auto colon = value.find(L':');
+ const auto target = value.substr(0, colon);
+ const auto options = colon == std::wstring::npos ? std::wstring_view{} : std::wstring_view{value}.substr(colon + 1);
+
+ return {
+ .MountType = Type::Tmpfs,
+ .Target = WideToMultiByte(target),
+ .TmpfsOptions = WideToMultiByte(std::wstring{options}),
+ };
+}
+
+void ValidateMountSpec(const Spec& mount)
+{
+ if (mount.Target.empty())
+ {
+ ThrowValidation(Localization::WSLCCLI_MountTargetRequiredError());
+ }
+
+ if (!mount.Target.starts_with('/'))
+ {
+ ThrowValidation(Localization::WSLCCLI_MountTargetAbsoluteError());
+ }
+
+ if (mount.MountType != Type::Tmpfs && (mount.TmpfsSizeBytes.has_value() || mount.TmpfsMode.has_value() || mount.TmpfsOptions.has_value()))
+ {
+ ThrowValidation(Localization::WSLCCLI_MountTmpfsOptionsTypeError());
+ }
+
+ if (mount.TmpfsSizeBytes.has_value() && mount.TmpfsSizeBytes.value() < 0)
+ {
+ ThrowValidation(Localization::WSLCCLI_MountTmpfsSizeNegativeError());
+ }
+
+ switch (mount.MountType)
+ {
+ case Type::Bind:
+ if (mount.Source.empty())
+ {
+ ThrowValidation(Localization::WSLCCLI_MountSourceRequiredError());
+ }
+
+ if (!std::filesystem::path(mount.Source).is_absolute())
+ {
+ ThrowValidation(Localization::WSLCCLI_MountBindSourceAbsoluteError());
+ }
+ break;
+
+ case Type::Volume:
+ if (!mount.Source.empty() && !IsValidNamedVolumeName(mount.Source))
+ {
+ ThrowValidation(Localization::WSLCCLI_MountVolumeSourceInvalidError());
+ }
+ break;
+
+ case Type::Tmpfs:
+ if (!mount.Source.empty())
+ {
+ ThrowValidation(Localization::WSLCCLI_MountTmpfsSourceUnsupportedError());
+ }
+ break;
+
+ default:
+ ThrowUnsupported(Localization::WSLCCLI_MountTypeUnsupportedGenericError());
+ }
+}
+
+void ValidateMountCollection(std::span mounts)
+{
+ std::unordered_set destinations;
+ for (const auto& mount : mounts)
+ {
+ ValidateMountSpec(mount);
+
+ auto destination = NormalizeDestination(mount.Target);
+ if (!destinations.emplace(destination).second)
+ {
+ throw MountValidationException(
+ ValidationError::DuplicateDestination,
+ Localization::WSLCCLI_DuplicateMountDestinationError(MultiByteToWide(destination)),
+ std::move(destination));
+ }
+ }
+}
+
+std::string FormatTmpfsOptions(const Spec& mount)
+{
+ WI_ASSERT(mount.MountType == Type::Tmpfs);
+
+ if (mount.TmpfsOptions.has_value())
+ {
+ return mount.TmpfsOptions.value();
+ }
+
+ std::vector options;
+ if (mount.ReadOnly)
+ {
+ options.emplace_back("ro");
+ }
+ if (mount.TmpfsMode.has_value() && mount.TmpfsMode.value() != 0)
+ {
+ options.emplace_back(std::format("mode={:o}", mount.TmpfsMode.value()));
+ }
+ if (mount.TmpfsSizeBytes.has_value() && mount.TmpfsSizeBytes.value() != 0)
+ {
+ options.emplace_back(std::format("size={}", FormatDockerTmpfsSize(mount.TmpfsSizeBytes.value())));
+ }
+
+ return wsl::shared::string::Join(options, ',');
+}
+
+std::string NormalizeDestination(std::string destination)
+{
+ std::replace(destination.begin(), destination.end(), '\\', '/');
+
+ std::vector components;
+ size_t start = 0;
+ while (start <= destination.size())
+ {
+ const auto end = destination.find('/', start);
+ const auto component = destination.substr(start, end - start);
+ if (!component.empty() && component != ".")
+ {
+ if (component == "..")
+ {
+ if (!components.empty())
+ {
+ components.pop_back();
+ }
+ }
+ else
+ {
+ components.emplace_back(component);
+ }
+ }
+
+ if (end == std::string::npos)
+ {
+ break;
+ }
+
+ start = end + 1;
+ }
+
+ std::string result = "/";
+ for (const auto& component : components)
+ {
+ if (result.size() > 1)
+ {
+ result += '/';
+ }
+
+ result += component;
+ }
+
+ return result;
+}
+
+bool IsValidNamedVolumeName(std::wstring_view name)
+{
+ static const std::wregex c_namedVolumeRegex(LR"(^[a-zA-Z0-9][a-zA-Z0-9_.-]{1,}$)");
+ return std::regex_match(name.begin(), name.end(), c_namedVolumeRegex);
+}
+
+} // namespace wsl::windows::common::mount
diff --git a/src/windows/common/MountSpecParsing.h b/src/windows/common/MountSpecParsing.h
new file mode 100644
index 000000000..84f76aada
--- /dev/null
+++ b/src/windows/common/MountSpecParsing.h
@@ -0,0 +1,125 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ MountSpecParsing.h
+
+Abstract:
+
+ Docker-compatible mount specification parsing.
+
+--*/
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace wsl::windows::common::mount {
+
+inline constexpr std::string_view c_dockerCliMountGrammarVersion = "25.0.3";
+
+enum class Type
+{
+ Bind,
+ Volume,
+ Tmpfs,
+};
+
+enum class BindSourcePolicy
+{
+ RequireExisting,
+ CreateIfMissing,
+};
+
+struct Spec
+{
+ Type MountType = Type::Volume;
+ std::wstring Source;
+ std::string Target;
+ bool ReadOnly = false;
+ BindSourcePolicy BindSource = BindSourcePolicy::RequireExisting;
+ std::optional TmpfsSizeBytes;
+ std::optional TmpfsMode;
+ std::optional TmpfsOptions;
+};
+
+enum class ValidationError
+{
+ InvalidSpecification,
+ DuplicateDestination,
+};
+
+class MountException : public std::exception
+{
+public:
+ explicit MountException(std::wstring reason) : m_reason(std::move(reason))
+ {
+ }
+
+ MountException(ValidationError error, std::wstring reason, std::string destination) :
+ m_error(error), m_reason(std::move(reason)), m_destination(std::move(destination))
+ {
+ }
+
+ const char* what() const noexcept override
+ {
+ return "mount error";
+ }
+
+ const std::wstring& Reason() const noexcept
+ {
+ return m_reason;
+ }
+
+ ValidationError Error() const noexcept
+ {
+ return m_error;
+ }
+
+ const std::string& Destination() const noexcept
+ {
+ return m_destination;
+ }
+
+private:
+ ValidationError m_error = ValidationError::InvalidSpecification;
+ std::wstring m_reason;
+ std::string m_destination;
+};
+
+class MountParseException : public MountException
+{
+public:
+ using MountException::MountException;
+};
+
+class MountUnsupportedException : public MountException
+{
+public:
+ using MountException::MountException;
+};
+
+class MountValidationException : public MountException
+{
+public:
+ using MountException::MountException;
+};
+
+Spec ParseDockerMountString(const std::wstring& value);
+Spec ParseDockerVolumeString(const std::wstring& value);
+Spec ParseDockerTmpfsString(const std::wstring& value);
+void ValidateMountSpec(const Spec& mount);
+void ValidateMountCollection(std::span mounts);
+std::string FormatTmpfsOptions(const Spec& mount);
+std::string NormalizeDestination(std::string destination);
+bool IsValidNamedVolumeName(std::wstring_view name);
+
+} // namespace wsl::windows::common::mount
diff --git a/src/windows/common/WSLCContainerLauncher.cpp b/src/windows/common/WSLCContainerLauncher.cpp
index 57a96e3d9..59e488d68 100644
--- a/src/windows/common/WSLCContainerLauncher.cpp
+++ b/src/windows/common/WSLCContainerLauncher.cpp
@@ -229,29 +229,74 @@ void WSLCContainerLauncher::AddUlimit(const std::string& Name, std::int64_t Soft
void wsl::windows::common::WSLCContainerLauncher::AddVolume(const std::wstring& HostPath, const std::string& ContainerPath, bool ReadOnly)
{
- // Store a copy of the path strings to the launcher to ensure the pointers in WSLCVolume remain valid.
- const auto& hostPath = m_hostPaths.emplace_back(HostPath);
- const auto& containerPath = m_containerPaths.emplace_back(ContainerPath);
-
- WSLCVolume vol{};
- vol.HostPath = hostPath.c_str();
- vol.ContainerPath = containerPath.c_str();
- vol.ReadOnly = ReadOnly ? TRUE : FALSE;
-
- m_volumes.push_back(vol);
+ AddMount({
+ .MountType = mount::Type::Bind,
+ .Source = HostPath,
+ .Target = ContainerPath,
+ .ReadOnly = ReadOnly,
+ .BindSource = mount::BindSourcePolicy::CreateIfMissing,
+ });
}
void wsl::windows::common::WSLCContainerLauncher::AddNamedVolume(const std::string& Name, const std::string& ContainerPath, bool ReadOnly)
{
- const auto& name = m_volumeNames.emplace_back(Name);
- const auto& containerPath = m_containerPaths.emplace_back(ContainerPath);
+ AddMount({
+ .MountType = mount::Type::Volume,
+ .Source = wsl::shared::string::MultiByteToWide(Name),
+ .Target = ContainerPath,
+ .ReadOnly = ReadOnly,
+ });
+}
+
+void wsl::windows::common::WSLCContainerLauncher::AddMount(const mount::Spec& Mount)
+{
+ WSLCMountSpec mount{};
+ switch (Mount.MountType)
+ {
+ case mount::Type::Bind:
+ mount.Type = WSLCMountTypeBind;
+ break;
+
+ case mount::Type::Volume:
+ mount.Type = WSLCMountTypeVolume;
+ break;
- WSLCNamedVolume volume{};
- volume.Name = name.c_str();
- volume.ContainerPath = containerPath.c_str();
- volume.ReadOnly = ReadOnly ? TRUE : FALSE;
+ case mount::Type::Tmpfs:
+ mount.Type = WSLCMountTypeTmpfs;
+ break;
+ }
+
+ if (!Mount.Source.empty())
+ {
+ mount.Source = m_mountSources.emplace_back(Mount.Source).c_str();
+ }
+
+ mount.Target = m_mountTargets.emplace_back(Mount.Target).c_str();
+ mount.ReadOnly = Mount.ReadOnly ? TRUE : FALSE;
+ if (Mount.MountType == mount::Type::Bind && Mount.BindSource == mount::BindSourcePolicy::CreateIfMissing)
+ {
+ WI_SetFlag(mount.Flags, WSLCMountSpecFlagsCreateSourceIfMissing);
+ }
- m_namedVolumes.push_back(volume);
+ if (Mount.TmpfsSizeBytes.has_value())
+ {
+ WI_SetFlag(mount.Flags, WSLCMountSpecFlagsTmpfsSize);
+ mount.TmpfsSizeBytes = Mount.TmpfsSizeBytes.value();
+ }
+
+ if (Mount.TmpfsMode.has_value())
+ {
+ WI_SetFlag(mount.Flags, WSLCMountSpecFlagsTmpfsMode);
+ mount.TmpfsMode = Mount.TmpfsMode.value();
+ }
+
+ if (Mount.TmpfsOptions.has_value())
+ {
+ WI_SetFlag(mount.Flags, WSLCMountSpecFlagsTmpfsOptions);
+ mount.TmpfsOptions = m_mountTmpfsOptions.emplace_back(Mount.TmpfsOptions.value()).c_str();
+ }
+
+ m_mounts.push_back(mount);
}
void wsl::windows::common::WSLCContainerLauncher::AddLabel(const std::string& Key, const std::string& Value)
@@ -269,15 +314,11 @@ void wsl::windows::common::WSLCContainerLauncher::AddLabel(const std::string& Ke
void wsl::windows::common::WSLCContainerLauncher::AddTmpfs(const std::string& ContainerPath, const std::string& Options)
{
- // Store a copy of the path/options strings to the launcher to ensure the pointers in WSLCTmpfsMount remain valid.
- const auto& containerPath = m_tmpfsContainerPaths.emplace_back(ContainerPath);
- const auto& options = m_tmpfsOptions.emplace_back(Options);
-
- WSLCTmpfsMount tmpfs{};
- tmpfs.Destination = containerPath.c_str();
- tmpfs.Options = options.c_str();
-
- m_tmpfsMounts.push_back(tmpfs);
+ AddMount({
+ .MountType = mount::Type::Tmpfs,
+ .Target = ContainerPath,
+ .TmpfsOptions = Options,
+ });
}
void wsl::windows::common::WSLCContainerLauncher::AddAdditionalNetwork(const std::string& Name)
@@ -412,18 +453,12 @@ std::pair> WSLCContainerLauncher::C
options.InitProcessOptions.CurrentDirectory = m_workingDirectory.c_str();
}
- options.VolumesCount = static_cast(m_volumes.size());
- options.Volumes = m_volumes.size() > 0 ? m_volumes.data() : nullptr;
-
- options.NamedVolumesCount = static_cast(m_namedVolumes.size());
- options.NamedVolumes = m_namedVolumes.size() > 0 ? m_namedVolumes.data() : nullptr;
+ options.MountsCount = static_cast(m_mounts.size());
+ options.Mounts = m_mounts.size() > 0 ? m_mounts.data() : nullptr;
options.LabelsCount = static_cast(m_labels.size());
options.Labels = m_labels.size() > 0 ? m_labels.data() : nullptr;
- options.TmpfsCount = static_cast(m_tmpfsMounts.size());
- options.Tmpfs = m_tmpfsMounts.size() > 0 ? m_tmpfsMounts.data() : nullptr;
-
options.ContainerNetwork.NetworkMode = m_networkMode.c_str();
// Each additional network becomes an entry in NetworkingConfig.EndpointsConfig.
diff --git a/src/windows/common/WSLCContainerLauncher.h b/src/windows/common/WSLCContainerLauncher.h
index 0ad00956c..52e6e2997 100644
--- a/src/windows/common/WSLCContainerLauncher.h
+++ b/src/windows/common/WSLCContainerLauncher.h
@@ -13,6 +13,7 @@ Module Name:
--*/
#pragma once
+#include "MountSpecParsing.h"
#include "WSLCProcessLauncher.h"
#include "docker_schema.h"
#include "wslc_schema.h"
@@ -59,6 +60,7 @@ class WSLCContainerLauncher : private WSLCProcessLauncher
void AddVolume(const std::wstring& HostPath, const std::string& ContainerPath, bool ReadOnly);
void AddNamedVolume(const std::string& Name, const std::string& ContainerPath, bool ReadOnly);
+ void AddMount(const mount::Spec& Mount);
void AddPort(uint16_t WindowsPort, uint16_t ContainerPort, int Family, int Protocol = IPPROTO_TCP, const std::optional& BindingAddress = {});
void AddLabel(const std::string& Key, const std::string& Value);
void AddTmpfs(const std::string& ContainerPath, const std::string& Options);
@@ -109,11 +111,10 @@ class WSLCContainerLauncher : private WSLCProcessLauncher
std::string m_image;
std::string m_name;
std::vector m_ports;
- std::vector m_volumes;
- std::vector m_namedVolumes;
- std::deque m_hostPaths;
- std::deque m_volumeNames;
- std::deque m_containerPaths;
+ std::vector m_mounts;
+ std::deque m_mountSources;
+ std::deque m_mountTargets;
+ std::deque m_mountTmpfsOptions;
std::string m_networkMode;
std::vector m_entrypoint;
WSLCSignal m_stopSignal = WSLCSignalNone;
@@ -135,9 +136,6 @@ class WSLCContainerLauncher : private WSLCProcessLauncher
std::vector m_labels;
std::deque m_labelKeys;
std::deque m_labelValues;
- std::vector m_tmpfsMounts;
- std::deque m_tmpfsContainerPaths;
- std::deque m_tmpfsOptions;
std::int64_t m_memoryBytes = 0;
std::int64_t m_nanoCpus = 0;
std::vector m_ulimits;
diff --git a/src/windows/inc/docker_schema.h b/src/windows/inc/docker_schema.h
index 4c1f23cbf..be76ebfed 100644
--- a/src/windows/inc/docker_schema.h
+++ b/src/windows/inc/docker_schema.h
@@ -239,6 +239,14 @@ inline void to_json(nlohmann::json& j, const ContainerNetworkRequest& v)
}
}
+struct MountTmpfsOptions
+{
+ std::int64_t SizeBytes{};
+ std::uint32_t Mode{};
+
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(MountTmpfsOptions, SizeBytes, Mode);
+};
+
struct Mount
{
std::string Name;
@@ -246,8 +254,9 @@ struct Mount
std::string Target;
std::string Type;
bool ReadOnly{};
+ std::optional TmpfsOptions;
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Mount, Name, Target, Source, Type, ReadOnly);
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Mount, Name, Target, Source, Type, ReadOnly, TmpfsOptions);
};
struct DeviceMapping
diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl
index 52e997346..e6d1b2be1 100644
--- a/src/windows/service/inc/wslc.idl
+++ b/src/windows/service/inc/wslc.idl
@@ -245,6 +245,37 @@ typedef struct _WSLCTmpfsMount
[unique] LPCSTR Options;
} WSLCTmpfsMount;
+typedef enum _WSLCMountType
+{
+ WSLCMountTypeBind,
+ WSLCMountTypeVolume,
+ WSLCMountTypeTmpfs,
+} WSLCMountType;
+
+typedef enum _WSLCMountSpecFlags
+{
+ WSLCMountSpecFlagsNone = 0,
+ WSLCMountSpecFlagsTmpfsSize = 1,
+ WSLCMountSpecFlagsTmpfsMode = 2,
+ WSLCMountSpecFlagsCreateSourceIfMissing = 4,
+ WSLCMountSpecFlagsTmpfsOptions = 8,
+} WSLCMountSpecFlags;
+
+cpp_quote("#define WSLCMountSpecFlagsValid (WSLCMountSpecFlagsTmpfsSize | WSLCMountSpecFlagsTmpfsMode | WSLCMountSpecFlagsCreateSourceIfMissing | WSLCMountSpecFlagsTmpfsOptions)")
+cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCMountSpecFlags);")
+
+typedef struct _WSLCMountSpec
+{
+ WSLCMountType Type;
+ [unique, string] LPCWSTR Source;
+ [string] LPCSTR Target;
+ BOOL ReadOnly;
+ WSLCMountSpecFlags Flags;
+ LONGLONG TmpfsSizeBytes;
+ ULONG TmpfsMode;
+ [unique, string] LPCSTR TmpfsOptions;
+} WSLCMountSpec;
+
typedef struct _WSLCUlimit
{
[string] LPCSTR Name;
@@ -328,6 +359,9 @@ typedef struct _WSLCContainerOptions
LONGLONG HealthTimeoutNs;
LONGLONG HealthStartPeriodNs;
LONG HealthRetries;
+
+ [unique, size_is(MountsCount)] const WSLCMountSpec* Mounts;
+ ULONG MountsCount;
} WSLCContainerOptions;
typedef char WSLCContainerId[WSLC_CONTAINER_ID_LENGTH + 1] ;
diff --git a/src/windows/wslc/arguments/ArgumentConvertedTypes.h b/src/windows/wslc/arguments/ArgumentConvertedTypes.h
index a495ff9a9..cae8b4919 100644
--- a/src/windows/wslc/arguments/ArgumentConvertedTypes.h
+++ b/src/windows/wslc/arguments/ArgumentConvertedTypes.h
@@ -18,6 +18,7 @@ Module Name:
#include "ArgumentTypes.h"
#include "ContainerModel.h"
#include "InspectModel.h"
+#include "MountSpecParsing.h"
#include "SpecParsing.h"
#include
@@ -33,6 +34,8 @@ struct BuildSecret;
namespace wsl::windows::wslc::argument::details {
+namespace mount = wsl::windows::common::mount;
+
// Local aliases so the ConvertedType tokens in the WSLC_ARGUMENTS X-macro (ArgumentDefinitions.h)
// resolve here regardless of include order. Aggregate converted types must be aliased because their
// commas would otherwise break X-macro argument parsing if written inline in the table.
@@ -47,6 +50,7 @@ using UlimitValue = std::tuple;
using KeyValuePair = std::pair;
using BuildOutput = wsl::windows::wslc::services::BuildOutput;
using BuildSecret = wsl::windows::wslc::services::BuildSecret;
+using ParsedMount = mount::Spec;
// Generate the ArgType -> converted type mapping from the X-macro. Every ArgType gets a
// specialization; arguments that are not converted map to NoConversion (their raw string is used
diff --git a/src/windows/wslc/arguments/ArgumentDefinitions.h b/src/windows/wslc/arguments/ArgumentDefinitions.h
index 618efcbaa..3a37150cb 100644
--- a/src/windows/wslc/arguments/ArgumentDefinitions.h
+++ b/src/windows/wslc/arguments/ArgumentDefinitions.h
@@ -93,7 +93,8 @@ _(Latest, "latest", L"l", Kind::Flag,
_(Link, "link", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_LinkArgDescription()) \
_(LinkLocalIp, "link-local-ip", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_LinkLocalIpArgDescription()) \
_(Memory, "memory", L"m", Kind::Value, int64_t, Localization::WSLCCLI_MemoryArgDescription()) \
-_(Name, "name", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_NameArgDescription()) \
+_(Mount, "mount", NO_ALIAS, Kind::Value, ParsedMount, Localization::WSLCCLI_MountArgDescription()) \
+_(Name, "name", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_NameArgDescription()) \
_(Network, "network", NO_ALIAS, Kind::Value, ParsedNetworkArgument, Localization::WSLCCLI_NetworkArgDescription()) \
_(NetworkAlias, "network-alias", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_NetworkAliasArgDescription()) \
_(NetworkName, "network-name", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_NetworkNameArgDescription()) \
@@ -130,7 +131,7 @@ _(Tail, "tail", L"n", Kind::Value,
_(Tag, "tag", L"t", Kind::Value, NoConversion, Localization::WSLCCLI_TagArgDescription()) \
_(Target, "target", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_TargetArgDescription()) \
_(Time, "time", L"t", Kind::Value, LONG, Localization::WSLCCLI_TimeArgDescription()) \
-_(TMPFS, "tmpfs", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_TMPFSArgDescription()) \
+_(TMPFS, "tmpfs", NO_ALIAS, Kind::Value, ParsedMount, Localization::WSLCCLI_TMPFSArgDescription()) \
_(TTY, "tty", L"t", Kind::Flag, NoConversion, Localization::WSLCCLI_TTYArgDescription()) \
_(Type, "type", L"t", Kind::Value, InspectType, Localization::WSLCCLI_TypeArgDescription()) \
_(Ulimit, "ulimit", NO_ALIAS, Kind::Value, UlimitValue, Localization::WSLCCLI_UlimitArgDescription()) \
@@ -139,7 +140,7 @@ _(Username, "username", L"u", Kind::Value,
_(Verbose, "verbose", NO_ALIAS, Kind::Flag, NoConversion, Localization::WSLCCLI_VerboseArgDescription()) \
_(Version, "version", L"v", Kind::Flag, NoConversion, Localization::WSLCCLI_VersionArgDescription()) \
/*_(Virtual, "virtualization", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_VirtualArgDescription())*/ \
-_(Volume, "volume", L"v", Kind::Value, NoConversion, Localization::WSLCCLI_VolumeArgDescription()) \
+_(Volume, "volume", L"v", Kind::Value, ParsedMount, Localization::WSLCCLI_VolumeArgDescription()) \
_(VolumeName, "volume-name", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_VolumeNameArgDescription()) \
_(Volumes, "volumes", L"v", Kind::Flag, NoConversion, Localization::WSLCCLI_RemoveVolumesArgDescription()) \
_(WorkDir, "workdir", L"w", Kind::Value, NoConversion, Localization::WSLCCLI_WorkingDirArgDescription()) \
diff --git a/src/windows/wslc/arguments/ArgumentValidation.cpp b/src/windows/wslc/arguments/ArgumentValidation.cpp
index 61e9a0393..c919844ee 100644
--- a/src/windows/wslc/arguments/ArgumentValidation.cpp
+++ b/src/windows/wslc/arguments/ArgumentValidation.cpp
@@ -20,6 +20,7 @@ Module Name:
#include "Exceptions.h"
#include "ImageService.h"
#include "Localization.h"
+#include "MountSpecParsing.h"
#include
#include
#include
@@ -31,6 +32,8 @@ using namespace wsl::shared::string;
namespace wsl::windows::wslc {
+namespace mount = wsl::windows::common::mount;
+
namespace argument::details {
struct RawArgMapAccess
{
@@ -228,7 +231,52 @@ void Argument::Validate(ArgMap& execArgs) const
break;
case ArgType::Volume:
- validation::ValidateVolumeMount(RawArgMapAccess::GetAll(execArgs));
+ CacheConverted(execArgs, m_name, [](const std::wstring& value, const std::wstring&) {
+ try
+ {
+ auto mountSpec = mount::ParseDockerVolumeString(value);
+ mount::ValidateMountSpec(mountSpec);
+ return mountSpec;
+ }
+ catch (const mount::MountException& ex)
+ {
+ throw ArgumentException(ex.Reason());
+ }
+ });
+ break;
+
+ case ArgType::TMPFS:
+ CacheConverted(execArgs, m_name, [](const std::wstring& value, const std::wstring&) {
+ try
+ {
+ auto mountSpec = mount::ParseDockerTmpfsString(value);
+ mount::ValidateMountSpec(mountSpec);
+ return mountSpec;
+ }
+ catch (const mount::MountException& ex)
+ {
+ throw ArgumentException(ex.Reason());
+ }
+ });
+ break;
+
+ case ArgType::Mount:
+ CacheConverted(execArgs, m_name, [](const std::wstring& value, const std::wstring&) {
+ try
+ {
+ auto mountSpec = mount::ParseDockerMountString(value);
+ mount::ValidateMountSpec(mountSpec);
+ return mountSpec;
+ }
+ catch (const mount::MountUnsupportedException& ex)
+ {
+ throw ArgumentException(Localization::WSLCCLI_UnsupportedMountError(value, ex.Reason()));
+ }
+ catch (const mount::MountException& ex)
+ {
+ throw ArgumentException(Localization::WSLCCLI_InvalidMountError(value, ex.Reason()));
+ }
+ });
break;
case ArgType::WorkDir:
@@ -304,14 +352,6 @@ void ValidateWSLCSignalFromString(const std::vector& values, const
}
}
-void ValidateVolumeMount(const std::vector& values)
-{
- for (const auto& value : values)
- {
- std::ignore = models::VolumeMount::Parse(value);
- }
-}
-
// Validates that each --filter argument is in the form "key=value". Rejects entries without an '=';
// the runtime validates the key and value for specific objects.
void ValidateFilter(const std::vector& values)
diff --git a/src/windows/wslc/arguments/ArgumentValidation.h b/src/windows/wslc/arguments/ArgumentValidation.h
index 880d344ae..f0b84f626 100644
--- a/src/windows/wslc/arguments/ArgumentValidation.h
+++ b/src/windows/wslc/arguments/ArgumentValidation.h
@@ -78,7 +78,6 @@ void ValidateFormatTypeFromString(const std::vector& values, const
void ValidateGpus(const std::vector& values, const std::wstring& argName);
-void ValidateVolumeMount(const std::vector& values);
void ValidateFilter(const std::vector& values);
} // namespace wsl::windows::wslc::validation
diff --git a/src/windows/wslc/commands/ContainerCreateCommand.cpp b/src/windows/wslc/commands/ContainerCreateCommand.cpp
index 89125ac0c..04f5ffa06 100644
--- a/src/windows/wslc/commands/ContainerCreateCommand.cpp
+++ b/src/windows/wslc/commands/ContainerCreateCommand.cpp
@@ -52,6 +52,7 @@ std::vector ContainerCreateCommand::GetArguments() const
Argument::Create(ArgType::Interactive),
Argument::Create(ArgType::Label, false, Limit::Unlimited),
Argument::Create(ArgType::Memory),
+ Argument::Create(ArgType::Mount, false, Limit::Unlimited),
Argument::Create(ArgType::Name),
Argument::Create(ArgType::Network, false, Limit::Unlimited),
Argument::Create(ArgType::NetworkAlias, false, Limit::Unlimited),
diff --git a/src/windows/wslc/commands/ContainerRunCommand.cpp b/src/windows/wslc/commands/ContainerRunCommand.cpp
index c636e57c9..33b93e359 100644
--- a/src/windows/wslc/commands/ContainerRunCommand.cpp
+++ b/src/windows/wslc/commands/ContainerRunCommand.cpp
@@ -52,6 +52,7 @@ std::vector ContainerRunCommand::GetArguments() const
Argument::Create(ArgType::Interactive),
Argument::Create(ArgType::Label, false, Limit::Unlimited),
Argument::Create(ArgType::Memory),
+ Argument::Create(ArgType::Mount, false, Limit::Unlimited),
Argument::Create(ArgType::Name),
Argument::Create(ArgType::Network, false, Limit::Unlimited),
Argument::Create(ArgType::NetworkAlias, false, Limit::Unlimited),
diff --git a/src/windows/wslc/services/ContainerModel.cpp b/src/windows/wslc/services/ContainerModel.cpp
index baaaa562f..d1b3018b9 100644
--- a/src/windows/wslc/services/ContainerModel.cpp
+++ b/src/windows/wslc/services/ContainerModel.cpp
@@ -13,6 +13,7 @@ Module Name:
#include "precomp.h"
#include "ContainerModel.h"
+#include
namespace wsl::windows::wslc::models {
@@ -155,89 +156,28 @@ void PublishPort::Validate() const
// Source: https://github.com/moby/moby/blob/master/volume/validate.go
bool VolumeMount::IsValidNamedVolumeName(const std::wstring& name)
{
- static const std::wregex namedVolumeRegex(LR"(^[a-zA-Z0-9][a-zA-Z0-9_.-]{1,}$)");
- return std::regex_match(name, namedVolumeRegex);
+ return mount::IsValidNamedVolumeName(name);
}
VolumeMount VolumeMount::Parse(const std::wstring& value)
{
- auto lastColon = value.rfind(':');
- if (lastColon == std::wstring::npos)
+ mount::Spec mountSpec;
+ try
{
- THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_VolumeInvalidSpec(value, Localization::WSLCCLI_VolumeFormatUsage()));
+ mountSpec = mount::ParseDockerVolumeString(value);
+ mount::ValidateMountSpec(mountSpec);
}
-
- VolumeMount vm;
- auto splitColon = lastColon;
- const auto lastToken = value.substr(lastColon + 1);
- if (IsValidMode(lastToken))
- {
- vm.m_isReadOnlyMode = IsReadOnlyMode(lastToken);
- if (lastColon == 0)
- {
- THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_VolumeInvalidSpec(value, Localization::WSLCCLI_VolumeFormatUsage()));
- }
-
- splitColon = value.rfind(':', lastColon - 1);
- if (splitColon == std::wstring::npos)
- {
- THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_VolumeInvalidSpec(value, Localization::WSLCCLI_VolumeFormatUsage()));
- }
-
- vm.m_containerPath = WideToMultiByte(value.substr(splitColon + 1, lastColon - splitColon - 1));
- }
- else
- {
- vm.m_containerPath = WideToMultiByte(lastToken);
- }
-
- if (vm.m_containerPath.empty())
- {
- THROW_HR_WITH_USER_ERROR(
- E_INVALIDARG, Localization::WSLCCLI_VolumeContainerPathEmpty(value, Localization::WSLCCLI_VolumeFormatUsage()));
- }
-
- if (vm.m_containerPath[0] != '/')
- {
- THROW_HR_WITH_USER_ERROR(
- E_INVALIDARG, Localization::WSLCCLI_VolumeContainerPathNotAbsolute(value, Localization::WSLCCLI_VolumeFormatUsage()));
- }
-
- const auto rawHostPath = value.substr(0, splitColon);
- if (rawHostPath.empty())
+ catch (const mount::MountException& ex)
{
- THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_VolumeHostPathEmpty(value, Localization::WSLCCLI_VolumeFormatUsage()));
+ THROW_HR_WITH_USER_ERROR(E_INVALIDARG, ex.Reason());
}
- // This is where we need to check if the user is referencing a named volume.
- // This can be either an existing named volume or a new named volume that will be created.
- if (VolumeMount::IsValidNamedVolumeName(rawHostPath))
- {
- vm.m_isNamedVolume = true;
- vm.m_host = rawHostPath;
- }
- else
- {
- // Not a named volume, so it must be a path.
- // Use wil::GetFullPathNameW to resolve relative paths against the CWD.
- std::wstring resolvedHostPath;
- const auto hr = wil::GetFullPathNameW(rawHostPath.c_str(), resolvedHostPath);
- if (FAILED(hr))
- {
- THROW_HR_WITH_USER_ERROR(hr, Localization::WSLCCLI_VolumeHostPathInvalid(value, rawHostPath));
- }
-
- // GetFileAttributesW validates the resolved path syntax without requiring existence.
- // ERROR_INVALID_NAME indicates illegal characters in the path (e.g. ":" as a component).
- if (GetFileAttributesW(resolvedHostPath.c_str()) == INVALID_FILE_ATTRIBUTES && GetLastError() == ERROR_INVALID_NAME)
- {
- THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_VolumeHostPathInvalid(value, rawHostPath));
- }
-
- vm.m_host = std::move(resolvedHostPath);
- }
-
- return vm;
+ VolumeMount volume;
+ volume.m_host = std::move(mountSpec.Source);
+ volume.m_containerPath = std::move(mountSpec.Target);
+ volume.m_isReadOnlyMode = mountSpec.ReadOnly;
+ volume.m_isNamedVolume = mountSpec.MountType == mount::Type::Volume;
+ return volume;
}
std::optional EnvironmentVariable::Parse(const std::wstring& entry)
@@ -318,19 +258,21 @@ std::vector EnvironmentVariable::ParseFile(const std::wstring& fil
return envVars;
}
-TmpfsMount TmpfsMount::Parse(const std::string& value)
+void ValidateUniqueMountDestinations(const ContainerOptions& options)
{
- TmpfsMount result{};
- auto colonPos = value.find(':');
- if (colonPos == std::string::npos)
+ try
{
- result.m_containerPath = value;
- return result;
+ 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())));
+ }
- result.m_containerPath = value.substr(0, colonPos);
- result.m_options = value.substr(colonPos + 1);
- return result;
+ throw;
+ }
}
CidFile::CidFile(const std::optional& path)
diff --git a/src/windows/wslc/services/ContainerModel.h b/src/windows/wslc/services/ContainerModel.h
index 3db13fde4..964ea2f19 100644
--- a/src/windows/wslc/services/ContainerModel.h
+++ b/src/windows/wslc/services/ContainerModel.h
@@ -14,6 +14,7 @@ Module Name:
#pragma once
+#include "MountSpecParsing.h"
#include
#include
#include
@@ -22,6 +23,8 @@ Module Name:
namespace wsl::windows::wslc::models {
+namespace mount = wsl::windows::common::mount;
+
// Valid formats for container list output.
enum class FormatType
{
@@ -73,7 +76,7 @@ struct ContainerOptions
bool NoHealthcheck = false;
bool Gpu = false;
std::vector Ports;
- std::vector Volumes;
+ std::vector Mounts;
std::string WorkingDirectory;
std::vector Entrypoint;
std::optional User{};
@@ -84,7 +87,6 @@ struct ContainerOptions
std::vector DnsOptions;
std::vector Networks;
std::vector NetworkAliases;
- std::vector Tmpfs;
std::vector> Labels;
std::optional CidFile{};
std::optional MemoryBytes{};
@@ -305,34 +307,9 @@ struct VolumeMount
std::string m_containerPath;
bool m_isReadOnlyMode = false;
bool m_isNamedVolume = false;
-
- static bool IsReadOnlyMode(const std::wstring& mode)
- {
- return mode == L"ro";
- }
-
- static bool IsValidMode(const std::wstring& mode)
- {
- return IsReadOnlyMode(mode) || mode == L"rw";
- }
};
-struct TmpfsMount
-{
- std::string ContainerPath() const
- {
- return m_containerPath;
- }
- std::string Options() const
- {
- return m_options;
- }
- static TmpfsMount Parse(const std::string& value);
-
-private:
- std::string m_containerPath;
- std::string m_options;
-};
+void ValidateUniqueMountDestinations(const ContainerOptions& options);
class CidFile
{
diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp
index d59ce98dc..bcdc969c9 100644
--- a/src/windows/wslc/services/ContainerService.cpp
+++ b/src/windows/wslc/services/ContainerService.cpp
@@ -29,6 +29,8 @@ Module Name:
#include
namespace wsl::windows::wslc::services {
+namespace mount = wsl::windows::common::mount;
+
using wsl::windows::common::ClientRunningWSLCProcess;
using wsl::windows::common::wslc_schema::InspectContainer;
using namespace wsl::windows::common::wslutil;
@@ -148,20 +150,9 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(Terminal& termi
}
}
- // Add volumes if specified
- for (const auto& volumeSpec : options.Volumes)
+ for (const auto& mountSpec : options.Mounts)
{
- auto volume = VolumeMount::Parse(volumeSpec);
- auto host = volume.Host();
- auto container = volume.ContainerPath();
- if (volume.IsNamedVolume())
- {
- containerLauncher.AddNamedVolume(string::WideToMultiByte(host), container, volume.IsReadOnly());
- }
- else
- {
- containerLauncher.AddVolume(host, container, volume.IsReadOnly());
- }
+ containerLauncher.AddMount(mountSpec);
}
containerLauncher.SetContainerFlags(containerFlags);
@@ -268,12 +259,6 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(Terminal& termi
containerLauncher.SetDnsOptions(std::vector(options.DnsOptions));
}
- for (const auto& tmpfsSpec : options.Tmpfs)
- {
- auto tmpfsMount = TmpfsMount::Parse(tmpfsSpec);
- containerLauncher.AddTmpfs(tmpfsMount.ContainerPath(), tmpfsMount.Options());
- }
-
for (const auto& [key, value] : options.Labels)
{
containerLauncher.AddLabel(key, value);
diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp
index 0cf57fdda..cb5b1cde5 100644
--- a/src/windows/wslc/tasks/ContainerTasks.cpp
+++ b/src/windows/wslc/tasks/ContainerTasks.cpp
@@ -19,6 +19,7 @@ Module Name:
#include "ContainerService.h"
#include "ContainerTasks.h"
#include "ImageModel.h"
+#include "MountSpecParsing.h"
#include "SessionModel.h"
#include "SessionService.h"
#include "TableOutput.h"
@@ -658,11 +659,13 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
if (context.Args.Contains(ArgType::Volume))
{
auto volumes = context.Args.GetAllValues();
- options.Volumes.reserve(options.Volumes.size() + volumes.size());
- for (const auto& volume : volumes)
- {
- options.Volumes.emplace_back(volume);
- }
+ options.Mounts.insert(options.Mounts.end(), std::make_move_iterator(volumes.begin()), std::make_move_iterator(volumes.end()));
+ }
+
+ if (context.Args.Contains(ArgType::Mount))
+ {
+ auto mounts = context.Args.GetAllValues();
+ options.Mounts.insert(options.Mounts.end(), std::make_move_iterator(mounts.begin()), std::make_move_iterator(mounts.end()));
}
options.Remove = context.Args.GetValue();
@@ -827,13 +830,11 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
if (context.Args.Contains(ArgType::TMPFS))
{
auto tmpfs = context.Args.GetAllValues();
- options.Tmpfs.reserve(options.Tmpfs.size() + tmpfs.size());
- for (const auto& value : tmpfs)
- {
- options.Tmpfs.emplace_back(WideToMultiByte(value));
- }
+ options.Mounts.insert(options.Mounts.end(), std::make_move_iterator(tmpfs.begin()), std::make_move_iterator(tmpfs.end()));
}
+ ValidateUniqueMountDestinations(options);
+
for (const auto& label : context.Args.GetAllValues())
{
options.Labels.push_back(label);
diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp
index 7f60402df..5bc4ce583 100644
--- a/src/windows/wslcsession/WSLCContainer.cpp
+++ b/src/windows/wslcsession/WSLCContainer.cpp
@@ -24,6 +24,8 @@ Module Name:
#include "WSLCProcessIO.h"
#include "WSLCVolumes.h"
#include "APICompat.h"
+#include "MountSpecParsing.h"
+#include
namespace apicompat = wsl::windows::common::apicompat;
@@ -424,9 +426,21 @@ auto MountVolumes(std::vector& volumes, WSLCVirtualMachine& par
for (auto& volume : volumes)
{
- // Create a new directory if it doesn't exist.
- if (!std::filesystem::exists(volume.HostPath))
+ std::error_code error;
+ const auto sourceExists = std::filesystem::exists(volume.HostPath, error);
+ if (error)
{
+ throw wsl::windows::common::mount::MountValidationException(
+ Localization::MessageWslcBindSourcePathError(volume.HostPath, error.message()));
+ }
+
+ if (!sourceExists)
+ {
+ if (!volume.CreateSourceIfMissing)
+ {
+ throw wsl::windows::common::mount::MountValidationException(Localization::MessageWslcBindSourcePathNotFound(volume.HostPath));
+ }
+
auto result = wil::CreateDirectoryDeepNoThrow(volume.HostPath.c_str());
if (FAILED(result))
{
@@ -443,6 +457,18 @@ auto MountVolumes(std::vector& volumes, WSLCVirtualMachine& par
return std::move(errorCleanup);
}
+auto MountVolumesWithUserError(std::vector& volumes, WSLCVirtualMachine& parentVM)
+{
+ try
+ {
+ return MountVolumes(volumes, parentVM);
+ }
+ catch (const wsl::windows::common::mount::MountValidationException& ex)
+ {
+ THROW_HR_WITH_USER_ERROR(E_INVALIDARG, ex.Reason());
+ }
+}
+
WSLCContainerState DockerStateToWSLCState(ContainerState state)
{
// TODO: Handle other states like Paused, Restarting, etc.
@@ -531,6 +557,210 @@ std::map StripInternalLabels(std::optional{}));
}
+std::vector ConvertAndValidateMounts(const WSLCContainerOptions& containerOptions)
+{
+ namespace mount = wsl::windows::common::mount;
+
+ THROW_HR_IF(E_INVALIDARG, containerOptions.MountsCount > 0 && containerOptions.Mounts == nullptr);
+
+ std::vector mounts;
+ mounts.reserve(containerOptions.MountsCount);
+ for (ULONG i = 0; i < containerOptions.MountsCount; ++i)
+ {
+ const auto& value = containerOptions.Mounts[i];
+ THROW_HR_IF_NULL_MSG(E_INVALIDARG, value.Target, "Mount at index %lu has null Target", i);
+ THROW_HR_IF_MSG(
+ E_INVALIDARG,
+ WI_IsAnyFlagSet(value.Flags, ~WSLCMountSpecFlagsValid),
+ "Mount at index %lu has invalid flags: 0x%x",
+ i,
+ value.Flags);
+
+ mount::Type type;
+ switch (value.Type)
+ {
+ case WSLCMountTypeBind:
+ type = mount::Type::Bind;
+ break;
+
+ case WSLCMountTypeVolume:
+ type = mount::Type::Volume;
+ break;
+
+ case WSLCMountTypeTmpfs:
+ type = mount::Type::Tmpfs;
+ break;
+
+ default:
+ THROW_HR_MSG(E_INVALIDARG, "Mount at index %lu has invalid type: %d", i, value.Type);
+ }
+
+ THROW_HR_IF_MSG(
+ E_INVALIDARG,
+ type != mount::Type::Bind && WI_IsFlagSet(value.Flags, WSLCMountSpecFlagsCreateSourceIfMissing),
+ "Mount at index %lu specifies create-source-if-missing for a non-bind mount",
+ i);
+ THROW_HR_IF_MSG(
+ E_INVALIDARG,
+ type != mount::Type::Tmpfs && WI_IsFlagSet(value.Flags, WSLCMountSpecFlagsTmpfsOptions),
+ "Mount at index %lu specifies tmpfs options for a non-tmpfs mount",
+ i);
+ THROW_HR_IF_MSG(
+ E_INVALIDARG,
+ WI_IsFlagSet(value.Flags, WSLCMountSpecFlagsTmpfsOptions) &&
+ WI_IsAnyFlagSet(value.Flags, WSLCMountSpecFlagsTmpfsSize | WSLCMountSpecFlagsTmpfsMode),
+ "Mount at index %lu combines legacy and structured tmpfs options",
+ i);
+
+ mounts.push_back({
+ .MountType = type,
+ .Source = value.Source != nullptr ? value.Source : L"",
+ .Target = value.Target,
+ .ReadOnly = static_cast(value.ReadOnly),
+ .BindSource = WI_IsFlagSet(value.Flags, WSLCMountSpecFlagsCreateSourceIfMissing) ? mount::BindSourcePolicy::CreateIfMissing
+ : mount::BindSourcePolicy::RequireExisting,
+ .TmpfsSizeBytes = WI_IsFlagSet(value.Flags, WSLCMountSpecFlagsTmpfsSize) ? std::optional{value.TmpfsSizeBytes} : std::nullopt,
+ .TmpfsMode = WI_IsFlagSet(value.Flags, WSLCMountSpecFlagsTmpfsMode) ? std::optional{value.TmpfsMode} : std::nullopt,
+ .TmpfsOptions = WI_IsFlagSet(value.Flags, WSLCMountSpecFlagsTmpfsOptions)
+ ? std::optional{value.TmpfsOptions != nullptr ? value.TmpfsOptions : ""}
+ : std::nullopt,
+ });
+ }
+
+ try
+ {
+ mount::ValidateMountCollection(mounts);
+ for (const auto& mount : mounts)
+ {
+ if (mount.MountType == mount::Type::Bind)
+ {
+ if (mount.BindSource == mount::BindSourcePolicy::CreateIfMissing)
+ {
+ continue;
+ }
+
+ std::error_code error;
+ const auto sourceExists = std::filesystem::exists(mount.Source, error);
+ if (error)
+ {
+ throw mount::MountValidationException(Localization::MessageWslcBindSourcePathError(mount.Source, error.message()));
+ }
+
+ if (!sourceExists)
+ {
+ throw mount::MountValidationException(Localization::MessageWslcBindSourcePathNotFound(mount.Source));
+ }
+ }
+ }
+ }
+ catch (const mount::MountException& ex)
+ {
+ if (ex.Error() == mount::ValidationError::DuplicateDestination)
+ {
+ THROW_HR_WITH_USER_ERROR(
+ E_INVALIDARG, Localization::WSLCCLI_DuplicateMountDestinationError(wsl::shared::string::MultiByteToWide(ex.Destination())));
+ }
+
+ THROW_HR_WITH_USER_ERROR(E_INVALIDARG, ex.Reason());
+ }
+
+ std::unordered_set destinations;
+ const auto addDestination = [&](const char* destination) {
+ THROW_HR_IF_NULL(E_INVALIDARG, destination);
+
+ const auto normalizedDestination = mount::NormalizeDestination(destination);
+ THROW_HR_WITH_USER_ERROR_IF(
+ E_INVALIDARG,
+ Localization::WSLCCLI_DuplicateMountDestinationError(wsl::shared::string::MultiByteToWide(normalizedDestination)),
+ !destinations.emplace(normalizedDestination).second);
+ };
+
+ for (const auto& mount : mounts)
+ {
+ addDestination(mount.Target.c_str());
+ }
+
+ THROW_HR_IF(E_INVALIDARG, containerOptions.VolumesCount > 0 && containerOptions.Volumes == nullptr);
+ 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);
+ }
+
+ 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);
+ }
+
+ THROW_HR_IF(E_INVALIDARG, containerOptions.TmpfsCount > 0 && containerOptions.Tmpfs == nullptr);
+ for (ULONG i = 0; i < containerOptions.TmpfsCount; ++i)
+ {
+ addDestination(containerOptions.Tmpfs[i].Destination);
+ }
+
+ return mounts;
+}
+
+struct PreparedBindMount
+{
+ WSLCVolumeMount Volume;
+ std::string DockerSource;
+};
+
+enum class MissingBindSource
+{
+ Create,
+ Reject,
+};
+
+PreparedBindMount PrepareBindMount(const std::wstring& source, const std::string& target, bool readOnly, MissingBindSource missingSource)
+{
+ GUID volumeId;
+ THROW_IF_FAILED(CoCreateGuid(&volumeId));
+
+ auto parentVMPath = std::format("/mnt/{}", wsl::shared::string::GuidToString(volumeId));
+ std::filesystem::path hostPath = source;
+ THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(source), !hostPath.is_absolute());
+
+ std::wstring sourceFilename;
+ {
+ std::error_code ec;
+ hostPath = std::filesystem::canonical(hostPath, ec);
+ if (!ec)
+ {
+ if (std::filesystem::is_regular_file(hostPath))
+ {
+ sourceFilename = hostPath.filename().wstring();
+ hostPath = hostPath.parent_path();
+ }
+ }
+ else if (ec == std::errc::no_such_file_or_directory)
+ {
+ hostPath = source;
+ }
+ else
+ {
+ THROW_HR_WITH_USER_ERROR(E_FAIL, Localization::MessageWslcFailedToMountVolume(source, ec.message()));
+ }
+ }
+
+ auto dockerSource = sourceFilename.empty() ? parentVMPath : std::format("{}/{}", parentVMPath, sourceFilename);
+ return {
+ .Volume =
+ {
+ .HostPath = std::move(hostPath),
+ .ParentVMPath = std::move(parentVMPath),
+ .ContainerPath = target,
+ .ReadOnly = readOnly,
+ .SourceFilename = std::move(sourceFilename),
+ .CreateSourceIfMissing = missingSource == MissingBindSource::Create,
+ },
+ .DockerSource = std::move(dockerSource),
+ };
+}
+
void ProcessNamedVolumes(const WSLCContainerOptions& containerOptions, wsl::windows::common::docker_schema::CreateContainer& request)
{
THROW_HR_IF(E_INVALIDARG, containerOptions.NamedVolumesCount > 0 && containerOptions.NamedVolumes == nullptr);
@@ -910,7 +1140,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
Localization::MessageWslcVolumeNotAvailable(wsl::shared::string::Join(unavailableVolumes, ',')),
!unavailableVolumes.empty());
- auto volumeCleanup = MountVolumes(m_mountedVolumes, m_runtime.Vm());
+ auto volumeCleanup = MountVolumesWithUserError(m_mountedVolumes, m_runtime.Vm());
auto portCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { UnmapPorts(); });
MapPorts();
@@ -1611,7 +1841,9 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec
}
// Map mounts without exposing Linux paths from the utility VM.
- wslcInspect.Mounts.reserve(m_mountedVolumes.size() + dockerInspect.Mounts.size() + dockerInspect.HostConfig.Tmpfs.size());
+ wslcInspect.Mounts.reserve(
+ m_mountedVolumes.size() + dockerInspect.Mounts.size() + dockerInspect.HostConfig.Tmpfs.size() +
+ dockerInspect.HostConfig.Mounts.size());
for (const auto& volume : m_mountedVolumes)
{
wslc_schema::InspectMount mountInfo{};
@@ -1650,6 +1882,13 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec
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;
+ });
+ if (structuredMount != dockerInspect.HostConfig.Mounts.end())
+ {
+ mountInfo.Source = structuredMount->Source;
+ }
mountInfo.Destination = volume.Destination;
mountInfo.ReadWrite = volume.RW;
@@ -1668,6 +1907,20 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec
wslcInspect.Mounts.push_back(std::move(mountInfo));
}
+ // Bind mounts are populated from m_mountedVolumes so their inspect source is the Windows host path.
+ for (const auto& mount : dockerInspect.HostConfig.Mounts)
+ {
+ if (mount.Type == "tmpfs")
+ {
+ wslc_schema::InspectMount mountInfo{};
+ mountInfo.Type = mount.Type;
+ mountInfo.Source = mount.Source;
+ mountInfo.Destination = mount.Target;
+ mountInfo.ReadWrite = !mount.ReadOnly;
+ wslcInspect.Mounts.push_back(std::move(mountInfo));
+ }
+ }
+
// Config.Labels is the Docker-shape location; top-level Labels is a legacy alias.
wslcInspect.Config.Labels = m_labels;
wslcInspect.Labels = m_labels;
@@ -1708,6 +1961,7 @@ std::shared_ptr WSLCContainerImpl::Create(
auto& virtualMachine = runtime.Vm();
auto& DockerClient = runtime.Docker();
auto& EventTracker = runtime.Events();
+ const auto mounts = ConvertAndValidateMounts(containerOptions);
common::docker_schema::CreateContainer request;
request.Image = containerOptions.Image;
@@ -1869,73 +2123,22 @@ std::shared_ptr WSLCContainerImpl::Create(
request.Healthcheck = std::move(health);
}
- if (containerOptions.VolumesCount > 0)
- {
- THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.Volumes, "Volumes is null with VolumesCount=%lu", containerOptions.VolumesCount);
- }
-
// Build bind mount list from container options.
std::vector volumes;
- volumes.reserve(containerOptions.VolumesCount);
+ volumes.reserve(containerOptions.VolumesCount + mounts.size());
std::vector binds;
binds.reserve(containerOptions.VolumesCount);
for (ULONG i = 0; i < containerOptions.VolumesCount; i++)
{
- GUID volumeId;
- THROW_IF_FAILED(CoCreateGuid(&volumeId));
-
- auto parentVMPath = std::format("/mnt/{}", wsl::shared::string::GuidToString(volumeId));
auto volume = containerOptions.Volumes[i];
-
- THROW_HR_IF_NULL_MSG(E_INVALIDARG, volume.HostPath, "Volumes[%lu].HostPath is null", i);
- THROW_HR_IF_NULL_MSG(E_INVALIDARG, volume.ContainerPath, "Volumes[%lu].ContainerPath is null", i);
-
- std::filesystem::path hostPath = volume.HostPath;
- THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(volume.HostPath), !hostPath.is_absolute());
-
- std::wstring sourceFilename;
-
- {
- // Resolve symlinks.
- std::error_code ec;
- hostPath = std::filesystem::canonical(hostPath, ec);
- if (!ec)
- {
- // When the host path is a file, mount the parent directory in the VM
- // and bind only the specific file into the container via Docker.
- if (std::filesystem::is_regular_file(hostPath))
- {
- sourceFilename = hostPath.filename().wstring();
- hostPath = hostPath.parent_path();
- }
- }
- else
- {
- if (ec == std::errc::no_such_file_or_directory)
- {
- // Path doesn't exist, assume directory.
- hostPath = volume.HostPath;
- }
- else
- {
- THROW_HR_WITH_USER_ERROR(E_FAIL, Localization::MessageWslcFailedToMountVolume(volume.HostPath, ec.message()));
- }
- }
- }
-
- volumes.push_back(WSLCVolumeMount{hostPath, parentVMPath, volume.ContainerPath, static_cast(volume.ReadOnly), sourceFilename});
-
- auto options = volume.ReadOnly ? "ro" : "rw";
- auto bindSource = sourceFilename.empty() ? parentVMPath : std::format("{}/{}", parentVMPath, sourceFilename);
- auto bind = std::format("{}:{}:{}", bindSource, volume.ContainerPath, options);
-
- binds.push_back(std::move(bind));
+ auto prepared =
+ PrepareBindMount(volume.HostPath, volume.ContainerPath, static_cast(volume.ReadOnly), MissingBindSource::Create);
+ binds.push_back(std::format("{}:{}:{}", prepared.DockerSource, volume.ContainerPath, volume.ReadOnly ? "ro" : "rw"));
+ volumes.push_back(std::move(prepared.Volume));
}
- request.HostConfig.Binds = std::move(binds);
-
// Process tmpfs mounts from container options.
if (containerOptions.TmpfsCount > 0)
{
@@ -1953,6 +2156,56 @@ std::shared_ptr WSLCContainerImpl::Create(
ProcessNamedVolumes(containerOptions, request);
+ for (const auto& mount : mounts)
+ {
+ common::docker_schema::Mount dockerMount{
+ .Target = mount.Target,
+ .ReadOnly = mount.ReadOnly,
+ };
+
+ switch (mount.MountType)
+ {
+ case wsl::windows::common::mount::Type::Bind:
+ {
+ // Docker's colon-delimited bind format cannot represent ':' in the target.
+ const auto missingSource = mount.BindSource == wsl::windows::common::mount::BindSourcePolicy::CreateIfMissing
+ ? MissingBindSource::Create
+ : MissingBindSource::Reject;
+ auto prepared = PrepareBindMount(mount.Source, mount.Target, mount.ReadOnly, missingSource);
+ dockerMount.Source = std::move(prepared.DockerSource);
+ dockerMount.Type = "bind";
+ volumes.push_back(std::move(prepared.Volume));
+ break;
+ }
+
+ case wsl::windows::common::mount::Type::Volume:
+ dockerMount.Source = wsl::shared::string::WideToMultiByte(mount.Source);
+ dockerMount.Type = "volume";
+ break;
+
+ case wsl::windows::common::mount::Type::Tmpfs:
+ if (mount.TmpfsOptions.has_value())
+ {
+ request.HostConfig.Tmpfs[mount.Target] = mount.TmpfsOptions.value();
+ continue;
+ }
+
+ dockerMount.Type = "tmpfs";
+ if (mount.TmpfsSizeBytes.has_value() || mount.TmpfsMode.has_value())
+ {
+ dockerMount.TmpfsOptions = common::docker_schema::MountTmpfsOptions{
+ .SizeBytes = mount.TmpfsSizeBytes.value_or(0),
+ .Mode = mount.TmpfsMode.value_or(0),
+ };
+ }
+ break;
+ }
+
+ request.HostConfig.Mounts.push_back(std::move(dockerMount));
+ }
+
+ request.HostConfig.Binds = std::move(binds);
+
// Configure GPU support if requested.
if (WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsGpu))
{
@@ -2090,8 +2343,12 @@ std::shared_ptr WSLCContainerImpl::Create(
request.Labels[WSLCContainerMetadataLabel] = SerializeContainerMetadata(metadata);
request.Labels.insert(requestedLabels.begin(), requestedLabels.end());
- // Send the request to docker.
- auto result = DockerClient.CreateContainer(request, containerName);
+ // Docker validates structured bind sources during container creation, so their VM paths must exist here.
+ // Release the temporary shares before returning; Start remounts them for the container lifetime.
+ auto result = [&]() {
+ auto volumeCleanup = MountVolumesWithUserError(volumes, virtualMachine);
+ return DockerClient.CreateContainer(request, containerName);
+ }();
// Surface any warnings returned by Docker (e.g., deprecated features, configuration issues).
for (const auto& warning : result.Warnings)
@@ -2145,12 +2402,20 @@ std::shared_ptr WSLCContainerImpl::Create(
// Collect the names of referenced docker named volumes so Start() can verify
// they are available before running the container.
std::vector namedVolumes;
- namedVolumes.reserve(containerOptions.NamedVolumesCount);
+ namedVolumes.reserve(containerOptions.NamedVolumesCount + mounts.size());
for (ULONG i = 0; i < containerOptions.NamedVolumesCount; i++)
{
namedVolumes.emplace_back(containerOptions.NamedVolumes[i].Name);
}
+ for (const auto& mount : mounts)
+ {
+ if (mount.MountType == wsl::windows::common::mount::Type::Volume && !mount.Source.empty())
+ {
+ namedVolumes.emplace_back(wsl::shared::string::WideToMultiByte(mount.Source));
+ }
+ }
+
auto mergedLabels = StripInternalLabels(std::move(inspectData.Config.Labels));
auto container = std::make_shared(
diff --git a/src/windows/wslcsession/WSLCContainerMetadata.h b/src/windows/wslcsession/WSLCContainerMetadata.h
index 0d19c88ad..a81a46526 100644
--- a/src/windows/wslcsession/WSLCContainerMetadata.h
+++ b/src/windows/wslcsession/WSLCContainerMetadata.h
@@ -44,11 +44,12 @@ struct WSLCVolumeMount
// Non-empty when the mount target is a single file rather than a directory.
std::wstring SourceFilename;
+ bool CreateSourceIfMissing{true};
// Runtime-only field. Not serialized to JSON.
bool Mounted{};
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(WSLCVolumeMount, HostPath, ParentVMPath, ContainerPath, ReadOnly, SourceFilename);
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(WSLCVolumeMount, HostPath, ParentVMPath, ContainerPath, ReadOnly, SourceFilename, CreateSourceIfMissing);
};
struct WSLCContainerMetadataV1
diff --git a/test/windows/wslc/WSLCCLIMountParserUnitTests.cpp b/test/windows/wslc/WSLCCLIMountParserUnitTests.cpp
new file mode 100644
index 000000000..d9667a915
--- /dev/null
+++ b/test/windows/wslc/WSLCCLIMountParserUnitTests.cpp
@@ -0,0 +1,548 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ WSLCCLIMountParserUnitTests.cpp
+
+Abstract:
+
+ Unit tests for Docker-compatible --mount parsing.
+
+--*/
+
+#include "precomp.h"
+#include "windows/Common.h"
+#include "WSLCCLITestHelpers.h"
+#include "ContainerModel.h"
+#include "MountSpecParsing.h"
+
+using namespace wsl::windows::common;
+using namespace wsl::windows::wslc::models;
+using namespace wsl::shared;
+using namespace WEX::Logging;
+using namespace WEX::Common;
+
+namespace WSLCCLIMountParserUnitTests {
+
+namespace {
+
+ mount::Spec ParseAndValidate(const std::wstring& value)
+ {
+ auto mountSpec = mount::ParseDockerMountString(value);
+ mount::ValidateMountSpec(mountSpec);
+ return mountSpec;
+ }
+
+ struct ValidMountCase
+ {
+ const wchar_t* Input;
+ mount::Type Type;
+ const wchar_t* Source;
+ const char* Target;
+ bool ReadOnly;
+ std::optional TmpfsSizeBytes;
+ std::optional TmpfsMode;
+ const char* TmpfsOptions;
+ };
+
+ enum class ExpectedException
+ {
+ Parse,
+ Unsupported,
+ Validation,
+ };
+
+ struct InvalidMountCase
+ {
+ const wchar_t* Input;
+ std::wstring ExpectedReason;
+ ExpectedException Exception = ExpectedException::Parse;
+ };
+
+ 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, {}, {}, ""},
+ {L"type=volume,source=data-volume,target=/path:voldir",
+ mount::Type::Volume,
+ L"data-volume",
+ "/path:voldir",
+ false,
+ {},
+ {},
+ ""},
+ {L"TYPE=VOLUME,SOURCE=data-volume,TARGET=/data", mount::Type::Volume, L"data-volume", "/data", false, {}, {}, ""},
+ {L"type=VoLuMe,source=data-volume,target=/data", mount::Type::Volume, L"data-volume", "/data", false, {}, {}, ""},
+ {L"type=volume,src=data-volume,dst=/data", mount::Type::Volume, L"data-volume", "/data", false, {}, {}, ""},
+ {L"type=volume,src=data-volume,destination=/data", mount::Type::Volume, L"data-volume", "/data", false, {}, {}, ""},
+ {L"type=volume,source=first,source=second,target=/data", mount::Type::Volume, L"second", "/data", false, {}, {}, ""},
+ {L"type=volume,source=data-volume,target=/first,target=/second",
+ mount::Type::Volume,
+ L"data-volume",
+ "/second",
+ false,
+ {},
+ {},
+ ""},
+ {L"type=volume,type=bind,source=C:\\data,target=/data", mount::Type::Bind, L"C:\\data", "/data", false, {}, {}, ""},
+ {L"type=volume,source=data-volume,target=/data,readonly", mount::Type::Volume, L"data-volume", "/data", true, {}, {}, ""},
+ {L"type=volume,source=data-volume,target=/data,ro", mount::Type::Volume, L"data-volume", "/data", true, {}, {}, ""},
+ {L"type=volume,source=data-volume,target=/data,readonly=1",
+ mount::Type::Volume,
+ L"data-volume",
+ "/data",
+ true,
+ {},
+ {},
+ ""},
+ {L"type=volume,source=data-volume,target=/data,readonly=t",
+ mount::Type::Volume,
+ L"data-volume",
+ "/data",
+ true,
+ {},
+ {},
+ ""},
+ {L"type=volume,source=data-volume,target=/data,readonly=T",
+ mount::Type::Volume,
+ L"data-volume",
+ "/data",
+ true,
+ {},
+ {},
+ ""},
+ {L"type=volume,source=data-volume,target=/data,readonly=TRUE",
+ mount::Type::Volume,
+ L"data-volume",
+ "/data",
+ true,
+ {},
+ {},
+ ""},
+ {L"type=volume,source=data-volume,target=/data,readonly=true",
+ mount::Type::Volume,
+ L"data-volume",
+ "/data",
+ true,
+ {},
+ {},
+ ""},
+ {L"type=volume,source=data-volume,target=/data,readonly=True",
+ mount::Type::Volume,
+ L"data-volume",
+ "/data",
+ true,
+ {},
+ {},
+ ""},
+ {L"type=volume,source=data-volume,target=/data,readonly=0",
+ mount::Type::Volume,
+ L"data-volume",
+ "/data",
+ false,
+ {},
+ {},
+ ""},
+ {L"type=volume,source=data-volume,target=/data,readonly=f",
+ mount::Type::Volume,
+ L"data-volume",
+ "/data",
+ false,
+ {},
+ {},
+ ""},
+ {L"type=volume,source=data-volume,target=/data,readonly=F",
+ mount::Type::Volume,
+ L"data-volume",
+ "/data",
+ false,
+ {},
+ {},
+ ""},
+ {L"type=volume,source=data-volume,target=/data,readonly=FALSE",
+ mount::Type::Volume,
+ L"data-volume",
+ "/data",
+ false,
+ {},
+ {},
+ ""},
+ {L"type=volume,source=data-volume,target=/data,readonly=false",
+ mount::Type::Volume,
+ L"data-volume",
+ "/data",
+ false,
+ {},
+ {},
+ ""},
+ {L"type=volume,source=data-volume,target=/data,readonly=False",
+ mount::Type::Volume,
+ L"data-volume",
+ "/data",
+ false,
+ {},
+ {},
+ ""},
+ {L"type=volume,source=data-volume,target=/data,readonly=true,readonly=false",
+ mount::Type::Volume,
+ L"data-volume",
+ "/data",
+ false,
+ {},
+ {},
+ ""},
+ {L"type=bind,\"source=C:\\mount,a\",target=/data", mount::Type::Bind, L"C:\\mount,a", "/data", false, {}, {}, ""},
+ {L"type=bind,source=C:\\mount with spaces,target=/data",
+ mount::Type::Bind,
+ L"C:\\mount with spaces",
+ "/data",
+ false,
+ {},
+ {},
+ ""},
+ {L"type=bind,source=C:\\mount,target=/path:mntdir", mount::Type::Bind, L"C:\\mount", "/path:mntdir", false, {}, {}, ""},
+ {L"type=bind,source=C:\\,target=/data", mount::Type::Bind, L"C:\\", "/data", false, {}, {}, ""},
+ {L"type=bind,source=\\\\server\\share,target=/data", mount::Type::Bind, L"\\\\server\\share", "/data", false, {}, {}, ""},
+ {L"type=bind,source=C:\\mount,target=/data,bind-recursive=enabled",
+ mount::Type::Bind,
+ L"C:\\mount",
+ "/data",
+ false,
+ {},
+ {},
+ ""},
+ {L"type=volume,source=A_,target=/data", mount::Type::Volume, L"A_", "/data", false, {}, {}, ""},
+ {L"type=volume,source=data.volume-1,target=/data", mount::Type::Volume, L"data.volume-1", "/data", false, {}, {}, ""},
+ {L"type=tmpfs,target=/tmp", mount::Type::Tmpfs, L"", "/tmp", false, {}, {}, ""},
+ {L"type=tmpfs,target=/path:tmpfs", mount::Type::Tmpfs, L"", "/path:tmpfs", false, {}, {}, ""},
+ {L"type=tmpfs,target=/tmp,readonly", mount::Type::Tmpfs, L"", "/tmp", true, {}, {}, "ro"},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=0", mount::Type::Tmpfs, L"", "/tmp", false, 0, {}, ""},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=1", mount::Type::Tmpfs, L"", "/tmp", false, 1, {}, "size=1"},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=1024", mount::Type::Tmpfs, L"", "/tmp", false, 1024, {}, "size=1k"},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=1536", mount::Type::Tmpfs, L"", "/tmp", false, 1536, {}, "size=1536"},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=1k", mount::Type::Tmpfs, L"", "/tmp", false, 1024, {}, "size=1k"},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=1KB", mount::Type::Tmpfs, L"", "/tmp", false, 1024, {}, "size=1k"},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=1KiB", mount::Type::Tmpfs, L"", "/tmp", false, 1024, {}, "size=1k"},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=1MB", mount::Type::Tmpfs, L"", "/tmp", false, 1LL << 20, {}, "size=1m"},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=1MiB", mount::Type::Tmpfs, L"", "/tmp", false, 1LL << 20, {}, "size=1m"},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=1GB", mount::Type::Tmpfs, L"", "/tmp", false, 1LL << 30, {}, "size=1g"},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=1.5MB", mount::Type::Tmpfs, L"", "/tmp", false, 1536LL << 10, {}, "size=1536k"},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=+1MB", mount::Type::Tmpfs, L"", "/tmp", false, 1LL << 20, {}, "size=1m"},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=1e3", mount::Type::Tmpfs, L"", "/tmp", false, 1000, {}, "size=1000"},
+ {L"type=tmpfs,target=/tmp,tmpfs-mode=0000", mount::Type::Tmpfs, L"", "/tmp", false, {}, 0, ""},
+ {L"type=tmpfs,target=/tmp,tmpfs-mode=0700", mount::Type::Tmpfs, L"", "/tmp", false, {}, 0700, "mode=700"},
+ {L"type=tmpfs,target=/tmp,tmpfs-mode=+0700", mount::Type::Tmpfs, L"", "/tmp", false, {}, 0700, "mode=700"},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=1MB,tmpfs-mode=0700,readonly",
+ mount::Type::Tmpfs,
+ L"",
+ "/tmp",
+ true,
+ 1LL << 20,
+ 0700,
+ "ro,mode=700,size=1m"},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=0,tmpfs-mode=0000,readonly=false", mount::Type::Tmpfs, L"", "/tmp", false, 0, 0, ""},
+ };
+
+ const InvalidMountCase c_invalidMountCases[] = {
+ {L"", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"")},
+ {L",", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"")},
+ {L"type=volume,source=data-volume,target=/data,", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"")},
+ {L",type=volume,source=data-volume,target=/data", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"")},
+ {L"type=bind,\"source=C:\\mount,target=/data", Localization::WSLCCLI_MountMalformedCsvError()},
+ {L"type=volume,bogus", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"bogus")},
+ {L"type=volume,bogus=value", Localization::WSLCCLI_MountUnexpectedKeyError(L"bogus", L"bogus=value")},
+ {L"type", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"type")},
+ {L"source", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"source")},
+ {L"target", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"target")},
+ {L"type=,source=data-volume,target=/data", Localization::WSLCCLI_MountTypeRequiredError()},
+ {L"type=volume,source=data-volume", Localization::WSLCCLI_MountTargetRequiredError(), ExpectedException::Validation},
+ {L"type=volume,source=data-volume,target=", Localization::WSLCCLI_MountTargetRequiredError(), ExpectedException::Validation},
+ {L"type=volume,source=data-volume,dst=", Localization::WSLCCLI_MountTargetRequiredError(), ExpectedException::Validation},
+ {L"type=cluster,source=data-volume,target=/data", Localization::WSLCCLI_MountTypeUnsupportedError(L"cluster"), ExpectedException::Unsupported},
+ {L"type=npipe,source=data-volume,target=/data", Localization::WSLCCLI_MountTypeUnsupportedError(L"npipe"), ExpectedException::Unsupported},
+ {L"type=bogus,source=data-volume,target=/data", Localization::WSLCCLI_MountTypeUnsupportedError(L"bogus"), ExpectedException::Unsupported},
+ {L"type=CLUSTER,source=data-volume,target=/data", Localization::WSLCCLI_MountTypeUnsupportedError(L"cluster"), ExpectedException::Unsupported},
+ {L"type=bind,target=/data", Localization::WSLCCLI_MountSourceRequiredError(), ExpectedException::Validation},
+ {L"type=bind,source=relative,target=/data", Localization::WSLCCLI_MountBindSourceAbsoluteError(), ExpectedException::Validation},
+ {L"type=volume,source=a,target=/data", Localization::WSLCCLI_MountVolumeSourceInvalidError(), ExpectedException::Validation},
+ {L"type=volume,source=data/volume,target=/data", Localization::WSLCCLI_MountVolumeSourceInvalidError(), ExpectedException::Validation},
+ {L"type=volume,source=C:\\mount,target=/data", Localization::WSLCCLI_MountVolumeSourceInvalidError(), ExpectedException::Validation},
+ {L"type=tmpfs,source=data-volume,target=/data", Localization::WSLCCLI_MountTmpfsSourceUnsupportedError(), ExpectedException::Validation},
+ {L"type=volume,source=data-volume,target=data", Localization::WSLCCLI_MountTargetAbsoluteError(), ExpectedException::Validation},
+ {L"type=volume,source=data-volume,dst=.", Localization::WSLCCLI_MountTargetAbsoluteError(), ExpectedException::Validation},
+ {L"type=volume,source=data-volume,destination=\\data", Localization::WSLCCLI_MountTargetAbsoluteError(), ExpectedException::Validation},
+ {L"type=bind,source=C:\\mount,target=data", Localization::WSLCCLI_MountTargetAbsoluteError(), ExpectedException::Validation},
+ {L"type=tmpfs,target=data", Localization::WSLCCLI_MountTargetAbsoluteError(), ExpectedException::Validation},
+ {L"type=volume,source=data-volume,target=/data,readonly=no",
+ Localization::WSLCCLI_MountInvalidValueError(L"readonly", L"no")},
+ {L"type=volume,source=data-volume,target=/data,readonly=yes",
+ Localization::WSLCCLI_MountInvalidValueError(L"readonly", L"yes")},
+ {L"type=volume,source=data-volume,target=/data,readonly=", Localization::WSLCCLI_MountInvalidValueError(L"readonly", L"")},
+ {L"type=volume,source=data-volume,target=/data,readonly=2",
+ Localization::WSLCCLI_MountInvalidValueError(L"readonly", L"2")},
+ {L"type=volume,source=data-volume,target=/data,volume-nocopy=no",
+ Localization::WSLCCLI_MountInvalidValueError(L"volume-nocopy", L"no")},
+ {L"type=volume,source=data-volume,target=/data,volume-nocopy=",
+ Localization::WSLCCLI_MountInvalidValueError(L"volume-nocopy", L"")},
+ {L"type=bind,source=C:\\mount,target=/data,bind-nonrecursive=no",
+ Localization::WSLCCLI_MountInvalidValueError(L"bind-nonrecursive", L"no")},
+ {L"type=bind,source=C:\\mount,target=/data,bind-recursive=",
+ Localization::WSLCCLI_MountInvalidBindRecursiveValueError(L"bind-recursive", L"")},
+ {L"type=bind,source=C:\\mount,target=/data,bind-recursive=Enabled",
+ Localization::WSLCCLI_MountInvalidBindRecursiveValueError(L"bind-recursive", L"Enabled")},
+ {L"type=bind,source=C:\\mount,target=/data,bind-recursive=bogus",
+ Localization::WSLCCLI_MountInvalidBindRecursiveValueError(L"bind-recursive", L"bogus")},
+ {L"type=bind,source=C:\\mount,target=/data,bind-recursive=writable",
+ Localization::WSLCCLI_MountOptionRequiresReadonlyError(L"bind-recursive=writable")},
+ {L"type=bind,source=C:\\mount,target=/data,bind-recursive=readonly",
+ Localization::WSLCCLI_MountOptionRequiresReadonlyError(L"bind-recursive=readonly")},
+ {L"type=bind,source=C:\\mount,target=/data,bind-recursive=readonly,readonly",
+ Localization::WSLCCLI_MountBindRecursiveReadonlyRequiresPropagationError()},
+ {L"type=bind,source=C:\\mount,target=/data,consistency=cached",
+ Localization::WSLCCLI_MountOptionUnsupportedError(L"consistency"),
+ ExpectedException::Unsupported},
+ {L"type=bind,source=C:\\mount,target=/data,bind-propagation=rprivate",
+ Localization::WSLCCLI_MountOptionUnsupportedError(L"bind-propagation"),
+ ExpectedException::Unsupported},
+ {L"type=bind,source=C:\\mount,target=/data,bind-nonrecursive",
+ Localization::WSLCCLI_MountOptionUnsupportedError(L"bind-nonrecursive"),
+ ExpectedException::Unsupported},
+ {L"type=bind,source=C:\\mount,target=/data,bind-nonrecursive=true",
+ Localization::WSLCCLI_MountOptionUnsupportedError(L"bind-nonrecursive"),
+ ExpectedException::Unsupported},
+ {L"type=bind,source=C:\\mount,target=/data,bind-recursive=disabled",
+ Localization::WSLCCLI_MountOptionUnsupportedError(L"bind-recursive"),
+ ExpectedException::Unsupported},
+ {L"type=bind,source=C:\\mount,target=/data,bind-recursive=writable,readonly",
+ Localization::WSLCCLI_MountOptionUnsupportedError(L"bind-recursive"),
+ ExpectedException::Unsupported},
+ {L"type=bind,source=C:\\mount,target=/data,bind-recursive=readonly,readonly,bind-propagation=rprivate",
+ Localization::WSLCCLI_MountOptionUnsupportedError(L"bind-recursive"),
+ ExpectedException::Unsupported},
+ {L"type=volume,source=data-volume,target=/data,volume-nocopy",
+ Localization::WSLCCLI_MountOptionUnsupportedError(L"volume-nocopy"),
+ ExpectedException::Unsupported},
+ {L"type=volume,source=data-volume,target=/data,volume-nocopy=true",
+ Localization::WSLCCLI_MountOptionUnsupportedError(L"volume-nocopy"),
+ ExpectedException::Unsupported},
+ {L"type=volume,source=data-volume,target=/data,volume-label=a=b",
+ Localization::WSLCCLI_MountOptionUnsupportedError(L"volume-label"),
+ ExpectedException::Unsupported},
+ {L"type=volume,source=data-volume,target=/data,volume-driver=local",
+ Localization::WSLCCLI_MountOptionUnsupportedError(L"volume-driver"),
+ ExpectedException::Unsupported},
+ {L"type=volume,source=data-volume,target=/data,volume-opt=a=b",
+ Localization::WSLCCLI_MountOptionUnsupportedError(L"volume-opt"),
+ ExpectedException::Unsupported},
+ {L"type=bind,source=C:\\mount,target=/data,volume-nocopy=true",
+ Localization::WSLCCLI_MountOptionFamilyMismatchError(L"volume-*", L"bind")},
+ {L"type=volume,source=data-volume,target=/data,bind-propagation=rprivate",
+ Localization::WSLCCLI_MountOptionFamilyMismatchError(L"bind-*", L"volume")},
+ {L"type=volume,source=data-volume,target=/data,bind-recursive=enabled",
+ Localization::WSLCCLI_MountOptionFamilyMismatchError(L"bind-*", L"volume")},
+ {L"type=volume,source=data-volume,target=/data,tmpfs-size=1m",
+ Localization::WSLCCLI_MountOptionFamilyMismatchError(L"tmpfs-*", L"volume")},
+ {L"type=tmpfs,target=/tmp,volume-label=a=b", Localization::WSLCCLI_MountOptionFamilyMismatchError(L"volume-*", L"tmpfs")},
+ {L"type=tmpfs,target=/tmp,bind-nonrecursive", Localization::WSLCCLI_MountOptionFamilyMismatchError(L"bind-*", L"tmpfs")},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"")},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=bad", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"bad")},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=-1", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"-1")},
+ {L"type=tmpfs,target=/tmp,\"tmpfs-size=1,5MB\"", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"1,5MB")},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=1XB", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"1XB")},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=1Ki", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"1Ki")},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=1BB", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"1BB")},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=nan", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"nan")},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=inf", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"inf")},
+ {L"type=tmpfs,target=/tmp,tmpfs-size=9223372036854775808",
+ Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"9223372036854775808")},
+ {L"type=tmpfs,target=/tmp,tmpfs-mode=", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"")},
+ {L"type=tmpfs,target=/tmp,tmpfs-mode=-1", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"-1")},
+ {L"type=tmpfs,target=/tmp,tmpfs-mode=8", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"8")},
+ {L"type=tmpfs,target=/tmp,tmpfs-mode=0899", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"0899")},
+ {L"type=tmpfs,target=/tmp,tmpfs-mode=0x700", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"0x700")},
+ {L"type=tmpfs,target=/tmp,tmpfs-mode=40000000000",
+ Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"40000000000")},
+ };
+
+} // namespace
+
+class WSLCCLIMountParserUnitTests
+{
+ WSLC_TEST_CLASS(WSLCCLIMountParserUnitTests)
+
+ TEST_METHOD(Mount_ValidCases)
+ {
+ for (const auto& testCase : c_validMountCases)
+ {
+ Log::Comment(String().Format(L"Accepting: %ls", testCase.Input));
+
+ const auto actual = ParseAndValidate(testCase.Input);
+ VERIFY_ARE_EQUAL(static_cast(testCase.Type), static_cast(actual.MountType));
+ VERIFY_ARE_EQUAL(std::wstring(testCase.Source), actual.Source);
+ VERIFY_ARE_EQUAL(std::string(testCase.Target), actual.Target);
+ VERIFY_ARE_EQUAL(testCase.ReadOnly, actual.ReadOnly);
+ VERIFY_ARE_EQUAL(testCase.TmpfsSizeBytes.has_value(), actual.TmpfsSizeBytes.has_value());
+ if (testCase.TmpfsSizeBytes.has_value() && actual.TmpfsSizeBytes.has_value())
+ {
+ VERIFY_ARE_EQUAL(testCase.TmpfsSizeBytes.value(), actual.TmpfsSizeBytes.value());
+ }
+
+ VERIFY_ARE_EQUAL(testCase.TmpfsMode.has_value(), actual.TmpfsMode.has_value());
+ if (testCase.TmpfsMode.has_value() && actual.TmpfsMode.has_value())
+ {
+ VERIFY_ARE_EQUAL(testCase.TmpfsMode.value(), actual.TmpfsMode.value());
+ }
+
+ const auto actualTmpfsOptions = actual.MountType == mount::Type::Tmpfs ? mount::FormatTmpfsOptions(actual) : std::string{};
+ VERIFY_ARE_EQUAL(std::string(testCase.TmpfsOptions), actualTmpfsOptions);
+ }
+ }
+
+ TEST_METHOD(Mount_InvalidCases)
+ {
+ for (const auto& testCase : c_invalidMountCases)
+ {
+ Log::Comment(String().Format(L"Rejecting: %ls", testCase.Input));
+
+ try
+ {
+ (void)ParseAndValidate(testCase.Input);
+ VERIFY_FAIL(L"Expected MountException for invalid mount spec");
+ }
+ catch (const mount::MountException& ex)
+ {
+ VERIFY_ARE_EQUAL(testCase.ExpectedReason, ex.Reason());
+ switch (testCase.Exception)
+ {
+ case ExpectedException::Parse:
+ VERIFY_IS_TRUE(dynamic_cast(&ex) != nullptr);
+ break;
+
+ case ExpectedException::Unsupported:
+ VERIFY_IS_TRUE(dynamic_cast(&ex) != nullptr);
+ break;
+
+ case ExpectedException::Validation:
+ VERIFY_IS_TRUE(dynamic_cast(&ex) != nullptr);
+ break;
+ }
+ }
+ }
+ }
+
+ TEST_METHOD(Volume_ValidCases)
+ {
+ const auto bind = mount::ParseDockerVolumeString(LR"(C:\hostPath:/data:ro)");
+ VERIFY_ARE_EQUAL(static_cast(mount::Type::Bind), static_cast(bind.MountType));
+ VERIFY_ARE_EQUAL(std::wstring(LR"(C:\hostPath)"), bind.Source);
+ VERIFY_ARE_EQUAL(std::string("/data"), bind.Target);
+ VERIFY_IS_TRUE(bind.ReadOnly);
+ VERIFY_ARE_EQUAL(static_cast(mount::BindSourcePolicy::CreateIfMissing), static_cast(bind.BindSource));
+
+ const auto volume = mount::ParseDockerVolumeString(L"named-volume:/data");
+ VERIFY_ARE_EQUAL(static_cast(mount::Type::Volume), static_cast(volume.MountType));
+ VERIFY_ARE_EQUAL(std::wstring(L"named-volume"), volume.Source);
+ VERIFY_ARE_EQUAL(std::string("/data"), volume.Target);
+ VERIFY_IS_FALSE(volume.ReadOnly);
+ VERIFY_ARE_EQUAL(static_cast(mount::BindSourcePolicy::RequireExisting), static_cast(volume.BindSource));
+ }
+
+ TEST_METHOD(Mount_DotRelativeBindSourceUsesCurrentDirectory)
+ {
+ const auto expected = (std::filesystem::current_path() / L"mount").lexically_normal().wstring();
+ const auto actual = ParseAndValidate(L"type=bind,source=.\\mount,target=/data");
+ VERIFY_ARE_EQUAL(expected, actual.Source);
+ }
+
+ TEST_METHOD(Mount_TypedSpecsAreValidated)
+ {
+ const mount::Spec relativeBind{
+ .MountType = mount::Type::Bind,
+ .Source = L"relative",
+ .Target = "/data",
+ };
+ VERIFY_THROWS(mount::ValidateMountSpec(relativeBind), mount::MountValidationException);
+
+ const mount::Spec relativeTarget{
+ .MountType = mount::Type::Volume,
+ .Source = L"data-volume",
+ .Target = "data",
+ };
+ VERIFY_THROWS(mount::ValidateMountSpec(relativeTarget), mount::MountValidationException);
+
+ const mount::Spec tmpfsWithSource{
+ .MountType = mount::Type::Tmpfs,
+ .Source = L"data-volume",
+ .Target = "/data",
+ };
+ VERIFY_THROWS(mount::ValidateMountSpec(tmpfsWithSource), mount::MountValidationException);
+
+ const mount::Spec bindWithTmpfsOptions{
+ .MountType = mount::Type::Bind,
+ .Source = L"C:\\data",
+ .Target = "/data",
+ .TmpfsSizeBytes = 1024,
+ };
+ VERIFY_THROWS(mount::ValidateMountSpec(bindWithTmpfsOptions), mount::MountValidationException);
+
+ const mount::Spec negativeTmpfsSize{
+ .MountType = mount::Type::Tmpfs,
+ .Target = "/data",
+ .TmpfsSizeBytes = -1,
+ };
+ VERIFY_THROWS(mount::ValidateMountSpec(negativeTmpfsSize), mount::MountValidationException);
+
+ const mount::Spec tmpfs{
+ .MountType = mount::Type::Tmpfs,
+ .Target = "/data",
+ .TmpfsSizeBytes = 1024,
+ .TmpfsMode = 0700,
+ };
+ VERIFY_NO_THROW(mount::ValidateMountSpec(tmpfs));
+
+ const mount::Spec duplicateMounts[] = {
+ {.MountType = mount::Type::Tmpfs, .Target = "/data"},
+ {.MountType = mount::Type::Volume, .Source = L"data-volume", .Target = "/data/"},
+ };
+ try
+ {
+ mount::ValidateMountCollection(duplicateMounts);
+ VERIFY_FAIL(L"Expected MountValidationException for duplicate destinations");
+ }
+ catch (const mount::MountValidationException& ex)
+ {
+ VERIFY_ARE_EQUAL(static_cast(mount::ValidationError::DuplicateDestination), static_cast(ex.Error()));
+ VERIFY_ARE_EQUAL(std::string("/data"), ex.Destination());
+ }
+ }
+
+ TEST_METHOD(Mount_DuplicateDestinationsAreRejected)
+ {
+ ContainerOptions options;
+ options.Mounts = {
+ mount::ParseDockerTmpfsString(L"/data"),
+ {.MountType = mount::Type::Volume, .Source = L"data-volume", .Target = "/data/"},
+ };
+ VERIFY_THROWS(ValidateUniqueMountDestinations(options), wil::ResultException);
+
+ options.Mounts = {
+ {.MountType = mount::Type::Tmpfs, .Target = "/data/../cache"},
+ {.MountType = mount::Type::Volume, .Source = L"data-volume", .Target = "/cache"},
+ };
+ VERIFY_THROWS(ValidateUniqueMountDestinations(options), wil::ResultException);
+ }
+
+ TEST_METHOD(Mount_UniqueDestinationsAreAccepted)
+ {
+ ContainerOptions options;
+ options.Mounts = {
+ mount::ParseDockerTmpfsString(L"/cache"),
+ {.MountType = mount::Type::Volume, .Source = L"data-volume", .Target = "/data"},
+ {.MountType = mount::Type::Bind, .Source = L"C:\\logs", .Target = "/logs"},
+ };
+ VERIFY_NO_THROW(ValidateUniqueMountDestinations(options));
+ }
+};
+
+} // namespace WSLCCLIMountParserUnitTests
diff --git a/test/windows/wslc/WSLCCLITmpfsParserUnitTests.cpp b/test/windows/wslc/WSLCCLITmpfsParserUnitTests.cpp
index bf96f8374..ddf26b410 100644
--- a/test/windows/wslc/WSLCCLITmpfsParserUnitTests.cpp
+++ b/test/windows/wslc/WSLCCLITmpfsParserUnitTests.cpp
@@ -13,10 +13,9 @@ Module Name:
#include "precomp.h"
#include "windows/Common.h"
-#include "WSLCCLITestHelpers.h"
-#include "ContainerModel.h"
+#include "MountSpecParsing.h"
-using namespace wsl::windows::wslc;
+using namespace wsl::windows::common;
namespace WSLCCLITmpfsParserUnitTests {
@@ -26,26 +25,40 @@ class WSLCCLITmpfsParserUnitTests
TEST_METHOD(WSLCCLITmpfsMount_Parse)
{
- std::vector> validTmpfsSpecs = {
- {"", "", ""},
- {"/tmp", "/tmp", ""},
- {"/tmp:size=50m", "/tmp", "size=50m"},
- {"/var/tmp:size=1g", "/var/tmp", "size=1g"},
- {"/tmp:size=50m,mode=1777", "/tmp", "size=50m,mode=1777"},
- {"/cache:uid=1000,gid=1000", "/cache", "uid=1000,gid=1000"},
- {"/mnt/ramdisk:size=256k,nr_inodes=1k", "/mnt/ramdisk", "size=256k,nr_inodes=1k"},
- {"/securetmp:mode=0700", "/securetmp", "mode=0700"},
- {"/scratch:nosuid,nodev,noexec", "/scratch", "nosuid,nodev,noexec"},
- {"/wsl/tmp:size=2g,uid=0,gid=0,mode=1777", "/wsl/tmp", "size=2g,uid=0,gid=0,mode=1777"},
+ const std::vector> validTmpfsSpecs = {
+ {L"", "", ""},
+ {L"/tmp", "/tmp", ""},
+ {L"/tmp:size=50m", "/tmp", "size=50m"},
+ {L"/var/tmp:size=1g", "/var/tmp", "size=1g"},
+ {L"/tmp:size=50m,mode=1777", "/tmp", "size=50m,mode=1777"},
+ {L"/cache:uid=1000,gid=1000", "/cache", "uid=1000,gid=1000"},
+ {L"/mnt/ramdisk:size=256k,nr_inodes=1k", "/mnt/ramdisk", "size=256k,nr_inodes=1k"},
+ {L"/securetmp:mode=0700", "/securetmp", "mode=0700"},
+ {L"/scratch:nosuid,nodev,noexec", "/scratch", "nosuid,nodev,noexec"},
+ {L"/wsl/tmp:size=2g,uid=0,gid=0,mode=1777", "/wsl/tmp", "size=2g,uid=0,gid=0,mode=1777"},
};
- for (const auto& [input, expectedContainerPath, expectedOptions] : validTmpfsSpecs)
+ for (const auto& [input, expectedTarget, expectedOptions] : validTmpfsSpecs)
{
- auto result = models::TmpfsMount::Parse(input);
- VERIFY_ARE_EQUAL(expectedContainerPath, result.ContainerPath());
- VERIFY_ARE_EQUAL(expectedOptions, result.Options());
+ const auto result = mount::ParseDockerTmpfsString(input);
+ VERIFY_ARE_EQUAL(static_cast(mount::Type::Tmpfs), static_cast(result.MountType));
+ VERIFY_ARE_EQUAL(expectedTarget, result.Target);
+ VERIFY_IS_TRUE(result.TmpfsOptions.has_value());
+ VERIFY_ARE_EQUAL(expectedOptions, result.TmpfsOptions.value());
}
}
+
+ TEST_METHOD(WSLCCLITmpfsMount_Validate)
+ {
+ auto valid = mount::ParseDockerTmpfsString(L"/tmp:size=50m");
+ VERIFY_NO_THROW(mount::ValidateMountSpec(valid));
+
+ auto empty = mount::ParseDockerTmpfsString(L":size=50m");
+ VERIFY_THROWS(mount::ValidateMountSpec(empty), mount::MountValidationException);
+
+ auto relative = mount::ParseDockerTmpfsString(L"tmp:size=50m");
+ VERIFY_THROWS(mount::ValidateMountSpec(relative), mount::MountValidationException);
+ }
};
-} // namespace WSLCCLITmpfsParserUnitTests
\ No newline at end of file
+} // namespace WSLCCLITmpfsParserUnitTests
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
index 747d6d0cf..90a4a743d 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
@@ -46,6 +46,7 @@ class WSLCE2EContainerCreateTests
EnsureImageIsDeleted(AlpineImage);
EnsureImageIsDeleted(DebianImage);
EnsureImageIsDeleted(HelloWorldImage);
+ EnsureVolumeDoesNotExist(WslcVolumeName);
EnsureNetworkDoesNotExist(TestNetworkName);
VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), nullptr));
@@ -61,6 +62,7 @@ class WSLCE2EContainerCreateTests
VolumeTestFile1 = wsl::windows::common::filesystem::GetTempFilename();
VolumeTestFile2 = wsl::windows::common::filesystem::GetTempFilename();
EnsureContainerDoesNotExist(WslcContainerName);
+ EnsureVolumeDoesNotExist(WslcVolumeName);
EnsureNetworkDoesNotExist(TestNetworkName);
return true;
}
@@ -352,8 +354,7 @@ class WSLCE2EContainerCreateTests
RunWslc(std::format(L"container run --name {} --volume :/containerPath {}", WslcContainerName, AlpineImage.NameAndTag()));
result.Verify({.Stdout = L"", .ExitCode = 1});
VERIFY_IS_TRUE(result.StderrContainsSubstring(
- L"Invalid volume specifications: ':/containerPath'. Host path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
+ Localization::WSLCCLI_VolumeHostPathEmpty(L":/containerPath", Localization::WSLCCLI_VolumeFormatUsage())));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -362,8 +363,7 @@ class WSLCE2EContainerCreateTests
std::format(L"container run --name {} --volume C:\\hostPath::ro {}", WslcContainerName, AlpineImage.NameAndTag()));
result.Verify({.Stdout = L"", .ExitCode = 1});
VERIFY_IS_TRUE(result.StderrContainsSubstring(
- L"Invalid volume specifications: 'C:\\hostPath::ro'. Container path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
+ Localization::WSLCCLI_VolumeContainerPathEmpty(L"C:\\hostPath::ro", Localization::WSLCCLI_VolumeFormatUsage())));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -372,17 +372,15 @@ class WSLCE2EContainerCreateTests
std::format(L"container run --name {} --volume :/containerPath:ro {}", WslcContainerName, AlpineImage.NameAndTag()));
result.Verify({.Stdout = L"", .ExitCode = 1});
VERIFY_IS_TRUE(result.StderrContainsSubstring(
- L"Invalid volume specifications: ':/containerPath:ro'. Host path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
+ Localization::WSLCCLI_VolumeHostPathEmpty(L":/containerPath:ro", Localization::WSLCCLI_VolumeFormatUsage())));
EnsureContainerDoesNotExist(WslcContainerName);
}
{
auto result = RunWslc(std::format(L"container run --name {} --volume \"\" {}", WslcContainerName, AlpineImage.NameAndTag()));
result.Verify({.Stdout = L"", .ExitCode = 1});
- VERIFY_IS_TRUE(
- result.StderrContainsSubstring(L"Invalid volume specifications: ''. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ Localization::WSLCCLI_VolumeInvalidSpec(L"", Localization::WSLCCLI_VolumeFormatUsage())));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -391,8 +389,7 @@ class WSLCE2EContainerCreateTests
RunWslc(std::format(L"container run --name {} --volume C:\\hostPath: {}", WslcContainerName, AlpineImage.NameAndTag()));
result.Verify({.Stdout = L"", .ExitCode = 1});
VERIFY_IS_TRUE(result.StderrContainsSubstring(
- L"Invalid volume specifications: 'C:\\hostPath:'. Container path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
+ Localization::WSLCCLI_VolumeContainerPathEmpty(L"C:\\hostPath:", Localization::WSLCCLI_VolumeFormatUsage())));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -401,17 +398,15 @@ class WSLCE2EContainerCreateTests
RunWslc(std::format(L"container run --name {} --volume C:\\hostPath:ro {}", WslcContainerName, AlpineImage.NameAndTag()));
result.Verify({.Stdout = L"", .ExitCode = 1});
VERIFY_IS_TRUE(result.StderrContainsSubstring(
- L"Invalid volume specifications: 'C:\\hostPath:ro'. Container path must be an absolute path (starting with '/'). "
- L"Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
+ Localization::WSLCCLI_VolumeContainerPathNotAbsolute(L"C:\\hostPath:ro", Localization::WSLCCLI_VolumeFormatUsage())));
EnsureContainerDoesNotExist(WslcContainerName);
}
{
auto result = RunWslc(std::format(L"container run --name {} --volume :ro {}", WslcContainerName, AlpineImage.NameAndTag()));
result.Verify({.Stdout = L"", .ExitCode = 1});
- VERIFY_IS_TRUE(
- result.StderrContainsSubstring(L"Invalid volume specifications: ':ro'. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ Localization::WSLCCLI_VolumeInvalidSpec(L":ro", Localization::WSLCCLI_VolumeFormatUsage())));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -420,8 +415,7 @@ class WSLCE2EContainerCreateTests
std::format(L"container run --name {} --volume C:\\hostPath::rw {}", WslcContainerName, AlpineImage.NameAndTag()));
result.Verify({.Stdout = L"", .ExitCode = 1});
VERIFY_IS_TRUE(result.StderrContainsSubstring(
- L"Invalid volume specifications: 'C:\\hostPath::rw'. Container path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
+ Localization::WSLCCLI_VolumeContainerPathEmpty(L"C:\\hostPath::rw", Localization::WSLCCLI_VolumeFormatUsage())));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -429,10 +423,8 @@ class WSLCE2EContainerCreateTests
auto result = RunWslc(std::format(
L"container run --name {} --volume C:\\hostPath:/containerPath:invalid_mode {}", WslcContainerName, AlpineImage.NameAndTag()));
result.Verify({.Stdout = L"", .ExitCode = 1});
- VERIFY_IS_TRUE(result.StderrContainsSubstring(
- L"Invalid volume specifications: 'C:\\hostPath:/containerPath:invalid_mode'. Container path must be an absolute "
- L"path (starting with '/'). Expected format: :[:mode]\r\nError code: "
- L"E_INVALIDARG"));
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_VolumeContainerPathNotAbsolute(
+ L"C:\\hostPath:/containerPath:invalid_mode", Localization::WSLCCLI_VolumeFormatUsage())));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -440,10 +432,8 @@ class WSLCE2EContainerCreateTests
auto result = RunWslc(std::format(
L"container run --name {} --volume C:\\hostPath:/containerPath:ro:extra {}", WslcContainerName, AlpineImage.NameAndTag()));
result.Verify({.Stdout = L"", .ExitCode = 1});
- VERIFY_IS_TRUE(result.StderrContainsSubstring(
- L"Invalid volume specifications: 'C:\\hostPath:/containerPath:ro:extra'. Container path must be an absolute path "
- L"(starting with '/'). Expected format: :[:mode]\r\nError code: "
- L"E_INVALIDARG"));
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_VolumeContainerPathNotAbsolute(
+ L"C:\\hostPath:/containerPath:ro:extra", Localization::WSLCCLI_VolumeFormatUsage())));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -451,9 +441,8 @@ class WSLCE2EContainerCreateTests
auto result = RunWslc(std::format(
L"container run --name {} --volume C:\\hostPath:/containerPath: {}", WslcContainerName, AlpineImage.NameAndTag()));
result.Verify({.Stdout = L"", .ExitCode = 1});
- VERIFY_IS_TRUE(result.StderrContainsSubstring(
- L"Invalid volume specifications: 'C:\\hostPath:/containerPath:'. Container path cannot be empty. Expected "
- L"format: :[:mode]\r\nError code: E_INVALIDARG"));
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_VolumeContainerPathEmpty(
+ L"C:\\hostPath:/containerPath:", Localization::WSLCCLI_VolumeFormatUsage())));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -462,9 +451,7 @@ class WSLCE2EContainerCreateTests
auto result = RunWslc(
std::format(L"container run --name {} --volume \"::/container:ro\" {}", WslcContainerName, AlpineImage.NameAndTag()));
result.Verify({.Stdout = L"", .ExitCode = 1});
- VERIFY_IS_TRUE(
- result.StderrContainsSubstring(L"Invalid volume specifications: '::/container:ro'. Host path ':' is not a valid "
- L"Windows path.\r\nError code: E_INVALIDARG"));
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_VolumeHostPathInvalid(L"::/container:ro", L":")));
EnsureContainerDoesNotExist(WslcContainerName);
}
}
@@ -479,8 +466,7 @@ class WSLCE2EContainerCreateTests
std::format(L"container run --name {} --volume \"C:\\hostPath\" {}", WslcContainerName, AlpineImage.NameAndTag()));
result.Verify({.Stdout = L"", .ExitCode = 1});
VERIFY_IS_TRUE(result.StderrContainsSubstring(
- L"Invalid volume specifications: 'C:\\hostPath'. Container path must be an absolute path (starting with '/'). "
- L"Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
+ Localization::WSLCCLI_VolumeContainerPathNotAbsolute(L"C:\\hostPath", Localization::WSLCCLI_VolumeFormatUsage())));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -488,8 +474,7 @@ class WSLCE2EContainerCreateTests
auto result = RunWslc(std::format(L"container run --name {} --volume \":\" {}", WslcContainerName, AlpineImage.NameAndTag()));
result.Verify({.Stdout = L"", .ExitCode = 1});
VERIFY_IS_TRUE(result.StderrContainsSubstring(
- L"Invalid volume specifications: ':'. Container path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
+ Localization::WSLCCLI_VolumeContainerPathEmpty(L":", Localization::WSLCCLI_VolumeFormatUsage())));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -499,8 +484,7 @@ class WSLCE2EContainerCreateTests
RunWslc(std::format(L"container run --name {} --volume \"::\" {}", WslcContainerName, AlpineImage.NameAndTag()));
result.Verify({.Stdout = L"", .ExitCode = 1});
VERIFY_IS_TRUE(result.StderrContainsSubstring(
- L"Invalid volume specifications: '::'. Container path cannot be empty. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
+ Localization::WSLCCLI_VolumeContainerPathEmpty(L"::", Localization::WSLCCLI_VolumeFormatUsage())));
EnsureContainerDoesNotExist(WslcContainerName);
}
@@ -508,9 +492,8 @@ class WSLCE2EContainerCreateTests
auto result =
RunWslc(std::format(L"container run --name {} --volume \"e2e_test\" {}", WslcContainerName, AlpineImage.NameAndTag()));
result.Verify({.Stdout = L"", .ExitCode = 1});
- VERIFY_IS_TRUE(
- result.StderrContainsSubstring(L"Invalid volume specifications: 'e2e_test'. Expected format: :[:mode]\r\nError code: E_INVALIDARG"));
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ Localization::WSLCCLI_VolumeInvalidSpec(L"e2e_test", Localization::WSLCCLI_VolumeFormatUsage())));
EnsureContainerDoesNotExist(WslcContainerName);
}
}
@@ -698,8 +681,7 @@ class WSLCE2EContainerCreateTests
auto result =
RunWslc(std::format(L"container create --name {} --tmpfs wslc-tmpfs {}", WslcContainerName, DebianImage.NameAndTag()));
result.Verify({.Stdout = L"", .ExitCode = 1});
- VERIFY_IS_TRUE(result.StderrContainsSubstring(
- L"invalid mount path: 'wslc-tmpfs' mount path must be absolute\r\nError code: E_FAIL"));
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_MountTargetAbsoluteError()));
}
WSLC_TEST_METHOD(WSLCE2E_Container_Create_Tmpfs_EmptyDestination_Fails)
@@ -707,8 +689,242 @@ class WSLCE2EContainerCreateTests
auto result =
RunWslc(std::format(L"container create --name {} --tmpfs :size=64k {}", WslcContainerName, DebianImage.NameAndTag()));
result.Verify({.Stdout = L"", .ExitCode = 1});
- VERIFY_IS_TRUE(
- result.StderrContainsSubstring(L"invalid mount path: '' mount path must be absolute\r\nError code: E_FAIL"));
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_MountTargetRequiredError()));
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_Tmpfs_Success)
+ {
+ auto result = RunWslc(std::format(
+ L"container create --name {} --mount type=tmpfs,target=/path:tmpfs,tmpfs-size=1MB,tmpfs-mode=0700 {} sh -c "
+ L"\"echo -n 'tmpfs_test' > /path:tmpfs/data && cat /path:tmpfs/data && echo && stat -c '%a' /path:tmpfs && "
+ L"df -k /path:tmpfs | awk 'NR == 2 {{print $2}}'\"",
+ WslcContainerName,
+ DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const auto inspect = InspectContainer(WslcContainerName);
+ VERIFY_ARE_EQUAL(1u, inspect.Mounts.size());
+ VERIFY_ARE_EQUAL("tmpfs", inspect.Mounts[0].Type);
+ VERIFY_ARE_EQUAL("", inspect.Mounts[0].Source);
+ VERIFY_ARE_EQUAL("/path:tmpfs", inspect.Mounts[0].Destination);
+ VERIFY_IS_TRUE(inspect.Mounts[0].ReadWrite);
+
+ result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
+ result.Verify({.Stdout = L"tmpfs_test\n700\n1024\n", .Stderr = L"", .ExitCode = 0});
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_Tmpfs_PreservesMountForm)
+ {
+ auto result = RunWslc(std::format(
+ L"container create --name {} --tmpfs /legacy-tmpfs --mount type=tmpfs,target=/modern-tmpfs,readonly {} true",
+ WslcContainerName,
+ DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const auto inspect = InspectContainer(WslcContainerName);
+ VERIFY_ARE_EQUAL(2u, inspect.Mounts.size());
+
+ const auto legacyMount =
+ std::ranges::find_if(inspect.Mounts, [](const auto& mount) { return mount.Destination == "/legacy-tmpfs"; });
+ VERIFY_IS_TRUE(legacyMount != inspect.Mounts.end());
+ VERIFY_ARE_EQUAL("tmpfs", legacyMount->Type);
+ VERIFY_ARE_EQUAL("", legacyMount->Source);
+ VERIFY_IS_TRUE(legacyMount->ReadWrite);
+
+ const auto modernMount =
+ std::ranges::find_if(inspect.Mounts, [](const auto& mount) { return mount.Destination == "/modern-tmpfs"; });
+ VERIFY_IS_TRUE(modernMount != inspect.Mounts.end());
+ VERIFY_ARE_EQUAL("tmpfs", modernMount->Type);
+ VERIFY_ARE_EQUAL("", modernMount->Source);
+ VERIFY_IS_FALSE(modernMount->ReadWrite);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_Bind_Success)
+ {
+ WriteTestFileContent(VolumeTestFile1, "WSLC Mount Bind Test");
+
+ const auto hostDirectory = VolumeTestFile1.parent_path();
+ const auto fileName = VolumeTestFile1.filename().wstring();
+ auto result = RunWslc(std::format(
+ L"container create --name {} --mount \"type=bind,source={},target=/path:mntdir,readonly\" {} cat /path:mntdir/{}",
+ WslcContainerName,
+ hostDirectory.wstring(),
+ DebianImage.NameAndTag(),
+ fileName));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const auto inspect = InspectContainer(WslcContainerName);
+ VERIFY_ARE_EQUAL(1u, inspect.Mounts.size());
+ VERIFY_ARE_EQUAL("bind", inspect.Mounts[0].Type);
+ VERIFY_ARE_EQUAL(std::filesystem::canonical(hostDirectory).string(), inspect.Mounts[0].Source);
+ VERIFY_ARE_EQUAL("/path:mntdir", inspect.Mounts[0].Destination);
+ VERIFY_IS_FALSE(inspect.Mounts[0].ReadWrite);
+
+ result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
+ result.Verify({.Stdout = L"WSLC Mount Bind Test", .Stderr = L"", .ExitCode = 0});
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_Bind_MissingSource_Fails)
+ {
+ const auto source = VolumeTestFile1;
+ VERIFY_IS_TRUE(DeleteFileW(source.c_str()));
+ auto cleanupSource = wil::scope_exit([&]() {
+ std::error_code error;
+ std::filesystem::remove_all(source, error);
+ });
+
+ 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});
+ VERIFY_IS_FALSE(std::filesystem::exists(source));
+ EnsureContainerDoesNotExist(WslcContainerName);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_MissingSource_CreatesDirectory)
+ {
+ const auto source = VolumeTestFile1;
+ VERIFY_IS_TRUE(DeleteFileW(source.c_str()));
+ auto cleanupSource = wil::scope_exit([&]() {
+ std::error_code error;
+ std::filesystem::remove_all(source, error);
+ });
+
+ 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);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_Volume_Success)
+ {
+ auto result = RunWslc(std::format(
+ L"container create --name {} --mount type=volume,source={},target=/path:voldir {} sh -c \"echo -n 'WSLC Mount Volume "
+ L"Test' > /path:voldir/test.txt\"",
+ WslcContainerName,
+ WslcVolumeName,
+ DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const auto inspect = InspectContainer(WslcContainerName);
+ VERIFY_ARE_EQUAL(1u, inspect.Mounts.size());
+ VERIFY_ARE_EQUAL("volume", inspect.Mounts[0].Type);
+ VERIFY_ARE_EQUAL(string::WideToMultiByte(WslcVolumeName), inspect.Mounts[0].Source);
+ VERIFY_ARE_EQUAL("/path:voldir", inspect.Mounts[0].Destination);
+ VERIFY_IS_TRUE(inspect.Mounts[0].ReadWrite);
+
+ result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
+ result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
+ EnsureContainerDoesNotExist(WslcContainerName);
+
+ result = RunWslc(std::format(
+ L"container create --name {} --mount type=volume,source={},target=/path:voldir {} cat /path:voldir/test.txt",
+ WslcContainerName,
+ WslcVolumeName,
+ DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
+ result.Verify({.Stdout = L"WSLC Mount Volume Test", .Stderr = L"", .ExitCode = 0});
+ EnsureContainerDoesNotExist(WslcContainerName);
+
+ result = RunWslc(std::format(
+ L"container create --rm --name {} --mount type=volume,target=/anonymous {} sh -c "
+ L"\"echo -n anonymous-volume > /anonymous/value && cat /anonymous/value\"",
+ WslcContainerName,
+ DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const auto anonymousInspect = InspectContainer(WslcContainerName);
+ VERIFY_ARE_EQUAL(1u, anonymousInspect.Mounts.size());
+ VERIFY_ARE_EQUAL("volume", anonymousInspect.Mounts[0].Type);
+ VERIFY_IS_FALSE(anonymousInspect.Mounts[0].Name.empty());
+ VERIFY_IS_TRUE(anonymousInspect.Mounts[0].Source.empty());
+ VERIFY_ARE_EQUAL("/anonymous", anonymousInspect.Mounts[0].Destination);
+ VERIFY_IS_TRUE(anonymousInspect.Mounts[0].ReadWrite);
+
+ result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
+ result.Verify({.Stdout = L"anonymous-volume", .Stderr = L"", .ExitCode = 0});
+ EnsureContainerDoesNotExist(WslcContainerName);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_ReadOnly_IsReadOnly)
+ {
+ auto result = RunWslc(std::format(
+ L"container create --name {} --mount type=volume,source={},target=/data {} sh -c \"echo -n original > /data/value\"",
+ WslcContainerName,
+ WslcVolumeName,
+ DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
+ result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
+ EnsureContainerDoesNotExist(WslcContainerName);
+
+ result = RunWslc(std::format(
+ L"container create --name {} --mount type=volume,source={},target=/data,readonly {} sh -c \"echo changed > "
+ L"/data/value\"",
+ WslcContainerName,
+ WslcVolumeName,
+ DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
+ result.Verify({.Stdout = L"", .Stderr = L"sh: 1: cannot create /data/value: Read-only file system\n", .ExitCode = 2});
+ EnsureContainerDoesNotExist(WslcContainerName);
+
+ result = RunWslc(std::format(
+ L"container create --name {} --mount type=volume,source={},target=/data {} cat /data/value",
+ WslcContainerName,
+ WslcVolumeName,
+ DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
+ result.Verify({.Stdout = L"original", .Stderr = L"", .ExitCode = 0});
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_InvalidType_Fails)
+ {
+ constexpr auto mount = L"type=bogus,target=/x";
+ auto result =
+ RunWslc(std::format(L"container create --name {} --mount {} {} true", WslcContainerName, mount, DebianImage.NameAndTag()));
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ Localization::WSLCCLI_UnsupportedMountError(mount, Localization::WSLCCLI_MountTypeUnsupportedError(L"bogus"))));
+ EnsureContainerDoesNotExist(WslcContainerName);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_RelativeTarget_Fails)
+ {
+ constexpr auto mount = L"type=tmpfs,target=data";
+ auto result =
+ RunWslc(std::format(L"container create --name {} --mount {} {} true", WslcContainerName, mount, DebianImage.NameAndTag()));
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ Localization::WSLCCLI_InvalidMountError(mount, Localization::WSLCCLI_MountTargetAbsoluteError())));
+ EnsureContainerDoesNotExist(WslcContainerName);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_DuplicateDestination_Fails)
+ {
+ constexpr std::wstring_view duplicateMountArguments[] = {
+ L"--mount type=tmpfs,target=/data --mount type=tmpfs,target=/data/",
+ L"--tmpfs /data --volume data-volume:/data/",
+ L"--tmpfs /data --mount type=volume,source=data-volume,target=/data/",
+ L"--volume data-volume:/data --mount type=tmpfs,target=/data/",
+ };
+
+ for (const auto arguments : duplicateMountArguments)
+ {
+ const auto result =
+ RunWslc(std::format(L"container create --name {} {} {} true", WslcContainerName, arguments, DebianImage.NameAndTag()));
+ result.Verify({.Stdout = L"", .Stderr = FormatWslcError(Localization::WSLCCLI_DuplicateMountDestinationError(L"/data")), .ExitCode = 1});
+ EnsureContainerDoesNotExist(WslcContainerName);
+ }
}
WSLC_TEST_METHOD(WSLCE2E_Container_Create_WorkDir)
@@ -1571,6 +1787,9 @@ while True:
// Test network name
const std::wstring TestNetworkName = L"wslc-test-network";
+ // Test named volume
+ const std::wstring WslcVolumeName = L"wslc-test-volume";
+
// Test environment variables
const std::wstring HostEnvVariableName = L"WSLC_TEST_HOST_ENV";
const std::wstring HostEnvVariableName2 = L"WSLC_TEST_HOST_ENV2";
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
index 84e50f542..d1d84407c 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
@@ -17,6 +17,7 @@ Module Name:
#include "WSLCE2EHelpers.h"
namespace WSLCE2ETests {
+using namespace wsl::shared;
class WSLCE2EContainerRunTests
{
@@ -1071,6 +1072,81 @@ class WSLCE2EContainerRunTests
result.Verify({.Stderr = L"", .ExitCode = 0});
}
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Mount_Tmpfs_Success)
+ {
+ auto result = RunWslc(std::format(
+ L"container run --rm --mount type=tmpfs,target=/wslc-tmpfs {} sh -c \"echo -n 'tmpfs_test' > /wslc-tmpfs/data && cat "
+ L"/wslc-tmpfs/data\"",
+ DebianImage.NameAndTag()));
+ result.Verify({.Stdout = L"tmpfs_test", .Stderr = L"", .ExitCode = 0});
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Mount_Bind_Success)
+ {
+ WriteTestFileContent(EnvTestFile1, "WSLC Mount Bind Test");
+
+ const auto hostDirectory = EnvTestFile1.parent_path();
+ const auto fileName = EnvTestFile1.filename().wstring();
+ auto result = RunWslc(std::format(
+ L"container run --rm --mount \"type=bind,source={},target=/data,readonly\" {} cat /data/{}",
+ hostDirectory.wstring(),
+ DebianImage.NameAndTag(),
+ fileName));
+ result.Verify({.Stdout = L"WSLC Mount Bind Test", .Stderr = L"", .ExitCode = 0});
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Mount_Volume_Success)
+ {
+ auto result = RunWslc(std::format(
+ L"container run --rm --mount type=volume,source={},target=/data {} sh -c \"echo -n 'WSLC Mount Volume Test' > "
+ L"/data/test.txt\"",
+ WslcVolumeName,
+ DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ result = RunWslc(std::format(
+ L"container run --rm --mount type=volume,source={},target=/data {} cat /data/test.txt", WslcVolumeName, DebianImage.NameAndTag()));
+ result.Verify({.Stdout = L"WSLC Mount Volume Test", .Stderr = L"", .ExitCode = 0});
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Mount_ReadOnly_IsReadOnly)
+ {
+ auto result = RunWslc(std::format(
+ L"container run --rm --mount type=volume,source={},target=/data {} sh -c \"echo -n original > /data/value\"",
+ WslcVolumeName,
+ DebianImage.NameAndTag()));
+ result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
+
+ result = RunWslc(std::format(
+ L"container run --rm --mount type=volume,source={},target=/data,readonly {} sh -c \"echo changed > /data/value\"",
+ WslcVolumeName,
+ DebianImage.NameAndTag()));
+ result.Verify({.Stdout = L"", .Stderr = L"sh: 1: cannot create /data/value: Read-only file system\n", .ExitCode = 2});
+
+ result = RunWslc(std::format(
+ L"container run --rm --mount type=volume,source={},target=/data {} cat /data/value", WslcVolumeName, DebianImage.NameAndTag()));
+ result.Verify({.Stdout = L"original", .Stderr = L"", .ExitCode = 0});
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Mount_InvalidType_Fails)
+ {
+ constexpr auto mount = L"type=bogus,target=/x";
+ auto result = RunWslc(std::format(L"container run --rm --mount {} {} true", mount, DebianImage.NameAndTag()));
+ result.Verify({.Stdout = L"", .ExitCode = 1});
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
+ Localization::WSLCCLI_UnsupportedMountError(mount, Localization::WSLCCLI_MountTypeUnsupportedError(L"bogus"))));
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Mount_DuplicateDestination_Fails)
+ {
+ auto result = RunWslc(std::format(
+ L"container run --rm --name {} --mount type=tmpfs,target=/data --mount type=tmpfs,target=/data/ {} true",
+ WslcContainerName,
+ DebianImage.NameAndTag()));
+ result.Verify({.Stdout = L"", .Stderr = FormatWslcError(Localization::WSLCCLI_DuplicateMountDestinationError(L"/data")), .ExitCode = 1});
+ EnsureContainerDoesNotExist(WslcContainerName);
+ }
+
WSLC_TEST_METHOD(WSLCE2E_Container_Run_WithLabel_Success)
{
auto result = RunWslc(std::format(
diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.h b/test/windows/wslc/e2e/WSLCE2EHelpers.h
index 9c692aab0..355574302 100644
--- a/test/windows/wslc/e2e/WSLCE2EHelpers.h
+++ b/test/windows/wslc/e2e/WSLCE2EHelpers.h
@@ -23,6 +23,11 @@ Module Name:
namespace WSLCE2ETests {
+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);
+}
+
// VT sequence constants and helpers for TTY testing.
// Sequences are sourced from wsl::windows::common::vt (VTSupport.h).
namespace VT {