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