| 1 | /*++ |
| 2 | |
| 3 | Copyright (c) Microsoft. All rights reserved. |
| 4 | |
| 5 | Module Name: |
| 6 | |
| 7 | WSLCE2EHelpers.cpp |
| 8 | |
| 9 | Abstract: |
| 10 | |
| 11 | This file contains helper functions for end-to-end tests of WSLC. |
| 12 | --*/ |
| 13 | |
| 14 | #include "precomp.h" |
| 15 | #include "WSLCSessionDefaults.h" |
| 16 | #include "ImageModel.h" |
| 17 | #include "VolumeModel.h" |
| 18 | #include "windows/Common.h" |
| 19 | #include "WSLCExecutor.h" |
| 20 | #include "WSLCE2EHelpers.h" |
| 21 | #include "TestImageRegistry.h" |
| 22 | #include <JsonUtils.h> |
| 23 | #include <wslutil.h> |
| 24 | |
| 25 | extern std::wstring g_testDataPath; |
| 26 | |
| 27 | namespace WSLCE2ETests { |
| 28 | |
| 29 | using namespace WEX::Logging; |
| 30 | |
| 31 | namespace wslc_schema = wsl::windows::common::wslc_schema; |
| 32 | using wsl::windows::common::RunningWSLCContainer; |
| 33 | using wsl::windows::common::WSLCContainerLauncher; |
| 34 | |
| 35 | namespace { |
| 36 | // Lazily compute the session storage base path. |
| 37 | struct SessionStorageBasePathAccessor |
| 38 | { |
| 39 | operator const std::filesystem::path&() const |
| 40 | { |
| 41 | static const std::filesystem::path basePath = |
| 42 | std::filesystem::absolute(std::filesystem::current_path() / L"wslc-cli-test-sessions"); |
| 43 | return basePath; |
| 44 | } |
| 45 | }; |
| 46 | |
| 47 | static wil::com_ptr<IWSLCSessionManager> OpenSessionManager() |
| 48 | { |
| 49 | wil::com_ptr<IWSLCSessionManager> sessionManager; |
| 50 | VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager))); |
| 51 | wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get()); |
| 52 | return sessionManager; |
| 53 | } |
| 54 | |
| 55 | wil::com_ptr<IWSLCSession> CreateSession(const WSLCSessionSettings& sessionSettings, WSLCSessionFlags Flags = WSLCSessionFlagsNone) |
| 56 | { |
| 57 | const auto sessionManager = OpenSessionManager(); |
| 58 | wil::com_ptr<IWSLCSession> session; |
| 59 | VERIFY_SUCCEEDED(sessionManager->CreateSession(&sessionSettings, Flags, nullptr, &session)); |
| 60 | wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); |
| 61 | |
| 62 | WSLCSessionState state{}; |
| 63 | VERIFY_SUCCEEDED(session->GetState(&state)); |
| 64 | VERIFY_ARE_EQUAL(state, WSLCSessionStateRunning); |
| 65 | |
| 66 | return session; |
| 67 | } |
| 68 | |
| 69 | WSLCSessionSettings GetDefaultSessionSettings(LPCWSTR name, LPCWSTR storagePath, WSLCNetworkingMode networkingMode = WSLCNetworkingModeNone) |
| 70 | { |
| 71 | WSLCSessionSettings settings{}; |
| 72 | settings.DisplayName = name; |
| 73 | settings.CpuCount = 4; |
| 74 | settings.MemoryMb = 2048; |
| 75 | settings.BootTimeoutMs = 30 * 1000; |
| 76 | settings.StoragePath = storagePath; |
| 77 | settings.MaximumStorageSizeMb = 4096; // 4GB. |
| 78 | settings.NetworkingMode = networkingMode; |
| 79 | return settings; |
| 80 | } |
| 81 | |
| 82 | wil::com_ptr<IWSLCSession> CreateCustomSession( |
| 83 | const std::wstring& sessionName, const std::filesystem::path& storagePath, WSLCNetworkingMode networkingMode = WSLCNetworkingModeNone) |
| 84 | { |
| 85 | WSLCSessionSettings sessionSettings = GetDefaultSessionSettings(sessionName.c_str(), storagePath.c_str(), networkingMode); |
| 86 | return CreateSession(sessionSettings); |
| 87 | } |
| 88 | |
| 89 | void CleanupCustomSession(wil::com_ptr<IWSLCSession>& session, const std::filesystem::path& storagePath) |
| 90 | { |
| 91 | if (session) |
| 92 | { |
| 93 | LOG_IF_FAILED(session->Terminate()); |
| 94 | } |
| 95 | |
| 96 | session.reset(); |
| 97 | |
| 98 | if (!storagePath.empty()) |
| 99 | { |
| 100 | std::error_code error; |
| 101 | std::filesystem::remove_all(storagePath, error); |
| 102 | if (error) |
| 103 | { |
| 104 | Log::Error(std::format(L"Failed to cleanup storage path {}: {}", storagePath.wstring(), error.message()).c_str()); |
| 105 | } |
| 106 | } |
| 107 | } |
| 108 | } // namespace |
| 109 | |
| 110 | const TestImage& AlpineTestImage() |
| 111 | { |
| 112 | static const TestImage image{L"alpine", L"latest", std::filesystem::path{g_testDataPath} / L"alpine-latest.tar"}; |
| 113 | return image; |
| 114 | } |
| 115 | |
| 116 | const TestImage& DebianTestImage() |
| 117 | { |
| 118 | static const TestImage image{L"debian", L"latest", std::filesystem::path{g_testDataPath} / L"debian-latest.tar"}; |
| 119 | return image; |
| 120 | } |
| 121 | |
| 122 | const TestImage& HelloWorldTestImage() |
| 123 | { |
| 124 | static const TestImage image{L"hello-world", L"latest", std::filesystem::path{g_testDataPath} / L"HelloWorldSaved.tar"}; |
| 125 | return image; |
| 126 | } |
| 127 | |
| 128 | const TestImage& PythonTestImage() |
| 129 | { |
| 130 | static const TestImage image{L"python", L"3.12-alpine", std::filesystem::path{g_testDataPath} / L"python-3_12-alpine.tar"}; |
| 131 | return image; |
| 132 | } |
| 133 | |
| 134 | const TestImage& InvalidTestImage() |
| 135 | { |
| 136 | static const TestImage image{L"mcr.microsoft.com/invalid-image", L"latest", L"INVALID_PATH"}; |
| 137 | return image; |
| 138 | } |
| 139 | |
| 140 | TestSession TestSession::Create(const std::wstring& displayName, WSLCNetworkingMode networkingMode) |
| 141 | { |
| 142 | const std::filesystem::path& basePath = SessionStorageBasePathAccessor(); |
| 143 | auto storagePath = basePath / displayName; |
| 144 | auto session = CreateCustomSession(displayName, storagePath, networkingMode); |
| 145 | return TestSession{displayName, storagePath.wstring(), std::move(session)}; |
| 146 | } |
| 147 | |
| 148 | TestSession::~TestSession() |
| 149 | { |
| 150 | CleanupCustomSession(m_session, m_storagePath); |
| 151 | } |
| 152 | |
| 153 | void VerifyContainerIsListed(const std::wstring& containerNameOrId, const std::wstring& status, const std::wstring& sessionName) |
| 154 | { |
| 155 | // The status column reports the runtime's description, e.g. "Up 5 seconds", so map the logical |
| 156 | // state callers pass in onto the text that description starts with. |
| 157 | std::wstring expectedStatus = status; |
| 158 | if (status == L"created") |
| 159 | { |
| 160 | expectedStatus = L"Created"; |
| 161 | } |
| 162 | else if (status == L"running") |
| 163 | { |
| 164 | expectedStatus = L"Up "; |
| 165 | } |
| 166 | else if (status == L"exited") |
| 167 | { |
| 168 | expectedStatus = L"Exited ("; |
| 169 | } |
| 170 | |
| 171 | std::wstring command = L"container list --no-trunc --all"; |
| 172 | if (!sessionName.empty()) |
| 173 | { |
| 174 | command = std::format(L"--session \"{}\" container list --no-trunc --all", sessionName); |
| 175 | } |
| 176 | |
| 177 | auto result = RunWslc(command); |
| 178 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 179 | |
| 180 | auto outputLines = result.GetStdoutLines(); |
| 181 | for (const auto& line : outputLines) |
| 182 | { |
| 183 | if (line.find(containerNameOrId) != std::wstring::npos) |
| 184 | { |
| 185 | const std::wstring message = L"Container '" + containerNameOrId + L"' found in container list output but status '" + |
| 186 | expectedStatus + L"' was not found in the same line"; |
| 187 | VERIFY_ARE_NOT_EQUAL(std::wstring::npos, line.find(expectedStatus), message.c_str()); |
| 188 | return; |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | const std::wstring message = L"Container '" + containerNameOrId + L"' not found in container list output"; |
| 193 | VERIFY_FAIL(message.c_str()); |
| 194 | } |
| 195 | |
| 196 | void VerifyImageIsUsed(const TestImage& image) |
| 197 | { |
| 198 | auto result = RunWslc(L"container list --no-trunc --all"); |
| 199 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 200 | auto outputLines = result.GetStdoutLines(); |
| 201 | for (const auto& line : outputLines) |
| 202 | { |
| 203 | if (line.find(image.NameAndTag()) != std::wstring::npos) |
| 204 | { |
| 205 | return; |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | VERIFY_FAIL(std::format(L"Image '{}' not found in container list output", image.NameAndTag()).c_str()); |
| 210 | } |
| 211 | |
| 212 | void VerifyImageIsNotUsed(const TestImage& image) |
| 213 | { |
| 214 | auto result = RunWslc(L"container list --no-trunc --all"); |
| 215 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 216 | auto outputLines = result.GetStdoutLines(); |
| 217 | for (const auto& line : outputLines) |
| 218 | { |
| 219 | if (line.find(image.NameAndTag()) != std::wstring::npos) |
| 220 | { |
| 221 | VERIFY_FAIL(std::format(L"Image '{}' found in container list output", image.NameAndTag()).c_str()); |
| 222 | } |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | void VerifyImageIsListed(const TestImage& image) |
| 227 | { |
| 228 | auto result = RunWslc(L"image list --format json"); |
| 229 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 230 | auto images = ParseNdjsonOutputAs<wsl::windows::wslc::models::ImageOutputInformation>(result); |
| 231 | for (const auto& img : images) |
| 232 | { |
| 233 | if (img.Repository == wsl::shared::string::WideToMultiByte(image.Name) && |
| 234 | img.Tag == wsl::shared::string::WideToMultiByte(image.Tag)) |
| 235 | { |
| 236 | return; |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | VERIFY_FAIL(std::format(L"Image '{}' not found in image list output", image.NameAndTag()).c_str()); |
| 241 | } |
| 242 | |
| 243 | void VerifyVolumeIsListed(const std::wstring& volumeName) |
| 244 | { |
| 245 | auto result = RunWslc(L"volume list --format json"); |
| 246 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 247 | auto volumes = ParseNdjsonOutputAs<VolumeListOutput>(result); |
| 248 | for (const auto& vol : volumes) |
| 249 | { |
| 250 | if (vol.Name == wsl::shared::string::WideToMultiByte(volumeName)) |
| 251 | { |
| 252 | return; |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | VERIFY_FAIL(std::format(L"Volume '{}' not found in volume list output", volumeName).c_str()); |
| 257 | } |
| 258 | |
| 259 | void VerifyVolumeIsNotListed(const std::wstring& volumeName) |
| 260 | { |
| 261 | auto result = RunWslc(L"volume list --format json"); |
| 262 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 263 | auto volumes = ParseNdjsonOutputAs<VolumeListOutput>(result); |
| 264 | for (const auto& vol : volumes) |
| 265 | { |
| 266 | if (vol.Name == wsl::shared::string::WideToMultiByte(volumeName)) |
| 267 | { |
| 268 | VERIFY_FAIL(std::format(L"Volume '{}' found in volume list output", volumeName).c_str()); |
| 269 | } |
| 270 | } |
| 271 | } |
| 272 | |
| 273 | void VerifyNetworkIsListed(const std::wstring& networkName) |
| 274 | { |
| 275 | auto result = RunWslc(L"network list --format json"); |
| 276 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 277 | auto networks = ParseNdjsonOutputAs<NetworkListOutput>(result); |
| 278 | for (const auto& net : networks) |
| 279 | { |
| 280 | if (net.Name == wsl::shared::string::WideToMultiByte(networkName)) |
| 281 | { |
| 282 | return; |
| 283 | } |
| 284 | } |
| 285 | |
| 286 | VERIFY_FAIL(std::format(L"Network '{}' not found in network list output", networkName).c_str()); |
| 287 | } |
| 288 | |
| 289 | void VerifyNetworkIsNotListed(const std::wstring& networkName) |
| 290 | { |
| 291 | auto result = RunWslc(L"network list --format json"); |
| 292 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 293 | auto networks = ParseNdjsonOutputAs<NetworkListOutput>(result); |
| 294 | for (const auto& net : networks) |
| 295 | { |
| 296 | if (net.Name == wsl::shared::string::WideToMultiByte(networkName)) |
| 297 | { |
| 298 | VERIFY_FAIL(std::format(L"Network '{}' found in network list output", networkName).c_str()); |
| 299 | } |
| 300 | } |
| 301 | } |
| 302 | |
| 303 | std::string GetHashId(const std::string& id, bool fullId) |
| 304 | { |
| 305 | return wsl::windows::common::string::TruncateId(id, !fullId); |
| 306 | } |
| 307 | |
| 308 | wslc_schema::InspectContainer InspectContainer(const std::wstring& containerName) |
| 309 | { |
| 310 | auto result = RunWslc(std::format(L"container inspect {}", containerName)); |
| 311 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 312 | auto inspectData = wsl::shared::FromJson<std::vector<wslc_schema::InspectContainer>>(result.Stdout.value().c_str()); |
| 313 | VERIFY_ARE_EQUAL(1u, inspectData.size()); |
| 314 | return inspectData[0]; |
| 315 | } |
| 316 | |
| 317 | wslc_schema::Health WaitForContainerHealth(const std::wstring& containerName, const std::string_view& expectedStatus, std::chrono::milliseconds timeout) |
| 318 | { |
| 319 | try |
| 320 | { |
| 321 | return wsl::shared::retry::RetryWithTimeout<wslc_schema::Health>( |
| 322 | [&]() { |
| 323 | const auto inspect = InspectContainer(containerName); |
| 324 | THROW_HR_IF(E_FAIL, !inspect.State.Health.has_value()); |
| 325 | THROW_HR_IF(E_FAIL, inspect.State.Health->Status != expectedStatus); |
| 326 | return inspect.State.Health.value(); |
| 327 | }, |
| 328 | std::chrono::seconds(1), |
| 329 | timeout); |
| 330 | } |
| 331 | catch (...) |
| 332 | { |
| 333 | const auto inspect = InspectContainer(containerName); |
| 334 | const std::string actual = inspect.State.Health.has_value() ? inspect.State.Health->Status : "<none>"; |
| 335 | VERIFY_FAIL(std::format( |
| 336 | L"Container '{}' did not reach health status '{}' (last status: '{}')", |
| 337 | containerName, |
| 338 | wsl::shared::string::MultiByteToWide(std::string(expectedStatus)), |
| 339 | wsl::shared::string::MultiByteToWide(actual)) |
| 340 | .c_str()); |
| 341 | throw; |
| 342 | } |
| 343 | } |
| 344 | |
| 345 | wslc_schema::InspectImage InspectImage(const std::wstring& imageName) |
| 346 | { |
| 347 | auto result = RunWslc(std::format(L"image inspect {}", imageName)); |
| 348 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 349 | auto inspectData = wsl::shared::FromJson<std::vector<wslc_schema::InspectImage>>(result.Stdout.value().c_str()); |
| 350 | VERIFY_ARE_EQUAL(1u, inspectData.size()); |
| 351 | return inspectData[0]; |
| 352 | } |
| 353 | |
| 354 | wslc_schema::InspectVolume InspectVolume(const std::wstring& volumeName) |
| 355 | { |
| 356 | auto result = RunWslc(std::format(L"volume inspect {}", volumeName)); |
| 357 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 358 | auto inspectData = wsl::shared::FromJson<std::vector<wslc_schema::InspectVolume>>(result.Stdout.value().c_str()); |
| 359 | VERIFY_ARE_EQUAL(1u, inspectData.size()); |
| 360 | return inspectData[0]; |
| 361 | } |
| 362 | |
| 363 | void EnsureContainerDoesNotExist(const std::wstring& containerName) |
| 364 | { |
| 365 | const auto name = wsl::shared::string::WideToMultiByte(containerName); |
| 366 | const auto containers = ListAllContainers(); |
| 367 | auto it = std::ranges::find_if(containers, [&](const auto& c) { return c.Names == name; }); |
| 368 | if (it == containers.end()) |
| 369 | { |
| 370 | return; |
| 371 | } |
| 372 | |
| 373 | auto result = RunWslc(std::format(L"container remove --force {}", containerName)); |
| 374 | // Tolerate WSLC_E_CONTAINER_NOT_FOUND - container already removed |
| 375 | if (result.ExitCode != 0 && (!result.Stderr.has_value() || result.Stderr.value().find(L"WSLC_E_CONTAINER_NOT_FOUND") == std::wstring::npos)) |
| 376 | { |
| 377 | result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| 378 | } |
| 379 | } |
| 380 | |
| 381 | std::vector<wsl::windows::wslc::models::ContainerOutputInformation> ListAllContainers() |
| 382 | { |
| 383 | // --no-trunc keeps the full ids, which callers use to address containers. |
| 384 | auto result = RunWslc(L"container list --all --format json --no-trunc"); |
| 385 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 386 | return ParseNdjsonOutputAs<wsl::windows::wslc::models::ContainerOutputInformation>(result); |
| 387 | } |
| 388 | |
| 389 | void EnsureImageContainersAreDeleted(const TestImage& image) |
| 390 | { |
| 391 | auto containers = ListAllContainers(); |
| 392 | for (const auto& container : containers) |
| 393 | { |
| 394 | auto nameAndTag = wsl::shared::string::WideToMultiByte(image.NameAndTag()); |
| 395 | if (container.Image.find(nameAndTag) != std::string::npos) |
| 396 | { |
| 397 | auto result = RunWslc(std::format(L"container remove --force {}", container.ID)); |
| 398 | result.Verify({.Stdout = std::format(L"{}\r\n", container.ID), .Stderr = L"", .ExitCode = 0}); |
| 399 | } |
| 400 | } |
| 401 | } |
| 402 | |
| 403 | void DeleteImagesWithRepositoryPrefix(const std::wstring& repositoryPrefix) |
| 404 | { |
| 405 | auto result = RunWslc(L"image list --format json"); |
| 406 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 407 | |
| 408 | const auto images = ParseNdjsonOutputAs<wsl::windows::wslc::models::ImageOutputInformation>(result); |
| 409 | const auto prefix = wsl::shared::string::WideToMultiByte(repositoryPrefix); |
| 410 | for (const auto& image : images) |
| 411 | { |
| 412 | if (image.Repository.starts_with(prefix)) |
| 413 | { |
| 414 | // No container cleanup here: the images this prunes are only ever built and inspected, never used to |
| 415 | // create containers, so image delete --force is sufficient. If a future test containerizes a built |
| 416 | // image, remove its container in that test's cleanup rather than broadening this prefix-based safety net. |
| 417 | const auto nameAndTag = wsl::shared::string::MultiByteToWide(std::format("{}:{}", image.Repository, image.Tag)); |
| 418 | RunWslc(std::format(L"image delete --force {}", nameAndTag)).Verify({.Stderr = L"", .ExitCode = 0}); |
| 419 | } |
| 420 | } |
| 421 | } |
| 422 | |
| 423 | void EnsureNoUntaggedImages() |
| 424 | { |
| 425 | auto result = RunWslc(L"image list --format json --no-trunc --filter dangling=true"); |
| 426 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 427 | |
| 428 | const auto images = ParseNdjsonOutputAs<wsl::windows::wslc::models::ImageOutputInformation>(result); |
| 429 | |
| 430 | for (const auto& image : images) |
| 431 | { |
| 432 | const auto id = wsl::shared::string::MultiByteToWide(GetHashId(image.ID, true)); |
| 433 | auto deleteResult = RunWslc(std::format(L"image delete --force {}", id)); |
| 434 | |
| 435 | // Tolerate WSLC_E_IMAGE_NOT_FOUND - an untagged image may already be gone if it was a |
| 436 | // parent/child of another untagged image deleted earlier in this loop. |
| 437 | if (deleteResult.ExitCode != 0 && |
| 438 | (!deleteResult.Stderr.has_value() || deleteResult.Stderr.value().find(L"WSLC_E_IMAGE_NOT_FOUND") == std::wstring::npos)) |
| 439 | { |
| 440 | deleteResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 441 | } |
| 442 | } |
| 443 | } |
| 444 | |
| 445 | void EnsureSessionIsTerminated(const std::wstring& sessionName) |
| 446 | { |
| 447 | std::wstring targetSession = sessionName; |
| 448 | if (targetSession.empty()) |
| 449 | { |
| 450 | auto isElevated = wsl::windows::common::security::IsTokenElevated(wil::open_current_access_token(TOKEN_QUERY).get()); |
| 451 | auto baseName = isElevated ? wsl::windows::wslc::DefaultAdminSessionName : wsl::windows::wslc::DefaultSessionName; |
| 452 | |
| 453 | wchar_t username[256 + 1] = {}; |
| 454 | DWORD usernameLen = ARRAYSIZE(username); |
| 455 | THROW_IF_WIN32_BOOL_FALSE(GetUserNameW(username, &usernameLen)); |
| 456 | |
| 457 | targetSession = std::format(L"{}-{}", baseName, username); |
| 458 | } |
| 459 | |
| 460 | auto listResult = RunWslc(L"system session list"); |
| 461 | listResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 462 | |
| 463 | auto stdoutLines = listResult.GetStdoutLines(); |
| 464 | for (const auto& line : stdoutLines) |
| 465 | { |
| 466 | // Check if the line ends with the target session name |
| 467 | if (line.size() >= targetSession.size() && line.compare(line.size() - targetSession.size(), targetSession.size(), targetSession) == 0) |
| 468 | { |
| 469 | auto result = RunWslc(std::format(L"--session \"{}\" system session terminate", targetSession)); |
| 470 | result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| 471 | break; |
| 472 | } |
| 473 | } |
| 474 | } |
| 475 | |
| 476 | void EnsureVolumeDoesNotExist(const std::wstring& volumeName) |
| 477 | { |
| 478 | auto result = RunWslc(L"volume list --format json"); |
| 479 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 480 | auto volumes = ParseNdjsonOutputAs<VolumeListOutput>(result); |
| 481 | for (const auto& vol : volumes) |
| 482 | { |
| 483 | if (vol.Name == wsl::shared::string::WideToMultiByte(volumeName)) |
| 484 | { |
| 485 | auto deleteResult = RunWslc(std::format(L"volume rm {}", volumeName)); |
| 486 | deleteResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 487 | break; |
| 488 | } |
| 489 | } |
| 490 | } |
| 491 | |
| 492 | void EnsureNetworkDoesNotExist(const std::wstring& networkName) |
| 493 | { |
| 494 | auto result = RunWslc(L"network list --format json"); |
| 495 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 496 | auto networks = ParseNdjsonOutputAs<NetworkListOutput>(result); |
| 497 | for (const auto& net : networks) |
| 498 | { |
| 499 | if (net.Name == wsl::shared::string::WideToMultiByte(networkName)) |
| 500 | { |
| 501 | auto deleteResult = RunWslc(std::format(L"network rm {}", networkName)); |
| 502 | deleteResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 503 | break; |
| 504 | } |
| 505 | } |
| 506 | } |
| 507 | |
| 508 | wslc_schema::Network InspectNetwork(const std::wstring& networkName) |
| 509 | { |
| 510 | auto result = RunWslc(std::format(L"network inspect {}", networkName)); |
| 511 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 512 | auto inspectData = wsl::shared::FromJson<std::vector<wslc_schema::Network>>(result.Stdout.value().c_str()); |
| 513 | VERIFY_ARE_EQUAL(1u, inspectData.size()); |
| 514 | return inspectData[0]; |
| 515 | } |
| 516 | |
| 517 | wil::com_ptr<IWSLCSession> OpenDefaultElevatedSession() |
| 518 | { |
| 519 | // Ensure the default elevated session exists before opening it via COM. |
| 520 | RunWslcAndVerify(L"container list", {.Stderr = L"", .ExitCode = 0}); |
| 521 | |
| 522 | wil::com_ptr<IWSLCSessionManager> sessionManager; |
| 523 | VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager))); |
| 524 | wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get()); |
| 525 | |
| 526 | wil::com_ptr<IWSLCSession> session; |
| 527 | VERIFY_SUCCEEDED(sessionManager->OpenSessionByName(nullptr, &session)); |
| 528 | wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); |
| 529 | |
| 530 | return std::move(session); |
| 531 | } |
| 532 | |
| 533 | std::pair<RunningWSLCContainer, std::string> StartLocalRegistry( |
| 534 | IWSLCSession& session, const std::string& username, const std::string& password, USHORT port, const std::wstring& tlsCertDir) |
| 535 | { |
| 536 | // Check if the registry image is already loaded on this session. |
| 537 | wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images; |
| 538 | THROW_IF_FAILED(session.ListImages(nullptr, &images, images.size_address<ULONG>())); |
| 539 | |
| 540 | bool found = std::ranges::any_of( |
| 541 | std::span{images.get(), images.size()}, [](const auto& e) { return std::strcmp(e.Image, "wslc-registry:latest") == 0; }); |
| 542 | |
| 543 | if (!found) |
| 544 | { |
| 545 | LoadTestImage(session, "wslc-registry:latest"); |
| 546 | } |
| 547 | |
| 548 | const bool useTls = !tlsCertDir.empty(); |
| 549 | |
| 550 | std::vector<std::string> env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)}; |
| 551 | |
| 552 | if (!username.empty()) |
| 553 | { |
| 554 | env.push_back(std::format("USERNAME={}", username)); |
| 555 | env.push_back(std::format("PASSWORD={}", password)); |
| 556 | } |
| 557 | |
| 558 | if (useTls) |
| 559 | { |
| 560 | env.push_back("REGISTRY_HTTP_TLS_CERTIFICATE=/certs/server.crt"); |
| 561 | env.push_back("REGISTRY_HTTP_TLS_KEY=/certs/server.key"); |
| 562 | } |
| 563 | |
| 564 | // TLS needs a non-loopback address for real verification, so use bridge networking and reach the |
| 565 | // container by its bridge IP. Plain HTTP uses host networking with a published loopback port. |
| 566 | WSLCContainerLauncher launcher("wslc-registry:latest", {}, {}, env, useTls ? "bridge" : "host"); |
| 567 | launcher.SetEntrypoint({"/entrypoint.sh"}); |
| 568 | |
| 569 | if (useTls) |
| 570 | { |
| 571 | launcher.AddVolume(tlsCertDir, "/certs", true); |
| 572 | } |
| 573 | else |
| 574 | { |
| 575 | launcher.AddPort(port, port, AF_INET); |
| 576 | } |
| 577 | |
| 578 | auto container = launcher.Launch(session); |
| 579 | |
| 580 | // Wait for the registry to bind the port before continuing. |
| 581 | auto initProcess = container.GetInitProcess(); |
| 582 | WaitForOutput(initProcess.GetStdHandle(2), std::format("listening on [::]:{}", port)); |
| 583 | |
| 584 | if (useTls) |
| 585 | { |
| 586 | auto inspect = container.Inspect(); |
| 587 | THROW_HR_IF(E_UNEXPECTED, inspect.NetworkSettings.Networks.empty()); |
| 588 | auto address = std::format("{}:{}", inspect.NetworkSettings.Networks.begin()->second.IPAddress, port); |
| 589 | |
| 590 | return {std::move(container), std::move(address)}; |
| 591 | } |
| 592 | else |
| 593 | { |
| 594 | auto address = std::format("127.0.0.1:{}", port); |
| 595 | auto url = std::format(L"http://{}/v2/", wsl::shared::string::MultiByteToWide(address)); |
| 596 | |
| 597 | int expectedCode = username.empty() ? 200 : 401; |
| 598 | ExpectHttpResponse(url.c_str(), expectedCode, true); |
| 599 | |
| 600 | return {std::move(container), std::move(address)}; |
| 601 | } |
| 602 | } |
| 603 | |
| 604 | std::wstring TagImageForRegistry(const std::wstring& imageName, const std::wstring& registryAddress) |
| 605 | { |
| 606 | auto registryImage = std::format(L"{}/{}", registryAddress, imageName); |
| 607 | RunWslcAndVerify(std::format(L"image tag {} {}", imageName, registryImage), {.ExitCode = 0}); |
| 608 | return registryImage; |
| 609 | } |
| 610 | |
| 611 | void WriteTestFile(const std::filesystem::path& filePath, const std::vector<std::string>& lines) |
| 612 | { |
| 613 | std::ofstream file(filePath, std::ios::out | std::ios::trunc | std::ios::binary); |
| 614 | VERIFY_IS_TRUE(file.is_open()); |
| 615 | for (const auto& line : lines) |
| 616 | { |
| 617 | file << line << "\n"; |
| 618 | } |
| 619 | |
| 620 | VERIFY_IS_TRUE(file.good()); |
| 621 | } |
| 622 | |
| 623 | void WriteTestFileContent(const std::filesystem::path& filePath, const std::string& content) |
| 624 | { |
| 625 | std::ofstream file(filePath, std::ios::out | std::ios::trunc | std::ios::binary); |
| 626 | THROW_HR_IF_MSG(E_FAIL, !file.is_open(), "Failed to open %ls for writing", filePath.c_str()); |
| 627 | file << content; |
| 628 | THROW_HR_IF_MSG(E_FAIL, !file.good(), "Failed to write to %ls", filePath.c_str()); |
| 629 | } |
| 630 | |
| 631 | std::wstring GetPythonHttpServerScript(uint16_t port) |
| 632 | { |
| 633 | return std::format(L"python3 -u -m http.server {}", port); |
| 634 | } |
| 635 | |
| 636 | std::wstring GetPythonUdpEchoServerScript(uint16_t port) |
| 637 | { |
| 638 | // Inline Python UDP echo server: echoes each received datagram back uppercased, forever. |
| 639 | return std::format( |
| 640 | L"python3 -c \"import socket;" |
| 641 | L"s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM);" |
| 642 | L"s.bind(('0.0.0.0',{}));" |
| 643 | L"[s.sendto(d.upper(),a) for d,a in iter(lambda:s.recvfrom(1024),0)]\"", |
| 644 | port); |
| 645 | } |
| 646 | |
| 647 | std::string SendUdpAndReceive(uint16_t hostPort, const std::string& payload, const std::string& expectedReply, int family) |
| 648 | { |
| 649 | SOCKADDR_INET addr{}; |
| 650 | addr.si_family = static_cast<ADDRESS_FAMILY>(family); |
| 651 | INETADDR_SETLOOPBACK(reinterpret_cast<PSOCKADDR>(&addr)); |
| 652 | SS_PORT(&addr) = htons(hostPort); |
| 653 | |
| 654 | const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60); |
| 655 | do |
| 656 | { |
| 657 | wil::unique_socket sock{::socket(family, SOCK_DGRAM, IPPROTO_UDP)}; |
| 658 | THROW_LAST_ERROR_IF(!sock); |
| 659 | |
| 660 | DWORD timeout = 1000; |
| 661 | THROW_LAST_ERROR_IF(setsockopt(sock.get(), SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&timeout), sizeof(timeout)) == SOCKET_ERROR); |
| 662 | |
| 663 | if (sendto(sock.get(), payload.data(), static_cast<int>(payload.size()), 0, reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)) != SOCKET_ERROR) |
| 664 | { |
| 665 | char buf[1024]; |
| 666 | const int received = recvfrom(sock.get(), buf, sizeof(buf), 0, nullptr, nullptr); |
| 667 | if (received != SOCKET_ERROR && received > 0 && std::string(buf, received) == expectedReply) |
| 668 | { |
| 669 | return std::string(buf, received); |
| 670 | } |
| 671 | } |
| 672 | } while (std::chrono::steady_clock::now() < deadline); |
| 673 | |
| 674 | VERIFY_FAIL(L"Timed out waiting for expected UDP echo reply from container"); |
| 675 | return {}; |
| 676 | } |
| 677 | |
| 678 | namespace { |
| 679 | |
| 680 | void WaitForTtySize(const WSLCInteractiveSession& session, SHORT columns, SHORT rows) |
| 681 | { |
| 682 | WaitForPseudoConsoleOutput(session, std::format("{} {}\r\n", rows, columns)); |
| 683 | } |
| 684 | |
| 685 | } // namespace |
| 686 | |
| 687 | void WaitForPseudoConsoleOutput(const WSLCInteractiveSession& session, const std::string& expected, std::chrono::seconds timeout) |
| 688 | { |
| 689 | try |
| 690 | { |
| 691 | wsl::shared::retry::RetryWithTimeout<void>( |
| 692 | [&]() { |
| 693 | const std::string data = session.GetStdoutData(); |
| 694 | THROW_HR_IF(E_ABORT, data.find(expected) == std::string::npos); |
| 695 | }, |
| 696 | std::chrono::milliseconds(200), |
| 697 | timeout); |
| 698 | } |
| 699 | catch (...) |
| 700 | { |
| 701 | const std::string data = session.GetStdoutData(); |
| 702 | VERIFY_FAIL(std::format( |
| 703 | L"Timed out waiting for \"{}\". Captured pseudoconsole output: \"{}\"", |
| 704 | wsl::shared::string::MultiByteToWide(EscapeString(expected)), |
| 705 | wsl::shared::string::MultiByteToWide(EscapeString(data))) |
| 706 | .c_str()); |
| 707 | } |
| 708 | } |
| 709 | |
| 710 | void VerifyPseudoConsoleTtySize(WSLCInteractiveSession& session, SHORT columns, SHORT rows) |
| 711 | { |
| 712 | constexpr SHORT resizedColumns = 100; |
| 713 | constexpr SHORT resizedRows = 37; |
| 714 | VERIFY_IS_TRUE(columns != resizedColumns || rows != resizedRows, L"Resized tty size must differ from the initial size"); |
| 715 | |
| 716 | WaitForTtySize(session, columns, rows); |
| 717 | |
| 718 | session.ResizePseudoConsole(resizedColumns, resizedRows); |
| 719 | WaitForTtySize(session, resizedColumns, resizedRows); |
| 720 | |
| 721 | session.Terminate(); |
| 722 | } |
| 723 | } // namespace WSLCE2ETests |