diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 178d6cf0c..017479c7e 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -735,7 +735,7 @@ interface IWSLCSession : IUnknown // Network management. HRESULT CreateNetwork([in] const WSLCNetworkOptions* Options, [in, unique] IWarningCallback* WarningCallback); HRESULT DeleteNetwork([in] LPCSTR Name); - HRESULT ListNetworks([out, size_is(, *Count)] WSLCNetworkInformation** Networks, [out] ULONG* Count); + HRESULT ListNetworks([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out, size_is(, *Count)] WSLCNetworkInformation** Networks, [out] ULONG* Count); HRESULT InspectNetwork([in] LPCSTR Name, [out] LPSTR* Output); HRESULT PruneNetworks([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out, size_is(, *NetworksCount)] WSLCNetworkName** Networks, [out] ULONG* NetworksCount); diff --git a/src/windows/wslc/commands/NetworkListCommand.cpp b/src/windows/wslc/commands/NetworkListCommand.cpp index 1897c1937..35047d199 100644 --- a/src/windows/wslc/commands/NetworkListCommand.cpp +++ b/src/windows/wslc/commands/NetworkListCommand.cpp @@ -27,6 +27,7 @@ namespace wsl::windows::wslc { std::vector NetworkListCommand::GetArguments() const { return { + Argument::Create(ArgType::Filter, false, Limit::Unlimited), Argument::Create(ArgType::Format), Argument::Create(ArgType::Quiet, false, std::nullopt, Localization::WSLCCLI_NetworkListQuietArgDesc()), }; diff --git a/src/windows/wslc/services/NetworkService.cpp b/src/windows/wslc/services/NetworkService.cpp index 35bc8ef35..d9ff688a5 100644 --- a/src/windows/wslc/services/NetworkService.cpp +++ b/src/windows/wslc/services/NetworkService.cpp @@ -75,11 +75,19 @@ void NetworkService::Delete(models::Session& session, const std::string& name) THROW_IF_FAILED(session.Get()->DeleteNetwork(name.c_str())); } -std::vector NetworkService::List(models::Session& session) +std::vector NetworkService::List(models::Session& session, const std::vector>& filters) { + std::vector filterEntries; + filterEntries.reserve(filters.size()); + for (const auto& [key, value] : filters) + { + filterEntries.push_back({.Key = key.c_str(), .Value = value.c_str()}); + } + wil::unique_cotaskmem_array_ptr rawNetworks; ULONG count = 0; - THROW_IF_FAILED(session.Get()->ListNetworks(&rawNetworks, &count)); + THROW_IF_FAILED(session.Get()->ListNetworks( + filterEntries.empty() ? nullptr : filterEntries.data(), static_cast(filterEntries.size()), &rawNetworks, &count)); std::vector networks; networks.reserve(count); diff --git a/src/windows/wslc/services/NetworkService.h b/src/windows/wslc/services/NetworkService.h index 090379d15..a37e4fb77 100644 --- a/src/windows/wslc/services/NetworkService.h +++ b/src/windows/wslc/services/NetworkService.h @@ -24,7 +24,7 @@ struct NetworkService { static void Create(Terminal& terminal, models::Session& session, const models::CreateNetworkOptions& createOptions); static void Delete(models::Session& session, const std::string& name); - static std::vector List(models::Session& session); + static std::vector List(models::Session& session, const std::vector>& filters = {}); static wsl::windows::common::wslc_schema::Network Inspect(models::Session& session, const std::string& name); static models::PruneNetworksResult Prune(models::Session& session, const std::vector>& filters = {}); static void Connect(models::Session& session, const models::ConnectNetworkOptions& connectOptions); diff --git a/src/windows/wslc/tasks/NetworkTasks.cpp b/src/windows/wslc/tasks/NetworkTasks.cpp index 10cb417a9..5dafec4c6 100644 --- a/src/windows/wslc/tasks/NetworkTasks.cpp +++ b/src/windows/wslc/tasks/NetworkTasks.cpp @@ -139,7 +139,9 @@ void GetNetworks(CLIExecutionContext& context) { WI_ASSERT(context.Data.Contains(Data::Session)); auto& session = context.Data.Get(); - context.Data.Add(NetworkService::List(session)); + + auto filters = context.Args.GetAllValues(); + context.Data.Add(NetworkService::List(session, filters)); } void InspectNetworks(CLIExecutionContext& context) diff --git a/src/windows/wslcsession/DockerHTTPClient.cpp b/src/windows/wslcsession/DockerHTTPClient.cpp index a85325af0..7a6bd398c 100644 --- a/src/windows/wslcsession/DockerHTTPClient.cpp +++ b/src/windows/wslcsession/DockerHTTPClient.cpp @@ -493,9 +493,16 @@ void DockerHTTPClient::DisconnectContainerFromNetwork(const std::string& Network Transaction(verb::post, URL::Create("/networks/{}/disconnect", NetworkName), Request); } -std::vector DockerHTTPClient::ListNetworks() +std::vector DockerHTTPClient::ListNetworks(const std::map>& filters) { - return Transaction>(verb::get, URL::Create("/networks")); + auto url = URL::Create("/networks"); + + if (!filters.empty()) + { + url.SetParameter("filters", nlohmann::json(filters).dump()); + } + + return Transaction>(verb::get, url); } docker_schema::Network DockerHTTPClient::InspectNetwork(const std::string& Name) diff --git a/src/windows/wslcsession/DockerHTTPClient.h b/src/windows/wslcsession/DockerHTTPClient.h index d98108446..dc66a8df5 100644 --- a/src/windows/wslcsession/DockerHTTPClient.h +++ b/src/windows/wslcsession/DockerHTTPClient.h @@ -157,7 +157,7 @@ class DockerHTTPClient // Network management. common::docker_schema::CreateNetworkResponse CreateNetwork(const common::docker_schema::CreateNetwork& Request); void RemoveNetwork(const std::string& Name); - std::vector ListNetworks(); + std::vector ListNetworks(const std::map>& filters = {}); common::docker_schema::Network InspectNetwork(const std::string& Name); void ConnectContainerToNetwork(const std::string& NetworkName, const common::docker_schema::ContainerNetworkRequest& Request); void DisconnectContainerFromNetwork(const std::string& NetworkName, const common::docker_schema::ContainerNetworkRequest& Request); diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index cf084e03d..e45394a8a 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -2979,7 +2979,7 @@ try } CATCH_RETURN(); -HRESULT WSLCSession::ListNetworks(WSLCNetworkInformation** Networks, ULONG* Count) +HRESULT WSLCSession::ListNetworks(const WSLCFilter* Filters, ULONG FiltersCount, WSLCNetworkInformation** Networks, ULONG* Count) try { WSLCExecutionContext context(this); @@ -2990,9 +2990,46 @@ try *Networks = nullptr; *Count = 0; + auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); + const bool filtered = !filters.empty(); + + if (filtered) + { + // Scope the filtered query to WSLC-managed networks. + filters["label"].push_back(WSLCNetworkManagedLabel); + } + auto lock = AcquireLease(); + if (filtered) + { + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); + } + std::lock_guard networksLock(m_networksLock); + std::optional> dockerNames; + if (filtered) + { + std::vector dockerNetworks; + try + { + dockerNetworks = m_runtime.Docker().ListNetworks(filters); + } + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list networks"); + + dockerNames.emplace(); + for (const auto& dockerNetwork : dockerNetworks) + { + dockerNames->insert(dockerNetwork.Name); + + // Only report networks that we manage. + if (!m_networks.contains(dockerNetwork.Name)) + { + WSL_LOG("ListedUnknownNetwork", TraceLoggingValue(dockerNetwork.Name.c_str(), "NetworkName")); + } + } + } + if (m_networks.empty()) { return S_OK; @@ -3003,6 +3040,11 @@ try ULONG index = 0; for (const auto& [name, entry] : m_networks) { + if (dockerNames.has_value() && !dockerNames->contains(name)) + { + continue; + } + THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Name, name.c_str()) != 0); THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Id, entry.Id.c_str()) != 0); THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Driver, entry.Driver.c_str()) != 0); diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index 14379c2eb..0a302522c 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -194,7 +194,9 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession // Network management. IFACEMETHOD(CreateNetwork)(_In_ const WSLCNetworkOptions* Options, _In_opt_ IWarningCallback* WarningCallback) override; IFACEMETHOD(DeleteNetwork)(_In_ LPCSTR Name) override; - IFACEMETHOD(ListNetworks)(_Out_ WSLCNetworkInformation** Networks, _Out_ ULONG* Count) override; + IFACEMETHOD(ListNetworks) + (_In_reads_opt_(FiltersCount) const WSLCFilter* Filters, _In_ ULONG FiltersCount, _Out_ WSLCNetworkInformation** Networks, _Out_ ULONG* Count) + override; IFACEMETHOD(InspectNetwork)(_In_ LPCSTR Name, _Out_ LPSTR* Output) override; IFACEMETHOD(PruneNetworks) (_In_reads_opt_(FiltersCount) const WSLCFilter* Filters, _In_ ULONG FiltersCount, _Out_ WSLCNetworkName** Networks, _Out_ ULONG* NetworksCount) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index cd3085375..48e8f51af 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -5374,7 +5374,7 @@ class WSLCTests // List should start empty. wil::unique_cotaskmem_array_ptr networks; - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address())); + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address())); VERIFY_ARE_EQUAL(0u, networks.size()); WSLCNetworkOptions options{}; @@ -5387,7 +5387,7 @@ class WSLCTests auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); }); // Verify it appears in the list with correct fields. - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address())); + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address())); VERIFY_ARE_EQUAL(1u, networks.size()); VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name)); VERIFY_ARE_EQUAL(std::string("bridge"), std::string(networks[0].Driver)); @@ -5400,7 +5400,7 @@ class WSLCTests VERIFY_SUCCEEDED(m_defaultSession->DeleteNetwork(networkName.c_str())); // List should be empty again. - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address())); + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address())); VERIFY_ARE_EQUAL(0u, networks.size()); // Delete non-existent should fail. @@ -5418,6 +5418,82 @@ class WSLCTests VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options, nullptr)); } + WSLC_TEST_METHOD(ListNetworksFilters) + { + const std::string netA = "wslc-flt-net-a"; + const std::string netB = "wslc-flt-net-b"; + const std::string netC = "wslc-flt-net-c"; + const std::string testLabelKey = "wslc.test.list_filter"; + const std::string testLabelValue = "1"; + const std::string testLabelKV = testLabelKey + "=" + testLabelValue; + const std::string managedLabel = "com.microsoft.wsl.network.managed"; + + auto cleanup = wil::scope_exit([&]() { + for (const auto& name : {netA, netB, netC}) + { + LOG_IF_FAILED(m_defaultSession->DeleteNetwork(name.c_str())); + } + }); + + CreateNamedNetwork(netA, {{testLabelKey.c_str(), testLabelValue.c_str()}, {"env", "prod"}, {"tier", "web"}}); + CreateNamedNetwork(netB, {{testLabelKey.c_str(), testLabelValue.c_str()}, {"env", "test"}}); + CreateNamedNetwork(netC, {{testLabelKey.c_str(), testLabelValue.c_str()}, {"env", "prod"}}); + + auto expectListFails = [&](HRESULT expected, const std::vector& filters) { + const WSLCFilter* filtersPtr = filters.empty() ? nullptr : filters.data(); + const ULONG filtersCount = static_cast(filters.size()); + + wil::unique_cotaskmem_array_ptr networks; + VERIFY_ARE_EQUAL( + expected, m_defaultSession->ListNetworks(filtersPtr, filtersCount, networks.addressof(), networks.size_address())); + }; + + auto expectList = [&](const std::vector& expected, + const std::vector& filters, + const std::source_location& source = std::source_location::current()) { + const WSLCFilter* filtersPtr = filters.empty() ? nullptr : filters.data(); + const ULONG filtersCount = static_cast(filters.size()); + + wil::unique_cotaskmem_array_ptr networks; + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(filtersPtr, filtersCount, networks.addressof(), networks.size_address())); + + std::vector names; + for (const auto& n : networks) + { + names.emplace_back(n.Name); + VERIFY_IS_TRUE(strlen(n.Id) > 0); + VERIFY_ARE_EQUAL(std::string("bridge"), std::string(n.Driver)); + } + + VerifyAreEqualUnordered(expected, names, source); + }; + + const std::vector all{netA, netB, netC}; + + expectList(all, {{"label", testLabelKV.c_str()}}); + + // label== selects a subset within this test's scope. + expectList({netA, netC}, {{"label", testLabelKV.c_str()}, {"label", "env=prod"}}); + expectList({netB}, {{"label", testLabelKV.c_str()}, {"label", "env=test"}}); + + // Multiple label filters are AND'd. + expectList({netA}, {{"label", testLabelKV.c_str()}, {"label", "env=prod"}, {"label", "tier=web"}}); + + // label= (key-only) matches any stored value. + expectList(all, {{"label", testLabelKV.c_str()}, {"label", "env"}}); + + // driver filter combined with the test-scope label. + expectList(all, {{"label", testLabelKV.c_str()}, {"driver", "bridge"}}); + expectList({}, {{"label", testLabelKV.c_str()}, {"driver", "nonexistent"}}); + + // Explicit managed-label filter is idempotent with the auto-injected one. + expectList(all, {{"label", testLabelKV.c_str()}, {"label", managedLabel.c_str()}}); + + // Null filter key/value is rejected. + expectListFails(E_POINTER, {{nullptr, "anything"}}); + expectListFails(E_POINTER, {{"label", nullptr}}); + } + WSLC_TEST_METHOD(PruneNetworksTest) { auto expectPrune = [&](const std::vector& expected, @@ -5457,7 +5533,7 @@ class WSLCTests expectPrune({a, b}); wil::unique_cotaskmem_array_ptr networks; - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address())); + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address())); for (const auto& n : networks) { VERIFY_ARE_NOT_EQUAL(a, std::string(n.Name)); @@ -5598,7 +5674,7 @@ class WSLCTests VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options, nullptr)); wil::unique_cotaskmem_array_ptr networks; - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address())); + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address())); VERIFY_ARE_EQUAL(1u, networks.size()); VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name)); } @@ -5661,7 +5737,7 @@ class WSLCTests VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options, nullptr)); wil::unique_cotaskmem_array_ptr networks; - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address())); + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address())); VERIFY_ARE_EQUAL(1u, networks.size()); VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name)); VERIFY_ARE_EQUAL(std::string("bridge"), std::string(networks[0].Driver)); @@ -5899,7 +5975,7 @@ class WSLCTests ResetTestSession(); wil::unique_cotaskmem_array_ptr networks; - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address())); + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address())); VERIFY_ARE_EQUAL(1u, networks.size()); VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name)); VERIFY_ARE_EQUAL(std::string("bridge"), std::string(networks[0].Driver)); @@ -5952,11 +6028,11 @@ class WSLCTests VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&optionsC, nullptr)); wil::unique_cotaskmem_array_ptr networks; - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address())); + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address())); VERIFY_ARE_EQUAL(3u, networks.size()); VERIFY_SUCCEEDED(m_defaultSession->DeleteNetwork(networkNameB.c_str())); - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address())); + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address())); VERIFY_ARE_EQUAL(2u, networks.size()); } diff --git a/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp index 83101a40c..cde0d84f3 100644 --- a/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp @@ -28,6 +28,10 @@ class WSLCE2ENetworkListTests { EnsureNetworkDoesNotExist(TestNetworkName); EnsureNetworkDoesNotExist(TestNetworkName2); + for (const auto& name : FilterTestNetworkNames) + { + EnsureNetworkDoesNotExist(name); + } return true; } @@ -35,6 +39,10 @@ class WSLCE2ENetworkListTests { EnsureNetworkDoesNotExist(TestNetworkName); EnsureNetworkDoesNotExist(TestNetworkName2); + for (const auto& name : FilterTestNetworkNames) + { + EnsureNetworkDoesNotExist(name); + } return true; } @@ -92,8 +100,182 @@ class WSLCE2ENetworkListTests VERIFY_ARE_NOT_EQUAL(names.end(), std::find(names.begin(), names.end(), WideToMultiByte(TestNetworkName2))); } + WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_MalformedValue) + { + // Filter values must be of the form key=value; bare keys are rejected by the CLI. + const auto result = RunWslc(L"network list --filter label"); + result.Verify({.Stdout = L"", .ExitCode = 1}); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label"))); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_InvalidKey) + { + // Unknown filter keys are rejected by the Docker daemon. + const auto result = RunWslc(L"network list --filter color=blue"); + VERIFY_ARE_EQUAL(1, result.ExitCode); + VERIFY_IS_TRUE(result.Stderr.has_value()); + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.Stderr->find(L"invalid filter")); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_Driver) + { + const std::wstring alpha = L"wslc-flt-list-driver-alpha"; + const std::wstring beta = L"wslc-flt-list-driver-beta"; + auto cleanup = wil::scope_exit([&]() { + EnsureNetworkDoesNotExist(alpha); + EnsureNetworkDoesNotExist(beta); + }); + + auto result = RunWslc(std::format(L"network create --driver bridge {}", alpha)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + result = RunWslc(std::format(L"network create --driver bridge {}", beta)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + auto listNames = [&](const std::wstring& filterArgs) { + auto r = RunWslc(std::format(L"network list --format json {}", filterArgs)); + r.Verify({.Stderr = L"", .ExitCode = 0}); + const auto networks = ParseNdjsonOutputAs(r); + std::set names; + for (const auto& n : networks) + { + names.insert(n.Name); + } + return names; + }; + + { + const auto names = listNames(L"--filter driver=bridge"); + VERIFY_IS_TRUE(names.contains(WideToMultiByte(alpha))); + VERIFY_IS_TRUE(names.contains(WideToMultiByte(beta))); + } + + // overlay networks require swarm mode; none exist in the test session. + { + const auto names = listNames(L"--filter driver=overlay"); + VERIFY_IS_FALSE(names.contains(WideToMultiByte(alpha))); + VERIFY_IS_FALSE(names.contains(WideToMultiByte(beta))); + } + } + + WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_Label) + { + const std::wstring alpha = L"wslc-flt-list-label-alpha"; + const std::wstring beta = L"wslc-flt-list-label-beta"; + const std::wstring scopeKey = L"wslc.e2e.list_filter_label"; + const std::wstring scopeValue = L"1"; + + auto cleanup = wil::scope_exit([&]() { + EnsureNetworkDoesNotExist(alpha); + EnsureNetworkDoesNotExist(beta); + }); + + // alpha carries both scope-key=1 and env=prod; beta carries only scope-key=1. + auto result = + RunWslc(std::format(L"network create --driver bridge --label {}={} --label env=prod {}", scopeKey, scopeValue, alpha)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + result = RunWslc(std::format(L"network create --driver bridge --label {}={} {}", scopeKey, scopeValue, beta)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + auto listNames = [&](const std::wstring& filterArgs) { + auto r = RunWslc(std::format(L"network list --format json {}", filterArgs)); + r.Verify({.Stderr = L"", .ExitCode = 0}); + const auto networks = ParseNdjsonOutputAs(r); + std::set names; + for (const auto& n : networks) + { + names.insert(n.Name); + } + return names; + }; + + // label= (key-only) matches any value. + { + const auto names = listNames(std::format(L"--filter label={}", scopeKey)); + VERIFY_IS_TRUE(names.contains(WideToMultiByte(alpha))); + VERIFY_IS_TRUE(names.contains(WideToMultiByte(beta))); + } + + // label== narrows to alpha. + { + const auto names = listNames(std::format(L"--filter label={}={} --filter label=env=prod", scopeKey, scopeValue)); + VERIFY_IS_TRUE(names.contains(WideToMultiByte(alpha))); + VERIFY_IS_FALSE(names.contains(WideToMultiByte(beta))); + } + + // Multiple --filter label= entries are AND'd. + { + const auto names = listNames(std::format(L"--filter label={} --filter label=env=prod", scopeKey)); + VERIFY_IS_TRUE(names.contains(WideToMultiByte(alpha))); + VERIFY_IS_FALSE(names.contains(WideToMultiByte(beta))); + } + } + + WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_JsonEmptyIsExactlyEmpty) + { + const std::wstring alpha = L"wslc-flt-list-empty-alpha"; + auto cleanup = wil::scope_exit([&]() { EnsureNetworkDoesNotExist(alpha); }); + + auto result = RunWslc(std::format(L"network create --driver bridge {}", alpha)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + // NDJSON with zero rows must be exactly empty stdout — not "[]", not "\n". + result = RunWslc(L"network list --format json --filter name=wslc-flt-list-no-such-network-zzz"); + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); + } + + WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_Name) + { + const std::wstring alpha = L"wslc-flt-list-name-alpha"; + const std::wstring beta = L"wslc-flt-list-name-beta"; + auto cleanup = wil::scope_exit([&]() { + EnsureNetworkDoesNotExist(alpha); + EnsureNetworkDoesNotExist(beta); + }); + + auto result = RunWslc(std::format(L"network create --driver bridge {}", alpha)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + result = RunWslc(std::format(L"network create --driver bridge {}", beta)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + auto listNames = [&](const std::wstring& filterArgs) { + auto r = RunWslc(std::format(L"network list --format json {}", filterArgs)); + r.Verify({.Stderr = L"", .ExitCode = 0}); + const auto networks = ParseNdjsonOutputAs(r); + std::set names; + for (const auto& n : networks) + { + names.insert(n.Name); + } + return names; + }; + + // Docker's `name` filter is a substring match; the shared prefix picks up both networks. + { + const auto names = listNames(L"--filter name=wslc-flt-list-name-"); + VERIFY_IS_TRUE(names.contains(WideToMultiByte(alpha))); + VERIFY_IS_TRUE(names.contains(WideToMultiByte(beta))); + } + + // A narrower substring selects only the matching one. + { + const auto names = listNames(L"--filter name=name-alpha"); + VERIFY_IS_TRUE(names.contains(WideToMultiByte(alpha))); + VERIFY_IS_FALSE(names.contains(WideToMultiByte(beta))); + } + } + private: const std::wstring TestNetworkName = L"wslc-e2e-network-list"; const std::wstring TestNetworkName2 = L"wslc-e2e-network-list-2"; + const std::vector FilterTestNetworkNames = { + L"wslc-flt-list-driver-alpha", + L"wslc-flt-list-driver-beta", + L"wslc-flt-list-label-alpha", + L"wslc-flt-list-label-beta", + L"wslc-flt-list-empty-alpha", + L"wslc-flt-list-name-alpha", + L"wslc-flt-list-name-beta", + }; }; } // namespace WSLCE2ETests