master
cpp 1,678 lines 76.8 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCE2EContainerRunTests.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
20 namespace WSLCE2ETests {
21 using namespace wsl::shared;
22
23 class WSLCE2EContainerRunTests
24 {
25 WSLC_TEST_CLASS(WSLCE2EContainerRunTests)
26
27 TEST_CLASS_SETUP(ClassSetup)
28 {
29 TestImageRegistry::Instance().EnsureLoaded(DebianImage);
30 TestImageRegistry::Instance().EnsureLoaded(HelloWorldImage);
31 TestImageRegistry::Instance().EnsureLoaded(PythonImage);
32
33 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), HostEnvVariableValue.c_str()));
34 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName2.c_str(), HostEnvVariableValue2.c_str()));
35
36 // Initialize Winsock for loopback connectivity tests
37 WSADATA wsaData{};
38 const int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
39 THROW_HR_IF(HRESULT_FROM_WIN32(result), result != 0);
40 return true;
41 }
42
43 TEST_CLASS_CLEANUP(ClassCleanup)
44 {
45 EnsureContainerDoesNotExist(WslcContainerName);
46 EnsureContainerDoesNotExist(WslcContainerName2);
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
53 // Cleanup Winsock
54 WSACleanup();
55 return true;
56 }
57
58 TEST_METHOD_SETUP(TestMethodSetup)
59 {
60 EnsureContainerDoesNotExist(WslcContainerName);
61 EnsureContainerDoesNotExist(WslcContainerName2);
62 EnsureVolumeDoesNotExist(WslcVolumeName);
63 EnsureNetworkDoesNotExist(TestNetworkName);
64
65 EnvTestFile1 = wsl::windows::common::filesystem::GetTempFilename();
66 EnvTestFile2 = wsl::windows::common::filesystem::GetTempFilename();
67 return true;
68 }
69
70 TEST_METHOD_CLEANUP(TestMethodCleanup)
71 {
72 DeleteFileW(EnvTestFile1.c_str());
73 DeleteFileW(EnvTestFile2.c_str());
74 return true;
75 }
76
77 WSLC_TEST_METHOD(WSLCE2E_Container_Run_HelpCommand)
78 {
79 auto result = RunWslc(L"container run --help");
80 result.Verify({.Stderr = L"", .ExitCode = 0});
81 VERIFY_IS_FALSE(result.Stdout.value().empty());
82 }
83
84 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Container_With_Command)
85 {
86 VerifyContainerIsNotListed(WslcContainerName);
87
88 auto command = L"echo echo_from_container";
89 auto result = RunWslc(std::format(L"container run --name {} {} {}", WslcContainerName, DebianImage.NameAndTag(), command));
90 result.Verify({.Stdout = L"echo_from_container\n", .Stderr = L"", .ExitCode = 0});
91
92 VerifyContainerIsListed(WslcContainerName, L"exited");
93 }
94
95 WSLC_TEST_METHOD(WSLCE2E_Container_Run_PullPolicy)
96 {
97 auto session = OpenDefaultElevatedSession();
98 auto [registryContainer, registryAddress] = StartLocalRegistry(*session);
99 auto registryImage = TagImageForRegistry(HelloWorldImage.NameAndTag(), wsl::shared::string::MultiByteToWide(registryAddress));
100 auto cleanup = wil::scope_exit([&]() {
101 EnsureContainerDoesNotExist(WslcContainerName);
102 RunWslc(std::format(L"image delete --force {}", registryImage));
103 });
104
105 auto result = RunWslc(std::format(L"container run --pull=never --rm --name {} {}", WslcContainerName, registryImage));
106 result.Verify({.Stderr = L"", .ExitCode = 0});
107 VERIFY_IS_TRUE(result.Stdout.has_value());
108 VERIFY_IS_FALSE(result.Stdout->empty());
109
110 result = RunWslc(std::format(L"container run --pull=always --rm --name {} {}", WslcContainerName, registryImage));
111 const auto errorMessage = FormatErrorMessage(
112 std::format(L"manifest for {} not found: manifest unknown: manifest unknown", registryImage),
113 L"WSLC_E_IMAGE_NOT_FOUND");
114 result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1});
115 VerifyContainerIsNotListed(WslcContainerName);
116
117 RunWslcAndVerify(std::format(L"push {}", registryImage), {.Stderr = L"", .ExitCode = 0});
118 RunWslcAndVerify(std::format(L"image delete --force {}", registryImage), {.ExitCode = 0});
119
120 result = RunWslc(std::format(L"container run --pull=missing --rm --name {} {}", WslcContainerName, registryImage));
121 result.Verify({.ExitCode = 0});
122 VERIFY_IS_TRUE(result.Stdout.has_value());
123 VERIFY_IS_FALSE(result.Stdout->empty());
124 }
125
126 WSLC_TEST_METHOD(WSLCE2E_Container_Run_CIDFile_Valid)
127 {
128 // Prepare a CID file path that does not exist
129 const auto cidFilePath = wsl::windows::common::filesystem::GetTempFilename();
130 VERIFY_IS_TRUE(DeleteFileW(cidFilePath.c_str()));
131 auto deleteCidFile = wil::scope_exit([&]() { VERIFY_IS_TRUE(DeleteFileW(cidFilePath.c_str())); });
132
133 auto result = RunWslc(std::format(
134 L"container run -d --cidfile \"{}\" --name {} {} sleep infinity",
135 EscapePath(cidFilePath.wstring()),
136 WslcContainerName,
137 DebianImage.NameAndTag()));
138 result.Verify({.Stderr = L"", .ExitCode = 0});
139
140 const auto containerId = result.GetStdoutOneLine();
141 VERIFY_IS_TRUE(std::filesystem::exists(cidFilePath));
142 VERIFY_ARE_EQUAL(containerId, ReadFileContent(cidFilePath.wstring()));
143 }
144
145 WSLC_TEST_METHOD(WSLCE2E_Container_Run_CIDFile_AlreadyExists)
146 {
147 const auto cidFilePath = wsl::windows::common::filesystem::GetTempFilename();
148 auto deleteCidFile = wil::scope_exit([&]() { VERIFY_IS_TRUE(DeleteFileW(cidFilePath.c_str())); });
149
150 auto result = RunWslc(std::format(
151 L"container run --cidfile \"{}\" --name {} {}", EscapePath(cidFilePath.wstring()), WslcContainerName, DebianImage.NameAndTag()));
152 result.Verify(
153 {.Stderr = FormatErrorMessage(
154 std::format(L"CID file '{}' already exists", EscapePath(cidFilePath.wstring())), L"ERROR_FILE_EXISTS"),
155 .ExitCode = 1});
156
157 VerifyContainerIsNotListed(WslcContainerName);
158 }
159
160 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Entrypoint)
161 {
162 auto result = RunWslc(std::format(L"container run --rm --entrypoint /bin/whoami {}", DebianImage.NameAndTag()));
163 result.Verify({.Stdout = L"root\n", .Stderr = L"", .ExitCode = 0});
164 }
165
166 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Entrypoint_And_Arguments)
167 {
168 auto result = RunWslc(
169 std::format(L"container run --rm --entrypoint /bin/echo {} hello from entrypoint with args", DebianImage.NameAndTag()));
170 result.Verify({.Stdout = L"hello from entrypoint with args\n", .Stderr = L"", .ExitCode = 0});
171 }
172
173 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Entrypoint_Invalid_Path)
174 {
175 auto result = RunWslc(std::format(L"container run --rm --entrypoint /bin/does-not-exist {}", DebianImage.NameAndTag()));
176 result.Verify(
177 {.Stdout = L"",
178 .Stderr = FormatErrorMessage(
179 L"failed to create task for container: failed to create shim task: OCI runtime create failed: runc create "
180 L"failed: unable to start container process: error during container init: exec: \"/bin/does-not-exist\": stat "
181 L"/bin/does-not-exist: no such file or directory: unknown",
182 L"E_INVALIDARG"),
183 .ExitCode = 1});
184 }
185
186 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Entrypoint_Detach_Lifecycle)
187 {
188 auto result = RunWslc(std::format(
189 L"container run --name {} -d --entrypoint /bin/sleep {} infinity", WslcContainerName, DebianImage.NameAndTag()));
190 result.Verify({.Stderr = L"", .ExitCode = 0});
191
192 VerifyContainerIsListed(WslcContainerName, L"running");
193 }
194
195 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Remove)
196 {
197 VerifyContainerIsNotListed(WslcContainerName);
198
199 // Run the container with a valid image
200 auto result = RunWslc(std::format(L"container run --rm --name {} {} echo hello", WslcContainerName, DebianImage.NameAndTag()));
201 result.Verify({.Stderr = L"", .ExitCode = 0});
202
203 // Run should be deleted on return so no retry.
204 VerifyContainerIsNotListed(WslcContainerName);
205 }
206
207 WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvOption)
208 {
209 VerifyContainerIsNotListed(WslcContainerName);
210
211 auto result = RunWslc(std::format(
212 L"container run --rm --name {} -e {}=A {} env", WslcContainerName, HostEnvVariableName, DebianImage.NameAndTag()));
213 result.Verify({.Stderr = L"", .ExitCode = 0});
214
215 VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}=A", HostEnvVariableName)));
216 }
217
218 WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvOption_MultipleValues)
219 {
220 VerifyContainerIsNotListed(WslcContainerName);
221
222 auto result = RunWslc(std::format(
223 L"container run --rm --name {} -e {}=A -e {}=B {} env",
224 WslcContainerName,
225 HostEnvVariableName,
226 HostEnvVariableName2,
227 DebianImage.NameAndTag()));
228 result.Verify({.Stderr = L"", .ExitCode = 0});
229
230 VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}=A", HostEnvVariableName)));
231 VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}=B", HostEnvVariableName2)));
232 }
233
234 WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvOption_KeyOnly_UsesHostValue)
235 {
236 VerifyContainerIsNotListed(WslcContainerName);
237
238 auto result = RunWslc(std::format(
239 L"container run --rm --name {} -e {} {} env", WslcContainerName, HostEnvVariableName, DebianImage.NameAndTag()));
240 result.Verify({.Stderr = L"", .ExitCode = 0});
241
242 VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}={}", HostEnvVariableName, HostEnvVariableValue)));
243 }
244
245 WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvOption_KeyOnly_MultipleValues_UsesHostValues)
246 {
247 VerifyContainerIsNotListed(WslcContainerName);
248
249 auto result = RunWslc(std::format(
250 L"container run --rm --name {} -e {} -e {} {} env",
251 WslcContainerName,
252 HostEnvVariableName,
253 HostEnvVariableName2,
254 DebianImage.NameAndTag()));
255 result.Verify({.Stderr = L"", .ExitCode = 0});
256
257 VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}={}", HostEnvVariableName, HostEnvVariableValue)));
258 VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}={}", HostEnvVariableName2, HostEnvVariableValue2)));
259 }
260
261 WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvOption_EmptyValue)
262 {
263 VerifyContainerIsNotListed(WslcContainerName);
264
265 auto result = RunWslc(std::format(
266 L"container run --rm --name {} -e {}= {} env", WslcContainerName, HostEnvVariableName, DebianImage.NameAndTag()));
267 result.Verify({.Stderr = L"", .ExitCode = 0});
268
269 VERIFY_IS_TRUE(result.StdoutContainsLine(std::format(L"{}=", HostEnvVariableName)));
270 }
271
272 WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvFile)
273 {
274 VerifyContainerIsNotListed(WslcContainerName);
275
276 WriteTestFile(EnvTestFile1, {"WSLC_TEST_ENV_FILE_A=env-file-a", "WSLC_TEST_ENV_FILE_B=env-file-b"});
277
278 auto result = RunWslc(std::format(
279 L"container run --rm --name {} --env-file {} {} env",
280 WslcContainerName,
281 EscapePath(EnvTestFile1.wstring()),
282 DebianImage.NameAndTag()));
283 result.Verify({.Stderr = L"", .ExitCode = 0});
284
285 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_FILE_A=env-file-a"));
286 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_FILE_B=env-file-b"));
287 }
288
289 WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvOption_MixedWithEnvFile)
290 {
291 VerifyContainerIsNotListed(WslcContainerName);
292
293 WriteTestFile(EnvTestFile1, {"WSLC_TEST_ENV_MIX_FILE_A=from-file-a", "WSLC_TEST_ENV_MIX_FILE_B=from-file-b"});
294
295 auto result = RunWslc(std::format(
296 L"container run --rm --name {} -e WSLC_TEST_ENV_MIX_CLI=from-cli --env-file {} {} env",
297 WslcContainerName,
298 EscapePath(EnvTestFile1.wstring()),
299 DebianImage.NameAndTag()));
300 result.Verify({.Stderr = L"", .ExitCode = 0});
301
302 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_MIX_FILE_A=from-file-a"));
303 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_MIX_FILE_B=from-file-b"));
304 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_MIX_CLI=from-cli"));
305 }
306
307 WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvFile_MultipleFiles)
308 {
309 VerifyContainerIsNotListed(WslcContainerName);
310
311 WriteTestFile(EnvTestFile1, {"WSLC_TEST_ENV_FILE_MULTI_A=file1-a", "WSLC_TEST_ENV_FILE_MULTI_B=file1-b"});
312
313 WriteTestFile(EnvTestFile2, {"WSLC_TEST_ENV_FILE_MULTI_C=file2-c", "WSLC_TEST_ENV_FILE_MULTI_D=file2-d"});
314
315 auto result = RunWslc(std::format(
316 L"container run --rm --name {} --env-file {} --env-file {} {} env",
317 WslcContainerName,
318 EscapePath(EnvTestFile1.wstring()),
319 EscapePath(EnvTestFile2.wstring()),
320 DebianImage.NameAndTag()));
321 result.Verify({.Stderr = L"", .ExitCode = 0});
322
323 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_FILE_MULTI_A=file1-a"));
324 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_FILE_MULTI_B=file1-b"));
325 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_FILE_MULTI_C=file2-c"));
326 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_FILE_MULTI_D=file2-d"));
327 }
328
329 WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvFile_MissingFile)
330 {
331 VerifyContainerIsNotListed(WslcContainerName);
332
333 auto result = RunWslc(std::format(
334 L"container run --rm --name {} --env-file ENV_FILE_NOT_FOUND {} env", WslcContainerName, DebianImage.NameAndTag()));
335 result.Verify({.Stdout = L"", .ExitCode = 1});
336 VERIFY_IS_TRUE(result.StderrContainsSubstring(
337 L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG"));
338 }
339
340 WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvFile_InvalidContent)
341 {
342 VerifyContainerIsNotListed(WslcContainerName);
343
344 WriteTestFile(EnvTestFile1, {"WSLC_TEST_ENV_VALID=ok", "BAD KEY=value"});
345
346 auto result = RunWslc(std::format(
347 L"container run --rm --name {} --env-file {} {} env",
348 WslcContainerName,
349 EscapePath(EnvTestFile1.wstring()),
350 DebianImage.NameAndTag()));
351 result.Verify({.Stdout = L"", .ExitCode = 1});
352 VERIFY_IS_TRUE(result.StderrContainsSubstring(
353 L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG"));
354 }
355
356 WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvFile_DuplicateKeys_Precedence)
357 {
358 VerifyContainerIsNotListed(WslcContainerName);
359
360 WriteTestFile(EnvTestFile1, {"WSLC_TEST_ENV_DUP=from-file-1"});
361
362 WriteTestFile(EnvTestFile2, {"WSLC_TEST_ENV_DUP=from-file-2"});
363
364 // Later --env-file should win over earlier --env-file for duplicate keys
365 auto result = RunWslc(std::format(
366 L"container run --rm --name {} --env-file {} --env-file {} {} env",
367 WslcContainerName,
368 EscapePath(EnvTestFile1.wstring()),
369 EscapePath(EnvTestFile2.wstring()),
370 DebianImage.NameAndTag()));
371 result.Verify({.Stderr = L"", .ExitCode = 0});
372
373 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_DUP=from-file-2"));
374
375 // Explicit -e should win over env-file value for duplicate keys
376 result = RunWslc(std::format(
377 L"container run --rm --name {} -e WSLC_TEST_ENV_DUP=from-cli --env-file {} --env-file {} {} env",
378 WslcContainerName,
379 EscapePath(EnvTestFile1.wstring()),
380 EscapePath(EnvTestFile2.wstring()),
381 DebianImage.NameAndTag()));
382 result.Verify({.Stderr = L"", .ExitCode = 0});
383
384 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_DUP=from-cli"));
385 }
386
387 WSLC_TEST_METHOD(WSLCE2E_Container_Run_EnvFile_ValueContainsEquals)
388 {
389 VerifyContainerIsNotListed(WslcContainerName);
390
391 WriteTestFile(EnvTestFile1, {"WSLC_TEST_ENV_EQUALS=value=with=equals"});
392
393 auto result = RunWslc(std::format(
394 L"container run --rm --name {} --env-file {} {} env",
395 WslcContainerName,
396 EscapePath(EnvTestFile1.wstring()),
397 DebianImage.NameAndTag()));
398 result.Verify({.Stderr = L"", .ExitCode = 0});
399
400 VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_ENV_EQUALS=value=with=equals"));
401 }
402
403 WSLC_TEST_METHOD(WSLCE2E_Container_Run_UserOption_NameRoot)
404 {
405 auto result = RunWslc(std::format(L"container run --rm -u root {} sh -c \"id -un; id -u; id -g\"", DebianImage.NameAndTag()));
406 result.Verify({.Stdout = L"root\n0\n0\n", .Stderr = L"", .ExitCode = 0});
407 }
408
409 WSLC_TEST_METHOD(WSLCE2E_Container_Run_UserOption_UidRoot)
410 {
411 auto result = RunWslc(std::format(L"container run --rm -u 0 {} id -u", DebianImage.NameAndTag()));
412 result.Verify({.Stdout = L"0\n", .Stderr = L"", .ExitCode = 0});
413 }
414
415 WSLC_TEST_METHOD(WSLCE2E_Container_Run_UserOption_UidGidRoot)
416 {
417 auto result = RunWslc(std::format(L"container run --rm -u 0:0 {} sh -c \"id -u; id -g\"", DebianImage.NameAndTag()));
418 result.Verify({.Stdout = L"0\n0\n", .Stderr = L"", .ExitCode = 0});
419 }
420
421 WSLC_TEST_METHOD(WSLCE2E_Container_Run_UserOption_UnknownUser_Fails)
422 {
423 auto result = RunWslc(std::format(L"container run --rm -u user_does_not_exist {} id -u", DebianImage.NameAndTag()));
424 result.Verify(
425 {.Stderr =
426 FormatErrorMessage(L"unable to find user user_does_not_exist: no matching entries in passwd file", L"E_FAIL"),
427 .ExitCode = 1});
428 }
429
430 WSLC_TEST_METHOD(WSLCE2E_Container_Run_UserOption_UnknownGroup_Fails)
431 {
432 auto result = RunWslc(std::format(L"container run --rm -u root:badgid {} id -u", DebianImage.NameAndTag()));
433 result.Verify({.Stderr = FormatErrorMessage(L"unable to find group badgid: no matching entries in group file", L"E_FAIL"), .ExitCode = 1});
434 }
435
436 WSLC_TEST_METHOD(WSLCE2E_Container_Run_UserOption_NameGroupRoot)
437 {
438 auto result =
439 RunWslc(std::format(L"container run --rm -u root:root {} sh -c \"id -un; id -u; id -g\"", DebianImage.NameAndTag()));
440 result.Verify({.Stdout = L"root\n0\n0\n", .Stderr = L"", .ExitCode = 0});
441 }
442
443 WSLC_TEST_METHOD(WSLCE2E_Container_Run_UserOption_NonRootUser_Succeeds)
444 {
445 auto result = RunWslc(std::format(L"container run --rm -u nobody {} sh -c \"id -un; id -u; id -g\"", DebianImage.NameAndTag()));
446 result.Verify({.Stdout = L"nobody\n65534\n65534\n", .Stderr = L"", .ExitCode = 0});
447 }
448
449 WSLC_TEST_METHOD(WSLCE2E_Container_Run_PortMultipleMappings)
450 {
451 // Start a container with a simple server listening on a port
452 // Map two host ports to the same container port
453 auto result = RunWslc(std::format(
454 L"container run -d --name {} -p {}:{} -p {}:{} {} {}",
455 WslcContainerName,
456 HostTestPort1,
457 ContainerTestPort,
458 HostTestPort2,
459 ContainerTestPort,
460 PythonImage.NameAndTag(),
461 GetPythonHttpServerScript(ContainerTestPort)));
462 result.Verify({.Stderr = L"", .ExitCode = 0});
463
464 WaitForContainerOutput(WslcContainerName, "Serving HTTP on");
465
466 // From the host side, verify we can connect to both ports
467 ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", HostTestPort1).c_str(), HTTP_STATUS_OK, true);
468 ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", HostTestPort2).c_str(), HTTP_STATUS_OK, true);
469 }
470
471 WSLC_TEST_METHOD(WSLCE2E_Container_Run_PortAlreadyInUse)
472 {
473 // Start a container with a simple server listening on a port
474 auto result1 = RunWslc(std::format(
475 L"container run -d --name {} -p {}:{} {} {}",
476 WslcContainerName,
477 HostTestPort1,
478 ContainerTestPort,
479 PythonImage.NameAndTag(),
480 GetPythonHttpServerScript(ContainerTestPort)));
481 result1.Verify({.Stderr = L"", .ExitCode = 0});
482
483 // Create a second container mapping the same host port to validate the full error message
484 auto createResult =
485 RunWslc(std::format(L"container create -p {}:{} {}", HostTestPort1, ContainerTestPort, DebianImage.NameAndTag()));
486 createResult.Verify({.Stderr = L"", .ExitCode = 0});
487 auto containerId = createResult.GetStdoutOneLine();
488
489 // Attempt to start — should fail with port conflict
490 auto startResult = RunWslc(std::format(L"container start {}", containerId));
491 startResult.Verify(
492 {.Stderr = FormatErrorMessage(
493 std::format(
494 L"Failed to map port '127.0.0.1:{}/tcp', Only one usage of each socket address (protocol/network "
495 L"address/port) is normally permitted. ",
496 HostTestPort1),
497 L"WSAEADDRINUSE"),
498 .ExitCode = 1});
499
500 // Clean up the created container
501 RunWslc(std::format(L"container rm {}", containerId)).Verify({.Stderr = L"", .ExitCode = 0});
502
503 // Verify 'container run' auto-cleans up on port conflict (no ghost container)
504 auto runResult = RunWslc(std::format(
505 L"container run --name {} -p {}:{} {}", WslcContainerName2, HostTestPort1, ContainerTestPort, DebianImage.NameAndTag()));
506 runResult.Verify({.ExitCode = 1});
507
508 VerifyContainerIsNotListed(WslcContainerName2);
509
510 // Repeat the conflict scenario for an IPv6 loopback ([::1]) binding to validate the IPv6 error message.
511 auto ipv6Server = RunWslc(std::format(
512 L"container run -d --name {} -p [::1]:{}:{} {} {}",
513 WslcContainerName2,
514 HostTestPort2,
515 ContainerTestPort,
516 PythonImage.NameAndTag(),
517 GetPythonHttpServerScript(ContainerTestPort)));
518 ipv6Server.Verify({.Stderr = L"", .ExitCode = 0});
519
520 // Create a second container mapping the same IPv6 address/port to validate the full error message.
521 auto ipv6CreateResult =
522 RunWslc(std::format(L"container create -p [::1]:{}:{} {}", HostTestPort2, ContainerTestPort, DebianImage.NameAndTag()));
523 ipv6CreateResult.Verify({.Stderr = L"", .ExitCode = 0});
524 auto ipv6ContainerId = ipv6CreateResult.GetStdoutOneLine();
525
526 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
527 RunWslc(std::format(L"container rm {}", ipv6ContainerId)).Verify({.Stderr = L"", .ExitCode = 0});
528 });
529
530 // Attempt to start — should fail with a port conflict, with the IPv6 address bracketed in the message.
531 auto ipv6StartResult = RunWslc(std::format(L"container start {}", ipv6ContainerId));
532 ipv6StartResult.Verify(
533 {.Stderr = FormatErrorMessage(
534 std::format(
535 L"Failed to map port '[::1]:{}/tcp', Only one usage of each socket address (protocol/network "
536 L"address/port) is normally permitted. ",
537 HostTestPort2),
538 L"WSAEADDRINUSE"),
539 .ExitCode = 1});
540 }
541
542 WSLC_TEST_METHOD(WSLCE2E_Container_Run_PortEphemeral)
543 {
544 // Start a container with an ephemeral host port mapping (-p 8080 means host picks a random port)
545 auto result = RunWslc(std::format(
546 L"container run -d --name {} -p {} {} {}", WslcContainerName, ContainerTestPort, PythonImage.NameAndTag(), GetPythonHttpServerScript(ContainerTestPort)));
547 result.Verify({.Stderr = L"", .ExitCode = 0});
548
549 // Wait for the in-container HTTP server to start listening before connecting.
550 WaitForContainerOutput(WslcContainerName, "Serving HTTP on");
551
552 // Inspect the container to find the allocated host port
553 auto inspectContainer = InspectContainer(WslcContainerName);
554 auto portKey = std::to_string(ContainerTestPort) + "/tcp";
555 VERIFY_IS_TRUE(inspectContainer.Ports.contains(portKey));
556
557 auto portBindings = inspectContainer.Ports[portKey];
558 VERIFY_ARE_EQUAL(1u, portBindings.size());
559
560 auto hostPort = std::stoi(portBindings[0].HostPort);
561 VERIFY_IS_TRUE(hostPort > 0);
562
563 // Verify we can connect to the server on the ephemeral port
564 ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", hostPort).c_str(), HTTP_STATUS_OK, true);
565 }
566
567 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Port_UDP)
568 {
569 // Start a container with a UDP echo server listening on a port.
570 auto result = RunWslc(std::format(
571 L"container run -d --name {} -p {}:{}/udp {} {}",
572 WslcContainerName,
573 HostTestPort1,
574 ContainerTestPort,
575 PythonImage.NameAndTag(),
576 GetPythonUdpEchoServerScript(ContainerTestPort)));
577 result.Verify({.Stderr = L"", .ExitCode = 0});
578
579 // Send a datagram from the host and verify the container echoes it back uppercased.
580 SendUdpAndReceive(HostTestPort1, "hello", "HELLO");
581
582 // Verify the UDP port mapping is reflected in the container inspect data.
583 auto inspectContainer = InspectContainer(WslcContainerName);
584 auto portKey = std::to_string(ContainerTestPort) + "/udp";
585 VERIFY_IS_TRUE(inspectContainer.Ports.contains(portKey));
586
587 auto portBindings = inspectContainer.Ports[portKey];
588 VERIFY_ARE_EQUAL(1u, portBindings.size());
589 VERIFY_ARE_EQUAL(std::to_string(HostTestPort1), portBindings[0].HostPort);
590 VERIFY_ARE_EQUAL("127.0.0.1", portBindings[0].HostIp);
591 }
592
593 // https://github.com/microsoft/WSL/issues/14433
594 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Port_HostIP)
595 {
596 // Start a container with a server listening on a port, bound to a specific host IP (127.0.0.1).
597 auto result = RunWslc(std::format(
598 L"container run -d --name {} -p 127.0.0.1:{}:{} {} {}",
599 WslcContainerName,
600 HostTestPort1,
601 ContainerTestPort,
602 PythonImage.NameAndTag(),
603 GetPythonHttpServerScript(ContainerTestPort)));
604 result.Verify({.Stderr = L"", .ExitCode = 0});
605
606 WaitForContainerOutput(WslcContainerName, "Serving HTTP on");
607
608 ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", HostTestPort1).c_str(), HTTP_STATUS_OK, true);
609
610 auto inspectContainer = InspectContainer(WslcContainerName);
611 auto portKey = std::to_string(ContainerTestPort) + "/tcp";
612 VERIFY_IS_TRUE(inspectContainer.Ports.contains(portKey));
613
614 auto portBindings = inspectContainer.Ports[portKey];
615 VERIFY_ARE_EQUAL(1u, portBindings.size());
616 VERIFY_ARE_EQUAL(std::to_string(HostTestPort1), portBindings[0].HostPort);
617 VERIFY_ARE_EQUAL("127.0.0.1", portBindings[0].HostIp);
618 }
619
620 WSLC_TEST_METHOD(WSLCE2E_Container_Run_HostLoopback)
621 {
622 VerifyHostLoopback("default", "host.wslc.internal", false);
623 VerifyHostLoopback("default", "host.wslc.internal", true);
624 VerifyHostLoopback("host.containers.internal", "host.containers.internal", false);
625 }
626
627 // Verifies that 'session.defaultBindingAddress: default' resolves to the built-in
628 // loopback default (127.0.0.1) for a published port specified without an explicit host IP.
629 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Port_DefaultBindingAddress_Default)
630 {
631 const auto settingsPath = wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"wslc" / L"settings.yaml";
632 HostFileChange settings(settingsPath, "session:\n defaultBindingAddress: default\n");
633
634 auto result = RunWslc(std::format(
635 L"container run -d --name {} -p {}:{} {} {}",
636 WslcContainerName,
637 HostTestPort1,
638 ContainerTestPort,
639 PythonImage.NameAndTag(),
640 GetPythonHttpServerScript(ContainerTestPort)));
641 result.Verify({.Stderr = L"", .ExitCode = 0});
642
643 WaitForContainerOutput(WslcContainerName, "Serving HTTP on");
644
645 ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", HostTestPort1).c_str(), HTTP_STATUS_OK, true);
646
647 auto inspectContainer = InspectContainer(WslcContainerName);
648 auto portKey = std::to_string(ContainerTestPort) + "/tcp";
649 VERIFY_IS_TRUE(inspectContainer.Ports.contains(portKey));
650
651 auto portBindings = inspectContainer.Ports[portKey];
652 VERIFY_ARE_EQUAL(1u, portBindings.size());
653 VERIFY_ARE_EQUAL(std::to_string(HostTestPort1), portBindings[0].HostPort);
654 VERIFY_ARE_EQUAL("127.0.0.1", portBindings[0].HostIp);
655 }
656
657 // Verifies that a configured 'session.defaultBindingAddress' overrides the loopback
658 // default for a published port specified without an explicit host IP.
659 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Port_DefaultBindingAddress_Override)
660 {
661 const auto hostIp = GetHostAdapterIpv4();
662 if (!hostIp.has_value())
663 {
664 WEX::Logging::Log::Comment(L"Skipping: no suitable non-loopback host IPv4 address was found.");
665 return;
666 }
667
668 const auto settingsPath = wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"wslc" / L"settings.yaml";
669 HostFileChange settings(settingsPath, std::format("session:\n defaultBindingAddress: {}\n", *hostIp));
670
671 auto result = RunWslc(std::format(
672 L"container run -d --name {} -p {}:{} {} {}",
673 WslcContainerName,
674 HostTestPort1,
675 ContainerTestPort,
676 PythonImage.NameAndTag(),
677 GetPythonHttpServerScript(ContainerTestPort)));
678 result.Verify({.Stderr = L"", .ExitCode = 0});
679
680 WaitForContainerOutput(WslcContainerName, "Serving HTTP on");
681
682 const auto hostIpWide = std::wstring(hostIp->begin(), hostIp->end());
683 ExpectHttpResponse(std::format(L"http://{}:{}", hostIpWide, HostTestPort1).c_str(), HTTP_STATUS_OK, true);
684
685 auto inspectContainer = InspectContainer(WslcContainerName);
686 auto portKey = std::to_string(ContainerTestPort) + "/tcp";
687 VERIFY_IS_TRUE(inspectContainer.Ports.contains(portKey));
688
689 auto portBindings = inspectContainer.Ports[portKey];
690 VERIFY_ARE_EQUAL(1u, portBindings.size());
691 VERIFY_ARE_EQUAL(std::to_string(HostTestPort1), portBindings[0].HostPort);
692 VERIFY_ARE_EQUAL(wsl::shared::string::WideToMultiByte(*hostIp), portBindings[0].HostIp);
693 }
694
695 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Port_TCP)
696 {
697 // Start a container with a simple server listening on a port
698 auto result = RunWslc(std::format(
699 L"container run -d --name {} -p {}:{} {} {}",
700 WslcContainerName,
701 HostTestPort1,
702 ContainerTestPort,
703 PythonImage.NameAndTag(),
704 GetPythonHttpServerScript(ContainerTestPort)));
705 result.Verify({.Stderr = L"", .ExitCode = 0});
706
707 // Wait for the in-container HTTP server to start listening before connecting.
708 WaitForContainerOutput(WslcContainerName, "Serving HTTP on");
709
710 // Verify we can connect to the server from the host side
711 ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", HostTestPort1).c_str(), HTTP_STATUS_OK, true);
712
713 // Verify the port mapping is correct in the container inspect data
714 auto inspectContainer = InspectContainer(WslcContainerName);
715 auto portKey = std::to_string(ContainerTestPort) + "/tcp";
716 VERIFY_IS_TRUE(inspectContainer.Ports.contains(portKey));
717
718 auto portBindings = inspectContainer.Ports[portKey];
719 VERIFY_ARE_EQUAL(1u, portBindings.size());
720 VERIFY_ARE_EQUAL(std::to_string(HostTestPort1), portBindings[0].HostPort);
721 VERIFY_ARE_EQUAL("127.0.0.1", portBindings[0].HostIp);
722 }
723
724 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Interactive_TTY)
725 {
726 VerifyContainerIsNotListed(WslcContainerName);
727
728 const auto& prompt = ">";
729 auto session = RunWslcInteractive(
730 std::format(L"container run -it -e PS1={} --name {} {} bash --norc", prompt, WslcContainerName, DebianImage.NameAndTag()));
731 VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running");
732
733 // 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.
734 session.IgnoreSequence(VT::BuildContainerAttachPrompt(prompt));
735
736 const auto& expectedPrompt = VT::BuildContainerPrompt(prompt, true);
737 session.ExpectStdout(expectedPrompt);
738
739 session.WriteLine("echo hello");
740 session.ExpectCommandEcho("echo hello");
741 session.ExpectStdout("hello\r\n");
742 session.ExpectStdout(expectedPrompt);
743
744 session.WriteLine("whoami");
745 session.ExpectCommandEcho("whoami");
746 session.ExpectStdout("root\r\n");
747 session.ExpectStdout(expectedPrompt);
748
749 auto exitCode = session.ExitAndVerifyNoErrors();
750 VERIFY_ARE_EQUAL(0, exitCode);
751 }
752
753 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Interactive_NoTTY)
754 {
755 VerifyContainerIsNotListed(WslcContainerName);
756
757 auto session = RunWslcInteractive(std::format(L"container run -i --name {} {} cat", WslcContainerName, DebianImage.NameAndTag()));
758 VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running");
759
760 session.WriteLine("test line 1");
761 session.ExpectStdout("test line 1\n");
762 session.WriteLine("test line 2");
763 session.ExpectStdout("test line 2\n");
764
765 // Close stdin to signal EOF to cat
766 session.CloseStdin();
767
768 // Wait for cat to exit with code 0
769 auto exitCode = session.Wait(10000);
770 VERIFY_ARE_EQUAL(0, exitCode, L"Cat should exit with code 0 after receiving EOF");
771 session.VerifyNoErrors();
772 }
773
774 WSLC_TEST_METHOD(WSLCE2E_Container_Run_InteractiveNoTTY_SelfExitingCommand)
775 {
776 // Same stdin-relay teardown deadlock as WSLCE2E_Container_Exec_InteractiveNoTTY_SelfExitingCommand (see it
777 // for the root cause), but via `container run -i`. run and exec share the client relay (both route through
778 // AttachToCurrentConsole), so the hang is not exec-specific. The test requires run to exit with the client
779 // still holding stdin open; RunWslcInteractive supplies stdin as a synchronous (non-overlapped) pipe, the
780 // case that triggers the bug.
781 VerifyContainerIsNotListed(WslcContainerName);
782
783 auto session =
784 RunWslcInteractive(std::format(L"container run -i --name {} {} echo hello", WslcContainerName, DebianImage.NameAndTag()));
785
786 session.ExpectStdout("hello\n");
787
788 // Generous timeout: it only bounds the failure (hang) path.
789 auto exitCode = session.Wait(120000);
790 VERIFY_ARE_EQUAL(0, exitCode, L"echo should exit with code 0 without the client closing stdin");
791
792 // Closing stdin after exit must stay a clean no-op.
793 session.CloseStdin();
794 session.VerifyNoErrors();
795 }
796
797 WSLC_TEST_METHOD(WSLCE2E_Container_Run_InteractiveTTY_SelfExitingCommand)
798 {
799 // TTY counterpart of the above: `-t` routes through ConsoleService::RelayInteractiveTty, a separate
800 // stdin-worker teardown from the non-TTY path, so it could regress independently. Guards the TTY run path.
801 VerifyContainerIsNotListed(WslcContainerName);
802
803 auto session =
804 RunWslcInteractive(std::format(L"container run -it --name {} {} echo hello", WslcContainerName, DebianImage.NameAndTag()));
805
806 // The TTY translates the trailing LF to CRLF.
807 session.ExpectStdout("hello\r\n");
808
809 auto exitCode = session.Wait(120000);
810 VERIFY_ARE_EQUAL(0, exitCode, L"echo should exit with code 0 without the client closing stdin");
811
812 session.CloseStdin();
813 session.VerifyNoErrors();
814 }
815
816 WSLC_TEST_METHOD(WSLCE2E_Container_Run_PseudoConsole_TerminalSize)
817 {
818 VerifyContainerIsNotListed(WslcContainerName);
819
820 constexpr SHORT columns = 42;
821 constexpr SHORT rows = 43;
822 const auto commandLine = std::format(
823 L"container run --rm -it --name {} {} /bin/sh -c \"while true; do stty size; sleep 1; done\"",
824 WslcContainerName,
825 DebianImage.NameAndTag());
826
827 auto session = RunWslcInteractive(commandLine, ElevationType::Elevated, PseudoConsole{columns, rows});
828 VerifyPseudoConsoleTtySize(session, columns, rows);
829 }
830
831 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Tmpfs)
832 {
833 auto result = RunWslc(std::format(
834 L"container run --rm --tmpfs /wslc-tmpfs {} sh -c \"echo -n 'tmpfs_test' > /wslc-tmpfs/data && cat "
835 L"/wslc-tmpfs/data\"",
836 DebianImage.NameAndTag()));
837 result.Verify({.Stdout = L"tmpfs_test", .Stderr = L"", .ExitCode = 0});
838 }
839
840 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Tmpfs_With_Options)
841 {
842 auto result = RunWslc(std::format(
843 L"container run --rm --tmpfs /wslc-tmpfs:size=64k {} sh -c \"mount | grep -q ' on /wslc-tmpfs type tmpfs ' && echo "
844 L"mounted\"",
845 DebianImage.NameAndTag()));
846 result.Verify({.Stdout = L"mounted\n", .Stderr = L"", .ExitCode = 0});
847 }
848
849 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Tmpfs_Multiple_With_Options)
850 {
851 auto result = RunWslc(std::format(
852 L"container run --rm --tmpfs /wslc-tmpfs1:size=64k --tmpfs /wslc-tmpfs2:size=128k {} sh -c \"mount | grep -q ' on "
853 L"/wslc-tmpfs1 type tmpfs ' && mount | grep -q ' on /wslc-tmpfs2 type tmpfs ' && echo mounted\"",
854 DebianImage.NameAndTag()));
855 result.Verify({.Stdout = L"mounted\n", .Stderr = L"", .ExitCode = 0});
856 }
857
858 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Tmpfs_RelativePath_Fails)
859 {
860 auto result = RunWslc(std::format(L"container run --rm --tmpfs wslc-tmpfs {}", DebianImage.NameAndTag()));
861 result.Verify({.Stdout = L"", .ExitCode = 1});
862 VERIFY_IS_TRUE(result.StderrContainsSubstring(
863 Localization::WSLCCLI_InvalidTmpfsError(L"wslc-tmpfs", Localization::WSLCCLI_MountTargetAbsoluteError())));
864 }
865
866 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Tmpfs_EmptyDestination_Fails)
867 {
868 auto result = RunWslc(std::format(L"container run --rm --tmpfs :size=64k {}", DebianImage.NameAndTag()));
869 result.Verify({.Stdout = L"", .ExitCode = 1});
870 VERIFY_IS_TRUE(result.StderrContainsSubstring(
871 Localization::WSLCCLI_InvalidTmpfsError(L":size=64k", Localization::WSLCCLI_MountTargetRequiredError())));
872 }
873
874 WSLC_TEST_METHOD(WSLCE2E_Container_Run_WorkDir)
875 {
876 auto result = RunWslc(std::format(L"container run --rm --workdir /tmp {} pwd", DebianImage.NameAndTag()));
877 result.Verify({.Stdout = L"/tmp\n", .Stderr = L"", .ExitCode = 0});
878 }
879
880 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Hostname)
881 {
882 auto result = RunWslc(std::format(L"container run --rm --hostname my-test-host {} hostname", DebianImage.NameAndTag()));
883 result.Verify({.Stdout = L"my-test-host\n", .Stderr = L"", .ExitCode = 0});
884 }
885
886 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Domainname)
887 {
888 auto result = RunWslc(std::format(L"container run --rm --domainname my-test-domain {} dnsdomainname", DebianImage.NameAndTag()));
889 result.Verify({.Stdout = L"my-test-domain\n", .Stderr = L"", .ExitCode = 0});
890 }
891
892 WSLC_TEST_METHOD(WSLCE2E_Container_Run_DNS)
893 {
894 auto result =
895 RunWslc(std::format(L"container run --rm --dns 1.1.1.1 --dns 8.8.8.8 {} cat /etc/resolv.conf", DebianImage.NameAndTag()));
896 result.Verify({.Stderr = L"", .ExitCode = 0});
897 VERIFY_IS_TRUE(result.Stdout->find(L"nameserver 1.1.1.1") != std::wstring::npos);
898 VERIFY_IS_TRUE(result.Stdout->find(L"nameserver 8.8.8.8") != std::wstring::npos);
899 }
900
901 WSLC_TEST_METHOD(WSLCE2E_Container_Run_DNSSearch)
902 {
903 auto result = RunWslc(std::format(
904 L"container run --rm --dns-search example.com --dns-search test.local {} cat /etc/resolv.conf", DebianImage.NameAndTag()));
905 result.Verify({.Stderr = L"", .ExitCode = 0});
906 VERIFY_IS_TRUE(result.Stdout->find(L"search example.com test.local") != std::wstring::npos);
907 }
908
909 WSLC_TEST_METHOD(WSLCE2E_Container_Run_DNSOption)
910 {
911 auto result = RunWslc(std::format(
912 L"container run --rm --dns-option ndots:5 --dns-option timeout:3 {} cat /etc/resolv.conf", DebianImage.NameAndTag()));
913 result.Verify({.Stderr = L"", .ExitCode = 0});
914 VERIFY_IS_TRUE(result.Stdout->find(L"options ndots:5 timeout:3") != std::wstring::npos);
915 }
916
917 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Network_DefaultIsBridge)
918 {
919 auto result = RunWslc(std::format(L"container run --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
920 result.Verify({.Stderr = L"", .ExitCode = 0});
921
922 const auto inspect = InspectContainer(WslcContainerName);
923 VERIFY_ARE_EQUAL(std::string("bridge"), inspect.HostConfig.NetworkMode);
924 }
925
926 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Network_HostMode_Rejected)
927 {
928 auto result =
929 RunWslc(std::format(L"container run --name {} --network host {} true", WslcContainerName, DebianImage.NameAndTag()));
930 result.Verify({.Stdout = L"", .Stderr = wsl::shared::Localization::WSLCCLI_NetworkHostModeNotSupportedError() + L"\r\n", .ExitCode = 1});
931 VerifyContainerIsNotListed(WslcContainerName);
932 }
933
934 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Network_UserDefinedNetwork)
935 {
936 auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
937 result.Verify({.Stderr = L"", .ExitCode = 0});
938 auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
939
940 result = RunWslc(std::format(
941 L"container run --name {} --network {} {} true", WslcContainerName, TestNetworkName, DebianImage.NameAndTag()));
942 result.Verify({.Stderr = L"", .ExitCode = 0});
943
944 const auto inspect = InspectContainer(WslcContainerName);
945 VERIFY_ARE_EQUAL(wsl::shared::string::WideToMultiByte(TestNetworkName), inspect.HostConfig.NetworkMode);
946 VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(wsl::shared::string::WideToMultiByte(TestNetworkName)));
947 }
948
949 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Network_EmptyValue_Rejected)
950 {
951 auto result =
952 RunWslc(std::format(L"container run --rm --network \"\" --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
953 result.Verify({.Stdout = L"", .ExitCode = 1});
954 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid network value: network name cannot be empty or whitespace"));
955 }
956
957 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Network_NonexistentNetwork_Rejected)
958 {
959 auto result = RunWslc(std::format(
960 L"container run --rm --network does-not-exist --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
961 result.Verify({.Stderr = FormatErrorMessage(L"Network not found: 'does-not-exist'", L"WSLC_E_NETWORK_NOT_FOUND"), .ExitCode = 1});
962 }
963
964 WSLC_TEST_METHOD(WSLCE2E_Container_Run_NetworkAlias_Success)
965 {
966 auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
967 result.Verify({.Stderr = L"", .ExitCode = 0});
968 auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
969
970 result = RunWslc(std::format(
971 L"container run --name {} --network {} --network-alias db {} true", WslcContainerName, TestNetworkName, DebianImage.NameAndTag()));
972 result.Verify({.Stderr = L"", .ExitCode = 0});
973
974 const auto inspect = InspectContainer(WslcContainerName);
975 const auto networkName = wsl::shared::string::WideToMultiByte(TestNetworkName);
976 VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
977 const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
978 VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "db") != endpoint.Aliases.end());
979 }
980
981 WSLC_TEST_METHOD(WSLCE2E_Container_Run_NetworkAlias_DockerStyleMultiNetwork_Success)
982 {
983 const auto secondNetworkName = TestNetworkName + L"-2";
984 EnsureNetworkDoesNotExist(secondNetworkName);
985
986 auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
987 result.Verify({.Stderr = L"", .ExitCode = 0});
988 auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
989
990 result = RunWslc(std::format(L"network create --driver bridge {}", secondNetworkName));
991 result.Verify({.Stderr = L"", .ExitCode = 0});
992 auto cleanupSecondNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(secondNetworkName); });
993
994 result = RunWslc(std::format(
995 L"container run --name {} --network name={},alias=db,alias=primary "
996 L"--network name={},alias=cache,alias=replica {} true",
997 WslcContainerName,
998 TestNetworkName,
999 secondNetworkName,
1000 DebianImage.NameAndTag()));
1001 result.Verify({.Stderr = L"", .ExitCode = 0});
1002
1003 const auto inspect = InspectContainer(WslcContainerName);
1004 const auto networkName = wsl::shared::string::WideToMultiByte(TestNetworkName);
1005 const auto secondNetwork = wsl::shared::string::WideToMultiByte(secondNetworkName);
1006 VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
1007 VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(secondNetwork));
1008 const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
1009 const auto& secondEndpoint = inspect.NetworkSettings.Networks.at(secondNetwork);
1010 VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "db") != endpoint.Aliases.end());
1011 VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "primary") != endpoint.Aliases.end());
1012 VERIFY_IS_TRUE(std::ranges::find(secondEndpoint.Aliases, "cache") != secondEndpoint.Aliases.end());
1013 VERIFY_IS_TRUE(std::ranges::find(secondEndpoint.Aliases, "replica") != secondEndpoint.Aliases.end());
1014 }
1015
1016 WSLC_TEST_METHOD(WSLCE2E_Container_Run_NetworkAlias_NoNetwork_Rejected)
1017 {
1018 auto result =
1019 RunWslc(std::format(L"container run --rm --network-alias db --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
1020 result.Verify(
1021 {.Stderr = FormatErrorMessage(
1022 L"Network aliases require a user-defined network. Use --network to specify one.", L"E_INVALIDARG"),
1023 .ExitCode = 1});
1024 }
1025
1026 WSLC_TEST_METHOD(WSLCE2E_Container_Run_NetworkAlias_NoneMode_Rejected)
1027 {
1028 auto result = RunWslc(std::format(
1029 L"container run --rm --network none --network-alias db --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
1030 result.Verify(
1031 {.Stderr = FormatErrorMessage(
1032 L"Network aliases require a user-defined network. Use --network to specify one.", L"E_INVALIDARG"),
1033 .ExitCode = 1});
1034 }
1035
1036 WSLC_TEST_METHOD(WSLCE2E_Container_Run_NetworkAlias_MultipleNetworks_Rejected)
1037 {
1038 auto result = RunWslc(std::format(
1039 L"container run --rm --network bridge --network bridge --network-alias db --name {} {} true",
1040 WslcContainerName,
1041 DebianImage.NameAndTag()));
1042 result.Verify({.Stdout = L"", .ExitCode = 1});
1043 VERIFY_IS_TRUE(result.StderrContainsSubstring(
1044 wsl::shared::Localization::MessageWslcAliasAmbiguousWithMultipleNetworks() + L"\r\nError code: E_INVALIDARG"));
1045 }
1046
1047 WSLC_TEST_METHOD(WSLCE2E_Container_Run_NetworkAlias_EmptyValue_Rejected)
1048 {
1049 auto result = RunWslc(
1050 std::format(L"container run --rm --network-alias \"\" --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
1051 result.Verify({.Stdout = L"", .ExitCode = 1});
1052 VERIFY_IS_TRUE(
1053 result.StderrContainsSubstring(L"Invalid network-alias value: network alias cannot be empty or whitespace"));
1054 }
1055
1056 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ip_Success)
1057 {
1058 const std::wstring subnet = L"172.73.0.0/16";
1059 const std::wstring ipAddress = L"172.73.0.42";
1060
1061 auto result = RunWslc(std::format(L"network create --driver bridge --subnet {} {}", subnet, TestNetworkName));
1062 result.Verify({.Stderr = L"", .ExitCode = 0});
1063 auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
1064
1065 result = RunWslc(std::format(
1066 L"container run -d --name {} --network {} --ip {} {} sleep infinity",
1067 WslcContainerName,
1068 TestNetworkName,
1069 ipAddress,
1070 DebianImage.NameAndTag()));
1071 result.Verify({.Stderr = L"", .ExitCode = 0});
1072 // Registered after the network so it runs first; the network cannot be deleted while the container holds an endpoint.
1073 auto cleanupContainer = wil::scope_exit([&] { EnsureContainerDoesNotExist(WslcContainerName); });
1074
1075 const auto inspect = InspectContainer(WslcContainerName);
1076 const auto networkName = wsl::shared::string::WideToMultiByte(TestNetworkName);
1077 const auto expectedIp = wsl::shared::string::WideToMultiByte(ipAddress);
1078 VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
1079 const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
1080 VERIFY_ARE_EQUAL(expectedIp, endpoint.IPAddress);
1081 VERIFY_IS_TRUE(endpoint.IPAMConfig.has_value());
1082 VERIFY_ARE_EQUAL(expectedIp, endpoint.IPAMConfig->IPv4Address);
1083 }
1084
1085 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ip_NoNetwork_Rejected)
1086 {
1087 const std::wstring ipAddress = L"172.73.0.42";
1088
1089 auto result =
1090 RunWslc(std::format(L"container run --rm --ip {} --name {} {} true", ipAddress, WslcContainerName, DebianImage.NameAndTag()));
1091 result.Verify(
1092 {.Stderr = FormatErrorMessage(wsl::shared::Localization::MessageWslcIpRequiresUserDefinedNetwork(), L"E_INVALIDARG"),
1093 .ExitCode = 1});
1094 }
1095
1096 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ip_BridgeMode_Rejected)
1097 {
1098 const std::wstring ipAddress = L"172.73.0.42";
1099
1100 auto result = RunWslc(std::format(
1101 L"container run --rm --network bridge --ip {} --name {} {} true", ipAddress, WslcContainerName, DebianImage.NameAndTag()));
1102 result.Verify(
1103 {.Stderr = FormatErrorMessage(wsl::shared::Localization::MessageWslcIpRequiresUserDefinedNetwork(), L"E_INVALIDARG"),
1104 .ExitCode = 1});
1105 }
1106
1107 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ip_MultipleNetworks_Rejected)
1108 {
1109 const std::wstring ipAddress = L"172.73.0.42";
1110
1111 auto result = RunWslc(std::format(
1112 L"container run --rm --network bridge --network bridge --ip {} --name {} {} true",
1113 ipAddress,
1114 WslcContainerName,
1115 DebianImage.NameAndTag()));
1116 result.Verify({.Stdout = L"", .ExitCode = 1});
1117 VERIFY_IS_TRUE(result.StderrContainsSubstring(
1118 wsl::shared::Localization::MessageWslcIpAmbiguousWithMultipleNetworks() + L"\r\nError code: E_INVALIDARG"));
1119 }
1120
1121 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ip_InvalidValue_Rejected)
1122 {
1123 const std::wstring badIp = L"not-an-ip";
1124
1125 auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
1126 result.Verify({.Stderr = L"", .ExitCode = 0});
1127 auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
1128
1129 result = RunWslc(std::format(
1130 L"container run --rm --network {} --ip {} --name {} {} true", TestNetworkName, badIp, WslcContainerName, DebianImage.NameAndTag()));
1131 result.Verify({.Stdout = L"", .ExitCode = 1});
1132 VERIFY_IS_TRUE(result.Stderr.has_value());
1133 VerifyPatternMatch(
1134 wsl::shared::string::WideToMultiByte(result.Stderr.value()),
1135 std::format("*Invalid IP address '{}'*", wsl::shared::string::WideToMultiByte(badIp)));
1136 }
1137
1138 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Volume_NamedVolume_Success)
1139 {
1140 // Create a named volume
1141 auto result = RunWslc(std::format(L"volume create {}", WslcVolumeName));
1142 result.Verify({.Stderr = L"", .ExitCode = 0});
1143
1144 // Create a container with --rm that uses the named volume and writes a file to it
1145 result = RunWslc(std::format(
1146 L"container run --rm --volume {}:/data {} sh -c \"echo -n 'WSLC Named Volume Test' > /data/test.txt\"",
1147 WslcVolumeName,
1148 DebianImage.NameAndTag()));
1149 result.Verify({.Stderr = L"", .ExitCode = 0});
1150
1151 // Create another container that mounts the same named volume and verify the file content
1152 result = RunWslc(std::format(L"container run --rm --volume {}:/data {} cat /data/test.txt", WslcVolumeName, DebianImage.NameAndTag()));
1153 result.Verify({.Stdout = L"WSLC Named Volume Test", .Stderr = L"", .ExitCode = 0});
1154 }
1155
1156 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Volume_NamedVolume_AutoCreate)
1157 {
1158 auto result = RunWslc(std::format(
1159 L"container run --rm --volume {}:/data {} sh -c \"echo -n 'WSLC Named Volume Test' > /data/test.txt\"",
1160 WslcVolumeName,
1161 DebianImage.NameAndTag()));
1162 result.Verify({.Stderr = L"", .ExitCode = 0});
1163
1164 // Verify the volume was auto-created by removing it (fails if it doesn't exist).
1165 result = RunWslc(std::format(L"volume rm {}", WslcVolumeName));
1166 result.Verify({.Stderr = L"", .ExitCode = 0});
1167 }
1168
1169 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Mount_Tmpfs_Success)
1170 {
1171 auto result = RunWslc(std::format(
1172 L"container run --rm --mount type=tmpfs,target=/wslc-tmpfs {} sh -c \"echo -n 'tmpfs_test' > /wslc-tmpfs/data && cat "
1173 L"/wslc-tmpfs/data\"",
1174 DebianImage.NameAndTag()));
1175 result.Verify({.Stdout = L"tmpfs_test", .Stderr = L"", .ExitCode = 0});
1176 }
1177
1178 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Mount_Bind_Success)
1179 {
1180 WriteTestFileContent(EnvTestFile1, "WSLC Mount Bind Test");
1181
1182 const auto hostDirectory = EnvTestFile1.parent_path();
1183 const auto fileName = EnvTestFile1.filename().wstring();
1184 auto result = RunWslc(std::format(
1185 L"container run --rm --mount \"type=bind,source={},target=/data,readonly\" {} cat /data/{}",
1186 hostDirectory.wstring(),
1187 DebianImage.NameAndTag(),
1188 fileName));
1189 result.Verify({.Stdout = L"WSLC Mount Bind Test", .Stderr = L"", .ExitCode = 0});
1190 }
1191
1192 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Volume_Bind_VirtioFs_MapShared_Success)
1193 {
1194 const auto hostDirectory = EnvTestFile1.parent_path();
1195 const auto fileName = EnvTestFile1.filename().wstring();
1196 VERIFY_IS_TRUE(DeleteFileW(EnvTestFile1.c_str()));
1197
1198 constexpr auto mapSharedScript =
1199 LR"PY(
1200 import mmap
1201 import os
1202 import sys
1203
1204 with open('/proc/mounts', encoding='utf-8') as mounts_file:
1205 mounts = (line.split() for line in mounts_file)
1206 if not any(fields[1:3] == ['/data', 'virtiofs'] for fields in mounts):
1207 raise RuntimeError('/data is not mounted as virtiofs')
1208
1209 fd = os.open(sys.argv[1], os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_CLOEXEC, 0o777)
1210 os.ftruncate(fd, 32 * 1024)
1211 with mmap.mmap(fd, 32 * 1024, flags=mmap.MAP_SHARED, prot=mmap.PROT_READ | mmap.PROT_WRITE) as mapping:
1212 mapping[0:1] = b'W'
1213 mapping.flush()
1214 if os.pread(fd, 1, 0) != b'W':
1215 raise RuntimeError('MAP_SHARED write was not visible through the file')
1216 )PY";
1217
1218 auto result = RunWslc(std::format(
1219 L"container run --rm --volume \"{}:/data\" {} python3 -c \"{}\" /data/{}", hostDirectory.wstring(), PythonImage.NameAndTag(), mapSharedScript, fileName));
1220 result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
1221 VERIFY_IS_TRUE(std::filesystem::exists(EnvTestFile1));
1222 VERIFY_ARE_EQUAL(32ull * 1024, std::filesystem::file_size(EnvTestFile1));
1223 VERIFY_ARE_EQUAL(L'W', ReadFileContent(EnvTestFile1.wstring())[0]);
1224 }
1225
1226 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Mount_Volume_Success)
1227 {
1228 auto result = RunWslc(std::format(
1229 L"container run --rm --mount type=volume,source={},target=/data {} sh -c \"echo -n 'WSLC Mount Volume Test' > "
1230 L"/data/test.txt\"",
1231 WslcVolumeName,
1232 DebianImage.NameAndTag()));
1233 result.Verify({.Stderr = L"", .ExitCode = 0});
1234
1235 result = RunWslc(std::format(
1236 L"container run --rm --mount type=volume,source={},target=/data {} cat /data/test.txt", WslcVolumeName, DebianImage.NameAndTag()));
1237 result.Verify({.Stdout = L"WSLC Mount Volume Test", .Stderr = L"", .ExitCode = 0});
1238 }
1239
1240 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Mount_ReadOnly_IsReadOnly)
1241 {
1242 auto result = RunWslc(std::format(
1243 L"container run --rm --mount type=volume,source={},target=/data {} sh -c \"echo -n original > /data/value\"",
1244 WslcVolumeName,
1245 DebianImage.NameAndTag()));
1246 result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
1247
1248 result = RunWslc(std::format(
1249 L"container run --rm --mount type=volume,source={},target=/data,readonly {} sh -c \"echo changed > /data/value\"",
1250 WslcVolumeName,
1251 DebianImage.NameAndTag()));
1252 result.Verify({.Stdout = L"", .Stderr = L"sh: 1: cannot create /data/value: Read-only file system\n", .ExitCode = 2});
1253
1254 result = RunWslc(std::format(
1255 L"container run --rm --mount type=volume,source={},target=/data {} cat /data/value", WslcVolumeName, DebianImage.NameAndTag()));
1256 result.Verify({.Stdout = L"original", .Stderr = L"", .ExitCode = 0});
1257 }
1258
1259 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Mount_InvalidType_Fails)
1260 {
1261 constexpr auto mount = L"type=bogus,target=/x";
1262 auto result = RunWslc(std::format(L"container run --rm --mount {} {} true", mount, DebianImage.NameAndTag()));
1263 result.Verify({.Stdout = L"", .ExitCode = 1});
1264 VERIFY_IS_TRUE(result.StderrContainsSubstring(
1265 Localization::WSLCCLI_UnsupportedMountError(mount, Localization::WSLCCLI_MountTypeUnsupportedError(L"bogus"))));
1266 }
1267
1268 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Mount_DuplicateDestination_Fails)
1269 {
1270 auto result = RunWslc(std::format(
1271 L"container run --rm --name {} --mount type=tmpfs,target=/data --mount type=tmpfs,target=/data/ {} true",
1272 WslcContainerName,
1273 DebianImage.NameAndTag()));
1274 result.Verify(
1275 {.Stdout = L"",
1276 .Stderr = FormatErrorMessage(Localization::WSLCCLI_DuplicateMountDestinationError(L"/data"), L"E_INVALIDARG"),
1277 .ExitCode = 1});
1278 EnsureContainerDoesNotExist(WslcContainerName);
1279 }
1280
1281 WSLC_TEST_METHOD(WSLCE2E_Container_Run_WithLabel_Success)
1282 {
1283 auto result = RunWslc(std::format(
1284 L"container run --name {} --label A=1 --label B=2 {} echo hello", WslcContainerName, DebianImage.NameAndTag()));
1285 result.Verify({.Stdout = L"hello\n", .Stderr = L"", .ExitCode = 0});
1286
1287 auto inspect = InspectContainer(WslcContainerName);
1288 VERIFY_ARE_EQUAL("1", inspect.Labels["A"]);
1289 VERIFY_ARE_EQUAL("2", inspect.Labels["B"]);
1290 }
1291
1292 WSLC_TEST_METHOD(WSLCE2E_Container_Run_StopSignal)
1293 {
1294 constexpr int ExpectedExitCode = 42;
1295 auto result = RunWslc(std::format(
1296 LR"(container run -d --stop-signal SIGUSR1 --name {} {} bash -c "trap 'exit {}' SIGUSR1; while true; do sleep 1; done")",
1297 WslcContainerName,
1298 DebianImage.NameAndTag(),
1299 ExpectedExitCode));
1300 result.Verify({.Stderr = L"", .ExitCode = 0});
1301
1302 result = RunWslc(std::format(L"container stop {}", WslcContainerName));
1303 result.Verify({.Stderr = L"", .ExitCode = 0});
1304
1305 const auto inspect = InspectContainer(WslcContainerName);
1306 VERIFY_IS_FALSE(inspect.State.Running);
1307 VERIFY_ARE_EQUAL(ExpectedExitCode, inspect.State.ExitCode);
1308 }
1309
1310 WSLC_TEST_METHOD(WSLCE2E_Container_Run_StopTimeout)
1311 {
1312 // A positive value is forwarded to the container configuration.
1313 {
1314 constexpr int ExpectedStopTimeout = 25;
1315 auto result = RunWslc(std::format(
1316 L"container run -d --stop-timeout {} --name {} {} sleep infinity",
1317 ExpectedStopTimeout,
1318 WslcContainerName,
1319 DebianImage.NameAndTag()));
1320 result.Verify({.Stderr = L"", .ExitCode = 0});
1321
1322 const auto inspect = InspectContainer(WslcContainerName);
1323 VERIFY_IS_TRUE(inspect.Config.StopTimeout.has_value());
1324 VERIFY_ARE_EQUAL(ExpectedStopTimeout, inspect.Config.StopTimeout.value());
1325 EnsureContainerDoesNotExist(WslcContainerName);
1326 }
1327
1328 // A value of 0 (stop the container immediately) is a valid, explicit timeout.
1329 {
1330 auto result = RunWslc(std::format(
1331 L"container run -d --stop-timeout 0 --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
1332 result.Verify({.Stderr = L"", .ExitCode = 0});
1333
1334 const auto inspect = InspectContainer(WslcContainerName);
1335 VERIFY_IS_TRUE(inspect.Config.StopTimeout.has_value());
1336 VERIFY_ARE_EQUAL(0, inspect.Config.StopTimeout.value());
1337 EnsureContainerDoesNotExist(WslcContainerName);
1338 }
1339
1340 // A value of -1 means "no timeout"; it is a valid, explicit value forwarded to the configuration.
1341 {
1342 auto result = RunWslc(std::format(
1343 L"container run -d --stop-timeout -1 --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
1344 result.Verify({.Stderr = L"", .ExitCode = 0});
1345
1346 const auto inspect = InspectContainer(WslcContainerName);
1347 VERIFY_IS_TRUE(inspect.Config.StopTimeout.has_value());
1348 VERIFY_ARE_EQUAL(-1, inspect.Config.StopTimeout.value());
1349 EnsureContainerDoesNotExist(WslcContainerName);
1350 }
1351
1352 // When --stop-timeout is not specified, no timeout is forwarded to the container configuration.
1353 {
1354 auto result =
1355 RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
1356 result.Verify({.Stderr = L"", .ExitCode = 0});
1357
1358 const auto inspect = InspectContainer(WslcContainerName);
1359 VERIFY_IS_FALSE(inspect.Config.StopTimeout.has_value());
1360 EnsureContainerDoesNotExist(WslcContainerName);
1361 }
1362 }
1363
1364 WSLC_TEST_METHOD(WSLCE2E_Container_Run_StopTimeout_Invalid)
1365 {
1366 {
1367 auto result =
1368 RunWslc(std::format(L"container run --rm --stop-timeout abc --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1369 result.Verify({.Stdout = L"", .ExitCode = 1});
1370 VERIFY_IS_TRUE(result.StderrContainsSubstring(
1371 wsl::shared::Localization::WSLCCLI_InvalidIntegerArgumentError(L"stop-timeout", L"abc")));
1372 EnsureContainerDoesNotExist(WslcContainerName);
1373 }
1374
1375 {
1376 auto result =
1377 RunWslc(std::format(L"container run --rm --stop-timeout -2 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1378 result.Verify({.Stderr = FormatErrorMessage(L"Invalid stop timeout value: -2", L"E_INVALIDARG"), .ExitCode = 1});
1379 EnsureContainerDoesNotExist(WslcContainerName);
1380 }
1381
1382 // Validate that the correct error is displayed if the user passes the exact 'WSLC_STOP_TIMEOUT_DEFAULT' value.
1383 {
1384 auto result = RunWslc(std::format(
1385 L"container run --rm --stop-timeout {} --name {} {}", WSLC_STOP_TIMEOUT_DEFAULT, WslcContainerName, DebianImage.NameAndTag()));
1386 result.Verify({.Stderr = FormatErrorMessage(L"Invalid stop timeout value: -2147483648", L"E_INVALIDARG"), .ExitCode = 1});
1387 EnsureContainerDoesNotExist(WslcContainerName);
1388 }
1389 }
1390
1391 WSLC_TEST_METHOD(WSLCE2E_Container_Run_ShmSize)
1392 {
1393 auto result = RunWslc(std::format(
1394 L"container run --rm --shm-size 1.5G {} sh -c \"df -B1 /dev/shm --output=size | sed 1d\"", DebianImage.NameAndTag()));
1395 result.Verify({.Stdout = L"1610612736\n", .Stderr = L"", .ExitCode = 0});
1396 }
1397
1398 WSLC_TEST_METHOD(WSLCE2E_Container_Run_ShmSize_Invalid)
1399 {
1400 {
1401 auto result =
1402 RunWslc(std::format(L"container run --rm --shm-size invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1403 result.Verify({.Stdout = L"", .ExitCode = 1});
1404 VERIFY_IS_TRUE(result.StderrContainsSubstring(
1405 wsl::shared::Localization::WSLCCLI_InvalidMemorySizeError(L"shm-size", L"invalid")));
1406 EnsureContainerDoesNotExist(WslcContainerName);
1407 }
1408
1409 {
1410 auto result =
1411 RunWslc(std::format(L"container run --rm --shm-size 128X --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1412 result.Verify({.Stdout = L"", .ExitCode = 1});
1413 VERIFY_IS_TRUE(
1414 result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidMemorySizeError(L"shm-size", L"128X")));
1415 EnsureContainerDoesNotExist(WslcContainerName);
1416 }
1417 }
1418
1419 WSLC_TEST_METHOD(WSLCE2E_Container_Run_HealthCheck)
1420 {
1421 // All health-check options are forwarded to the container configuration.
1422 {
1423 auto result = RunWslc(std::format(
1424 LR"(container run -d --health-cmd "exit 0" --health-interval 5s --health-timeout 3s --health-retries 2 --health-start-period 1s --name {} {} sleep infinity)",
1425 WslcContainerName,
1426 DebianImage.NameAndTag()));
1427 result.Verify({.Stderr = L"", .ExitCode = 0});
1428
1429 const auto inspect = InspectContainer(WslcContainerName);
1430 VERIFY_IS_TRUE(inspect.Config.Healthcheck.has_value());
1431
1432 const auto& health = inspect.Config.Healthcheck.value();
1433 VERIFY_IS_TRUE(health.Test.has_value());
1434 const std::vector<std::string> expectedTest{"CMD-SHELL", "exit 0"};
1435 VERIFY_ARE_EQUAL(expectedTest, health.Test.value());
1436
1437 // Durations are reported in nanoseconds.
1438 VERIFY_ARE_EQUAL(5'000'000'000LL, health.Interval.value_or(0));
1439 VERIFY_ARE_EQUAL(3'000'000'000LL, health.Timeout.value_or(0));
1440 VERIFY_ARE_EQUAL(1'000'000'000LL, health.StartPeriod.value_or(0));
1441 VERIFY_ARE_EQUAL(2, health.Retries.value_or(0));
1442 EnsureContainerDoesNotExist(WslcContainerName);
1443 }
1444
1445 // When no health option is specified, no health check is forwarded.
1446 {
1447 auto result =
1448 RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
1449 result.Verify({.Stderr = L"", .ExitCode = 0});
1450
1451 const auto inspect = InspectContainer(WslcContainerName);
1452 VERIFY_IS_FALSE(inspect.Config.Healthcheck.has_value());
1453 EnsureContainerDoesNotExist(WslcContainerName);
1454 }
1455 }
1456
1457 WSLC_TEST_METHOD(WSLCE2E_Container_Run_HealthCheck_Invalid)
1458 {
1459 auto result = RunWslc(
1460 std::format(L"container run --rm --health-timeout invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1461 result.Verify({.Stdout = L"", .ExitCode = 1});
1462 VERIFY_IS_TRUE(result.StderrContainsSubstring(
1463 wsl::shared::Localization::WSLCCLI_InvalidDurationError(L"health-timeout", L"invalid")));
1464 EnsureContainerDoesNotExist(WslcContainerName);
1465 }
1466
1467 WSLC_TEST_METHOD(WSLCE2E_Container_Run_HealthStatus_Healthy)
1468 {
1469 // A health check that always succeeds should drive the container to the "healthy" state.
1470 auto result = RunWslc(std::format(
1471 LR"(container run -d --health-cmd "exit 0" --health-interval 1s --health-timeout 3s --health-retries 1 --name {} {} sleep infinity)",
1472 WslcContainerName,
1473 DebianImage.NameAndTag()));
1474 result.Verify({.Stderr = L"", .ExitCode = 0});
1475
1476 const auto health = WaitForContainerHealth(WslcContainerName, "healthy");
1477 VERIFY_ARE_EQUAL(0, health.FailingStreak);
1478 VERIFY_IS_FALSE(health.Log.empty());
1479 VERIFY_ARE_EQUAL(0, health.Log.back().ExitCode);
1480
1481 EnsureContainerDoesNotExist(WslcContainerName);
1482 }
1483
1484 WSLC_TEST_METHOD(WSLCE2E_Container_Run_HealthStatus_Unhealthy)
1485 {
1486 // A health check that always fails should drive the container to the "unhealthy" state.
1487 auto result = RunWslc(std::format(
1488 LR"(container run -d --health-cmd "exit 1" --health-interval 1s --health-timeout 3s --health-retries 1 --name {} {} sleep infinity)",
1489 WslcContainerName,
1490 DebianImage.NameAndTag()));
1491 result.Verify({.Stderr = L"", .ExitCode = 0});
1492
1493 const auto health = WaitForContainerHealth(WslcContainerName, "unhealthy");
1494 VERIFY_IS_TRUE(health.FailingStreak >= 1);
1495 VERIFY_IS_FALSE(health.Log.empty());
1496 VERIFY_ARE_EQUAL(1, health.Log.back().ExitCode);
1497
1498 EnsureContainerDoesNotExist(WslcContainerName);
1499 }
1500
1501 WSLC_TEST_METHOD(WSLCE2E_Container_Run_HealthStatus_Timeout)
1502 {
1503 auto result = RunWslc(std::format(
1504 LR"(container run -d --health-cmd "sleep 30" --health-interval 1s --health-timeout 1s --health-retries 1 --name {} {} sleep infinity)",
1505 WslcContainerName,
1506 DebianImage.NameAndTag()));
1507 result.Verify({.Stderr = L"", .ExitCode = 0});
1508
1509 const auto health = WaitForContainerHealth(WslcContainerName, "unhealthy");
1510 VERIFY_IS_TRUE(health.FailingStreak >= 1);
1511 VERIFY_IS_FALSE(health.Log.empty());
1512 VERIFY_ARE_EQUAL(-1, health.Log.back().ExitCode);
1513
1514 EnsureContainerDoesNotExist(WslcContainerName);
1515 }
1516
1517 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Cpus)
1518 {
1519 auto result = RunWslc(std::format(L"container run --name {} --cpus 1.5 {} true", WslcContainerName, DebianImage.NameAndTag()));
1520 result.Verify({.Stderr = L"", .ExitCode = 0});
1521
1522 const auto inspect = InspectContainer(WslcContainerName);
1523 VERIFY_ARE_EQUAL(static_cast<int64_t>(1'500'000'000), inspect.HostConfig.NanoCpus);
1524 }
1525
1526 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Memory)
1527 {
1528 auto result = RunWslc(std::format(L"container run --name {} --memory 32M {} true", WslcContainerName, DebianImage.NameAndTag()));
1529 // Note: stderr is not asserted here because some kernels emit a swap-limit warning
1530 // ("Your kernel does not support swap limit capabilities...") when a memory limit is set.
1531 result.Verify({.ExitCode = 0});
1532
1533 const auto inspect = InspectContainer(WslcContainerName);
1534 VERIFY_ARE_EQUAL(static_cast<int64_t>(32) * 1024 * 1024, inspect.HostConfig.Memory);
1535 }
1536
1537 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ulimit)
1538 {
1539 auto result = RunWslc(std::format(
1540 L"container run --name {} --ulimit nofile=1024:2048 --ulimit nproc=512 {} true", WslcContainerName, DebianImage.NameAndTag()));
1541 result.Verify({.Stderr = L"", .ExitCode = 0});
1542
1543 const auto inspect = InspectContainer(WslcContainerName);
1544 VERIFY_ARE_EQUAL(static_cast<size_t>(2), inspect.HostConfig.Ulimits.size());
1545
1546 std::map<std::string, std::pair<int64_t, int64_t>> byName;
1547 for (const auto& ul : inspect.HostConfig.Ulimits)
1548 {
1549 byName[ul.Name] = {ul.Soft, ul.Hard};
1550 }
1551
1552 VERIFY_IS_TRUE(byName.contains("nofile"));
1553 VERIFY_ARE_EQUAL(static_cast<int64_t>(1024), byName["nofile"].first);
1554 VERIFY_ARE_EQUAL(static_cast<int64_t>(2048), byName["nofile"].second);
1555
1556 VERIFY_IS_TRUE(byName.contains("nproc"));
1557 VERIFY_ARE_EQUAL(static_cast<int64_t>(512), byName["nproc"].first);
1558 VERIFY_ARE_EQUAL(static_cast<int64_t>(512), byName["nproc"].second);
1559 }
1560
1561 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Cpus_Invalid)
1562 {
1563 auto result = RunWslc(std::format(L"container run --rm --cpus 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1564 result.Verify({.Stdout = L"", .ExitCode = 1});
1565 VERIFY_IS_TRUE(result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidCpusError(L"cpus", L"0")));
1566 EnsureContainerDoesNotExist(WslcContainerName);
1567 }
1568
1569 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Memory_Invalid)
1570 {
1571 auto result =
1572 RunWslc(std::format(L"container run --rm --memory invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1573 result.Verify({.Stdout = L"", .ExitCode = 1});
1574 VERIFY_IS_TRUE(
1575 result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidMemorySizeError(L"memory", L"invalid")));
1576 EnsureContainerDoesNotExist(WslcContainerName);
1577 }
1578
1579 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ulimit_Invalid)
1580 {
1581 auto result =
1582 RunWslc(std::format(L"container run --rm --ulimit nofile --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1583 result.Verify({.Stdout = L"", .ExitCode = 1});
1584 VERIFY_IS_TRUE(
1585 result.StderrContainsSubstring(wsl::shared::Localization::WSLCCLI_InvalidUlimitError(L"ulimit", L"nofile")));
1586 EnsureContainerDoesNotExist(WslcContainerName);
1587 }
1588
1589 WSLC_TEST_METHOD(WSLCE2E_Container_Run_StopSignal_Invalid)
1590 {
1591 {
1592 auto result = RunWslc(
1593 std::format(L"container run --rm --stop-signal SIGINVALID --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1594 result.Verify({.Stdout = L"", .ExitCode = 1});
1595 VERIFY_IS_TRUE(
1596 result.StderrContainsSubstring(L"Invalid stop-signal value: SIGINVALID is not a recognized signal name or number "
1597 L"(Example: SIGKILL, kill, or 9)."));
1598 EnsureContainerDoesNotExist(WslcContainerName);
1599 }
1600
1601 {
1602 auto result =
1603 RunWslc(std::format(L"container run --rm --stop-signal 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1604 result.Verify({.Stdout = L"", .ExitCode = 1});
1605 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid stop-signal value: 0 is out of valid range (1-31)."));
1606 EnsureContainerDoesNotExist(WslcContainerName);
1607 }
1608
1609 {
1610 auto result =
1611 RunWslc(std::format(L"container run --rm --stop-signal 99 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1612 result.Verify({.Stdout = L"", .ExitCode = 1});
1613 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Invalid stop-signal value: 99 is out of valid range (1-31)."));
1614 EnsureContainerDoesNotExist(WslcContainerName);
1615 }
1616 }
1617
1618 private:
1619 void VerifyHostLoopback(std::string_view setting, std::string_view dnsName, bool forceTcp)
1620 {
1621 EnsureSessionIsTerminated();
1622 auto terminateSession = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { EnsureSessionIsTerminated(); });
1623
1624 const auto settingsPath = wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"wslc" / L"settings.yaml";
1625 HostFileChange settings(settingsPath, std::format("session:\n hostLoopback: \"{}\"\n", setting));
1626
1627 const auto endpoint = std::format(L"http://127.0.0.1:{}/", HostLoopbackTestPort);
1628 UniqueWebServer server(endpoint.c_str(), L"host-loopback-ok");
1629 ExpectHttpResponse(endpoint.c_str(), HTTP_STATUS_OK, true);
1630
1631 auto command = std::format(
1632 L"python3 -c \"import http.client,socket;"
1633 L"a=socket.getaddrinfo('{}',{},socket.AF_INET,socket.SOCK_STREAM)[0][4];"
1634 L"c=http.client.HTTPConnection(*a,timeout=60);"
1635 L"c.request('GET','/');"
1636 L"assert c.getresponse().read()==b'host-loopback-ok';"
1637 L"assert ('use-vc' in open('/etc/resolv.conf').read()) == {}\"", // Validate that the DNS setting was applied
1638 std::string(dnsName),
1639 HostLoopbackTestPort,
1640 forceTcp ? "True" : "False");
1641
1642 auto result = RunWslc(std::format(
1643 L"container run {} --rm --name {} {} {}", forceTcp ? "--dns-option=use-vc" : "", WslcContainerName, PythonImage.NameAndTag(), command));
1644 result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
1645 }
1646
1647 // Test container name
1648 const std::wstring WslcContainerName = L"wslc-test-container";
1649 const std::wstring WslcContainerName2 = L"wslc-test-container-2";
1650
1651 // Test environment variables
1652 const std::wstring HostEnvVariableName = L"WSLC_TEST_HOST_ENV";
1653 const std::wstring HostEnvVariableName2 = L"WSLC_TEST_HOST_ENV2";
1654 const std::wstring HostEnvVariableValue = L"wslc-host-env-value";
1655 const std::wstring HostEnvVariableValue2 = L"wslc-host-env-value2";
1656
1657 // Test images
1658 const TestImage& DebianImage = DebianTestImage();
1659 const TestImage& HelloWorldImage = HelloWorldTestImage();
1660 const TestImage& PythonImage = PythonTestImage();
1661
1662 // Test environment variable files
1663 std::filesystem::path EnvTestFile1;
1664 std::filesystem::path EnvTestFile2;
1665
1666 // Test ports
1667 const uint16_t ContainerTestPort = 8080;
1668 const uint16_t HostTestPort1 = 1234;
1669 const uint16_t HostTestPort2 = 1235;
1670 const uint16_t HostLoopbackTestPort = 1236;
1671
1672 // Test named volume
1673 const std::wstring WslcVolumeName = L"wslc-test-volume";
1674
1675 // Test user-defined network
1676 const std::wstring TestNetworkName = L"wslc-test-network";
1677 };
1678 } // namespace WSLCE2ETests