@samitouri / QOSAMI-WSL / commits / 643e0526

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