Add volume listing and pruning functionality (#40457)

Kevin Vega committed May 18, 2026 at 14:30 UTC 34bebd53cc0a231b22a8dd98451ab7ddf74ccb90
14 files changed +549 -76
localization/strings/en-US/Resources.resw
+4
@@ -2264,6 +2264,10 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2264 <value>Invalid image: '{}'</value>
2265 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2266 </data>
2267 + <data name = "MessageWslcInvalidFilter" xml:space = "preserve" >
2268 + <value>Invalid filter: '{}'</value>
2269 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2270 + </data>
2271 <data name = "MessageWslcInvalidName" xml:space = "preserve" >
2272 <value>Invalid name: '{}'</value>
2273 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/windows/inc/docker_schema.h
+8
@@ -386,6 +386,14 @@ struct PruneImageResult
386 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(PruneImageResult, ImagesDeleted, SpaceReclaimed);
387 };
388
389 +struct PruneVolumeResult
390 +{
391 + std::optional<std::vector<std::string>> VolumesDeleted;
392 + uint64_t SpaceReclaimed{};
393 +
394 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(PruneVolumeResult, VolumesDeleted, SpaceReclaimed);
395 +};
396 +
397 struct ImportStatus
398 {
399 std::string status;
src/windows/service/inc/wslc.idl
+2 -16
@@ -636,13 +636,6 @@ typedef struct _WSLCVolumeInformation
636 char Driver[WSLC_MAX_VOLUME_DRIVER_LENGTH + 1];
637 } WSLCVolumeInformation;
638
639 -typedef struct _WSLCPruneVolumesResults
640 -{
641 - [unique, size_is(VolumesCount)] WSLCVolumeName* Volumes;
642 - ULONG VolumesCount;
643 - ULONGLONG SpaceReclaimed;
644 -} WSLCPruneVolumesResults;
645 -
639 typedef struct _WSLCNetworkOptions
640 {
641 LPCSTR Name;
@@ -667,13 +660,6 @@ typedef struct _WSLCPruneLabelFilter
660 BOOL Present;
661 } WSLCPruneLabelFilter;
662
670 -typedef struct _WSLCPruneVolumesOptions
671 -{
672 - BOOL All; // If TRUE, prune all unused volumes. If FALSE, only anonymous volumes.
673 - [unique, size_is(LabelsCount)] const WSLCPruneLabelFilter* Labels;
674 - ULONG LabelsCount;
675 -} WSLCPruneVolumesOptions;
676 -
663 typedef struct _WSLCPruneContainersResults
664 {
665 [unique, size_is(ContainersCount)] WSLCContainerId* Containers;
@@ -800,12 +786,12 @@ interface IWSLCSession : IUnknown
786 // Volume management.
787 HRESULT CreateVolume([in] const WSLCVolumeOptions* Options, [out] WSLCVolumeInformation* VolumeInfo);
788 HRESULT DeleteVolume([in] LPCSTR Name);
803 - HRESULT ListVolumes([out, size_is(, *Count)] WSLCVolumeInformation** Volumes, [out] ULONG* Count);
789 + HRESULT ListVolumes([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out, size_is(, *Count)] WSLCVolumeInformation** Volumes, [out] ULONG* Count);
790 HRESULT InspectVolume([in] LPCSTR Name, [out] LPSTR* Output);
791
792 HRESULT Authenticate([in] LPCSTR ServerAddress, [in] LPCSTR Username, [in] LPCSTR Password, [out] LPSTR* IdentityToken);
793 HRESULT PushImage([in] LPCSTR Image, [in] LPCSTR RegistryAuthenticationInformation, [in, unique] IProgressCallback* ProgressCallback);
808 - HRESULT PruneVolumes([in, unique] const WSLCPruneVolumesOptions* Options, [out] WSLCPruneVolumesResults* Results);
794 + HRESULT PruneVolumes([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out, size_is(, *VolumesCount)] WSLCVolumeName** Volumes, [out] ULONG* VolumesCount, [out] ULONGLONG* SpaceReclaimed);
795
796 // Network management.
797 HRESULT CreateNetwork([in] const WSLCNetworkOptions* Options);
src/windows/wslc/services/VolumeService.cpp
+1 -1
@@ -63,7 +63,7 @@ std::vector<WSLCVolumeInformation> VolumeService::List(models::Session& session)
63 {
64 wil::unique_cotaskmem_array_ptr<WSLCVolumeInformation> rawVolumes;
65 ULONG count = 0;
66 - THROW_IF_FAILED(session.Get()->ListVolumes(&rawVolumes, &count));
66 + THROW_IF_FAILED(session.Get()->ListVolumes(nullptr, 0, &rawVolumes, &count));
67
68 std::vector<WSLCVolumeInformation> volumes;
69 volumes.reserve(count);
src/windows/wslcsession/DockerHTTPClient.cpp
+34 -4
@@ -66,9 +66,20 @@ nlohmann::json PruneFiltersToJson(const TFilters& filters)
66 }
67 }
68
69 - if (filters.until.has_value())
69 + if constexpr (requires { filters.all; })
70 {
71 - j["until"] = nlohmann::json::array({std::to_string(filters.until.value())});
71 + if (filters.all.has_value() && filters.all.value())
72 + {
73 + j["all"] = nlohmann::json::array({"true"});
74 + }
75 + }
76 +
77 + if constexpr (requires { filters.until; })
78 + {
79 + if (filters.until.has_value())
80 + {
81 + j["until"] = nlohmann::json::array({std::to_string(filters.until.value())});
82 + }
83 }
84
85 if (!filters.presentLabels.empty())
@@ -453,12 +464,31 @@ void DockerHTTPClient::RemoveVolume(const std::string& Name)
464 Transaction(verb::delete_, URL::Create("/volumes/{}", Name));
465 }
466
456 -std::vector<docker_schema::Volume> DockerHTTPClient::ListVolumes()
467 +std::vector<docker_schema::Volume> DockerHTTPClient::ListVolumes(const std::map<std::string, std::vector<std::string>>& filters)
468 {
458 - auto response = Transaction<docker_schema::EmptyRequest, docker_schema::ListVolumesResponse>(verb::get, URL::Create("/volumes"));
469 + auto url = URL::Create("/volumes");
470 +
471 + if (!filters.empty())
472 + {
473 + url.SetParameter("filters", nlohmann::json(filters).dump());
474 + }
475 +
476 + auto response = Transaction<docker_schema::EmptyRequest, docker_schema::ListVolumesResponse>(verb::get, url);
477 return response.Volumes;
478 }
479
480 +docker_schema::PruneVolumeResult DockerHTTPClient::PruneVolumes(const std::map<std::string, std::vector<std::string>>& filters)
481 +{
482 + auto url = URL::Create("/volumes/prune");
483 +
484 + if (!filters.empty())
485 + {
486 + url.SetParameter("filters", nlohmann::json(filters).dump());
487 + }
488 +
489 + return Transaction<docker_schema::EmptyRequest, docker_schema::PruneVolumeResult>(verb::post, url);
490 +}
491 +
492 docker_schema::CreateNetworkResponse DockerHTTPClient::CreateNetwork(const docker_schema::CreateNetwork& Request)
493 {
494 return Transaction(verb::post, URL::Create("/networks/create"), Request);
src/windows/wslcsession/DockerHTTPClient.h
+2 -1
@@ -142,7 +142,8 @@ public:
142 common::docker_schema::Volume CreateVolume(const common::docker_schema::CreateVolume& Request);
143 common::docker_schema::Volume InspectVolume(const std::string& Name);
144 void RemoveVolume(const std::string& Name);
145 - std::vector<common::docker_schema::Volume> ListVolumes();
145 + std::vector<common::docker_schema::Volume> ListVolumes(const std::map<std::string, std::vector<std::string>>& filters = {});
146 + common::docker_schema::PruneVolumeResult PruneVolumes(const std::map<std::string, std::vector<std::string>>& filters = {});
147
148 // Network management.
149 common::docker_schema::CreateNetworkResponse CreateNetwork(const common::docker_schema::CreateNetwork& Request);
src/windows/wslcsession/IWSLCVolume.h
+4
@@ -18,6 +18,7 @@ Abstract:
18 #pragma once
19
20 #include "wslc.h"
21 +#include <map>
22 #include <string>
23
24 namespace wsl::windows::service::wslc {
@@ -35,6 +36,9 @@ public:
36 // driver (which may be "local" for guest volumes).
37 virtual const char* Driver() const noexcept = 0;
38
39 + // The user-specified labels on this volume (excludes the WSLC metadata label).
40 + virtual const std::map<std::string, std::string>& Labels() const noexcept = 0;
41 +
42 // Remove the volume from docker and release any host-side resources
43 // (e.g. detach/delete the VHD for VHD volumes). Throws on failure.
44 virtual void Delete() = 0;
src/windows/wslcsession/WSLCGuestVolume.h
+5
@@ -64,6 +64,11 @@ public:
64 {
65 return WSLCGuestVolumeDriver;
66 }
67 + const std::map<std::string, std::string>& Labels() const noexcept override
68 + {
69 + return m_labels;
70 + }
71 +
72 void Delete() override;
73 std::string Inspect() const override;
74 WSLCVolumeInformation GetVolumeInformation() const override;
src/windows/wslcsession/WSLCSession.cpp
+48 -10
@@ -2093,7 +2093,7 @@ try
2093 }
2094 CATCH_RETURN();
2095
2096 -HRESULT WSLCSession::ListVolumes(WSLCVolumeInformation** Volumes, ULONG* Count)
2096 +HRESULT WSLCSession::ListVolumes(const WSLCFilter* Filters, ULONG FiltersCount, WSLCVolumeInformation** Volumes, ULONG* Count)
2097 try
2098 {
2099 COMServiceExecutionContext context;
@@ -2104,10 +2104,12 @@ try
2104 *Volumes = nullptr;
2105 *Count = 0;
2106
2107 + auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
2108 +
2109 auto lock = m_lock.lock_shared();
2110 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes);
2111
2110 - auto volumeList = m_volumes->ListVolumes();
2112 + auto volumeList = m_volumes->ListVolumes(std::move(filters));
2113
2114 if (volumeList.empty())
2115 {
@@ -2115,10 +2117,7 @@ try
2117 }
2118
2119 auto output = wil::make_unique_cotaskmem<WSLCVolumeInformation[]>(volumeList.size());
2118 - for (size_t i = 0; i < volumeList.size(); i++)
2119 - {
2120 - output[i] = volumeList[i];
2121 - }
2120 + memcpy(output.get(), volumeList.data(), volumeList.size() * sizeof(WSLCVolumeInformation));
2121
2122 *Count = static_cast<ULONG>(volumeList.size());
2123 *Volumes = output.release();
@@ -2149,12 +2148,51 @@ try
2148 }
2149 CATCH_RETURN();
2150
2152 -HRESULT WSLCSession::PruneVolumes(const WSLCPruneVolumesOptions* /*Options*/, WSLCPruneVolumesResults* /*Results*/)
2151 +HRESULT WSLCSession::PruneVolumes(const WSLCFilter* Filters, ULONG FiltersCount, WSLCVolumeName** Volumes, ULONG* VolumesCount, ULONGLONG* SpaceReclaimed)
2152 +try
2153 {
2154 - // TODO: Implement volume pruning. Docker's volume prune API skips bind-mount volumes,
2155 - // so WSLC VHD volumes require custom handling.
2156 - return E_NOTIMPL;
2154 + COMServiceExecutionContext context;
2155 +
2156 + RETURN_HR_IF_NULL(E_POINTER, Volumes);
2157 + RETURN_HR_IF_NULL(E_POINTER, VolumesCount);
2158 + RETURN_HR_IF_NULL(E_POINTER, SpaceReclaimed);
2159 + *Volumes = nullptr;
2160 + *VolumesCount = 0;
2161 + *SpaceReclaimed = 0;
2162 +
2163 + auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
2164 +
2165 + auto lock = m_lock.lock_shared();
2166 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes);
2167 +
2168 + WSLCVolumes::PruneVolumesResult pruneResult;
2169 + try
2170 + {
2171 + pruneResult = m_volumes->PruneVolumes(filters);
2172 + }
2173 + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune volumes");
2174 +
2175 + *SpaceReclaimed = pruneResult.SpaceReclaimed;
2176 +
2177 + if (!pruneResult.Volumes.empty())
2178 + {
2179 + auto output = wil::make_unique_cotaskmem<WSLCVolumeName[]>(pruneResult.Volumes.size());
2180 + for (size_t i = 0; i < pruneResult.Volumes.size(); ++i)
2181 + {
2182 + THROW_HR_IF_MSG(
2183 + E_UNEXPECTED,
2184 + strcpy_s(output[i], pruneResult.Volumes[i].c_str()) != 0,
2185 + "Unexpected volume name length: %hs",
2186 + pruneResult.Volumes[i].c_str());
2187 + }
2188 +
2189 + *Volumes = output.release();
2190 + *VolumesCount = static_cast<ULONG>(pruneResult.Volumes.size());
2191 + }
2192 +
2193 + return S_OK;
2194 }
2195 +CATCH_RETURN();
2196
2197 int WSLCSession::StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs)
2198 {
src/windows/wslcsession/WSLCSession.h
+9 -2
@@ -129,9 +129,16 @@ public:
129 // Volume management.
130 IFACEMETHOD(CreateVolume)(_In_ const WSLCVolumeOptions* Options, _Out_ WSLCVolumeInformation* VolumeInfo) override;
131 IFACEMETHOD(DeleteVolume)(_In_ LPCSTR Name) override;
132 - IFACEMETHOD(ListVolumes)(_Out_ WSLCVolumeInformation** Volumes, _Out_ ULONG* Count) override;
132 + IFACEMETHOD(ListVolumes)
133 + (_In_reads_opt_(FiltersCount) const WSLCFilter* Filters, _In_ ULONG FiltersCount, _Out_ WSLCVolumeInformation** Volumes, _Out_ ULONG* Count)
134 + override;
135 IFACEMETHOD(InspectVolume)(_In_ LPCSTR Name, _Out_ LPSTR* Output) override;
134 - IFACEMETHOD(PruneVolumes)(_In_opt_ const WSLCPruneVolumesOptions* Options, _Out_ WSLCPruneVolumesResults* Results) override;
136 + IFACEMETHOD(PruneVolumes)
137 + (_In_reads_opt_(FiltersCount) const WSLCFilter* Filters,
138 + _In_ ULONG FiltersCount,
139 + _Out_ WSLCVolumeName** Volumes,
140 + _Out_ ULONG* VolumesCount,
141 + _Out_ ULONGLONG* SpaceReclaimed) override;
142
143 // Network management.
144 IFACEMETHOD(CreateNetwork)(_In_ const WSLCNetworkOptions* Options) override;
src/windows/wslcsession/WSLCVhdVolume.h
+5
@@ -69,6 +69,11 @@ public:
69 {
70 return WSLCVhdVolumeDriver;
71 }
72 + const std::map<std::string, std::string>& Labels() const noexcept override
73 + {
74 + return m_labels;
75 + }
76 +
77 void Delete() override;
78 std::string Inspect() const override;
79 WSLCVolumeInformation GetVolumeInformation() const override;
src/windows/wslcsession/WSLCVolumes.cpp
+81 -2
@@ -140,15 +140,51 @@ void WSLCVolumes::DeleteVolume(LPCSTR Name)
140 m_expectedEvents.emplace_back(Name, VolumeEvent::Destroy);
141 }
142
143 -std::vector<WSLCVolumeInformation> WSLCVolumes::ListVolumes() const
143 +std::vector<WSLCVolumeInformation> WSLCVolumes::ListVolumes(std::map<std::string, std::vector<std::string>>&& Filters) const
144 {
145 + // Pull the driver filter out and forward everything else to docker for filtering.
146 + // Driver filter is special-cased because our driver concept doesn't map 1:1 to docker's.
147 + std::vector<std::string> drivers;
148 + auto it = Filters.find("driver");
149 + if (it != Filters.end())
150 + {
151 + drivers = std::move(it->second);
152 + Filters.erase(it);
153 + }
154 +
155 + std::vector<wsl::windows::common::docker_schema::Volume> dockerVolumes;
156 + try
157 + {
158 + dockerVolumes = m_dockerClient.ListVolumes(Filters);
159 + }
160 + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list volumes");
161 +
162 + std::unordered_set<std::string> dockerVolumeNames;
163 + dockerVolumeNames.reserve(dockerVolumes.size());
164 + for (const auto& vol : dockerVolumes)
165 + {
166 + dockerVolumeNames.insert(vol.Name);
167 + }
168 +
169 auto lock = m_lock.lock_shared();
170
171 std::vector<WSLCVolumeInformation> result;
148 - result.reserve(m_volumes.size());
172 + result.reserve(dockerVolumeNames.size());
173
174 for (const auto& [name, vol] : m_volumes)
175 {
176 + // Must be in docker's filtered list.
177 + if (!dockerVolumeNames.contains(name))
178 + {
179 + continue;
180 + }
181 +
182 + // Apply driver filter using the WSLC driver names.
183 + if (!drivers.empty() && std::ranges::find(drivers, vol->Driver()) == drivers.end())
184 + {
185 + continue;
186 + }
187 +
188 result.push_back(vol->GetVolumeInformation());
189 }
190
@@ -171,6 +207,49 @@ bool WSLCVolumes::ContainsVolume(const std::string& Name) const
207 return m_volumes.contains(Name);
208 }
209
210 +WSLCVolumes::PruneVolumesResult WSLCVolumes::PruneVolumes(const std::map<std::string, std::vector<std::string>>& Filters)
211 +{
212 + auto lock = m_lock.lock_exclusive();
213 +
214 + auto dockerResult = m_dockerClient.PruneVolumes(Filters);
215 +
216 + PruneVolumesResult result{};
217 + result.SpaceReclaimed = dockerResult.SpaceReclaimed;
218 +
219 + if (!dockerResult.VolumesDeleted.has_value() || dockerResult.VolumesDeleted->empty())
220 + {
221 + return result;
222 + }
223 +
224 + result.Volumes.reserve(dockerResult.VolumesDeleted->size());
225 +
226 + // TODO: VHD volumes are exposed to docker as bind mounts, which docker's volume
227 + // prune skips. So this only ever prunes guest volumes today. VHD volume pruning
228 + // requires custom handling that's not implemented yet.
229 + for (const auto& name : dockerResult.VolumesDeleted.value())
230 + {
231 + // Only report volumes that we manage.
232 + auto it = m_volumes.find(name);
233 + if (it == m_volumes.end())
234 + {
235 + WSL_LOG("PrunedUnknownVolume", TraceLoggingValue(name.c_str(), "name"));
236 + continue;
237 + }
238 +
239 + try
240 + {
241 + it->second->OnDeleted();
242 + }
243 + CATCH_LOG_MSG("Failed to release host resources for pruned volume: %hs", name.c_str());
244 +
245 + m_volumes.erase(it);
246 + m_expectedEvents.emplace_back(name, VolumeEvent::Destroy);
247 + result.Volumes.push_back(name);
248 + }
249 +
250 + return result;
251 +}
252 +
253 __requires_lock_held(m_lock) void WSLCVolumes::OpenVolumeExclusiveLockHeld(const std::string& volumeName)
254 {
255 if (volumeName.empty() || m_volumes.contains(volumeName))
src/windows/wslcsession/WSLCVolumes.h
+10 -1
@@ -40,7 +40,16 @@ public:
40
41 void DeleteVolume(_In_ LPCSTR Name);
42
43 - std::vector<WSLCVolumeInformation> ListVolumes() const;
43 + std::vector<WSLCVolumeInformation> ListVolumes(std::map<std::string, std::vector<std::string>>&& Filters) const;
44 +
45 + struct PruneVolumesResult
46 + {
47 + std::vector<std::string> Volumes;
48 + std::uint64_t SpaceReclaimed{};
49 + };
50 +
51 + PruneVolumesResult PruneVolumes(_In_ const std::map<std::string, std::vector<std::string>>& Filters);
52 +
53 std::string InspectVolume(_In_ const std::string& Name) const;
54
55 bool ContainsVolume(_In_ const std::string& Name) const;
test/windows/WSLCTests.cpp
+336 -39
@@ -549,6 +549,40 @@ class WSLCTests
549 return std::move(deletedImages);
550 }
551
552 + std::set<std::string> ListVolumes(const std::vector<WSLCFilter>& Filters = {})
553 + {
554 + const WSLCFilter* filtersPtr = Filters.empty() ? nullptr : Filters.data();
555 + const ULONG filtersCount = static_cast<ULONG>(Filters.size());
556 +
557 + wil::unique_cotaskmem_array_ptr<WSLCVolumeInformation> volumes;
558 + VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(filtersPtr, filtersCount, volumes.addressof(), volumes.size_address<ULONG>()));
559 +
560 + std::set<std::string> names;
561 + for (const auto& v : volumes)
562 + {
563 + names.insert(v.Name);
564 + }
565 + return names;
566 + }
567 +
568 + void CreateNamedVolume(
569 + const std::string& Name,
570 + const std::string& Driver,
571 + const std::vector<WSLCLabel>& Labels = {},
572 + const std::vector<WSLCDriverOption>& DriverOpts = {})
573 + {
574 + WSLCVolumeOptions options{};
575 + options.Name = Name.c_str();
576 + options.Driver = Driver.c_str();
577 + options.DriverOpts = DriverOpts.empty() ? nullptr : DriverOpts.data();
578 + options.DriverOptsCount = static_cast<ULONG>(DriverOpts.size());
579 + options.Labels = Labels.empty() ? nullptr : Labels.data();
580 + options.LabelsCount = static_cast<ULONG>(Labels.size());
581 +
582 + WSLCVolumeInformation info{};
583 + VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&options, &info));
584 + }
585 +
586 WSLC_TEST_METHOD(PullImage)
587 {
588 {
@@ -1038,7 +1072,7 @@ class WSLCTests
1072
1073 // Test with single label filter
1074 {
1041 - WSLCLabel labels[] = {{.Key = "test.label", .Value = nullptr}};
1075 + WSLCLabel labels[] = {{"test.label", ""}};
1076 options.Labels = labels;
1077 options.LabelsCount = 1;
1078
@@ -1047,7 +1081,7 @@ class WSLCTests
1081
1082 // Test with multiple label filters (labels are AND'ed together)
1083 {
1050 - WSLCLabel labels[] = {{.Key = "test.label1", .Value = nullptr}, {.Key = "test.label2", .Value = "value"}};
1084 + WSLCLabel labels[] = {{"test.label1", ""}, {"test.label2", "value"}};
1085 options.Labels = labels;
1086 options.LabelsCount = 2;
1087
@@ -1980,23 +2014,7 @@ class WSLCTests
2014 VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build:latest"));
2015 ExpectImagePresent(*m_defaultSession, "wslc-test-build:latest");
2016
1983 - auto listAnonymousVolumes = [&]() {
1984 - wil::unique_cotaskmem_array_ptr<WSLCVolumeInformation> volumes;
1985 - VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address<ULONG>()));
1986 -
1987 - std::vector<std::string> names;
1988 -
1989 - // TODO: Replace with filter for anonymous volumes in ListVolumes API.
1990 - for (const auto& vol : volumes)
1991 - {
1992 - if (std::string(vol.Driver) == "guest")
1993 - {
1994 - names.push_back(vol.Name);
1995 - }
1996 - }
1997 -
1998 - return names;
1999 - };
2017 + const std::vector<WSLCFilter> anonymousVolumeFilters = {{"driver", "guest"}, {"label", "com.docker.volume.anonymous="}};
2018
2019 // Session-restart scenario: an anonymous volume-backed container survives a session reset.
2020 {
@@ -2035,19 +2053,17 @@ class WSLCTests
2053
2054 // Clean up any leaked anonymous volumes when this block exits.
2055 auto volumeCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2038 - auto volumes = listAnonymousVolumes();
2039 - for (const auto& name : volumes)
2040 - {
2041 - LOG_IF_FAILED(m_defaultSession->DeleteVolume(name.c_str()));
2042 - }
2056 + wil::unique_cotaskmem_array_ptr<WSLCVolumeName> deleted;
2057 + ULONGLONG spaceReclaimed = 0;
2058 + LOG_IF_FAILED(m_defaultSession->PruneVolumes(nullptr, 0, deleted.addressof(), deleted.size_address<ULONG>(), &spaceReclaimed));
2059 });
2060
2045 - VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 1u);
2061 + VERIFY_ARE_EQUAL(ListVolumes(anonymousVolumeFilters).size(), 1u);
2062
2063 VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
2064
2065 // Anonymous volume was NOT deleted by Docker.
2050 - VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 1u);
2066 + VERIFY_ARE_EQUAL(ListVolumes(anonymousVolumeFilters).size(), 1u);
2067 }
2068
2069 // Delete container with WSLCDeleteFlagsDeleteVolumes -> anonymous volume is cleaned up.
@@ -2058,10 +2074,10 @@ class WSLCTests
2074
2075 VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
2076
2061 - VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 1u);
2077 + VERIFY_ARE_EQUAL(ListVolumes(anonymousVolumeFilters).size(), 1u);
2078
2079 VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsDeleteVolumes));
2064 - VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 0u);
2080 + VERIFY_ARE_EQUAL(ListVolumes(anonymousVolumeFilters).size(), 0u);
2081 }
2082
2083 // Container with WSLCContainerFlagsRm -> anonymous volume cleaned up when the container auto-removes on exit.
@@ -2070,10 +2086,10 @@ class WSLCTests
2086 launcher.SetContainerFlags(WSLCContainerFlagsRm);
2087
2088 auto container = launcher.Launch(*m_defaultSession);
2073 - VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 1u);
2089 + VERIFY_ARE_EQUAL(ListVolumes(anonymousVolumeFilters).size(), 1u);
2090 VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
2091
2076 - VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 0u);
2092 + VERIFY_ARE_EQUAL(ListVolumes(anonymousVolumeFilters).size(), 0u);
2093 }
2094 }
2095
@@ -4204,9 +4220,7 @@ class WSLCTests
4220 });
4221
4222 // Verify empty list is returned when no volumes exist.
4207 - wil::unique_cotaskmem_array_ptr<WSLCVolumeInformation> volumes;
4208 - VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address<ULONG>()));
4209 - VERIFY_ARE_EQUAL(0u, volumes.size());
4223 + VERIFY_IS_TRUE(ListVolumes().empty());
4224
4225 // Create a VHD volume and verify list returns one entry.
4226 WSLCDriverOption driverOpts[] = {{"SizeBytes", "1073741824"}};
@@ -4220,7 +4234,8 @@ class WSLCTests
4234 WSLCVolumeInformation volInfo{};
4235 VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&vhdOptions, &volInfo));
4236
4223 - VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address<ULONG>()));
4237 + wil::unique_cotaskmem_array_ptr<WSLCVolumeInformation> volumes;
4238 + VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(nullptr, 0, volumes.addressof(), volumes.size_address<ULONG>()));
4239 VERIFY_ARE_EQUAL(1u, volumes.size());
4240 VERIFY_ARE_EQUAL(std::string(volumes[0].Name), vhdVolumeName);
4241 VERIFY_ARE_EQUAL(std::string(volumes[0].Driver), std::string("vhd"));
@@ -4245,7 +4260,7 @@ class WSLCTests
4260 duplicateVhdOptions.DriverOptsCount = ARRAYSIZE(driverOpts);
4261 VERIFY_ARE_EQUAL(m_defaultSession->CreateVolume(&duplicateVhdOptions, &volInfo), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
4262
4248 - VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address<ULONG>()));
4263 + VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(nullptr, 0, volumes.addressof(), volumes.size_address<ULONG>()));
4264 VERIFY_ARE_EQUAL(2u, volumes.size());
4265
4266 std::map<std::string, std::string> namesToDrivers;
@@ -4283,12 +4298,294 @@ class WSLCTests
4298
4299 // Delete the VHD volume and verify only the guest volume remains.
4300 VERIFY_SUCCEEDED(m_defaultSession->DeleteVolume(vhdVolumeName.c_str()));
4286 - VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address<ULONG>()));
4301 + VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(nullptr, 0, volumes.addressof(), volumes.size_address<ULONG>()));
4302 VERIFY_ARE_EQUAL(1u, volumes.size());
4303 VERIFY_ARE_EQUAL(std::string(volumes[0].Name), guestVolumeName);
4304 VERIFY_ARE_EQUAL(std::string(volumes[0].Driver), std::string("guest"));
4305 }
4306
4307 + WSLC_TEST_METHOD(ListVolumesFilters)
4308 + {
4309 + const std::string vhdA = "wslc-list-vhd-a";
4310 + const std::string vhdB = "wslc-list-vhd-b";
4311 + const std::string guestA = "wslc-list-guest-a";
4312 + const std::string guestB = "wslc-list-guest-b";
4313 + const std::string otherName = "wslc-list-other-name";
4314 + const std::string emptyValVol = "wslc-list-empty-val";
4315 +
4316 + const std::vector<WSLCDriverOption> vhdOpts = {{"SizeBytes", "1073741824"}};
4317 +
4318 + auto cleanup = wil::scope_exit([&]() {
4319 + for (const auto& name : {vhdA, vhdB, guestA, guestB, otherName, emptyValVol})
4320 + {
4321 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(name.c_str()));
4322 + }
4323 + });
4324 +
4325 + CreateNamedVolume(vhdA, "vhd", {{"env", "prod"}}, vhdOpts);
4326 + CreateNamedVolume(vhdB, "vhd", {{"env", "test"}, {"tier", "db"}}, vhdOpts);
4327 + CreateNamedVolume(guestA, "guest", {{"env", "prod"}});
4328 + CreateNamedVolume(guestB, "guest");
4329 + CreateNamedVolume(otherName, "guest", {{"env", "test"}});
4330 + CreateNamedVolume(emptyValVol, "guest", {{"marker", ""}});
4331 +
4332 + auto expectListFails = [&](HRESULT expected, const std::vector<WSLCFilter>& filters) {
4333 + const WSLCFilter* filtersPtr = filters.empty() ? nullptr : filters.data();
4334 + const ULONG filtersCount = static_cast<ULONG>(filters.size());
4335 +
4336 + wil::unique_cotaskmem_array_ptr<WSLCVolumeInformation> volumes;
4337 + VERIFY_ARE_EQUAL(
4338 + expected, m_defaultSession->ListVolumes(filtersPtr, filtersCount, volumes.addressof(), volumes.size_address<ULONG>()));
4339 + };
4340 +
4341 + auto expectList = [&](const std::vector<std::string>& expected,
4342 + const std::vector<WSLCFilter>& filters = {},
4343 + const std::source_location& source = std::source_location::current()) {
4344 + const WSLCFilter* filtersPtr = filters.empty() ? nullptr : filters.data();
4345 + const ULONG filtersCount = static_cast<ULONG>(filters.size());
4346 +
4347 + wil::unique_cotaskmem_array_ptr<WSLCVolumeInformation> volumes;
4348 + VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(filtersPtr, filtersCount, volumes.addressof(), volumes.size_address<ULONG>()));
4349 +
4350 + std::vector<std::string> names;
4351 + for (const auto& v : volumes)
4352 + {
4353 + names.emplace_back(v.Name);
4354 + }
4355 +
4356 + VerifyAreEqualUnordered(expected, names, source);
4357 + };
4358 +
4359 + const std::vector<std::string> all{vhdA, vhdB, guestA, guestB, otherName, emptyValVol};
4360 +
4361 + // No filter returns every volume.
4362 + expectList(all);
4363 +
4364 + // Filter by driver name.
4365 + expectList({vhdA, vhdB}, {{"driver", "vhd"}});
4366 + expectList({guestA, guestB, otherName, emptyValVol}, {{"driver", "guest"}});
4367 + expectList({}, {{"driver", "nonexistent"}});
4368 +
4369 + // Filter by volume name.
4370 + expectList({vhdA, vhdB}, {{"name", "vhd"}});
4371 +
4372 + // Anchored regex matches exactly one volume.
4373 + const auto anchoredVhdA = "^" + vhdA + "$";
4374 + expectList({vhdA}, {{"name", anchoredVhdA.c_str()}});
4375 +
4376 + // Regex name filter.
4377 + expectList({vhdA, vhdB}, {{"name", "vhd-."}});
4378 +
4379 + // Filter by label key (any value matches): label=<key> form.
4380 + expectList({vhdA, vhdB, guestA, otherName}, {{"label", "env"}});
4381 +
4382 + // Filter by label key=value.
4383 + expectList({vhdA, guestA}, {{"label", "env=prod"}});
4384 +
4385 + // Multiple labels are AND'ed together.
4386 + expectList({vhdB}, {{"label", "env=test"}, {"label", "tier=db"}});
4387 +
4388 + // Unknown label key matches nothing.
4389 + expectList({}, {{"label", "nope"}});
4390 +
4391 + // Unknown name matches nothing.
4392 + expectList({}, {{"name", "nope"}});
4393 +
4394 + // Combined driver + name + label filter.
4395 + expectList({vhdA}, {{"driver", "vhd"}, {"name", "a"}, {"label", "env=prod"}});
4396 +
4397 + // Dangling filter is supported by docker. All our named test volumes
4398 + // are unused, so they are all dangling; combine with a name prefix to
4399 + // exclude any leftover dangling volumes from other tests.
4400 + expectList(all, {{"dangling", "true"}, {"name", "^wslc-list-"}});
4401 +
4402 + // label=<key> (key-only) matches the volume with the marker label regardless of stored value.
4403 + expectList({emptyValVol}, {{"label", "marker"}});
4404 +
4405 + // label=<key>= (explicit empty value) matches only volumes whose stored value is also the empty string.
4406 + expectList({emptyValVol}, {{"label", "marker="}});
4407 +
4408 + // No volume stores `env` with an empty value, so env= matches nothing.
4409 + expectList({}, {{"label", "env="}});
4410 +
4411 + // env (key-only) matches every volume that has the key, regardless of its stored value.
4412 + expectList({vhdA, vhdB, guestA, otherName}, {{"label", "env"}});
4413 +
4414 + // Unknown filter keys are rejected.
4415 + expectListFails(E_INVALIDARG, {{"bogus", "x"}});
4416 +
4417 + // Null filter key/value is rejected.
4418 + expectListFails(E_POINTER, {{nullptr, "anything"}});
4419 + expectListFails(E_POINTER, {{"label", nullptr}});
4420 + }
4421 +
4422 + WSLC_TEST_METHOD(PruneVolumesTest)
4423 + {
4424 + auto expectPrune = [&](const std::vector<std::string>& expected,
4425 + const std::vector<WSLCFilter>& filters = {},
4426 + const std::source_location& source = std::source_location::current()) {
4427 + const WSLCFilter* filtersPtr = filters.empty() ? nullptr : filters.data();
4428 + const ULONG filtersCount = static_cast<ULONG>(filters.size());
4429 +
4430 + wil::unique_cotaskmem_array_ptr<WSLCVolumeName> deleted;
4431 + ULONGLONG spaceReclaimed = 0;
4432 + VERIFY_SUCCEEDED(m_defaultSession->PruneVolumes(
4433 + filtersPtr, filtersCount, deleted.addressof(), deleted.size_address<ULONG>(), &spaceReclaimed));
4434 +
4435 + std::vector<std::string> names;
4436 + for (const auto& n : deleted)
4437 + {
4438 + names.emplace_back(n);
4439 + }
4440 +
4441 + VerifyAreEqualUnordered(expected, names, source);
4442 + };
4443 +
4444 + // Prune with no eligible volumes (none created yet) returns an empty set.
4445 + expectPrune({}, {{"all", "true"}});
4446 +
4447 + // Default (no all=true) only prunes anonymous volumes; with none present, returns empty.
4448 + expectPrune({});
4449 +
4450 + // all=true prunes unused named guest volumes.
4451 + {
4452 + const std::string a = "wslc-prune-guest-a";
4453 + const std::string b = "wslc-prune-guest-b";
4454 +
4455 + auto cleanup = wil::scope_exit([&]() {
4456 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(a.c_str()));
4457 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(b.c_str()));
4458 + });
4459 +
4460 + CreateNamedVolume(a, "guest");
4461 + CreateNamedVolume(b, "guest");
4462 +
4463 + expectPrune({a, b}, {{"all", "true"}});
4464 +
4465 + auto volumes = ListVolumes();
4466 + VERIFY_IS_FALSE(volumes.contains(a));
4467 + VERIFY_IS_FALSE(volumes.contains(b));
4468 + }
4469 +
4470 + // In-use volume is not pruned.
4471 + {
4472 + const std::string name = "wslc-prune-in-use";
4473 + CreateNamedVolume(name, "guest");
4474 +
4475 + auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteVolume(name.c_str())); });
4476 +
4477 + WSLCContainerLauncher launcher("debian:latest", "wslc-prune-in-use-holder", {"sleep", "99999"});
4478 + launcher.AddNamedVolume(name, "/data", false);
4479 + auto container = launcher.Launch(*m_defaultSession);
4480 +
4481 + expectPrune({}, {{"all", "true"}});
4482 + VERIFY_IS_TRUE(ListVolumes().contains(name));
4483 +
4484 + VERIFY_SUCCEEDED(container.Get().Kill(WSLCSignalSIGKILL));
4485 + }
4486 +
4487 + // Label filter (present, key=value).
4488 + {
4489 + const std::string labeled = "wslc-prune-labeled";
4490 + const std::string unlabeled = "wslc-prune-unlabeled";
4491 +
4492 + auto cleanup = wil::scope_exit([&]() {
4493 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(labeled.c_str()));
4494 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(unlabeled.c_str()));
4495 + });
4496 +
4497 + CreateNamedVolume(labeled, "guest", {{"wslc-prune-test", "yes"}});
4498 + CreateNamedVolume(unlabeled, "guest");
4499 +
4500 + expectPrune({labeled}, {{"all", "true"}, {"label", "wslc-prune-test=yes"}});
4501 + }
4502 +
4503 + // Label filter (present, key only).
4504 + {
4505 + const std::string labeled = "wslc-prune-keyonly";
4506 + const std::string unlabeled = "wslc-prune-keyonly-no";
4507 +
4508 + auto cleanup = wil::scope_exit([&]() {
4509 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(labeled.c_str()));
4510 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(unlabeled.c_str()));
4511 + });
4512 +
4513 + CreateNamedVolume(labeled, "guest", {{"wslc-prune-keyonly", "anything"}});
4514 + CreateNamedVolume(unlabeled, "guest");
4515 +
4516 + // Value without '=' matches any volume with the key (Docker `label=key`).
4517 + expectPrune({labeled}, {{"all", "true"}, {"label", "wslc-prune-keyonly"}});
4518 + }
4519 +
4520 + // Label filter (absent, key only).
4521 + {
4522 + const std::string keep = "wslc-prune-keep";
4523 + const std::string drop = "wslc-prune-drop";
4524 +
4525 + auto cleanup = wil::scope_exit([&]() {
4526 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(keep.c_str()));
4527 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(drop.c_str()));
4528 + });
4529 +
4530 + CreateNamedVolume(keep, "guest", {{"wslc-prune-keep", "yes"}});
4531 + CreateNamedVolume(drop, "guest");
4532 +
4533 + // `label!` filters out volumes that have the key (Docker `label!=key`).
4534 + expectPrune({drop}, {{"all", "true"}, {"label!", "wslc-prune-keep"}});
4535 + }
4536 +
4537 + // VHD volumes are not pruned (docker skips bind-mount volumes).
4538 + {
4539 + const std::string vhdName = "wslc-prune-vhd-skip";
4540 + const std::string guestName = "wslc-prune-vhd-skip-guest";
4541 +
4542 + auto cleanup = wil::scope_exit([&]() {
4543 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(vhdName.c_str()));
4544 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(guestName.c_str()));
4545 + });
4546 +
4547 + CreateNamedVolume(vhdName, "vhd", {}, {{"SizeBytes", "1073741824"}});
4548 + CreateNamedVolume(guestName, "guest");
4549 +
4550 + expectPrune({guestName}, {{"all", "true"}});
4551 +
4552 + VERIFY_IS_TRUE(ListVolumes().contains(vhdName));
4553 + }
4554 +
4555 + // ListVolumes / InspectVolume reflect prune results.
4556 + {
4557 + const std::string name = "wslc-prune-listsync";
4558 + CreateNamedVolume(name, "guest");
4559 +
4560 + expectPrune({name}, {{"all", "true"}});
4561 + VERIFY_IS_FALSE(ListVolumes().contains(name));
4562 + }
4563 +
4564 + // Filter with null Key rejected.
4565 + {
4566 + WSLCFilter filters[] = {{nullptr, "true"}};
4567 +
4568 + wil::unique_cotaskmem_array_ptr<WSLCVolumeName> deleted;
4569 + ULONGLONG spaceReclaimed = 0;
4570 +
4571 + VERIFY_ARE_EQUAL(
4572 + E_POINTER,
4573 + m_defaultSession->PruneVolumes(filters, ARRAYSIZE(filters), deleted.addressof(), deleted.size_address<ULONG>(), &spaceReclaimed));
4574 + }
4575 +
4576 + // Filter with null Value rejected.
4577 + {
4578 + WSLCFilter filters[] = {{"label", nullptr}};
4579 +
4580 + wil::unique_cotaskmem_array_ptr<WSLCVolumeName> deleted;
4581 + ULONGLONG spaceReclaimed = 0;
4582 +
4583 + VERIFY_ARE_EQUAL(
4584 + E_POINTER,
4585 + m_defaultSession->PruneVolumes(filters, ARRAYSIZE(filters), deleted.addressof(), deleted.size_address<ULONG>(), &spaceReclaimed));
4586 + }
4587 + }
4588 +
4589 WSLC_TEST_METHOD(NetworkCreateDeleteListTest)
4590 {
4591 const std::string networkName = "test-network";
@@ -9251,12 +9548,12 @@ class WSLCTests
9548
9549 // Prune with a label filter that no dangling image has - should not prune anything.
9550 auto [deletedImages, spaceReclaimed] =
9254 - pruneImages(WSLCPruneImagesFlagsNone, 0, {{.Key = "nonexistent.label", .Value = nullptr, .Present = true}});
9551 + pruneImages(WSLCPruneImagesFlagsNone, 0, {{.Key = "nonexistent.label", .Value = "", .Present = true}});
9552 VERIFY_ARE_EQUAL(deletedImages.size(), 0u);
9553
9554 // Prune with absent label filter - dangling image doesn't have the label, so it matches.
9555 auto [deletedImages2, spaceReclaimed2] =
9259 - pruneImages(WSLCPruneImagesFlagsNone, 0, {{.Key = "nonexistent.label", .Value = nullptr, .Present = false}});
9556 + pruneImages(WSLCPruneImagesFlagsNone, 0, {{.Key = "nonexistent.label", .Value = "", .Present = false}});
9557 VERIFY_IS_TRUE(deletedImages2.size() > 0);
9558 }
9559
@@ -9300,7 +9597,7 @@ class WSLCTests
9597 E_INVALIDARG);
9598
9599 // Null label key.
9303 - WSLCPruneLabelFilter nullKeyFilter{.Key = nullptr, .Value = nullptr, .Present = false};
9600 + WSLCPruneLabelFilter nullKeyFilter{.Key = nullptr, .Value = "", .Present = false};
9601 invalidOptions.Flags = WSLCPruneImagesFlagsNone;
9602 invalidOptions.Labels = &nullKeyFilter;
9603 invalidOptions.LabelsCount = 1;