@samitouri / QOSAMI-WSL / commits / facdc432

Add wslc network connect and disconnect commands (#41011)

beena352 committed Jul 9, 2026 at 11:35 UTC facdc432a7482b377797dbd9379789a799249aca
10 files changed +399
localization/strings/en-US/Resources.resw
+12
@@ -3177,6 +3177,18 @@ On first run, creates the file with all settings commented out at their defaults
3177 <value>Deleted: {}</value>
3178 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3179 </data>
3180 + <data name="WSLCCLI_NetworkConnectDesc" xml:space="preserve">
3181 + <value>Connect a container to a network.</value>
3182 + </data>
3183 + <data name="WSLCCLI_NetworkConnectLongDesc" xml:space="preserve">
3184 + <value>Connects a container to an existing network.</value>
3185 + </data>
3186 + <data name="WSLCCLI_NetworkDisconnectDesc" xml:space="preserve">
3187 + <value>Disconnect a container from a network.</value>
3188 + </data>
3189 + <data name="WSLCCLI_NetworkDisconnectLongDesc" xml:space="preserve">
3190 + <value>Disconnects a container from an existing network.</value>
3191 + </data>
3192 <data name="WSLCCLI_VolumeCommandDesc" xml:space="preserve">
3193 <value>Manage volumes.</value>
3194 </data>
src/windows/wslc/commands/NetworkCommand.cpp
+2
@@ -27,6 +27,8 @@ std::vector<std::unique_ptr<Command>> NetworkCommand::GetCommands() const
27 commands.push_back(std::make_unique<NetworkInspectCommand>(FullName()));
28 commands.push_back(std::make_unique<NetworkListCommand>(FullName()));
29 commands.push_back(std::make_unique<NetworkPruneCommand>(FullName()));
30 + commands.push_back(std::make_unique<NetworkConnectCommand>(FullName()));
31 + commands.push_back(std::make_unique<NetworkDisconnectCommand>(FullName()));
32 return commands;
33 }
34
src/windows/wslc/commands/NetworkCommand.h
+30
@@ -104,6 +104,36 @@ struct NetworkPruneCommand final : public Command
104 std::wstring ShortDescription() const override;
105 std::wstring LongDescription() const override;
106
107 +protected:
108 + void ExecuteInternal(CLIExecutionContext& context) const override;
109 +};
110 +
111 +// Connect Command
112 +struct NetworkConnectCommand final : public Command
113 +{
114 + constexpr static std::wstring_view CommandName = L"connect";
115 + NetworkConnectCommand(const std::wstring& parent) : Command(CommandName, parent)
116 + {
117 + }
118 + std::vector<Argument> GetArguments() const override;
119 + std::wstring ShortDescription() const override;
120 + std::wstring LongDescription() const override;
121 +
122 +protected:
123 + void ExecuteInternal(CLIExecutionContext& context) const override;
124 +};
125 +
126 +// Disconnect Command
127 +struct NetworkDisconnectCommand final : public Command
128 +{
129 + constexpr static std::wstring_view CommandName = L"disconnect";
130 + NetworkDisconnectCommand(const std::wstring& parent) : Command(CommandName, parent)
131 + {
132 + }
133 + std::vector<Argument> GetArguments() const override;
134 + std::wstring ShortDescription() const override;
135 + std::wstring LongDescription() const override;
136 +
137 protected:
138 void ExecuteInternal(CLIExecutionContext& context) const override;
139 };
src/windows/wslc/commands/NetworkConnectCommand.cpp new
+50
@@ -0,0 +1,50 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + NetworkConnectCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "NetworkCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "SessionTasks.h"
18 +#include "NetworkTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Network Connect Command
27 +std::vector<Argument> NetworkConnectCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::NetworkName, true),
31 + Argument::Create(ArgType::ContainerId, true),
32 + };
33 +}
34 +
35 +std::wstring NetworkConnectCommand::ShortDescription() const
36 +{
37 + return Localization::WSLCCLI_NetworkConnectDesc();
38 +}
39 +
40 +std::wstring NetworkConnectCommand::LongDescription() const
41 +{
42 + return Localization::WSLCCLI_NetworkConnectLongDesc();
43 +}
44 +
45 +void NetworkConnectCommand::ExecuteInternal(CLIExecutionContext& context) const
46 +{
47 + context << ResolveSession //
48 + << ConnectNetwork;
49 +}
50 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/NetworkDisconnectCommand.cpp new
+50
@@ -0,0 +1,50 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + NetworkDisconnectCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "NetworkCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "SessionTasks.h"
18 +#include "NetworkTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Network Disconnect Command
27 +std::vector<Argument> NetworkDisconnectCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::NetworkName, true),
31 + Argument::Create(ArgType::ContainerId, true),
32 + };
33 +}
34 +
35 +std::wstring NetworkDisconnectCommand::ShortDescription() const
36 +{
37 + return Localization::WSLCCLI_NetworkDisconnectDesc();
38 +}
39 +
40 +std::wstring NetworkDisconnectCommand::LongDescription() const
41 +{
42 + return Localization::WSLCCLI_NetworkDisconnectLongDesc();
43 +}
44 +
45 +void NetworkDisconnectCommand::ExecuteInternal(CLIExecutionContext& context) const
46 +{
47 + context << ResolveSession //
48 + << DisconnectNetwork;
49 +}
50 +} // namespace wsl::windows::wslc
src/windows/wslc/services/NetworkService.cpp
+17
@@ -115,4 +115,21 @@ models::PruneNetworksResult NetworkService::Prune(models::Session& session, cons
115
116 return result;
117 }
118 +
119 +void NetworkService::Connect(models::Session& session, const std::string& networkName, const std::string& containerId)
120 +{
121 + wil::com_ptr<IWSLCContainer> container;
122 + THROW_IF_FAILED(session.Get()->OpenContainer(containerId.c_str(), &container));
123 +
124 + WSLCNetworkConnectionOptions options{};
125 + options.NetworkName = networkName.c_str();
126 + THROW_IF_FAILED(container->ConnectToNetwork(&options));
127 +}
128 +
129 +void NetworkService::Disconnect(models::Session& session, const std::string& networkName, const std::string& containerId)
130 +{
131 + wil::com_ptr<IWSLCContainer> container;
132 + THROW_IF_FAILED(session.Get()->OpenContainer(containerId.c_str(), &container));
133 + THROW_IF_FAILED(container->DisconnectFromNetwork(networkName.c_str()));
134 +}
135 } // namespace wsl::windows::wslc::services
src/windows/wslc/services/NetworkService.h
+2
@@ -25,5 +25,7 @@ struct NetworkService
25 static std::vector<WSLCNetworkInformation> List(models::Session& session);
26 static wsl::windows::common::wslc_schema::Network Inspect(models::Session& session, const std::string& name);
27 static models::PruneNetworksResult Prune(models::Session& session, const std::vector<std::pair<std::string, std::string>>& filters = {});
28 + static void Connect(models::Session& session, const std::string& networkName, const std::string& containerId);
29 + static void Disconnect(models::Session& session, const std::string& networkName, const std::string& containerId);
30 };
31 } // namespace wsl::windows::wslc::services
src/windows/wslc/tasks/NetworkTasks.cpp
+22
@@ -227,4 +227,26 @@ void PruneNetworks(CLIExecutionContext& context)
227 PrintMessage(Localization::WSLCCLI_NetworkPruneDeleted(MultiByteToWide(networkName)));
228 }
229 }
230 +
231 +void ConnectNetwork(CLIExecutionContext& context)
232 +{
233 + WI_ASSERT(context.Data.Contains(Data::Session));
234 + WI_ASSERT(context.Args.Contains(ArgType::NetworkName));
235 + WI_ASSERT(context.Args.Contains(ArgType::ContainerId));
236 +
237 + const auto networkName = WideToMultiByte(context.Args.Get<ArgType::NetworkName>());
238 + const auto containerId = WideToMultiByte(context.Args.Get<ArgType::ContainerId>());
239 + NetworkService::Connect(context.Data.Get<Data::Session>(), networkName, containerId);
240 +}
241 +
242 +void DisconnectNetwork(CLIExecutionContext& context)
243 +{
244 + WI_ASSERT(context.Data.Contains(Data::Session));
245 + WI_ASSERT(context.Args.Contains(ArgType::NetworkName));
246 + WI_ASSERT(context.Args.Contains(ArgType::ContainerId));
247 +
248 + const auto networkName = WideToMultiByte(context.Args.Get<ArgType::NetworkName>());
249 + const auto containerId = WideToMultiByte(context.Args.Get<ArgType::ContainerId>());
250 + NetworkService::Disconnect(context.Data.Get<Data::Session>(), networkName, containerId);
251 +}
252 } // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/NetworkTasks.h
+2
@@ -22,4 +22,6 @@ void GetNetworks(wsl::windows::wslc::execution::CLIExecutionContext& context);
22 void InspectNetworks(wsl::windows::wslc::execution::CLIExecutionContext& context);
23 void ListNetworks(wsl::windows::wslc::execution::CLIExecutionContext& context);
24 void PruneNetworks(wsl::windows::wslc::execution::CLIExecutionContext& context);
25 +void ConnectNetwork(wsl::windows::wslc::execution::CLIExecutionContext& context);
26 +void DisconnectNetwork(wsl::windows::wslc::execution::CLIExecutionContext& context);
27 } // namespace wsl::windows::wslc::task
test/windows/wslc/e2e/WSLCE2ENetworkTests.cpp
+212
@@ -24,6 +24,27 @@ class WSLCE2ENetworkTests
24 {
25 WSLC_TEST_CLASS(WSLCE2ENetworkTests)
26
27 + TEST_CLASS_SETUP(ClassSetup)
28 + {
29 + EnsureImageIsLoaded(DebianImage);
30 + return true;
31 + }
32 +
33 + TEST_CLASS_CLEANUP(ClassCleanup)
34 + {
35 + EnsureContainerDoesNotExist(WslcContainerName);
36 + EnsureNetworkDoesNotExist(TestNetworkName);
37 + EnsureImageIsDeleted(DebianImage);
38 + return true;
39 + }
40 +
41 + TEST_METHOD_SETUP(TestMethodSetup)
42 + {
43 + EnsureContainerDoesNotExist(WslcContainerName);
44 + EnsureNetworkDoesNotExist(TestNetworkName);
45 + return true;
46 + }
47 +
48 WSLC_TEST_METHOD(WSLCE2E_Network_HelpCommand)
49 {
50 auto result = RunWslc(L"network --help");
@@ -42,7 +63,155 @@ class WSLCE2ENetworkTests
63 result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Unrecognized command: 'INVALID_CMD'\r\n", .ExitCode = 1});
64 }
65
66 + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_HelpCommand)
67 + {
68 + auto result = RunWslc(L"network connect --help");
69 + result.Verify({.Stdout = GetConnectHelpMessage(), .Stderr = L"", .ExitCode = 0});
70 + }
71 +
72 + WSLC_TEST_METHOD(WSLCE2E_Network_Disconnect_HelpCommand)
73 + {
74 + auto result = RunWslc(L"network disconnect --help");
75 + result.Verify({.Stdout = GetDisconnectHelpMessage(), .Stderr = L"", .ExitCode = 0});
76 + }
77 +
78 + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_MissingNetworkName)
79 + {
80 + auto result = RunWslc(L"network connect");
81 + result.Verify({.Stdout = GetConnectHelpMessage(), .Stderr = L"Required argument not provided: 'network-name'\r\n", .ExitCode = 1});
82 + }
83 +
84 + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_MissingContainerId)
85 + {
86 + auto result = RunWslc(std::format(L"network connect {}", TestNetworkName));
87 + result.Verify({.Stdout = GetConnectHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1});
88 + }
89 +
90 + WSLC_TEST_METHOD(WSLCE2E_Network_Disconnect_MissingNetworkName)
91 + {
92 + auto result = RunWslc(L"network disconnect");
93 + result.Verify({.Stdout = GetDisconnectHelpMessage(), .Stderr = L"Required argument not provided: 'network-name'\r\n", .ExitCode = 1});
94 + }
95 +
96 + WSLC_TEST_METHOD(WSLCE2E_Network_Disconnect_MissingContainerId)
97 + {
98 + auto result = RunWslc(std::format(L"network disconnect {}", TestNetworkName));
99 + result.Verify({.Stdout = GetDisconnectHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1});
100 + }
101 +
102 + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_Valid)
103 + {
104 + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
105 + result.Verify({.Stderr = L"", .ExitCode = 0});
106 +
107 + result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
108 + result.Verify({.Stderr = L"", .ExitCode = 0});
109 +
110 + result = RunWslc(std::format(L"network connect {} {}", TestNetworkName, WslcContainerName));
111 + result.Verify({.Stderr = L"", .ExitCode = 0});
112 +
113 + const auto inspect = InspectContainer(WslcContainerName);
114 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(string::WideToMultiByte(TestNetworkName)));
115 + }
116 +
117 + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_UnknownNetwork)
118 + {
119 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
120 + result.Verify({.Stderr = L"", .ExitCode = 0});
121 +
122 + result = RunWslc(std::format(L"network connect {} {}", TestNetworkName, WslcContainerName));
123 + result.Verify(
124 + {.Stderr = std::format(L"Network not found: '{}'\r\nError code: WSLC_E_NETWORK_NOT_FOUND\r\n", TestNetworkName), .ExitCode = 1});
125 + }
126 +
127 + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_UnknownContainer)
128 + {
129 + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
130 + result.Verify({.Stderr = L"", .ExitCode = 0});
131 +
132 + result = RunWslc(std::format(L"network connect {} {}", TestNetworkName, WslcContainerName));
133 + result.Verify(
134 + {.Stderr = std::format(L"Container '{}' not found.\r\nError code: WSLC_E_CONTAINER_NOT_FOUND\r\n", WslcContainerName),
135 + .ExitCode = 1});
136 + }
137 +
138 + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_NoneMode_Rejected)
139 + {
140 + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
141 + result.Verify({.Stderr = L"", .ExitCode = 0});
142 +
143 + result = RunWslc(
144 + std::format(L"container run -d --network none --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
145 + result.Verify({.Stderr = L"", .ExitCode = 0});
146 +
147 + result = RunWslc(std::format(L"network connect {} {}", TestNetworkName, WslcContainerName));
148 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
149 + VERIFY_IS_TRUE(result.Stderr.has_value());
150 + VerifyPatternMatch(string::WideToMultiByte(result.Stderr.value()), "*'host' or 'none'*Error code: *\r\n");
151 + }
152 +
153 + WSLC_TEST_METHOD(WSLCE2E_Network_Connect_AlreadyConnected_DockerErrorPropagated)
154 + {
155 + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
156 + result.Verify({.Stderr = L"", .ExitCode = 0});
157 +
158 + result = RunWslc(std::format(
159 + L"container run -d --network {} --name {} {} sleep infinity", TestNetworkName, WslcContainerName, DebianImage.NameAndTag()));
160 + result.Verify({.Stderr = L"", .ExitCode = 0});
161 +
162 + result = RunWslc(std::format(L"network connect {} {}", TestNetworkName, WslcContainerName));
163 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
164 + VERIFY_IS_TRUE(result.Stderr.has_value());
165 + VerifyPatternMatch(string::WideToMultiByte(result.Stderr.value()), "*already exists*Error code: *\r\n");
166 + }
167 +
168 + WSLC_TEST_METHOD(WSLCE2E_Network_Disconnect_Valid)
169 + {
170 + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
171 + result.Verify({.Stderr = L"", .ExitCode = 0});
172 +
173 + result = RunWslc(std::format(
174 + L"container run -d --network {} --name {} {} sleep infinity", TestNetworkName, WslcContainerName, DebianImage.NameAndTag()));
175 + result.Verify({.Stderr = L"", .ExitCode = 0});
176 +
177 + {
178 + const auto inspect = InspectContainer(WslcContainerName);
179 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(string::WideToMultiByte(TestNetworkName)));
180 + }
181 +
182 + result = RunWslc(std::format(L"network disconnect {} {}", TestNetworkName, WslcContainerName));
183 + result.Verify({.Stderr = L"", .ExitCode = 0});
184 +
185 + const auto inspect = InspectContainer(WslcContainerName);
186 + VERIFY_IS_FALSE(inspect.NetworkSettings.Networks.contains(string::WideToMultiByte(TestNetworkName)));
187 + }
188 +
189 + WSLC_TEST_METHOD(WSLCE2E_Network_Disconnect_UnknownNetwork)
190 + {
191 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
192 + result.Verify({.Stderr = L"", .ExitCode = 0});
193 +
194 + result = RunWslc(std::format(L"network disconnect {} {}", TestNetworkName, WslcContainerName));
195 + result.Verify(
196 + {.Stderr = std::format(L"Network not found: '{}'\r\nError code: WSLC_E_NETWORK_NOT_FOUND\r\n", TestNetworkName), .ExitCode = 1});
197 + }
198 +
199 + WSLC_TEST_METHOD(WSLCE2E_Network_Disconnect_UnknownContainer)
200 + {
201 + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
202 + result.Verify({.Stderr = L"", .ExitCode = 0});
203 +
204 + result = RunWslc(std::format(L"network disconnect {} {}", TestNetworkName, WslcContainerName));
205 + result.Verify(
206 + {.Stderr = std::format(L"Container '{}' not found.\r\nError code: WSLC_E_CONTAINER_NOT_FOUND\r\n", WslcContainerName),
207 + .ExitCode = 1});
208 + }
209 +
210 private:
211 + const std::wstring WslcContainerName = L"wslc-e2e-network-connect-container";
212 + const std::wstring TestNetworkName = L"wslc-e2e-network-connect";
213 + const TestImage& DebianImage = DebianTestImage();
214 +
215 std::wstring GetHelpMessage() const
216 {
217 std::wstringstream output;
@@ -72,6 +241,8 @@ private:
241 {L"inspect", Localization::WSLCCLI_NetworkInspectDesc()},
242 {L"list", Localization::WSLCCLI_NetworkListDesc()},
243 {L"prune", Localization::WSLCCLI_NetworkPruneDesc()},
244 + {L"connect", Localization::WSLCCLI_NetworkConnectDesc()},
245 + {L"disconnect", Localization::WSLCCLI_NetworkDisconnectDesc()},
246 };
247
248 size_t maxLen = 0;
@@ -98,5 +269,46 @@ private:
269 << L"\r\n";
270 return options.str();
271 }
272 +
273 + std::wstring GetConnectHelpMessage() const
274 + {
275 + std::wstringstream output;
276 + output << GetWslcHeader() //
277 + << Localization::WSLCCLI_NetworkConnectLongDesc() + L"\r\n\r\n" //
278 + << L"Usage: wslc network connect [<options>] <network-name> <container-id>\r\n\r\n" //
279 + << GetSubcommandArguments() //
280 + << GetSubcommandOptions();
281 + return output.str();
282 + }
283 +
284 + std::wstring GetDisconnectHelpMessage() const
285 + {
286 + std::wstringstream output;
287 + output << GetWslcHeader() //
288 + << Localization::WSLCCLI_NetworkDisconnectLongDesc() + L"\r\n\r\n" //
289 + << L"Usage: wslc network disconnect [<options>] <network-name> <container-id>\r\n\r\n" //
290 + << GetSubcommandArguments() //
291 + << GetSubcommandOptions();
292 + return output.str();
293 + }
294 +
295 + std::wstring GetSubcommandArguments() const
296 + {
297 + std::wstringstream args;
298 + args << L"The following arguments are available:\r\n" //
299 + << L" network-name Network name\r\n" //
300 + << L" container-id Container ID\r\n" //
301 + << L"\r\n";
302 + return args.str();
303 + }
304 +
305 + std::wstring GetSubcommandOptions() const
306 + {
307 + std::wstringstream options;
308 + options << L"The following options are available:\r\n" //
309 + << L" -?,--help Shows help about the selected command\r\n" //
310 + << L"\r\n";
311 + return options.str();
312 + }
313 };
314 } // namespace WSLCE2ETests