diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 6eb41abef..d674f113e 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -3168,6 +3168,18 @@ On first run, creates the file with all settings commented out at their defaults Deleted: {} {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + Connect a container to a network. + + + Connects a container to an existing network. + + + Disconnect a container from a network. + + + Disconnects a container from an existing network. + Manage volumes. diff --git a/src/windows/wslc/commands/NetworkCommand.cpp b/src/windows/wslc/commands/NetworkCommand.cpp index 94146efa3..ca30b35f5 100644 --- a/src/windows/wslc/commands/NetworkCommand.cpp +++ b/src/windows/wslc/commands/NetworkCommand.cpp @@ -27,6 +27,8 @@ std::vector> NetworkCommand::GetCommands() const commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); return commands; } diff --git a/src/windows/wslc/commands/NetworkCommand.h b/src/windows/wslc/commands/NetworkCommand.h index acf4911e3..8bee46249 100644 --- a/src/windows/wslc/commands/NetworkCommand.h +++ b/src/windows/wslc/commands/NetworkCommand.h @@ -104,6 +104,36 @@ struct NetworkPruneCommand final : public Command std::wstring ShortDescription() const override; std::wstring LongDescription() const override; +protected: + void ExecuteInternal(CLIExecutionContext& context) const override; +}; + +// Connect Command +struct NetworkConnectCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"connect"; + NetworkConnectCommand(const std::wstring& parent) : Command(CommandName, parent) + { + } + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& context) const override; +}; + +// Disconnect Command +struct NetworkDisconnectCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"disconnect"; + NetworkDisconnectCommand(const std::wstring& parent) : Command(CommandName, parent) + { + } + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + protected: void ExecuteInternal(CLIExecutionContext& context) const override; }; diff --git a/src/windows/wslc/commands/NetworkConnectCommand.cpp b/src/windows/wslc/commands/NetworkConnectCommand.cpp new file mode 100644 index 000000000..462af190f --- /dev/null +++ b/src/windows/wslc/commands/NetworkConnectCommand.cpp @@ -0,0 +1,50 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + NetworkConnectCommand.cpp + +Abstract: + + Implementation of command execution logic. + +--*/ + +#include "NetworkCommand.h" +#include "CLIExecutionContext.h" +#include "SessionTasks.h" +#include "NetworkTasks.h" +#include "Task.h" + +using namespace wsl::windows::wslc::execution; +using namespace wsl::windows::wslc::task; +using namespace wsl::shared; + +namespace wsl::windows::wslc { +// Network Connect Command +std::vector NetworkConnectCommand::GetArguments() const +{ + return { + Argument::Create(ArgType::NetworkName, true), + Argument::Create(ArgType::ContainerId, true), + }; +} + +std::wstring NetworkConnectCommand::ShortDescription() const +{ + return Localization::WSLCCLI_NetworkConnectDesc(); +} + +std::wstring NetworkConnectCommand::LongDescription() const +{ + return Localization::WSLCCLI_NetworkConnectLongDesc(); +} + +void NetworkConnectCommand::ExecuteInternal(CLIExecutionContext& context) const +{ + context << ResolveSession // + << ConnectNetwork; +} +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/NetworkDisconnectCommand.cpp b/src/windows/wslc/commands/NetworkDisconnectCommand.cpp new file mode 100644 index 000000000..b40959b91 --- /dev/null +++ b/src/windows/wslc/commands/NetworkDisconnectCommand.cpp @@ -0,0 +1,50 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + NetworkDisconnectCommand.cpp + +Abstract: + + Implementation of command execution logic. + +--*/ + +#include "NetworkCommand.h" +#include "CLIExecutionContext.h" +#include "SessionTasks.h" +#include "NetworkTasks.h" +#include "Task.h" + +using namespace wsl::windows::wslc::execution; +using namespace wsl::windows::wslc::task; +using namespace wsl::shared; + +namespace wsl::windows::wslc { +// Network Disconnect Command +std::vector NetworkDisconnectCommand::GetArguments() const +{ + return { + Argument::Create(ArgType::NetworkName, true), + Argument::Create(ArgType::ContainerId, true), + }; +} + +std::wstring NetworkDisconnectCommand::ShortDescription() const +{ + return Localization::WSLCCLI_NetworkDisconnectDesc(); +} + +std::wstring NetworkDisconnectCommand::LongDescription() const +{ + return Localization::WSLCCLI_NetworkDisconnectLongDesc(); +} + +void NetworkDisconnectCommand::ExecuteInternal(CLIExecutionContext& context) const +{ + context << ResolveSession // + << DisconnectNetwork; +} +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/services/NetworkService.cpp b/src/windows/wslc/services/NetworkService.cpp index 20b87aa06..5e98756d5 100644 --- a/src/windows/wslc/services/NetworkService.cpp +++ b/src/windows/wslc/services/NetworkService.cpp @@ -115,4 +115,21 @@ models::PruneNetworksResult NetworkService::Prune(models::Session& session, cons return result; } + +void NetworkService::Connect(models::Session& session, const std::string& networkName, const std::string& containerId) +{ + wil::com_ptr container; + THROW_IF_FAILED(session.Get()->OpenContainer(containerId.c_str(), &container)); + + WSLCNetworkConnectionOptions options{}; + options.NetworkName = networkName.c_str(); + THROW_IF_FAILED(container->ConnectToNetwork(&options)); +} + +void NetworkService::Disconnect(models::Session& session, const std::string& networkName, const std::string& containerId) +{ + wil::com_ptr container; + THROW_IF_FAILED(session.Get()->OpenContainer(containerId.c_str(), &container)); + THROW_IF_FAILED(container->DisconnectFromNetwork(networkName.c_str())); +} } // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/NetworkService.h b/src/windows/wslc/services/NetworkService.h index 81b545836..e0e30db79 100644 --- a/src/windows/wslc/services/NetworkService.h +++ b/src/windows/wslc/services/NetworkService.h @@ -25,5 +25,7 @@ struct NetworkService static std::vector List(models::Session& session); static wsl::windows::common::wslc_schema::Network Inspect(models::Session& session, const std::string& name); static models::PruneNetworksResult Prune(models::Session& session, const std::vector>& filters = {}); + static void Connect(models::Session& session, const std::string& networkName, const std::string& containerId); + static void Disconnect(models::Session& session, const std::string& networkName, const std::string& containerId); }; } // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/tasks/NetworkTasks.cpp b/src/windows/wslc/tasks/NetworkTasks.cpp index d4a7a8467..12b49f948 100644 --- a/src/windows/wslc/tasks/NetworkTasks.cpp +++ b/src/windows/wslc/tasks/NetworkTasks.cpp @@ -227,4 +227,26 @@ void PruneNetworks(CLIExecutionContext& context) PrintMessage(Localization::WSLCCLI_NetworkPruneDeleted(MultiByteToWide(networkName))); } } + +void ConnectNetwork(CLIExecutionContext& context) +{ + WI_ASSERT(context.Data.Contains(Data::Session)); + WI_ASSERT(context.Args.Contains(ArgType::NetworkName)); + WI_ASSERT(context.Args.Contains(ArgType::ContainerId)); + + const auto networkName = WideToMultiByte(context.Args.Get()); + const auto containerId = WideToMultiByte(context.Args.Get()); + NetworkService::Connect(context.Data.Get(), networkName, containerId); +} + +void DisconnectNetwork(CLIExecutionContext& context) +{ + WI_ASSERT(context.Data.Contains(Data::Session)); + WI_ASSERT(context.Args.Contains(ArgType::NetworkName)); + WI_ASSERT(context.Args.Contains(ArgType::ContainerId)); + + const auto networkName = WideToMultiByte(context.Args.Get()); + const auto containerId = WideToMultiByte(context.Args.Get()); + NetworkService::Disconnect(context.Data.Get(), networkName, containerId); +} } // namespace wsl::windows::wslc::task diff --git a/src/windows/wslc/tasks/NetworkTasks.h b/src/windows/wslc/tasks/NetworkTasks.h index 13c1ade81..ad590ae1a 100644 --- a/src/windows/wslc/tasks/NetworkTasks.h +++ b/src/windows/wslc/tasks/NetworkTasks.h @@ -22,4 +22,6 @@ void GetNetworks(wsl::windows::wslc::execution::CLIExecutionContext& context); void InspectNetworks(wsl::windows::wslc::execution::CLIExecutionContext& context); void ListNetworks(wsl::windows::wslc::execution::CLIExecutionContext& context); void PruneNetworks(wsl::windows::wslc::execution::CLIExecutionContext& context); +void ConnectNetwork(wsl::windows::wslc::execution::CLIExecutionContext& context); +void DisconnectNetwork(wsl::windows::wslc::execution::CLIExecutionContext& context); } // namespace wsl::windows::wslc::task diff --git a/test/windows/wslc/e2e/WSLCE2ENetworkTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkTests.cpp index 87489dfa5..8f73a5465 100644 --- a/test/windows/wslc/e2e/WSLCE2ENetworkTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ENetworkTests.cpp @@ -24,6 +24,27 @@ class WSLCE2ENetworkTests { WSLC_TEST_CLASS(WSLCE2ENetworkTests) + TEST_CLASS_SETUP(ClassSetup) + { + EnsureImageIsLoaded(DebianImage); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + EnsureContainerDoesNotExist(WslcContainerName); + EnsureNetworkDoesNotExist(TestNetworkName); + EnsureImageIsDeleted(DebianImage); + return true; + } + + TEST_METHOD_SETUP(TestMethodSetup) + { + EnsureContainerDoesNotExist(WslcContainerName); + EnsureNetworkDoesNotExist(TestNetworkName); + return true; + } + WSLC_TEST_METHOD(WSLCE2E_Network_HelpCommand) { auto result = RunWslc(L"network --help"); @@ -42,7 +63,155 @@ class WSLCE2ENetworkTests result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Unrecognized command: 'INVALID_CMD'\r\n", .ExitCode = 1}); } + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_HelpCommand) + { + auto result = RunWslc(L"network connect --help"); + result.Verify({.Stdout = GetConnectHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Disconnect_HelpCommand) + { + auto result = RunWslc(L"network disconnect --help"); + result.Verify({.Stdout = GetDisconnectHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_MissingNetworkName) + { + auto result = RunWslc(L"network connect"); + result.Verify({.Stdout = GetConnectHelpMessage(), .Stderr = L"Required argument not provided: 'network-name'\r\n", .ExitCode = 1}); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_MissingContainerId) + { + auto result = RunWslc(std::format(L"network connect {}", TestNetworkName)); + result.Verify({.Stdout = GetConnectHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1}); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Disconnect_MissingNetworkName) + { + auto result = RunWslc(L"network disconnect"); + result.Verify({.Stdout = GetDisconnectHelpMessage(), .Stderr = L"Required argument not provided: 'network-name'\r\n", .ExitCode = 1}); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Disconnect_MissingContainerId) + { + auto result = RunWslc(std::format(L"network disconnect {}", TestNetworkName)); + result.Verify({.Stdout = GetDisconnectHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1}); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_Valid) + { + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + result = RunWslc(std::format(L"network connect {} {}", TestNetworkName, WslcContainerName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + const auto inspect = InspectContainer(WslcContainerName); + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(string::WideToMultiByte(TestNetworkName))); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_UnknownNetwork) + { + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + result = RunWslc(std::format(L"network connect {} {}", TestNetworkName, WslcContainerName)); + result.Verify( + {.Stderr = std::format(L"Network not found: '{}'\r\nError code: WSLC_E_NETWORK_NOT_FOUND\r\n", TestNetworkName), .ExitCode = 1}); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_UnknownContainer) + { + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + result = RunWslc(std::format(L"network connect {} {}", TestNetworkName, WslcContainerName)); + result.Verify( + {.Stderr = std::format(L"Container '{}' not found.\r\nError code: WSLC_E_CONTAINER_NOT_FOUND\r\n", WslcContainerName), + .ExitCode = 1}); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_NoneMode_Rejected) + { + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + result = RunWslc( + std::format(L"container run -d --network none --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + result = RunWslc(std::format(L"network connect {} {}", TestNetworkName, WslcContainerName)); + VERIFY_ARE_EQUAL(1u, result.ExitCode.value()); + VERIFY_IS_TRUE(result.Stderr.has_value()); + VerifyPatternMatch(string::WideToMultiByte(result.Stderr.value()), "*'host' or 'none'*Error code: *\r\n"); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_AlreadyConnected_DockerErrorPropagated) + { + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + result = RunWslc(std::format( + L"container run -d --network {} --name {} {} sleep infinity", TestNetworkName, WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + result = RunWslc(std::format(L"network connect {} {}", TestNetworkName, WslcContainerName)); + VERIFY_ARE_EQUAL(1u, result.ExitCode.value()); + VERIFY_IS_TRUE(result.Stderr.has_value()); + VerifyPatternMatch(string::WideToMultiByte(result.Stderr.value()), "*already exists*Error code: *\r\n"); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Disconnect_Valid) + { + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + result = RunWslc(std::format( + L"container run -d --network {} --name {} {} sleep infinity", TestNetworkName, WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + { + const auto inspect = InspectContainer(WslcContainerName); + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(string::WideToMultiByte(TestNetworkName))); + } + + result = RunWslc(std::format(L"network disconnect {} {}", TestNetworkName, WslcContainerName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + const auto inspect = InspectContainer(WslcContainerName); + VERIFY_IS_FALSE(inspect.NetworkSettings.Networks.contains(string::WideToMultiByte(TestNetworkName))); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Disconnect_UnknownNetwork) + { + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + result = RunWslc(std::format(L"network disconnect {} {}", TestNetworkName, WslcContainerName)); + result.Verify( + {.Stderr = std::format(L"Network not found: '{}'\r\nError code: WSLC_E_NETWORK_NOT_FOUND\r\n", TestNetworkName), .ExitCode = 1}); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_Disconnect_UnknownContainer) + { + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + result = RunWslc(std::format(L"network disconnect {} {}", TestNetworkName, WslcContainerName)); + result.Verify( + {.Stderr = std::format(L"Container '{}' not found.\r\nError code: WSLC_E_CONTAINER_NOT_FOUND\r\n", WslcContainerName), + .ExitCode = 1}); + } + private: + const std::wstring WslcContainerName = L"wslc-e2e-network-connect-container"; + const std::wstring TestNetworkName = L"wslc-e2e-network-connect"; + const TestImage& DebianImage = DebianTestImage(); + std::wstring GetHelpMessage() const { std::wstringstream output; @@ -72,6 +241,8 @@ class WSLCE2ENetworkTests {L"inspect", Localization::WSLCCLI_NetworkInspectDesc()}, {L"list", Localization::WSLCCLI_NetworkListDesc()}, {L"prune", Localization::WSLCCLI_NetworkPruneDesc()}, + {L"connect", Localization::WSLCCLI_NetworkConnectDesc()}, + {L"disconnect", Localization::WSLCCLI_NetworkDisconnectDesc()}, }; size_t maxLen = 0; @@ -98,5 +269,46 @@ class WSLCE2ENetworkTests << L"\r\n"; return options.str(); } + + std::wstring GetConnectHelpMessage() const + { + std::wstringstream output; + output << GetWslcHeader() // + << Localization::WSLCCLI_NetworkConnectLongDesc() + L"\r\n\r\n" // + << L"Usage: wslc network connect [] \r\n\r\n" // + << GetSubcommandArguments() // + << GetSubcommandOptions(); + return output.str(); + } + + std::wstring GetDisconnectHelpMessage() const + { + std::wstringstream output; + output << GetWslcHeader() // + << Localization::WSLCCLI_NetworkDisconnectLongDesc() + L"\r\n\r\n" // + << L"Usage: wslc network disconnect [] \r\n\r\n" // + << GetSubcommandArguments() // + << GetSubcommandOptions(); + return output.str(); + } + + std::wstring GetSubcommandArguments() const + { + std::wstringstream args; + args << L"The following arguments are available:\r\n" // + << L" network-name Network name\r\n" // + << L" container-id Container ID\r\n" // + << L"\r\n"; + return args.str(); + } + + std::wstring GetSubcommandOptions() const + { + std::wstringstream options; + options << L"The following options are available:\r\n" // + << L" -?,--help Shows help about the selected command\r\n" // + << L"\r\n"; + return options.str(); + } }; } // namespace WSLCE2ETests