[CLI] Init support for network prune command (#40813)
* Init network prune * Fix tests * Clang format
AmirMS committed
Jun 16, 2026 at 14:38 UTC
31038d536a89b648c8af1a813772fbb74714f211
11 files changed
+380
localization/strings/en-US/Resources.resw
+10
@@ -3093,6 +3093,16 @@ On first run, creates the file with all settings commented out at their defaults
3093
<data name="WSLCCLI_NetworkListQuietArgDesc" xml:space="preserve">
3094
<value>Outputs the network names only</value>
3095
</data>
3096
+ <data name="WSLCCLI_NetworkPruneDesc" xml:space="preserve">
3097
+ <value>Remove all unused networks.</value>
3098
+ </data>
3099
+ <data name="WSLCCLI_NetworkPruneLongDesc" xml:space="preserve">
3100
+ <value>Removes all unused networks. A network is considered unused when it is not referenced by any container.</value>
3101
+ </data>
3102
+ <data name="WSLCCLI_NetworkPruneDeleted" xml:space="preserve">
3103
+ <value>Deleted: {}</value>
3104
+ <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3105
+ </data>
3106
<data name="WSLCCLI_VolumeCommandDesc" xml:space="preserve">
3107
<value>Manage volumes.</value>
3108
</data>
src/windows/wslc/commands/NetworkCommand.cpp
+1
@@ -26,6 +26,7 @@ std::vector<std::unique_ptr<Command>> NetworkCommand::GetCommands() const
26
commands.push_back(std::make_unique<NetworkRemoveCommand>(FullName()));
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
return commands;
31
}
32
src/windows/wslc/commands/NetworkCommand.h
+15
@@ -92,4 +92,19 @@ protected:
92
void ValidateArgumentsInternal(const ArgMap& execArgs) const override;
93
void ExecuteInternal(CLIExecutionContext& context) const override;
94
};
95
+
96
+// Prune Command
97
+struct NetworkPruneCommand final : public Command
98
+{
99
+ constexpr static std::wstring_view CommandName = L"prune";
100
+ NetworkPruneCommand(const std::wstring& parent) : Command(CommandName, parent)
101
+ {
102
+ }
103
+ std::vector<Argument> GetArguments() const override;
104
+ std::wstring ShortDescription() const override;
105
+ std::wstring LongDescription() const override;
106
+
107
+protected:
108
+ void ExecuteInternal(CLIExecutionContext& context) const override;
109
+};
110
} // namespace wsl::windows::wslc
src/windows/wslc/commands/NetworkPruneCommand.cpp
new
+51
@@ -0,0 +1,51 @@
1
+/*++
2
+
3
+Copyright (c) Microsoft. All rights reserved.
4
+
5
+Module Name:
6
+
7
+ NetworkPruneCommand.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 Prune Command
27
+std::vector<Argument> NetworkPruneCommand::GetArguments() const
28
+{
29
+ return {
30
+ Argument::Create(ArgType::Filter, false, NO_LIMIT),
31
+ Argument::Create(ArgType::Session),
32
+ };
33
+}
34
+
35
+std::wstring NetworkPruneCommand::ShortDescription() const
36
+{
37
+ return Localization::WSLCCLI_NetworkPruneDesc();
38
+}
39
+
40
+std::wstring NetworkPruneCommand::LongDescription() const
41
+{
42
+ return Localization::WSLCCLI_NetworkPruneLongDesc();
43
+}
44
+
45
+void NetworkPruneCommand::ExecuteInternal(CLIExecutionContext& context) const
46
+{
47
+ context //
48
+ << CreateSession //
49
+ << PruneNetworks;
50
+}
51
+} // namespace wsl::windows::wslc
src/windows/wslc/services/NetworkModel.h
+5
@@ -27,4 +27,9 @@ struct CreateNetworkOptions
27
std::vector<std::pair<std::string, std::string>> Labels{};
28
};
29
30
+struct PruneNetworksResult
31
+{
32
+ std::vector<std::string> PrunedNetworks;
33
+};
34
+
35
} // namespace wsl::windows::wslc::models
src/windows/wslc/services/NetworkService.cpp
+23
@@ -81,4 +81,27 @@ wsl::windows::common::wslc_schema::Network NetworkService::Inspect(models::Sessi
81
THROW_IF_FAILED(session.Get()->InspectNetwork(name.c_str(), &output));
82
return FromJson<wsl::windows::common::wslc_schema::Network>(output.get());
83
}
84
+
85
+models::PruneNetworksResult NetworkService::Prune(models::Session& session, const std::vector<std::pair<std::string, std::string>>& filters)
86
+{
87
+ std::vector<WSLCFilter> filterEntries;
88
+ filterEntries.reserve(filters.size());
89
+ for (const auto& [key, value] : filters)
90
+ {
91
+ filterEntries.push_back({.Key = key.c_str(), .Value = value.c_str()});
92
+ }
93
+
94
+ wil::unique_cotaskmem_array_ptr<WSLCNetworkName> networks;
95
+ THROW_IF_FAILED(session.Get()->PruneNetworks(
96
+ filterEntries.empty() ? nullptr : filterEntries.data(), static_cast<ULONG>(filterEntries.size()), &networks, networks.size_address<ULONG>()));
97
+
98
+ models::PruneNetworksResult result;
99
+ result.PrunedNetworks.reserve(networks.size());
100
+ for (auto ptr = networks.get(), end = networks.get() + networks.size(); ptr != end; ++ptr)
101
+ {
102
+ result.PrunedNetworks.emplace_back(*ptr);
103
+ }
104
+
105
+ return result;
106
+}
107
} // namespace wsl::windows::wslc::services
src/windows/wslc/services/NetworkService.h
+1
@@ -24,5 +24,6 @@ struct NetworkService
24
static void Delete(models::Session& session, const std::string& name);
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
};
29
} // namespace wsl::windows::wslc::services
src/windows/wslc/tasks/NetworkTasks.cpp
+19
@@ -191,4 +191,23 @@ void ListNetworks(CLIExecutionContext& context)
191
THROW_HR(E_UNEXPECTED);
192
}
193
}
194
+
195
+void PruneNetworks(CLIExecutionContext& context)
196
+{
197
+ WI_ASSERT(context.Data.Contains(Data::Session));
198
+ auto& session = context.Data.Get<Data::Session>();
199
+
200
+ std::vector<std::pair<std::string, std::string>> filters;
201
+ for (const auto& value : context.Args.GetAll<ArgType::Filter>())
202
+ {
203
+ filters.push_back(validation::ParseFilter(value));
204
+ }
205
+
206
+ auto result = NetworkService::Prune(session, filters);
207
+
208
+ for (const auto& networkName : result.PrunedNetworks)
209
+ {
210
+ PrintMessage(Localization::WSLCCLI_NetworkPruneDeleted(MultiByteToWide(networkName)));
211
+ }
212
+}
213
} // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/NetworkTasks.h
+1
@@ -21,4 +21,5 @@ void DeleteNetworks(wsl::windows::wslc::execution::CLIExecutionContext& context)
21
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
} // namespace wsl::windows::wslc::task
test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp
new
+253
@@ -0,0 +1,253 @@
1
+/*++
2
+
3
+Copyright (c) Microsoft. All rights reserved.
4
+
5
+Module Name:
6
+
7
+ WSLCE2ENetworkPruneTests.cpp
8
+
9
+Abstract:
10
+
11
+ This file contains end-to-end tests for the WSLC network prune command.
12
+--*/
13
+
14
+#include "precomp.h"
15
+#include "windows/Common.h"
16
+#include "WSLCExecutor.h"
17
+#include "WSLCE2EHelpers.h"
18
+
19
+namespace WSLCE2ETests {
20
+using namespace wsl::shared;
21
+
22
+class WSLCE2ENetworkPruneTests
23
+{
24
+ WSLC_TEST_CLASS(WSLCE2ENetworkPruneTests)
25
+
26
+ TEST_CLASS_SETUP(ClassSetup)
27
+ {
28
+ EnsureImageIsLoaded(DebianImage);
29
+ CleanUpAllTestState();
30
+ return true;
31
+ }
32
+
33
+ TEST_METHOD_SETUP(MethodSetup)
34
+ {
35
+ CleanUpAllTestState();
36
+ return true;
37
+ }
38
+
39
+ TEST_CLASS_CLEANUP(ClassCleanup)
40
+ {
41
+ CleanUpAllTestState();
42
+ EnsureImageIsDeleted(DebianImage);
43
+ return true;
44
+ }
45
+
46
+ WSLC_TEST_METHOD(WSLCE2E_Network_Prune_HelpCommand)
47
+ {
48
+ const auto result = RunWslc(L"network prune --help");
49
+ result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
50
+ }
51
+
52
+ WSLC_TEST_METHOD(WSLCE2E_Network_Prune_NoNetworks)
53
+ {
54
+ // Prune when no unused networks exist should succeed without reporting any of our test networks.
55
+ const auto result = RunWslc(L"network prune");
56
+ result.Verify({.Stderr = L"", .ExitCode = 0});
57
+
58
+ VERIFY_IS_FALSE(result.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName)));
59
+ VERIFY_IS_FALSE(result.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName2)));
60
+ }
61
+
62
+ WSLC_TEST_METHOD(WSLCE2E_Network_Prune_RemovesUnusedNetwork)
63
+ {
64
+ RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)).Verify({.Stderr = L"", .ExitCode = 0});
65
+ VerifyNetworkIsListed(TestNetworkName);
66
+
67
+ auto cleanup = wil::scope_exit([&]() { EnsureNetworkDoesNotExist(TestNetworkName); });
68
+
69
+ const auto result = RunWslc(L"network prune");
70
+ result.Verify({.Stderr = L"", .ExitCode = 0});
71
+
72
+ auto output = result.GetStdoutLines();
73
+ VERIFY_ARE_EQUAL(1u, output.size());
74
+ VERIFY_ARE_NOT_EQUAL(std::wstring::npos, output[0].find(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName)));
75
+
76
+ VerifyNetworkIsNotListed(TestNetworkName);
77
+ }
78
+
79
+ WSLC_TEST_METHOD(WSLCE2E_Network_Prune_RemovesMultipleNetworks)
80
+ {
81
+ RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)).Verify({.Stderr = L"", .ExitCode = 0});
82
+ RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName2)).Verify({.Stderr = L"", .ExitCode = 0});
83
+ VerifyNetworkIsListed(TestNetworkName);
84
+ VerifyNetworkIsListed(TestNetworkName2);
85
+
86
+ auto cleanup = wil::scope_exit([&]() {
87
+ EnsureNetworkDoesNotExist(TestNetworkName);
88
+ EnsureNetworkDoesNotExist(TestNetworkName2);
89
+ });
90
+
91
+ const auto result = RunWslc(L"network prune");
92
+ result.Verify({.Stderr = L"", .ExitCode = 0});
93
+
94
+ VERIFY_IS_TRUE(result.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName)));
95
+ VERIFY_IS_TRUE(result.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName2)));
96
+
97
+ VerifyNetworkIsNotListed(TestNetworkName);
98
+ VerifyNetworkIsNotListed(TestNetworkName2);
99
+ }
100
+
101
+ WSLC_TEST_METHOD(WSLCE2E_Network_Prune_InUseNetwork_Preserved)
102
+ {
103
+ RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)).Verify({.Stderr = L"", .ExitCode = 0});
104
+ VerifyNetworkIsListed(TestNetworkName);
105
+
106
+ // Start a container attached to the network so it is considered in use.
107
+ RunWslc(std::format(
108
+ L"container run -d --name {} --network {} {} sleep infinity", WslcContainerName, TestNetworkName, DebianImage.NameAndTag()))
109
+ .Verify({.Stderr = L"", .ExitCode = 0});
110
+
111
+ auto cleanup = wil::scope_exit([&]() {
112
+ EnsureContainerDoesNotExist(WslcContainerName);
113
+ EnsureNetworkDoesNotExist(TestNetworkName);
114
+ });
115
+
116
+ const auto result = RunWslc(L"network prune");
117
+ result.Verify({.Stderr = L"", .ExitCode = 0});
118
+
119
+ VERIFY_IS_FALSE(
120
+ result.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName)),
121
+ L"Network in use by a running container must not be pruned");
122
+
123
+ VerifyNetworkIsListed(TestNetworkName);
124
+ }
125
+
126
+ WSLC_TEST_METHOD(WSLCE2E_Network_Prune_LabelFilter_PreservesNonMatchingNetwork)
127
+ {
128
+ RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)).Verify({.Stderr = L"", .ExitCode = 0});
129
+ VerifyNetworkIsListed(TestNetworkName);
130
+
131
+ auto cleanup = wil::scope_exit([&]() { EnsureNetworkDoesNotExist(TestNetworkName); });
132
+
133
+ // A label filter that does not match the network should preserve it.
134
+ const auto filteredPrune = RunWslc(L"network prune --filter label=wslc.test.never=present");
135
+ filteredPrune.Verify({.Stderr = L"", .ExitCode = 0});
136
+ VERIFY_IS_FALSE(
137
+ filteredPrune.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName)),
138
+ L"Filtered prune should not have deleted the non-matching network");
139
+ VerifyNetworkIsListed(TestNetworkName);
140
+
141
+ // A subsequent unfiltered prune should still remove it, proving the filter
142
+ // was the reason it survived.
143
+ const auto unfilteredPrune = RunWslc(L"network prune");
144
+ unfilteredPrune.Verify({.Stderr = L"", .ExitCode = 0});
145
+ VERIFY_IS_TRUE(unfilteredPrune.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName)));
146
+ VerifyNetworkIsNotListed(TestNetworkName);
147
+ }
148
+
149
+ WSLC_TEST_METHOD(WSLCE2E_Network_Prune_LabelFilter_MatchingValueIsDeleted)
150
+ {
151
+ RunWslc(std::format(L"network create --driver bridge --label wslc.test.prune=keep {}", TestNetworkName)).Verify({.Stderr = L"", .ExitCode = 0});
152
+ RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName2)).Verify({.Stderr = L"", .ExitCode = 0});
153
+ VerifyNetworkIsListed(TestNetworkName);
154
+ VerifyNetworkIsListed(TestNetworkName2);
155
+
156
+ auto cleanup = wil::scope_exit([&]() {
157
+ EnsureNetworkDoesNotExist(TestNetworkName);
158
+ EnsureNetworkDoesNotExist(TestNetworkName2);
159
+ });
160
+
161
+ const auto result = RunWslc(L"network prune --filter label=wslc.test.prune=keep");
162
+ result.Verify({.Stderr = L"", .ExitCode = 0});
163
+
164
+ VERIFY_IS_TRUE(result.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName)));
165
+ VERIFY_IS_FALSE(
166
+ result.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName2)),
167
+ L"Network without the matching label must not be deleted");
168
+
169
+ VerifyNetworkIsNotListed(TestNetworkName);
170
+ VerifyNetworkIsListed(TestNetworkName2);
171
+ }
172
+
173
+ WSLC_TEST_METHOD(WSLCE2E_Network_Prune_NegatedLabelFilter_PreservesLabeledNetwork)
174
+ {
175
+ RunWslc(std::format(L"network create --driver bridge --label wslc.test.keep=yes {}", TestNetworkName)).Verify({.Stderr = L"", .ExitCode = 0});
176
+ RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName2)).Verify({.Stderr = L"", .ExitCode = 0});
177
+ VerifyNetworkIsListed(TestNetworkName);
178
+ VerifyNetworkIsListed(TestNetworkName2);
179
+
180
+ auto cleanup = wil::scope_exit([&]() {
181
+ EnsureNetworkDoesNotExist(TestNetworkName);
182
+ EnsureNetworkDoesNotExist(TestNetworkName2);
183
+ });
184
+
185
+ const auto result = RunWslc(L"network prune --filter label!=wslc.test.keep");
186
+ result.Verify({.Stderr = L"", .ExitCode = 0});
187
+
188
+ VERIFY_IS_TRUE(result.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName2)));
189
+ VERIFY_IS_FALSE(
190
+ result.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName)),
191
+ L"Labeled network must be preserved when prune negates that label");
192
+
193
+ VerifyNetworkIsListed(TestNetworkName);
194
+ VerifyNetworkIsNotListed(TestNetworkName2);
195
+ }
196
+
197
+ WSLC_TEST_METHOD(WSLCE2E_Network_Prune_Filter_MalformedValue)
198
+ {
199
+ const auto result = RunWslc(L"network prune --filter label");
200
+ result.Verify({.Stdout = GetHelpMessage(), .Stderr = Localization::WSLCCLI_InvalidFilterError(L"label") + L"\r\n", .ExitCode = 1});
201
+ }
202
+
203
+ WSLC_TEST_METHOD(WSLCE2E_Network_Prune_Filter_InvalidKey)
204
+ {
205
+ const auto result = RunWslc(L"network prune --filter color=red");
206
+ result.Verify({.Stdout = L"", .Stderr = L"invalid filter 'color'\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
207
+ }
208
+
209
+private:
210
+ const TestImage& DebianImage = DebianTestImage();
211
+ const std::wstring TestNetworkName = L"wslc-e2e-network-prune";
212
+ const std::wstring TestNetworkName2 = L"wslc-e2e-network-prune-2";
213
+ const std::wstring WslcContainerName = L"wslc-network-prune-test-container";
214
+
215
+ void CleanUpAllTestState()
216
+ {
217
+ EnsureContainerDoesNotExist(WslcContainerName);
218
+ EnsureNetworkDoesNotExist(TestNetworkName);
219
+ EnsureNetworkDoesNotExist(TestNetworkName2);
220
+ }
221
+
222
+ std::wstring GetHelpMessage() const
223
+ {
224
+ std::wstringstream output;
225
+ output << GetWslcHeader() //
226
+ << GetDescription() //
227
+ << GetUsage() //
228
+ << GetAvailableOptions();
229
+ return output.str();
230
+ }
231
+
232
+ std::wstring GetDescription() const
233
+ {
234
+ return Localization::WSLCCLI_NetworkPruneLongDesc() + L"\r\n\r\n";
235
+ }
236
+
237
+ std::wstring GetUsage() const
238
+ {
239
+ return L"Usage: wslc network prune [<options>]\r\n\r\n";
240
+ }
241
+
242
+ std::wstring GetAvailableOptions() const
243
+ {
244
+ std::wstringstream options;
245
+ options << L"The following options are available:\r\n"
246
+ << L" -f,--filter " << Localization::WSLCCLI_FilterArgDescription() << L"\r\n"
247
+ << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n"
248
+ << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
249
+ << L"\r\n";
250
+ return options.str();
251
+ }
252
+};
253
+} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2ENetworkTests.cpp
+1
@@ -71,6 +71,7 @@ private:
71
{L"remove", Localization::WSLCCLI_NetworkRemoveDesc()},
72
{L"inspect", Localization::WSLCCLI_NetworkInspectDesc()},
73
{L"list", Localization::WSLCCLI_NetworkListDesc()},
74
+ {L"prune", Localization::WSLCCLI_NetworkPruneDesc()},
75
};
76
77
size_t maxLen = 0;