diff --git a/src/windows/common/WSLAContainerLauncher.cpp b/src/windows/common/WSLAContainerLauncher.cpp index 838654017..d355845d8 100644 --- a/src/windows/common/WSLAContainerLauncher.cpp +++ b/src/windows/common/WSLAContainerLauncher.cpp @@ -48,8 +48,9 @@ WSLAContainerLauncher::WSLAContainerLauncher( const std::string& EntryPoint, const std::vector& Arguments, const std::vector& Environment, + WSLA_CONTAINER_NETWORK_TYPE containerNetworkType, ProcessFlags Flags) : - WSLAProcessLauncher(EntryPoint, Arguments, Environment, Flags), m_image(Image), m_name(Name) + WSLAProcessLauncher(EntryPoint, Arguments, Environment, Flags), m_image(Image), m_name(Name), m_containerNetworkType(containerNetworkType) { } @@ -60,6 +61,7 @@ std::pair> WSLAContainerLauncher::L options.Name = m_name.c_str(); auto [processOptions, commandLinePtrs, environmentPtrs] = CreateProcessOptions(); options.InitProcessOptions = processOptions; + options.ContainerNetwork.ContainerNetworkType = m_containerNetworkType; if (m_executable.empty()) { diff --git a/src/windows/common/WSLAContainerLauncher.h b/src/windows/common/WSLAContainerLauncher.h index dd8b037a4..ef79ae874 100644 --- a/src/windows/common/WSLAContainerLauncher.h +++ b/src/windows/common/WSLAContainerLauncher.h @@ -45,6 +45,7 @@ class WSLAContainerLauncher : private WSLAProcessLauncher const std::string& EntryPoint = "", const std::vector& Arguments = {}, const std::vector& Environment = {}, + WSLA_CONTAINER_NETWORK_TYPE containerNetworkType = WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_HOST, ProcessFlags Flags = ProcessFlags::Stdout | ProcessFlags::Stderr); void AddVolume(const std::string& HostPath, const std::string& ContainerPath, bool ReadOnly); @@ -56,5 +57,6 @@ class WSLAContainerLauncher : private WSLAProcessLauncher private: std::string m_image; std::string m_name; + WSLA_CONTAINER_NETWORK_TYPE m_containerNetworkType; }; } // namespace wsl::windows::common \ No newline at end of file diff --git a/src/windows/wslaservice/exe/WSLAContainer.cpp b/src/windows/wslaservice/exe/WSLAContainer.cpp index 56502cae0..af7a7ee41 100644 --- a/src/windows/wslaservice/exe/WSLAContainer.cpp +++ b/src/windows/wslaservice/exe/WSLAContainer.cpp @@ -18,11 +18,10 @@ Module Name: using wsl::windows::service::wsla::WSLAContainer; -// Constants for required default arguments for "nerdctl run..." -static std::vector defaultNerdctlRunArgs{//"--pull=never", // TODO: Uncomment once PullImage() is implemented. - "--net=host", // TODO: default for now, change later - "--ulimit", - "nofile=65536:65536"}; +// Constants for required default arguments for "nerdctl create..." +static std::vector defaultNerdctlCreateArgs{//"--pull=never", // TODO: Uncomment once PullImage() is implemented. + "--ulimit", + "nofile=65536:65536"}; WSLAContainer::WSLAContainer(WSLAVirtualMachine* parentVM, const WSLA_CONTAINER_OPTIONS& Options, std::string&& Id, ContainerEventTracker& tracker) : m_parentVM(parentVM), m_name(Options.Name), m_image(Options.Image), m_id(std::move(Id)) @@ -102,7 +101,7 @@ void WSLAContainer::OnEvent(ContainerEvent event) HRESULT WSLAContainer::Stop(int Signal, ULONG TimeoutMs) try { - std::lock_guard lock(m_lock); + std::lock_guard lock(m_lock); if (State() == WslaContainerStateExited) { @@ -324,6 +323,31 @@ std::vector WSLAContainer::PrepareNerdctlCreateCommand(const WSLA_C args.push_back("create"); args.push_back("--name"); args.push_back(options.Name); + + switch (options.ContainerNetwork.ContainerNetworkType) + { + case WSLA_CONTAINER_NETWORK_HOST: + args.push_back("--net=host"); + break; + case WSLA_CONTAINER_NETWORK_NONE: + args.push_back("--net=none"); + break; + case WSLA_CONTAINER_NETWORK_BRIDGE: + args.push_back("--net=bridge"); + break; + // TODO: uncomment and implement when we have custom networks + // case WSLA_CONTAINER_NETWORK_CUSTOM: + // args.push_back(std::format("--net={}", options.ContainerNetwork.ContainerNetworkName)); + // break; + default: + THROW_HR_MSG( + E_INVALIDARG, + "No such network: type: %i, name: %hs", + options.ContainerNetwork.ContainerNetworkType, + options.ContainerNetwork.ContainerNetworkName); + break; + } + if (options.ShmSize > 0) { args.push_back(std::format("--shm-size={}m", options.ShmSize)); @@ -335,7 +359,7 @@ std::vector WSLAContainer::PrepareNerdctlCreateCommand(const WSLA_C args.push_back("all"); } - args.insert(args.end(), defaultNerdctlRunArgs.begin(), defaultNerdctlRunArgs.end()); + args.insert(args.end(), defaultNerdctlCreateArgs.begin(), defaultNerdctlCreateArgs.end()); args.insert(args.end(), inputOptions.begin(), inputOptions.end()); if (options.InitProcessOptions.Executable != nullptr) @@ -383,4 +407,4 @@ std::optional WSLAContainer::GetNerdctlStatus() // N.B. nerdctl inspect can return with exit code 0 and no output. Return an empty optional if that happens. return status.empty() ? std::optional{} : status; -} +} \ No newline at end of file diff --git a/src/windows/wslaservice/inc/wslaservice.idl b/src/windows/wslaservice/inc/wslaservice.idl index c06c92703..abaaaf9af 100644 --- a/src/windows/wslaservice/inc/wslaservice.idl +++ b/src/windows/wslaservice/inc/wslaservice.idl @@ -135,6 +135,20 @@ struct WSLA_PORT_MAPPING USHORT ContainerPort; }; +enum WSLA_CONTAINER_NETWORK_TYPE +{ + WSLA_CONTAINER_NETWORK_BRIDGE = 0, + WSLA_CONTAINER_NETWORK_HOST = 1, + WSLA_CONTAINER_NETWORK_NONE = 2, + // WSLA_CONTAINER_NETWORK_CUSTOM = 3 // TODO: Implement when implementing custom networks +}; +struct WSLA_CONTAINER_NETWORK +{ + // TODO: Change default to bridge when implemented + enum WSLA_CONTAINER_NETWORK_TYPE ContainerNetworkType; + LPCSTR ContainerNetworkName; +}; + enum WSLA_CONTAINER_FLAGS { WSLA_CONTAINER_FLAG_ENABLE_GPU = 1 @@ -154,6 +168,7 @@ struct WSLA_CONTAINER_OPTIONS // TODO: List specific GPU devices. // TODO: Flags on wether the caller wants to override entrypoint, args, or both. ULONGLONG ShmSize; + struct WSLA_CONTAINER_NETWORK ContainerNetwork; }; enum WSLA_CONTAINER_STATE diff --git a/test/windows/WSLATests.cpp b/test/windows/WSLATests.cpp index a6b469bd9..afddfb71d 100644 --- a/test/windows/WSLATests.cpp +++ b/test/windows/WSLATests.cpp @@ -1168,7 +1168,13 @@ class WSLATests { WSLAContainerLauncher launcher( - "debian:latest", "test-default-entrypoint", "/bin/cat", {}, {}, ProcessFlags::Stdin | ProcessFlags::Stdout | ProcessFlags::Stderr); + "debian:latest", + "test-default-entrypoint", + "/bin/cat", + {}, + {}, + WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_HOST, + ProcessFlags::Stdin | ProcessFlags::Stdout | ProcessFlags::Stderr); // For now, validate that trying to use stdin without a tty returns the appropriate error. auto result = wil::ResultFromException([&]() { auto container = launcher.Launch(*session); }); @@ -1268,7 +1274,13 @@ class WSLATests // Create a stuck container. WSLAContainerLauncher launcher( - "debian:latest", "test-container-1", "sleep", {"sleep", "99999"}, {}, ProcessFlags::Stdout | ProcessFlags::Stderr); + "debian:latest", + "test-container-1", + "sleep", + {"sleep", "99999"}, + {}, + WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_HOST, + ProcessFlags::Stdout | ProcessFlags::Stderr); auto container = launcher.Launch(*session); @@ -1312,7 +1324,13 @@ class WSLATests { // Create a container WSLAContainerLauncher launcher( - "debian:latest", "test-container-2", "sleep", {"sleep", "99999"}, {}, ProcessFlags::Stdout | ProcessFlags::Stderr); + "debian:latest", + "test-container-2", + "sleep", + {"sleep", "99999"}, + {}, + WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_HOST, + ProcessFlags::Stdout | ProcessFlags::Stderr); auto container = launcher.Launch(*session); @@ -1346,7 +1364,13 @@ class WSLATests // Validate that container names are unique. { WSLAContainerLauncher launcher( - "debian:latest", "test-unique-name", "sleep", {"sleep", "99999"}, {}, ProcessFlags::Stdout | ProcessFlags::Stderr); + "debian:latest", + "test-unique-name", + "sleep", + {"sleep", "99999"}, + {}, + WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_HOST, + ProcessFlags::Stdout | ProcessFlags::Stderr); auto container = launcher.Launch(*session); VERIFY_ARE_EQUAL(container.State(), WslaContainerStateRunning); @@ -1396,7 +1420,13 @@ class WSLATests // Verify that the same name can be reused now that the container is deleted. WSLAContainerLauncher otherLauncher( - "debian:latest", "test-unique-name", "echo", {"OK"}, {}, ProcessFlags::Stdout | ProcessFlags::Stderr); + "debian:latest", + "test-unique-name", + "echo", + {"OK"}, + {}, + WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_HOST, + ProcessFlags::Stdout | ProcessFlags::Stderr); auto result = otherLauncher.Launch(*session).GetInitProcess().WaitAndCaptureOutput(); VERIFY_ARE_EQUAL(result.Output[1], "OK\n"); @@ -1404,6 +1434,153 @@ class WSLATests } } + TEST_METHOD(ContainerNetwork) + { + WSL2_TEST_ONLY(); + SKIP_TEST_ARM64(); + + auto storagePath = std::filesystem::current_path() / "test-storage"; + + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { + std::error_code error; + + std::filesystem::remove_all(storagePath, error); + if (error) + { + LogError("Failed to cleanup storage path %ws: %hs", storagePath.c_str(), error.message().c_str()); + } + }); + + auto settings = GetDefaultSessionSettings(); + settings.NetworkingMode = WSLANetworkingModeNAT; + settings.StoragePath = storagePath.c_str(); + settings.MaximumStorageSizeMb = 1024; + + auto session = CreateSession(settings); + + auto expectContainerList = [&](const std::vector>& expectedContainers) { + wil::unique_cotaskmem_array_ptr containers; + + VERIFY_SUCCEEDED(session->ListContainers(&containers, containers.size_address())); + VERIFY_ARE_EQUAL(expectedContainers.size(), containers.size()); + + for (size_t i = 0; i < expectedContainers.size(); i++) + { + const auto& [expectedName, expectedImage, expectedState] = expectedContainers[i]; + VERIFY_ARE_EQUAL(expectedName, containers[i].Name); + VERIFY_ARE_EQUAL(expectedImage, containers[i].Image); + VERIFY_ARE_EQUAL(expectedState, containers[i].State); + } + }; + + // Verify that containers launch successfully when host and none are used as network modes + // TODO: Test bridge network container launch when VHD with bridge cni is ready + // TODO: Add port mapping related tests when port mapping is implemented + { + WSLAContainerLauncher launcher( + "debian:latest", + "test-network", + {}, + {"sleep", "99999"}, + {}, + WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_HOST, + ProcessFlags::Stdout | ProcessFlags::Stderr); + + auto container = launcher.Launch(*session); + VERIFY_ARE_EQUAL(container.State(), WslaContainerStateRunning); + auto result = ExpectCommandResult( + session.get(), + {"/usr/bin/nerdctl", "inspect", "-f", "'{{ index .Config.Labels \"nerdctl/networks\" }}'", "test-network"}, + 0); + VERIFY_ARE_EQUAL(result.Output[1], "'[\"host\"]'\n"); + + VERIFY_SUCCEEDED(container.Get().Stop(15, 50000)); + + expectContainerList({{"test-network", "debian:latest", WslaContainerStateExited}}); + + // Verify that the container is in exited state. + VERIFY_ARE_EQUAL(container.State(), WslaContainerStateExited); + + // Verify that deleting a container stopped via Stop() works. + VERIFY_SUCCEEDED(container.Get().Delete()); + + expectContainerList({}); + } + + { + WSLAContainerLauncher launcher( + "debian:latest", + "test-network", + {}, + {"sleep", "99999"}, + {}, + WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_NONE, + ProcessFlags::Stdout | ProcessFlags::Stderr); + + auto container = launcher.Launch(*session); + VERIFY_ARE_EQUAL(container.State(), WslaContainerStateRunning); + auto result = ExpectCommandResult( + session.get(), + {"/usr/bin/nerdctl", "inspect", "-f", "'{{ index .Config.Labels \"nerdctl/networks\" }}'", "test-network"}, + 0); + VERIFY_ARE_EQUAL(result.Output[1], "'[\"none\"]'\n"); + + VERIFY_SUCCEEDED(container.Get().Stop(15, 50000)); + + expectContainerList({{"test-network", "debian:latest", WslaContainerStateExited}}); + + // Verify that the container is in exited state. + VERIFY_ARE_EQUAL(container.State(), WslaContainerStateExited); + + // Verify that deleting a container stopped via Stop() works. + VERIFY_SUCCEEDED(container.Get().Delete()); + + expectContainerList({}); + } + + { + WSLAContainerLauncher launcher( + "debian:latest", + "test-network", + {}, + {"sleep", "99999"}, + {}, + (WSLA_CONTAINER_NETWORK_TYPE)6, // WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_NONE, + ProcessFlags::Stdout | ProcessFlags::Stderr); + + auto retVal = launcher.LaunchNoThrow(*session); + VERIFY_ARE_EQUAL(retVal.first, E_INVALIDARG); + } + + // Test bridge when ready + /* + { + WSLAContainerLauncher launcher( + "debian:latest", "test-network", {}, {"sleep", "99999"}, {}, WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_BRIDGE, ProcessFlags::Stdout | ProcessFlags::Stderr); + + auto container = launcher.Launch(*session); + VERIFY_ARE_EQUAL(container.State(), WslaContainerStateRunning); + auto result = ExpectCommandResult( + session.get(), + {"/usr/bin/nerdctl", "inspect", "-f", "'{{ index .Config.Labels \"nerdctl/networks\" }}'", "test-network"}, + 0); + VERIFY_ARE_EQUAL(result.Output[1], "'[\"bridge\"]'\n"); + + VERIFY_SUCCEEDED(container.Get().Stop(15, 50000)); + + expectContainerList({{"test-network", "debian:latest", WslaContainerStateExited}}); + + // Verify that the container is in exited state. + VERIFY_ARE_EQUAL(container.State(), WslaContainerStateExited); + + // Verify that deleting a container stopped via Stop() works. + VERIFY_SUCCEEDED(container.Get().Delete()); + + expectContainerList({}); + } + */ + } + TEST_METHOD(Exec) { WSL2_TEST_ONLY(); @@ -1416,7 +1593,13 @@ class WSLATests // Create a container. WSLAContainerLauncher launcher( - "debian:latest", "test-container-exec", {}, {"sleep", "99999"}, {}, ProcessFlags::Stdout | ProcessFlags::Stderr); + "debian:latest", + "test-container-exec", + {}, + {"sleep", "99999"}, + {}, + WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_NONE, + ProcessFlags::Stdout | ProcessFlags::Stderr); auto container = launcher.Launch(*session); @@ -1480,4 +1663,4 @@ class WSLATests // TODO: Implement proper handling of executables that don't exist in the container. } } -}; \ No newline at end of file +};