master
cpp 1,918 lines 89.1 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCE2EContainerCreateTests.cpp
8
9 Abstract:
10
11 This file contains end-to-end tests for WSLC.
12 --*/
13
14 #include "precomp.h"
15 #include "windows/Common.h"
16 #include "WSLCExecutor.h"
17 #include "WSLCE2EHelpers.h"
18 #include "TestImageRegistry.h"
19 #include <fstream>
20 #include <wil/network.h>
21 #include <wil/resource.h>
22
23 namespace WSLCE2ETests {
24 using namespace wsl::shared;
25
26 using namespace WEX::Logging;
27
28 class WSLCE2EContainerCreateTests
29 {
30 WSLC_TEST_CLASS(WSLCE2EContainerCreateTests)
31
32 TEST_CLASS_SETUP(ClassSetup)
33 {
34 TestImageRegistry::Instance().EnsureLoaded(AlpineImage);
35 TestImageRegistry::Instance().EnsureLoaded(DebianImage);
36 TestImageRegistry::Instance().EnsureLoaded(HelloWorldImage);
37
38 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), HostEnvVariableValue.c_str()));
39 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName2.c_str(), HostEnvVariableValue2.c_str()));
40 VERIFY_IS_TRUE(::SetEnvironmentVariableW(MissingHostEnvVariableName.c_str(), nullptr));
41 return true;
42 }
43
44 TEST_CLASS_CLEANUP(ClassCleanup)
45 {
46 EnsureContainerDoesNotExist(WslcContainerName);
47 EnsureVolumeDoesNotExist(WslcVolumeName);
48 EnsureNetworkDoesNotExist(TestNetworkName);
49
50 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), nullptr));
51 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName2.c_str(), nullptr));
52 VERIFY_IS_TRUE(::SetEnvironmentVariableW(MissingHostEnvVariableName.c_str(), nullptr));
53 return true;
54 }
55
56 TEST_METHOD_SETUP(TestMethodSetup)
57 {
58 EnvTestFile1 = wsl::windows::common::filesystem::GetTempFilename();
59 EnvTestFile2 = wsl::windows::common::filesystem::GetTempFilename();
60 VolumeTestFile1 = wsl::windows::common::filesystem::GetTempFilename();
61 VolumeTestFile2 = wsl::windows::common::filesystem::GetTempFilename();
62 EnsureContainerDoesNotExist(WslcContainerName);
63 EnsureVolumeDoesNotExist(WslcVolumeName);
64 EnsureNetworkDoesNotExist(TestNetworkName);
65 return true;
66 }
67
68 TEST_METHOD_CLEANUP(TestMethodCleanup)
69 {
70 DeleteFileW(EnvTestFile1.c_str());
71 DeleteFileW(EnvTestFile2.c_str());
72 DeleteFileW(VolumeTestFile1.c_str());
73 DeleteFileW(VolumeTestFile2.c_str());
74 return true;
75 }
76
77 WSLC_TEST_METHOD(WSLCE2E_Container_Create_HelpCommand)
78 {
79 auto result = RunWslc(L"container create --help");
80 result.Verify({.Stderr = L"", .ExitCode = 0});
81 VERIFY_IS_FALSE(result.Stdout.value().empty());
82 }
83
84 WSLC_TEST_METHOD(WSLCE2E_Container_Create_MissingImage)
85 {
86 auto result = RunWslc(L"container create --name " + WslcContainerName);
87 result.Verify({.Stdout = L"", .ExitCode = 1});
88 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'image'"));
89 }
90
91 WSLC_TEST_METHOD(WSLCE2E_Container_Create_InvalidImage)
92 {
93 auto session = OpenDefaultElevatedSession();
94
95 auto [registryContainer, registryAddress] = StartLocalRegistry(*session, "", "", 5000);
96 auto reference = std::format(L"{}/invalid-image:latest", registryAddress);
97
98 auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, reference));
99
100 const auto expectedError = std::format(L"Image '{}' not found, pulling\r\n", reference) +
101 FormatErrorMessage(
102 std::format(L"manifest for {} not found: manifest unknown: manifest unknown", reference),
103 L"WSLC_E_IMAGE_NOT_FOUND");
104 result.Verify({.Stderr = expectedError, .ExitCode = 1});
105 }
106
107 WSLC_TEST_METHOD(WSLCE2E_Container_Create_PullPolicy)
108 {
109 auto session = OpenDefaultElevatedSession();
110 auto [registryContainer, registryAddress] = StartLocalRegistry(*session);
111 auto registryImage = TagImageForRegistry(HelloWorldImage.NameAndTag(), string::MultiByteToWide(registryAddress));
112 auto cleanup = wil::scope_exit([&]() {
113 EnsureContainerDoesNotExist(WslcContainerName);
114 RunWslc(std::format(L"image delete --force {}", registryImage));
115 });
116
117 auto result = RunWslc(std::format(L"container create --pull=never --name {} {}", WslcContainerName, registryImage));
118 result.Verify({.Stderr = L"", .ExitCode = 0});
119 VerifyContainerIsListed(WslcContainerName, L"created");
120 EnsureContainerDoesNotExist(WslcContainerName);
121
122 result = RunWslc(std::format(L"container create --pull=always --name {} {}", WslcContainerName, registryImage));
123 const auto errorMessage = FormatErrorMessage(
124 std::format(L"manifest for {} not found: manifest unknown: manifest unknown", registryImage),
125 L"WSLC_E_IMAGE_NOT_FOUND");
126 result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1});
127 VerifyContainerIsNotListed(WslcContainerName);
128
129 RunWslcAndVerify(std::format(L"push {}", registryImage), {.Stderr = L"", .ExitCode = 0});
130 RunWslcAndVerify(std::format(L"image delete --force {}", registryImage), {.ExitCode = 0});
131
132 result = RunWslc(std::format(L"container create --pull=missing --name {} {}", WslcContainerName, registryImage));
133 result.Verify({.ExitCode = 0});
134 VerifyContainerIsListed(WslcContainerName, L"created");
135 EnsureContainerDoesNotExist(WslcContainerName);
136 }
137
138 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Valid)
139 {
140 VerifyContainerIsNotListed(WslcContainerName);
141
142 // Create the container with a valid image
143 auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
144 result.Verify({.Stderr = L"", .ExitCode = 0});
145 std::wstring containerId = result.GetStdoutOneLine();
146
147 // Verify the container is listed with the correct status
148 VerifyContainerIsListed(containerId, L"created");
149 }
150
151 WSLC_TEST_METHOD(WSLCE2E_Container_Create_CIDFile_Valid)
152 {
153 // Prepare a CID file path that does not exist
154 const auto cidFilePath = wsl::windows::common::filesystem::GetTempFilename();
155 VERIFY_IS_TRUE(DeleteFileW(cidFilePath.c_str()));
156 auto deleteCidFile = wil::scope_exit([&]() { VERIFY_IS_TRUE(DeleteFileW(cidFilePath.c_str())); });
157
158 auto result = RunWslc(std::format(
159 L"container create --cidfile \"{}\" --name {} {}", EscapePath(cidFilePath.wstring()), WslcContainerName, DebianImage.NameAndTag()));
160 result.Verify({.Stderr = L"", .ExitCode = 0});
161
162 const auto containerId = result.GetStdoutOneLine();
163 VERIFY_IS_TRUE(std::filesystem::exists(cidFilePath));
164 VERIFY_ARE_EQUAL(containerId, ReadFileContent(cidFilePath.wstring()));
165 }
166
167 WSLC_TEST_METHOD(WSLCE2E_Container_Create_CIDFile_AlreadyExists)
168 {
169 const auto cidFilePath = wsl::windows::common::filesystem::GetTempFilename();
170 auto deleteCidFile = wil::scope_exit([&]() { VERIFY_IS_TRUE(DeleteFileW(cidFilePath.c_str())); });
171
172 auto result = RunWslc(std::format(
173 L"container create --cidfile \"{}\" --name {} {}", EscapePath(cidFilePath.wstring()), WslcContainerName, DebianImage.NameAndTag()));
174 result.Verify(
175 {.Stderr = FormatErrorMessage(
176 std::format(L"CID file '{}' already exists", EscapePath(cidFilePath.wstring())), L"ERROR_FILE_EXISTS"),
177 .ExitCode = 1});
178
179 VerifyContainerIsNotListed(WslcContainerName);
180 }
181
182 WSLC_TEST_METHOD(WSLCE2E_Container_Create_DuplicateContainerName)
183 {
184 VerifyContainerIsNotListed(WslcContainerName);
185
186 // Create the container with a valid image
187 auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
188 result.Verify({.Stderr = L"", .ExitCode = 0});
189 auto containerId = result.GetStdoutOneLine();
190
191 // Attempt to create another container with the same name
192 result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
193 result.Verify(
194 {.Stderr = FormatErrorMessage(
195 std::format(
196 L"Conflict. The container name \"/{}\" is already in use by container \"{}\". You have to remove "
197 L"(or rename) that container to be able to reuse that name.",
198 WslcContainerName,
199 containerId),
200 L"ERROR_ALREADY_EXISTS"),
201 .ExitCode = 1});
202 }
203
204 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_WriteFromHostReadFromContainer)
205 {
206 // Write to a temp file that we will mount as a volume to the container
207 const std::wstring tempFile = VolumeTestFile1.wstring();
208 std::wofstream out(tempFile);
209 out << L"WSLC Volume Test";
210 out.close();
211
212 auto hostDirectory = VolumeTestFile1.parent_path();
213 auto fileName = VolumeTestFile1.filename().wstring();
214
215 auto result = RunWslc(std::format(
216 L"container run --name {} --volume \"{}:/data:ro\" {} cat /data/{}",
217 WslcContainerName,
218 hostDirectory.wstring(),
219 AlpineImage.NameAndTag(),
220 fileName));
221 result.Verify({.Stdout = L"WSLC Volume Test", .Stderr = L"", .ExitCode = 0});
222 }
223
224 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_WriteFromContainerReadFromHost_ReadWritePermissionByDefault)
225 {
226 auto hostDirectory = VolumeTestFile1.parent_path();
227 auto fileName = VolumeTestFile1.filename().wstring();
228 auto result = RunWslc(std::format(
229 L"container run --name {} --volume \"{}:/data\" {} sh -c \"echo -n 'WSLC Volume Test' > /data/{}\"",
230 WslcContainerName,
231 hostDirectory.wstring(),
232 AlpineImage.NameAndTag(),
233 fileName));
234 result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
235
236 // Read all file content
237 auto content = ReadFileContent(VolumeTestFile1.wstring());
238 VERIFY_ARE_EQUAL(L"WSLC Volume Test", content);
239 }
240
241 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_WriteFromContainerReadFromHost_ReadWritePermission)
242 {
243 auto hostDirectory = VolumeTestFile1.parent_path();
244 auto fileName = VolumeTestFile1.filename().wstring();
245 auto result = RunWslc(std::format(
246 L"container run --name {} --volume \"{}:/data:rw\" {} sh -c \"echo -n 'WSLC Volume Test' > /data/{}\"",
247 WslcContainerName,
248 hostDirectory.wstring(),
249 AlpineImage.NameAndTag(),
250 fileName));
251 result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
252
253 // Read all file content
254 std::wifstream in(VolumeTestFile1);
255 std::wstringstream buffer;
256 buffer << in.rdbuf();
257 VERIFY_ARE_EQUAL(L"WSLC Volume Test", buffer.str());
258 }
259
260 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_WriteFromContainerReadFromHost_ReadOnlyPermission_Fail)
261 {
262 auto hostDirectory = VolumeTestFile1.parent_path();
263 auto fileName = VolumeTestFile1.filename().wstring();
264 auto result = RunWslc(std::format(
265 L"container run --name {} --volume \"{}:/data:ro\" {} sh -c \"echo -n 'WSLC Volume Test' > /data/{}\"",
266 WslcContainerName,
267 hostDirectory.wstring(),
268 AlpineImage.NameAndTag(),
269 fileName));
270 auto errorMessage = std::format(L"sh: can't create /data/{}: Read-only file system\n", fileName);
271 result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1});
272 }
273
274 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_Multiple_WriteFromContainerReadFromHost_ReadWritePermission)
275 {
276 // Mount multiple volumes to the container
277 auto hostDirectory1 = VolumeTestFile1.parent_path();
278 auto fileName1 = VolumeTestFile1.filename().wstring();
279 auto hostDirectory2 = VolumeTestFile2.parent_path();
280 auto fileName2 = VolumeTestFile2.filename().wstring();
281 auto result = RunWslc(std::format(
282 L"container run --name {} --volume \"{}:/data1:rw\" --volume \"{}:/data2:rw\" {} sh -c \"echo -n 'Test1' > "
283 L"/data1/{} && "
284 L"echo -n 'Test2' > /data2/{}\"",
285 WslcContainerName,
286 hostDirectory1.wstring(),
287 hostDirectory2.wstring(),
288 AlpineImage.NameAndTag(),
289 fileName1,
290 fileName2));
291
292 result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
293
294 // Read all file content for both files
295 std::wifstream in1(VolumeTestFile1);
296 std::wstringstream buffer1;
297 buffer1 << in1.rdbuf();
298 VERIFY_ARE_EQUAL(L"Test1", buffer1.str());
299
300 std::wifstream in2(VolumeTestFile2);
301 std::wstringstream buffer2;
302 buffer2 << in2.rdbuf();
303 VERIFY_ARE_EQUAL(L"Test2", buffer2.str());
304 }
305
306 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_RelativeHostPath)
307 {
308 // Create a uniquely-named subdirectory relative to the CWD and pass it as a relative path
309 // to wslc. Using a GUID suffix avoids collisions on parallel runs or after a prior crash.
310 // Uses "./" prefix to disambiguate from a Docker named volume name.
311 GUID runId;
312 THROW_IF_FAILED(CoCreateGuid(&runId));
313 const auto dirName =
314 L"wslc-vol-relpath-" + wsl::shared::string::GuidToString<wchar_t>(runId, wsl::shared::string::GuidToStringFlags::None);
315 const auto absoluteDir = std::filesystem::current_path() / dirName;
316 std::filesystem::create_directories(absoluteDir);
317 auto cleanupDir = wil::scope_exit([&]() { std::filesystem::remove_all(absoluteDir); });
318
319 const auto testFile = absoluteDir / L"reltest.txt";
320 const auto relativeDir = L"./" + dirName;
321
322 // Write a file from the host and verify the container can read it via the relative path mount.
323 {
324 std::ofstream out(testFile);
325 VERIFY_IS_TRUE(out.is_open(), L"Failed to open test file for writing (host -> container test)");
326 out << "WSLC Relative Path Test";
327 VERIFY_IS_TRUE(out.good(), L"Failed to write to test file (host -> container test)");
328 }
329
330 auto result = RunWslc(std::format(
331 L"container run --rm --name {} --volume \"{}:/data:ro\" {} cat /data/reltest.txt",
332 WslcContainerName,
333 relativeDir,
334 AlpineImage.NameAndTag()));
335 result.Verify({.Stdout = L"WSLC Relative Path Test", .Stderr = L"", .ExitCode = 0});
336
337 EnsureContainerDoesNotExist(WslcContainerName);
338
339 // Write a file from the container and verify the host can read it back via the relative path mount.
340 result = RunWslc(std::format(
341 L"container run --rm --name {} --volume \"{}:/data:rw\" {} sh -c \"echo -n 'WSLC Relative Path Write Test' > "
342 L"/data/reltest.txt\"",
343 WslcContainerName,
344 relativeDir,
345 AlpineImage.NameAndTag()));
346 result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
347
348 std::ifstream in(testFile);
349 VERIFY_IS_TRUE(in.is_open(), L"Failed to open test file for reading (container -> host test)");
350 std::stringstream buffer;
351 buffer << in.rdbuf();
352 VERIFY_IS_TRUE(in.good() || in.eof(), L"Failed to read test file (container -> host test)");
353 VERIFY_ARE_EQUAL("WSLC Relative Path Write Test", buffer.str());
354 }
355
356 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_Invalid)
357 {
358 {
359 auto result =
360 RunWslc(std::format(L"container run --name {} --volume :/containerPath {}", WslcContainerName, AlpineImage.NameAndTag()));
361 result.Verify({.Stdout = L"", .ExitCode = 1});
362 VERIFY_IS_TRUE(result.StderrContainsSubstring(
363 Localization::WSLCCLI_VolumeHostPathEmpty(L":/containerPath", Localization::WSLCCLI_VolumeFormatUsage())));
364 EnsureContainerDoesNotExist(WslcContainerName);
365 }
366
367 {
368 auto result = RunWslc(
369 std::format(L"container run --name {} --volume C:\\hostPath::ro {}", WslcContainerName, AlpineImage.NameAndTag()));
370 result.Verify({.Stdout = L"", .ExitCode = 1});
371 VERIFY_IS_TRUE(result.StderrContainsSubstring(
372 Localization::WSLCCLI_VolumeContainerPathEmpty(L"C:\\hostPath::ro", Localization::WSLCCLI_VolumeFormatUsage())));
373 EnsureContainerDoesNotExist(WslcContainerName);
374 }
375
376 {
377 auto result = RunWslc(
378 std::format(L"container run --name {} --volume :/containerPath:ro {}", WslcContainerName, AlpineImage.NameAndTag()));
379 result.Verify({.Stdout = L"", .ExitCode = 1});
380 VERIFY_IS_TRUE(result.StderrContainsSubstring(
381 Localization::WSLCCLI_VolumeHostPathEmpty(L":/containerPath:ro", Localization::WSLCCLI_VolumeFormatUsage())));
382 EnsureContainerDoesNotExist(WslcContainerName);
383 }
384
385 {
386 auto result = RunWslc(std::format(L"container run --name {} --volume \"\" {}", WslcContainerName, AlpineImage.NameAndTag()));
387 result.Verify({.Stdout = L"", .ExitCode = 1});
388 VERIFY_IS_TRUE(result.StderrContainsSubstring(
389 Localization::WSLCCLI_VolumeInvalidSpec(L"", Localization::WSLCCLI_VolumeFormatUsage())));
390 EnsureContainerDoesNotExist(WslcContainerName);
391 }
392
393 {
394 auto result =
395 RunWslc(std::format(L"container run --name {} --volume C:\\hostPath: {}", WslcContainerName, AlpineImage.NameAndTag()));
396 result.Verify({.Stdout = L"", .ExitCode = 1});
397 VERIFY_IS_TRUE(result.StderrContainsSubstring(
398 Localization::WSLCCLI_VolumeContainerPathEmpty(L"C:\\hostPath:", Localization::WSLCCLI_VolumeFormatUsage())));
399 EnsureContainerDoesNotExist(WslcContainerName);
400 }
401
402 {
403 auto result =
404 RunWslc(std::format(L"container run --name {} --volume C:\\hostPath:ro {}", WslcContainerName, AlpineImage.NameAndTag()));
405 result.Verify({.Stdout = L"", .ExitCode = 1});
406 VERIFY_IS_TRUE(result.StderrContainsSubstring(
407 Localization::WSLCCLI_VolumeContainerPathNotAbsolute(L"C:\\hostPath:ro", Localization::WSLCCLI_VolumeFormatUsage())));
408 EnsureContainerDoesNotExist(WslcContainerName);
409 }
410
411 {
412 auto result = RunWslc(std::format(L"container run --name {} --volume :ro {}", WslcContainerName, AlpineImage.NameAndTag()));
413 result.Verify({.Stdout = L"", .ExitCode = 1});
414 VERIFY_IS_TRUE(result.StderrContainsSubstring(
415 Localization::WSLCCLI_VolumeInvalidSpec(L":ro", Localization::WSLCCLI_VolumeFormatUsage())));
416 EnsureContainerDoesNotExist(WslcContainerName);
417 }
418
419 {
420 auto result = RunWslc(
421 std::format(L"container run --name {} --volume C:\\hostPath::rw {}", WslcContainerName, AlpineImage.NameAndTag()));
422 result.Verify({.Stdout = L"", .ExitCode = 1});
423 VERIFY_IS_TRUE(result.StderrContainsSubstring(
424 Localization::WSLCCLI_VolumeContainerPathEmpty(L"C:\\hostPath::rw", Localization::WSLCCLI_VolumeFormatUsage())));
425 EnsureContainerDoesNotExist(WslcContainerName);
426 }
427
428 {
429 auto result = RunWslc(std::format(
430 L"container run --name {} --volume C:\\hostPath:/containerPath:invalid_mode {}", WslcContainerName, AlpineImage.NameAndTag()));
431 result.Verify({.Stdout = L"", .ExitCode = 1});
432 VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_VolumeContainerPathNotAbsolute(
433 L"C:\\hostPath:/containerPath:invalid_mode", Localization::WSLCCLI_VolumeFormatUsage())));
434 EnsureContainerDoesNotExist(WslcContainerName);
435 }
436
437 {
438 auto result = RunWslc(std::format(
439 L"container run --name {} --volume C:\\hostPath:/containerPath:ro:extra {}", WslcContainerName, AlpineImage.NameAndTag()));
440 result.Verify({.Stdout = L"", .ExitCode = 1});
441 VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_VolumeContainerPathNotAbsolute(
442 L"C:\\hostPath:/containerPath:ro:extra", Localization::WSLCCLI_VolumeFormatUsage())));
443 EnsureContainerDoesNotExist(WslcContainerName);
444 }
445
446 {
447 auto result = RunWslc(std::format(
448 L"container run --name {} --volume C:\\hostPath:/containerPath: {}", WslcContainerName, AlpineImage.NameAndTag()));
449 result.Verify({.Stdout = L"", .ExitCode = 1});
450 VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_VolumeContainerPathEmpty(
451 L"C:\\hostPath:/containerPath:", Localization::WSLCCLI_VolumeFormatUsage())));
452 EnsureContainerDoesNotExist(WslcContainerName);
453 }
454
455 {
456 // "::/container:ro" - host=":", container="/container". ":" is not a valid Windows path.
457 auto result = RunWslc(
458 std::format(L"container run --name {} --volume \"::/container:ro\" {}", WslcContainerName, AlpineImage.NameAndTag()));
459 result.Verify({.Stdout = L"", .ExitCode = 1});
460 VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_VolumeHostPathInvalid(L"::/container:ro", L":")));
461 EnsureContainerDoesNotExist(WslcContainerName);
462 }
463 }
464
465 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_NotSupported)
466 {
467 // Commands tested in this method are not currently supported in WSLC,
468 // so we just verify that they fail with the expected error message.
469 // https://github.com/microsoft/WSL/issues/14432
470 {
471 auto result = RunWslc(
472 std::format(L"container run --name {} --volume \"C:\\hostPath\" {}", WslcContainerName, AlpineImage.NameAndTag()));
473 result.Verify({.Stdout = L"", .ExitCode = 1});
474 VERIFY_IS_TRUE(result.StderrContainsSubstring(
475 Localization::WSLCCLI_VolumeContainerPathNotAbsolute(L"C:\\hostPath", Localization::WSLCCLI_VolumeFormatUsage())));
476 EnsureContainerDoesNotExist(WslcContainerName);
477 }
478
479 {
480 auto result = RunWslc(std::format(L"container run --name {} --volume \":\" {}", WslcContainerName, AlpineImage.NameAndTag()));
481 result.Verify({.Stdout = L"", .ExitCode = 1});
482 VERIFY_IS_TRUE(result.StderrContainsSubstring(
483 Localization::WSLCCLI_VolumeContainerPathEmpty(L":", Localization::WSLCCLI_VolumeFormatUsage())));
484 EnsureContainerDoesNotExist(WslcContainerName);
485 }
486
487 {
488 // "::" splits as host=":", container="". Container path empty check fires first.
489 auto result =
490 RunWslc(std::format(L"container run --name {} --volume \"::\" {}", WslcContainerName, AlpineImage.NameAndTag()));
491 result.Verify({.Stdout = L"", .ExitCode = 1});
492 VERIFY_IS_TRUE(result.StderrContainsSubstring(
493 Localization::WSLCCLI_VolumeContainerPathEmpty(L"::", Localization::WSLCCLI_VolumeFormatUsage())));
494 EnsureContainerDoesNotExist(WslcContainerName);
495 }
496
497 {
498 auto result =
499 RunWslc(std::format(L"container run --name {} --volume \"e2e_test\" {}", WslcContainerName, AlpineImage.NameAndTag()));
500 result.Verify({.Stdout = L"", .ExitCode = 1});
501 VERIFY_IS_TRUE(result.StderrContainsSubstring(
502 Localization::WSLCCLI_VolumeInvalidSpec(L"e2e_test", Localization::WSLCCLI_VolumeFormatUsage())));
503 EnsureContainerDoesNotExist(WslcContainerName);
504 }
505 }
506
507 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Remove)
508 {
509 VerifyContainerIsNotListed(WslcContainerName);
510
511 auto result = RunWslc(std::format(L"container create --rm --name {} {} echo hello", WslcContainerName, DebianImage.NameAndTag()));
512 result.Verify({.Stderr = L"", .ExitCode = 0});
513
514 // Start the container.
515 result = RunWslc(std::format(L"container start {}", WslcContainerName));
516 result.Verify({.Stdout = std::format(L"{}\r\n", WslcContainerName), .Stderr = L"", .ExitCode = 0});
517
518 // Verify with retry timeout of 1 minute.
519 VerifyContainerIsNotListed(WslcContainerName, std::chrono::seconds(2), std::chrono::minutes(1));
520 }
521
522 WSLC_TEST_METHOD(WSLCE2E_Container_Start_AlreadyRunning)
523 {
524 auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
525 result.Verify({.Stderr = L"", .ExitCode = 0});
526
527 auto containerId = result.GetStdoutOneLine();
528 VERIFY_IS_FALSE(containerId.empty());
529
530 VerifyContainerIsListed(containerId, L"running");
531
532 // Start again - should succeed without error
533 result = RunWslc(std::format(L"container start {}", WslcContainerName));
534 result.Verify({.Stdout = std::format(L"{}\r\n", WslcContainerName), .Stderr = L"", .ExitCode = 0});
535
536 // Verify the container is still running
537 VerifyContainerIsListed(containerId, L"running");
538 }
539
540 WSLC_TEST_METHOD(WSLCE2E_Container_CreateStartAttach_TTY)
541 {
542 VerifyContainerIsNotListed(WslcContainerName);
543
544 const auto& prompt = ">";
545 auto result = RunWslc(std::format(
546 L"container create -it -e PS1={} --name {} {} bash --norc", prompt, WslcContainerName, DebianImage.NameAndTag()));
547 result.Verify({.Stderr = L"", .ExitCode = 0});
548 auto containerId = result.GetStdoutOneLine();
549
550 const auto& expectedPrompt = VT::BuildContainerPrompt(prompt, true);
551
552 auto session = RunWslcInteractive(std::format(L"container start --attach {}", containerId));
553 VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running");
554
555 // Ignore resize-repaint messages. Those are emitted when the the tty initial size is set, which can happen before or after we start running commands.
556 session.IgnoreSequence(VT::BuildContainerAttachPrompt(prompt));
557
558 session.ExpectStdout(expectedPrompt);
559
560 session.WriteLine("echo hello");
561 session.ExpectCommandEcho("echo hello");
562 session.ExpectStdout("hello\r\n");
563 session.ExpectStdout(expectedPrompt);
564
565 session.WriteLine("whoami");
566 session.ExpectCommandEcho("whoami");
567 session.ExpectStdout("root\r\n");
568 session.ExpectStdout(expectedPrompt);
569
570 session.ExitAndVerifyNoErrors();
571 auto exitCode = session.Wait();
572 VERIFY_ARE_EQUAL(0, exitCode);
573 }
574
575 WSLC_TEST_METHOD(WSLCE2E_Container_CreateStartAttach_NoTTY)
576 {
577 VerifyContainerIsNotListed(WslcContainerName);
578 auto result = RunWslc(std::format(L"container create -i --name {} {} cat", WslcContainerName, DebianImage.NameAndTag()));
579 result.Verify({.Stderr = L"", .ExitCode = 0});
580 auto containerId = result.GetStdoutOneLine();
581
582 // Start with attach
583 auto session = RunWslcInteractive(std::format(L"container start --attach {}", containerId));
584 VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running");
585
586 session.WriteLine("test line 1");
587 session.ExpectStdout("test line 1\n");
588 session.WriteLine("test line 2");
589 session.ExpectStdout("test line 2\n");
590
591 // Close stdin to signal EOF to cat
592 session.CloseStdin();
593
594 // Wait for cat to exit with code 0
595 auto exitCode = session.Wait(10000);
596 VERIFY_ARE_EQUAL(0, exitCode, L"Cat should exit with code 0 after receiving EOF");
597 session.VerifyNoErrors();
598 }
599
600 WSLC_TEST_METHOD(WSLCE2E_Container_CreateStartAttach_ShortRunningInitProcess)
601 {
602 VerifyContainerIsNotListed(WslcContainerName);
603
604 constexpr auto ExpectedExitCode = 37;
605
606 auto result = RunWslc(std::format(
607 L"container create --name {} {} sh -c \"echo lifecycle works; exit {}\"", WslcContainerName, AlpineImage.NameAndTag(), ExpectedExitCode));
608
609 result.Verify({.Stderr = L"", .ExitCode = 0});
610
611 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
612 result.Verify({.Stdout = L"lifecycle works\n", .Stderr = L"", .ExitCode = ExpectedExitCode});
613 }
614
615 WSLC_TEST_METHOD(WSLCE2E_Container_Create_UserOption_UidRoot)
616 {
617 auto result = RunWslc(
618 std::format(L"container create --name {} -u 0 {} sh -c \"id -u; id -g\"", WslcContainerName, DebianImage.NameAndTag()));
619 result.Verify({.Stderr = L"", .ExitCode = 0});
620
621 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
622 result.Verify({.Stdout = L"0\n0\n", .Stderr = L"", .ExitCode = 0});
623 }
624
625 WSLC_TEST_METHOD(WSLCE2E_Container_Create_UserOption_NameGroupRoot)
626 {
627 auto result = RunWslc(std::format(
628 L"container create --name {} -u root:root {} sh -c \"id -un; id -u; id -g\"", WslcContainerName, DebianImage.NameAndTag()));
629 result.Verify({.Stderr = L"", .ExitCode = 0});
630
631 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
632 result.Verify({.Stdout = L"root\n0\n0\n", .Stderr = L"", .ExitCode = 0});
633 }
634
635 WSLC_TEST_METHOD(WSLCE2E_Container_Create_UserOption_UnknownUser_Fails)
636 {
637 auto result = RunWslc(
638 std::format(L"container create --name {} -u user_does_not_exist {} id -u", WslcContainerName, DebianImage.NameAndTag()));
639 result.Verify({.Stderr = L"", .ExitCode = 0});
640
641 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
642 result.Verify(
643 {.Stderr =
644 FormatErrorMessage(L"unable to find user user_does_not_exist: no matching entries in passwd file", L"E_FAIL"),
645 .ExitCode = 1});
646 }
647
648 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Tmpfs)
649 {
650 auto result = RunWslc(std::format(
651 L"container create --name {} --tmpfs /wslc-tmpfs {} sh -c \"echo -n 'tmpfs_test' > /wslc-tmpfs/data && cat "
652 L"/wslc-tmpfs/data\"",
653 WslcContainerName,
654 DebianImage.NameAndTag()));
655 result.Verify({.Stderr = L"", .ExitCode = 0});
656
657 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
658 result.Verify({.Stdout = L"tmpfs_test", .Stderr = L"", .ExitCode = 0});
659 }
660
661 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Tmpfs_With_Options)
662 {
663 auto result = RunWslc(std::format(
664 L"container create --name {} --tmpfs /wslc-tmpfs:size=64k {} sh -c \"mount | grep -q ' on /wslc-tmpfs type tmpfs ' "
665 L"&& echo mounted\"",
666 WslcContainerName,
667 DebianImage.NameAndTag()));
668 result.Verify({.Stderr = L"", .ExitCode = 0});
669
670 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
671 result.Verify({.Stdout = L"mounted\n", .Stderr = L"", .ExitCode = 0});
672 }
673
674 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Tmpfs_Multiple_With_Options)
675 {
676 auto result = RunWslc(std::format(
677 L"container create --name {} --tmpfs /wslc-tmpfs1:size=64k --tmpfs /wslc-tmpfs2:size=128k {} sh -c \"mount | grep -q "
678 L"' on /wslc-tmpfs1 type tmpfs ' && mount | grep -q ' on /wslc-tmpfs2 type tmpfs ' && echo mounted\"",
679 WslcContainerName,
680 DebianImage.NameAndTag()));
681 result.Verify({.Stderr = L"", .ExitCode = 0});
682
683 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
684 result.Verify({.Stdout = L"mounted\n", .Stderr = L"", .ExitCode = 0});
685 }
686
687 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Tmpfs_RelativePath_Fails)
688 {
689 auto result =
690 RunWslc(std::format(L"container create --name {} --tmpfs wslc-tmpfs {}", WslcContainerName, DebianImage.NameAndTag()));
691 result.Verify({.Stdout = L"", .ExitCode = 1});
692 VERIFY_IS_TRUE(result.StderrContainsSubstring(
693 Localization::WSLCCLI_InvalidTmpfsError(L"wslc-tmpfs", Localization::WSLCCLI_MountTargetAbsoluteError())));
694 }
695
696 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Tmpfs_EmptyDestination_Fails)
697 {
698 auto result =
699 RunWslc(std::format(L"container create --name {} --tmpfs :size=64k {}", WslcContainerName, DebianImage.NameAndTag()));
700 result.Verify({.Stdout = L"", .ExitCode = 1});
701 VERIFY_IS_TRUE(result.StderrContainsSubstring(
702 Localization::WSLCCLI_InvalidTmpfsError(L":size=64k", Localization::WSLCCLI_MountTargetRequiredError())));
703 }
704
705 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_Tmpfs_Success)
706 {
707 auto result = RunWslc(std::format(
708 L"container create --name {} --mount type=tmpfs,target=/path:tmpfs,tmpfs-size=1MB,tmpfs-mode=0700 {} sh -c "
709 L"\"echo -n 'tmpfs_test' > /path:tmpfs/data && cat /path:tmpfs/data && echo && stat -c '%a' /path:tmpfs && "
710 L"df -k /path:tmpfs | awk 'NR == 2 {{print $2}}'\"",
711 WslcContainerName,
712 DebianImage.NameAndTag()));
713 result.Verify({.Stderr = L"", .ExitCode = 0});
714
715 const auto inspect = InspectContainer(WslcContainerName);
716 VERIFY_ARE_EQUAL(1u, inspect.Mounts.size());
717 VERIFY_ARE_EQUAL("tmpfs", inspect.Mounts[0].Type);
718 VERIFY_ARE_EQUAL("", inspect.Mounts[0].Source);
719 VERIFY_ARE_EQUAL("/path:tmpfs", inspect.Mounts[0].Destination);
720 VERIFY_IS_TRUE(inspect.Mounts[0].ReadWrite);
721
722 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
723 result.Verify({.Stdout = L"tmpfs_test\n700\n1024\n", .Stderr = L"", .ExitCode = 0});
724 }
725
726 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_Tmpfs_PreservesMountForm)
727 {
728 auto result = RunWslc(std::format(
729 L"container create --name {} --tmpfs /legacy-tmpfs --mount type=tmpfs,target=/modern-tmpfs,readonly {} true",
730 WslcContainerName,
731 DebianImage.NameAndTag()));
732 result.Verify({.Stderr = L"", .ExitCode = 0});
733
734 const auto inspect = InspectContainer(WslcContainerName);
735 VERIFY_ARE_EQUAL(2u, inspect.Mounts.size());
736
737 const auto legacyMount =
738 std::ranges::find_if(inspect.Mounts, [](const auto& mount) { return mount.Destination == "/legacy-tmpfs"; });
739 VERIFY_IS_TRUE(legacyMount != inspect.Mounts.end());
740 VERIFY_ARE_EQUAL("tmpfs", legacyMount->Type);
741 VERIFY_ARE_EQUAL("", legacyMount->Source);
742 VERIFY_IS_TRUE(legacyMount->ReadWrite);
743
744 const auto modernMount =
745 std::ranges::find_if(inspect.Mounts, [](const auto& mount) { return mount.Destination == "/modern-tmpfs"; });
746 VERIFY_IS_TRUE(modernMount != inspect.Mounts.end());
747 VERIFY_ARE_EQUAL("tmpfs", modernMount->Type);
748 VERIFY_ARE_EQUAL("", modernMount->Source);
749 VERIFY_IS_FALSE(modernMount->ReadWrite);
750 }
751
752 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_Bind_Success)
753 {
754 WriteTestFileContent(VolumeTestFile1, "WSLC Mount Bind Test");
755
756 const auto hostDirectory = VolumeTestFile1.parent_path();
757 const auto fileName = VolumeTestFile1.filename().wstring();
758 auto result = RunWslc(std::format(
759 L"container create --name {} --mount \"type=bind,source={},target=/path:mntdir,readonly\" {} cat /path:mntdir/{}",
760 WslcContainerName,
761 hostDirectory.wstring(),
762 DebianImage.NameAndTag(),
763 fileName));
764 result.Verify({.Stderr = L"", .ExitCode = 0});
765
766 const auto inspect = InspectContainer(WslcContainerName);
767 VERIFY_ARE_EQUAL(1u, inspect.Mounts.size());
768 VERIFY_ARE_EQUAL("bind", inspect.Mounts[0].Type);
769 VERIFY_ARE_EQUAL(std::filesystem::canonical(hostDirectory).string(), inspect.Mounts[0].Source);
770 VERIFY_ARE_EQUAL("/path:mntdir", inspect.Mounts[0].Destination);
771 VERIFY_IS_FALSE(inspect.Mounts[0].ReadWrite);
772
773 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
774 result.Verify({.Stdout = L"WSLC Mount Bind Test", .Stderr = L"", .ExitCode = 0});
775 }
776
777 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_Bind_MissingSource_Fails)
778 {
779 const auto source = VolumeTestFile1;
780 VERIFY_IS_TRUE(DeleteFileW(source.c_str()));
781 auto cleanupSource = wil::scope_exit([&]() {
782 std::error_code error;
783 std::filesystem::remove_all(source, error);
784 });
785
786 auto result = RunWslc(std::format(
787 L"container run --name {} --mount \"type=bind,source={},target=/data\" {} true",
788 WslcContainerName,
789 source.wstring(),
790 AlpineImage.NameAndTag()));
791 result.Verify(
792 {.Stdout = L"",
793 .Stderr = FormatErrorMessage(Localization::MessageWslcBindSourcePathNotFound(source.wstring()), L"E_INVALIDARG"),
794 .ExitCode = 1});
795 VERIFY_IS_FALSE(std::filesystem::exists(source));
796 EnsureContainerDoesNotExist(WslcContainerName);
797 }
798
799 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Volume_MissingSource_CreatesDirectory)
800 {
801 const auto source = VolumeTestFile1;
802 VERIFY_IS_TRUE(DeleteFileW(source.c_str()));
803 auto cleanupSource = wil::scope_exit([&]() {
804 std::error_code error;
805 std::filesystem::remove_all(source, error);
806 });
807
808 auto result = RunWslc(std::format(
809 L"container run --name {} --volume \"{}:/data\" {} true", WslcContainerName, source.wstring(), AlpineImage.NameAndTag()));
810 result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
811 VERIFY_IS_TRUE(std::filesystem::is_directory(source));
812 EnsureContainerDoesNotExist(WslcContainerName);
813 }
814
815 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_Volume_Success)
816 {
817 auto result = RunWslc(std::format(
818 L"container create --name {} --mount type=volume,source={},target=/path:voldir {} sh -c \"echo -n 'WSLC Mount Volume "
819 L"Test' > /path:voldir/test.txt\"",
820 WslcContainerName,
821 WslcVolumeName,
822 DebianImage.NameAndTag()));
823 result.Verify({.Stderr = L"", .ExitCode = 0});
824
825 const auto inspect = InspectContainer(WslcContainerName);
826 VERIFY_ARE_EQUAL(1u, inspect.Mounts.size());
827 VERIFY_ARE_EQUAL("volume", inspect.Mounts[0].Type);
828 VERIFY_ARE_EQUAL(string::WideToMultiByte(WslcVolumeName), inspect.Mounts[0].Source);
829 VERIFY_ARE_EQUAL("/path:voldir", inspect.Mounts[0].Destination);
830 VERIFY_IS_TRUE(inspect.Mounts[0].ReadWrite);
831
832 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
833 result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
834 EnsureContainerDoesNotExist(WslcContainerName);
835
836 result = RunWslc(std::format(
837 L"container create --name {} --mount type=volume,source={},target=/path:voldir {} cat /path:voldir/test.txt",
838 WslcContainerName,
839 WslcVolumeName,
840 DebianImage.NameAndTag()));
841 result.Verify({.Stderr = L"", .ExitCode = 0});
842
843 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
844 result.Verify({.Stdout = L"WSLC Mount Volume Test", .Stderr = L"", .ExitCode = 0});
845 EnsureContainerDoesNotExist(WslcContainerName);
846
847 result = RunWslc(std::format(
848 L"container create --rm --name {} --mount type=volume,target=/anonymous {} sh -c "
849 L"\"echo -n anonymous-volume > /anonymous/value && cat /anonymous/value\"",
850 WslcContainerName,
851 DebianImage.NameAndTag()));
852 result.Verify({.Stderr = L"", .ExitCode = 0});
853
854 const auto anonymousInspect = InspectContainer(WslcContainerName);
855 VERIFY_ARE_EQUAL(1u, anonymousInspect.Mounts.size());
856 VERIFY_ARE_EQUAL("volume", anonymousInspect.Mounts[0].Type);
857 VERIFY_IS_FALSE(anonymousInspect.Mounts[0].Name.empty());
858 VERIFY_IS_TRUE(anonymousInspect.Mounts[0].Source.empty());
859 VERIFY_ARE_EQUAL("/anonymous", anonymousInspect.Mounts[0].Destination);
860 VERIFY_IS_TRUE(anonymousInspect.Mounts[0].ReadWrite);
861
862 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
863 result.Verify({.Stdout = L"anonymous-volume", .Stderr = L"", .ExitCode = 0});
864 EnsureContainerDoesNotExist(WslcContainerName);
865 }
866
867 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_ReadOnly_IsReadOnly)
868 {
869 auto result = RunWslc(std::format(
870 L"container create --name {} --mount type=volume,source={},target=/data {} sh -c \"echo -n original > /data/value\"",
871 WslcContainerName,
872 WslcVolumeName,
873 DebianImage.NameAndTag()));
874 result.Verify({.Stderr = L"", .ExitCode = 0});
875
876 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
877 result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
878 EnsureContainerDoesNotExist(WslcContainerName);
879
880 result = RunWslc(std::format(
881 L"container create --name {} --mount type=volume,source={},target=/data,readonly {} sh -c \"echo changed > "
882 L"/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"", .Stderr = L"sh: 1: cannot create /data/value: Read-only file system\n", .ExitCode = 2});
890 EnsureContainerDoesNotExist(WslcContainerName);
891
892 result = RunWslc(std::format(
893 L"container create --name {} --mount type=volume,source={},target=/data {} cat /data/value",
894 WslcContainerName,
895 WslcVolumeName,
896 DebianImage.NameAndTag()));
897 result.Verify({.Stderr = L"", .ExitCode = 0});
898
899 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
900 result.Verify({.Stdout = L"original", .Stderr = L"", .ExitCode = 0});
901 }
902
903 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_InvalidType_Fails)
904 {
905 constexpr auto mount = L"type=bogus,target=/x";
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_UnsupportedMountError(mount, Localization::WSLCCLI_MountTypeUnsupportedError(L"bogus"))));
911 EnsureContainerDoesNotExist(WslcContainerName);
912 }
913
914 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_RelativeTarget_Fails)
915 {
916 constexpr auto mount = L"type=tmpfs,target=data";
917 auto result =
918 RunWslc(std::format(L"container create --name {} --mount {} {} true", WslcContainerName, mount, DebianImage.NameAndTag()));
919 result.Verify({.Stdout = L"", .ExitCode = 1});
920 VERIFY_IS_TRUE(result.StderrContainsSubstring(
921 Localization::WSLCCLI_InvalidMountError(mount, Localization::WSLCCLI_MountTargetAbsoluteError())));
922 EnsureContainerDoesNotExist(WslcContainerName);
923 }
924
925 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Mount_DuplicateDestination_Fails)
926 {
927 constexpr std::wstring_view duplicateMountArguments[] = {
928 L"--mount type=tmpfs,target=/data --mount type=tmpfs,target=/data/",
929 L"--tmpfs /data --volume data-volume:/data/",
930 L"--tmpfs /data --mount type=volume,source=data-volume,target=/data/",
931 L"--volume data-volume:/data --mount type=tmpfs,target=/data/",
932 };
933
934 for (const auto arguments : duplicateMountArguments)
935 {
936 const auto result =
937 RunWslc(std::format(L"container create --name {} {} {} true", WslcContainerName, arguments, DebianImage.NameAndTag()));
938 result.Verify(
939 {.Stdout = L"",
940 .Stderr = FormatErrorMessage(Localization::WSLCCLI_DuplicateMountDestinationError(L"/data"), L"E_INVALIDARG"),
941 .ExitCode = 1});
942 EnsureContainerDoesNotExist(WslcContainerName);
943 }
944 }
945
946 WSLC_TEST_METHOD(WSLCE2E_Container_Create_WorkDir)
947 {
948 auto result =
949 RunWslc(std::format(L"container create --name {} --workdir /tmp {} pwd", WslcContainerName, DebianImage.NameAndTag()));
950 result.Verify({.Stderr = L"", .ExitCode = 0});
951
952 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
953 result.Verify({.Stdout = L"/tmp\n", .Stderr = L"", .ExitCode = 0});
954 }
955
956 WSLC_TEST_METHOD(WSLCE2E_Container_Create_WithLabel_Success)
957 {
958 auto result =
959 RunWslc(std::format(L"container create --name {} --label A=1 --label B=2 {}", WslcContainerName, DebianImage.NameAndTag()));
960 result.Verify({.Stderr = L"", .ExitCode = 0});
961
962 auto inspect = InspectContainer(WslcContainerName);
963 VERIFY_ARE_EQUAL("1", inspect.Labels["A"]);
964 VERIFY_ARE_EQUAL("2", inspect.Labels["B"]);
965 }
966
967 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Hostname)
968 {
969 auto result = RunWslc(std::format(
970 L"container create --name {} --hostname my-test-host {} hostname", WslcContainerName, DebianImage.NameAndTag()));
971 result.Verify({.Stderr = L"", .ExitCode = 0});
972
973 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
974 result.Verify({.Stdout = L"my-test-host\n", .Stderr = L"", .ExitCode = 0});
975 }
976
977 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Domainname)
978 {
979 auto result = RunWslc(std::format(
980 L"container create --name {} --domainname my-test-domain {} dnsdomainname", WslcContainerName, DebianImage.NameAndTag()));
981 result.Verify({.Stderr = L"", .ExitCode = 0});
982
983 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
984 result.Verify({.Stdout = L"my-test-domain\n", .Stderr = L"", .ExitCode = 0});
985 }
986
987 WSLC_TEST_METHOD(WSLCE2E_Container_Create_DNS)
988 {
989 auto result = RunWslc(std::format(
990 L"container create --name {} --dns 1.1.1.1 --dns 8.8.8.8 {} cat /etc/resolv.conf", WslcContainerName, DebianImage.NameAndTag()));
991 result.Verify({.Stderr = L"", .ExitCode = 0});
992
993 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
994 result.Verify({.Stderr = L"", .ExitCode = 0});
995 VERIFY_IS_TRUE(result.Stdout->find(L"nameserver 1.1.1.1") != std::wstring::npos);
996 VERIFY_IS_TRUE(result.Stdout->find(L"nameserver 8.8.8.8") != std::wstring::npos);
997 }
998
999 WSLC_TEST_METHOD(WSLCE2E_Container_Create_DNSSearch)
1000 {
1001 auto result = RunWslc(std::format(
1002 L"container create --name {} --dns-search example.com --dns-search test.local {} cat /etc/resolv.conf",
1003 WslcContainerName,
1004 DebianImage.NameAndTag()));
1005 result.Verify({.Stderr = L"", .ExitCode = 0});
1006
1007 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
1008 result.Verify({.Stderr = L"", .ExitCode = 0});
1009 VERIFY_IS_TRUE(result.Stdout->find(L"search example.com test.local") != std::wstring::npos);
1010 }
1011
1012 WSLC_TEST_METHOD(WSLCE2E_Container_Create_DNSOption)
1013 {
1014 auto result = RunWslc(std::format(
1015 L"container create --name {} --dns-option ndots:5 --dns-option timeout:3 {} cat /etc/resolv.conf",
1016 WslcContainerName,
1017 DebianImage.NameAndTag()));
1018 result.Verify({.Stderr = L"", .ExitCode = 0});
1019
1020 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
1021 result.Verify({.Stderr = L"", .ExitCode = 0});
1022 VERIFY_IS_TRUE(result.Stdout->find(L"options ndots:5 timeout:3") != std::wstring::npos);
1023 }
1024
1025 WSLC_TEST_METHOD(WSLCE2E_Container_Create_StopSignal)
1026 {
1027 constexpr int ExpectedExitCode = 42;
1028 auto result = RunWslc(std::format(
1029 LR"(container create --stop-signal SIGUSR1 --name {} {} bash -c "trap 'exit {}' SIGUSR1; while true; do sleep 1; done")",
1030 WslcContainerName,
1031 DebianImage.NameAndTag(),
1032 ExpectedExitCode));
1033 result.Verify({.Stderr = L"", .ExitCode = 0});
1034 const auto containerId = result.GetStdoutOneLine();
1035 VerifyContainerIsListed(containerId, L"created");
1036
1037 result = RunWslc(std::format(L"container start {}", WslcContainerName));
1038 result.Verify({.Stderr = L"", .ExitCode = 0});
1039 VerifyContainerIsListed(containerId, L"running");
1040
1041 result = RunWslc(std::format(L"container stop {}", WslcContainerName));
1042 result.Verify({.Stderr = L"", .ExitCode = 0});
1043
1044 const auto inspect = InspectContainer(WslcContainerName);
1045 VERIFY_IS_FALSE(inspect.State.Running);
1046 VERIFY_ARE_EQUAL(ExpectedExitCode, inspect.State.ExitCode);
1047 }
1048
1049 WSLC_TEST_METHOD(WSLCE2E_Container_Create_StopTimeout)
1050 {
1051 // A positive value is forwarded to the container configuration.
1052 {
1053 constexpr int ExpectedStopTimeout = 30;
1054 auto result = RunWslc(std::format(
1055 L"container create --stop-timeout {} --name {} {}", ExpectedStopTimeout, WslcContainerName, DebianImage.NameAndTag()));
1056 result.Verify({.Stderr = L"", .ExitCode = 0});
1057
1058 const auto inspect = InspectContainer(WslcContainerName);
1059 VERIFY_IS_TRUE(inspect.Config.StopTimeout.has_value());
1060 VERIFY_ARE_EQUAL(ExpectedStopTimeout, inspect.Config.StopTimeout.value());
1061 EnsureContainerDoesNotExist(WslcContainerName);
1062 }
1063
1064 // A value of 0 (stop the container immediately) is a valid, explicit timeout.
1065 {
1066 auto result =
1067 RunWslc(std::format(L"container create --stop-timeout 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1068 result.Verify({.Stderr = L"", .ExitCode = 0});
1069
1070 const auto inspect = InspectContainer(WslcContainerName);
1071 VERIFY_IS_TRUE(inspect.Config.StopTimeout.has_value());
1072 VERIFY_ARE_EQUAL(0, inspect.Config.StopTimeout.value());
1073 EnsureContainerDoesNotExist(WslcContainerName);
1074 }
1075
1076 // A value of -1 means "no timeout"; it is a valid, explicit value forwarded to the configuration.
1077 {
1078 auto result =
1079 RunWslc(std::format(L"container create --stop-timeout -1 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1080 result.Verify({.Stderr = L"", .ExitCode = 0});
1081
1082 const auto inspect = InspectContainer(WslcContainerName);
1083 VERIFY_IS_TRUE(inspect.Config.StopTimeout.has_value());
1084 VERIFY_ARE_EQUAL(-1, inspect.Config.StopTimeout.value());
1085 EnsureContainerDoesNotExist(WslcContainerName);
1086 }
1087
1088 // When --stop-timeout is not specified, no timeout is forwarded to the container configuration.
1089 {
1090 auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1091 result.Verify({.Stderr = L"", .ExitCode = 0});
1092
1093 const auto inspect = InspectContainer(WslcContainerName);
1094 VERIFY_IS_FALSE(inspect.Config.StopTimeout.has_value());
1095 EnsureContainerDoesNotExist(WslcContainerName);
1096 }
1097 }
1098
1099 WSLC_TEST_METHOD(WSLCE2E_Container_Create_StopTimeout_Invalid)
1100 {
1101 {
1102 auto result =
1103 RunWslc(std::format(L"container create --stop-timeout abc --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1104 result.Verify({.Stdout = L"", .ExitCode = 1});
1105 VERIFY_IS_TRUE(result.StderrContainsSubstring(
1106 wsl::shared::Localization::WSLCCLI_InvalidIntegerArgumentError(L"stop-timeout", L"abc")));
1107 VerifyContainerIsNotListed(WslcContainerName);
1108 }
1109
1110 {
1111 auto result =
1112 RunWslc(std::format(L"container create --stop-timeout -2 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1113 result.Verify({.Stderr = FormatErrorMessage(L"Invalid stop timeout value: -2", L"E_INVALIDARG"), .ExitCode = 1});
1114 VerifyContainerIsNotListed(WslcContainerName);
1115 }
1116 }
1117
1118 WSLC_TEST_METHOD(WSLCE2E_Container_Create_ShmSize)
1119 {
1120 auto cleanup = wil::scope_exit([&] { EnsureContainerDoesNotExist(WslcContainerName); });
1121
1122 auto result = RunWslc(std::format(
1123 L"container create --shm-size 1.5G --name {} {} sh -c \"df -B1 /dev/shm --output=size | sed 1d\"",
1124 WslcContainerName,
1125 DebianImage.NameAndTag()));
1126 result.Verify({.Stderr = L"", .ExitCode = 0});
1127
1128 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
1129 result.Verify({.Stdout = L"1610612736\n", .Stderr = L"", .ExitCode = 0});
1130 }
1131
1132 WSLC_TEST_METHOD(WSLCE2E_Container_Create_ShmSize_Invalid)
1133 {
1134 {
1135 auto result =
1136 RunWslc(std::format(L"container create --shm-size invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1137 result.Verify({.Stdout = L"", .ExitCode = 1});
1138 VERIFY_IS_TRUE(result.StderrContainsSubstring(
1139 wsl::shared::Localization::WSLCCLI_InvalidMemorySizeError(L"shm-size", L"invalid")));
1140 VerifyContainerIsNotListed(WslcContainerName);
1141 }
1142
1143 {
1144 auto result =
1145 RunWslc(std::format(L"container create --shm-size 128X --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1146 result.Verify({.Stdout = L"", .ExitCode = 1});
1147 VERIFY_IS_TRUE(
1148 result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidMemorySizeError(L"shm-size", L"128X")));
1149 VerifyContainerIsNotListed(WslcContainerName);
1150 }
1151 }
1152
1153 WSLC_TEST_METHOD(WSLCE2E_Container_Create_HealthCheck)
1154 {
1155 // All health-check options are forwarded to the container configuration.
1156 {
1157 auto result = RunWslc(std::format(
1158 LR"(container create --health-cmd "exit 0" --health-interval 5s --health-timeout 3s --health-retries 2 --health-start-period 1s --name {} {})",
1159 WslcContainerName,
1160 DebianImage.NameAndTag()));
1161 result.Verify({.Stderr = L"", .ExitCode = 0});
1162
1163 const auto inspect = InspectContainer(WslcContainerName);
1164 VERIFY_IS_TRUE(inspect.Config.Healthcheck.has_value());
1165
1166 const auto& health = inspect.Config.Healthcheck.value();
1167 VERIFY_IS_TRUE(health.Test.has_value());
1168 const std::vector<std::string> expectedTest{"CMD-SHELL", "exit 0"};
1169 VERIFY_ARE_EQUAL(expectedTest, health.Test.value());
1170
1171 // Durations are reported in nanoseconds.
1172 VERIFY_IS_TRUE(health.Interval.has_value());
1173 VERIFY_ARE_EQUAL(5'000'000'000LL, health.Interval.value());
1174 VERIFY_IS_TRUE(health.Timeout.has_value());
1175 VERIFY_ARE_EQUAL(3'000'000'000LL, health.Timeout.value());
1176 VERIFY_IS_TRUE(health.StartPeriod.has_value());
1177 VERIFY_ARE_EQUAL(1'000'000'000LL, health.StartPeriod.value());
1178 VERIFY_IS_TRUE(health.Retries.has_value());
1179 VERIFY_ARE_EQUAL(2, health.Retries.value());
1180
1181 EnsureContainerDoesNotExist(WslcContainerName);
1182 }
1183
1184 // Only --health-cmd: the command is forwarded, other fields fall back to the default.
1185 {
1186 auto result = RunWslc(
1187 std::format(LR"(container create --health-cmd "exit 1" --name {} {})", WslcContainerName, DebianImage.NameAndTag()));
1188 result.Verify({.Stderr = L"", .ExitCode = 0});
1189
1190 const auto inspect = InspectContainer(WslcContainerName);
1191 VERIFY_IS_TRUE(inspect.Config.Healthcheck.has_value());
1192
1193 const auto& health = inspect.Config.Healthcheck.value();
1194 VERIFY_IS_TRUE(health.Test.has_value());
1195 const std::vector<std::string> expectedTest{"CMD-SHELL", "exit 1"};
1196 VERIFY_ARE_EQUAL(expectedTest, health.Test.value());
1197
1198 EnsureContainerDoesNotExist(WslcContainerName);
1199 }
1200
1201 // When no health option is specified, no health check is forwarded.
1202 {
1203 auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1204 result.Verify({.Stderr = L"", .ExitCode = 0});
1205
1206 const auto inspect = InspectContainer(WslcContainerName);
1207 VERIFY_IS_FALSE(inspect.Config.Healthcheck.has_value());
1208 EnsureContainerDoesNotExist(WslcContainerName);
1209 }
1210 }
1211
1212 WSLC_TEST_METHOD(WSLCE2E_Container_Create_HealthCheck_Invalid)
1213 {
1214 {
1215 auto result = RunWslc(std::format(
1216 L"container create --health-interval notaduration --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1217 result.Verify({.Stdout = L"", .ExitCode = 1});
1218 VERIFY_IS_TRUE(result.StderrContainsSubstring(
1219 wsl::shared::Localization::WSLCCLI_InvalidDurationError(L"health-interval", L"notaduration")));
1220 VerifyContainerIsNotListed(WslcContainerName);
1221 }
1222
1223 {
1224 auto result =
1225 RunWslc(std::format(L"container create --health-retries abc --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1226 result.Verify({.Stdout = L"", .ExitCode = 1});
1227 VERIFY_IS_TRUE(result.StderrContainsSubstring(
1228 wsl::shared::Localization::WSLCCLI_InvalidIntegerArgumentError(L"health-retries", L"abc")));
1229 VerifyContainerIsNotListed(WslcContainerName);
1230 }
1231
1232 {
1233 auto result = RunWslc(std::format(
1234 LR"(container create --no-healthcheck --health-cmd "exit 0" --name {} {})", WslcContainerName, DebianImage.NameAndTag()));
1235 result.Verify({.Stdout = L"", .ExitCode = 1});
1236 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"cannot be combined with other health check options"));
1237 VerifyContainerIsNotListed(WslcContainerName);
1238 }
1239
1240 {
1241 auto result = RunWslc(std::format(
1242 L"container create --no-healthcheck --health-interval 5s --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1243 result.Verify({.Stdout = L"", .ExitCode = 1});
1244 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"cannot be combined with other health check options"));
1245 VerifyContainerIsNotListed(WslcContainerName);
1246 }
1247 }
1248
1249 WSLC_TEST_METHOD(WSLCE2E_Container_Create_StopSignal_Invalid)
1250 {
1251 {
1252 auto result = RunWslc(
1253 std::format(L"container create --stop-signal SIGINVALID --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1254 result.Verify({.Stdout = L"", .ExitCode = 1});
1255 VERIFY_IS_TRUE(
1256 result.StderrContainsSubstring(L"Invalid stop-signal value: SIGINVALID is not a recognized signal name or number "
1257 L"(Example: SIGKILL, kill, or 9)."));
1258 VerifyContainerIsNotListed(WslcContainerName);
1259 }
1260
1261 {
1262 auto result =
1263 RunWslc(std::format(L"container create --stop-signal 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1264 result.Verify({.Stdout = L"", .ExitCode = 1});
1265 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid stop-signal value: 0 is out of valid range (1-31)."));
1266 VerifyContainerIsNotListed(WslcContainerName);
1267 }
1268
1269 {
1270 auto result =
1271 RunWslc(std::format(L"container create --stop-signal 99 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1272 result.Verify({.Stdout = L"", .ExitCode = 1});
1273 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid stop-signal value: 99 is out of valid range (1-31)."));
1274 VerifyContainerIsNotListed(WslcContainerName);
1275 }
1276 }
1277
1278 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_DefaultIsBridge)
1279 {
1280 auto result = RunWslc(std::format(L"container create --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
1281 result.Verify({.Stderr = L"", .ExitCode = 0});
1282
1283 const auto inspect = InspectContainer(WslcContainerName);
1284 VERIFY_ARE_EQUAL(std::string("bridge"), inspect.HostConfig.NetworkMode);
1285 }
1286
1287 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_HostMode_Rejected)
1288 {
1289 auto result =
1290 RunWslc(std::format(L"container create --name {} --network host {} true", WslcContainerName, DebianImage.NameAndTag()));
1291 result.Verify({.Stdout = L"", .Stderr = wsl::shared::Localization::WSLCCLI_NetworkHostModeNotSupportedError() + L"\r\n", .ExitCode = 1});
1292 VerifyContainerIsNotListed(WslcContainerName);
1293 }
1294
1295 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_HostMode_WithMultipleNetworks_Rejected)
1296 {
1297 auto result = RunWslc(std::format(
1298 L"container create --name {} --network bridge --network host {} true", WslcContainerName, DebianImage.NameAndTag()));
1299 result.Verify({.Stdout = L"", .Stderr = wsl::shared::Localization::WSLCCLI_NetworkHostModeNotSupportedError() + L"\r\n", .ExitCode = 1});
1300 VerifyContainerIsNotListed(WslcContainerName);
1301 }
1302
1303 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_UserDefinedNetwork)
1304 {
1305 auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
1306 result.Verify({.Stderr = L"", .ExitCode = 0});
1307 auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
1308
1309 result = RunWslc(std::format(
1310 L"container create --name {} --network {} {} true", WslcContainerName, TestNetworkName, DebianImage.NameAndTag()));
1311 result.Verify({.Stderr = L"", .ExitCode = 0});
1312
1313 const auto inspect = InspectContainer(WslcContainerName);
1314 VERIFY_ARE_EQUAL(wsl::shared::string::WideToMultiByte(TestNetworkName), inspect.HostConfig.NetworkMode);
1315 }
1316
1317 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_EmptyValue_Rejected)
1318 {
1319 auto result = RunWslc(std::format(L"container create --network \"\" --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1320 result.Verify({.Stdout = L"", .ExitCode = 1});
1321 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid network value: network name cannot be empty or whitespace"));
1322 VerifyContainerIsNotListed(WslcContainerName);
1323 }
1324
1325 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_NonexistentNetwork_Rejected)
1326 {
1327 auto result = RunWslc(
1328 std::format(L"container create --network does-not-exist --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
1329 result.Verify({.Stderr = FormatErrorMessage(L"Network not found: 'does-not-exist'", L"WSLC_E_NETWORK_NOT_FOUND"), .ExitCode = 1});
1330 VerifyContainerIsNotListed(WslcContainerName);
1331 }
1332
1333 WSLC_TEST_METHOD(WSLCE2E_Container_Create_NetworkAlias_Success)
1334 {
1335 auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
1336 result.Verify({.Stderr = L"", .ExitCode = 0});
1337 auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
1338
1339 result = RunWslc(std::format(
1340 L"container create --name {} --network {} --network-alias db {} true", WslcContainerName, TestNetworkName, DebianImage.NameAndTag()));
1341 result.Verify({.Stderr = L"", .ExitCode = 0});
1342
1343 const auto inspect = InspectContainer(WslcContainerName);
1344 const auto networkName = wsl::shared::string::WideToMultiByte(TestNetworkName);
1345 VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
1346 const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
1347 VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "db") != endpoint.Aliases.end());
1348 }
1349
1350 WSLC_TEST_METHOD(WSLCE2E_Container_Create_NetworkAlias_DockerStyleMultiNetwork_Success)
1351 {
1352 const auto secondNetworkName = TestNetworkName + L"-2";
1353 EnsureNetworkDoesNotExist(secondNetworkName);
1354
1355 auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
1356 result.Verify({.Stderr = L"", .ExitCode = 0});
1357 auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
1358
1359 result = RunWslc(std::format(L"network create --driver bridge {}", secondNetworkName));
1360 result.Verify({.Stderr = L"", .ExitCode = 0});
1361 auto cleanupSecondNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(secondNetworkName); });
1362
1363 result = RunWslc(std::format(
1364 L"container create --name {} --network name={},alias=db,alias=primary --network name={},alias=cache {} true",
1365 WslcContainerName,
1366 TestNetworkName,
1367 secondNetworkName,
1368 DebianImage.NameAndTag()));
1369 result.Verify({.Stderr = L"", .ExitCode = 0});
1370
1371 const auto inspect = InspectContainer(WslcContainerName);
1372 const auto networkName = wsl::shared::string::WideToMultiByte(TestNetworkName);
1373 const auto secondNetwork = wsl::shared::string::WideToMultiByte(secondNetworkName);
1374 VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
1375 VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(secondNetwork));
1376 const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
1377 const auto& secondEndpoint = inspect.NetworkSettings.Networks.at(secondNetwork);
1378 VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "db") != endpoint.Aliases.end());
1379 VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "primary") != endpoint.Aliases.end());
1380 VERIFY_IS_TRUE(std::ranges::find(secondEndpoint.Aliases, "cache") != secondEndpoint.Aliases.end());
1381 }
1382
1383 WSLC_TEST_METHOD(WSLCE2E_Container_Create_NetworkAlias_NoNetwork_Rejected)
1384 {
1385 auto result =
1386 RunWslc(std::format(L"container create --network-alias db --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
1387 result.Verify(
1388 {.Stderr = FormatErrorMessage(
1389 L"Network aliases require a user-defined network. Use --network to specify one.", L"E_INVALIDARG"),
1390 .ExitCode = 1});
1391 VerifyContainerIsNotListed(WslcContainerName);
1392 }
1393
1394 WSLC_TEST_METHOD(WSLCE2E_Container_Create_NetworkAlias_NoneMode_Rejected)
1395 {
1396 auto result = RunWslc(std::format(
1397 L"container create --network none --network-alias db --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
1398 result.Verify(
1399 {.Stderr = FormatErrorMessage(
1400 L"Network aliases require a user-defined network. Use --network to specify one.", L"E_INVALIDARG"),
1401 .ExitCode = 1});
1402 VerifyContainerIsNotListed(WslcContainerName);
1403 }
1404
1405 WSLC_TEST_METHOD(WSLCE2E_Container_Create_NetworkAlias_MultipleNetworks_Rejected)
1406 {
1407 auto result = RunWslc(std::format(
1408 L"container create --network bridge --network bridge --network-alias db --name {} {} true",
1409 WslcContainerName,
1410 DebianImage.NameAndTag()));
1411 result.Verify({.Stdout = L"", .ExitCode = 1});
1412 VERIFY_IS_TRUE(result.StderrContainsSubstring(
1413 wsl::shared::Localization::MessageWslcAliasAmbiguousWithMultipleNetworks() + L"\r\nError code: E_INVALIDARG"));
1414 VerifyContainerIsNotListed(WslcContainerName);
1415 }
1416
1417 WSLC_TEST_METHOD(WSLCE2E_Container_Create_NetworkAlias_EmptyValue_Rejected)
1418 {
1419 auto result =
1420 RunWslc(std::format(L"container create --network-alias \"\" --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
1421 result.Verify({.Stdout = L"", .ExitCode = 1});
1422 VERIFY_IS_TRUE(
1423 result.StderrContainsSubstring(L"Invalid network-alias value: network alias cannot be empty or whitespace"));
1424 VerifyContainerIsNotListed(WslcContainerName);
1425 }
1426
1427 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ip_Success)
1428 {
1429 const std::wstring subnet = L"172.74.0.0/16";
1430 const std::wstring ipAddress = L"172.74.0.42";
1431
1432 auto result = RunWslc(std::format(L"network create --driver bridge --subnet {} {}", subnet, TestNetworkName));
1433 result.Verify({.Stderr = L"", .ExitCode = 0});
1434 auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
1435
1436 result = RunWslc(std::format(
1437 L"container create --name {} --network {} --ip {} {} true", WslcContainerName, TestNetworkName, ipAddress, DebianImage.NameAndTag()));
1438 result.Verify({.Stderr = L"", .ExitCode = 0});
1439
1440 const auto inspect = InspectContainer(WslcContainerName);
1441 const auto networkName = wsl::shared::string::WideToMultiByte(TestNetworkName);
1442 const auto expectedIp = wsl::shared::string::WideToMultiByte(ipAddress);
1443 VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
1444 const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
1445 VERIFY_IS_TRUE(endpoint.IPAMConfig.has_value());
1446 VERIFY_ARE_EQUAL(expectedIp, endpoint.IPAMConfig->IPv4Address);
1447 }
1448
1449 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ip_NoNetwork_Rejected)
1450 {
1451 const std::wstring ipAddress = L"172.74.0.42";
1452
1453 auto result =
1454 RunWslc(std::format(L"container create --ip {} --name {} {} true", ipAddress, WslcContainerName, DebianImage.NameAndTag()));
1455 result.Verify(
1456 {.Stderr = FormatErrorMessage(wsl::shared::Localization::MessageWslcIpRequiresUserDefinedNetwork(), L"E_INVALIDARG"),
1457 .ExitCode = 1});
1458 VerifyContainerIsNotListed(WslcContainerName);
1459 }
1460
1461 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ip_BridgeMode_Rejected)
1462 {
1463 const std::wstring ipAddress = L"172.74.0.42";
1464
1465 auto result = RunWslc(std::format(
1466 L"container create --network bridge --ip {} --name {} {} true", ipAddress, WslcContainerName, DebianImage.NameAndTag()));
1467 result.Verify(
1468 {.Stderr = FormatErrorMessage(wsl::shared::Localization::MessageWslcIpRequiresUserDefinedNetwork(), L"E_INVALIDARG"),
1469 .ExitCode = 1});
1470 VerifyContainerIsNotListed(WslcContainerName);
1471 }
1472
1473 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ip_MultipleNetworks_Rejected)
1474 {
1475 const std::wstring ipAddress = L"172.74.0.42";
1476
1477 auto result = RunWslc(std::format(
1478 L"container create --network bridge --network bridge --ip {} --name {} {} true",
1479 ipAddress,
1480 WslcContainerName,
1481 DebianImage.NameAndTag()));
1482 result.Verify({.Stdout = L"", .ExitCode = 1});
1483 VERIFY_IS_TRUE(result.StderrContainsSubstring(
1484 wsl::shared::Localization::MessageWslcIpAmbiguousWithMultipleNetworks() + L"\r\nError code: E_INVALIDARG"));
1485 VerifyContainerIsNotListed(WslcContainerName);
1486 }
1487
1488 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ip_InvalidValue_Rejected)
1489 {
1490 const std::wstring badIp = L"not-an-ip";
1491
1492 auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
1493 result.Verify({.Stderr = L"", .ExitCode = 0});
1494 auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
1495
1496 result = RunWslc(std::format(
1497 L"container create --network {} --ip {} --name {} {} true", TestNetworkName, badIp, WslcContainerName, DebianImage.NameAndTag()));
1498 result.Verify({.Stdout = L"", .ExitCode = 1});
1499 VERIFY_IS_TRUE(result.Stderr.has_value());
1500 VerifyPatternMatch(
1501 wsl::shared::string::WideToMultiByte(result.Stderr.value()),
1502 std::format("*Invalid IP address '{}'*", wsl::shared::string::WideToMultiByte(badIp)));
1503 VerifyContainerIsNotListed(WslcContainerName);
1504 }
1505
1506 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Cpus)
1507 {
1508 auto result = RunWslc(std::format(L"container create --name {} --cpus 0.5 {} true", WslcContainerName, DebianImage.NameAndTag()));
1509 result.Verify({.Stderr = L"", .ExitCode = 0});
1510
1511 const auto inspect = InspectContainer(WslcContainerName);
1512 VERIFY_ARE_EQUAL(static_cast<int64_t>(500'000'000), inspect.HostConfig.NanoCpus);
1513 }
1514
1515 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Cpus_Invalid)
1516 {
1517 auto result = RunWslc(std::format(L"container create --cpus 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1518 result.Verify({.Stdout = L"", .ExitCode = 1});
1519 VERIFY_IS_TRUE(result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidCpusError(L"cpus", L"0")));
1520 EnsureContainerDoesNotExist(WslcContainerName);
1521 }
1522
1523 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Memory)
1524 {
1525 auto result =
1526 RunWslc(std::format(L"container create --name {} --memory 32M {} true", WslcContainerName, DebianImage.NameAndTag()));
1527 // stderr not asserted: some kernels emit a swap-limit warning when --memory is set.
1528 result.Verify({.ExitCode = 0});
1529
1530 const auto inspect = InspectContainer(WslcContainerName);
1531 VERIFY_ARE_EQUAL(static_cast<int64_t>(32) * 1024 * 1024, inspect.HostConfig.Memory);
1532 }
1533
1534 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Memory_Invalid)
1535 {
1536 auto result =
1537 RunWslc(std::format(L"container create --memory invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1538 result.Verify({.Stdout = L"", .ExitCode = 1});
1539 VERIFY_IS_TRUE(
1540 result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidMemorySizeError(L"memory", L"invalid")));
1541 EnsureContainerDoesNotExist(WslcContainerName);
1542 }
1543
1544 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ulimit)
1545 {
1546 auto result = RunWslc(std::format(
1547 L"container create --name {} --ulimit nofile=1024:2048 --ulimit nproc=512 {} true", WslcContainerName, DebianImage.NameAndTag()));
1548 result.Verify({.Stderr = L"", .ExitCode = 0});
1549
1550 const auto inspect = InspectContainer(WslcContainerName);
1551 VERIFY_ARE_EQUAL(static_cast<size_t>(2), inspect.HostConfig.Ulimits.size());
1552
1553 std::map<std::string, std::pair<int64_t, int64_t>> byName;
1554 for (const auto& ul : inspect.HostConfig.Ulimits)
1555 {
1556 byName[ul.Name] = {ul.Soft, ul.Hard};
1557 }
1558
1559 VERIFY_IS_TRUE(byName.contains("nofile"));
1560 VERIFY_ARE_EQUAL(static_cast<int64_t>(1024), byName["nofile"].first);
1561 VERIFY_ARE_EQUAL(static_cast<int64_t>(2048), byName["nofile"].second);
1562
1563 VERIFY_IS_TRUE(byName.contains("nproc"));
1564 VERIFY_ARE_EQUAL(static_cast<int64_t>(512), byName["nproc"].first);
1565 VERIFY_ARE_EQUAL(static_cast<int64_t>(512), byName["nproc"].second);
1566 }
1567
1568 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ulimit_Invalid)
1569 {
1570 auto result = RunWslc(std::format(L"container create --ulimit nofile --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1571 result.Verify({.Stdout = L"", .ExitCode = 1});
1572 VERIFY_IS_TRUE(
1573 result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidUlimitError(L"ulimit", L"nofile")));
1574 EnsureContainerDoesNotExist(WslcContainerName);
1575 }
1576
1577 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Entrypoint)
1578 {
1579 auto result =
1580 RunWslc(std::format(L"container create --name {} --entrypoint /bin/whoami {}", WslcContainerName, DebianImage.NameAndTag()));
1581 result.Verify({.Stderr = L"", .ExitCode = 0});
1582
1583 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
1584 result.Verify({.Stdout = L"root\n", .Stderr = L"", .ExitCode = 0});
1585 }
1586
1587 WSLC_TEST_METHOD(WSLCE2E_Container_Create_EnvFile)
1588 {
1589 WriteTestFile(
1590 EnvTestFile1, {"WSLC_TEST_CREATE_ENV_FILE_A=create-env-file-a", "WSLC_TEST_CREATE_ENV_FILE_B=create-env-file-b"});
1591
1592 auto result = RunWslc(std::format(
1593 L"container create --name {} --env-file {} {} env", WslcContainerName, EscapePath(EnvTestFile1.wstring()), DebianImage.NameAndTag()));
1594 result.Verify({.Stderr = L"", .ExitCode = 0});
1595
1596 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
1597 result.Verify({.Stderr = L"", .ExitCode = 0});
1598
1599 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_CREATE_ENV_FILE_A=create-env-file-a"));
1600 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_CREATE_ENV_FILE_B=create-env-file-b"));
1601 }
1602
1603 WSLC_TEST_METHOD(WSLCE2E_Container_Create_EnvFile_MissingFile)
1604 {
1605 auto result = RunWslc(std::format(
1606 L"container create --name {} --env-file ENV_FILE_NOT_FOUND {} env", WslcContainerName, DebianImage.NameAndTag()));
1607 result.Verify({.Stdout = L"", .ExitCode = 1});
1608 VERIFY_IS_TRUE(result.StderrContainsSubstring(
1609 L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG"));
1610 EnsureContainerDoesNotExist(WslcContainerName);
1611 }
1612
1613 WSLC_TEST_METHOD(WSLCE2E_Container_Create_EnvFile_InvalidContent)
1614 {
1615 WriteTestFile(EnvTestFile1, {"WSLC_TEST_ENV_VALID=ok", "BAD KEY=value"});
1616
1617 auto result = RunWslc(std::format(
1618 L"container create --name {} --env-file {} {} env", WslcContainerName, EscapePath(EnvTestFile1.wstring()), DebianImage.NameAndTag()));
1619 result.Verify({.Stdout = L"", .ExitCode = 1});
1620 VERIFY_IS_TRUE(result.StderrContainsSubstring(
1621 L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG"));
1622 EnsureContainerDoesNotExist(WslcContainerName);
1623 }
1624
1625 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Publish_TCP)
1626 {
1627 // Port bindings only show up in inspect after start, so create then start before inspecting.
1628 auto result = RunWslc(std::format(
1629 L"container create --name {} -p {}:{} {} sleep 5", WslcContainerName, HostTestPort1, ContainerTestPort, DebianImage.NameAndTag()));
1630 result.Verify({.Stderr = L"", .ExitCode = 0});
1631
1632 result = RunWslc(std::format(L"container start {}", WslcContainerName));
1633 result.Verify({.Stderr = L"", .ExitCode = 0});
1634
1635 // Verify the port mapping is correct in the container inspect data
1636 const auto inspect = InspectContainer(WslcContainerName);
1637 const auto portKey = std::to_string(ContainerTestPort) + "/tcp";
1638 VERIFY_IS_TRUE(inspect.Ports.contains(portKey));
1639
1640 const auto& bindings = inspect.Ports.at(portKey);
1641 VERIFY_ARE_EQUAL(1u, bindings.size());
1642 VERIFY_ARE_EQUAL(std::to_string(HostTestPort1), bindings[0].HostPort);
1643 VERIFY_ARE_EQUAL("127.0.0.1", bindings[0].HostIp);
1644 }
1645
1646 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Publish_MultipleMappings)
1647 {
1648 // Map two host ports to the same container port.
1649 auto result = RunWslc(std::format(
1650 L"container create --name {} -p {}:{} -p {}:{} {} sleep 5",
1651 WslcContainerName,
1652 HostTestPort1,
1653 ContainerTestPort,
1654 HostTestPort2,
1655 ContainerTestPort,
1656 DebianImage.NameAndTag()));
1657 result.Verify({.Stderr = L"", .ExitCode = 0});
1658
1659 result = RunWslc(std::format(L"container start {}", WslcContainerName));
1660 result.Verify({.Stderr = L"", .ExitCode = 0});
1661
1662 // Both host ports should be bound to the same container port.
1663 const auto inspect = InspectContainer(WslcContainerName);
1664 const auto portKey = std::to_string(ContainerTestPort) + "/tcp";
1665 VERIFY_IS_TRUE(inspect.Ports.contains(portKey));
1666
1667 const auto& bindings = inspect.Ports.at(portKey);
1668 VERIFY_ARE_EQUAL(2u, bindings.size());
1669
1670 bool foundHostPort1 = false;
1671 bool foundHostPort2 = false;
1672 for (const auto& binding : bindings)
1673 {
1674 if (binding.HostPort == std::to_string(HostTestPort1))
1675 {
1676 foundHostPort1 = true;
1677 }
1678 else if (binding.HostPort == std::to_string(HostTestPort2))
1679 {
1680 foundHostPort2 = true;
1681 }
1682 }
1683 VERIFY_IS_TRUE(foundHostPort1);
1684 VERIFY_IS_TRUE(foundHostPort2);
1685 }
1686
1687 // https://github.com/microsoft/WSL/issues/14433
1688 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Publish_Ephemeral)
1689 {
1690 // -p <containerPort> (no host port) means the host picks a random port.
1691 auto result = RunWslc(std::format(
1692 L"container create --name {} -p {} {} sleep 5", WslcContainerName, ContainerTestPort, DebianImage.NameAndTag()));
1693 result.Verify({.Stderr = L"", .ExitCode = 0});
1694
1695 result = RunWslc(std::format(L"container start {}", WslcContainerName));
1696 result.Verify({.Stderr = L"", .ExitCode = 0});
1697
1698 // Inspect the container to verify a host port was allocated.
1699 const auto inspect = InspectContainer(WslcContainerName);
1700 const auto portKey = std::to_string(ContainerTestPort) + "/tcp";
1701 VERIFY_IS_TRUE(inspect.Ports.contains(portKey));
1702
1703 const auto& bindings = inspect.Ports.at(portKey);
1704 VERIFY_ARE_EQUAL(1u, bindings.size());
1705 VERIFY_IS_TRUE(std::stoi(bindings[0].HostPort) > 0);
1706 }
1707
1708 // https://github.com/microsoft/WSL/issues/14433
1709 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Publish_UDP)
1710 {
1711 // Port bindings only show up in inspect after start, so create then start before inspecting.
1712 auto result = RunWslc(std::format(
1713 L"container create --name {} -p {}:{}/udp {} sleep 5",
1714 WslcContainerName,
1715 HostTestPort1,
1716 ContainerTestPort,
1717 DebianImage.NameAndTag()));
1718 result.Verify({.Stderr = L"", .ExitCode = 0});
1719
1720 result = RunWslc(std::format(L"container start {}", WslcContainerName));
1721 result.Verify({.Stderr = L"", .ExitCode = 0});
1722
1723 // Verify the UDP port mapping is correct in the container inspect data.
1724 const auto inspect = InspectContainer(WslcContainerName);
1725 const auto portKey = std::to_string(ContainerTestPort) + "/udp";
1726 VERIFY_IS_TRUE(inspect.Ports.contains(portKey));
1727
1728 const auto& bindings = inspect.Ports.at(portKey);
1729 VERIFY_ARE_EQUAL(1u, bindings.size());
1730 VERIFY_ARE_EQUAL(std::to_string(HostTestPort1), bindings[0].HostPort);
1731 VERIFY_ARE_EQUAL("127.0.0.1", bindings[0].HostIp);
1732 }
1733
1734 // https://github.com/microsoft/WSL/issues/14433
1735 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Publish_HostIP)
1736 {
1737 // Port bindings only show up in inspect after start, so create then start before inspecting.
1738 auto result = RunWslc(std::format(
1739 L"container create --name {} -p 127.0.0.1:{}:{} {} sleep 5",
1740 WslcContainerName,
1741 HostTestPort1,
1742 ContainerTestPort,
1743 DebianImage.NameAndTag()));
1744 result.Verify({.Stderr = L"", .ExitCode = 0});
1745
1746 result = RunWslc(std::format(L"container start {}", WslcContainerName));
1747 result.Verify({.Stderr = L"", .ExitCode = 0});
1748
1749 // Verify the port mapping is bound to the requested host IP in the container inspect data.
1750 const auto inspect = InspectContainer(WslcContainerName);
1751 const auto portKey = std::to_string(ContainerTestPort) + "/tcp";
1752 VERIFY_IS_TRUE(inspect.Ports.contains(portKey));
1753
1754 const auto& bindings = inspect.Ports.at(portKey);
1755 VERIFY_ARE_EQUAL(1u, bindings.size());
1756 VERIFY_ARE_EQUAL(std::to_string(HostTestPort1), bindings[0].HostPort);
1757 VERIFY_ARE_EQUAL("127.0.0.1", bindings[0].HostIp);
1758 }
1759
1760 WSLC_TEST_METHOD(WSLCE2E_Container_Create_PublishAll_DualStack)
1761 {
1762 // Remove the container and built image on exit. Both helpers are idempotent, so this is safe
1763 // to arm before either resource exists.
1764 auto cleanup = wil::scope_exit([&] {
1765 EnsureContainerDoesNotExist(WslcContainerName);
1766 TestImageRegistry::Instance().Delete(PublishAllImage);
1767 });
1768
1769 // Load the Python base image so the test image can be built offline.
1770 TestImageRegistry::Instance().EnsureLoaded(PythonImage);
1771
1772 // Build an image that exposes a TCP and a UDP port and ships a server that listens on both,
1773 // so publish-all can be exercised end to end.
1774 auto testRoot = std::filesystem::current_path() / L"wslc-e2e-publish-all";
1775 auto cleanupDir = SetupTestDirectory(testRoot);
1776
1777 auto contextDir = testRoot / L"context";
1778 std::error_code ec;
1779 std::filesystem::create_directories(contextDir, ec);
1780 THROW_HR_IF(E_FAIL, ec.value() != 0 || !std::filesystem::exists(contextDir));
1781
1782 // Dual-stack HTTP server on 8080/tcp plus a UDP echo server on 9090/udp in one process. Both
1783 // sockets bind before "SERVERS READY" is printed, so that marker signals both ports accept.
1784 WriteTestFileContent(
1785 contextDir / L"server.py",
1786 R"PY(
1787 import contextlib, socket, threading
1788 from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
1789
1790 class DualStackHttp(ThreadingHTTPServer):
1791 address_family = socket.AF_INET6
1792
1793 def server_bind(self):
1794 with contextlib.suppress(Exception):
1795 self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
1796 super().server_bind()
1797
1798 http_server = DualStackHttp(('::', 8080), SimpleHTTPRequestHandler)
1799
1800 udp = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
1801 udp.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
1802 udp.bind(('::', 9090))
1803
1804 threading.Thread(target=http_server.serve_forever, daemon=True).start()
1805 print('SERVERS READY', flush=True)
1806
1807 while True:
1808 data, address = udp.recvfrom(1024)
1809 udp.sendto(data.upper(), address)
1810 )PY");
1811
1812 auto dockerfilePath = testRoot / L"Dockerfile";
1813 WriteTestFileContent(
1814 dockerfilePath,
1815 std::format(
1816 "FROM {}\n"
1817 "COPY server.py /server.py\n"
1818 "EXPOSE 8080/tcp\n"
1819 "EXPOSE 9090/udp\n",
1820 string::WideToMultiByte(PythonImage.NameAndTag())));
1821
1822 auto buildResult = RunWslc(std::format(
1823 L"build \"{}\" -f \"{}\" -t {}", contextDir.wstring(), dockerfilePath.wstring(), PublishAllImage.NameAndTag()));
1824 buildResult.Verify({.Stdout = L"", .ExitCode = 0});
1825
1826 // Port bindings only show up in inspect after start, so create then start before inspecting.
1827 auto result = RunWslc(
1828 std::format(L"container create --name {} -P {} python3 -u /server.py", WslcContainerName, PublishAllImage.NameAndTag()));
1829 result.Verify({.Stderr = L"", .ExitCode = 0});
1830
1831 result = RunWslc(std::format(L"container start {}", WslcContainerName));
1832 result.Verify({.Stderr = L"", .ExitCode = 0});
1833
1834 // Wait until both listeners are bound before probing the published ports.
1835 WaitForContainerOutput(WslcContainerName, "SERVERS READY");
1836
1837 // Each exposed port must be published dual-stack: one IPv4 (127.0.0.1) and one IPv6 (::1)
1838 // loopback binding, each on an ephemeral host port. Return both host ports for the protocol.
1839 const auto inspect = InspectContainer(WslcContainerName);
1840 auto getDualStackHostPorts = [&](const std::string& portKey) -> std::pair<uint16_t, uint16_t> {
1841 VERIFY_IS_TRUE(inspect.Ports.contains(portKey));
1842
1843 const auto& bindings = inspect.Ports.at(portKey);
1844 VERIFY_ARE_EQUAL(2u, bindings.size());
1845
1846 uint16_t ipv4Port = 0;
1847 uint16_t ipv6Port = 0;
1848 for (const auto& binding : bindings)
1849 {
1850 const int hostPort = std::stoi(binding.HostPort);
1851 VERIFY_IS_TRUE(hostPort > 0 && hostPort <= 65535);
1852 if (binding.HostIp == "127.0.0.1")
1853 {
1854 ipv4Port = static_cast<uint16_t>(hostPort);
1855 }
1856 else if (binding.HostIp == "::1")
1857 {
1858 ipv6Port = static_cast<uint16_t>(hostPort);
1859 }
1860 }
1861
1862 VERIFY_IS_TRUE(ipv4Port > 0);
1863 VERIFY_IS_TRUE(ipv6Port > 0);
1864 return {ipv4Port, ipv6Port};
1865 };
1866
1867 // TCP: the HTTP server must respond over both IPv4 and IPv6 loopback.
1868 const auto [tcpIpv4, tcpIpv6] = getDualStackHostPorts("8080/tcp");
1869 ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", tcpIpv4).c_str(), HTTP_STATUS_OK, true);
1870 ExpectHttpResponse(std::format(L"http://[::1]:{}", tcpIpv6).c_str(), HTTP_STATUS_OK, true);
1871
1872 // UDP: the echo server must reply over both IPv4 and IPv6 loopback.
1873 const auto [udpIpv4, udpIpv6] = getDualStackHostPorts("9090/udp");
1874 SendUdpAndReceive(udpIpv4, "hello", "HELLO", AF_INET);
1875 SendUdpAndReceive(udpIpv6, "hello", "HELLO", AF_INET6);
1876 }
1877
1878 private:
1879 // Test container name
1880 const std::wstring WslcContainerName = L"wslc-test-container";
1881
1882 // Test network name
1883 const std::wstring TestNetworkName = L"wslc-test-network";
1884
1885 // Test named volume
1886 const std::wstring WslcVolumeName = L"wslc-test-volume";
1887
1888 // Test environment variables
1889 const std::wstring HostEnvVariableName = L"WSLC_TEST_HOST_ENV";
1890 const std::wstring HostEnvVariableName2 = L"WSLC_TEST_HOST_ENV2";
1891 const std::wstring HostEnvVariableValue = L"wslc-host-env-value";
1892 const std::wstring HostEnvVariableValue2 = L"wslc-host-env-value2";
1893 const std::wstring MissingHostEnvVariableName = L"WSLC_TEST_MISSING_HOST_ENV";
1894
1895 // Test environment variable files
1896 std::filesystem::path EnvTestFile1;
1897 std::filesystem::path EnvTestFile2;
1898
1899 // Test images
1900 const TestImage& AlpineImage = AlpineTestImage();
1901 const TestImage& DebianImage = DebianTestImage();
1902 const TestImage& HelloWorldImage = HelloWorldTestImage();
1903 const TestImage& PythonImage = PythonTestImage();
1904 const TestImage& InvalidImage = InvalidTestImage();
1905
1906 // Image built at test time with exposed TCP and UDP ports for publish-all coverage.
1907 const TestImage PublishAllImage{L"wslc-e2e-publish-all", L"latest", L""};
1908
1909 // Test ports
1910 const uint16_t ContainerTestPort = 8080;
1911 const uint16_t HostTestPort1 = 1234;
1912 const uint16_t HostTestPort2 = 1235;
1913
1914 // Test volume files
1915 std::filesystem::path VolumeTestFile1;
1916 std::filesystem::path VolumeTestFile2;
1917 };
1918 } // namespace WSLCE2ETests