Image filters (#40584)
Kevin Vega committed
May 29, 2026 at 10:52 UTC
84284bfac8294716524c9402d1b244951b75dee8
7 files changed
+107
-326
src/windows/service/inc/wslc.idl
+9
-42
@@ -151,14 +151,11 @@ typedef struct _WSLCImageInformation
151
typedef enum _WSLCListImagesFlags
152
{
153
WSLCListImagesFlagsNone = 0,
154
- WSLCListImagesFlagsAll = 1, // Show all images (default hides intermediate images)
155
- WSLCListImagesFlagsDigests = 2, // Include digest information
156
- WSLCListImagesFlagsDanglingTrue = 4, // Show only dangling images (untagged)
157
- WSLCListImagesFlagsDanglingFalse = 8, // Show only non-dangling images (tagged)
158
- // Note: If neither dangling flag is set, no dangling filter is passed (default: both).
154
+ WSLCListImagesFlagsAll = 1, // Show all images (default hides intermediate images)
155
+ WSLCListImagesFlagsDigests = 2, // Include digest information
156
} WSLCListImagesFlags;
157
161
-cpp_quote("#define WSLCListImagesFlagsValid (WSLCListImagesFlagsAll | WSLCListImagesFlagsDigests | WSLCListImagesFlagsDanglingTrue | WSLCListImagesFlagsDanglingFalse)")
158
+cpp_quote("#define WSLCListImagesFlagsValid (WSLCListImagesFlagsAll | WSLCListImagesFlagsDigests)")
159
160
cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCListImagesFlags);")
161
@@ -181,15 +178,12 @@ typedef KeyValuePair WSLCFilter;
178
typedef KeyValuePairInformation WSLCLabelInformation;
179
typedef KeyValuePairInformation WSLCDriverOptionInformation;
180
184
-typedef struct _WSLCListImageOptions
181
+typedef struct _WSLCListImagesOptions
182
{
183
DWORD Flags; // WSLCListImagesFlags (can combine with bitwise OR)
187
- [unique] LPCSTR Reference; // Filter by reference (name[:tag])
188
- [unique] LPCSTR Before; // Filter: show images created before this image
189
- [unique] LPCSTR Since; // Filter: show images created since this image
190
- [unique, size_is(LabelsCount)] const WSLCLabel* Labels;
191
- ULONG LabelsCount;
192
-} WSLCListImageOptions;
184
+ [unique, size_is(FiltersCount)] const WSLCFilter* Filters;
185
+ ULONG FiltersCount;
186
+} WSLCListImagesOptions;
187
188
typedef enum _WSLCProcessFlags
189
{
@@ -676,13 +670,6 @@ typedef struct _WSLCNetworkInformation
670
char Driver[64];
671
} WSLCNetworkInformation;
672
679
-typedef struct _WSLCPruneLabelFilter
680
-{
681
- LPCSTR Key;
682
- [unique] LPCSTR Value;
683
- BOOL Present;
684
-} WSLCPruneLabelFilter;
685
-
673
typedef struct _WSLCPruneContainersResults
674
{
675
[unique, size_is(ContainersCount)] WSLCContainerId* Containers;
@@ -690,26 +677,6 @@ typedef struct _WSLCPruneContainersResults
677
ULONGLONG SpaceReclaimed;
678
} WSLCPruneContainersResults;
679
693
-typedef enum _WSLCPruneImagesFlags
694
-{
695
- WSLCPruneImagesFlagsNone = 0,
696
- WSLCPruneImagesFlagsDanglingTrue = 1, // Only prune dangling (untagged) images.
697
- WSLCPruneImagesFlagsDanglingFalse = 2, // Prune all images not used by any container.
698
- // Note: If neither dangling flag is set, no dangling filter is passed (Docker defaults to dangling-only).
699
-} WSLCPruneImagesFlags;
700
-
701
-cpp_quote("#define WSLCPruneImagesFlagsValid (WSLCPruneImagesFlagsDanglingTrue | WSLCPruneImagesFlagsDanglingFalse)")
702
-
703
-cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCPruneImagesFlags);")
704
-
705
-typedef struct _WSLCPruneImagesOptions
706
-{
707
- DWORD Flags; // WSLCPruneImagesFlags
708
- ULONGLONG Until;
709
- [unique, size_is(LabelsCount)] const WSLCPruneLabelFilter* Labels;
710
- ULONG LabelsCount;
711
-} WSLCPruneImagesOptions;
712
-
680
typedef enum _WSLCListContainersFlags
681
{
682
WSLCListContainersFlagsNone = 0,
@@ -767,11 +734,11 @@ interface IWSLCSession : IUnknown
734
HRESULT LoadImage([in] WSLCHandle ImageHandle, [in, unique] IProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength);
735
HRESULT ImportImage([in] WSLCHandle ImageHandle, [in] LPCSTR ImageName, [in, unique] IProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength);
736
HRESULT SaveImage([in] WSLCHandle OutputHandle, [in] LPCSTR ImageNameOrID, [in, unique] IProgressCallback * ProgressCallback, [in, unique, system_handle(sh_event)] HANDLE CancelEvent);
770
- HRESULT ListImages([in, unique] const WSLCListImageOptions* Options, [out, size_is(, *Count)] WSLCImageInformation** Images, [out] ULONG* Count);
737
+ HRESULT ListImages([in, unique] const WSLCListImagesOptions* Options, [out, size_is(, *Count)] WSLCImageInformation** Images, [out] ULONG* Count);
738
HRESULT DeleteImage([in] const WSLCDeleteImageOptions* Options, [out, size_is(, *Count)] WSLCDeletedImageInformation** DeletedImages, [out] ULONG* Count);
739
HRESULT TagImage([in] const WSLCTagImageOptions* Options);
740
HRESULT InspectImage([in] LPCSTR ImageNameOrId, [out] LPSTR* Output);
774
- HRESULT PruneImages([in, unique] const WSLCPruneImagesOptions* Options, [out, size_is(, *DeletedImagesCount)] WSLCDeletedImageInformation** DeletedImages, [out] ULONG* DeletedImagesCount, [out] ULONGLONG* SpaceReclaimed);
741
+ HRESULT PruneImages([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out, size_is(, *DeletedImagesCount)] WSLCDeletedImageInformation** DeletedImages, [out] ULONG* DeletedImagesCount, [out] ULONGLONG* SpaceReclaimed);
742
743
// Container management.
744
HRESULT CreateContainer([in] const WSLCContainerOptions* Options, [out] IWSLCContainer** Container);
src/windows/wslc/services/ImageService.cpp
+2
-6
@@ -299,15 +299,11 @@ void ImageService::Save(wsl::windows::wslc::models::Session& session, const std:
299
300
wsl::windows::wslc::models::PruneImagesResult ImageService::Prune(wsl::windows::wslc::models::Session& session, bool all)
301
{
302
- WSLCPruneImagesOptions options{};
303
- if (all)
304
- {
305
- WI_SetFlag(options.Flags, WSLCPruneImagesFlagsDanglingFalse);
306
- }
302
+ WSLCFilter filter{.Key = "dangling", .Value = all ? "false" : "true"};
303
304
wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
305
ULONGLONG spaceReclaimed = 0;
310
- THROW_IF_FAILED(session.Get()->PruneImages(&options, &deletedImages, deletedImages.size_address<ULONG>(), &spaceReclaimed));
306
+ THROW_IF_FAILED(session.Get()->PruneImages(&filter, 1, &deletedImages, deletedImages.size_address<ULONG>(), &spaceReclaimed));
307
308
wsl::windows::wslc::models::PruneImagesResult result;
309
result.SpaceReclaimed = spaceReclaimed;
src/windows/wslcsession/DockerHTTPClient.cpp
+6
-74
@@ -53,47 +53,6 @@ bool IsResponseChunked(const http::response_parser<http::buffer_body>::value_typ
53
54
return true;
55
}
56
-template <typename TFilters>
57
-nlohmann::json PruneFiltersToJson(const TFilters& filters)
58
-{
59
- nlohmann::json j;
60
-
61
- if constexpr (requires { filters.dangling; })
62
- {
63
- if (filters.dangling.has_value())
64
- {
65
- j["dangling"] = nlohmann::json::array({filters.dangling.value() ? "true" : "false"});
66
- }
67
- }
68
-
69
- if constexpr (requires { filters.all; })
70
- {
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())
86
- {
87
- j["label"] = filters.presentLabels;
88
- }
89
-
90
- if (!filters.absentLabels.empty())
91
- {
92
- j["label!"] = filters.absentLabels;
93
- }
94
-
95
- return j;
96
-}
56
57
} // namespace
58
@@ -231,43 +190,16 @@ std::string DockerHTTPClient::Authenticate(const std::string& serverAddress, con
190
return response.IdentityToken.value_or("");
191
}
192
234
-std::vector<docker_schema::Image> DockerHTTPClient::ListImages(bool all, bool digests, const ListImagesFilters& filters)
193
+std::vector<docker_schema::Image> DockerHTTPClient::ListImages(bool all, bool digests, const std::map<std::string, std::vector<std::string>>& filters)
194
{
195
auto url = URL::Create("/images/json");
196
197
url.SetParameter("all", all);
198
url.SetParameter("digests", digests);
199
241
- // Build filters JSON if any filters are set
242
- nlohmann::json filtersJson;
243
-
244
- if (filters.reference.has_value())
245
- {
246
- filtersJson["reference"] = nlohmann::json::array({filters.reference.value()});
247
- }
248
-
249
- if (filters.before.has_value())
250
- {
251
- filtersJson["before"] = nlohmann::json::array({filters.before.value()});
252
- }
253
-
254
- if (filters.since.has_value())
255
- {
256
- filtersJson["since"] = nlohmann::json::array({filters.since.value()});
257
- }
258
-
259
- if (filters.dangling.has_value())
260
- {
261
- filtersJson["dangling"] = nlohmann::json::array({filters.dangling.value() ? "true" : "false"});
262
- }
263
-
264
- if (!filters.labels.empty())
265
- {
266
- filtersJson["label"] = filters.labels;
267
- }
268
-
269
- if (!filtersJson.empty())
200
+ if (!filters.empty())
201
{
202
+ nlohmann::json filtersJson = filters;
203
url.SetParameter("filters", filtersJson.dump());
204
}
205
@@ -295,13 +227,13 @@ std::pair<uint32_t, wil::unique_socket> DockerHTTPClient::SaveImage(const std::s
227
return {response.result_int(), std::move(socket)};
228
}
229
298
-docker_schema::PruneImageResult DockerHTTPClient::PruneImages(const PruneImagesFilters& filters)
230
+docker_schema::PruneImageResult DockerHTTPClient::PruneImages(const std::map<std::string, std::vector<std::string>>& filters)
231
{
232
auto url = URL::Create("/images/prune");
233
302
- auto filtersJson = PruneFiltersToJson(filters);
303
- if (!filtersJson.empty())
234
+ if (!filters.empty())
235
{
236
+ nlohmann::json filtersJson = filters;
237
url.SetParameter("filters", filtersJson.dump());
238
}
239
src/windows/wslcsession/DockerHTTPClient.h
+3
-19
@@ -152,23 +152,6 @@ public:
152
common::docker_schema::Network InspectNetwork(const std::string& Name);
153
154
// Image management.
155
- struct ListImagesFilters
156
- {
157
- std::optional<std::string> reference;
158
- std::optional<std::string> before;
159
- std::optional<std::string> since;
160
- std::optional<bool> dangling;
161
- std::vector<std::string> labels;
162
- };
163
-
164
- struct PruneImagesFilters
165
- {
166
- std::optional<bool> dangling;
167
- std::optional<std::uint64_t> until;
168
- std::vector<std::string> presentLabels;
169
- std::vector<std::string> absentLabels;
170
- };
171
-
155
std::unique_ptr<HTTPRequestContext> PullImage(
156
const std::string& Repo, const std::optional<std::string>& tagOrDigest, const std::optional<std::string>& registryAuth = std::nullopt);
157
std::unique_ptr<HTTPRequestContext> ImportImage(const std::string& Repo, const std::string& Tag, uint64_t ContentLength);
@@ -176,11 +159,12 @@ public:
159
void TagImage(const std::string& Id, const std::string& Repo, const std::string& Tag);
160
std::unique_ptr<HTTPRequestContext> PushImage(const std::string& ImageName, const std::optional<std::string>& tag, const std::string& registryAuth);
161
std::string Authenticate(const std::string& serverAddress, const std::string& username, const std::string& password);
179
- std::vector<common::docker_schema::Image> ListImages(bool all = false, bool digests = false, const ListImagesFilters& filters = {});
162
+ std::vector<common::docker_schema::Image> ListImages(
163
+ bool all = false, bool digests = false, const std::map<std::string, std::vector<std::string>>& filters = {});
164
common::docker_schema::InspectImage InspectImage(const std::string& NameOrId);
165
std::vector<common::docker_schema::DeletedImage> DeleteImage(const char* Image, bool Force, bool NoPrune); // Image can be ID or Repo:Tag.
166
std::pair<uint32_t, wil::unique_socket> SaveImage(const std::string& NameOrId);
183
- common::docker_schema::PruneImageResult PruneImages(const PruneImagesFilters& filters = {});
167
+ common::docker_schema::PruneImageResult PruneImages(const std::map<std::string, std::vector<std::string>>& filters = {});
168
169
// Exec.
170
common::docker_schema::CreateExecResponse CreateExec(const std::string& Container, const common::docker_schema::CreateExec& Request);
src/windows/wslcsession/WSLCSession.cpp
+15
-106
@@ -1221,7 +1221,7 @@ void WSLCSession::SaveImageImpl(std::pair<uint32_t, wil::unique_socket>& SocketC
1221
}
1222
}
1223
1224
-HRESULT WSLCSession::ListImages(const WSLCListImageOptions* Options, WSLCImageInformation** Images, ULONG* Count)
1224
+HRESULT WSLCSession::ListImages(const WSLCListImagesOptions* Options, WSLCImageInformation** Images, ULONG* Count)
1225
try
1226
{
1227
COMServiceExecutionContext context;
@@ -1232,71 +1232,27 @@ try
1232
*Count = 0;
1233
*Images = nullptr;
1234
1235
- if (Options != nullptr)
1236
- {
1237
- RETURN_HR_IF(E_INVALIDARG, WI_IsFlagSet(Options->Flags, WSLCListImagesFlagsDanglingTrue) && WI_IsFlagSet(Options->Flags, WSLCListImagesFlagsDanglingFalse));
1238
- RETURN_HR_IF(E_INVALIDARG, Options->LabelsCount > 0 && Options->Labels == nullptr);
1239
- RETURN_HR_IF(E_INVALIDARG, Options->Reference != nullptr && strlen(Options->Reference) > WSLC_MAX_IMAGE_NAME_LENGTH);
1240
- }
1241
-
1242
- auto lock = m_lock.lock_shared();
1243
-
1244
- THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1245
-
1246
- // Extract options for Docker API
1235
bool all = false;
1236
bool digests = false;
1249
- DockerHTTPClient::ListImagesFilters filters;
1237
+ std::map<std::string, std::vector<std::string>> filters;
1238
1239
if (Options != nullptr)
1240
{
1241
+ THROW_HR_IF_MSG(
1242
+ E_INVALIDARG,
1243
+ WI_IsAnyFlagSet(static_cast<WSLCListImagesFlags>(Options->Flags), ~WSLCListImagesFlagsValid),
1244
+ "Invalid flags: 0x%lx",
1245
+ Options->Flags);
1246
+
1247
all = WI_IsFlagSet(Options->Flags, WSLCListImagesFlagsAll);
1248
digests = WI_IsFlagSet(Options->Flags, WSLCListImagesFlagsDigests);
1249
1256
- if (Options->Reference != nullptr)
1257
- {
1258
- filters.reference = Options->Reference;
1259
- }
1260
-
1261
- if (Options->Before != nullptr)
1262
- {
1263
- filters.before = Options->Before;
1264
- }
1265
-
1266
- if (Options->Since != nullptr)
1267
- {
1268
- filters.since = Options->Since;
1269
- }
1270
-
1271
- // Check dangling flags (mutually exclusive in practice)
1272
- if (WI_IsFlagSet(Options->Flags, WSLCListImagesFlagsDanglingTrue))
1273
- {
1274
- filters.dangling = true;
1275
- }
1276
- else if (WI_IsFlagSet(Options->Flags, WSLCListImagesFlagsDanglingFalse))
1277
- {
1278
- filters.dangling = false;
1279
- }
1280
- // If neither flag is set, filters.dangling remains std::nullopt (show all)
1250
+ filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Options->Filters, Options->FiltersCount);
1251
+ }
1252
1282
- // Construct labels
1283
- if (Options->Labels != nullptr && Options->LabelsCount > 0)
1284
- {
1285
- for (ULONG i = 0; i < Options->LabelsCount; ++i)
1286
- {
1287
- const auto& label = Options->Labels[i];
1288
- RETURN_HR_IF_NULL(E_POINTER, label.Key);
1253
+ auto lock = m_lock.lock_shared();
1254
1290
- std::string labelFilter = label.Key;
1291
- if (label.Value != nullptr)
1292
- {
1293
- labelFilter += "=";
1294
- labelFilter += label.Value;
1295
- }
1296
- filters.labels.push_back(labelFilter);
1297
- }
1298
- }
1299
- }
1255
+ THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1256
1257
std::vector<docker_schema::Image> images;
1258
try
@@ -1600,7 +1556,8 @@ try
1556
}
1557
CATCH_RETURN();
1558
1603
-HRESULT WSLCSession::PruneImages(const WSLCPruneImagesOptions* Options, WSLCDeletedImageInformation** DeletedImages, ULONG* DeletedImagesCount, ULONGLONG* SpaceReclaimed)
1559
+HRESULT WSLCSession::PruneImages(
1560
+ const WSLCFilter* Filters, ULONG FiltersCount, WSLCDeletedImageInformation** DeletedImages, ULONG* DeletedImagesCount, ULONGLONG* SpaceReclaimed)
1561
try
1562
{
1563
COMServiceExecutionContext context;
@@ -1612,59 +1569,11 @@ try
1569
*DeletedImagesCount = 0;
1570
*SpaceReclaimed = 0;
1571
1615
- if (Options != nullptr)
1616
- {
1617
- RETURN_HR_IF(E_INVALIDARG, WI_IsFlagSet(Options->Flags, WSLCPruneImagesFlagsDanglingTrue) && WI_IsFlagSet(Options->Flags, WSLCPruneImagesFlagsDanglingFalse));
1618
- RETURN_HR_IF(E_INVALIDARG, WI_IsAnyFlagSet(static_cast<WSLCPruneImagesFlags>(Options->Flags), ~WSLCPruneImagesFlagsValid));
1619
- }
1572
+ auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
1573
1574
auto lock = m_lock.lock_shared();
1575
RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1576
1624
- DockerHTTPClient::PruneImagesFilters filters;
1625
-
1626
- if (Options != nullptr)
1627
- {
1628
- if (WI_IsFlagSet(Options->Flags, WSLCPruneImagesFlagsDanglingTrue))
1629
- {
1630
- filters.dangling = true;
1631
- }
1632
- else if (WI_IsFlagSet(Options->Flags, WSLCPruneImagesFlagsDanglingFalse))
1633
- {
1634
- filters.dangling = false;
1635
- }
1636
-
1637
- if (Options->Until > 0)
1638
- {
1639
- filters.until = Options->Until;
1640
- }
1641
-
1642
- if (Options->Labels != nullptr && Options->LabelsCount > 0)
1643
- {
1644
- for (ULONG i = 0; i < Options->LabelsCount; ++i)
1645
- {
1646
- const auto& filter = Options->Labels[i];
1647
- RETURN_HR_IF_NULL(E_POINTER, filter.Key);
1648
-
1649
- std::string labelFilter = filter.Key;
1650
- if (filter.Value != nullptr)
1651
- {
1652
- labelFilter += "=";
1653
- labelFilter += filter.Value;
1654
- }
1655
-
1656
- if (filter.Present)
1657
- {
1658
- filters.presentLabels.emplace_back(std::move(labelFilter));
1659
- }
1660
- else
1661
- {
1662
- filters.absentLabels.emplace_back(std::move(labelFilter));
1663
- }
1664
- }
1665
- }
1666
- }
1667
-
1577
docker_schema::PruneImageResult pruneResult;
1578
try
1579
{
src/windows/wslcsession/WSLCSession.h
+3
-2
@@ -96,14 +96,15 @@ public:
96
IFACEMETHOD(LoadImage)(_In_ const WSLCHandle ImageHandle, _In_ IProgressCallback* ProgressCallback, _In_ ULONGLONG ContentLength) override;
97
IFACEMETHOD(ImportImage)(_In_ const WSLCHandle ImageHandle, _In_ LPCSTR ImageName, _In_ IProgressCallback* ProgressCallback, _In_ ULONGLONG ContentLength) override;
98
IFACEMETHOD(SaveImage)(_In_ WSLCHandle OutputHandle, _In_ LPCSTR ImageNameOrID, _In_ IProgressCallback* ProgressCallback, _In_opt_ HANDLE CancelEvent) override;
99
- IFACEMETHOD(ListImages)(_In_opt_ const WSLCListImageOptions* Options, _Out_ WSLCImageInformation** Images, _Out_ ULONG* Count) override;
99
+ IFACEMETHOD(ListImages)(_In_opt_ const WSLCListImagesOptions* Options, _Out_ WSLCImageInformation** Images, _Out_ ULONG* Count) override;
100
IFACEMETHOD(DeleteImage)(_In_ const WSLCDeleteImageOptions* Options, _Out_ WSLCDeletedImageInformation** DeletedImages, _Out_ ULONG* Count) override;
101
IFACEMETHOD(TagImage)(_In_ const WSLCTagImageOptions* Options) override;
102
IFACEMETHOD(PushImage)(_In_ LPCSTR Image, _In_ LPCSTR RegistryAuthenticationInformation, _In_opt_ IProgressCallback* ProgressCallback) override;
103
IFACEMETHOD(InspectImage)(_In_ LPCSTR ImageNameOrId, _Out_ LPSTR* Output) override;
104
IFACEMETHOD(Authenticate)(_In_ LPCSTR ServerAddress, _In_ LPCSTR Username, _In_ LPCSTR Password, _Out_ LPSTR* IdentityToken) override;
105
IFACEMETHOD(PruneImages)(
106
- _In_opt_ const WSLCPruneImagesOptions* Options,
106
+ _In_opt_ const WSLCFilter* Filters,
107
+ _In_ ULONG FiltersCount,
108
_Out_ WSLCDeletedImageInformation** DeletedImages,
109
_Out_ ULONG* DeletedImagesCount,
110
_Out_ ULONGLONG* SpaceReclaimed) override;
test/windows/WSLCTests.cpp
+69
-77
@@ -853,9 +853,8 @@ class WSLCTests
853
854
LogInfo("Test: Multiple tags for same image return separate entries");
855
{
856
- WSLCListImageOptions options{};
857
- options.Flags = WSLCListImagesFlagsNone;
858
- options.Reference = "debian";
856
+ WSLCFilter filter{.Key = "reference", .Value = "debian"};
857
+ WSLCListImagesOptions options{.Flags = WSLCListImagesFlagsNone, .Filters = &filter, .FiltersCount = 1};
858
859
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
860
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
@@ -877,9 +876,8 @@ class WSLCTests
876
877
LogInfo("Test: Filter by specific reference");
878
{
880
- WSLCListImageOptions options{};
881
- options.Flags = WSLCListImagesFlagsNone;
882
- options.Reference = "debian:test-tag1";
879
+ WSLCFilter filter{.Key = "reference", .Value = "debian:test-tag1"};
880
+ WSLCListImagesOptions options{.Flags = WSLCListImagesFlagsNone, .Filters = &filter, .FiltersCount = 1};
881
882
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
883
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
@@ -900,9 +898,8 @@ class WSLCTests
898
899
LogInfo("Test: Digests flag");
900
{
903
- WSLCListImageOptions options{};
904
- options.Flags = WSLCListImagesFlagsDigests;
905
- options.Reference = "debian:latest";
901
+ WSLCFilter filter{.Key = "reference", .Value = "debian:latest"};
902
+ WSLCListImagesOptions options{.Flags = WSLCListImagesFlagsDigests, .Filters = &filter, .FiltersCount = 1};
903
904
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
905
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
@@ -921,6 +918,16 @@ class WSLCTests
918
// Note: Pulled images from registry should have digests, locally built may not
919
}
920
921
+ LogInfo("Test: Invalid flags are rejected");
922
+ {
923
+ constexpr auto c_invalidFlags = static_cast<WSLCListImagesFlags>(0x4 | 0x8);
924
+
925
+ WSLCListImagesOptions options{.Flags = c_invalidFlags, .Filters = nullptr, .FiltersCount = 0};
926
+ wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
927
+
928
+ VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
929
+ }
930
+
931
LogInfo("Test: Before/Since filters");
932
{
933
// Get all images to find their IDs and creation times
@@ -971,9 +978,8 @@ class WSLCTests
978
979
// Test 'since' filter - images created after the older image
980
{
974
- WSLCListImageOptions options{};
975
- options.Flags = WSLCListImagesFlagsNone;
976
- options.Since = olderId.c_str();
981
+ WSLCFilter filter{.Key = "since", .Value = olderId.c_str()};
982
+ WSLCListImagesOptions options{.Flags = WSLCListImagesFlagsNone, .Filters = &filter, .FiltersCount = 1};
983
984
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
985
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
@@ -994,9 +1000,8 @@ class WSLCTests
1000
1001
// Test 'before' filter - images created before the newer image
1002
{
997
- WSLCListImageOptions options{};
998
- options.Flags = WSLCListImagesFlagsNone;
999
- options.Before = newerId.c_str();
1003
+ WSLCFilter filter{.Key = "before", .Value = newerId.c_str()};
1004
+ WSLCListImagesOptions options{.Flags = WSLCListImagesFlagsNone, .Filters = &filter, .FiltersCount = 1};
1005
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
1006
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
1007
VERIFY_IS_TRUE(images.size() > 0);
@@ -1030,8 +1035,8 @@ class WSLCTests
1035
});
1036
1037
// List only dangling images
1033
- WSLCListImageOptions options{};
1034
- options.Flags = WSLCListImagesFlagsDanglingTrue;
1038
+ WSLCFilter danglingTrueFilter{.Key = "dangling", .Value = "true"};
1039
+ WSLCListImagesOptions options{.Flags = WSLCListImagesFlagsNone, .Filters = &danglingTrueFilter, .FiltersCount = 1};
1040
1041
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> danglingImages;
1042
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, danglingImages.addressof(), danglingImages.size_address<ULONG>()));
@@ -1046,7 +1051,8 @@ class WSLCTests
1051
}
1052
1053
// List non-dangling images
1049
- options.Flags = WSLCListImagesFlagsDanglingFalse;
1054
+ WSLCFilter danglingFalseFilter{.Key = "dangling", .Value = "false"};
1055
+ options.Filters = &danglingFalseFilter;
1056
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> nonDanglingImages;
1057
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, nonDanglingImages.addressof(), nonDanglingImages.size_address<ULONG>()));
1058
VERIFY_IS_TRUE(nonDanglingImages.size() > 0);
@@ -1061,29 +1067,26 @@ class WSLCTests
1067
1068
LogInfo("Test: Label filter");
1069
{
1064
- // Test with nullptr (no label filter)
1065
- WSLCListImageOptions options{};
1066
- options.Flags = WSLCListImagesFlagsNone;
1067
- options.Labels = nullptr;
1068
- options.LabelsCount = 0;
1070
+ // Test with no filters (nullptr)
1071
+ WSLCListImagesOptions options{.Flags = WSLCListImagesFlagsNone, .Filters = nullptr, .FiltersCount = 0};
1072
1073
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
1074
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
1075
1076
// Test with single label filter
1077
{
1075
- WSLCLabel labels[] = {{"test.label", ""}};
1076
- options.Labels = labels;
1077
- options.LabelsCount = 1;
1078
+ WSLCFilter filters[] = {{"label", "test.label"}};
1079
+ options.Filters = filters;
1080
+ options.FiltersCount = 1;
1081
1082
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
1083
}
1084
1085
// Test with multiple label filters (labels are AND'ed together)
1086
{
1084
- WSLCLabel labels[] = {{"test.label1", ""}, {"test.label2", "value"}};
1085
- options.Labels = labels;
1086
- options.LabelsCount = 2;
1087
+ WSLCFilter filters[] = {{"label", "test.label1"}, {"label", "test.label2=value"}};
1088
+ options.Filters = filters;
1089
+ options.FiltersCount = 2;
1090
1091
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
1092
}
@@ -9497,20 +9500,18 @@ class WSLCTests
9500
9501
WSLC_TEST_METHOD(ImagePrune)
9502
{
9500
- auto pruneImages =
9501
- [this](DWORD flags = WSLCPruneImagesFlagsNone, uint64_t until = 0, const std::vector<WSLCPruneLabelFilter>& labels = {}) {
9502
- wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
9503
- ULONGLONG spaceReclaimed = 0;
9504
- WSLCPruneImagesOptions options{};
9505
- options.Flags = flags;
9506
- options.Until = until;
9507
- options.Labels = labels.empty() ? nullptr : labels.data();
9508
- options.LabelsCount = static_cast<ULONG>(labels.size());
9509
-
9510
- VERIFY_SUCCEEDED(m_defaultSession->PruneImages(
9511
- &options, deletedImages.addressof(), deletedImages.size_address<ULONG>(), &spaceReclaimed));
9512
- return std::make_pair(std::move(deletedImages), spaceReclaimed);
9513
- };
9503
+ auto pruneImages = [this](const std::vector<WSLCFilter>& filters = {}) {
9504
+ wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
9505
+ ULONGLONG spaceReclaimed = 0;
9506
+
9507
+ VERIFY_SUCCEEDED(m_defaultSession->PruneImages(
9508
+ filters.empty() ? nullptr : filters.data(),
9509
+ static_cast<ULONG>(filters.size()),
9510
+ deletedImages.addressof(),
9511
+ deletedImages.size_address<ULONG>(),
9512
+ &spaceReclaimed));
9513
+ return std::make_pair(std::move(deletedImages), spaceReclaimed);
9514
+ };
9515
9516
// Helper to create a dangling image using only test-local tags:
9517
// Load alpine and hello-world under unique tags, then overwrite one with the other.
@@ -9531,13 +9532,13 @@ class WSLCTests
9532
};
9533
9534
auto cleanupDanglingImage = [this, &pruneImages]() {
9534
- pruneImages(WSLCPruneImagesFlagsDanglingTrue);
9535
+ pruneImages({{.Key = "dangling", .Value = "true"}});
9536
LOG_IF_FAILED(DeleteImageNoThrow("prune-test-a:v1", WSLCDeleteImageFlagsNone).first);
9537
LOG_IF_FAILED(DeleteImageNoThrow("prune-test-b:v1", WSLCDeleteImageFlagsNone).first);
9538
};
9539
9540
// Clean up any stale dangling images from prior tests.
9540
- pruneImages(WSLCPruneImagesFlagsDanglingTrue);
9541
+ pruneImages({{.Key = "dangling", .Value = "true"}});
9542
9543
// Prune with no unused images returns empty.
9544
{
@@ -9550,12 +9551,12 @@ class WSLCTests
9551
createDanglingImage();
9552
auto cleanup = wil::scope_exit([&]() { cleanupDanglingImage(); });
9553
9553
- // DanglingTrue should prune the now-dangling original alpine image.
9554
- auto [deletedImages, spaceReclaimed] = pruneImages(WSLCPruneImagesFlagsDanglingTrue);
9554
+ // dangling=true should prune the now-dangling original alpine image.
9555
+ auto [deletedImages, spaceReclaimed] = pruneImages({{.Key = "dangling", .Value = "true"}});
9556
VERIFY_IS_TRUE(deletedImages.size() > 0);
9557
9558
// A second prune should find nothing.
9558
- auto [deletedImages2, spaceReclaimed2] = pruneImages(WSLCPruneImagesFlagsDanglingTrue);
9559
+ auto [deletedImages2, spaceReclaimed2] = pruneImages({{.Key = "dangling", .Value = "true"}});
9560
VERIFY_ARE_EQUAL(deletedImages2.size(), 0u);
9561
}
9562
@@ -9566,12 +9567,12 @@ class WSLCTests
9567
9568
// Docker's 'until' filter uses the image's original Created timestamp, not load time.
9569
// Use timestamp 1 (near epoch) which is before any real image was built.
9569
- auto [deletedImages, spaceReclaimed] = pruneImages(WSLCPruneImagesFlagsNone, 1);
9570
+ auto [deletedImages, spaceReclaimed] = pruneImages({{.Key = "until", .Value = "1"}});
9571
VERIFY_ARE_EQUAL(deletedImages.size(), 0u);
9572
9573
// Use a timestamp far in the future to ensure the dangling image is pruned.
9573
- auto future = static_cast<uint64_t>(time(nullptr)) + 3600;
9574
- auto [deletedImages2, spaceReclaimed2] = pruneImages(WSLCPruneImagesFlagsNone, future);
9574
+ auto futureStr = std::to_string(static_cast<uint64_t>(time(nullptr)) + 3600);
9575
+ auto [deletedImages2, spaceReclaimed2] = pruneImages({{.Key = "until", .Value = futureStr.c_str()}});
9576
VERIFY_IS_TRUE(deletedImages2.size() > 0);
9577
}
9578
@@ -9581,17 +9582,15 @@ class WSLCTests
9582
auto cleanup = wil::scope_exit([&]() { cleanupDanglingImage(); });
9583
9584
// Prune with a label filter that no dangling image has - should not prune anything.
9584
- auto [deletedImages, spaceReclaimed] =
9585
- pruneImages(WSLCPruneImagesFlagsNone, 0, {{.Key = "nonexistent.label", .Value = "", .Present = true}});
9585
+ auto [deletedImages, spaceReclaimed] = pruneImages({{.Key = "label", .Value = "nonexistent.label"}});
9586
VERIFY_ARE_EQUAL(deletedImages.size(), 0u);
9587
9588
- // Prune with absent label filter - dangling image doesn't have the label, so it matches.
9589
- auto [deletedImages2, spaceReclaimed2] =
9590
- pruneImages(WSLCPruneImagesFlagsNone, 0, {{.Key = "nonexistent.label", .Value = "", .Present = false}});
9588
+ // Prune with absent label filter ("label!") - dangling image doesn't have the label, so it matches.
9589
+ auto [deletedImages2, spaceReclaimed2] = pruneImages({{.Key = "label!", .Value = "nonexistent.label"}});
9590
VERIFY_IS_TRUE(deletedImages2.size() > 0);
9591
}
9592
9594
- // Validate null Options uses defaults (dangling-only prune).
9593
+ // Validate null Filters uses defaults (dangling-only prune).
9594
{
9595
LoadTestImage(*m_defaultSession, "alpine:latest");
9596
WSLCTagImageOptions renameOptions{.Image = "alpine:latest", .Repo = "prune-test-a", .Tag = "v1"};
@@ -9601,42 +9600,35 @@ class WSLCTests
9600
9601
ExpectImagePresent(*m_defaultSession, "prune-test-a:v1");
9602
9604
- // Null options should not prune tagged images.
9603
+ // Null filters should not prune tagged images (docker defaults to dangling-only).
9604
wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
9605
ULONGLONG spaceReclaimed = 0;
9607
- VERIFY_SUCCEEDED(m_defaultSession->PruneImages(nullptr, deletedImages.addressof(), deletedImages.size_address<ULONG>(), &spaceReclaimed));
9606
+ VERIFY_SUCCEEDED(m_defaultSession->PruneImages(
9607
+ nullptr, 0, deletedImages.addressof(), deletedImages.size_address<ULONG>(), &spaceReclaimed));
9608
ExpectImagePresent(*m_defaultSession, "prune-test-a:v1");
9609
}
9610
9611
// Validate error paths.
9612
{
9613
- // Null output pointers - RPC rejects null [out] pointers before our code runs.
9613
wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
9614
ULONGLONG spaceReclaimed = 0;
9616
- VERIFY_ARE_EQUAL(
9617
- m_defaultSession->PruneImages(nullptr, nullptr, deletedImages.size_address<ULONG>(), &spaceReclaimed),
9618
- HRESULT_FROM_WIN32(RPC_X_NULL_REF_POINTER));
9615
9620
- // Mutually exclusive dangling flags.
9621
- WSLCPruneImagesOptions invalidOptions{};
9622
- invalidOptions.Flags = WSLCPruneImagesFlagsDanglingTrue | WSLCPruneImagesFlagsDanglingFalse;
9616
+ // Null output pointers - RPC rejects null [out] pointers before our code runs.
9617
VERIFY_ARE_EQUAL(
9624
- m_defaultSession->PruneImages(&invalidOptions, deletedImages.addressof(), deletedImages.size_address<ULONG>(), &spaceReclaimed),
9625
- E_INVALIDARG);
9618
+ m_defaultSession->PruneImages(nullptr, 0, nullptr, deletedImages.size_address<ULONG>(), &spaceReclaimed),
9619
+ HRESULT_FROM_WIN32(RPC_X_NULL_REF_POINTER));
9620
9627
- // Invalid flags.
9628
- invalidOptions.Flags = 0x4;
9621
+ // Unknown filter key - docker rejects with HTTP 400, mapped to E_INVALIDARG.
9622
+ WSLCFilter bogus{.Key = "bogus", .Value = "x"};
9623
VERIFY_ARE_EQUAL(
9630
- m_defaultSession->PruneImages(&invalidOptions, deletedImages.addressof(), deletedImages.size_address<ULONG>(), &spaceReclaimed),
9624
+ m_defaultSession->PruneImages(&bogus, 1, deletedImages.addressof(), deletedImages.size_address<ULONG>(), &spaceReclaimed),
9625
E_INVALIDARG);
9626
+ ValidateCOMErrorMessageContains(L"invalid filter 'bogus'");
9627
9633
- // Null label key.
9634
- WSLCPruneLabelFilter nullKeyFilter{.Key = nullptr, .Value = "", .Present = false};
9635
- invalidOptions.Flags = WSLCPruneImagesFlagsNone;
9636
- invalidOptions.Labels = &nullKeyFilter;
9637
- invalidOptions.LabelsCount = 1;
9628
+ // Null filter key - rejected by ParseKeyMultiValuePairs at the boundary.
9629
+ WSLCFilter nullKey{.Key = nullptr, .Value = "x"};
9630
VERIFY_ARE_EQUAL(
9639
- m_defaultSession->PruneImages(&invalidOptions, deletedImages.addressof(), deletedImages.size_address<ULONG>(), &spaceReclaimed),
9631
+ m_defaultSession->PruneImages(&nullKey, 1, deletedImages.addressof(), deletedImages.size_address<ULONG>(), &spaceReclaimed),
9632
E_POINTER);
9633
}
9634
}