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 {