Add PruneNetworks to WSLC service API (#40805)
beena352 committed
Jun 15, 2026 at 11:33 UTC
643e052635c18943bab2d190a5f9dc7532dff91e
7 files changed
+216
src/windows/inc/docker_schema.h
+7
@@ -427,6 +427,13 @@ struct PruneVolumeResult
427
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(PruneVolumeResult, VolumesDeleted, SpaceReclaimed);
428
};
429
430
+struct PruneNetworkResult
431
+{
432
+ std::optional<std::vector<std::string>> NetworksDeleted;
433
+
434
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(PruneNetworkResult, NetworksDeleted);
435
+};
436
+
437
struct ImportStatus
438
{
439
std::string status;
src/windows/service/inc/wslc.idl
+3
@@ -716,6 +716,8 @@ typedef struct _WSLCNetworkOptions
716
ULONG LabelsCount;
717
} WSLCNetworkOptions;
718
719
+typedef char WSLCNetworkName[WSLC_MAX_NETWORK_NAME_LENGTH + 1];
720
+
721
typedef struct _WSLCNetworkInformation
722
{
723
char Name[WSLC_MAX_NETWORK_NAME_LENGTH + 1];
@@ -852,6 +854,7 @@ interface IWSLCSession : IUnknown
854
HRESULT DeleteNetwork([in] LPCSTR Name);
855
HRESULT ListNetworks([out, size_is(, *Count)] WSLCNetworkInformation** Networks, [out] ULONG* Count);
856
HRESULT InspectNetwork([in] LPCSTR Name, [out] LPSTR* Output);
857
+ HRESULT PruneNetworks([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out, size_is(, *NetworksCount)] WSLCNetworkName** Networks, [out] ULONG* NetworksCount);
858
859
HRESULT RegisterCrashDumpCallback([in] ICrashDumpCallback* Callback, [out] IUnknown** Subscription);
860
}
src/windows/wslcsession/DockerHTTPClient.cpp
+12
@@ -469,6 +469,18 @@ docker_schema::Network DockerHTTPClient::InspectNetwork(const std::string& Name)
469
return Transaction<docker_schema::EmptyRequest, docker_schema::Network>(verb::get, URL::Create("/networks/{}", Name));
470
}
471
472
+docker_schema::PruneNetworkResult DockerHTTPClient::PruneNetworks(const std::map<std::string, std::vector<std::string>>& filters)
473
+{
474
+ auto url = URL::Create("/networks/prune");
475
+
476
+ if (!filters.empty())
477
+ {
478
+ url.SetParameter("filters", nlohmann::json(filters).dump());
479
+ }
480
+
481
+ return Transaction<docker_schema::EmptyRequest, docker_schema::PruneNetworkResult>(verb::post, url);
482
+}
483
+
484
wil::unique_socket DockerHTTPClient::ContainerLogs(const std::string& Id, WSLCLogsFlags Flags, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail)
485
{
486
auto url = URL::Create("/containers/{}/logs", Id);
src/windows/wslcsession/DockerHTTPClient.h
+1
@@ -152,6 +152,7 @@ public:
152
common::docker_schema::Network InspectNetwork(const std::string& Name);
153
void ConnectContainerToNetwork(const std::string& NetworkName, const common::docker_schema::ContainerNetworkRequest& Request);
154
void DisconnectContainerFromNetwork(const std::string& NetworkName, const common::docker_schema::ContainerNetworkRequest& Request);
155
+ common::docker_schema::PruneNetworkResult PruneNetworks(const std::map<std::string, std::vector<std::string>>& filters = {});
156
157
// Image management.
158
std::unique_ptr<HTTPRequestContext> PullImage(
src/windows/wslcsession/WSLCSession.cpp
+73
@@ -2516,6 +2516,79 @@ try
2516
}
2517
CATCH_RETURN();
2518
2519
+HRESULT WSLCSession::PruneNetworks(const WSLCFilter* Filters, ULONG FiltersCount, WSLCNetworkName** Networks, ULONG* NetworksCount)
2520
+try
2521
+{
2522
+ WSLCExecutionContext context(this);
2523
+
2524
+ RETURN_HR_IF_NULL(E_POINTER, Networks);
2525
+ RETURN_HR_IF_NULL(E_POINTER, NetworksCount);
2526
+ *Networks = nullptr;
2527
+ *NetworksCount = 0;
2528
+
2529
+ auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
2530
+
2531
+ // Scope the prune to WSLC-managed networks.
2532
+ filters["label"].push_back(WSLCNetworkManagedLabel);
2533
+
2534
+ auto lock = m_lock.lock_shared();
2535
+ THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient);
2536
+ THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
2537
+
2538
+ std::lock_guard networksLock(m_networksLock);
2539
+
2540
+ docker_schema::PruneNetworkResult pruneResult;
2541
+ try
2542
+ {
2543
+ pruneResult = m_dockerClient->PruneNetworks(filters);
2544
+ }
2545
+ CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune networks");
2546
+
2547
+ if (!pruneResult.NetworksDeleted.has_value() || pruneResult.NetworksDeleted->empty())
2548
+ {
2549
+ return S_OK;
2550
+ }
2551
+
2552
+ std::vector<std::string> deleted;
2553
+ deleted.reserve(pruneResult.NetworksDeleted->size());
2554
+ for (const auto& name : *pruneResult.NetworksDeleted)
2555
+ {
2556
+ // Only report networks that we manage.
2557
+ if (!m_networks.contains(name))
2558
+ {
2559
+ WSL_LOG("PrunedUnknownNetwork", TraceLoggingValue(name.c_str(), "NetworkName"));
2560
+ continue;
2561
+ }
2562
+ deleted.push_back(name);
2563
+ }
2564
+
2565
+ if (deleted.empty())
2566
+ {
2567
+ return S_OK;
2568
+ }
2569
+
2570
+ // Erase before marshalling: docker has already pruned these, so m_networks must stay in sync.
2571
+ for (const auto& name : deleted)
2572
+ {
2573
+ m_networks.erase(name);
2574
+ }
2575
+
2576
+ WSL_LOG("NetworksPruned", TraceLoggingValue(static_cast<ULONG>(deleted.size()), "Count"));
2577
+
2578
+ auto output = wil::make_unique_cotaskmem<WSLCNetworkName[]>(deleted.size());
2579
+ for (size_t i = 0; i < deleted.size(); ++i)
2580
+ {
2581
+ THROW_HR_IF_MSG(
2582
+ E_UNEXPECTED, strcpy_s(output[i], deleted[i].c_str()) != 0, "Unexpected network name length: %hs", deleted[i].c_str());
2583
+ }
2584
+
2585
+ *Networks = output.release();
2586
+ *NetworksCount = static_cast<ULONG>(deleted.size());
2587
+
2588
+ return S_OK;
2589
+}
2590
+CATCH_RETURN();
2591
+
2592
bool WSLCSession::WaitForEventOrSessionTerminating(HANDLE Event, std::chrono::milliseconds Timeout) const
2593
{
2594
const HANDLE waitHandles[] = {Event, m_sessionTerminatingEvent.get()};
src/windows/wslcsession/WSLCSession.h
+3
@@ -177,6 +177,9 @@ public:
177
IFACEMETHOD(DeleteNetwork)(_In_ LPCSTR Name) override;
178
IFACEMETHOD(ListNetworks)(_Out_ WSLCNetworkInformation** Networks, _Out_ ULONG* Count) override;
179
IFACEMETHOD(InspectNetwork)(_In_ LPCSTR Name, _Out_ LPSTR* Output) override;
180
+ IFACEMETHOD(PruneNetworks)
181
+ (_In_reads_opt_(FiltersCount) const WSLCFilter* Filters, _In_ ULONG FiltersCount, _Out_ WSLCNetworkName** Networks, _Out_ ULONG* NetworksCount)
182
+ override;
183
184
IFACEMETHOD(Terminate()) override;
185
test/windows/WSLCTests.cpp
+117
@@ -4883,6 +4883,123 @@ class WSLCTests
4883
VERIFY_ARE_EQUAL(WSLC_E_NETWORK_NOT_FOUND, m_defaultSession->DeleteNetwork(networkName.c_str()));
4884
}
4885
4886
+ void CreateNamedNetwork(const std::string& Name, const std::vector<WSLCLabel>& Labels = {})
4887
+ {
4888
+ WSLCNetworkOptions options{};
4889
+ options.Name = Name.c_str();
4890
+ options.Driver = "bridge";
4891
+ options.Labels = Labels.empty() ? nullptr : Labels.data();
4892
+ options.LabelsCount = static_cast<ULONG>(Labels.size());
4893
+
4894
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options, nullptr));
4895
+ }
4896
+
4897
+ WSLC_TEST_METHOD(PruneNetworksTest)
4898
+ {
4899
+ auto expectPrune = [&](const std::vector<std::string>& expected,
4900
+ const std::vector<WSLCFilter>& filters = {},
4901
+ const std::source_location& source = std::source_location::current()) {
4902
+ const WSLCFilter* filtersPtr = filters.empty() ? nullptr : filters.data();
4903
+ const ULONG filtersCount = static_cast<ULONG>(filters.size());
4904
+
4905
+ wil::unique_cotaskmem_array_ptr<WSLCNetworkName> deleted;
4906
+ VERIFY_SUCCEEDED(m_defaultSession->PruneNetworks(filtersPtr, filtersCount, deleted.addressof(), deleted.size_address<ULONG>()));
4907
+
4908
+ std::vector<std::string> names;
4909
+ for (const auto& n : deleted)
4910
+ {
4911
+ names.emplace_back(n);
4912
+ }
4913
+
4914
+ VerifyAreEqualUnordered(expected, names, source);
4915
+ };
4916
+
4917
+ // Prune with no managed networks present returns empty.
4918
+ expectPrune({});
4919
+
4920
+ // Prune removes unused managed networks.
4921
+ {
4922
+ const std::string a = "wslc-prune-net-a";
4923
+ const std::string b = "wslc-prune-net-b";
4924
+
4925
+ auto cleanup = wil::scope_exit([&]() {
4926
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(a.c_str()));
4927
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(b.c_str()));
4928
+ });
4929
+
4930
+ CreateNamedNetwork(a);
4931
+ CreateNamedNetwork(b);
4932
+
4933
+ expectPrune({a, b});
4934
+
4935
+ wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
4936
+ VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4937
+ for (const auto& n : networks)
4938
+ {
4939
+ VERIFY_ARE_NOT_EQUAL(a, std::string(n.Name));
4940
+ VERIFY_ARE_NOT_EQUAL(b, std::string(n.Name));
4941
+ }
4942
+
4943
+ cleanup.release();
4944
+ }
4945
+
4946
+ // Label filter (key=value).
4947
+ {
4948
+ const std::string labeled = "wslc-prune-net-labeled";
4949
+ const std::string unlabeled = "wslc-prune-net-unlabeled";
4950
+
4951
+ auto cleanup = wil::scope_exit([&]() {
4952
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(labeled.c_str()));
4953
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(unlabeled.c_str()));
4954
+ });
4955
+
4956
+ CreateNamedNetwork(labeled, {{"wslc-prune-net-test", "yes"}});
4957
+ CreateNamedNetwork(unlabeled);
4958
+
4959
+ expectPrune({labeled}, {{"label", "wslc-prune-net-test=yes"}});
4960
+
4961
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(unlabeled.c_str()));
4962
+ cleanup.release();
4963
+ }
4964
+
4965
+ // Label filter (negation).
4966
+ {
4967
+ const std::string keep = "wslc-prune-net-keep";
4968
+ const std::string drop = "wslc-prune-net-drop";
4969
+
4970
+ auto cleanup = wil::scope_exit([&]() {
4971
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(keep.c_str()));
4972
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(drop.c_str()));
4973
+ });
4974
+
4975
+ CreateNamedNetwork(keep, {{"wslc-prune-net-keep", "yes"}});
4976
+ CreateNamedNetwork(drop);
4977
+
4978
+ expectPrune({drop}, {{"label!", "wslc-prune-net-keep"}});
4979
+
4980
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(keep.c_str()));
4981
+ cleanup.release();
4982
+ }
4983
+
4984
+ // Filter with null Key rejected.
4985
+ {
4986
+ WSLCFilter filters[] = {{nullptr, "true"}};
4987
+
4988
+ wil::unique_cotaskmem_array_ptr<WSLCNetworkName> deleted;
4989
+ VERIFY_ARE_EQUAL(
4990
+ E_POINTER, m_defaultSession->PruneNetworks(filters, ARRAYSIZE(filters), deleted.addressof(), deleted.size_address<ULONG>()));
4991
+ }
4992
+
4993
+ // Filter with null Value rejected.
4994
+ {
4995
+ WSLCFilter filters[] = {{"label", nullptr}};
4996
+
4997
+ wil::unique_cotaskmem_array_ptr<WSLCNetworkName> deleted;
4998
+ VERIFY_ARE_EQUAL(
4999
+ E_POINTER, m_defaultSession->PruneNetworks(filters, ARRAYSIZE(filters), deleted.addressof(), deleted.size_address<ULONG>()));
5000
+ }
5001
+ }
5002
+
5003
WSLC_TEST_METHOD(NetworkCreateWithSubnetTest)
5004
{
5005
const std::string networkName = "subnet-test-net";