Add --filter support to wslc network list (#41318)

beena352 committed Aug 14, 2026 at 14:27 UTC c79638aa7a2bdaff4a7a85177432338b436e69b4
11 files changed +324 -21
src/windows/service/inc/wslc.idl
+1 -1
@@ -738,7 +738,7 @@ interface IWSLCSession : IUnknown
738 // Network management.
739 HRESULT CreateNetwork([in] const WSLCNetworkOptions* Options, [in, unique] IWarningCallback* WarningCallback);
740 HRESULT DeleteNetwork([in] LPCSTR Name);
741 - HRESULT ListNetworks([out, size_is(, *Count)] WSLCNetworkInformation** Networks, [out] ULONG* Count);
741 + HRESULT ListNetworks([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out, size_is(, *Count)] WSLCNetworkInformation** Networks, [out] ULONG* Count);
742 HRESULT InspectNetwork([in] LPCSTR Name, [out] LPSTR* Output);
743 HRESULT PruneNetworks([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out, size_is(, *NetworksCount)] WSLCNetworkName** Networks, [out] ULONG* NetworksCount);
744
src/windows/wslc/commands/NetworkListCommand.cpp
+1
@@ -27,6 +27,7 @@ namespace wsl::windows::wslc {
27 std::vector<Argument> NetworkListCommand::GetArguments() const
28 {
29 return {
30 + Argument::Create(ArgType::Filter, false, Limit::Unlimited),
31 Argument::Create(ArgType::Format),
32 Argument::Create(ArgType::NoTrunc),
33 Argument::Create(ArgType::Quiet, false, std::nullopt, Localization::WSLCCLI_NetworkListQuietArgDesc()),
src/windows/wslc/services/NetworkService.cpp
+10 -2
@@ -75,11 +75,19 @@ void NetworkService::Delete(models::Session& session, const std::string& name)
75 THROW_IF_FAILED(session.Get()->DeleteNetwork(name.c_str()));
76 }
77
78 -std::vector<WSLCNetworkInformation> NetworkService::List(models::Session& session)
78 +std::vector<WSLCNetworkInformation> NetworkService::List(models::Session& session, const std::vector<std::pair<std::string, std::string>>& filters)
79 {
80 + std::vector<WSLCFilter> filterEntries;
81 + filterEntries.reserve(filters.size());
82 + for (const auto& [key, value] : filters)
83 + {
84 + filterEntries.push_back({.Key = key.c_str(), .Value = value.c_str()});
85 + }
86 +
87 wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> rawNetworks;
88 ULONG count = 0;
82 - THROW_IF_FAILED(session.Get()->ListNetworks(&rawNetworks, &count));
89 + THROW_IF_FAILED(session.Get()->ListNetworks(
90 + filterEntries.empty() ? nullptr : filterEntries.data(), static_cast<ULONG>(filterEntries.size()), &rawNetworks, &count));
91
92 std::vector<WSLCNetworkInformation> networks;
93 networks.reserve(count);
src/windows/wslc/services/NetworkService.h
+1 -1
@@ -24,7 +24,7 @@ struct NetworkService
24 {
25 static void Create(Terminal& terminal, models::Session& session, const models::CreateNetworkOptions& createOptions);
26 static void Delete(models::Session& session, const std::string& name);
27 - static std::vector<WSLCNetworkInformation> List(models::Session& session);
27 + static std::vector<WSLCNetworkInformation> List(models::Session& session, const std::vector<std::pair<std::string, std::string>>& filters = {});
28 static wsl::windows::common::wslc_schema::Network Inspect(models::Session& session, const std::string& name);
29 static models::PruneNetworksResult Prune(models::Session& session, const std::vector<std::pair<std::string, std::string>>& filters = {});
30 static void Connect(models::Session& session, const models::ConnectNetworkOptions& connectOptions);
src/windows/wslc/tasks/NetworkTasks.cpp
+3 -1
@@ -139,7 +139,9 @@ void GetNetworks(CLIExecutionContext& context)
139 {
140 WI_ASSERT(context.Data.Contains(Data::Session));
141 auto& session = context.Data.Get<Data::Session>();
142 - context.Data.Add<Data::Networks>(NetworkService::List(session));
142 +
143 + auto filters = context.Args.GetAllValues<ArgType::Filter>();
144 + context.Data.Add<Data::Networks>(NetworkService::List(session, filters));
145 }
146
147 void InspectNetworks(CLIExecutionContext& context)
src/windows/wslcsession/DockerHTTPClient.cpp
+9 -2
@@ -493,9 +493,16 @@ void DockerHTTPClient::DisconnectContainerFromNetwork(const std::string& Network
493 Transaction(verb::post, URL::Create("/networks/{}/disconnect", NetworkName), Request);
494 }
495
496 -std::vector<docker_schema::Network> DockerHTTPClient::ListNetworks()
496 +std::vector<docker_schema::Network> DockerHTTPClient::ListNetworks(const std::map<std::string, std::vector<std::string>>& filters)
497 {
498 - return Transaction<docker_schema::EmptyRequest, std::vector<docker_schema::Network>>(verb::get, URL::Create("/networks"));
498 + auto url = URL::Create("/networks");
499 +
500 + if (!filters.empty())
501 + {
502 + url.SetParameter("filters", nlohmann::json(filters).dump());
503 + }
504 +
505 + return Transaction<docker_schema::EmptyRequest, std::vector<docker_schema::Network>>(verb::get, url);
506 }
507
508 docker_schema::Network DockerHTTPClient::InspectNetwork(const std::string& Name)
src/windows/wslcsession/DockerHTTPClient.h
+1 -1
@@ -157,7 +157,7 @@ public:
157 // Network management.
158 common::docker_schema::CreateNetworkResponse CreateNetwork(const common::docker_schema::CreateNetwork& Request);
159 void RemoveNetwork(const std::string& Name);
160 - std::vector<common::docker_schema::Network> ListNetworks();
160 + std::vector<common::docker_schema::Network> ListNetworks(const std::map<std::string, std::vector<std::string>>& filters = {});
161 common::docker_schema::Network InspectNetwork(const std::string& Name);
162 void ConnectContainerToNetwork(const std::string& NetworkName, const common::docker_schema::ContainerNetworkRequest& Request);
163 void DisconnectContainerFromNetwork(const std::string& NetworkName, const common::docker_schema::ContainerNetworkRequest& Request);
src/windows/wslcsession/WSLCSession.cpp
+28 -3
@@ -3008,7 +3008,7 @@ try
3008 }
3009 CATCH_RETURN();
3010
3011 -HRESULT WSLCSession::ListNetworks(WSLCNetworkInformation** Networks, ULONG* Count)
3011 +HRESULT WSLCSession::ListNetworks(const WSLCFilter* Filters, ULONG FiltersCount, WSLCNetworkInformation** Networks, ULONG* Count)
3012 try
3013 {
3014 WSLCExecutionContext context(this);
@@ -3019,12 +3019,27 @@ try
3019 *Networks = nullptr;
3020 *Count = 0;
3021
3022 + auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
3023 + const bool filtered = !filters.empty();
3024 +
3025 + if (filtered)
3026 + {
3027 + // Scope the filtered query to WSLC-managed networks.
3028 + filters["label"].push_back(WSLCNetworkManagedLabel);
3029 + }
3030 +
3031 auto lock = AcquireLease();
3032 +
3033 std::lock_guard networksLock(m_networksLock);
3034
3025 - if (m_networks.empty())
3035 + std::vector<docker_schema::Network> dockerNetworks;
3036 + if (filtered)
3037 {
3027 - return S_OK;
3038 + try
3039 + {
3040 + dockerNetworks = m_runtime.Docker().ListNetworks(filters);
3041 + }
3042 + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list networks");
3043 }
3044
3045 auto output = wil::make_unique_cotaskmem<WSLCNetworkInformation[]>(m_networks.size());
@@ -3032,12 +3047,22 @@ try
3047 ULONG index = 0;
3048 for (const auto& [name, entry] : m_networks)
3049 {
3050 + if (filtered && std::ranges::find_if(dockerNetworks, [&](const auto& n) { return n.Name == name; }) == dockerNetworks.end())
3051 + {
3052 + continue;
3053 + }
3054 +
3055 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Name, name.c_str()) != 0);
3056 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Id, entry.Id.c_str()) != 0);
3057 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Driver, entry.Driver.c_str()) != 0);
3058 index++;
3059 }
3060
3061 + if (index == 0)
3062 + {
3063 + return S_OK;
3064 + }
3065 +
3066 *Networks = output.release();
3067 *Count = index;
3068
src/windows/wslcsession/WSLCSession.h
+3 -1
@@ -194,7 +194,9 @@ public:
194 // Network management.
195 IFACEMETHOD(CreateNetwork)(_In_ const WSLCNetworkOptions* Options, _In_opt_ IWarningCallback* WarningCallback) override;
196 IFACEMETHOD(DeleteNetwork)(_In_ LPCSTR Name) override;
197 - IFACEMETHOD(ListNetworks)(_Out_ WSLCNetworkInformation** Networks, _Out_ ULONG* Count) override;
197 + IFACEMETHOD(ListNetworks)
198 + (_In_reads_opt_(FiltersCount) const WSLCFilter* Filters, _In_ ULONG FiltersCount, _Out_ WSLCNetworkInformation** Networks, _Out_ ULONG* Count)
199 + override;
200 IFACEMETHOD(InspectNetwork)(_In_ LPCSTR Name, _Out_ LPSTR* Output) override;
201 IFACEMETHOD(PruneNetworks)
202 (_In_reads_opt_(FiltersCount) const WSLCFilter* Filters, _In_ ULONG FiltersCount, _Out_ WSLCNetworkName** Networks, _Out_ ULONG* NetworksCount)
test/windows/WSLCTests.cpp
+85 -9
@@ -5424,7 +5424,7 @@ class WSLCTests
5424
5425 // List should start empty.
5426 wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
5427 - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
5427 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address<ULONG>()));
5428 VERIFY_ARE_EQUAL(0u, networks.size());
5429
5430 WSLCNetworkOptions options{};
@@ -5437,7 +5437,7 @@ class WSLCTests
5437 auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
5438
5439 // Verify it appears in the list with correct fields.
5440 - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
5440 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address<ULONG>()));
5441 VERIFY_ARE_EQUAL(1u, networks.size());
5442 VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name));
5443 VERIFY_ARE_EQUAL(std::string("bridge"), std::string(networks[0].Driver));
@@ -5450,7 +5450,7 @@ class WSLCTests
5450 VERIFY_SUCCEEDED(m_defaultSession->DeleteNetwork(networkName.c_str()));
5451
5452 // List should be empty again.
5453 - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
5453 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address<ULONG>()));
5454 VERIFY_ARE_EQUAL(0u, networks.size());
5455
5456 // Delete non-existent should fail.
@@ -5468,6 +5468,82 @@ class WSLCTests
5468 VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options, nullptr));
5469 }
5470
5471 + WSLC_TEST_METHOD(ListNetworksFilters)
5472 + {
5473 + const std::string netA = "wslc-flt-net-a";
5474 + const std::string netB = "wslc-flt-net-b";
5475 + const std::string netC = "wslc-flt-net-c";
5476 + const std::string testLabelKey = "wslc.test.list_filter";
5477 + const std::string testLabelValue = "1";
5478 + const std::string testLabelKV = testLabelKey + "=" + testLabelValue;
5479 + const std::string managedLabel = "com.microsoft.wsl.network.managed";
5480 +
5481 + auto cleanup = wil::scope_exit([&]() {
5482 + for (const auto& name : {netA, netB, netC})
5483 + {
5484 + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(name.c_str()));
5485 + }
5486 + });
5487 +
5488 + CreateNamedNetwork(netA, {{testLabelKey.c_str(), testLabelValue.c_str()}, {"env", "prod"}, {"tier", "web"}});
5489 + CreateNamedNetwork(netB, {{testLabelKey.c_str(), testLabelValue.c_str()}, {"env", "test"}});
5490 + CreateNamedNetwork(netC, {{testLabelKey.c_str(), testLabelValue.c_str()}, {"env", "prod"}});
5491 +
5492 + auto expectListFails = [&](HRESULT expected, const std::vector<WSLCFilter>& filters) {
5493 + const WSLCFilter* filtersPtr = filters.empty() ? nullptr : filters.data();
5494 + const ULONG filtersCount = static_cast<ULONG>(filters.size());
5495 +
5496 + wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
5497 + VERIFY_ARE_EQUAL(
5498 + expected, m_defaultSession->ListNetworks(filtersPtr, filtersCount, networks.addressof(), networks.size_address<ULONG>()));
5499 + };
5500 +
5501 + auto expectList = [&](const std::vector<std::string>& expected,
5502 + const std::vector<WSLCFilter>& filters,
5503 + const std::source_location& source = std::source_location::current()) {
5504 + const WSLCFilter* filtersPtr = filters.empty() ? nullptr : filters.data();
5505 + const ULONG filtersCount = static_cast<ULONG>(filters.size());
5506 +
5507 + wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
5508 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(filtersPtr, filtersCount, networks.addressof(), networks.size_address<ULONG>()));
5509 +
5510 + std::vector<std::string> names;
5511 + for (const auto& n : networks)
5512 + {
5513 + names.emplace_back(n.Name);
5514 + VERIFY_IS_TRUE(strlen(n.Id) > 0);
5515 + VERIFY_ARE_EQUAL(std::string("bridge"), std::string(n.Driver));
5516 + }
5517 +
5518 + VerifyAreEqualUnordered(expected, names, source);
5519 + };
5520 +
5521 + const std::vector<std::string> all{netA, netB, netC};
5522 +
5523 + expectList(all, {{"label", testLabelKV.c_str()}});
5524 +
5525 + // label=<key>=<value> selects a subset within this test's scope.
5526 + expectList({netA, netC}, {{"label", testLabelKV.c_str()}, {"label", "env=prod"}});
5527 + expectList({netB}, {{"label", testLabelKV.c_str()}, {"label", "env=test"}});
5528 +
5529 + // Multiple label filters are AND'd.
5530 + expectList({netA}, {{"label", testLabelKV.c_str()}, {"label", "env=prod"}, {"label", "tier=web"}});
5531 +
5532 + // label=<key> (key-only) matches any stored value.
5533 + expectList(all, {{"label", testLabelKV.c_str()}, {"label", "env"}});
5534 +
5535 + // driver filter combined with the test-scope label.
5536 + expectList(all, {{"label", testLabelKV.c_str()}, {"driver", "bridge"}});
5537 + expectList({}, {{"label", testLabelKV.c_str()}, {"driver", "nonexistent"}});
5538 +
5539 + // Explicit managed-label filter is idempotent with the auto-injected one.
5540 + expectList(all, {{"label", testLabelKV.c_str()}, {"label", managedLabel.c_str()}});
5541 +
5542 + // Null filter key/value is rejected.
5543 + expectListFails(E_POINTER, {{nullptr, "anything"}});
5544 + expectListFails(E_POINTER, {{"label", nullptr}});
5545 + }
5546 +
5547 WSLC_TEST_METHOD(PruneNetworksTest)
5548 {
5549 auto expectPrune = [&](const std::vector<std::string>& expected,
@@ -5507,7 +5583,7 @@ class WSLCTests
5583 expectPrune({a, b});
5584
5585 wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
5510 - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
5586 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address<ULONG>()));
5587 for (const auto& n : networks)
5588 {
5589 VERIFY_ARE_NOT_EQUAL(a, std::string(n.Name));
@@ -5648,7 +5724,7 @@ class WSLCTests
5724 VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options, nullptr));
5725
5726 wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
5651 - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
5727 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address<ULONG>()));
5728 VERIFY_ARE_EQUAL(1u, networks.size());
5729 VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name));
5730 }
@@ -5711,7 +5787,7 @@ class WSLCTests
5787 VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options, nullptr));
5788
5789 wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
5714 - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
5790 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address<ULONG>()));
5791 VERIFY_ARE_EQUAL(1u, networks.size());
5792 VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name));
5793 VERIFY_ARE_EQUAL(std::string("bridge"), std::string(networks[0].Driver));
@@ -5949,7 +6025,7 @@ class WSLCTests
6025 ResetTestSession();
6026
6027 wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
5952 - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
6028 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address<ULONG>()));
6029 VERIFY_ARE_EQUAL(1u, networks.size());
6030 VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name));
6031 VERIFY_ARE_EQUAL(std::string("bridge"), std::string(networks[0].Driver));
@@ -6002,11 +6078,11 @@ class WSLCTests
6078 VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&optionsC, nullptr));
6079
6080 wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
6005 - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
6081 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address<ULONG>()));
6082 VERIFY_ARE_EQUAL(3u, networks.size());
6083
6084 VERIFY_SUCCEEDED(m_defaultSession->DeleteNetwork(networkNameB.c_str()));
6009 - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
6085 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address<ULONG>()));
6086 VERIFY_ARE_EQUAL(2u, networks.size());
6087 }
6088
test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp
+182
@@ -28,6 +28,10 @@ class WSLCE2ENetworkListTests
28 {
29 EnsureNetworkDoesNotExist(TestNetworkName);
30 EnsureNetworkDoesNotExist(TestNetworkName2);
31 + for (const auto& name : FilterTestNetworkNames)
32 + {
33 + EnsureNetworkDoesNotExist(name);
34 + }
35 return true;
36 }
37
@@ -35,6 +39,10 @@ class WSLCE2ENetworkListTests
39 {
40 EnsureNetworkDoesNotExist(TestNetworkName);
41 EnsureNetworkDoesNotExist(TestNetworkName2);
42 + for (const auto& name : FilterTestNetworkNames)
43 + {
44 + EnsureNetworkDoesNotExist(name);
45 + }
46 return true;
47 }
48
@@ -155,8 +163,182 @@ class WSLCE2ENetworkListTests
163 }
164 }
165
166 + WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_MalformedValue)
167 + {
168 + // Filter values must be of the form key=value; bare keys are rejected by the CLI.
169 + const auto result = RunWslc(L"network list --filter label");
170 + result.Verify({.Stdout = L"", .ExitCode = 1});
171 + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label")));
172 + }
173 +
174 + WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_InvalidKey)
175 + {
176 + // Unknown filter keys are rejected by the Docker daemon.
177 + const auto result = RunWslc(L"network list --filter color=blue");
178 + VERIFY_ARE_EQUAL(1, result.ExitCode);
179 + VERIFY_IS_TRUE(result.Stderr.has_value());
180 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.Stderr->find(L"invalid filter"));
181 + }
182 +
183 + WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_Driver)
184 + {
185 + const std::wstring alpha = L"wslc-flt-list-driver-alpha";
186 + const std::wstring beta = L"wslc-flt-list-driver-beta";
187 + auto cleanup = wil::scope_exit([&]() {
188 + EnsureNetworkDoesNotExist(alpha);
189 + EnsureNetworkDoesNotExist(beta);
190 + });
191 +
192 + auto result = RunWslc(std::format(L"network create --driver bridge {}", alpha));
193 + result.Verify({.Stderr = L"", .ExitCode = 0});
194 + result = RunWslc(std::format(L"network create --driver bridge {}", beta));
195 + result.Verify({.Stderr = L"", .ExitCode = 0});
196 +
197 + auto listNames = [&](const std::wstring& filterArgs) {
198 + auto r = RunWslc(std::format(L"network list --format json {}", filterArgs));
199 + r.Verify({.Stderr = L"", .ExitCode = 0});
200 + const auto networks = ParseNdjsonOutputAs<WSLCNetworkInformation>(r);
201 + std::set<std::string> names;
202 + for (const auto& n : networks)
203 + {
204 + names.insert(n.Name);
205 + }
206 + return names;
207 + };
208 +
209 + {
210 + const auto names = listNames(L"--filter driver=bridge");
211 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(alpha)));
212 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(beta)));
213 + }
214 +
215 + // overlay networks require swarm mode; none exist in the test session.
216 + {
217 + const auto names = listNames(L"--filter driver=overlay");
218 + VERIFY_IS_FALSE(names.contains(WideToMultiByte(alpha)));
219 + VERIFY_IS_FALSE(names.contains(WideToMultiByte(beta)));
220 + }
221 + }
222 +
223 + WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_Label)
224 + {
225 + const std::wstring alpha = L"wslc-flt-list-label-alpha";
226 + const std::wstring beta = L"wslc-flt-list-label-beta";
227 + const std::wstring scopeKey = L"wslc.e2e.list_filter_label";
228 + const std::wstring scopeValue = L"1";
229 +
230 + auto cleanup = wil::scope_exit([&]() {
231 + EnsureNetworkDoesNotExist(alpha);
232 + EnsureNetworkDoesNotExist(beta);
233 + });
234 +
235 + // alpha carries both scope-key=1 and env=prod; beta carries only scope-key=1.
236 + auto result =
237 + RunWslc(std::format(L"network create --driver bridge --label {}={} --label env=prod {}", scopeKey, scopeValue, alpha));
238 + result.Verify({.Stderr = L"", .ExitCode = 0});
239 +
240 + result = RunWslc(std::format(L"network create --driver bridge --label {}={} {}", scopeKey, scopeValue, beta));
241 + result.Verify({.Stderr = L"", .ExitCode = 0});
242 +
243 + auto listNames = [&](const std::wstring& filterArgs) {
244 + auto r = RunWslc(std::format(L"network list --format json {}", filterArgs));
245 + r.Verify({.Stderr = L"", .ExitCode = 0});
246 + const auto networks = ParseNdjsonOutputAs<WSLCNetworkInformation>(r);
247 + std::set<std::string> names;
248 + for (const auto& n : networks)
249 + {
250 + names.insert(n.Name);
251 + }
252 + return names;
253 + };
254 +
255 + // label=<key> (key-only) matches any value.
256 + {
257 + const auto names = listNames(std::format(L"--filter label={}", scopeKey));
258 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(alpha)));
259 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(beta)));
260 + }
261 +
262 + // label=<key>=<value> narrows to alpha.
263 + {
264 + const auto names = listNames(std::format(L"--filter label={}={} --filter label=env=prod", scopeKey, scopeValue));
265 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(alpha)));
266 + VERIFY_IS_FALSE(names.contains(WideToMultiByte(beta)));
267 + }
268 +
269 + // Multiple --filter label= entries are AND'd.
270 + {
271 + const auto names = listNames(std::format(L"--filter label={} --filter label=env=prod", scopeKey));
272 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(alpha)));
273 + VERIFY_IS_FALSE(names.contains(WideToMultiByte(beta)));
274 + }
275 + }
276 +
277 + WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_JsonEmptyIsExactlyEmpty)
278 + {
279 + const std::wstring alpha = L"wslc-flt-list-empty-alpha";
280 + auto cleanup = wil::scope_exit([&]() { EnsureNetworkDoesNotExist(alpha); });
281 +
282 + auto result = RunWslc(std::format(L"network create --driver bridge {}", alpha));
283 + result.Verify({.Stderr = L"", .ExitCode = 0});
284 +
285 + // NDJSON with zero rows must be exactly empty stdout — not "[]", not "\n".
286 + result = RunWslc(L"network list --format json --filter name=wslc-flt-list-no-such-network-zzz");
287 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
288 + }
289 +
290 + WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_Name)
291 + {
292 + const std::wstring alpha = L"wslc-flt-list-name-alpha";
293 + const std::wstring beta = L"wslc-flt-list-name-beta";
294 + auto cleanup = wil::scope_exit([&]() {
295 + EnsureNetworkDoesNotExist(alpha);
296 + EnsureNetworkDoesNotExist(beta);
297 + });
298 +
299 + auto result = RunWslc(std::format(L"network create --driver bridge {}", alpha));
300 + result.Verify({.Stderr = L"", .ExitCode = 0});
301 + result = RunWslc(std::format(L"network create --driver bridge {}", beta));
302 + result.Verify({.Stderr = L"", .ExitCode = 0});
303 +
304 + auto listNames = [&](const std::wstring& filterArgs) {
305 + auto r = RunWslc(std::format(L"network list --format json {}", filterArgs));
306 + r.Verify({.Stderr = L"", .ExitCode = 0});
307 + const auto networks = ParseNdjsonOutputAs<WSLCNetworkInformation>(r);
308 + std::set<std::string> names;
309 + for (const auto& n : networks)
310 + {
311 + names.insert(n.Name);
312 + }
313 + return names;
314 + };
315 +
316 + // Docker's `name` filter is a substring match; the shared prefix picks up both networks.
317 + {
318 + const auto names = listNames(L"--filter name=wslc-flt-list-name-");
319 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(alpha)));
320 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(beta)));
321 + }
322 +
323 + // A narrower substring selects only the matching one.
324 + {
325 + const auto names = listNames(L"--filter name=name-alpha");
326 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(alpha)));
327 + VERIFY_IS_FALSE(names.contains(WideToMultiByte(beta)));
328 + }
329 + }
330 +
331 private:
332 const std::wstring TestNetworkName = L"wslc-e2e-network-list";
333 const std::wstring TestNetworkName2 = L"wslc-e2e-network-list-2";
334 + const std::vector<std::wstring> FilterTestNetworkNames = {
335 + L"wslc-flt-list-driver-alpha",
336 + L"wslc-flt-list-driver-beta",
337 + L"wslc-flt-list-label-alpha",
338 + L"wslc-flt-list-label-beta",
339 + L"wslc-flt-list-empty-alpha",
340 + L"wslc-flt-list-name-alpha",
341 + L"wslc-flt-list-name-beta",
342 + };
343 };
344 } // namespace WSLCE2ETests