CLI: Add --mount support for create and run (#41337)

David Bennett committed Aug 17, 2026 at 19:17 UTC 84f479b1afb36cf7c37208ca344a84234b7aa05d
27 files changed +2432 -334
localization/strings/en-US/Resources.resw
+101
@@ -2496,6 +2496,14 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2496 <value>Failed to create volume '{}': {}</value>
2497 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2498 </data>
2499 + <data name="MessageWslcBindSourcePathNotFound" xml:space="preserve">
2500 + <value>Bind source path does not exist: '{}'</value>
2501 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2502 + </data>
2503 + <data name="MessageWslcBindSourcePathError" xml:space="preserve">
2504 + <value>Failed to access bind source path '{}': {}</value>
2505 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2506 + </data>
2507 <data name = "MessageWslcTooManyVirtioFsShares" xml:space = "preserve" >
2508 <value>Too many volumes have been mounted (limit: {}). Restart the session to mount more volumes. This will be fixed in a future release.</value>
2509 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
@@ -3005,6 +3013,96 @@ On first run, creates the file with all settings commented out at their defaults
3013 <data name="WSLCCLI_InvalidFilterError" xml:space="preserve">
3014 <value>Invalid value "{}" for the '-f, --filter' option: bad format of filter (expected name=value)</value>
3015 <comment>{FixedPlaceholder="{}"}{Locked="--filter'"}Command line arguments, file names and string inserts should not be translated</comment> </data>
3016 + <data name="WSLCCLI_InvalidMountError" xml:space="preserve">
3017 + <value>Invalid argument "{}" for '--mount' option: {}</value>
3018 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}{Locked="--mount'"}Command line arguments, file names and string inserts should not be translated</comment>
3019 + </data>
3020 + <data name="WSLCCLI_InvalidTmpfsError" xml:space="preserve">
3021 + <value>Invalid argument "{}" for '--tmpfs' option: {}</value>
3022 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}{Locked="--tmpfs'"}Command line arguments, file names and string inserts should not be translated</comment>
3023 + </data>
3024 + <data name="WSLCCLI_UnsupportedMountError" xml:space="preserve">
3025 + <value>Argument "{}" for '--mount' option uses an unsupported feature: {}</value>
3026 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}{Locked="--mount'"}Command line arguments, file names and string inserts should not be translated</comment>
3027 + </data>
3028 + <data name="WSLCCLI_DuplicateMountDestinationError" xml:space="preserve">
3029 + <value>Duplicate mount point: {}</value>
3030 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3031 + </data>
3032 + <data name="WSLCCLI_MountMalformedCsvError" xml:space="preserve">
3033 + <value>The CSV input is malformed.</value>
3034 + <comment>{Locked="CSV"}Command line arguments should not be translated</comment>
3035 + </data>
3036 + <data name="WSLCCLI_MountFieldKeyValueRequiredError" xml:space="preserve">
3037 + <value>The field '{}' must be a key=value pair.</value>
3038 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3039 + </data>
3040 + <data name="WSLCCLI_MountUnexpectedKeyError" xml:space="preserve">
3041 + <value>The key '{}' is unexpected in '{}'.</value>
3042 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3043 + </data>
3044 + <data name="WSLCCLI_MountInvalidValueError" xml:space="preserve">
3045 + <value>The value for '{}' is invalid: '{}'.</value>
3046 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3047 + </data>
3048 + <data name="WSLCCLI_MountInvalidBindRecursiveValueError" xml:space="preserve">
3049 + <value>The value for '{}' is invalid: '{}'. Valid values are "enabled", "disabled", "writable", and "readonly".</value>
3050 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3051 + </data>
3052 + <data name="WSLCCLI_MountTypeRequiredError" xml:space="preserve">
3053 + <value>The mount type is required.</value>
3054 + </data>
3055 + <data name="WSLCCLI_MountOptionFamilyMismatchError" xml:space="preserve">
3056 + <value>Options matching '{}' cannot be used with mount type '{}'.</value>
3057 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3058 + </data>
3059 + <data name="WSLCCLI_MountOptionRequiresReadonlyError" xml:space="preserve">
3060 + <value>Option '{}' requires the 'readonly' option.</value>
3061 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3062 + </data>
3063 + <data name="WSLCCLI_MountBindRecursiveReadonlyRequiresPropagationError" xml:space="preserve">
3064 + <value>Option 'bind-recursive=readonly' requires the 'bind-propagation=rprivate' option.</value>
3065 + <comment>{Locked="bind-recursive=readonly"}{Locked="bind-propagation=rprivate"}Command line arguments should not be translated</comment>
3066 + </data>
3067 + <data name="WSLCCLI_MountTypeUnsupportedError" xml:space="preserve">
3068 + <value>Mount type '{}' is not supported.</value>
3069 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3070 + </data>
3071 + <data name="WSLCCLI_MountOptionUnsupportedError" xml:space="preserve">
3072 + <value>Option '{}' is not supported.</value>
3073 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3074 + </data>
3075 + <data name="WSLCCLI_MountTargetRequiredError" xml:space="preserve">
3076 + <value>The mount target is required.</value>
3077 + </data>
3078 + <data name="WSLCCLI_MountTargetAbsoluteError" xml:space="preserve">
3079 + <value>The mount target path must be absolute.</value>
3080 + </data>
3081 + <data name="WSLCCLI_MountTmpfsOptionsTypeError" xml:space="preserve">
3082 + <value>The tmpfs options are supported only for tmpfs mounts.</value>
3083 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
3084 + </data>
3085 + <data name="WSLCCLI_MountTmpfsSizeNegativeError" xml:space="preserve">
3086 + <value>The tmpfs size cannot be negative.</value>
3087 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
3088 + </data>
3089 + <data name="WSLCCLI_MountSourceRequiredError" xml:space="preserve">
3090 + <value>The mount source is required.</value>
3091 + </data>
3092 + <data name="WSLCCLI_MountBindSourceAbsoluteError" xml:space="preserve">
3093 + <value>The bind source path must be absolute.</value>
3094 + <comment>{Locked="bind"}Command line arguments should not be translated</comment>
3095 + </data>
3096 + <data name="WSLCCLI_MountVolumeSourceInvalidError" xml:space="preserve">
3097 + <value>The volume source must be a valid named volume.</value>
3098 + </data>
3099 + <data name="WSLCCLI_MountTmpfsSourceUnsupportedError" xml:space="preserve">
3100 + <value>The source option is not supported for tmpfs mounts.</value>
3101 + <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
3102 + </data>
3103 + <data name="WSLCCLI_MountTypeUnsupportedGenericError" xml:space="preserve">
3104 + <value>The mount type is not supported.</value>
3105 + </data>
3106 <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
3107 <value>Follow log output</value>
3108 </data>
@@ -3073,6 +3171,9 @@ On first run, creates the file with all settings commented out at their defaults
3171 <value>Memory limit (e.g. 512M, 1G)</value>
3172 <comment>{Locked="512M"}{Locked="1G"}Command line argument example values should not be translated</comment>
3173 </data>
3174 + <data name="WSLCCLI_MountArgDescription" xml:space="preserve">
3175 + <value>Attach a filesystem mount to the container</value>
3176 + </data>
3177 <data name="WSLCCLI_HostnameArgDescription" xml:space="preserve">
3178 <value>Container host name</value>
3179 </data>
src/windows/common/CMakeLists.txt
+2
@@ -26,6 +26,7 @@ set(SOURCES
26 LxssMessagePort.cpp
27 LxssSecurity.cpp
28 LxssServerPort.cpp
29 + MountSpecParsing.cpp
30 NatNetworking.cpp
31 notifications.cpp
32 Redirector.cpp
@@ -111,6 +112,7 @@ set(HEADERS
112 LxssPort.h
113 LxssSecurity.h
114 LxssServerPort.h
115 + MountSpecParsing.h
116 NatNetworking.h
117 notifications.h
118 precomp.h
src/windows/common/MountSpecParsing.cpp new
+690
@@ -0,0 +1,690 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + MountSpecParsing.cpp
8 +
9 +Abstract:
10 +
11 + Docker-compatible mount specification parsing.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "MountSpecParsing.h"
17 +#include "string.hpp"
18 +#include <algorithm>
19 +#include <array>
20 +#include <filesystem>
21 +#include <format>
22 +#include <limits>
23 +#include <regex>
24 +#include <unordered_set>
25 +#include <vector>
26 +
27 +using namespace wsl::shared;
28 +using namespace wsl::shared::string;
29 +
30 +namespace wsl::windows::common::mount {
31 +
32 +namespace {
33 +
34 + enum class Field
35 + {
36 + Type,
37 + Source,
38 + Target,
39 + ReadOnly,
40 + Consistency,
41 + BindPropagation,
42 + BindNonRecursive,
43 + BindRecursive,
44 + VolumeNoCopy,
45 + VolumeLabel,
46 + VolumeDriver,
47 + VolumeOption,
48 + TmpfsSize,
49 + TmpfsMode,
50 + };
51 +
52 + enum class Family
53 + {
54 + General,
55 + Bind,
56 + Volume,
57 + Tmpfs,
58 + };
59 +
60 + enum class Support
61 + {
62 + Supported,
63 + Unsupported,
64 + ValueDependent,
65 + };
66 +
67 + struct FieldDefinition
68 + {
69 + std::wstring_view Name;
70 + Field Id;
71 + Family OptionFamily;
72 + bool AllowsBareForm;
73 + Support SupportLevel;
74 + };
75 +
76 + // Keep this table aligned with docker/cli v25.0.3 opts/mount.go. It is the
77 + // authoritative list of accepted fields, aliases, option families, and WSLC support.
78 + constexpr std::array c_fieldDefinitions{
79 + FieldDefinition{L"type", Field::Type, Family::General, false, Support::ValueDependent},
80 + FieldDefinition{L"source", Field::Source, Family::General, false, Support::Supported},
81 + FieldDefinition{L"src", Field::Source, Family::General, false, Support::Supported},
82 + FieldDefinition{L"target", Field::Target, Family::General, false, Support::Supported},
83 + FieldDefinition{L"dst", Field::Target, Family::General, false, Support::Supported},
84 + FieldDefinition{L"destination", Field::Target, Family::General, false, Support::Supported},
85 + FieldDefinition{L"readonly", Field::ReadOnly, Family::General, true, Support::Supported},
86 + FieldDefinition{L"ro", Field::ReadOnly, Family::General, true, Support::Supported},
87 + FieldDefinition{L"consistency", Field::Consistency, Family::General, false, Support::Unsupported},
88 + FieldDefinition{L"bind-propagation", Field::BindPropagation, Family::Bind, false, Support::Unsupported},
89 + FieldDefinition{L"bind-nonrecursive", Field::BindNonRecursive, Family::Bind, true, Support::Unsupported},
90 + FieldDefinition{L"bind-recursive", Field::BindRecursive, Family::Bind, false, Support::ValueDependent},
91 + FieldDefinition{L"volume-nocopy", Field::VolumeNoCopy, Family::Volume, true, Support::Unsupported},
92 + FieldDefinition{L"volume-label", Field::VolumeLabel, Family::Volume, false, Support::Unsupported},
93 + FieldDefinition{L"volume-driver", Field::VolumeDriver, Family::Volume, false, Support::Unsupported},
94 + FieldDefinition{L"volume-opt", Field::VolumeOption, Family::Volume, false, Support::Unsupported},
95 + FieldDefinition{L"tmpfs-size", Field::TmpfsSize, Family::Tmpfs, false, Support::Supported},
96 + FieldDefinition{L"tmpfs-mode", Field::TmpfsMode, Family::Tmpfs, false, Support::Supported},
97 + };
98 +
99 + struct DockerMountSpec
100 + {
101 + std::wstring Type = L"volume";
102 + std::wstring Source;
103 + std::wstring Target;
104 + bool ReadOnly = false;
105 + bool HasVolumeOptions = false;
106 + bool HasBindOptions = false;
107 + bool HasTmpfsOptions = false;
108 + bool BindReadOnlyNonRecursive = false;
109 + bool BindReadOnlyForceRecursive = false;
110 + std::wstring BindPropagation;
111 + std::optional<int64_t> TmpfsSizeBytes;
112 + std::optional<uint32_t> TmpfsMode;
113 + std::optional<std::wstring> UnsupportedOption;
114 + };
115 +
116 + struct KeyValue
117 + {
118 + std::wstring Key;
119 + std::wstring Value;
120 + bool HadSeparator;
121 + };
122 +
123 + [[noreturn]] void ThrowValidation(std::wstring reason)
124 + {
125 + throw MountValidationException(std::move(reason));
126 + }
127 +
128 + [[noreturn]] void ThrowParse(std::wstring reason)
129 + {
130 + throw MountParseException(std::move(reason));
131 + }
132 +
133 + [[noreturn]] void ThrowUnsupported(std::wstring reason)
134 + {
135 + throw MountUnsupportedException(std::move(reason));
136 + }
137 +
138 + KeyValue SplitKeyValue(const std::wstring& value)
139 + {
140 + const auto position = value.find(L'=');
141 + if (position == std::wstring::npos)
142 + {
143 + return {.Key = value, .HadSeparator = false};
144 + }
145 +
146 + return {.Key = value.substr(0, position), .Value = value.substr(position + 1), .HadSeparator = true};
147 + }
148 +
149 + const FieldDefinition* FindField(std::wstring_view name)
150 + {
151 + const auto found = std::ranges::find_if(c_fieldDefinitions, [&](const auto& definition) { return definition.Name == name; });
152 + return found == c_fieldDefinitions.end() ? nullptr : &*found;
153 + }
154 +
155 + void RecordUnsupportedOption(DockerMountSpec& mount, std::wstring_view option)
156 + {
157 + if (!mount.UnsupportedOption.has_value())
158 + {
159 + mount.UnsupportedOption = option;
160 + }
161 + }
162 +
163 + std::optional<int64_t> ParseDockerRamInBytes(const std::wstring& value)
164 + {
165 + const auto parsed = wsl::windows::common::string::ParseStorageSize(value, wsl::windows::common::string::StorageSizeUnit::Binary);
166 + if (!parsed.has_value() || parsed.value() > static_cast<uint64_t>(std::numeric_limits<int64_t>::max()))
167 + {
168 + return std::nullopt;
169 + }
170 +
171 + return static_cast<int64_t>(parsed.value());
172 + }
173 +
174 + std::optional<uint32_t> ParseDockerTmpfsMode(const std::wstring& value)
175 + {
176 + if (value.empty() || value.front() == L'-')
177 + {
178 + return std::nullopt;
179 + }
180 +
181 + size_t position = value.front() == L'+' ? 1 : 0;
182 + if (position == value.size())
183 + {
184 + return std::nullopt;
185 + }
186 +
187 + uint64_t result = 0;
188 + for (; position < value.size(); ++position)
189 + {
190 + const auto digit = value[position];
191 + if (digit < L'0' || digit > L'7')
192 + {
193 + return std::nullopt;
194 + }
195 +
196 + result = (result * 8) + static_cast<uint64_t>(digit - L'0');
197 + if (result > std::numeric_limits<uint32_t>::max())
198 + {
199 + return std::nullopt;
200 + }
201 + }
202 +
203 + return static_cast<uint32_t>(result);
204 + }
205 +
206 + std::string FormatDockerTmpfsSize(int64_t sizeBytes)
207 + {
208 + for (const auto& [suffix, divisor] : std::array<std::pair<char, int64_t>, 3>{{{'g', 1LL << 30}, {'m', 1LL << 20}, {'k', 1LL << 10}}})
209 + {
210 + if ((sizeBytes % divisor) == 0)
211 + {
212 + return std::format("{}{}", sizeBytes / divisor, suffix);
213 + }
214 + }
215 +
216 + return std::to_string(sizeBytes);
217 + }
218 +
219 +} // namespace
220 +
221 +Spec ParseDockerMountString(const std::wstring& value)
222 +{
223 + const auto fields = SplitCsvFields(value);
224 + if (!fields.has_value())
225 + {
226 + ThrowParse(Localization::WSLCCLI_MountMalformedCsvError());
227 + }
228 +
229 + DockerMountSpec mount;
230 +
231 + for (const auto& field : *fields)
232 + {
233 + const auto keyValue = SplitKeyValue(field);
234 + const auto key = AsciiToLower(std::wstring_view(keyValue.Key));
235 + const auto definition = FindField(key);
236 + if (definition == nullptr)
237 + {
238 + if (!keyValue.HadSeparator)
239 + {
240 + ThrowParse(Localization::WSLCCLI_MountFieldKeyValueRequiredError(field));
241 + }
242 +
243 + ThrowParse(Localization::WSLCCLI_MountUnexpectedKeyError(key, field));
244 + }
245 +
246 + if (!keyValue.HadSeparator && !definition->AllowsBareForm)
247 + {
248 + ThrowParse(Localization::WSLCCLI_MountFieldKeyValueRequiredError(field));
249 + }
250 +
251 + switch (definition->OptionFamily)
252 + {
253 + case Family::General:
254 + break;
255 + case Family::Bind:
256 + mount.HasBindOptions = true;
257 + break;
258 + case Family::Volume:
259 + mount.HasVolumeOptions = true;
260 + break;
261 + case Family::Tmpfs:
262 + mount.HasTmpfsOptions = true;
263 + break;
264 + }
265 +
266 + switch (definition->Id)
267 + {
268 + case Field::Type:
269 + mount.Type = AsciiToLower(std::wstring_view(keyValue.Value));
270 + break;
271 +
272 + case Field::Source:
273 + mount.Source = keyValue.Value;
274 + if (mount.Source == L"." || mount.Source.starts_with(L".\\"))
275 + {
276 + std::error_code error;
277 + auto absolutePath = std::filesystem::absolute(mount.Source, error);
278 + if (!error)
279 + {
280 + mount.Source = absolutePath.lexically_normal().wstring();
281 + }
282 + }
283 + break;
284 +
285 + case Field::Target:
286 + mount.Target = keyValue.Value;
287 + break;
288 +
289 + case Field::ReadOnly:
290 + if (!keyValue.HadSeparator)
291 + {
292 + mount.ReadOnly = true;
293 + break;
294 + }
295 +
296 + if (const auto parsed = ParseBool(keyValue.Value.c_str(), true); parsed.has_value())
297 + {
298 + mount.ReadOnly = parsed.value();
299 + }
300 + else
301 + {
302 + ThrowParse(Localization::WSLCCLI_MountInvalidValueError(key, keyValue.Value));
303 + }
304 + break;
305 +
306 + case Field::Consistency:
307 + break;
308 +
309 + case Field::BindPropagation:
310 + mount.BindPropagation = AsciiToLower(std::wstring_view(keyValue.Value));
311 + break;
312 +
313 + case Field::BindNonRecursive:
314 + if (keyValue.HadSeparator && !ParseBool(keyValue.Value.c_str(), true).has_value())
315 + {
316 + ThrowParse(Localization::WSLCCLI_MountInvalidValueError(key, keyValue.Value));
317 + }
318 +
319 + break;
320 +
321 + case Field::BindRecursive:
322 + if (keyValue.Value == L"enabled")
323 + {
324 + break;
325 + }
326 +
327 + RecordUnsupportedOption(mount, key);
328 + if (keyValue.Value == L"disabled")
329 + {
330 + break;
331 + }
332 + if (keyValue.Value == L"writable")
333 + {
334 + mount.BindReadOnlyNonRecursive = true;
335 + break;
336 + }
337 + if (keyValue.Value == L"readonly")
338 + {
339 + mount.BindReadOnlyForceRecursive = true;
340 + break;
341 + }
342 +
343 + ThrowParse(Localization::WSLCCLI_MountInvalidBindRecursiveValueError(key, keyValue.Value));
344 +
345 + case Field::VolumeNoCopy:
346 + if (keyValue.HadSeparator && !ParseBool(keyValue.Value.c_str(), true).has_value())
347 + {
348 + ThrowParse(Localization::WSLCCLI_MountInvalidValueError(L"volume-nocopy", keyValue.Value));
349 + }
350 +
351 + break;
352 +
353 + case Field::VolumeLabel:
354 + case Field::VolumeDriver:
355 + case Field::VolumeOption:
356 + break;
357 +
358 + case Field::TmpfsSize:
359 + mount.TmpfsSizeBytes = ParseDockerRamInBytes(keyValue.Value);
360 + if (!mount.TmpfsSizeBytes.has_value())
361 + {
362 + ThrowParse(Localization::WSLCCLI_MountInvalidValueError(key, keyValue.Value));
363 + }
364 +
365 + break;
366 +
367 + case Field::TmpfsMode:
368 + mount.TmpfsMode = ParseDockerTmpfsMode(keyValue.Value);
369 + if (!mount.TmpfsMode.has_value())
370 + {
371 + ThrowParse(Localization::WSLCCLI_MountInvalidValueError(key, keyValue.Value));
372 + }
373 +
374 + break;
375 + }
376 +
377 + if (definition->SupportLevel == Support::Unsupported)
378 + {
379 + RecordUnsupportedOption(mount, key);
380 + }
381 + }
382 +
383 + if (mount.Type.empty())
384 + {
385 + ThrowParse(Localization::WSLCCLI_MountTypeRequiredError());
386 + }
387 +
388 + if (mount.HasVolumeOptions && mount.Type != L"volume")
389 + {
390 + ThrowParse(Localization::WSLCCLI_MountOptionFamilyMismatchError(L"volume-*", mount.Type));
391 + }
392 + if (mount.HasBindOptions && mount.Type != L"bind")
393 + {
394 + ThrowParse(Localization::WSLCCLI_MountOptionFamilyMismatchError(L"bind-*", mount.Type));
395 + }
396 + if (mount.HasTmpfsOptions && mount.Type != L"tmpfs")
397 + {
398 + ThrowParse(Localization::WSLCCLI_MountOptionFamilyMismatchError(L"tmpfs-*", mount.Type));
399 + }
400 +
401 + if (mount.BindReadOnlyNonRecursive && !mount.ReadOnly)
402 + {
403 + ThrowParse(Localization::WSLCCLI_MountOptionRequiresReadonlyError(L"bind-recursive=writable"));
404 + }
405 + if (mount.BindReadOnlyForceRecursive)
406 + {
407 + if (!mount.ReadOnly)
408 + {
409 + ThrowParse(Localization::WSLCCLI_MountOptionRequiresReadonlyError(L"bind-recursive=readonly"));
410 + }
411 + if (mount.BindPropagation != L"rprivate")
412 + {
413 + ThrowParse(Localization::WSLCCLI_MountBindRecursiveReadonlyRequiresPropagationError());
414 + }
415 + }
416 +
417 + Type type;
418 + if (mount.Type == L"bind")
419 + {
420 + type = Type::Bind;
421 + }
422 + else if (mount.Type == L"volume")
423 + {
424 + type = Type::Volume;
425 + }
426 + else if (mount.Type == L"tmpfs")
427 + {
428 + type = Type::Tmpfs;
429 + }
430 + else
431 + {
432 + ThrowUnsupported(Localization::WSLCCLI_MountTypeUnsupportedError(mount.Type));
433 + }
434 +
435 + if (mount.UnsupportedOption.has_value())
436 + {
437 + ThrowUnsupported(Localization::WSLCCLI_MountOptionUnsupportedError(mount.UnsupportedOption.value()));
438 + }
439 +
440 + return {
441 + .MountType = type,
442 + .Source = std::move(mount.Source),
443 + .Target = WideToMultiByte(mount.Target),
444 + .ReadOnly = mount.ReadOnly,
445 + .TmpfsSizeBytes = mount.TmpfsSizeBytes,
446 + .TmpfsMode = mount.TmpfsMode,
447 + };
448 +}
449 +
450 +Spec ParseDockerVolumeString(const std::wstring& value)
451 +{
452 + const auto formatUsage = Localization::WSLCCLI_VolumeFormatUsage();
453 + const auto lastColon = value.rfind(L':');
454 + if (lastColon == std::wstring::npos)
455 + {
456 + ThrowParse(Localization::WSLCCLI_VolumeInvalidSpec(value, formatUsage));
457 + }
458 +
459 + auto splitColon = lastColon;
460 + bool readOnly = false;
461 + std::wstring_view lastToken{value.data() + lastColon + 1, value.size() - lastColon - 1};
462 + if (lastToken == L"ro" || lastToken == L"rw")
463 + {
464 + readOnly = lastToken == L"ro";
465 + if (lastColon == 0)
466 + {
467 + ThrowParse(Localization::WSLCCLI_VolumeInvalidSpec(value, formatUsage));
468 + }
469 +
470 + splitColon = value.rfind(L':', lastColon - 1);
471 + if (splitColon == std::wstring::npos)
472 + {
473 + ThrowParse(Localization::WSLCCLI_VolumeInvalidSpec(value, formatUsage));
474 + }
475 + }
476 +
477 + const auto targetEnd = lastToken == L"ro" || lastToken == L"rw" ? lastColon : value.size();
478 + const auto target = value.substr(splitColon + 1, targetEnd - splitColon - 1);
479 + if (target.empty())
480 + {
481 + ThrowParse(Localization::WSLCCLI_VolumeContainerPathEmpty(value, formatUsage));
482 + }
483 +
484 + if (target.front() != L'/')
485 + {
486 + ThrowParse(Localization::WSLCCLI_VolumeContainerPathNotAbsolute(value, formatUsage));
487 + }
488 +
489 + const auto rawSource = value.substr(0, splitColon);
490 + if (rawSource.empty())
491 + {
492 + ThrowParse(Localization::WSLCCLI_VolumeHostPathEmpty(value, formatUsage));
493 + }
494 +
495 + if (IsValidNamedVolumeName(rawSource))
496 + {
497 + return {
498 + .MountType = Type::Volume,
499 + .Source = rawSource,
500 + .Target = WideToMultiByte(target),
501 + .ReadOnly = readOnly,
502 + };
503 + }
504 +
505 + std::wstring source;
506 + if (FAILED(wil::GetFullPathNameW(rawSource.c_str(), source)))
507 + {
508 + ThrowParse(Localization::WSLCCLI_VolumeHostPathInvalid(value, rawSource));
509 + }
510 +
511 + if (GetFileAttributesW(source.c_str()) == INVALID_FILE_ATTRIBUTES && GetLastError() == ERROR_INVALID_NAME)
512 + {
513 + ThrowParse(Localization::WSLCCLI_VolumeHostPathInvalid(value, rawSource));
514 + }
515 +
516 + return {
517 + .MountType = Type::Bind,
518 + .Source = std::move(source),
519 + .Target = WideToMultiByte(target),
520 + .ReadOnly = readOnly,
521 + .BindSource = BindSourcePolicy::CreateIfMissing,
522 + };
523 +}
524 +
525 +Spec ParseDockerTmpfsString(const std::wstring& value)
526 +{
527 + const auto colon = value.find(L':');
528 + const auto target = value.substr(0, colon);
529 + const auto options = colon == std::wstring::npos ? std::wstring_view{} : std::wstring_view{value}.substr(colon + 1);
530 +
531 + return {
532 + .MountType = Type::Tmpfs,
533 + .Target = WideToMultiByte(target),
534 + .TmpfsOptions = WideToMultiByte(std::wstring{options}),
535 + };
536 +}
537 +
538 +void ValidateMountSpec(const Spec& mount)
539 +{
540 + if (mount.Target.empty())
541 + {
542 + ThrowValidation(Localization::WSLCCLI_MountTargetRequiredError());
543 + }
544 +
545 + if (!mount.Target.starts_with('/'))
546 + {
547 + ThrowValidation(Localization::WSLCCLI_MountTargetAbsoluteError());
548 + }
549 +
550 + if (mount.MountType != Type::Tmpfs && (mount.TmpfsSizeBytes.has_value() || mount.TmpfsMode.has_value() || mount.TmpfsOptions.has_value()))
551 + {
552 + ThrowValidation(Localization::WSLCCLI_MountTmpfsOptionsTypeError());
553 + }
554 +
555 + if (mount.TmpfsSizeBytes.has_value() && mount.TmpfsSizeBytes.value() < 0)
556 + {
557 + ThrowValidation(Localization::WSLCCLI_MountTmpfsSizeNegativeError());
558 + }
559 +
560 + switch (mount.MountType)
561 + {
562 + case Type::Bind:
563 + if (mount.Source.empty())
564 + {
565 + ThrowValidation(Localization::WSLCCLI_MountSourceRequiredError());
566 + }
567 +
568 + if (!std::filesystem::path(mount.Source).is_absolute())
569 + {
570 + ThrowValidation(Localization::WSLCCLI_MountBindSourceAbsoluteError());
571 + }
572 + break;
573 +
574 + case Type::Volume:
575 + if (!mount.Source.empty() && !IsValidNamedVolumeName(mount.Source))
576 + {
577 + ThrowValidation(Localization::WSLCCLI_MountVolumeSourceInvalidError());
578 + }
579 + break;
580 +
581 + case Type::Tmpfs:
582 + if (!mount.Source.empty())
583 + {
584 + ThrowValidation(Localization::WSLCCLI_MountTmpfsSourceUnsupportedError());
585 + }
586 + break;
587 +
588 + default:
589 + ThrowUnsupported(Localization::WSLCCLI_MountTypeUnsupportedGenericError());
590 + }
591 +}
592 +
593 +void ValidateMountCollection(std::span<const Spec> mounts)
594 +{
595 + std::unordered_set<std::string> destinations;
596 + for (const auto& mount : mounts)
597 + {
598 + ValidateMountSpec(mount);
599 +
600 + auto destination = NormalizeDestination(mount.Target);
601 + if (!destinations.emplace(destination).second)
602 + {
603 + throw MountValidationException(
604 + ValidationError::DuplicateDestination,
605 + Localization::WSLCCLI_DuplicateMountDestinationError(MultiByteToWide(destination)),
606 + std::move(destination));
607 + }
608 + }
609 +}
610 +
611 +std::string FormatTmpfsOptions(const Spec& mount)
612 +{
613 + WI_ASSERT(mount.MountType == Type::Tmpfs);
614 +
615 + if (mount.TmpfsOptions.has_value())
616 + {
617 + return mount.TmpfsOptions.value();
618 + }
619 +
620 + std::vector<std::string> options;
621 + if (mount.ReadOnly)
622 + {
623 + options.emplace_back("ro");
624 + }
625 + if (mount.TmpfsMode.has_value() && mount.TmpfsMode.value() != 0)
626 + {
627 + options.emplace_back(std::format("mode={:o}", mount.TmpfsMode.value()));
628 + }
629 + if (mount.TmpfsSizeBytes.has_value() && mount.TmpfsSizeBytes.value() != 0)
630 + {
631 + options.emplace_back(std::format("size={}", FormatDockerTmpfsSize(mount.TmpfsSizeBytes.value())));
632 + }
633 +
634 + return wsl::shared::string::Join<char>(options, ',');
635 +}
636 +
637 +std::string NormalizeDestination(std::string destination)
638 +{
639 + std::replace(destination.begin(), destination.end(), '\\', '/');
640 +
641 + std::vector<std::string> components;
642 + size_t start = 0;
643 + while (start <= destination.size())
644 + {
645 + const auto end = destination.find('/', start);
646 + const auto component = destination.substr(start, end - start);
647 + if (!component.empty() && component != ".")
648 + {
649 + if (component == "..")
650 + {
651 + if (!components.empty())
652 + {
653 + components.pop_back();
654 + }
655 + }
656 + else
657 + {
658 + components.emplace_back(component);
659 + }
660 + }
661 +
662 + if (end == std::string::npos)
663 + {
664 + break;
665 + }
666 +
667 + start = end + 1;
668 + }
669 +
670 + std::string result = "/";
671 + for (const auto& component : components)
672 + {
673 + if (result.size() > 1)
674 + {
675 + result += '/';
676 + }
677 +
678 + result += component;
679 + }
680 +
681 + return result;
682 +}
683 +
684 +bool IsValidNamedVolumeName(std::wstring_view name)
685 +{
686 + static const std::wregex c_namedVolumeRegex(LR"(^[a-zA-Z0-9][a-zA-Z0-9_.-]{1,}$)");
687 + return std::regex_match(name.begin(), name.end(), c_namedVolumeRegex);
688 +}
689 +
690 +} // namespace wsl::windows::common::mount
src/windows/common/MountSpecParsing.h new
+125
@@ -0,0 +1,125 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + MountSpecParsing.h
8 +
9 +Abstract:
10 +
11 + Docker-compatible mount specification parsing.
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +#include <cstdint>
18 +#include <exception>
19 +#include <optional>
20 +#include <span>
21 +#include <string>
22 +#include <string_view>
23 +#include <utility>
24 +
25 +namespace wsl::windows::common::mount {
26 +
27 +inline constexpr std::string_view c_dockerCliMountGrammarVersion = "25.0.3";
28 +
29 +enum class Type
30 +{
31 + Bind,
32 + Volume,
33 + Tmpfs,
34 +};
35 +
36 +enum class BindSourcePolicy
37 +{
38 + RequireExisting,
39 + CreateIfMissing,
40 +};
41 +
42 +struct Spec
43 +{
44 + Type MountType = Type::Volume;
45 + std::wstring Source;
46 + std::string Target;
47 + bool ReadOnly = false;
48 + BindSourcePolicy BindSource = BindSourcePolicy::RequireExisting;
49 + std::optional<int64_t> TmpfsSizeBytes;
50 + std::optional<uint32_t> TmpfsMode;
51 + std::optional<std::string> TmpfsOptions;
52 +};
53 +
54 +enum class ValidationError
55 +{
56 + InvalidSpecification,
57 + DuplicateDestination,
58 +};
59 +
60 +class MountException : public std::exception
61 +{
62 +public:
63 + explicit MountException(std::wstring reason) : m_reason(std::move(reason))
64 + {
65 + }
66 +
67 + MountException(ValidationError error, std::wstring reason, std::string destination) :
68 + m_error(error), m_reason(std::move(reason)), m_destination(std::move(destination))
69 + {
70 + }
71 +
72 + const char* what() const noexcept override
73 + {
74 + return "mount error";
75 + }
76 +
77 + const std::wstring& Reason() const noexcept
78 + {
79 + return m_reason;
80 + }
81 +
82 + ValidationError Error() const noexcept
83 + {
84 + return m_error;
85 + }
86 +
87 + const std::string& Destination() const noexcept
88 + {
89 + return m_destination;
90 + }
91 +
92 +private:
93 + ValidationError m_error = ValidationError::InvalidSpecification;
94 + std::wstring m_reason;
95 + std::string m_destination;
96 +};
97 +
98 +class MountParseException : public MountException
99 +{
100 +public:
101 + using MountException::MountException;
102 +};
103 +
104 +class MountUnsupportedException : public MountException
105 +{
106 +public:
107 + using MountException::MountException;
108 +};
109 +
110 +class MountValidationException : public MountException
111 +{
112 +public:
113 + using MountException::MountException;
114 +};
115 +
116 +Spec ParseDockerMountString(const std::wstring& value);
117 +Spec ParseDockerVolumeString(const std::wstring& value);
118 +Spec ParseDockerTmpfsString(const std::wstring& value);
119 +void ValidateMountSpec(const Spec& mount);
120 +void ValidateMountCollection(std::span<const Spec> mounts);
121 +std::string FormatTmpfsOptions(const Spec& mount);
122 +std::string NormalizeDestination(std::string destination);
123 +bool IsValidNamedVolumeName(std::wstring_view name);
124 +
125 +} // namespace wsl::windows::common::mount
src/windows/common/WSLCContainerLauncher.cpp
+69 -34
@@ -229,29 +229,74 @@ void WSLCContainerLauncher::AddUlimit(const std::string& Name, std::int64_t Soft
229
230 void wsl::windows::common::WSLCContainerLauncher::AddVolume(const std::wstring& HostPath, const std::string& ContainerPath, bool ReadOnly)
231 {
232 - // Store a copy of the path strings to the launcher to ensure the pointers in WSLCVolume remain valid.
233 - const auto& hostPath = m_hostPaths.emplace_back(HostPath);
234 - const auto& containerPath = m_containerPaths.emplace_back(ContainerPath);
235 -
236 - WSLCVolume vol{};
237 - vol.HostPath = hostPath.c_str();
238 - vol.ContainerPath = containerPath.c_str();
239 - vol.ReadOnly = ReadOnly ? TRUE : FALSE;
240 -
241 - m_volumes.push_back(vol);
232 + AddMount({
233 + .MountType = mount::Type::Bind,
234 + .Source = HostPath,
235 + .Target = ContainerPath,
236 + .ReadOnly = ReadOnly,
237 + .BindSource = mount::BindSourcePolicy::CreateIfMissing,
238 + });
239 }
240
241 void wsl::windows::common::WSLCContainerLauncher::AddNamedVolume(const std::string& Name, const std::string& ContainerPath, bool ReadOnly)
242 {
246 - const auto& name = m_volumeNames.emplace_back(Name);
247 - const auto& containerPath = m_containerPaths.emplace_back(ContainerPath);
243 + AddMount({
244 + .MountType = mount::Type::Volume,
245 + .Source = wsl::shared::string::MultiByteToWide(Name),
246 + .Target = ContainerPath,
247 + .ReadOnly = ReadOnly,
248 + });
249 +}
250 +
251 +void wsl::windows::common::WSLCContainerLauncher::AddMount(const mount::Spec& Mount)
252 +{
253 + WSLCMountSpec mount{};
254 + switch (Mount.MountType)
255 + {
256 + case mount::Type::Bind:
257 + mount.Type = WSLCMountTypeBind;
258 + break;
259 +
260 + case mount::Type::Volume:
261 + mount.Type = WSLCMountTypeVolume;
262 + break;
263
249 - WSLCNamedVolume volume{};
250 - volume.Name = name.c_str();
251 - volume.ContainerPath = containerPath.c_str();
252 - volume.ReadOnly = ReadOnly ? TRUE : FALSE;
264 + case mount::Type::Tmpfs:
265 + mount.Type = WSLCMountTypeTmpfs;
266 + break;
267 + }
268 +
269 + if (!Mount.Source.empty())
270 + {
271 + mount.Source = m_mountSources.emplace_back(Mount.Source).c_str();
272 + }
273 +
274 + mount.Target = m_mountTargets.emplace_back(Mount.Target).c_str();
275 + mount.ReadOnly = Mount.ReadOnly ? TRUE : FALSE;
276 + if (Mount.MountType == mount::Type::Bind && Mount.BindSource == mount::BindSourcePolicy::CreateIfMissing)
277 + {
278 + WI_SetFlag(mount.Flags, WSLCMountSpecFlagsCreateSourceIfMissing);
279 + }
280
254 - m_namedVolumes.push_back(volume);
281 + if (Mount.TmpfsSizeBytes.has_value())
282 + {
283 + WI_SetFlag(mount.Flags, WSLCMountSpecFlagsTmpfsSize);
284 + mount.TmpfsSizeBytes = Mount.TmpfsSizeBytes.value();
285 + }
286 +
287 + if (Mount.TmpfsMode.has_value())
288 + {
289 + WI_SetFlag(mount.Flags, WSLCMountSpecFlagsTmpfsMode);
290 + mount.TmpfsMode = Mount.TmpfsMode.value();
291 + }
292 +
293 + if (Mount.TmpfsOptions.has_value())
294 + {
295 + WI_SetFlag(mount.Flags, WSLCMountSpecFlagsTmpfsOptions);
296 + mount.TmpfsOptions = m_mountTmpfsOptions.emplace_back(Mount.TmpfsOptions.value()).c_str();
297 + }
298 +
299 + m_mounts.push_back(mount);
300 }
301
302 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
314
315 void wsl::windows::common::WSLCContainerLauncher::AddTmpfs(const std::string& ContainerPath, const std::string& Options)
316 {
272 - // Store a copy of the path/options strings to the launcher to ensure the pointers in WSLCTmpfsMount remain valid.
273 - const auto& containerPath = m_tmpfsContainerPaths.emplace_back(ContainerPath);
274 - const auto& options = m_tmpfsOptions.emplace_back(Options);
275 -
276 - WSLCTmpfsMount tmpfs{};
277 - tmpfs.Destination = containerPath.c_str();
278 - tmpfs.Options = options.c_str();
279 -
280 - m_tmpfsMounts.push_back(tmpfs);
317 + AddMount({
318 + .MountType = mount::Type::Tmpfs,
319 + .Target = ContainerPath,
320 + .TmpfsOptions = Options,
321 + });
322 }
323
324 void wsl::windows::common::WSLCContainerLauncher::AddAdditionalNetwork(const std::string& Name)
@@ -412,18 +453,12 @@ std::pair<HRESULT, std::optional<RunningWSLCContainer>> WSLCContainerLauncher::C
453 options.InitProcessOptions.CurrentDirectory = m_workingDirectory.c_str();
454 }
455
415 - options.VolumesCount = static_cast<ULONG>(m_volumes.size());
416 - options.Volumes = m_volumes.size() > 0 ? m_volumes.data() : nullptr;
417 -
418 - options.NamedVolumesCount = static_cast<ULONG>(m_namedVolumes.size());
419 - options.NamedVolumes = m_namedVolumes.size() > 0 ? m_namedVolumes.data() : nullptr;
456 + options.MountsCount = static_cast<ULONG>(m_mounts.size());
457 + options.Mounts = m_mounts.size() > 0 ? m_mounts.data() : nullptr;
458
459 options.LabelsCount = static_cast<ULONG>(m_labels.size());
460 options.Labels = m_labels.size() > 0 ? m_labels.data() : nullptr;
461
424 - options.TmpfsCount = static_cast<ULONG>(m_tmpfsMounts.size());
425 - options.Tmpfs = m_tmpfsMounts.size() > 0 ? m_tmpfsMounts.data() : nullptr;
426 -
462 options.ContainerNetwork.NetworkMode = m_networkMode.c_str();
463
464 // Each additional network becomes an entry in NetworkingConfig.EndpointsConfig.
src/windows/common/WSLCContainerLauncher.h
+6 -8
@@ -13,6 +13,7 @@ Abstract:
13 --*/
14
15 #pragma once
16 +#include "MountSpecParsing.h"
17 #include "WSLCProcessLauncher.h"
18 #include "docker_schema.h"
19 #include "wslc_schema.h"
@@ -59,6 +60,7 @@ public:
60
61 void AddVolume(const std::wstring& HostPath, const std::string& ContainerPath, bool ReadOnly);
62 void AddNamedVolume(const std::string& Name, const std::string& ContainerPath, bool ReadOnly);
63 + void AddMount(const mount::Spec& Mount);
64 void AddPort(uint16_t WindowsPort, uint16_t ContainerPort, int Family, int Protocol = IPPROTO_TCP, const std::optional<std::string>& BindingAddress = {});
65 void AddLabel(const std::string& Key, const std::string& Value);
66 void AddTmpfs(const std::string& ContainerPath, const std::string& Options);
@@ -109,11 +111,10 @@ private:
111 std::string m_image;
112 std::string m_name;
113 std::vector<WSLCPortMapping> m_ports;
112 - std::vector<WSLCVolume> m_volumes;
113 - std::vector<WSLCNamedVolume> m_namedVolumes;
114 - std::deque<std::wstring> m_hostPaths;
115 - std::deque<std::string> m_volumeNames;
116 - std::deque<std::string> m_containerPaths;
114 + std::vector<WSLCMountSpec> m_mounts;
115 + std::deque<std::wstring> m_mountSources;
116 + std::deque<std::string> m_mountTargets;
117 + std::deque<std::string> m_mountTmpfsOptions;
118 std::string m_networkMode;
119 std::vector<std::string> m_entrypoint;
120 WSLCSignal m_stopSignal = WSLCSignalNone;
@@ -135,9 +136,6 @@ private:
136 std::vector<WSLCLabel> m_labels;
137 std::deque<std::string> m_labelKeys;
138 std::deque<std::string> m_labelValues;
138 - std::vector<WSLCTmpfsMount> m_tmpfsMounts;
139 - std::deque<std::string> m_tmpfsContainerPaths;
140 - std::deque<std::string> m_tmpfsOptions;
139 std::int64_t m_memoryBytes = 0;
140 std::int64_t m_nanoCpus = 0;
141 std::vector<WSLCUlimit> m_ulimits;
src/windows/inc/docker_schema.h
+10 -1
@@ -239,6 +239,14 @@ inline void to_json(nlohmann::json& j, const ContainerNetworkRequest& v)
239 }
240 }
241
242 +struct MountTmpfsOptions
243 +{
244 + std::int64_t SizeBytes{};
245 + std::uint32_t Mode{};
246 +
247 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(MountTmpfsOptions, SizeBytes, Mode);
248 +};
249 +
250 struct Mount
251 {
252 std::string Name;
@@ -246,8 +254,9 @@ struct Mount
254 std::string Target;
255 std::string Type;
256 bool ReadOnly{};
257 + std::optional<MountTmpfsOptions> TmpfsOptions;
258
250 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Mount, Name, Target, Source, Type, ReadOnly);
259 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Mount, Name, Target, Source, Type, ReadOnly, TmpfsOptions);
260 };
261
262 struct DeviceMapping
src/windows/service/inc/wslc.idl
+34
@@ -245,6 +245,37 @@ typedef struct _WSLCTmpfsMount
245 [unique] LPCSTR Options;
246 } WSLCTmpfsMount;
247
248 +typedef enum _WSLCMountType
249 +{
250 + WSLCMountTypeBind,
251 + WSLCMountTypeVolume,
252 + WSLCMountTypeTmpfs,
253 +} WSLCMountType;
254 +
255 +typedef enum _WSLCMountSpecFlags
256 +{
257 + WSLCMountSpecFlagsNone = 0,
258 + WSLCMountSpecFlagsTmpfsSize = 1,
259 + WSLCMountSpecFlagsTmpfsMode = 2,
260 + WSLCMountSpecFlagsCreateSourceIfMissing = 4,
261 + WSLCMountSpecFlagsTmpfsOptions = 8,
262 +} WSLCMountSpecFlags;
263 +
264 +cpp_quote("#define WSLCMountSpecFlagsValid (WSLCMountSpecFlagsTmpfsSize | WSLCMountSpecFlagsTmpfsMode | WSLCMountSpecFlagsCreateSourceIfMissing | WSLCMountSpecFlagsTmpfsOptions)")
265 +cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCMountSpecFlags);")
266 +
267 +typedef struct _WSLCMountSpec
268 +{
269 + WSLCMountType Type;
270 + [unique, string] LPCWSTR Source;
271 + [string] LPCSTR Target;
272 + BOOL ReadOnly;
273 + WSLCMountSpecFlags Flags;
274 + LONGLONG TmpfsSizeBytes;
275 + ULONG TmpfsMode;
276 + [unique, string] LPCSTR TmpfsOptions;
277 +} WSLCMountSpec;
278 +
279 typedef struct _WSLCUlimit
280 {
281 [string] LPCSTR Name;
@@ -328,6 +359,9 @@ typedef struct _WSLCContainerOptions
359 LONGLONG HealthTimeoutNs;
360 LONGLONG HealthStartPeriodNs;
361 LONG HealthRetries;
362 +
363 + [unique, size_is(MountsCount)] const WSLCMountSpec* Mounts;
364 + ULONG MountsCount;
365 } WSLCContainerOptions;
366
367 typedef char WSLCContainerId[WSLC_CONTAINER_ID_LENGTH + 1] ;
src/windows/wslc/arguments/ArgumentConvertedTypes.h
+4
@@ -18,6 +18,7 @@ Abstract:
18 #include "ArgumentTypes.h"
19 #include "ContainerModel.h"
20 #include "InspectModel.h"
21 +#include "MountSpecParsing.h"
22 #include "SpecParsing.h"
23
24 #include <cstdint>
@@ -33,6 +34,8 @@ struct BuildSecret;
34
35 namespace wsl::windows::wslc::argument::details {
36
37 +namespace mount = wsl::windows::common::mount;
38 +
39 // Local aliases so the ConvertedType tokens in the WSLC_ARGUMENTS X-macro (ArgumentDefinitions.h)
40 // resolve here regardless of include order. Aggregate converted types must be aliased because their
41 // commas would otherwise break X-macro argument parsing if written inline in the table.
@@ -47,6 +50,7 @@ using UlimitValue = std::tuple<std::string, int64_t, int64_t>;
50 using KeyValuePair = std::pair<std::string, std::string>;
51 using BuildOutput = wsl::windows::wslc::services::BuildOutput;
52 using BuildSecret = wsl::windows::wslc::services::BuildSecret;
53 +using ParsedMount = mount::Spec;
54
55 // Generate the ArgType -> converted type mapping from the X-macro. Every ArgType gets a
56 // specialization; arguments that are not converted map to NoConversion (their raw string is used
src/windows/wslc/arguments/ArgumentDefinitions.h
+4 -3
@@ -93,7 +93,8 @@ _(Latest, "latest", L"l", Kind::Flag,
93 _(Link, "link", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_LinkArgDescription()) \
94 _(LinkLocalIp, "link-local-ip", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_LinkLocalIpArgDescription()) \
95 _(Memory, "memory", L"m", Kind::Value, int64_t, Localization::WSLCCLI_MemoryArgDescription()) \
96 -_(Name, "name", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_NameArgDescription()) \
96 +_(Mount, "mount", NO_ALIAS, Kind::Value, ParsedMount, Localization::WSLCCLI_MountArgDescription()) \
97 +_(Name, "name", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_NameArgDescription()) \
98 _(Network, "network", NO_ALIAS, Kind::Value, ParsedNetworkArgument, Localization::WSLCCLI_NetworkArgDescription()) \
99 _(NetworkAlias, "network-alias", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_NetworkAliasArgDescription()) \
100 _(NetworkName, "network-name", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_NetworkNameArgDescription()) \
@@ -130,7 +131,7 @@ _(Tail, "tail", L"n", Kind::Value,
131 _(Tag, "tag", L"t", Kind::Value, NoConversion, Localization::WSLCCLI_TagArgDescription()) \
132 _(Target, "target", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_TargetArgDescription()) \
133 _(Time, "time", L"t", Kind::Value, LONG, Localization::WSLCCLI_TimeArgDescription()) \
133 -_(TMPFS, "tmpfs", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_TMPFSArgDescription()) \
134 +_(TMPFS, "tmpfs", NO_ALIAS, Kind::Value, ParsedMount, Localization::WSLCCLI_TMPFSArgDescription()) \
135 _(TTY, "tty", L"t", Kind::Flag, NoConversion, Localization::WSLCCLI_TTYArgDescription()) \
136 _(Type, "type", L"t", Kind::Value, InspectType, Localization::WSLCCLI_TypeArgDescription()) \
137 _(Ulimit, "ulimit", NO_ALIAS, Kind::Value, UlimitValue, Localization::WSLCCLI_UlimitArgDescription()) \
@@ -139,7 +140,7 @@ _(Username, "username", L"u", Kind::Value,
140 _(Verbose, "verbose", NO_ALIAS, Kind::Flag, NoConversion, Localization::WSLCCLI_VerboseArgDescription()) \
141 _(Version, "version", L"v", Kind::Flag, NoConversion, Localization::WSLCCLI_VersionArgDescription()) \
142 /*_(Virtual, "virtualization", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_VirtualArgDescription())*/ \
142 -_(Volume, "volume", L"v", Kind::Value, NoConversion, Localization::WSLCCLI_VolumeArgDescription()) \
143 +_(Volume, "volume", L"v", Kind::Value, ParsedMount, Localization::WSLCCLI_VolumeArgDescription()) \
144 _(VolumeName, "volume-name", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_VolumeNameArgDescription()) \
145 _(Volumes, "volumes", L"v", Kind::Flag, NoConversion, Localization::WSLCCLI_RemoveVolumesArgDescription()) \
146 _(WorkDir, "workdir", L"w", Kind::Value, NoConversion, Localization::WSLCCLI_WorkingDirArgDescription()) \
src/windows/wslc/arguments/ArgumentValidation.cpp
+49 -9
@@ -20,6 +20,7 @@ Abstract:
20 #include "Exceptions.h"
21 #include "ImageService.h"
22 #include "Localization.h"
23 +#include "MountSpecParsing.h"
24 #include <algorithm>
25 #include <type_traits>
26 #include <utility>
@@ -31,6 +32,8 @@ using namespace wsl::shared::string;
32
33 namespace wsl::windows::wslc {
34
35 +namespace mount = wsl::windows::common::mount;
36 +
37 namespace argument::details {
38 struct RawArgMapAccess
39 {
@@ -228,7 +231,52 @@ void Argument::Validate(ArgMap& execArgs) const
231 break;
232
233 case ArgType::Volume:
231 - validation::ValidateVolumeMount(RawArgMapAccess::GetAll<ArgType::Volume>(execArgs));
234 + CacheConverted<ArgType::Volume>(execArgs, m_name, [](const std::wstring& value, const std::wstring&) {
235 + try
236 + {
237 + auto mountSpec = mount::ParseDockerVolumeString(value);
238 + mount::ValidateMountSpec(mountSpec);
239 + return mountSpec;
240 + }
241 + catch (const mount::MountException& ex)
242 + {
243 + throw ArgumentException(ex.Reason());
244 + }
245 + });
246 + break;
247 +
248 + case ArgType::TMPFS:
249 + CacheConverted<ArgType::TMPFS>(execArgs, m_name, [](const std::wstring& value, const std::wstring&) {
250 + try
251 + {
252 + auto mountSpec = mount::ParseDockerTmpfsString(value);
253 + mount::ValidateMountSpec(mountSpec);
254 + return mountSpec;
255 + }
256 + catch (const mount::MountException& ex)
257 + {
258 + throw ArgumentException(Localization::WSLCCLI_InvalidTmpfsError(value, ex.Reason()));
259 + }
260 + });
261 + break;
262 +
263 + case ArgType::Mount:
264 + CacheConverted<ArgType::Mount>(execArgs, m_name, [](const std::wstring& value, const std::wstring&) {
265 + try
266 + {
267 + auto mountSpec = mount::ParseDockerMountString(value);
268 + mount::ValidateMountSpec(mountSpec);
269 + return mountSpec;
270 + }
271 + catch (const mount::MountUnsupportedException& ex)
272 + {
273 + throw ArgumentException(Localization::WSLCCLI_UnsupportedMountError(value, ex.Reason()));
274 + }
275 + catch (const mount::MountException& ex)
276 + {
277 + throw ArgumentException(Localization::WSLCCLI_InvalidMountError(value, ex.Reason()));
278 + }
279 + });
280 break;
281
282 case ArgType::WorkDir:
@@ -304,14 +352,6 @@ void ValidateWSLCSignalFromString(const std::vector<std::wstring>& values, const
352 }
353 }
354
307 -void ValidateVolumeMount(const std::vector<std::wstring>& values)
308 -{
309 - for (const auto& value : values)
310 - {
311 - std::ignore = models::VolumeMount::Parse(value);
312 - }
313 -}
314 -
355 // Validates that each --filter argument is in the form "key=value". Rejects entries without an '=';
356 // the runtime validates the key and value for specific objects.
357 void ValidateFilter(const std::vector<std::wstring>& values)
src/windows/wslc/arguments/ArgumentValidation.h
-1
@@ -78,7 +78,6 @@ void ValidateFormatTypeFromString(const std::vector<std::wstring>& values, const
78
79 void ValidateGpus(const std::vector<std::wstring>& values, const std::wstring& argName);
80
81 -void ValidateVolumeMount(const std::vector<std::wstring>& values);
81 void ValidateFilter(const std::vector<std::wstring>& values);
82
83 } // namespace wsl::windows::wslc::validation
src/windows/wslc/commands/ContainerCreateCommand.cpp
+1
@@ -52,6 +52,7 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
52 Argument::Create(ArgType::Interactive),
53 Argument::Create(ArgType::Label, false, Limit::Unlimited),
54 Argument::Create(ArgType::Memory),
55 + Argument::Create(ArgType::Mount, false, Limit::Unlimited),
56 Argument::Create(ArgType::Name),
57 Argument::Create(ArgType::Network, false, Limit::Unlimited),
58 Argument::Create(ArgType::NetworkAlias, false, Limit::Unlimited),
src/windows/wslc/commands/ContainerRunCommand.cpp
+1
@@ -52,6 +52,7 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
52 Argument::Create(ArgType::Interactive),
53 Argument::Create(ArgType::Label, false, Limit::Unlimited),
54 Argument::Create(ArgType::Memory),
55 + Argument::Create(ArgType::Mount, false, Limit::Unlimited),
56 Argument::Create(ArgType::Name),
57 Argument::Create(ArgType::Network, false, Limit::Unlimited),
58 Argument::Create(ArgType::NetworkAlias, false, Limit::Unlimited),
src/windows/wslc/services/ContainerModel.cpp
+25 -83
@@ -13,6 +13,7 @@ Abstract:
13
14 #include "precomp.h"
15 #include "ContainerModel.h"
16 +#include <unordered_set>
17
18 namespace wsl::windows::wslc::models {
19
@@ -155,89 +156,28 @@ void PublishPort::Validate() const
156 // Source: https://github.com/moby/moby/blob/master/volume/validate.go
157 bool VolumeMount::IsValidNamedVolumeName(const std::wstring& name)
158 {
158 - static const std::wregex namedVolumeRegex(LR"(^[a-zA-Z0-9][a-zA-Z0-9_.-]{1,}$)");
159 - return std::regex_match(name, namedVolumeRegex);
159 + return mount::IsValidNamedVolumeName(name);
160 }
161
162 VolumeMount VolumeMount::Parse(const std::wstring& value)
163 {
164 - auto lastColon = value.rfind(':');
165 - if (lastColon == std::wstring::npos)
164 + mount::Spec mountSpec;
165 + try
166 {
167 - THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_VolumeInvalidSpec(value, Localization::WSLCCLI_VolumeFormatUsage()));
167 + mountSpec = mount::ParseDockerVolumeString(value);
168 + mount::ValidateMountSpec(mountSpec);
169 }
169 -
170 - VolumeMount vm;
171 - auto splitColon = lastColon;
172 - const auto lastToken = value.substr(lastColon + 1);
173 - if (IsValidMode(lastToken))
174 - {
175 - vm.m_isReadOnlyMode = IsReadOnlyMode(lastToken);
176 - if (lastColon == 0)
177 - {
178 - THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_VolumeInvalidSpec(value, Localization::WSLCCLI_VolumeFormatUsage()));
179 - }
180 -
181 - splitColon = value.rfind(':', lastColon - 1);
182 - if (splitColon == std::wstring::npos)
183 - {
184 - THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_VolumeInvalidSpec(value, Localization::WSLCCLI_VolumeFormatUsage()));
185 - }
186 -
187 - vm.m_containerPath = WideToMultiByte(value.substr(splitColon + 1, lastColon - splitColon - 1));
188 - }
189 - else
190 - {
191 - vm.m_containerPath = WideToMultiByte(lastToken);
192 - }
193 -
194 - if (vm.m_containerPath.empty())
195 - {
196 - THROW_HR_WITH_USER_ERROR(
197 - E_INVALIDARG, Localization::WSLCCLI_VolumeContainerPathEmpty(value, Localization::WSLCCLI_VolumeFormatUsage()));
198 - }
199 -
200 - if (vm.m_containerPath[0] != '/')
201 - {
202 - THROW_HR_WITH_USER_ERROR(
203 - E_INVALIDARG, Localization::WSLCCLI_VolumeContainerPathNotAbsolute(value, Localization::WSLCCLI_VolumeFormatUsage()));
204 - }
205 -
206 - const auto rawHostPath = value.substr(0, splitColon);
207 - if (rawHostPath.empty())
170 + catch (const mount::MountException& ex)
171 {
209 - THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_VolumeHostPathEmpty(value, Localization::WSLCCLI_VolumeFormatUsage()));
172 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, ex.Reason());
173 }
174
212 - // This is where we need to check if the user is referencing a named volume.
213 - // This can be either an existing named volume or a new named volume that will be created.
214 - if (VolumeMount::IsValidNamedVolumeName(rawHostPath))
215 - {
216 - vm.m_isNamedVolume = true;
217 - vm.m_host = rawHostPath;
218 - }
219 - else
220 - {
221 - // Not a named volume, so it must be a path.
222 - // Use wil::GetFullPathNameW to resolve relative paths against the CWD.
223 - std::wstring resolvedHostPath;
224 - const auto hr = wil::GetFullPathNameW(rawHostPath.c_str(), resolvedHostPath);
225 - if (FAILED(hr))
226 - {
227 - THROW_HR_WITH_USER_ERROR(hr, Localization::WSLCCLI_VolumeHostPathInvalid(value, rawHostPath));
228 - }
229 -
230 - // GetFileAttributesW validates the resolved path syntax without requiring existence.
231 - // ERROR_INVALID_NAME indicates illegal characters in the path (e.g. ":" as a component).
232 - if (GetFileAttributesW(resolvedHostPath.c_str()) == INVALID_FILE_ATTRIBUTES && GetLastError() == ERROR_INVALID_NAME)
233 - {
234 - THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_VolumeHostPathInvalid(value, rawHostPath));
235 - }
236 -
237 - vm.m_host = std::move(resolvedHostPath);
238 - }
239 -
240 - return vm;
175 + VolumeMount volume;
176 + volume.m_host = std::move(mountSpec.Source);
177 + volume.m_containerPath = std::move(mountSpec.Target);
178 + volume.m_isReadOnlyMode = mountSpec.ReadOnly;
179 + volume.m_isNamedVolume = mountSpec.MountType == mount::Type::Volume;
180 + return volume;
181 }
182
183 std::optional<std::wstring> EnvironmentVariable::Parse(const std::wstring& entry)
@@ -318,19 +258,21 @@ std::vector<std::wstring> EnvironmentVariable::ParseFile(const std::wstring& fil
258 return envVars;
259 }
260
321 -TmpfsMount TmpfsMount::Parse(const std::string& value)
261 +void ValidateUniqueMountDestinations(const ContainerOptions& options)
262 {
323 - TmpfsMount result{};
324 - auto colonPos = value.find(':');
325 - if (colonPos == std::string::npos)
263 + try
264 {
327 - result.m_containerPath = value;
328 - return result;
265 + mount::ValidateMountCollection(options.Mounts);
266 }
267 + catch (const mount::MountException& ex)
268 + {
269 + if (ex.Error() == mount::ValidationError::DuplicateDestination)
270 + {
271 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_DuplicateMountDestinationError(MultiByteToWide(ex.Destination())));
272 + }
273
331 - result.m_containerPath = value.substr(0, colonPos);
332 - result.m_options = value.substr(colonPos + 1);
333 - return result;
274 + throw;
275 + }
276 }
277
278 CidFile::CidFile(const std::optional<std::wstring>& path)
src/windows/wslc/services/ContainerModel.h
+5 -28
@@ -14,6 +14,7 @@ Abstract:
14
15 #pragma once
16
17 +#include "MountSpecParsing.h"
18 #include <wslservice.h>
19 #include <wslc.h>
20 #include <optional>
@@ -22,6 +23,8 @@ Abstract:
23
24 namespace wsl::windows::wslc::models {
25
26 +namespace mount = wsl::windows::common::mount;
27 +
28 // Valid formats for container list output.
29 enum class FormatType
30 {
@@ -73,7 +76,7 @@ struct ContainerOptions
76 bool NoHealthcheck = false;
77 bool Gpu = false;
78 std::vector<std::string> Ports;
76 - std::vector<std::wstring> Volumes;
79 + std::vector<mount::Spec> Mounts;
80 std::string WorkingDirectory;
81 std::vector<std::string> Entrypoint;
82 std::optional<std::string> User{};
@@ -84,7 +87,6 @@ struct ContainerOptions
87 std::vector<std::string> DnsOptions;
88 std::vector<ContainerNetwork> Networks;
89 std::vector<std::string> NetworkAliases;
87 - std::vector<std::string> Tmpfs;
90 std::vector<std::pair<std::string, std::string>> Labels;
91 std::optional<std::wstring> CidFile{};
92 std::optional<int64_t> MemoryBytes{};
@@ -305,34 +307,9 @@ private:
307 std::string m_containerPath;
308 bool m_isReadOnlyMode = false;
309 bool m_isNamedVolume = false;
308 -
309 - static bool IsReadOnlyMode(const std::wstring& mode)
310 - {
311 - return mode == L"ro";
312 - }
313 -
314 - static bool IsValidMode(const std::wstring& mode)
315 - {
316 - return IsReadOnlyMode(mode) || mode == L"rw";
317 - }
310 };
311
320 -struct TmpfsMount
321 -{
322 - std::string ContainerPath() const
323 - {
324 - return m_containerPath;
325 - }
326 - std::string Options() const
327 - {
328 - return m_options;
329 - }
330 - static TmpfsMount Parse(const std::string& value);
331 -
332 -private:
333 - std::string m_containerPath;
334 - std::string m_options;
335 -};
312 +void ValidateUniqueMountDestinations(const ContainerOptions& options);
313
314 class CidFile
315 {
src/windows/wslc/services/ContainerService.cpp
+4 -19
@@ -29,6 +29,8 @@ Abstract:
29 #include <wslc.h>
30
31 namespace wsl::windows::wslc::services {
32 +namespace mount = wsl::windows::common::mount;
33 +
34 using wsl::windows::common::ClientRunningWSLCProcess;
35 using wsl::windows::common::wslc_schema::InspectContainer;
36 using namespace wsl::windows::common::wslutil;
@@ -148,20 +150,9 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(Terminal& termi
150 }
151 }
152
151 - // Add volumes if specified
152 - for (const auto& volumeSpec : options.Volumes)
153 + for (const auto& mountSpec : options.Mounts)
154 {
154 - auto volume = VolumeMount::Parse(volumeSpec);
155 - auto host = volume.Host();
156 - auto container = volume.ContainerPath();
157 - if (volume.IsNamedVolume())
158 - {
159 - containerLauncher.AddNamedVolume(string::WideToMultiByte(host), container, volume.IsReadOnly());
160 - }
161 - else
162 - {
163 - containerLauncher.AddVolume(host, container, volume.IsReadOnly());
164 - }
155 + containerLauncher.AddMount(mountSpec);
156 }
157
158 containerLauncher.SetContainerFlags(containerFlags);
@@ -268,12 +259,6 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(Terminal& termi
259 containerLauncher.SetDnsOptions(std::vector<std::string>(options.DnsOptions));
260 }
261
271 - for (const auto& tmpfsSpec : options.Tmpfs)
272 - {
273 - auto tmpfsMount = TmpfsMount::Parse(tmpfsSpec);
274 - containerLauncher.AddTmpfs(tmpfsMount.ContainerPath(), tmpfsMount.Options());
275 - }
276 -
262 for (const auto& [key, value] : options.Labels)
263 {
264 containerLauncher.AddLabel(key, value);
src/windows/wslc/tasks/ContainerTasks.cpp
+11 -10
@@ -19,6 +19,7 @@ Abstract:
19 #include "ContainerService.h"
20 #include "ContainerTasks.h"
21 #include "ImageModel.h"
22 +#include "MountSpecParsing.h"
23 #include "SessionModel.h"
24 #include "SessionService.h"
25 #include "TableOutput.h"
@@ -658,11 +659,13 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
659 if (context.Args.Contains(ArgType::Volume))
660 {
661 auto volumes = context.Args.GetAllValues<ArgType::Volume>();
661 - options.Volumes.reserve(options.Volumes.size() + volumes.size());
662 - for (const auto& volume : volumes)
663 - {
664 - options.Volumes.emplace_back(volume);
665 - }
662 + options.Mounts.insert(options.Mounts.end(), std::make_move_iterator(volumes.begin()), std::make_move_iterator(volumes.end()));
663 + }
664 +
665 + if (context.Args.Contains(ArgType::Mount))
666 + {
667 + auto mounts = context.Args.GetAllValues<ArgType::Mount>();
668 + options.Mounts.insert(options.Mounts.end(), std::make_move_iterator(mounts.begin()), std::make_move_iterator(mounts.end()));
669 }
670
671 options.Remove = context.Args.GetValue<ArgType::Remove>();
@@ -827,13 +830,11 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
830 if (context.Args.Contains(ArgType::TMPFS))
831 {
832 auto tmpfs = context.Args.GetAllValues<ArgType::TMPFS>();
830 - options.Tmpfs.reserve(options.Tmpfs.size() + tmpfs.size());
831 - for (const auto& value : tmpfs)
832 - {
833 - options.Tmpfs.emplace_back(WideToMultiByte(value));
834 - }
833 + options.Mounts.insert(options.Mounts.end(), std::make_move_iterator(tmpfs.begin()), std::make_move_iterator(tmpfs.end()));
834 }
835
836 + ValidateUniqueMountDestinations(options);
837 +
838 for (const auto& label : context.Args.GetAllValues<ArgType::Label>())
839 {
840 options.Labels.push_back(label);
src/windows/wslcsession/WSLCContainer.cpp
+328 -63
@@ -24,6 +24,8 @@ Abstract:
24 #include "WSLCProcessIO.h"
25 #include "WSLCVolumes.h"
26 #include "APICompat.h"
27 +#include "MountSpecParsing.h"
28 +#include <unordered_set>
29
30 namespace apicompat = wsl::windows::common::apicompat;
31
@@ -424,9 +426,21 @@ auto MountVolumes(std::vector<WSLCVolumeMount>& volumes, WSLCVirtualMachine& par
426
427 for (auto& volume : volumes)
428 {
427 - // Create a new directory if it doesn't exist.
428 - if (!std::filesystem::exists(volume.HostPath))
429 + std::error_code error;
430 + const auto sourceExists = std::filesystem::exists(volume.HostPath, error);
431 + if (error)
432 {
433 + throw wsl::windows::common::mount::MountValidationException(
434 + Localization::MessageWslcBindSourcePathError(volume.HostPath, error.message()));
435 + }
436 +
437 + if (!sourceExists)
438 + {
439 + if (!volume.CreateSourceIfMissing)
440 + {
441 + throw wsl::windows::common::mount::MountValidationException(Localization::MessageWslcBindSourcePathNotFound(volume.HostPath));
442 + }
443 +
444 auto result = wil::CreateDirectoryDeepNoThrow(volume.HostPath.c_str());
445 if (FAILED(result))
446 {
@@ -443,6 +457,18 @@ auto MountVolumes(std::vector<WSLCVolumeMount>& volumes, WSLCVirtualMachine& par
457 return std::move(errorCleanup);
458 }
459
460 +auto MountVolumesWithUserError(std::vector<WSLCVolumeMount>& volumes, WSLCVirtualMachine& parentVM)
461 +{
462 + try
463 + {
464 + return MountVolumes(volumes, parentVM);
465 + }
466 + catch (const wsl::windows::common::mount::MountValidationException& ex)
467 + {
468 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, ex.Reason());
469 + }
470 +}
471 +
472 WSLCContainerState DockerStateToWSLCState(ContainerState state)
473 {
474 // TODO: Handle other states like Paused, Restarting, etc.
@@ -531,6 +557,210 @@ std::map<std::string, std::string> StripInternalLabels(std::optional<std::map<st
557 return StripInternalLabels(std::move(labels).value_or(std::map<std::string, std::string>{}));
558 }
559
560 +std::vector<wsl::windows::common::mount::Spec> ConvertAndValidateMounts(const WSLCContainerOptions& containerOptions)
561 +{
562 + namespace mount = wsl::windows::common::mount;
563 +
564 + THROW_HR_IF(E_INVALIDARG, containerOptions.MountsCount > 0 && containerOptions.Mounts == nullptr);
565 +
566 + std::vector<mount::Spec> mounts;
567 + mounts.reserve(containerOptions.MountsCount);
568 + for (ULONG i = 0; i < containerOptions.MountsCount; ++i)
569 + {
570 + const auto& value = containerOptions.Mounts[i];
571 + THROW_HR_IF_NULL_MSG(E_INVALIDARG, value.Target, "Mount at index %lu has null Target", i);
572 + THROW_HR_IF_MSG(
573 + E_INVALIDARG,
574 + WI_IsAnyFlagSet(value.Flags, ~WSLCMountSpecFlagsValid),
575 + "Mount at index %lu has invalid flags: 0x%x",
576 + i,
577 + value.Flags);
578 +
579 + mount::Type type;
580 + switch (value.Type)
581 + {
582 + case WSLCMountTypeBind:
583 + type = mount::Type::Bind;
584 + break;
585 +
586 + case WSLCMountTypeVolume:
587 + type = mount::Type::Volume;
588 + break;
589 +
590 + case WSLCMountTypeTmpfs:
591 + type = mount::Type::Tmpfs;
592 + break;
593 +
594 + default:
595 + THROW_HR_MSG(E_INVALIDARG, "Mount at index %lu has invalid type: %d", i, value.Type);
596 + }
597 +
598 + THROW_HR_IF_MSG(
599 + E_INVALIDARG,
600 + type != mount::Type::Bind && WI_IsFlagSet(value.Flags, WSLCMountSpecFlagsCreateSourceIfMissing),
601 + "Mount at index %lu specifies create-source-if-missing for a non-bind mount",
602 + i);
603 + THROW_HR_IF_MSG(
604 + E_INVALIDARG,
605 + type != mount::Type::Tmpfs && WI_IsFlagSet(value.Flags, WSLCMountSpecFlagsTmpfsOptions),
606 + "Mount at index %lu specifies tmpfs options for a non-tmpfs mount",
607 + i);
608 + THROW_HR_IF_MSG(
609 + E_INVALIDARG,
610 + WI_IsFlagSet(value.Flags, WSLCMountSpecFlagsTmpfsOptions) &&
611 + WI_IsAnyFlagSet(value.Flags, WSLCMountSpecFlagsTmpfsSize | WSLCMountSpecFlagsTmpfsMode),
612 + "Mount at index %lu combines legacy and structured tmpfs options",
613 + i);
614 +
615 + mounts.push_back({
616 + .MountType = type,
617 + .Source = value.Source != nullptr ? value.Source : L"",
618 + .Target = value.Target,
619 + .ReadOnly = static_cast<bool>(value.ReadOnly),
620 + .BindSource = WI_IsFlagSet(value.Flags, WSLCMountSpecFlagsCreateSourceIfMissing) ? mount::BindSourcePolicy::CreateIfMissing
621 + : mount::BindSourcePolicy::RequireExisting,
622 + .TmpfsSizeBytes = WI_IsFlagSet(value.Flags, WSLCMountSpecFlagsTmpfsSize) ? std::optional<int64_t>{value.TmpfsSizeBytes} : std::nullopt,
623 + .TmpfsMode = WI_IsFlagSet(value.Flags, WSLCMountSpecFlagsTmpfsMode) ? std::optional<uint32_t>{value.TmpfsMode} : std::nullopt,
624 + .TmpfsOptions = WI_IsFlagSet(value.Flags, WSLCMountSpecFlagsTmpfsOptions)
625 + ? std::optional<std::string>{value.TmpfsOptions != nullptr ? value.TmpfsOptions : ""}
626 + : std::nullopt,
627 + });
628 + }
629 +
630 + try
631 + {
632 + mount::ValidateMountCollection(mounts);
633 + for (const auto& mount : mounts)
634 + {
635 + if (mount.MountType == mount::Type::Bind)
636 + {
637 + if (mount.BindSource == mount::BindSourcePolicy::CreateIfMissing)
638 + {
639 + continue;
640 + }
641 +
642 + std::error_code error;
643 + const auto sourceExists = std::filesystem::exists(mount.Source, error);
644 + if (error)
645 + {
646 + throw mount::MountValidationException(Localization::MessageWslcBindSourcePathError(mount.Source, error.message()));
647 + }
648 +
649 + if (!sourceExists)
650 + {
651 + throw mount::MountValidationException(Localization::MessageWslcBindSourcePathNotFound(mount.Source));
652 + }
653 + }
654 + }
655 + }
656 + catch (const mount::MountException& ex)
657 + {
658 + if (ex.Error() == mount::ValidationError::DuplicateDestination)
659 + {
660 + THROW_HR_WITH_USER_ERROR(
661 + E_INVALIDARG, Localization::WSLCCLI_DuplicateMountDestinationError(wsl::shared::string::MultiByteToWide(ex.Destination())));
662 + }
663 +
664 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, ex.Reason());
665 + }
666 +
667 + std::unordered_set<std::string> destinations;
668 + const auto addDestination = [&](const char* destination) {
669 + THROW_HR_IF_NULL(E_INVALIDARG, destination);
670 +
671 + const auto normalizedDestination = mount::NormalizeDestination(destination);
672 + THROW_HR_WITH_USER_ERROR_IF(
673 + E_INVALIDARG,
674 + Localization::WSLCCLI_DuplicateMountDestinationError(wsl::shared::string::MultiByteToWide(normalizedDestination)),
675 + !destinations.emplace(normalizedDestination).second);
676 + };
677 +
678 + for (const auto& mount : mounts)
679 + {
680 + addDestination(mount.Target.c_str());
681 + }
682 +
683 + THROW_HR_IF(E_INVALIDARG, containerOptions.VolumesCount > 0 && containerOptions.Volumes == nullptr);
684 + for (ULONG i = 0; i < containerOptions.VolumesCount; ++i)
685 + {
686 + THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.Volumes[i].HostPath, "Volumes[%lu].HostPath is null", i);
687 + addDestination(containerOptions.Volumes[i].ContainerPath);
688 + }
689 +
690 + THROW_HR_IF(E_INVALIDARG, containerOptions.NamedVolumesCount > 0 && containerOptions.NamedVolumes == nullptr);
691 + for (ULONG i = 0; i < containerOptions.NamedVolumesCount; ++i)
692 + {
693 + THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.NamedVolumes[i].Name, "NamedVolume at index %lu has null Name", i);
694 + addDestination(containerOptions.NamedVolumes[i].ContainerPath);
695 + }
696 +
697 + THROW_HR_IF(E_INVALIDARG, containerOptions.TmpfsCount > 0 && containerOptions.Tmpfs == nullptr);
698 + for (ULONG i = 0; i < containerOptions.TmpfsCount; ++i)
699 + {
700 + addDestination(containerOptions.Tmpfs[i].Destination);
701 + }
702 +
703 + return mounts;
704 +}
705 +
706 +struct PreparedBindMount
707 +{
708 + WSLCVolumeMount Volume;
709 + std::string DockerSource;
710 +};
711 +
712 +enum class MissingBindSource
713 +{
714 + Create,
715 + Reject,
716 +};
717 +
718 +PreparedBindMount PrepareBindMount(const std::wstring& source, const std::string& target, bool readOnly, MissingBindSource missingSource)
719 +{
720 + GUID volumeId;
721 + THROW_IF_FAILED(CoCreateGuid(&volumeId));
722 +
723 + auto parentVMPath = std::format("/mnt/{}", wsl::shared::string::GuidToString<char>(volumeId));
724 + std::filesystem::path hostPath = source;
725 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(source), !hostPath.is_absolute());
726 +
727 + std::wstring sourceFilename;
728 + {
729 + std::error_code ec;
730 + hostPath = std::filesystem::canonical(hostPath, ec);
731 + if (!ec)
732 + {
733 + if (std::filesystem::is_regular_file(hostPath))
734 + {
735 + sourceFilename = hostPath.filename().wstring();
736 + hostPath = hostPath.parent_path();
737 + }
738 + }
739 + else if (ec == std::errc::no_such_file_or_directory)
740 + {
741 + hostPath = source;
742 + }
743 + else
744 + {
745 + THROW_HR_WITH_USER_ERROR(E_FAIL, Localization::MessageWslcFailedToMountVolume(source, ec.message()));
746 + }
747 + }
748 +
749 + auto dockerSource = sourceFilename.empty() ? parentVMPath : std::format("{}/{}", parentVMPath, sourceFilename);
750 + return {
751 + .Volume =
752 + {
753 + .HostPath = std::move(hostPath),
754 + .ParentVMPath = std::move(parentVMPath),
755 + .ContainerPath = target,
756 + .ReadOnly = readOnly,
757 + .SourceFilename = std::move(sourceFilename),
758 + .CreateSourceIfMissing = missingSource == MissingBindSource::Create,
759 + },
760 + .DockerSource = std::move(dockerSource),
761 + };
762 +}
763 +
764 void ProcessNamedVolumes(const WSLCContainerOptions& containerOptions, wsl::windows::common::docker_schema::CreateContainer& request)
765 {
766 THROW_HR_IF(E_INVALIDARG, containerOptions.NamedVolumesCount > 0 && containerOptions.NamedVolumes == nullptr);
@@ -910,7 +1140,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
1140 Localization::MessageWslcVolumeNotAvailable(wsl::shared::string::Join(unavailableVolumes, ',')),
1141 !unavailableVolumes.empty());
1142
913 - auto volumeCleanup = MountVolumes(m_mountedVolumes, m_runtime.Vm());
1143 + auto volumeCleanup = MountVolumesWithUserError(m_mountedVolumes, m_runtime.Vm());
1144
1145 auto portCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { UnmapPorts(); });
1146 MapPorts();
@@ -1611,7 +1841,9 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec
1841 }
1842
1843 // Map mounts without exposing Linux paths from the utility VM.
1614 - wslcInspect.Mounts.reserve(m_mountedVolumes.size() + dockerInspect.Mounts.size() + dockerInspect.HostConfig.Tmpfs.size());
1844 + wslcInspect.Mounts.reserve(
1845 + m_mountedVolumes.size() + dockerInspect.Mounts.size() + dockerInspect.HostConfig.Tmpfs.size() +
1846 + dockerInspect.HostConfig.Mounts.size());
1847 for (const auto& volume : m_mountedVolumes)
1848 {
1849 wslc_schema::InspectMount mountInfo{};
@@ -1650,6 +1882,13 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec
1882 wslc_schema::InspectMount mountInfo{};
1883 mountInfo.Type = volume.Type;
1884 mountInfo.Name = volume.Name;
1885 + const auto structuredMount = std::ranges::find_if(dockerInspect.HostConfig.Mounts, [&](const auto& mount) {
1886 + return mount.Type == "volume" && mount.Target == volume.Destination;
1887 + });
1888 + if (structuredMount != dockerInspect.HostConfig.Mounts.end())
1889 + {
1890 + mountInfo.Source = structuredMount->Source;
1891 + }
1892 mountInfo.Destination = volume.Destination;
1893 mountInfo.ReadWrite = volume.RW;
1894
@@ -1668,6 +1907,20 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec
1907 wslcInspect.Mounts.push_back(std::move(mountInfo));
1908 }
1909
1910 + // Bind mounts are populated from m_mountedVolumes so their inspect source is the Windows host path.
1911 + for (const auto& mount : dockerInspect.HostConfig.Mounts)
1912 + {
1913 + if (mount.Type == "tmpfs")
1914 + {
1915 + wslc_schema::InspectMount mountInfo{};
1916 + mountInfo.Type = mount.Type;
1917 + mountInfo.Source = mount.Source;
1918 + mountInfo.Destination = mount.Target;
1919 + mountInfo.ReadWrite = !mount.ReadOnly;
1920 + wslcInspect.Mounts.push_back(std::move(mountInfo));
1921 + }
1922 + }
1923 +
1924 // Config.Labels is the Docker-shape location; top-level Labels is a legacy alias.
1925 wslcInspect.Config.Labels = m_labels;
1926 wslcInspect.Labels = m_labels;
@@ -1708,6 +1961,7 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1961 auto& virtualMachine = runtime.Vm();
1962 auto& DockerClient = runtime.Docker();
1963 auto& EventTracker = runtime.Events();
1964 + const auto mounts = ConvertAndValidateMounts(containerOptions);
1965
1966 common::docker_schema::CreateContainer request;
1967 request.Image = containerOptions.Image;
@@ -1869,73 +2123,22 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2123 request.Healthcheck = std::move(health);
2124 }
2125
1872 - if (containerOptions.VolumesCount > 0)
1873 - {
1874 - THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.Volumes, "Volumes is null with VolumesCount=%lu", containerOptions.VolumesCount);
1875 - }
1876 -
2126 // Build bind mount list from container options.
2127 std::vector<WSLCVolumeMount> volumes;
1879 - volumes.reserve(containerOptions.VolumesCount);
2128 + volumes.reserve(containerOptions.VolumesCount + mounts.size());
2129
2130 std::vector<std::string> binds;
2131 binds.reserve(containerOptions.VolumesCount);
2132
2133 for (ULONG i = 0; i < containerOptions.VolumesCount; i++)
2134 {
1886 - GUID volumeId;
1887 - THROW_IF_FAILED(CoCreateGuid(&volumeId));
1888 -
1889 - auto parentVMPath = std::format("/mnt/{}", wsl::shared::string::GuidToString<char>(volumeId));
2135 auto volume = containerOptions.Volumes[i];
1891 -
1892 - THROW_HR_IF_NULL_MSG(E_INVALIDARG, volume.HostPath, "Volumes[%lu].HostPath is null", i);
1893 - THROW_HR_IF_NULL_MSG(E_INVALIDARG, volume.ContainerPath, "Volumes[%lu].ContainerPath is null", i);
1894 -
1895 - std::filesystem::path hostPath = volume.HostPath;
1896 - THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(volume.HostPath), !hostPath.is_absolute());
1897 -
1898 - std::wstring sourceFilename;
1899 -
1900 - {
1901 - // Resolve symlinks.
1902 - std::error_code ec;
1903 - hostPath = std::filesystem::canonical(hostPath, ec);
1904 - if (!ec)
1905 - {
1906 - // When the host path is a file, mount the parent directory in the VM
1907 - // and bind only the specific file into the container via Docker.
1908 - if (std::filesystem::is_regular_file(hostPath))
1909 - {
1910 - sourceFilename = hostPath.filename().wstring();
1911 - hostPath = hostPath.parent_path();
1912 - }
1913 - }
1914 - else
1915 - {
1916 - if (ec == std::errc::no_such_file_or_directory)
1917 - {
1918 - // Path doesn't exist, assume directory.
1919 - hostPath = volume.HostPath;
1920 - }
1921 - else
1922 - {
1923 - THROW_HR_WITH_USER_ERROR(E_FAIL, Localization::MessageWslcFailedToMountVolume(volume.HostPath, ec.message()));
1924 - }
1925 - }
1926 - }
1927 -
1928 - volumes.push_back(WSLCVolumeMount{hostPath, parentVMPath, volume.ContainerPath, static_cast<bool>(volume.ReadOnly), sourceFilename});
1929 -
1930 - auto options = volume.ReadOnly ? "ro" : "rw";
1931 - auto bindSource = sourceFilename.empty() ? parentVMPath : std::format("{}/{}", parentVMPath, sourceFilename);
1932 - auto bind = std::format("{}:{}:{}", bindSource, volume.ContainerPath, options);
1933 -
1934 - binds.push_back(std::move(bind));
2136 + auto prepared =
2137 + PrepareBindMount(volume.HostPath, volume.ContainerPath, static_cast<bool>(volume.ReadOnly), MissingBindSource::Create);
2138 + binds.push_back(std::format("{}:{}:{}", prepared.DockerSource, volume.ContainerPath, volume.ReadOnly ? "ro" : "rw"));
2139 + volumes.push_back(std::move(prepared.Volume));
2140 }
2141
1937 - request.HostConfig.Binds = std::move(binds);
1938 -
2142 // Process tmpfs mounts from container options.
2143 if (containerOptions.TmpfsCount > 0)
2144 {
@@ -1953,6 +2156,56 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2156
2157 ProcessNamedVolumes(containerOptions, request);
2158
2159 + for (const auto& mount : mounts)
2160 + {
2161 + common::docker_schema::Mount dockerMount{
2162 + .Target = mount.Target,
2163 + .ReadOnly = mount.ReadOnly,
2164 + };
2165 +
2166 + switch (mount.MountType)
2167 + {
2168 + case wsl::windows::common::mount::Type::Bind:
2169 + {
2170 + // Docker's colon-delimited bind format cannot represent ':' in the target.
2171 + const auto missingSource = mount.BindSource == wsl::windows::common::mount::BindSourcePolicy::CreateIfMissing
2172 + ? MissingBindSource::Create
2173 + : MissingBindSource::Reject;
2174 + auto prepared = PrepareBindMount(mount.Source, mount.Target, mount.ReadOnly, missingSource);
2175 + dockerMount.Source = std::move(prepared.DockerSource);
2176 + dockerMount.Type = "bind";
2177 + volumes.push_back(std::move(prepared.Volume));
2178 + break;
2179 + }
2180 +
2181 + case wsl::windows::common::mount::Type::Volume:
2182 + dockerMount.Source = wsl::shared::string::WideToMultiByte(mount.Source);
2183 + dockerMount.Type = "volume";
2184 + break;
2185 +
2186 + case wsl::windows::common::mount::Type::Tmpfs:
2187 + if (mount.TmpfsOptions.has_value())
2188 + {
2189 + request.HostConfig.Tmpfs[mount.Target] = mount.TmpfsOptions.value();
2190 + continue;
2191 + }
2192 +
2193 + dockerMount.Type = "tmpfs";
2194 + if (mount.TmpfsSizeBytes.has_value() || mount.TmpfsMode.has_value())
2195 + {
2196 + dockerMount.TmpfsOptions = common::docker_schema::MountTmpfsOptions{
2197 + .SizeBytes = mount.TmpfsSizeBytes.value_or(0),
2198 + .Mode = mount.TmpfsMode.value_or(0),
2199 + };
2200 + }
2201 + break;
2202 + }
2203 +
2204 + request.HostConfig.Mounts.push_back(std::move(dockerMount));
2205 + }
2206 +
2207 + request.HostConfig.Binds = std::move(binds);
2208 +
2209 // Configure GPU support if requested.
2210 if (WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsGpu))
2211 {
@@ -2090,8 +2343,12 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2343 request.Labels[WSLCContainerMetadataLabel] = SerializeContainerMetadata(metadata);
2344 request.Labels.insert(requestedLabels.begin(), requestedLabels.end());
2345
2093 - // Send the request to docker.
2094 - auto result = DockerClient.CreateContainer(request, containerName);
2346 + // Docker validates structured bind sources during container creation, so their VM paths must exist here.
2347 + // Release the temporary shares before returning; Start remounts them for the container lifetime.
2348 + auto result = [&]() {
2349 + auto volumeCleanup = MountVolumesWithUserError(volumes, virtualMachine);
2350 + return DockerClient.CreateContainer(request, containerName);
2351 + }();
2352
2353 // Surface any warnings returned by Docker (e.g., deprecated features, configuration issues).
2354 for (const auto& warning : result.Warnings)
@@ -2145,12 +2402,20 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2402 // Collect the names of referenced docker named volumes so Start() can verify
2403 // they are available before running the container.
2404 std::vector<std::string> namedVolumes;
2148 - namedVolumes.reserve(containerOptions.NamedVolumesCount);
2405 + namedVolumes.reserve(containerOptions.NamedVolumesCount + mounts.size());
2406 for (ULONG i = 0; i < containerOptions.NamedVolumesCount; i++)
2407 {
2408 namedVolumes.emplace_back(containerOptions.NamedVolumes[i].Name);
2409 }
2410
2411 + for (const auto& mount : mounts)
2412 + {
2413 + if (mount.MountType == wsl::windows::common::mount::Type::Volume && !mount.Source.empty())
2414 + {
2415 + namedVolumes.emplace_back(wsl::shared::string::WideToMultiByte(mount.Source));
2416 + }
2417 + }
2418 +
2419 auto mergedLabels = StripInternalLabels(std::move(inspectData.Config.Labels));
2420
2421 auto container = std::make_shared<WSLCContainerImpl>(
src/windows/wslcsession/WSLCContainerMetadata.h
+2 -1
@@ -44,11 +44,12 @@ struct WSLCVolumeMount
44
45 // Non-empty when the mount target is a single file rather than a directory.
46 std::wstring SourceFilename;
47 + bool CreateSourceIfMissing{true};
48
49 // Runtime-only field. Not serialized to JSON.
50 bool Mounted{};
51
51 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(WSLCVolumeMount, HostPath, ParentVMPath, ContainerPath, ReadOnly, SourceFilename);
52 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(WSLCVolumeMount, HostPath, ParentVMPath, ContainerPath, ReadOnly, SourceFilename, CreateSourceIfMissing);
53 };
54
55 struct WSLCContainerMetadataV1
test/windows/WSLCTests.cpp
+14 -7
@@ -6447,15 +6447,15 @@ class WSLCTests
6447 ValidateProcessOutput(process, {}, 0);
6448 }
6449
6450 - // Validate that relative tmpfs paths are rejected by Docker.
6450 + // Validate that relative tmpfs paths are rejected.
6451 {
6452 WSLCContainerLauncher launcher("debian:latest", "test-tmpfs-relative", {"/bin/cat"});
6453 launcher.AddTmpfs("relative-path", "");
6454
6455 auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
6456 - VERIFY_ARE_EQUAL(hresult, E_FAIL);
6456 + VERIFY_ARE_EQUAL(hresult, E_INVALIDARG);
6457
6458 - ValidateCOMErrorMessage(L"invalid mount path: 'relative-path' mount path must be absolute");
6458 + ValidateCOMErrorMessage(wsl::shared::Localization::WSLCCLI_MountTargetAbsoluteError());
6459 }
6460
6461 // Validate that invalid tmpfs options are rejected by Docker.
@@ -8539,9 +8539,16 @@ class WSLCTests
8539
8540 if (expectedSource.has_value())
8541 {
8542 - const std::filesystem::path actualSource(it->Source);
8543 - VERIFY_IS_TRUE(actualSource.is_absolute());
8544 - VERIFY_IS_TRUE(std::filesystem::equivalent(actualSource, expectedSource.value()));
8542 + if (expectedType == "bind")
8543 + {
8544 + const std::filesystem::path actualSource(it->Source);
8545 + VERIFY_IS_TRUE(actualSource.is_absolute());
8546 + VERIFY_IS_TRUE(std::filesystem::equivalent(actualSource, expectedSource.value()));
8547 + }
8548 + else
8549 + {
8550 + VERIFY_ARE_EQUAL(it->Source, expectedSource->string());
8551 + }
8552 }
8553 else
8554 {
@@ -8606,7 +8613,7 @@ class WSLCTests
8613 details.Mounts,
8614 {{"/test-volume", "bind", testFolder, true},
8615 {"/test-volume-ro", "bind", testFolderReadOnly, false},
8609 - {"/test-guest-volume", "volume", std::nullopt, true},
8616 + {"/test-guest-volume", "volume", std::filesystem::path{guestVolumeName}, true},
8617 {"/mnt/wslc-tmpfs-inspect", "tmpfs", std::nullopt, true}});
8618
8619 VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
test/windows/wslc/WSLCCLIArgumentUnitTests.cpp
+18 -1
@@ -33,6 +33,8 @@ using namespace WEX::Common;
33 using namespace WEX::TestExecution;
34
35 namespace WSLCCLIArgumentUnitTests {
36 +namespace mount = wsl::windows::common::mount;
37 +
38 using RawArgMapBase = EnumBasedVariantMap<ArgType, wsl::windows::wslc::argument::details::ArgDataMapping, &ArgMapInvalidateValidatedCache>;
39
40 static_assert(!std::is_convertible_v<ArgMap*, RawArgMapBase*>);
@@ -410,6 +412,22 @@ class WSLCCLIArgumentUnitTests
412 // string -> int64_t (nano CPUs)
413 VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::Cpus>(L"1.5"), validation::GetNanoCpusFromString(L"1.5"));
414
415 + // mount strings -> mount::Spec
416 + {
417 + const auto volume = ValidateAndGetCached<ArgType::Volume>(LR"(C:\hostPath:/containerPath)");
418 + VERIFY_ARE_EQUAL(static_cast<int>(mount::Type::Bind), static_cast<int>(volume.MountType));
419 + VERIFY_ARE_EQUAL(static_cast<int>(mount::BindSourcePolicy::CreateIfMissing), static_cast<int>(volume.BindSource));
420 +
421 + const auto tmpfs = ValidateAndGetCached<ArgType::TMPFS>(L"/tmp:size=64k");
422 + VERIFY_ARE_EQUAL(static_cast<int>(mount::Type::Tmpfs), static_cast<int>(tmpfs.MountType));
423 + VERIFY_IS_TRUE(tmpfs.TmpfsOptions.has_value());
424 + VERIFY_ARE_EQUAL(std::string("size=64k"), tmpfs.TmpfsOptions.value());
425 +
426 + const auto structured = ValidateAndGetCached<ArgType::Mount>(L"type=volume,source=data-volume,target=/data");
427 + VERIFY_ARE_EQUAL(static_cast<int>(mount::Type::Volume), static_cast<int>(structured.MountType));
428 + VERIFY_ARE_EQUAL(std::wstring(L"data-volume"), structured.Source);
429 + }
430 +
431 // string -> tuple<name, soft, hard> (ulimit)
432 auto ulimit = ValidateAndGetCached<ArgType::Ulimit>(L"nofile=1024:2048");
433 VERIFY_ARE_EQUAL(std::get<0>(ulimit), std::string("nofile"));
@@ -544,7 +562,6 @@ class WSLCCLIArgumentUnitTests
562
563 const std::vector<Case> cases = {
564 {ArgType::Gpus, L"all"},
547 - {ArgType::Volume, LR"(C:\hostPath:/containerPath)"},
565 {ArgType::WorkDir, L"/app"},
566 {ArgType::NetworkAlias, L"myalias"},
567 };
test/windows/wslc/WSLCCLIMountParserUnitTests.cpp new
+548
@@ -0,0 +1,548 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCCLIMountParserUnitTests.cpp
8 +
9 +Abstract:
10 +
11 + Unit tests for Docker-compatible --mount parsing.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "windows/Common.h"
17 +#include "WSLCCLITestHelpers.h"
18 +#include "ContainerModel.h"
19 +#include "MountSpecParsing.h"
20 +
21 +using namespace wsl::windows::common;
22 +using namespace wsl::windows::wslc::models;
23 +using namespace wsl::shared;
24 +using namespace WEX::Logging;
25 +using namespace WEX::Common;
26 +
27 +namespace WSLCCLIMountParserUnitTests {
28 +
29 +namespace {
30 +
31 + mount::Spec ParseAndValidate(const std::wstring& value)
32 + {
33 + auto mountSpec = mount::ParseDockerMountString(value);
34 + mount::ValidateMountSpec(mountSpec);
35 + return mountSpec;
36 + }
37 +
38 + struct ValidMountCase
39 + {
40 + const wchar_t* Input;
41 + mount::Type Type;
42 + const wchar_t* Source;
43 + const char* Target;
44 + bool ReadOnly;
45 + std::optional<int64_t> TmpfsSizeBytes;
46 + std::optional<uint32_t> TmpfsMode;
47 + const char* TmpfsOptions;
48 + };
49 +
50 + enum class ExpectedException
51 + {
52 + Parse,
53 + Unsupported,
54 + Validation,
55 + };
56 +
57 + struct InvalidMountCase
58 + {
59 + const wchar_t* Input;
60 + std::wstring ExpectedReason;
61 + ExpectedException Exception = ExpectedException::Parse;
62 + };
63 +
64 + constexpr ValidMountCase c_validMountCases[] = {
65 + {L"type=volume,target=/data", mount::Type::Volume, L"", "/data", false, {}, {}, ""},
66 + {L"source=data-volume,target=/data", mount::Type::Volume, L"data-volume", "/data", false, {}, {}, ""},
67 + {L"type=volume,source=data-volume,target=/path:voldir",
68 + mount::Type::Volume,
69 + L"data-volume",
70 + "/path:voldir",
71 + false,
72 + {},
73 + {},
74 + ""},
75 + {L"TYPE=VOLUME,SOURCE=data-volume,TARGET=/data", mount::Type::Volume, L"data-volume", "/data", false, {}, {}, ""},
76 + {L"type=VoLuMe,source=data-volume,target=/data", mount::Type::Volume, L"data-volume", "/data", false, {}, {}, ""},
77 + {L"type=volume,src=data-volume,dst=/data", mount::Type::Volume, L"data-volume", "/data", false, {}, {}, ""},
78 + {L"type=volume,src=data-volume,destination=/data", mount::Type::Volume, L"data-volume", "/data", false, {}, {}, ""},
79 + {L"type=volume,source=first,source=second,target=/data", mount::Type::Volume, L"second", "/data", false, {}, {}, ""},
80 + {L"type=volume,source=data-volume,target=/first,target=/second",
81 + mount::Type::Volume,
82 + L"data-volume",
83 + "/second",
84 + false,
85 + {},
86 + {},
87 + ""},
88 + {L"type=volume,type=bind,source=C:\\data,target=/data", mount::Type::Bind, L"C:\\data", "/data", false, {}, {}, ""},
89 + {L"type=volume,source=data-volume,target=/data,readonly", mount::Type::Volume, L"data-volume", "/data", true, {}, {}, ""},
90 + {L"type=volume,source=data-volume,target=/data,ro", mount::Type::Volume, L"data-volume", "/data", true, {}, {}, ""},
91 + {L"type=volume,source=data-volume,target=/data,readonly=1",
92 + mount::Type::Volume,
93 + L"data-volume",
94 + "/data",
95 + true,
96 + {},
97 + {},
98 + ""},
99 + {L"type=volume,source=data-volume,target=/data,readonly=t",
100 + mount::Type::Volume,
101 + L"data-volume",
102 + "/data",
103 + true,
104 + {},
105 + {},
106 + ""},
107 + {L"type=volume,source=data-volume,target=/data,readonly=T",
108 + mount::Type::Volume,
109 + L"data-volume",
110 + "/data",
111 + true,
112 + {},
113 + {},
114 + ""},
115 + {L"type=volume,source=data-volume,target=/data,readonly=TRUE",
116 + mount::Type::Volume,
117 + L"data-volume",
118 + "/data",
119 + true,
120 + {},
121 + {},
122 + ""},
123 + {L"type=volume,source=data-volume,target=/data,readonly=true",
124 + mount::Type::Volume,
125 + L"data-volume",
126 + "/data",
127 + true,
128 + {},
129 + {},
130 + ""},
131 + {L"type=volume,source=data-volume,target=/data,readonly=True",
132 + mount::Type::Volume,
133 + L"data-volume",
134 + "/data",
135 + true,
136 + {},
137 + {},
138 + ""},
139 + {L"type=volume,source=data-volume,target=/data,readonly=0",
140 + mount::Type::Volume,
141 + L"data-volume",
142 + "/data",
143 + false,
144 + {},
145 + {},
146 + ""},
147 + {L"type=volume,source=data-volume,target=/data,readonly=f",
148 + mount::Type::Volume,
149 + L"data-volume",
150 + "/data",
151 + false,
152 + {},
153 + {},
154 + ""},
155 + {L"type=volume,source=data-volume,target=/data,readonly=F",
156 + mount::Type::Volume,
157 + L"data-volume",
158 + "/data",
159 + false,
160 + {},
161 + {},
162 + ""},
163 + {L"type=volume,source=data-volume,target=/data,readonly=FALSE",
164 + mount::Type::Volume,
165 + L"data-volume",
166 + "/data",
167 + false,
168 + {},
169 + {},
170 + ""},
171 + {L"type=volume,source=data-volume,target=/data,readonly=false",
172 + mount::Type::Volume,
173 + L"data-volume",
174 + "/data",
175 + false,
176 + {},
177 + {},
178 + ""},
179 + {L"type=volume,source=data-volume,target=/data,readonly=False",
180 + mount::Type::Volume,
181 + L"data-volume",
182 + "/data",
183 + false,
184 + {},
185 + {},
186 + ""},
187 + {L"type=volume,source=data-volume,target=/data,readonly=true,readonly=false",
188 + mount::Type::Volume,
189 + L"data-volume",
190 + "/data",
191 + false,
192 + {},
193 + {},
194 + ""},
195 + {L"type=bind,\"source=C:\\mount,a\",target=/data", mount::Type::Bind, L"C:\\mount,a", "/data", false, {}, {}, ""},
196 + {L"type=bind,source=C:\\mount with spaces,target=/data",
197 + mount::Type::Bind,
198 + L"C:\\mount with spaces",
199 + "/data",
200 + false,
201 + {},
202 + {},
203 + ""},
204 + {L"type=bind,source=C:\\mount,target=/path:mntdir", mount::Type::Bind, L"C:\\mount", "/path:mntdir", false, {}, {}, ""},
205 + {L"type=bind,source=C:\\,target=/data", mount::Type::Bind, L"C:\\", "/data", false, {}, {}, ""},
206 + {L"type=bind,source=\\\\server\\share,target=/data", mount::Type::Bind, L"\\\\server\\share", "/data", false, {}, {}, ""},
207 + {L"type=bind,source=C:\\mount,target=/data,bind-recursive=enabled",
208 + mount::Type::Bind,
209 + L"C:\\mount",
210 + "/data",
211 + false,
212 + {},
213 + {},
214 + ""},
215 + {L"type=volume,source=A_,target=/data", mount::Type::Volume, L"A_", "/data", false, {}, {}, ""},
216 + {L"type=volume,source=data.volume-1,target=/data", mount::Type::Volume, L"data.volume-1", "/data", false, {}, {}, ""},
217 + {L"type=tmpfs,target=/tmp", mount::Type::Tmpfs, L"", "/tmp", false, {}, {}, ""},
218 + {L"type=tmpfs,target=/path:tmpfs", mount::Type::Tmpfs, L"", "/path:tmpfs", false, {}, {}, ""},
219 + {L"type=tmpfs,target=/tmp,readonly", mount::Type::Tmpfs, L"", "/tmp", true, {}, {}, "ro"},
220 + {L"type=tmpfs,target=/tmp,tmpfs-size=0", mount::Type::Tmpfs, L"", "/tmp", false, 0, {}, ""},
221 + {L"type=tmpfs,target=/tmp,tmpfs-size=1", mount::Type::Tmpfs, L"", "/tmp", false, 1, {}, "size=1"},
222 + {L"type=tmpfs,target=/tmp,tmpfs-size=1024", mount::Type::Tmpfs, L"", "/tmp", false, 1024, {}, "size=1k"},
223 + {L"type=tmpfs,target=/tmp,tmpfs-size=1536", mount::Type::Tmpfs, L"", "/tmp", false, 1536, {}, "size=1536"},
224 + {L"type=tmpfs,target=/tmp,tmpfs-size=1k", mount::Type::Tmpfs, L"", "/tmp", false, 1024, {}, "size=1k"},
225 + {L"type=tmpfs,target=/tmp,tmpfs-size=1KB", mount::Type::Tmpfs, L"", "/tmp", false, 1024, {}, "size=1k"},
226 + {L"type=tmpfs,target=/tmp,tmpfs-size=1KiB", mount::Type::Tmpfs, L"", "/tmp", false, 1024, {}, "size=1k"},
227 + {L"type=tmpfs,target=/tmp,tmpfs-size=1MB", mount::Type::Tmpfs, L"", "/tmp", false, 1LL << 20, {}, "size=1m"},
228 + {L"type=tmpfs,target=/tmp,tmpfs-size=1MiB", mount::Type::Tmpfs, L"", "/tmp", false, 1LL << 20, {}, "size=1m"},
229 + {L"type=tmpfs,target=/tmp,tmpfs-size=1GB", mount::Type::Tmpfs, L"", "/tmp", false, 1LL << 30, {}, "size=1g"},
230 + {L"type=tmpfs,target=/tmp,tmpfs-size=1.5MB", mount::Type::Tmpfs, L"", "/tmp", false, 1536LL << 10, {}, "size=1536k"},
231 + {L"type=tmpfs,target=/tmp,tmpfs-size=+1MB", mount::Type::Tmpfs, L"", "/tmp", false, 1LL << 20, {}, "size=1m"},
232 + {L"type=tmpfs,target=/tmp,tmpfs-size=1e3", mount::Type::Tmpfs, L"", "/tmp", false, 1000, {}, "size=1000"},
233 + {L"type=tmpfs,target=/tmp,tmpfs-mode=0000", mount::Type::Tmpfs, L"", "/tmp", false, {}, 0, ""},
234 + {L"type=tmpfs,target=/tmp,tmpfs-mode=0700", mount::Type::Tmpfs, L"", "/tmp", false, {}, 0700, "mode=700"},
235 + {L"type=tmpfs,target=/tmp,tmpfs-mode=+0700", mount::Type::Tmpfs, L"", "/tmp", false, {}, 0700, "mode=700"},
236 + {L"type=tmpfs,target=/tmp,tmpfs-size=1MB,tmpfs-mode=0700,readonly",
237 + mount::Type::Tmpfs,
238 + L"",
239 + "/tmp",
240 + true,
241 + 1LL << 20,
242 + 0700,
243 + "ro,mode=700,size=1m"},
244 + {L"type=tmpfs,target=/tmp,tmpfs-size=0,tmpfs-mode=0000,readonly=false", mount::Type::Tmpfs, L"", "/tmp", false, 0, 0, ""},
245 + };
246 +
247 + const InvalidMountCase c_invalidMountCases[] = {
248 + {L"", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"")},
249 + {L",", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"")},
250 + {L"type=volume,source=data-volume,target=/data,", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"")},
251 + {L",type=volume,source=data-volume,target=/data", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"")},
252 + {L"type=bind,\"source=C:\\mount,target=/data", Localization::WSLCCLI_MountMalformedCsvError()},
253 + {L"type=volume,bogus", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"bogus")},
254 + {L"type=volume,bogus=value", Localization::WSLCCLI_MountUnexpectedKeyError(L"bogus", L"bogus=value")},
255 + {L"type", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"type")},
256 + {L"source", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"source")},
257 + {L"target", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"target")},
258 + {L"type=,source=data-volume,target=/data", Localization::WSLCCLI_MountTypeRequiredError()},
259 + {L"type=volume,source=data-volume", Localization::WSLCCLI_MountTargetRequiredError(), ExpectedException::Validation},
260 + {L"type=volume,source=data-volume,target=", Localization::WSLCCLI_MountTargetRequiredError(), ExpectedException::Validation},
261 + {L"type=volume,source=data-volume,dst=", Localization::WSLCCLI_MountTargetRequiredError(), ExpectedException::Validation},
262 + {L"type=cluster,source=data-volume,target=/data", Localization::WSLCCLI_MountTypeUnsupportedError(L"cluster"), ExpectedException::Unsupported},
263 + {L"type=npipe,source=data-volume,target=/data", Localization::WSLCCLI_MountTypeUnsupportedError(L"npipe"), ExpectedException::Unsupported},
264 + {L"type=bogus,source=data-volume,target=/data", Localization::WSLCCLI_MountTypeUnsupportedError(L"bogus"), ExpectedException::Unsupported},
265 + {L"type=CLUSTER,source=data-volume,target=/data", Localization::WSLCCLI_MountTypeUnsupportedError(L"cluster"), ExpectedException::Unsupported},
266 + {L"type=bind,target=/data", Localization::WSLCCLI_MountSourceRequiredError(), ExpectedException::Validation},
267 + {L"type=bind,source=relative,target=/data", Localization::WSLCCLI_MountBindSourceAbsoluteError(), ExpectedException::Validation},
268 + {L"type=volume,source=a,target=/data", Localization::WSLCCLI_MountVolumeSourceInvalidError(), ExpectedException::Validation},
269 + {L"type=volume,source=data/volume,target=/data", Localization::WSLCCLI_MountVolumeSourceInvalidError(), ExpectedException::Validation},
270 + {L"type=volume,source=C:\\mount,target=/data", Localization::WSLCCLI_MountVolumeSourceInvalidError(), ExpectedException::Validation},
271 + {L"type=tmpfs,source=data-volume,target=/data", Localization::WSLCCLI_MountTmpfsSourceUnsupportedError(), ExpectedException::Validation},
272 + {L"type=volume,source=data-volume,target=data", Localization::WSLCCLI_MountTargetAbsoluteError(), ExpectedException::Validation},
273 + {L"type=volume,source=data-volume,dst=.", Localization::WSLCCLI_MountTargetAbsoluteError(), ExpectedException::Validation},
274 + {L"type=volume,source=data-volume,destination=\\data", Localization::WSLCCLI_MountTargetAbsoluteError(), ExpectedException::Validation},
275 + {L"type=bind,source=C:\\mount,target=data", Localization::WSLCCLI_MountTargetAbsoluteError(), ExpectedException::Validation},
276 + {L"type=tmpfs,target=data", Localization::WSLCCLI_MountTargetAbsoluteError(), ExpectedException::Validation},
277 + {L"type=volume,source=data-volume,target=/data,readonly=no",
278 + Localization::WSLCCLI_MountInvalidValueError(L"readonly", L"no")},
279 + {L"type=volume,source=data-volume,target=/data,readonly=yes",
280 + Localization::WSLCCLI_MountInvalidValueError(L"readonly", L"yes")},
281 + {L"type=volume,source=data-volume,target=/data,readonly=", Localization::WSLCCLI_MountInvalidValueError(L"readonly", L"")},
282 + {L"type=volume,source=data-volume,target=/data,readonly=2",
283 + Localization::WSLCCLI_MountInvalidValueError(L"readonly", L"2")},
284 + {L"type=volume,source=data-volume,target=/data,volume-nocopy=no",
285 + Localization::WSLCCLI_MountInvalidValueError(L"volume-nocopy", L"no")},
286 + {L"type=volume,source=data-volume,target=/data,volume-nocopy=",
287 + Localization::WSLCCLI_MountInvalidValueError(L"volume-nocopy", L"")},
288 + {L"type=bind,source=C:\\mount,target=/data,bind-nonrecursive=no",
289 + Localization::WSLCCLI_MountInvalidValueError(L"bind-nonrecursive", L"no")},
290 + {L"type=bind,source=C:\\mount,target=/data,bind-recursive=",
291 + Localization::WSLCCLI_MountInvalidBindRecursiveValueError(L"bind-recursive", L"")},
292 + {L"type=bind,source=C:\\mount,target=/data,bind-recursive=Enabled",
293 + Localization::WSLCCLI_MountInvalidBindRecursiveValueError(L"bind-recursive", L"Enabled")},
294 + {L"type=bind,source=C:\\mount,target=/data,bind-recursive=bogus",
295 + Localization::WSLCCLI_MountInvalidBindRecursiveValueError(L"bind-recursive", L"bogus")},
296 + {L"type=bind,source=C:\\mount,target=/data,bind-recursive=writable",
297 + Localization::WSLCCLI_MountOptionRequiresReadonlyError(L"bind-recursive=writable")},
298 + {L"type=bind,source=C:\\mount,target=/data,bind-recursive=readonly",
299 + Localization::WSLCCLI_MountOptionRequiresReadonlyError(L"bind-recursive=readonly")},
300 + {L"type=bind,source=C:\\mount,target=/data,bind-recursive=readonly,readonly",
301 + Localization::WSLCCLI_MountBindRecursiveReadonlyRequiresPropagationError()},
302 + {L"type=bind,source=C:\\mount,target=/data,consistency=cached",
303 + Localization::WSLCCLI_MountOptionUnsupportedError(L"consistency"),
304 + ExpectedException::Unsupported},
305 + {L"type=bind,source=C:\\mount,target=/data,bind-propagation=rprivate",
306 + Localization::WSLCCLI_MountOptionUnsupportedError(L"bind-propagation"),
307 + ExpectedException::Unsupported},
308 + {L"type=bind,source=C:\\mount,target=/data,bind-nonrecursive",
309 + Localization::WSLCCLI_MountOptionUnsupportedError(L"bind-nonrecursive"),
310 + ExpectedException::Unsupported},
311 + {L"type=bind,source=C:\\mount,target=/data,bind-nonrecursive=true",
312 + Localization::WSLCCLI_MountOptionUnsupportedError(L"bind-nonrecursive"),
313 + ExpectedException::Unsupported},
314 + {L"type=bind,source=C:\\mount,target=/data,bind-recursive=disabled",
315 + Localization::WSLCCLI_MountOptionUnsupportedError(L"bind-recursive"),
316 + ExpectedException::Unsupported},
317 + {L"type=bind,source=C:\\mount,target=/data,bind-recursive=writable,readonly",
318 + Localization::WSLCCLI_MountOptionUnsupportedError(L"bind-recursive"),
319 + ExpectedException::Unsupported},
320 + {L"type=bind,source=C:\\mount,target=/data,bind-recursive=readonly,readonly,bind-propagation=rprivate",
321 + Localization::WSLCCLI_MountOptionUnsupportedError(L"bind-recursive"),
322 + ExpectedException::Unsupported},
323 + {L"type=volume,source=data-volume,target=/data,volume-nocopy",
324 + Localization::WSLCCLI_MountOptionUnsupportedError(L"volume-nocopy"),
325 + ExpectedException::Unsupported},
326 + {L"type=volume,source=data-volume,target=/data,volume-nocopy=true",
327 + Localization::WSLCCLI_MountOptionUnsupportedError(L"volume-nocopy"),
328 + ExpectedException::Unsupported},
329 + {L"type=volume,source=data-volume,target=/data,volume-label=a=b",
330 + Localization::WSLCCLI_MountOptionUnsupportedError(L"volume-label"),
331 + ExpectedException::Unsupported},
332 + {L"type=volume,source=data-volume,target=/data,volume-driver=local",
333 + Localization::WSLCCLI_MountOptionUnsupportedError(L"volume-driver"),
334 + ExpectedException::Unsupported},
335 + {L"type=volume,source=data-volume,target=/data,volume-opt=a=b",
336 + Localization::WSLCCLI_MountOptionUnsupportedError(L"volume-opt"),
337 + ExpectedException::Unsupported},
338 + {L"type=bind,source=C:\\mount,target=/data,volume-nocopy=true",
339 + Localization::WSLCCLI_MountOptionFamilyMismatchError(L"volume-*", L"bind")},
340 + {L"type=volume,source=data-volume,target=/data,bind-propagation=rprivate",
341 + Localization::WSLCCLI_MountOptionFamilyMismatchError(L"bind-*", L"volume")},
342 + {L"type=volume,source=data-volume,target=/data,bind-recursive=enabled",
343 + Localization::WSLCCLI_MountOptionFamilyMismatchError(L"bind-*", L"volume")},
344 + {L"type=volume,source=data-volume,target=/data,tmpfs-size=1m",
345 + Localization::WSLCCLI_MountOptionFamilyMismatchError(L"tmpfs-*", L"volume")},
346 + {L"type=tmpfs,target=/tmp,volume-label=a=b", Localization::WSLCCLI_MountOptionFamilyMismatchError(L"volume-*", L"tmpfs")},
347 + {L"type=tmpfs,target=/tmp,bind-nonrecursive", Localization::WSLCCLI_MountOptionFamilyMismatchError(L"bind-*", L"tmpfs")},
348 + {L"type=tmpfs,target=/tmp,tmpfs-size=", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"")},
349 + {L"type=tmpfs,target=/tmp,tmpfs-size=bad", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"bad")},
350 + {L"type=tmpfs,target=/tmp,tmpfs-size=-1", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"-1")},
351 + {L"type=tmpfs,target=/tmp,\"tmpfs-size=1,5MB\"", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"1,5MB")},
352 + {L"type=tmpfs,target=/tmp,tmpfs-size=1XB", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"1XB")},
353 + {L"type=tmpfs,target=/tmp,tmpfs-size=1Ki", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"1Ki")},
354 + {L"type=tmpfs,target=/tmp,tmpfs-size=1BB", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"1BB")},
355 + {L"type=tmpfs,target=/tmp,tmpfs-size=nan", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"nan")},
356 + {L"type=tmpfs,target=/tmp,tmpfs-size=inf", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"inf")},
357 + {L"type=tmpfs,target=/tmp,tmpfs-size=9223372036854775808",
358 + Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"9223372036854775808")},
359 + {L"type=tmpfs,target=/tmp,tmpfs-mode=", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"")},
360 + {L"type=tmpfs,target=/tmp,tmpfs-mode=-1", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"-1")},
361 + {L"type=tmpfs,target=/tmp,tmpfs-mode=8", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"8")},
362 + {L"type=tmpfs,target=/tmp,tmpfs-mode=0899", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"0899")},
363 + {L"type=tmpfs,target=/tmp,tmpfs-mode=0x700", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"0x700")},
364 + {L"type=tmpfs,target=/tmp,tmpfs-mode=40000000000",
365 + Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"40000000000")},
366 + };
367 +
368 +} // namespace
369 +
370 +class WSLCCLIMountParserUnitTests
371 +{
372 + WSLC_TEST_CLASS(WSLCCLIMountParserUnitTests)
373 +
374 + TEST_METHOD(Mount_ValidCases)
375 + {
376 + for (const auto& testCase : c_validMountCases)
377 + {
378 + Log::Comment(String().Format(L"Accepting: %ls", testCase.Input));
379 +
380 + const auto actual = ParseAndValidate(testCase.Input);
381 + VERIFY_ARE_EQUAL(static_cast<int>(testCase.Type), static_cast<int>(actual.MountType));
382 + VERIFY_ARE_EQUAL(std::wstring(testCase.Source), actual.Source);
383 + VERIFY_ARE_EQUAL(std::string(testCase.Target), actual.Target);
384 + VERIFY_ARE_EQUAL(testCase.ReadOnly, actual.ReadOnly);
385 + VERIFY_ARE_EQUAL(testCase.TmpfsSizeBytes.has_value(), actual.TmpfsSizeBytes.has_value());
386 + if (testCase.TmpfsSizeBytes.has_value() && actual.TmpfsSizeBytes.has_value())
387 + {
388 + VERIFY_ARE_EQUAL(testCase.TmpfsSizeBytes.value(), actual.TmpfsSizeBytes.value());
389 + }
390 +
391 + VERIFY_ARE_EQUAL(testCase.TmpfsMode.has_value(), actual.TmpfsMode.has_value());
392 + if (testCase.TmpfsMode.has_value() && actual.TmpfsMode.has_value())
393 + {
394 + VERIFY_ARE_EQUAL(testCase.TmpfsMode.value(), actual.TmpfsMode.value());
395 + }
396 +
397 + const auto actualTmpfsOptions = actual.MountType == mount::Type::Tmpfs ? mount::FormatTmpfsOptions(actual) : std::string{};
398 + VERIFY_ARE_EQUAL(std::string(testCase.TmpfsOptions), actualTmpfsOptions);
399 + }
400 + }
401 +
402 + TEST_METHOD(Mount_InvalidCases)
403 + {
404 + for (const auto& testCase : c_invalidMountCases)
405 + {
406 + Log::Comment(String().Format(L"Rejecting: %ls", testCase.Input));
407 +
408 + try
409 + {
410 + (void)ParseAndValidate(testCase.Input);
411 + VERIFY_FAIL(L"Expected MountException for invalid mount spec");
412 + }
413 + catch (const mount::MountException& ex)
414 + {
415 + VERIFY_ARE_EQUAL(testCase.ExpectedReason, ex.Reason());
416 + switch (testCase.Exception)
417 + {
418 + case ExpectedException::Parse:
419 + VERIFY_IS_TRUE(dynamic_cast<const mount::MountParseException*>(&ex) != nullptr);
420 + break;
421 +
422 + case ExpectedException::Unsupported:
423 + VERIFY_IS_TRUE(dynamic_cast<const mount::MountUnsupportedException*>(&ex) != nullptr);
424 + break;
425 +
426 + case ExpectedException::Validation:
427 + VERIFY_IS_TRUE(dynamic_cast<const mount::MountValidationException*>(&ex) != nullptr);
428 + break;
429 + }
430 + }
431 + }
432 + }
433 +
434 + TEST_METHOD(Volume_ValidCases)
435 + {
436 + const auto bind = mount::ParseDockerVolumeString(LR"(C:\hostPath:/data:ro)");
437 + VERIFY_ARE_EQUAL(static_cast<int>(mount::Type::Bind), static_cast<int>(bind.MountType));
438 + VERIFY_ARE_EQUAL(std::wstring(LR"(C:\hostPath)"), bind.Source);
439 + VERIFY_ARE_EQUAL(std::string("/data"), bind.Target);
440 + VERIFY_IS_TRUE(bind.ReadOnly);
441 + VERIFY_ARE_EQUAL(static_cast<int>(mount::BindSourcePolicy::CreateIfMissing), static_cast<int>(bind.BindSource));
442 +
443 + const auto volume = mount::ParseDockerVolumeString(L"named-volume:/data");
444 + VERIFY_ARE_EQUAL(static_cast<int>(mount::Type::Volume), static_cast<int>(volume.MountType));
445 + VERIFY_ARE_EQUAL(std::wstring(L"named-volume"), volume.Source);
446 + VERIFY_ARE_EQUAL(std::string("/data"), volume.Target);
447 + VERIFY_IS_FALSE(volume.ReadOnly);
448 + VERIFY_ARE_EQUAL(static_cast<int>(mount::BindSourcePolicy::RequireExisting), static_cast<int>(volume.BindSource));
449 + }
450 +
451 + TEST_METHOD(Mount_DotRelativeBindSourceUsesCurrentDirectory)
452 + {
453 + const auto expected = (std::filesystem::current_path() / L"mount").lexically_normal().wstring();
454 + const auto actual = ParseAndValidate(L"type=bind,source=.\\mount,target=/data");
455 + VERIFY_ARE_EQUAL(expected, actual.Source);
456 + }
457 +
458 + TEST_METHOD(Mount_TypedSpecsAreValidated)
459 + {
460 + const mount::Spec relativeBind{
461 + .MountType = mount::Type::Bind,
462 + .Source = L"relative",
463 + .Target = "/data",
464 + };
465 + VERIFY_THROWS(mount::ValidateMountSpec(relativeBind), mount::MountValidationException);
466 +
467 + const mount::Spec relativeTarget{
468 + .MountType = mount::Type::Volume,
469 + .Source = L"data-volume",
470 + .Target = "data",
471 + };
472 + VERIFY_THROWS(mount::ValidateMountSpec(relativeTarget), mount::MountValidationException);
473 +
474 + const mount::Spec tmpfsWithSource{
475 + .MountType = mount::Type::Tmpfs,
476 + .Source = L"data-volume",
477 + .Target = "/data",
478 + };
479 + VERIFY_THROWS(mount::ValidateMountSpec(tmpfsWithSource), mount::MountValidationException);
480 +
481 + const mount::Spec bindWithTmpfsOptions{
482 + .MountType = mount::Type::Bind,
483 + .Source = L"C:\\data",
484 + .Target = "/data",
485 + .TmpfsSizeBytes = 1024,
486 + };
487 + VERIFY_THROWS(mount::ValidateMountSpec(bindWithTmpfsOptions), mount::MountValidationException);
488 +
489 + const mount::Spec negativeTmpfsSize{
490 + .MountType = mount::Type::Tmpfs,
491 + .Target = "/data",
492 + .TmpfsSizeBytes = -1,
493 + };
494 + VERIFY_THROWS(mount::ValidateMountSpec(negativeTmpfsSize), mount::MountValidationException);
495 +
496 + const mount::Spec tmpfs{
497 + .MountType = mount::Type::Tmpfs,
498 + .Target = "/data",
499 + .TmpfsSizeBytes = 1024,
500 + .TmpfsMode = 0700,
501 + };
502 + VERIFY_NO_THROW(mount::ValidateMountSpec(tmpfs));
503 +
504 + const mount::Spec duplicateMounts[] = {
505 + {.MountType = mount::Type::Tmpfs, .Target = "/data"},
506 + {.MountType = mount::Type::Volume, .Source = L"data-volume", .Target = "/data/"},
507 + };
508 + try
509 + {
510 + mount::ValidateMountCollection(duplicateMounts);
511 + VERIFY_FAIL(L"Expected MountValidationException for duplicate destinations");
512 + }
513 + catch (const mount::MountValidationException& ex)
514 + {
515 + VERIFY_ARE_EQUAL(static_cast<int>(mount::ValidationError::DuplicateDestination), static_cast<int>(ex.Error()));
516 + VERIFY_ARE_EQUAL(std::string("/data"), ex.Destination());
517 + }
518 + }
519 +
520 + TEST_METHOD(Mount_DuplicateDestinationsAreRejected)
521 + {
522 + ContainerOptions options;
523 + options.Mounts = {
524 + mount::ParseDockerTmpfsString(L"/data"),
525 + {.MountType = mount::Type::Volume, .Source = L"data-volume", .Target = "/data/"},
526 + };
527 + VERIFY_THROWS(ValidateUniqueMountDestinations(options), wil::ResultException);
528 +
529 + options.Mounts = {
530 + {.MountType = mount::Type::Tmpfs, .Target = "/data/../cache"},
531 + {.MountType = mount::Type::Volume, .Source = L"data-volume", .Target = "/cache"},
532 + };
533 + VERIFY_THROWS(ValidateUniqueMountDestinations(options), wil::ResultException);
534 + }
535 +
536 + TEST_METHOD(Mount_UniqueDestinationsAreAccepted)
537 + {
538 + ContainerOptions options;
539 + options.Mounts = {
540 + mount::ParseDockerTmpfsString(L"/cache"),
541 + {.MountType = mount::Type::Volume, .Source = L"data-volume", .Target = "/data"},
542 + {.MountType = mount::Type::Bind, .Source = L"C:\\logs", .Target = "/logs"},
543 + };
544 + VERIFY_NO_THROW(ValidateUniqueMountDestinations(options));
545 + }
546 +};
547 +
548 +} // namespace WSLCCLIMountParserUnitTests
test/windows/wslc/WSLCCLITmpfsParserUnitTests.cpp
+32 -19
@@ -13,10 +13,9 @@ Abstract:
13
14 #include "precomp.h"
15 #include "windows/Common.h"
16 -#include "WSLCCLITestHelpers.h"
17 -#include "ContainerModel.h"
16 +#include "MountSpecParsing.h"
17
19 -using namespace wsl::windows::wslc;
18 +using namespace wsl::windows::common;
19
20 namespace WSLCCLITmpfsParserUnitTests {
21
@@ -26,26 +25,40 @@ class WSLCCLITmpfsParserUnitTests
25
26 TEST_METHOD(WSLCCLITmpfsMount_Parse)
27 {
29 - std::vector<std::tuple<std::string, std::string, std::string>> validTmpfsSpecs = {
30 - {"", "", ""},
31 - {"/tmp", "/tmp", ""},
32 - {"/tmp:size=50m", "/tmp", "size=50m"},
33 - {"/var/tmp:size=1g", "/var/tmp", "size=1g"},
34 - {"/tmp:size=50m,mode=1777", "/tmp", "size=50m,mode=1777"},
35 - {"/cache:uid=1000,gid=1000", "/cache", "uid=1000,gid=1000"},
36 - {"/mnt/ramdisk:size=256k,nr_inodes=1k", "/mnt/ramdisk", "size=256k,nr_inodes=1k"},
37 - {"/securetmp:mode=0700", "/securetmp", "mode=0700"},
38 - {"/scratch:nosuid,nodev,noexec", "/scratch", "nosuid,nodev,noexec"},
39 - {"/wsl/tmp:size=2g,uid=0,gid=0,mode=1777", "/wsl/tmp", "size=2g,uid=0,gid=0,mode=1777"},
28 + const std::vector<std::tuple<std::wstring, std::string, std::string>> validTmpfsSpecs = {
29 + {L"", "", ""},
30 + {L"/tmp", "/tmp", ""},
31 + {L"/tmp:size=50m", "/tmp", "size=50m"},
32 + {L"/var/tmp:size=1g", "/var/tmp", "size=1g"},
33 + {L"/tmp:size=50m,mode=1777", "/tmp", "size=50m,mode=1777"},
34 + {L"/cache:uid=1000,gid=1000", "/cache", "uid=1000,gid=1000"},
35 + {L"/mnt/ramdisk:size=256k,nr_inodes=1k", "/mnt/ramdisk", "size=256k,nr_inodes=1k"},
36 + {L"/securetmp:mode=0700", "/securetmp", "mode=0700"},
37 + {L"/scratch:nosuid,nodev,noexec", "/scratch", "nosuid,nodev,noexec"},
38 + {L"/wsl/tmp:size=2g,uid=0,gid=0,mode=1777", "/wsl/tmp", "size=2g,uid=0,gid=0,mode=1777"},
39 };
40
42 - for (const auto& [input, expectedContainerPath, expectedOptions] : validTmpfsSpecs)
41 + for (const auto& [input, expectedTarget, expectedOptions] : validTmpfsSpecs)
42 {
44 - auto result = models::TmpfsMount::Parse(input);
45 - VERIFY_ARE_EQUAL(expectedContainerPath, result.ContainerPath());
46 - VERIFY_ARE_EQUAL(expectedOptions, result.Options());
43 + const auto result = mount::ParseDockerTmpfsString(input);
44 + VERIFY_ARE_EQUAL(static_cast<int>(mount::Type::Tmpfs), static_cast<int>(result.MountType));
45 + VERIFY_ARE_EQUAL(expectedTarget, result.Target);
46 + VERIFY_IS_TRUE(result.TmpfsOptions.has_value());
47 + VERIFY_ARE_EQUAL(expectedOptions, result.TmpfsOptions.value());
48 }
49 }
50 +
51 + TEST_METHOD(WSLCCLITmpfsMount_Validate)
52 + {
53 + auto valid = mount::ParseDockerTmpfsString(L"/tmp:size=50m");
54 + VERIFY_NO_THROW(mount::ValidateMountSpec(valid));
55 +
56 + auto empty = mount::ParseDockerTmpfsString(L":size=50m");
57 + VERIFY_THROWS(mount::ValidateMountSpec(empty), mount::MountValidationException);
58 +
59 + auto relative = mount::ParseDockerTmpfsString(L"tmp:size=50m");
60 + VERIFY_THROWS(mount::ValidateMountSpec(relative), mount::MountValidationException);
61 + }
62 };
63
51 -} // namespace WSLCCLITmpfsParserUnitTests
\ No newline at end of file
64 +} // namespace WSLCCLITmpfsParserUnitTests
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+265 -44
@@ -46,6 +46,7 @@ class WSLCE2EContainerCreateTests
46 EnsureImageIsDeleted(AlpineImage);
47 EnsureImageIsDeleted(DebianImage);
48 EnsureImageIsDeleted(HelloWorldImage);
49 + EnsureVolumeDoesNotExist(WslcVolumeName);
50 EnsureNetworkDoesNotExist(TestNetworkName);
51
52 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), nullptr));
@@ -61,6 +62,7 @@ class WSLCE2EContainerCreateTests
62 VolumeTestFile1 = wsl::windows::common::filesystem::GetTempFilename();
63 VolumeTestFile2 = wsl::windows::common::filesystem::GetTempFilename();
64 EnsureContainerDoesNotExist(WslcContainerName);
65 + EnsureVolumeDoesNotExist(WslcVolumeName);
66 EnsureNetworkDoesNotExist(TestNetworkName);
67 return true;
68 }
@@ -352,8 +354,7 @@ class WSLCE2EContainerCreateTests
354 RunWslc(std::format(L"container run --name {} --volume :/containerPath {}", WslcContainerName, AlpineImage.NameAndTag()));
355 result.Verify({.Stdout = L"", .ExitCode = 1});
356 VERIFY_IS_TRUE(result.StderrContainsSubstring(
355 - L"Invalid volume specifications: ':/containerPath'. Host path cannot be empty. Expected format: <host path | "
356 - L"named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
357 + Localization::WSLCCLI_VolumeHostPathEmpty(L":/containerPath", Localization::WSLCCLI_VolumeFormatUsage())));
358 EnsureContainerDoesNotExist(WslcContainerName);
359 }
360
@@ -362,8 +363,7 @@ class WSLCE2EContainerCreateTests
363 std::format(L"container run --name {} --volume C:\\hostPath::ro {}", WslcContainerName, AlpineImage.NameAndTag()));
364 result.Verify({.Stdout = L"", .ExitCode = 1});
365 VERIFY_IS_TRUE(result.StderrContainsSubstring(
365 - L"Invalid volume specifications: 'C:\\hostPath::ro'. Container path cannot be empty. Expected format: <host path "
366 - L"| named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
366 + Localization::WSLCCLI_VolumeContainerPathEmpty(L"C:\\hostPath::ro", Localization::WSLCCLI_VolumeFormatUsage())));
367 EnsureContainerDoesNotExist(WslcContainerName);
368 }
369
@@ -372,17 +372,15 @@ class WSLCE2EContainerCreateTests
372 std::format(L"container run --name {} --volume :/containerPath:ro {}", WslcContainerName, AlpineImage.NameAndTag()));
373 result.Verify({.Stdout = L"", .ExitCode = 1});
374 VERIFY_IS_TRUE(result.StderrContainsSubstring(
375 - L"Invalid volume specifications: ':/containerPath:ro'. Host path cannot be empty. Expected format: <host path | "
376 - L"named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
375 + Localization::WSLCCLI_VolumeHostPathEmpty(L":/containerPath:ro", Localization::WSLCCLI_VolumeFormatUsage())));
376 EnsureContainerDoesNotExist(WslcContainerName);
377 }
378
379 {
380 auto result = RunWslc(std::format(L"container run --name {} --volume \"\" {}", WslcContainerName, AlpineImage.NameAndTag()));
381 result.Verify({.Stdout = L"", .ExitCode = 1});
383 - VERIFY_IS_TRUE(
384 - result.StderrContainsSubstring(L"Invalid volume specifications: ''. Expected format: <host path | named "
385 - L"volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
382 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
383 + Localization::WSLCCLI_VolumeInvalidSpec(L"", Localization::WSLCCLI_VolumeFormatUsage())));
384 EnsureContainerDoesNotExist(WslcContainerName);
385 }
386
@@ -391,8 +389,7 @@ class WSLCE2EContainerCreateTests
389 RunWslc(std::format(L"container run --name {} --volume C:\\hostPath: {}", WslcContainerName, AlpineImage.NameAndTag()));
390 result.Verify({.Stdout = L"", .ExitCode = 1});
391 VERIFY_IS_TRUE(result.StderrContainsSubstring(
394 - L"Invalid volume specifications: 'C:\\hostPath:'. Container path cannot be empty. Expected format: <host path | "
395 - L"named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
392 + Localization::WSLCCLI_VolumeContainerPathEmpty(L"C:\\hostPath:", Localization::WSLCCLI_VolumeFormatUsage())));
393 EnsureContainerDoesNotExist(WslcContainerName);
394 }
395
@@ -401,17 +398,15 @@ class WSLCE2EContainerCreateTests
398 RunWslc(std::format(L"container run --name {} --volume C:\\hostPath:ro {}", WslcContainerName, AlpineImage.NameAndTag()));
399 result.Verify({.Stdout = L"", .ExitCode = 1});
400 VERIFY_IS_TRUE(result.StderrContainsSubstring(
404 - L"Invalid volume specifications: 'C:\\hostPath:ro'. Container path must be an absolute path (starting with '/'). "
405 - L"Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
401 + Localization::WSLCCLI_VolumeContainerPathNotAbsolute(L"C:\\hostPath:ro", Localization::WSLCCLI_VolumeFormatUsage())));
402 EnsureContainerDoesNotExist(WslcContainerName);
403 }
404
405 {
406 auto result = RunWslc(std::format(L"container run --name {} --volume :ro {}", WslcContainerName, AlpineImage.NameAndTag()));
407 result.Verify({.Stdout = L"", .ExitCode = 1});
412 - VERIFY_IS_TRUE(
413 - result.StderrContainsSubstring(L"Invalid volume specifications: ':ro'. Expected format: <host path | named "
414 - L"volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
408 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
409 + Localization::WSLCCLI_VolumeInvalidSpec(L":ro", Localization::WSLCCLI_VolumeFormatUsage())));
410 EnsureContainerDoesNotExist(WslcContainerName);
411 }
412
@@ -420,8 +415,7 @@ class WSLCE2EContainerCreateTests
415 std::format(L"container run --name {} --volume C:\\hostPath::rw {}", WslcContainerName, AlpineImage.NameAndTag()));
416 result.Verify({.Stdout = L"", .ExitCode = 1});
417 VERIFY_IS_TRUE(result.StderrContainsSubstring(
423 - L"Invalid volume specifications: 'C:\\hostPath::rw'. Container path cannot be empty. Expected format: <host path "
424 - L"| named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
418 + Localization::WSLCCLI_VolumeContainerPathEmpty(L"C:\\hostPath::rw", Localization::WSLCCLI_VolumeFormatUsage())));
419 EnsureContainerDoesNotExist(WslcContainerName);
420 }
421
@@ -429,10 +423,8 @@ class WSLCE2EContainerCreateTests
423 auto result = RunWslc(std::format(
424 L"container run --name {} --volume C:\\hostPath:/containerPath:invalid_mode {}", WslcContainerName, AlpineImage.NameAndTag()));
425 result.Verify({.Stdout = L"", .ExitCode = 1});
432 - VERIFY_IS_TRUE(result.StderrContainsSubstring(
433 - L"Invalid volume specifications: 'C:\\hostPath:/containerPath:invalid_mode'. Container path must be an absolute "
434 - L"path (starting with '/'). Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: "
435 - L"E_INVALIDARG"));
426 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_VolumeContainerPathNotAbsolute(
427 + L"C:\\hostPath:/containerPath:invalid_mode", Localization::WSLCCLI_VolumeFormatUsage())));
428 EnsureContainerDoesNotExist(WslcContainerName);
429 }
430
@@ -440,10 +432,8 @@ class WSLCE2EContainerCreateTests
432 auto result = RunWslc(std::format(
433 L"container run --name {} --volume C:\\hostPath:/containerPath:ro:extra {}", WslcContainerName, AlpineImage.NameAndTag()));
434 result.Verify({.Stdout = L"", .ExitCode = 1});
443 - VERIFY_IS_TRUE(result.StderrContainsSubstring(
444 - L"Invalid volume specifications: 'C:\\hostPath:/containerPath:ro:extra'. Container path must be an absolute path "
445 - L"(starting with '/'). Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: "
446 - L"E_INVALIDARG"));
435 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_VolumeContainerPathNotAbsolute(
436 + L"C:\\hostPath:/containerPath:ro:extra", Localization::WSLCCLI_VolumeFormatUsage())));
437 EnsureContainerDoesNotExist(WslcContainerName);
438 }
439
@@ -451,9 +441,8 @@ class WSLCE2EContainerCreateTests
441 auto result = RunWslc(std::format(
442 L"container run --name {} --volume C:\\hostPath:/containerPath: {}", WslcContainerName, AlpineImage.NameAndTag()));
443 result.Verify({.Stdout = L"", .ExitCode = 1});
454 - VERIFY_IS_TRUE(result.StderrContainsSubstring(
455 - L"Invalid volume specifications: 'C:\\hostPath:/containerPath:'. Container path cannot be empty. Expected "
456 - L"format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
444 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_VolumeContainerPathEmpty(
445 + L"C:\\hostPath:/containerPath:", Localization::WSLCCLI_VolumeFormatUsage())));
446 EnsureContainerDoesNotExist(WslcContainerName);
447 }
448
@@ -462,9 +451,7 @@ class WSLCE2EContainerCreateTests
451 auto result = RunWslc(
452 std::format(L"container run --name {} --volume \"::/container:ro\" {}", WslcContainerName, AlpineImage.NameAndTag()));
453 result.Verify({.Stdout = L"", .ExitCode = 1});
465 - VERIFY_IS_TRUE(
466 - result.StderrContainsSubstring(L"Invalid volume specifications: '::/container:ro'. Host path ':' is not a valid "
467 - L"Windows path.\r\nError code: E_INVALIDARG"));
454 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_VolumeHostPathInvalid(L"::/container:ro", L":")));
455 EnsureContainerDoesNotExist(WslcContainerName);
456 }
457 }
@@ -479,8 +466,7 @@ class WSLCE2EContainerCreateTests
466 std::format(L"container run --name {} --volume \"C:\\hostPath\" {}", WslcContainerName, AlpineImage.NameAndTag()));
467 result.Verify({.Stdout = L"", .ExitCode = 1});
468 VERIFY_IS_TRUE(result.StderrContainsSubstring(
482 - L"Invalid volume specifications: 'C:\\hostPath'. Container path must be an absolute path (starting with '/'). "
483 - L"Expected format: <host path | named volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
469 + Localization::WSLCCLI_VolumeContainerPathNotAbsolute(L"C:\\hostPath", Localization::WSLCCLI_VolumeFormatUsage())));
470 EnsureContainerDoesNotExist(WslcContainerName);
471 }
472
@@ -488,8 +474,7 @@ class WSLCE2EContainerCreateTests
474 auto result = RunWslc(std::format(L"container run --name {} --volume \":\" {}", WslcContainerName, AlpineImage.NameAndTag()));
475 result.Verify({.Stdout = L"", .ExitCode = 1});
476 VERIFY_IS_TRUE(result.StderrContainsSubstring(
491 - L"Invalid volume specifications: ':'. Container path cannot be empty. Expected format: <host path | named "
492 - L"volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
477 + Localization::WSLCCLI_VolumeContainerPathEmpty(L":", Localization::WSLCCLI_VolumeFormatUsage())));
478 EnsureContainerDoesNotExist(WslcContainerName);
479 }
480
@@ -499,8 +484,7 @@ class WSLCE2EContainerCreateTests
484 RunWslc(std::format(L"container run --name {} --volume \"::\" {}", WslcContainerName, AlpineImage.NameAndTag()));
485 result.Verify({.Stdout = L"", .ExitCode = 1});
486 VERIFY_IS_TRUE(result.StderrContainsSubstring(
502 - L"Invalid volume specifications: '::'. Container path cannot be empty. Expected format: <host path | named "
503 - L"volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
487 + Localization::WSLCCLI_VolumeContainerPathEmpty(L"::", Localization::WSLCCLI_VolumeFormatUsage())));
488 EnsureContainerDoesNotExist(WslcContainerName);
489 }
490
@@ -508,9 +492,8 @@ class WSLCE2EContainerCreateTests
492 auto result =
493 RunWslc(std::format(L"container run --name {} --volume \"e2e_test\" {}", WslcContainerName, AlpineImage.NameAndTag()));
494 result.Verify({.Stdout = L"", .ExitCode = 1});
511 - VERIFY_IS_TRUE(
512 - result.StderrContainsSubstring(L"Invalid volume specifications: 'e2e_test'. Expected format: <host path | named "
513 - L"volume>:<container path>[:mode]\r\nError code: E_INVALIDARG"));
495 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
496 + Localization::WSLCCLI_VolumeInvalidSpec(L"e2e_test", Localization::WSLCCLI_VolumeFormatUsage())));
497 EnsureContainerDoesNotExist(WslcContainerName);
498 }
499 }
@@ -699,7 +682,7 @@ class WSLCE2EContainerCreateTests
682 RunWslc(std::format(L"container create --name {} --tmpfs wslc-tmpfs {}", WslcContainerName, DebianImage.NameAndTag()));
683 result.Verify({.Stdout = L"", .ExitCode = 1});
684 VERIFY_IS_TRUE(result.StderrContainsSubstring(
702 - L"invalid mount path: 'wslc-tmpfs' mount path must be absolute\r\nError code: E_FAIL"));
685 + Localization::WSLCCLI_InvalidTmpfsError(L"wslc-tmpfs", Localization::WSLCCLI_MountTargetAbsoluteError())));
686 }
687
688 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Tmpfs_EmptyDestination_Fails)
@@ -707,8 +690,243 @@ class WSLCE2EContainerCreateTests
690 auto result =
691 RunWslc(std::format(L"container create --name {} --tmpfs :size=64k {}", WslcContainerName, DebianImage.NameAndTag()));
692 result.Verify({.Stdout = L"", .ExitCode = 1});
710 - VERIFY_IS_TRUE(
711 - result.StderrContainsSubstring(L"invalid mount path: '' mount path must be absolute\r\nError code: E_FAIL"));
693 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
694 + Localization::WSLCCLI_InvalidTmpfsError(L":size=64k", Localization::WSLCCLI_MountTargetRequiredError())));
695 + }
696 +
697 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_Tmpfs_Success)
698 + {
699 + auto result = RunWslc(std::format(
700 + L"container create --name {} --mount type=tmpfs,target=/path:tmpfs,tmpfs-size=1MB,tmpfs-mode=0700 {} sh -c "
701 + L"\"echo -n 'tmpfs_test' > /path:tmpfs/data && cat /path:tmpfs/data && echo && stat -c '%a' /path:tmpfs && "
702 + L"df -k /path:tmpfs | awk 'NR == 2 {{print $2}}'\"",
703 + WslcContainerName,
704 + DebianImage.NameAndTag()));
705 + result.Verify({.Stderr = L"", .ExitCode = 0});
706 +
707 + const auto inspect = InspectContainer(WslcContainerName);
708 + VERIFY_ARE_EQUAL(1u, inspect.Mounts.size());
709 + VERIFY_ARE_EQUAL("tmpfs", inspect.Mounts[0].Type);
710 + VERIFY_ARE_EQUAL("", inspect.Mounts[0].Source);
711 + VERIFY_ARE_EQUAL("/path:tmpfs", inspect.Mounts[0].Destination);
712 + VERIFY_IS_TRUE(inspect.Mounts[0].ReadWrite);
713 +
714 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
715 + result.Verify({.Stdout = L"tmpfs_test\n700\n1024\n", .Stderr = L"", .ExitCode = 0});
716 + }
717 +
718 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_Tmpfs_PreservesMountForm)
719 + {
720 + auto result = RunWslc(std::format(
721 + L"container create --name {} --tmpfs /legacy-tmpfs --mount type=tmpfs,target=/modern-tmpfs,readonly {} true",
722 + WslcContainerName,
723 + DebianImage.NameAndTag()));
724 + result.Verify({.Stderr = L"", .ExitCode = 0});
725 +
726 + const auto inspect = InspectContainer(WslcContainerName);
727 + VERIFY_ARE_EQUAL(2u, inspect.Mounts.size());
728 +
729 + const auto legacyMount =
730 + std::ranges::find_if(inspect.Mounts, [](const auto& mount) { return mount.Destination == "/legacy-tmpfs"; });
731 + VERIFY_IS_TRUE(legacyMount != inspect.Mounts.end());
732 + VERIFY_ARE_EQUAL("tmpfs", legacyMount->Type);
733 + VERIFY_ARE_EQUAL("", legacyMount->Source);
734 + VERIFY_IS_TRUE(legacyMount->ReadWrite);
735 +
736 + const auto modernMount =
737 + std::ranges::find_if(inspect.Mounts, [](const auto& mount) { return mount.Destination == "/modern-tmpfs"; });
738 + VERIFY_IS_TRUE(modernMount != inspect.Mounts.end());
739 + VERIFY_ARE_EQUAL("tmpfs", modernMount->Type);
740 + VERIFY_ARE_EQUAL("", modernMount->Source);
741 + VERIFY_IS_FALSE(modernMount->ReadWrite);
742 + }
743 +
744 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_Bind_Success)
745 + {
746 + WriteTestFileContent(VolumeTestFile1, "WSLC Mount Bind Test");
747 +
748 + const auto hostDirectory = VolumeTestFile1.parent_path();
749 + const auto fileName = VolumeTestFile1.filename().wstring();
750 + auto result = RunWslc(std::format(
751 + L"container create --name {} --mount \"type=bind,source={},target=/path:mntdir,readonly\" {} cat /path:mntdir/{}",
752 + WslcContainerName,
753 + hostDirectory.wstring(),
754 + DebianImage.NameAndTag(),
755 + fileName));
756 + result.Verify({.Stderr = L"", .ExitCode = 0});
757 +
758 + const auto inspect = InspectContainer(WslcContainerName);
759 + VERIFY_ARE_EQUAL(1u, inspect.Mounts.size());
760 + VERIFY_ARE_EQUAL("bind", inspect.Mounts[0].Type);
761 + VERIFY_ARE_EQUAL(std::filesystem::canonical(hostDirectory).string(), inspect.Mounts[0].Source);
762 + VERIFY_ARE_EQUAL("/path:mntdir", inspect.Mounts[0].Destination);
763 + VERIFY_IS_FALSE(inspect.Mounts[0].ReadWrite);
764 +
765 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
766 + result.Verify({.Stdout = L"WSLC Mount Bind Test", .Stderr = L"", .ExitCode = 0});
767 + }
768 +
769 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_Bind_MissingSource_Fails)
770 + {
771 + const auto source = VolumeTestFile1;
772 + VERIFY_IS_TRUE(DeleteFileW(source.c_str()));
773 + auto cleanupSource = wil::scope_exit([&]() {
774 + std::error_code error;
775 + std::filesystem::remove_all(source, error);
776 + });
777 +
778 + auto result = RunWslc(std::format(
779 + L"container run --name {} --mount \"type=bind,source={},target=/data\" {} true",
780 + WslcContainerName,
781 + source.wstring(),
782 + AlpineImage.NameAndTag()));
783 + result.Verify({.Stdout = L"", .Stderr = FormatWslcError(Localization::MessageWslcBindSourcePathNotFound(source.wstring())), .ExitCode = 1});
784 + VERIFY_IS_FALSE(std::filesystem::exists(source));
785 + EnsureContainerDoesNotExist(WslcContainerName);
786 + }
787 +
788 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_MissingSource_CreatesDirectory)
789 + {
790 + const auto source = VolumeTestFile1;
791 + VERIFY_IS_TRUE(DeleteFileW(source.c_str()));
792 + auto cleanupSource = wil::scope_exit([&]() {
793 + std::error_code error;
794 + std::filesystem::remove_all(source, error);
795 + });
796 +
797 + auto result = RunWslc(std::format(
798 + L"container run --name {} --volume \"{}:/data\" {} true", WslcContainerName, source.wstring(), AlpineImage.NameAndTag()));
799 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
800 + VERIFY_IS_TRUE(std::filesystem::is_directory(source));
801 + EnsureContainerDoesNotExist(WslcContainerName);
802 + }
803 +
804 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_Volume_Success)
805 + {
806 + auto result = RunWslc(std::format(
807 + L"container create --name {} --mount type=volume,source={},target=/path:voldir {} sh -c \"echo -n 'WSLC Mount Volume "
808 + L"Test' > /path:voldir/test.txt\"",
809 + WslcContainerName,
810 + WslcVolumeName,
811 + DebianImage.NameAndTag()));
812 + result.Verify({.Stderr = L"", .ExitCode = 0});
813 +
814 + const auto inspect = InspectContainer(WslcContainerName);
815 + VERIFY_ARE_EQUAL(1u, inspect.Mounts.size());
816 + VERIFY_ARE_EQUAL("volume", inspect.Mounts[0].Type);
817 + VERIFY_ARE_EQUAL(string::WideToMultiByte(WslcVolumeName), inspect.Mounts[0].Source);
818 + VERIFY_ARE_EQUAL("/path:voldir", inspect.Mounts[0].Destination);
819 + VERIFY_IS_TRUE(inspect.Mounts[0].ReadWrite);
820 +
821 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
822 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
823 + EnsureContainerDoesNotExist(WslcContainerName);
824 +
825 + result = RunWslc(std::format(
826 + L"container create --name {} --mount type=volume,source={},target=/path:voldir {} cat /path:voldir/test.txt",
827 + WslcContainerName,
828 + WslcVolumeName,
829 + DebianImage.NameAndTag()));
830 + result.Verify({.Stderr = L"", .ExitCode = 0});
831 +
832 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
833 + result.Verify({.Stdout = L"WSLC Mount Volume Test", .Stderr = L"", .ExitCode = 0});
834 + EnsureContainerDoesNotExist(WslcContainerName);
835 +
836 + result = RunWslc(std::format(
837 + L"container create --rm --name {} --mount type=volume,target=/anonymous {} sh -c "
838 + L"\"echo -n anonymous-volume > /anonymous/value && cat /anonymous/value\"",
839 + WslcContainerName,
840 + DebianImage.NameAndTag()));
841 + result.Verify({.Stderr = L"", .ExitCode = 0});
842 +
843 + const auto anonymousInspect = InspectContainer(WslcContainerName);
844 + VERIFY_ARE_EQUAL(1u, anonymousInspect.Mounts.size());
845 + VERIFY_ARE_EQUAL("volume", anonymousInspect.Mounts[0].Type);
846 + VERIFY_IS_FALSE(anonymousInspect.Mounts[0].Name.empty());
847 + VERIFY_IS_TRUE(anonymousInspect.Mounts[0].Source.empty());
848 + VERIFY_ARE_EQUAL("/anonymous", anonymousInspect.Mounts[0].Destination);
849 + VERIFY_IS_TRUE(anonymousInspect.Mounts[0].ReadWrite);
850 +
851 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
852 + result.Verify({.Stdout = L"anonymous-volume", .Stderr = L"", .ExitCode = 0});
853 + EnsureContainerDoesNotExist(WslcContainerName);
854 + }
855 +
856 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_ReadOnly_IsReadOnly)
857 + {
858 + auto result = RunWslc(std::format(
859 + L"container create --name {} --mount type=volume,source={},target=/data {} sh -c \"echo -n original > /data/value\"",
860 + WslcContainerName,
861 + WslcVolumeName,
862 + DebianImage.NameAndTag()));
863 + result.Verify({.Stderr = L"", .ExitCode = 0});
864 +
865 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
866 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
867 + EnsureContainerDoesNotExist(WslcContainerName);
868 +
869 + result = RunWslc(std::format(
870 + L"container create --name {} --mount type=volume,source={},target=/data,readonly {} sh -c \"echo changed > "
871 + L"/data/value\"",
872 + WslcContainerName,
873 + WslcVolumeName,
874 + DebianImage.NameAndTag()));
875 + result.Verify({.Stderr = L"", .ExitCode = 0});
876 +
877 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
878 + result.Verify({.Stdout = L"", .Stderr = L"sh: 1: cannot create /data/value: Read-only file system\n", .ExitCode = 2});
879 + EnsureContainerDoesNotExist(WslcContainerName);
880 +
881 + result = RunWslc(std::format(
882 + L"container create --name {} --mount type=volume,source={},target=/data {} cat /data/value",
883 + WslcContainerName,
884 + WslcVolumeName,
885 + DebianImage.NameAndTag()));
886 + result.Verify({.Stderr = L"", .ExitCode = 0});
887 +
888 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
889 + result.Verify({.Stdout = L"original", .Stderr = L"", .ExitCode = 0});
890 + }
891 +
892 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_InvalidType_Fails)
893 + {
894 + constexpr auto mount = L"type=bogus,target=/x";
895 + auto result =
896 + RunWslc(std::format(L"container create --name {} --mount {} {} true", WslcContainerName, mount, DebianImage.NameAndTag()));
897 + result.Verify({.Stdout = L"", .ExitCode = 1});
898 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
899 + Localization::WSLCCLI_UnsupportedMountError(mount, Localization::WSLCCLI_MountTypeUnsupportedError(L"bogus"))));
900 + EnsureContainerDoesNotExist(WslcContainerName);
901 + }
902 +
903 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_RelativeTarget_Fails)
904 + {
905 + constexpr auto mount = L"type=tmpfs,target=data";
906 + auto result =
907 + RunWslc(std::format(L"container create --name {} --mount {} {} true", WslcContainerName, mount, DebianImage.NameAndTag()));
908 + result.Verify({.Stdout = L"", .ExitCode = 1});
909 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
910 + Localization::WSLCCLI_InvalidMountError(mount, Localization::WSLCCLI_MountTargetAbsoluteError())));
911 + EnsureContainerDoesNotExist(WslcContainerName);
912 + }
913 +
914 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_DuplicateDestination_Fails)
915 + {
916 + constexpr std::wstring_view duplicateMountArguments[] = {
917 + L"--mount type=tmpfs,target=/data --mount type=tmpfs,target=/data/",
918 + L"--tmpfs /data --volume data-volume:/data/",
919 + L"--tmpfs /data --mount type=volume,source=data-volume,target=/data/",
920 + L"--volume data-volume:/data --mount type=tmpfs,target=/data/",
921 + };
922 +
923 + for (const auto arguments : duplicateMountArguments)
924 + {
925 + const auto result =
926 + RunWslc(std::format(L"container create --name {} {} {} true", WslcContainerName, arguments, DebianImage.NameAndTag()));
927 + result.Verify({.Stdout = L"", .Stderr = FormatWslcError(Localization::WSLCCLI_DuplicateMountDestinationError(L"/data")), .ExitCode = 1});
928 + EnsureContainerDoesNotExist(WslcContainerName);
929 + }
930 }
931
932 WSLC_TEST_METHOD(WSLCE2E_Container_Create_WorkDir)
@@ -1571,6 +1789,9 @@ private:
1789 // Test network name
1790 const std::wstring TestNetworkName = L"wslc-test-network";
1791
1792 + // Test named volume
1793 + const std::wstring WslcVolumeName = L"wslc-test-volume";
1794 +
1795 // Test environment variables
1796 const std::wstring HostEnvVariableName = L"WSLC_TEST_HOST_ENV";
1797 const std::wstring HostEnvVariableName2 = L"WSLC_TEST_HOST_ENV2";
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+79 -3
@@ -17,6 +17,7 @@ Abstract:
17 #include "WSLCE2EHelpers.h"
18
19 namespace WSLCE2ETests {
20 +using namespace wsl::shared;
21
22 class WSLCE2EContainerRunTests
23 {
@@ -847,15 +848,15 @@ class WSLCE2EContainerRunTests
848 auto result = RunWslc(std::format(L"container run --rm --tmpfs wslc-tmpfs {}", DebianImage.NameAndTag()));
849 result.Verify({.Stdout = L"", .ExitCode = 1});
850 VERIFY_IS_TRUE(result.StderrContainsSubstring(
850 - L"invalid mount path: 'wslc-tmpfs' mount path must be absolute\r\nError code: E_FAIL"));
851 + Localization::WSLCCLI_InvalidTmpfsError(L"wslc-tmpfs", Localization::WSLCCLI_MountTargetAbsoluteError())));
852 }
853
854 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Tmpfs_EmptyDestination_Fails)
855 {
856 auto result = RunWslc(std::format(L"container run --rm --tmpfs :size=64k {}", DebianImage.NameAndTag()));
857 result.Verify({.Stdout = L"", .ExitCode = 1});
857 - VERIFY_IS_TRUE(
858 - result.StderrContainsSubstring(L"invalid mount path: '' mount path must be absolute\r\nError code: E_FAIL"));
858 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
859 + Localization::WSLCCLI_InvalidTmpfsError(L":size=64k", Localization::WSLCCLI_MountTargetRequiredError())));
860 }
861
862 WSLC_TEST_METHOD(WSLCE2E_Container_Run_WorkDir)
@@ -1071,6 +1072,81 @@ class WSLCE2EContainerRunTests
1072 result.Verify({.Stderr = L"", .ExitCode = 0});
1073 }
1074
1075 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Mount_Tmpfs_Success)
1076 + {
1077 + auto result = RunWslc(std::format(
1078 + L"container run --rm --mount type=tmpfs,target=/wslc-tmpfs {} sh -c \"echo -n 'tmpfs_test' > /wslc-tmpfs/data && cat "
1079 + L"/wslc-tmpfs/data\"",
1080 + DebianImage.NameAndTag()));
1081 + result.Verify({.Stdout = L"tmpfs_test", .Stderr = L"", .ExitCode = 0});
1082 + }
1083 +
1084 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Mount_Bind_Success)
1085 + {
1086 + WriteTestFileContent(EnvTestFile1, "WSLC Mount Bind Test");
1087 +
1088 + const auto hostDirectory = EnvTestFile1.parent_path();
1089 + const auto fileName = EnvTestFile1.filename().wstring();
1090 + auto result = RunWslc(std::format(
1091 + L"container run --rm --mount \"type=bind,source={},target=/data,readonly\" {} cat /data/{}",
1092 + hostDirectory.wstring(),
1093 + DebianImage.NameAndTag(),
1094 + fileName));
1095 + result.Verify({.Stdout = L"WSLC Mount Bind Test", .Stderr = L"", .ExitCode = 0});
1096 + }
1097 +
1098 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Mount_Volume_Success)
1099 + {
1100 + auto result = RunWslc(std::format(
1101 + L"container run --rm --mount type=volume,source={},target=/data {} sh -c \"echo -n 'WSLC Mount Volume Test' > "
1102 + L"/data/test.txt\"",
1103 + WslcVolumeName,
1104 + DebianImage.NameAndTag()));
1105 + result.Verify({.Stderr = L"", .ExitCode = 0});
1106 +
1107 + result = RunWslc(std::format(
1108 + L"container run --rm --mount type=volume,source={},target=/data {} cat /data/test.txt", WslcVolumeName, DebianImage.NameAndTag()));
1109 + result.Verify({.Stdout = L"WSLC Mount Volume Test", .Stderr = L"", .ExitCode = 0});
1110 + }
1111 +
1112 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Mount_ReadOnly_IsReadOnly)
1113 + {
1114 + auto result = RunWslc(std::format(
1115 + L"container run --rm --mount type=volume,source={},target=/data {} sh -c \"echo -n original > /data/value\"",
1116 + WslcVolumeName,
1117 + DebianImage.NameAndTag()));
1118 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
1119 +
1120 + result = RunWslc(std::format(
1121 + L"container run --rm --mount type=volume,source={},target=/data,readonly {} sh -c \"echo changed > /data/value\"",
1122 + WslcVolumeName,
1123 + DebianImage.NameAndTag()));
1124 + result.Verify({.Stdout = L"", .Stderr = L"sh: 1: cannot create /data/value: Read-only file system\n", .ExitCode = 2});
1125 +
1126 + result = RunWslc(std::format(
1127 + L"container run --rm --mount type=volume,source={},target=/data {} cat /data/value", WslcVolumeName, DebianImage.NameAndTag()));
1128 + result.Verify({.Stdout = L"original", .Stderr = L"", .ExitCode = 0});
1129 + }
1130 +
1131 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Mount_InvalidType_Fails)
1132 + {
1133 + constexpr auto mount = L"type=bogus,target=/x";
1134 + auto result = RunWslc(std::format(L"container run --rm --mount {} {} true", mount, DebianImage.NameAndTag()));
1135 + result.Verify({.Stdout = L"", .ExitCode = 1});
1136 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
1137 + Localization::WSLCCLI_UnsupportedMountError(mount, Localization::WSLCCLI_MountTypeUnsupportedError(L"bogus"))));
1138 + }
1139 +
1140 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Mount_DuplicateDestination_Fails)
1141 + {
1142 + auto result = RunWslc(std::format(
1143 + L"container run --rm --name {} --mount type=tmpfs,target=/data --mount type=tmpfs,target=/data/ {} true",
1144 + WslcContainerName,
1145 + DebianImage.NameAndTag()));
1146 + result.Verify({.Stdout = L"", .Stderr = FormatWslcError(Localization::WSLCCLI_DuplicateMountDestinationError(L"/data")), .ExitCode = 1});
1147 + EnsureContainerDoesNotExist(WslcContainerName);
1148 + }
1149 +
1150 WSLC_TEST_METHOD(WSLCE2E_Container_Run_WithLabel_Success)
1151 {
1152 auto result = RunWslc(std::format(
test/windows/wslc/e2e/WSLCE2EHelpers.h
+5
@@ -23,6 +23,11 @@ Abstract:
23
24 namespace WSLCE2ETests {
25
26 +inline std::wstring FormatWslcError(const std::wstring& message, std::wstring_view errorCode = L"E_INVALIDARG")
27 +{
28 + return std::format(L"{}\r\nError code: {}\r\n", message, errorCode);
29 +}
30 +
31 // VT sequence constants and helpers for TTY testing.
32 // Sequences are sourced from wsl::windows::common::vt (VTSupport.h).
33 namespace VT {