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