@samitouri / QOSAMI-WSL / commits / 84284bfa

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 }