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;