[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;