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