Match docker's network list, inspect and prune output in wslc (#41377)

Reworks `wslc network list`, `inspect` and `prune` to emit docker-compatible output: list gains a 4-column NETWORK ID/NAME/DRIVER/SCOPE table with truncated IDs and name sorting, and inspect returns the full network object. `ListNetworks` now returns JSON via `[out] LPSTR*` rather than the fixed-size `WSLCNetworkInformation` struct, which could not express docker's shape (notably the `Labels` map) without leaking nested strings from the marshalled array. Timestamp helpers are consolidated into `wsl::windows::common::string` as `Rfc3339ToEpoch`, `EpochToLocalDisplayTime` and `Rfc3339ToUtcDisplayTime`, since network timestamps render as UTC with fractional seconds preserved while image and container timestamps render in local time. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

ggarzia-MSFT committed Aug 20, 2026 at 10:37 UTC fc0e0b0c0eaa933580a800c557cd692283886b8a
24 files changed +540 -238
localization/strings/en-US/Resources.resw
+2 -3
@@ -3546,9 +3546,8 @@ On first run, creates the file with all settings commented out at their defaults
3546 <data name="WSLCCLI_NetworkPruneLongDesc" xml:space="preserve">
3547 <value>Removes all unused networks. A network is considered unused when it is not referenced by any container.</value>
3548 </data>
3549 - <data name="WSLCCLI_NetworkPruneDeleted" xml:space="preserve">
3550 - <value>Deleted: {}</value>
3551 - <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3549 + <data name="WSLCCLI_NetworkPruneDeletedHeader" xml:space="preserve">
3550 + <value>Deleted Networks:</value>
3551 </data>
3552 <data name="WSLCCLI_NetworkConnectDesc" xml:space="preserve">
3553 <value>Connect a container to a network.</value>
src/shared/inc/JsonUtils.h
-20
@@ -203,26 +203,6 @@ struct adl_serializer<WSLCVolumeInformation>
203 strncpy_s(volume.Driver, sizeof(volume.Driver), driver.c_str(), _TRUNCATE);
204 }
205 };
206 -
207 -template <>
208 -struct adl_serializer<WSLCNetworkInformation>
209 -{
210 - static void to_json(json& j, const WSLCNetworkInformation& network)
211 - {
212 - j = json{{"Name", std::string(network.Name)}, {"ID", std::string(network.Id)}, {"Driver", std::string(network.Driver)}};
213 - }
214 -
215 - static void from_json(const json& j, WSLCNetworkInformation& network)
216 - {
217 - std::string name = j.at("Name").get<std::string>();
218 - std::string id = j.at("ID").get<std::string>();
219 - std::string driver = j.at("Driver").get<std::string>();
220 -
221 - strncpy_s(network.Name, sizeof(network.Name), name.c_str(), _TRUNCATE);
222 - strncpy_s(network.Id, sizeof(network.Id), id.c_str(), _TRUNCATE);
223 - strncpy_s(network.Driver, sizeof(network.Driver), driver.c_str(), _TRUNCATE);
224 - }
225 -};
206 #endif
207
208 } // namespace nlohmann
\ No newline at end of file
src/windows/common/string.cpp
+47 -1
@@ -16,6 +16,7 @@ Abstract:
16 #include <charconv>
17 #include <cmath>
18 #include <limits>
19 +#include <sstream>
20
21 std::vector<std::string> wsl::windows::common::string::InitializeStringSet(_In_count_(BufferSize) LPCSTR Buffer, _In_ SIZE_T BufferSize)
22 {
@@ -425,7 +426,17 @@ std::string wsl::windows::common::string::TruncateId(_In_ std::string_view id, b
426 return TruncateIdImpl(id, shortenLength);
427 }
428
428 -std::string wsl::windows::common::string::FormatDockerTimestamp(LONGLONG timestamp)
429 +std::uint64_t wsl::windows::common::string::Rfc3339ToEpoch(const std::string& timestamp)
430 +{
431 + std::chrono::sys_seconds utcSeconds;
432 + std::istringstream stream(timestamp);
433 + stream >> std::chrono::parse("%FT%H:%M:%S%Z", utcSeconds);
434 + THROW_HR_IF_MSG(E_INVALIDARG, stream.fail(), "Failed to parse timestamp '%hs'", timestamp.c_str());
435 +
436 + return static_cast<std::uint64_t>(utcSeconds.time_since_epoch().count());
437 +}
438 +
439 +std::string wsl::windows::common::string::EpochToLocalDisplayTime(LONGLONG timestamp)
440 {
441 const auto time =
442 std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::from_time_t(static_cast<std::time_t>(timestamp)));
@@ -442,3 +453,38 @@ std::string wsl::windows::common::string::FormatDockerTimestamp(LONGLONG timesta
453 return std::format("{:%F %T} +0000 UTC", time);
454 }
455 }
456 +
457 +std::string wsl::windows::common::string::Rfc3339ToUtcDisplayTime(std::string_view timestamp)
458 +{
459 + if (timestamp.empty())
460 + {
461 + return {};
462 + }
463 +
464 + // Fractional digits vary in length, so they are captured verbatim and re-inserted after formatting.
465 + std::string parsable{timestamp};
466 + std::string fraction;
467 + const auto separator = parsable.find('.');
468 + if (separator != std::string::npos)
469 + {
470 + auto end = separator + 1;
471 + while (end < parsable.size() && (std::isdigit(static_cast<unsigned char>(parsable[end])) != 0))
472 + {
473 + end++;
474 + }
475 +
476 + fraction = parsable.substr(separator, end - separator);
477 + parsable.erase(separator, end - separator);
478 + }
479 +
480 + std::chrono::sys_seconds parsed{};
481 + std::istringstream stream(parsable);
482 + stream >> std::chrono::parse("%FT%H:%M:%S%Z", parsed);
483 + if (stream.fail())
484 + {
485 + return std::string{timestamp};
486 + }
487 +
488 + // Network timestamps are reported in UTC rather than the local time zone.
489 + return std::format("{:%F %T}{} +0000 UTC", parsed, fraction);
490 +}
src/windows/common/string.hpp
+11 -3
@@ -67,9 +67,17 @@ std::string WideToMultiByte(_In_ std::wstring_view Source);
67 std::wstring TruncateId(_In_ std::wstring_view id, bool shortenLength = true);
68 std::string TruncateId(_In_ std::string_view id, bool shortenLength = true);
69
70 -// Formats a unix timestamp the way docker does, matching Go's time.Time.String() layout. Falls back
71 -// to UTC when the time zone database is unavailable.
72 -std::string FormatDockerTimestamp(LONGLONG timestamp);
70 +// Converts an RFC 3339 timestamp to seconds since the unix epoch. Only the 'Z' zone designator is
71 +// accepted; numeric offsets are not.
72 +std::uint64_t Rfc3339ToEpoch(const std::string& timestamp);
73 +
74 +// Renders seconds since the unix epoch in the local time zone, using the layout
75 +// "2006-01-02 15:04:05 -0700 MST". Falls back to UTC when the time zone database is unavailable.
76 +std::string EpochToLocalDisplayTime(LONGLONG timestamp);
77 +
78 +// Renders an RFC 3339 timestamp in the same layout, but as UTC and with its fractional seconds
79 +// preserved. The input is returned unchanged when it cannot be parsed.
80 +std::string Rfc3339ToUtcDisplayTime(std::string_view timestamp);
81
82 // Template implementation for TruncateId to avoid code duplication.
83 // Algorithm inspired from Moby for consistency in presentation of shortened IDs.
src/windows/inc/docker_schema.h
+78 -5
@@ -22,6 +22,20 @@ namespace wsl::windows::common::docker_schema {
22
23 using wsl::shared::EmptyObject;
24
25 +// Reads a value, treating both a missing key and an explicit null as absent. The daemon reports some
26 +// empty maps and objects as null, which the default deserializer rejects.
27 +template <typename T>
28 +T ValueOrNull(const nlohmann::json& json, const char* key, T defaultValue = T{})
29 +{
30 + const auto entry = json.find(key);
31 + if (entry == json.end() || entry->is_null())
32 + {
33 + return defaultValue;
34 + }
35 +
36 + return entry->get<T>();
37 +}
38 +
39 struct CreatedContainer
40 {
41 std::string Id;
@@ -127,8 +141,37 @@ struct IPAM
141 {
142 std::string Driver;
143 std::optional<std::vector<IPAMConfig>> Config;
144 + std::map<std::string, std::string> Options;
145 +};
146
131 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(IPAM, Driver, Config);
147 +inline void to_json(nlohmann::json& j, const IPAM& ipam)
148 +{
149 + j = nlohmann::json{{"Driver", ipam.Driver}, {"Config", ipam.Config}, {"Options", ipam.Options}};
150 +}
151 +
152 +inline void from_json(const nlohmann::json& j, IPAM& ipam)
153 +{
154 + ipam.Driver = ValueOrNull<std::string>(j, "Driver");
155 + ipam.Config = ValueOrNull<std::optional<std::vector<IPAMConfig>>>(j, "Config");
156 + ipam.Options = ValueOrNull<std::map<std::string, std::string>>(j, "Options");
157 +}
158 +
159 +struct NetworkConfigFrom
160 +{
161 + std::string Network;
162 +
163 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(NetworkConfigFrom, Network);
164 +};
165 +
166 +struct NetworkContainer
167 +{
168 + std::string Name;
169 + std::string EndpointID;
170 + std::string MacAddress;
171 + std::string IPv4Address;
172 + std::string IPv6Address;
173 +
174 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(NetworkContainer, Name, EndpointID, MacAddress, IPv4Address, IPv6Address);
175 };
176
177 struct CreateNetworkResponse
@@ -157,15 +200,45 @@ struct Network
200 {
201 std::string Id;
202 std::string Name;
203 + std::string Created;
204 std::string Driver;
205 std::string Scope;
206 + bool EnableIPv4{true};
207 + bool EnableIPv6{};
208 bool Internal{};
209 + bool Attachable{};
210 + bool Ingress{};
211 + bool ConfigOnly{};
212 + NetworkConfigFrom ConfigFrom;
213 IPAM IPAM;
164 - std::optional<std::map<std::string, std::string>> Options;
214 + std::map<std::string, std::string> Options;
215 std::map<std::string, std::string> Labels;
166 -
167 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Network, Id, Name, Driver, Scope, Internal, IPAM, Options, Labels);
168 -};
216 + std::map<std::string, NetworkContainer> Containers;
217 + nlohmann::json Status = nlohmann::json::object();
218 +};
219 +
220 +inline void from_json(const nlohmann::json& j, Network& network)
221 +{
222 + const Network defaults{};
223 +
224 + network.Id = ValueOrNull<std::string>(j, "Id");
225 + network.Name = ValueOrNull<std::string>(j, "Name");
226 + network.Created = ValueOrNull<std::string>(j, "Created");
227 + network.Driver = ValueOrNull<std::string>(j, "Driver");
228 + network.Scope = ValueOrNull<std::string>(j, "Scope");
229 + network.EnableIPv4 = ValueOrNull<bool>(j, "EnableIPv4", defaults.EnableIPv4);
230 + network.EnableIPv6 = ValueOrNull<bool>(j, "EnableIPv6");
231 + network.Internal = ValueOrNull<bool>(j, "Internal");
232 + network.Attachable = ValueOrNull<bool>(j, "Attachable");
233 + network.Ingress = ValueOrNull<bool>(j, "Ingress");
234 + network.ConfigOnly = ValueOrNull<bool>(j, "ConfigOnly");
235 + network.ConfigFrom = ValueOrNull<NetworkConfigFrom>(j, "ConfigFrom");
236 + network.IPAM = ValueOrNull<docker_schema::IPAM>(j, "IPAM");
237 + network.Options = ValueOrNull<std::map<std::string, std::string>>(j, "Options");
238 + network.Labels = ValueOrNull<std::map<std::string, std::string>>(j, "Labels");
239 + network.Containers = ValueOrNull<std::map<std::string, NetworkContainer>>(j, "Containers");
240 + network.Status = ValueOrNull<nlohmann::json>(j, "Status", defaults.Status);
241 +}
242
243 struct EndpointIPAMConfig
244 {
src/windows/inc/wslc_schema.h
+68 -5
@@ -221,30 +221,93 @@ struct IPAMConfig
221 std::string Subnet;
222 std::string Gateway;
223 std::string IPRange;
224 -
225 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(IPAMConfig, Subnet, Gateway, IPRange);
224 };
225
226 +// The gateway and ip range are omitted when unset rather than emitted as empty strings.
227 +inline void to_json(nlohmann::json& j, const IPAMConfig& config)
228 +{
229 + j = nlohmann::json::object();
230 + j["Subnet"] = config.Subnet;
231 +
232 + if (!config.Gateway.empty())
233 + {
234 + j["Gateway"] = config.Gateway;
235 + }
236 +
237 + if (!config.IPRange.empty())
238 + {
239 + j["IPRange"] = config.IPRange;
240 + }
241 +}
242 +
243 +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT_FROM_ONLY(IPAMConfig, Subnet, Gateway, IPRange);
244 +
245 struct IPAM
246 {
247 std::string Driver;
248 std::optional<std::vector<IPAMConfig>> Config;
249 + std::map<std::string, std::string> Options;
250 +
251 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(IPAM, Driver, Config, Options);
252 +};
253 +
254 +struct NetworkConfigFrom
255 +{
256 + std::string Network;
257 +
258 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(NetworkConfigFrom, Network);
259 +};
260 +
261 +struct NetworkContainer
262 +{
263 + std::string Name;
264 + std::string EndpointID;
265 + std::string MacAddress;
266 + std::string IPv4Address;
267 + std::string IPv6Address;
268
233 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(IPAM, Driver, Config);
269 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(NetworkContainer, Name, EndpointID, MacAddress, IPv4Address, IPv6Address);
270 };
271
272 struct Network
273 {
274 std::string Id;
275 std::string Name;
276 + std::string Created;
277 std::string Driver;
278 std::string Scope;
279 + bool EnableIPv4{true};
280 + bool EnableIPv6{};
281 bool Internal{};
282 + bool Attachable{};
283 + bool Ingress{};
284 + bool ConfigOnly{};
285 + NetworkConfigFrom ConfigFrom;
286 IPAM IPAM;
244 - std::optional<std::map<std::string, std::string>> Options;
287 + std::map<std::string, std::string> Options;
288 + std::map<std::string, std::string> Labels;
289 + std::map<std::string, NetworkContainer> Containers;
290 + nlohmann::json Status = nlohmann::json::object();
291 +
292 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(
293 + Network, Id, Name, Created, Driver, Scope, EnableIPv4, EnableIPv6, Internal, Attachable, Ingress, ConfigOnly, ConfigFrom, IPAM, Options, Labels, Containers, Status);
294 +};
295 +
296 +// The network properties carried from the session to the CLI for "network list". Values keep their
297 +// native types; the CLI renders the string output.
298 +struct NetworkListEntry
299 +{
300 + std::string Id;
301 + std::string Name;
302 + std::string Driver;
303 + std::string Scope;
304 + std::string Created;
305 + bool EnableIPv4{true};
306 + bool EnableIPv6{};
307 + bool Internal{};
308 std::map<std::string, std::string> Labels;
309
247 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Network, Id, Name, Driver, Scope, Internal, IPAM, Options, Labels);
310 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(NetworkListEntry, Id, Name, Driver, Scope, Created, EnableIPv4, EnableIPv6, Internal, Labels);
311 };
312
313 } // namespace wsl::windows::common::wslc_schema
src/windows/service/inc/wslc.idl
+2 -8
@@ -648,13 +648,6 @@ typedef struct _WSLCNetworkOptions
648
649 typedef char WSLCNetworkName[WSLC_MAX_NETWORK_NAME_LENGTH + 1];
650
651 -typedef struct _WSLCNetworkInformation
652 -{
653 - char Name[WSLC_MAX_NETWORK_NAME_LENGTH + 1];
654 - char Id[WSLC_CONTAINER_ID_LENGTH + 1];
655 - char Driver[64];
656 -} WSLCNetworkInformation;
657 -
651 typedef struct _WSLCPruneContainersResults
652 {
653 [unique, size_is(ContainersCount)] WSLCContainerId* Containers;
@@ -775,7 +768,8 @@ interface IWSLCSession : IUnknown
768 // Network management.
769 HRESULT CreateNetwork([in] const WSLCNetworkOptions* Options, [in, unique] IWarningCallback* WarningCallback);
770 HRESULT DeleteNetwork([in] LPCSTR Name);
778 - HRESULT ListNetworks([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out, size_is(, *Count)] WSLCNetworkInformation** Networks, [out] ULONG* Count);
771 + // Returns a JSON array of wslc_schema::NetworkListEntry objects describing the session's networks.
772 + HRESULT ListNetworks([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out] LPSTR* Output);
773 HRESULT InspectNetwork([in] LPCSTR Name, [out] LPSTR* Output);
774 HRESULT PruneNetworks([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out, size_is(, *NetworksCount)] WSLCNetworkName** Networks, [out] ULONG* NetworksCount);
775
src/windows/wslc/core/ExecutionContextData.h
+2 -1
@@ -18,6 +18,7 @@ Abstract:
18 #include "NetworkModel.h"
19 #include "SessionModel.h"
20 #include "wslc.h"
21 +#include <wslc_schema.h>
22
23 #include <string>
24
@@ -56,7 +57,7 @@ namespace details {
57 DEFINE_DATA_MAPPING(ContainerOptions, wsl::windows::wslc::models::ContainerOptions);
58 DEFINE_DATA_MAPPING(Images, std::vector<wsl::windows::wslc::models::ImageInformation>);
59 DEFINE_DATA_MAPPING(Volumes, std::vector<WSLCVolumeInformation>);
59 - DEFINE_DATA_MAPPING(Networks, std::vector<WSLCNetworkInformation>);
60 + DEFINE_DATA_MAPPING(Networks, std::vector<wsl::windows::common::wslc_schema::NetworkListEntry>);
61 DEFINE_DATA_MAPPING(NetworkEndpointOptions, wsl::windows::wslc::models::NetworkEndpointOptions);
62 } // namespace details
63
src/windows/wslc/services/NetworkModel.h
+16
@@ -56,4 +56,20 @@ struct PruneNetworksResult
56 std::vector<std::string> PrunedNetworks;
57 };
58
59 +// The shape emitted by "network list --format json"; every value is reported as a string.
60 +struct NetworkOutputInformation
61 +{
62 + std::string CreatedAt;
63 + std::string Driver;
64 + std::string ID;
65 + std::string IPv4;
66 + std::string IPv6;
67 + std::string Internal;
68 + std::string Labels;
69 + std::string Name;
70 + std::string Scope;
71 +
72 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(NetworkOutputInformation, CreatedAt, Driver, ID, IPv4, IPv6, Internal, Labels, Name, Scope);
73 +};
74 +
75 } // namespace wsl::windows::wslc::models
src/windows/wslc/services/NetworkService.cpp
+5 -12
@@ -75,7 +75,8 @@ 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, const std::vector<std::pair<std::string, std::string>>& filters)
78 +std::vector<wsl::windows::common::wslc_schema::NetworkListEntry> NetworkService::List(
79 + models::Session& session, const std::vector<std::pair<std::string, std::string>>& filters)
80 {
81 std::vector<WSLCFilter> filterEntries;
82 filterEntries.reserve(filters.size());
@@ -84,19 +85,11 @@ std::vector<WSLCNetworkInformation> NetworkService::List(models::Session& sessio
85 filterEntries.push_back({.Key = key.c_str(), .Value = value.c_str()});
86 }
87
87 - wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> rawNetworks;
88 - ULONG count = 0;
88 + wil::unique_cotaskmem_ansistring output;
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);
94 - for (auto ptr = rawNetworks.get(), end = rawNetworks.get() + count; ptr != end; ++ptr)
95 - {
96 - networks.push_back(*ptr);
97 - }
90 + filterEntries.empty() ? nullptr : filterEntries.data(), static_cast<ULONG>(filterEntries.size()), &output));
91
99 - return networks;
92 + return FromJson<std::vector<wsl::windows::common::wslc_schema::NetworkListEntry>>(output.get());
93 }
94
95 wsl::windows::common::wslc_schema::Network NetworkService::Inspect(models::Session& session, const std::string& name)
src/windows/wslc/services/NetworkService.h
+2 -1
@@ -24,7 +24,8 @@ 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, const std::vector<std::pair<std::string, std::string>>& filters = {});
27 + static std::vector<wsl::windows::common::wslc_schema::NetworkListEntry> List(
28 + models::Session& session, const std::vector<std::pair<std::string, std::string>>& filters = {});
29 static wsl::windows::common::wslc_schema::Network Inspect(models::Session& session, const std::string& name);
30 static models::PruneNetworksResult Prune(models::Session& session, const std::vector<std::pair<std::string, std::string>>& filters = {});
31 static void Connect(models::Session& session, const models::ConnectNetworkOptions& connectOptions);
src/windows/wslc/tasks/ImageTasks.cpp
+1 -1
@@ -82,7 +82,7 @@ namespace {
82 ImageOutputInformation entry;
83 entry.Containers = image.Containers < 0 ? std::string{c_notAvailable} : std::to_string(image.Containers);
84
85 - entry.CreatedAt = FormatDockerTimestamp(image.Created);
85 + entry.CreatedAt = EpochToLocalDisplayTime(image.Created);
86 entry.CreatedSince =
87 WideToMultiByte(ContainerService::FormatRelativeTime(image.Created > 0 ? static_cast<ULONGLONG>(image.Created) : 0));
88 entry.Digest = c_none;
src/windows/wslc/tasks/NetworkTasks.cpp
+57 -8
@@ -30,6 +30,36 @@ using namespace wsl::windows::wslc::services;
30
31 namespace wsl::windows::wslc::task {
32
33 +namespace {
34 +
35 + // Shared by the table and json output so the two cannot drift. The id is truncated unless --no-trunc is passed.
36 + NetworkOutputInformation ToNetworkOutput(const wslc_schema::NetworkListEntry& network, bool truncate)
37 + {
38 + NetworkOutputInformation entry;
39 + entry.CreatedAt = Rfc3339ToUtcDisplayTime(network.Created);
40 + entry.Driver = network.Driver;
41 + entry.ID = TruncateId(network.Id, truncate);
42 + entry.IPv4 = network.EnableIPv4 ? "true" : "false";
43 + entry.IPv6 = network.EnableIPv6 ? "true" : "false";
44 + entry.Internal = network.Internal ? "true" : "false";
45 + entry.Name = network.Name;
46 + entry.Scope = network.Scope;
47 +
48 + for (const auto& [key, value] : network.Labels)
49 + {
50 + if (!entry.Labels.empty())
51 + {
52 + entry.Labels += ",";
53 + }
54 +
55 + entry.Labels += value.empty() ? key : std::format("{}={}", key, value);
56 + }
57 +
58 + return entry;
59 + }
60 +
61 +} // namespace
62 +
63 static bool TryInspectNetwork(Terminal& terminal, Session& session, const std::string& networkName, std::optional<wslc_schema::Network>& inspectData)
64 {
65 try
@@ -172,6 +202,9 @@ void ListNetworks(CLIExecutionContext& context)
202 WI_ASSERT(context.Data.Contains(Data::Networks));
203 auto& networks = context.Data.Get<Data::Networks>();
204
205 + // Networks are reported in name order regardless of how the daemon returns them.
206 + std::ranges::sort(networks, {}, &wslc_schema::NetworkListEntry::Name);
207 +
208 const auto format = context.Args.GetValue<ArgType::Format>(FormatType::Table);
209 const bool quiet = context.Args.GetValue<ArgType::Quiet>();
210 const bool trunc = !context.Args.GetValue<ArgType::NoTrunc>();
@@ -191,22 +224,30 @@ void ListNetworks(CLIExecutionContext& context)
224 {
225 for (const auto& network : networks)
226 {
194 - auto json = nlohmann::json(network);
195 - json["ID"] = TruncateId(network.Id, trunc);
196 - context.Terminal.Output(L"{}\n", ToJsonW(json, c_jsonCompactIndent));
227 + context.Terminal.Output(L"{}\n", ToJsonW(ToNetworkOutput(network, trunc), c_jsonCompactIndent));
228 }
229
230 break;
231 }
232 case FormatType::Table:
233 {
203 - auto table = wsl::windows::wslc::TableOutput<3>(context.Terminal, {L"NETWORK ID", L"NAME", L"DRIVER"});
234 + // Every column has a minimum total width of ten characters, including the padding that follows it.
235 + constexpr size_t c_minimumColumnWidth = 7;
236 + auto table = wsl::windows::wslc::TableOutput<4>(
237 + context.Terminal,
238 + {L"NETWORK ID", L"NAME", L"DRIVER", L"SCOPE"},
239 + {ColumnWidthConfig{.MinWidth = c_minimumColumnWidth},
240 + ColumnWidthConfig{.MinWidth = c_minimumColumnWidth},
241 + ColumnWidthConfig{.MinWidth = c_minimumColumnWidth},
242 + ColumnWidthConfig{.MinWidth = c_minimumColumnWidth}});
243 for (const auto& network : networks)
244 {
245 + const auto entry = ToNetworkOutput(network, trunc);
246 table.WriteRow({
207 - MultiByteToWide(TruncateId(network.Id, trunc)),
208 - MultiByteToWide(network.Name),
209 - MultiByteToWide(network.Driver),
247 + MultiByteToWide(entry.ID),
248 + MultiByteToWide(entry.Name),
249 + MultiByteToWide(entry.Driver),
250 + MultiByteToWide(entry.Scope),
251 });
252 }
253
@@ -228,10 +269,18 @@ void PruneNetworks(CLIExecutionContext& context)
269
270 auto result = NetworkService::Prune(session, filters);
271
272 + if (result.PrunedNetworks.empty())
273 + {
274 + return;
275 + }
276 +
277 + context.Terminal.Output(L"{}\n", Localization::WSLCCLI_NetworkPruneDeletedHeader());
278 for (const auto& networkName : result.PrunedNetworks)
279 {
233 - context.Terminal.Output(L"{}\n", Localization::WSLCCLI_NetworkPruneDeleted(MultiByteToWide(networkName)));
280 + context.Terminal.Output(L"{}\n", MultiByteToWide(networkName));
281 }
282 +
283 + context.Terminal.Output(L"\n");
284 }
285
286 void ConnectNetwork(CLIExecutionContext& context)
src/windows/wslcsession/WSLCContainer.cpp
+2 -13
@@ -488,17 +488,6 @@ WSLCContainerState DockerStateToWSLCState(ContainerState state)
488 }
489 }
490
491 -std::uint64_t ParseDockerTimestamp(const std::string& timestamp)
492 -{
493 - // Docker timestamps are UTC ISO 8601, e.g. "2026-03-05T10:30:00.123456789Z".
494 - std::chrono::sys_seconds utcSeconds;
495 - std::istringstream stream(timestamp);
496 - stream >> std::chrono::parse("%FT%H:%M:%S%Z", utcSeconds);
497 - THROW_HR_IF_MSG(E_INVALIDARG, stream.fail(), "Failed to parse timestamp '%hs'", timestamp.c_str());
498 -
499 - return static_cast<std::uint64_t>(utcSeconds.time_since_epoch().count());
500 -}
501 -
491 std::string CleanContainerName(const std::string& name)
492 {
493 // Docker container names have a leading '/', strip it.
@@ -2432,7 +2421,7 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2421 std::move(mergedLabels),
2422 std::move(OnDeleted),
2423 WslcContainerStateCreated,
2435 - ParseDockerTimestamp(inspectData.Created),
2424 + wsl::windows::common::string::Rfc3339ToEpoch(inspectData.Created),
2425 containerOptions.InitProcessOptions.Flags,
2426 containerOptions.Flags);
2427
@@ -2544,7 +2533,7 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
2533
2534 if (!timestamp.empty())
2535 {
2547 - container->m_stateChangedAt = ParseDockerTimestamp(timestamp);
2536 + container->m_stateChangedAt = wsl::windows::common::string::Rfc3339ToEpoch(timestamp);
2537 }
2538 }
2539 }
src/windows/wslcsession/WSLCSession.cpp
+70 -65
@@ -2983,10 +2983,7 @@ try
2983 entry.Scope = full.Scope;
2984 entry.Internal = full.Internal;
2985 entry.Labels = full.Labels;
2986 - if (full.Options)
2987 - {
2988 - entry.Options = *full.Options;
2989 - }
2986 + entry.Options = full.Options;
2987 entry.IPAM.Driver = full.IPAM.Driver;
2988 if (full.IPAM.Config)
2989 {
@@ -3046,63 +3043,49 @@ try
3043 }
3044 CATCH_RETURN();
3045
3049 -HRESULT WSLCSession::ListNetworks(const WSLCFilter* Filters, ULONG FiltersCount, WSLCNetworkInformation** Networks, ULONG* Count)
3046 +HRESULT WSLCSession::ListNetworks(const WSLCFilter* Filters, ULONG FiltersCount, LPSTR* Output)
3047 try
3048 {
3049 WSLCExecutionContext context(this);
3050
3054 - RETURN_HR_IF_NULL(E_POINTER, Networks);
3055 - RETURN_HR_IF_NULL(E_POINTER, Count);
3051 + RETURN_HR_IF_NULL(E_POINTER, Output);
3052
3057 - *Networks = nullptr;
3058 - *Count = 0;
3053 + *Output = nullptr;
3054
3055 auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
3061 - const bool filtered = !filters.empty();
3062 -
3063 - if (filtered)
3064 - {
3065 - // Scope the filtered query to WSLC-managed networks.
3066 - filters["label"].push_back(WSLCNetworkManagedLabel);
3067 - }
3056
3057 auto lock = AcquireLease();
3058 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
3059 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
3060
3061 std::vector<docker_schema::Network> dockerNetworks;
3072 - if (filtered)
3062 + try
3063 {
3074 - try
3075 - {
3076 - dockerNetworks = m_runtime.Docker().ListNetworks(filters);
3077 - }
3078 - CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list networks");
3064 + dockerNetworks = m_runtime.Docker().ListNetworks(filters);
3065 }
3066 + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list networks");
3067
3081 - std::lock_guard networksLock(m_networksLock);
3082 -
3083 - auto output = wil::make_unique_cotaskmem<WSLCNetworkInformation[]>(m_networks.size());
3084 -
3085 - ULONG index = 0;
3086 - for (const auto& [name, entry] : m_networks)
3068 + std::vector<wslc_schema::NetworkListEntry> networks;
3069 + networks.reserve(dockerNetworks.size());
3070 + for (const auto& network : dockerNetworks)
3071 {
3088 - if (filtered && std::ranges::find_if(dockerNetworks, [&](const auto& n) { return n.Name == name; }) == dockerNetworks.end())
3089 - {
3090 - continue;
3091 - }
3072 + wslc_schema::NetworkListEntry entry;
3073 + entry.Id = network.Id;
3074 + entry.Name = network.Name;
3075 + entry.Driver = network.Driver;
3076 + entry.Scope = network.Scope;
3077 + entry.Created = network.Created;
3078 + entry.EnableIPv4 = network.EnableIPv4;
3079 + entry.EnableIPv6 = network.EnableIPv6;
3080 + entry.Internal = network.Internal;
3081 + entry.Labels = network.Labels;
3082 + entry.Labels.erase(WSLCNetworkManagedLabel);
3083
3093 - THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Name, name.c_str()) != 0);
3094 - THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Id, entry.Id.c_str()) != 0);
3095 - THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Driver, entry.Driver.c_str()) != 0);
3096 - index++;
3084 + networks.push_back(std::move(entry));
3085 }
3086
3099 - if (index == 0)
3100 - {
3101 - return S_OK;
3102 - }
3103 -
3104 - *Networks = output.release();
3105 - *Count = index;
3087 + std::string json = wsl::shared::ToJson(networks);
3088 + *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(json.c_str()).release();
3089
3090 return S_OK;
3091 }
@@ -3122,30 +3105,44 @@ try
3105 ValidateName(name.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH);
3106
3107 auto lock = AcquireLease();
3125 - std::lock_guard networksLock(m_networksLock);
3126 -
3127 - auto it = m_networks.find(name);
3128 - THROW_HR_WITH_USER_ERROR_IF(WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(name), it == m_networks.end());
3129 -
3130 - const auto& entry = it->second;
3108 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
3109 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
3110
3132 - wslc_schema::Network result;
3133 - result.Id = entry.Id;
3134 - result.Name = name;
3135 - result.Driver = entry.Driver;
3136 - result.Scope = entry.Scope;
3137 - result.Internal = entry.Internal;
3138 - result.Labels = entry.Labels;
3139 - if (!entry.Options.empty())
3111 + docker_schema::Network network;
3112 + try
3113 + {
3114 + network = m_runtime.Docker().InspectNetwork(name);
3115 + }
3116 + catch (const DockerHTTPException& e)
3117 {
3141 - result.Options = entry.Options;
3118 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(name), e.StatusCode() == 404);
3119 + THROW_DOCKER_USER_ERROR_MSG(e, "Failed to inspect network '%hs'", name.c_str());
3120 }
3121
3144 - result.IPAM.Driver = entry.IPAM.Driver;
3145 - if (entry.IPAM.Config)
3122 + wslc_schema::Network result;
3123 + result.Id = network.Id;
3124 + result.Name = network.Name;
3125 + result.Created = network.Created;
3126 + result.Driver = network.Driver;
3127 + result.Scope = network.Scope;
3128 + result.EnableIPv4 = network.EnableIPv4;
3129 + result.EnableIPv6 = network.EnableIPv6;
3130 + result.Internal = network.Internal;
3131 + result.Attachable = network.Attachable;
3132 + result.Ingress = network.Ingress;
3133 + result.ConfigOnly = network.ConfigOnly;
3134 + result.ConfigFrom.Network = network.ConfigFrom.Network;
3135 + result.Options = network.Options;
3136 + result.Labels = network.Labels;
3137 + result.Labels.erase(WSLCNetworkManagedLabel);
3138 + result.Status = network.Status;
3139 +
3140 + result.IPAM.Driver = network.IPAM.Driver;
3141 + result.IPAM.Options = network.IPAM.Options;
3142 + if (network.IPAM.Config)
3143 {
3144 auto& configs = result.IPAM.Config.emplace();
3148 - for (const auto& cfg : *entry.IPAM.Config)
3145 + for (const auto& cfg : *network.IPAM.Config)
3146 {
3147 wslc_schema::IPAMConfig inspectCfg;
3148 inspectCfg.Subnet = cfg.Subnet;
@@ -3155,6 +3152,17 @@ try
3152 }
3153 }
3154
3155 + for (const auto& [id, container] : network.Containers)
3156 + {
3157 + wslc_schema::NetworkContainer inspectContainer;
3158 + inspectContainer.Name = container.Name;
3159 + inspectContainer.EndpointID = container.EndpointID;
3160 + inspectContainer.MacAddress = container.MacAddress;
3161 + inspectContainer.IPv4Address = container.IPv4Address;
3162 + inspectContainer.IPv6Address = container.IPv6Address;
3163 + result.Containers.emplace(id, std::move(inspectContainer));
3164 + }
3165 +
3166 std::string json = wsl::shared::ToJson(result);
3167 *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(json.c_str()).release();
3168
@@ -3931,10 +3939,7 @@ void WSLCSession::RecoverExistingNetworks()
3939 entry.Scope = network.Scope;
3940 entry.Internal = network.Internal;
3941 entry.Labels = network.Labels;
3934 - if (network.Options)
3935 - {
3936 - entry.Options = *network.Options;
3937 - }
3942 + entry.Options = network.Options;
3943 entry.IPAM.Driver = network.IPAM.Driver;
3944 if (network.IPAM.Config)
3945 {
src/windows/wslcsession/WSLCSession.h
+1 -2
@@ -195,8 +195,7 @@ public:
195 IFACEMETHOD(CreateNetwork)(_In_ const WSLCNetworkOptions* Options, _In_opt_ IWarningCallback* WarningCallback) override;
196 IFACEMETHOD(DeleteNetwork)(_In_ LPCSTR Name) override;
197 IFACEMETHOD(ListNetworks)
198 - (_In_reads_opt_(FiltersCount) const WSLCFilter* Filters, _In_ ULONG FiltersCount, _Out_ WSLCNetworkInformation** Networks, _Out_ ULONG* Count)
199 - override;
198 + (_In_reads_opt_(FiltersCount) const WSLCFilter* Filters, _In_ ULONG FiltersCount, _Out_ LPSTR* Output) override;
199 IFACEMETHOD(InspectNetwork)(_In_ LPCSTR Name, _Out_ LPSTR* Output) override;
200 IFACEMETHOD(PruneNetworks)
201 (_In_reads_opt_(FiltersCount) const WSLCFilter* Filters, _In_ ULONG FiltersCount, _Out_ WSLCNetworkName** Networks, _Out_ ULONG* NetworksCount)
test/windows/WSLCTests.cpp
+86 -60
@@ -5422,10 +5422,8 @@ class WSLCTests
5422
5423 LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
5424
5425 - // List should start empty.
5426 - wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
5427 - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address<ULONG>()));
5428 - VERIFY_ARE_EQUAL(0u, networks.size());
5425 + // The network must not exist yet. The predefined networks are always listed.
5426 + VERIFY_IS_FALSE(NetworkIsListed(networkName));
5427
5428 WSLCNetworkOptions options{};
5429 options.Name = networkName.c_str();
@@ -5437,11 +5435,16 @@ class WSLCTests
5435 auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
5436
5437 // Verify it appears in the list with correct fields.
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));
5444 - VERIFY_IS_TRUE(strlen(networks[0].Id) > 0);
5438 + auto networks = ListNetworks();
5439 + const auto created = std::ranges::find_if(networks, [&](const auto& network) { return network.Name == networkName; });
5440 + VERIFY_ARE_NOT_EQUAL(networks.end(), created);
5441 + VERIFY_ARE_EQUAL(std::string("bridge"), created->Driver);
5442 + VERIFY_ARE_EQUAL(std::string("local"), created->Scope);
5443 + VERIFY_IS_FALSE(created->Id.empty());
5444 + VERIFY_IS_FALSE(created->Created.empty());
5445 +
5446 + // The label used to track wslc managed networks is an implementation detail and must not surface.
5447 + VERIFY_IS_FALSE(created->Labels.contains("com.microsoft.wsl.network.managed"));
5448
5449 // Duplicate name should fail.
5450 VERIFY_ARE_EQUAL(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), m_defaultSession->CreateNetwork(&options, nullptr));
@@ -5449,14 +5452,26 @@ class WSLCTests
5452 cleanup.release();
5453 VERIFY_SUCCEEDED(m_defaultSession->DeleteNetwork(networkName.c_str()));
5454
5452 - // List should be empty again.
5453 - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address<ULONG>()));
5454 - VERIFY_ARE_EQUAL(0u, networks.size());
5455 + VERIFY_IS_FALSE(NetworkIsListed(networkName));
5456
5457 // Delete non-existent should fail.
5458 VERIFY_ARE_EQUAL(WSLC_E_NETWORK_NOT_FOUND, m_defaultSession->DeleteNetwork(networkName.c_str()));
5459 }
5460
5461 + std::vector<wsl::windows::common::wslc_schema::NetworkListEntry> ListNetworks(const std::vector<WSLCFilter>& Filters = {})
5462 + {
5463 + wil::unique_cotaskmem_ansistring output;
5464 + VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(Filters.empty() ? nullptr : Filters.data(), static_cast<ULONG>(Filters.size()), &output));
5465 +
5466 + return wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::NetworkListEntry>>(output.get());
5467 + }
5468 +
5469 + bool NetworkIsListed(const std::string& Name)
5470 + {
5471 + const auto networks = ListNetworks();
5472 + return std::ranges::any_of(networks, [&](const auto& network) { return network.Name == Name; });
5473 + }
5474 +
5475 void CreateNamedNetwork(const std::string& Name, const std::vector<WSLCLabel>& Labels = {})
5476 {
5477 WSLCNetworkOptions options{};
@@ -5493,26 +5508,19 @@ class WSLCTests
5508 const WSLCFilter* filtersPtr = filters.empty() ? nullptr : filters.data();
5509 const ULONG filtersCount = static_cast<ULONG>(filters.size());
5510
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>()));
5511 + wil::unique_cotaskmem_ansistring output;
5512 + VERIFY_ARE_EQUAL(expected, m_defaultSession->ListNetworks(filtersPtr, filtersCount, &output));
5513 };
5514
5515 auto expectList = [&](const std::vector<std::string>& expected,
5516 const std::vector<WSLCFilter>& filters,
5517 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 -
5518 std::vector<std::string> names;
5511 - for (const auto& n : networks)
5519 + for (const auto& n : ListNetworks(filters))
5520 {
5521 names.emplace_back(n.Name);
5514 - VERIFY_IS_TRUE(strlen(n.Id) > 0);
5515 - VERIFY_ARE_EQUAL(std::string("bridge"), std::string(n.Driver));
5522 + VERIFY_IS_FALSE(n.Id.empty());
5523 + VERIFY_ARE_EQUAL(std::string("bridge"), n.Driver);
5524 }
5525
5526 VerifyAreEqualUnordered(expected, names, source);
@@ -5536,9 +5544,24 @@ class WSLCTests
5544 expectList(all, {{"label", testLabelKV.c_str()}, {"driver", "bridge"}});
5545 expectList({}, {{"label", testLabelKV.c_str()}, {"driver", "nonexistent"}});
5546
5539 - // Explicit managed-label filter is idempotent with the auto-injected one.
5547 + // Networks created by wslc carry the managed label, which can still be filtered on explicitly.
5548 expectList(all, {{"label", testLabelKV.c_str()}, {"label", managedLabel.c_str()}});
5549
5550 + // Predefined networks are not managed by wslc but are still listed.
5551 + {
5552 + const auto networks = ListNetworks();
5553 + std::vector<std::string> names;
5554 + for (const auto& n : networks)
5555 + {
5556 + names.emplace_back(n.Name);
5557 + }
5558 +
5559 + for (const auto& predefined : {"bridge", "host", "none"})
5560 + {
5561 + VERIFY_ARE_NOT_EQUAL(names.end(), std::ranges::find(names, predefined));
5562 + }
5563 + }
5564 +
5565 // Null filter key/value is rejected.
5566 expectListFails(E_POINTER, {{nullptr, "anything"}});
5567 expectListFails(E_POINTER, {{"label", nullptr}});
@@ -5582,13 +5605,8 @@ class WSLCTests
5605
5606 expectPrune({a, b});
5607
5585 - wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
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));
5590 - VERIFY_ARE_NOT_EQUAL(b, std::string(n.Name));
5591 - }
5608 + VERIFY_IS_FALSE(NetworkIsListed(a));
5609 + VERIFY_IS_FALSE(NetworkIsListed(b));
5610
5611 cleanup.release();
5612 }
@@ -5723,10 +5741,8 @@ class WSLCTests
5741
5742 VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options, nullptr));
5743
5726 - wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
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));
5744 + const auto networks = ListNetworks();
5745 + VERIFY_IS_TRUE(std::ranges::any_of(networks, [&](const auto& network) { return network.Name == networkName; }));
5746 }
5747
5748 WSLC_TEST_METHOD(NetworkCreateInvalidDriverAndOptionTest)
@@ -5786,11 +5802,10 @@ class WSLCTests
5802
5803 VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options, nullptr));
5804
5789 - wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
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));
5805 + const auto networks = ListNetworks();
5806 + const auto created = std::ranges::find_if(networks, [&](const auto& network) { return network.Name == networkName; });
5807 + VERIFY_ARE_NOT_EQUAL(networks.end(), created);
5808 + VERIFY_ARE_EQUAL(std::string("bridge"), created->Driver);
5809 }
5810
5811 WSLC_TEST_METHOD(NetworkCreateReservedNameTest)
@@ -5997,11 +6012,10 @@ class WSLCTests
6012 VERIFY_IS_NOT_NULL(output.get());
6013
6014 auto inspect = wsl::shared::FromJson<wsl::windows::common::wslc_schema::Network>(output.get());
6000 - VERIFY_IS_TRUE(inspect.Options.has_value());
6001 - VERIFY_IS_TRUE(inspect.Options->contains("my.abc.key"));
6002 - VERIFY_IS_TRUE(inspect.Options->contains("com.example.flag"));
6003 - VERIFY_ARE_EQUAL(std::string("mygod"), inspect.Options->at("my.abc.key"));
6004 - VERIFY_ARE_EQUAL(std::string("1"), inspect.Options->at("com.example.flag"));
6015 + VERIFY_IS_TRUE(inspect.Options.contains("my.abc.key"));
6016 + VERIFY_IS_TRUE(inspect.Options.contains("com.example.flag"));
6017 + VERIFY_ARE_EQUAL(std::string("mygod"), inspect.Options.at("my.abc.key"));
6018 + VERIFY_ARE_EQUAL(std::string("1"), inspect.Options.at("com.example.flag"));
6019 }
6020
6021 WSLC_TEST_METHOD(NetworkSessionRecoveryTest)
@@ -6024,12 +6038,11 @@ class WSLCTests
6038 // Reset the session (simulates session restart).
6039 ResetTestSession();
6040
6027 - wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
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));
6032 - VERIFY_IS_TRUE(strlen(networks[0].Id) > 0);
6041 + const auto networks = ListNetworks();
6042 + const auto recovered = std::ranges::find_if(networks, [&](const auto& network) { return network.Name == networkName; });
6043 + VERIFY_ARE_NOT_EQUAL(networks.end(), recovered);
6044 + VERIFY_ARE_EQUAL(std::string("bridge"), recovered->Driver);
6045 + VERIFY_IS_FALSE(recovered->Id.empty());
6046
6047 // Verify arbitrary driver options survive session recovery.
6048 wil::unique_cotaskmem_ansistring output;
@@ -6037,9 +6050,8 @@ class WSLCTests
6050 VERIFY_IS_NOT_NULL(output.get());
6051
6052 auto inspect = wsl::shared::FromJson<wsl::windows::common::wslc_schema::Network>(output.get());
6040 - VERIFY_IS_TRUE(inspect.Options.has_value());
6041 - VERIFY_IS_TRUE(inspect.Options->contains("recovery.test.key"));
6042 - VERIFY_ARE_EQUAL(std::string("preserved"), inspect.Options->at("recovery.test.key"));
6053 + VERIFY_IS_TRUE(inspect.Options.contains("recovery.test.key"));
6054 + VERIFY_ARE_EQUAL(std::string("preserved"), inspect.Options.at("recovery.test.key"));
6055 }
6056
6057 WSLC_TEST_METHOD(NetworkMultipleCreateListDeleteTest)
@@ -6077,13 +6089,27 @@ class WSLCTests
6089 optionsC.Internal = TRUE;
6090 VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&optionsC, nullptr));
6091
6080 - wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
6081 - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address<ULONG>()));
6082 - VERIFY_ARE_EQUAL(3u, networks.size());
6092 + auto listedNames = [&]() {
6093 + std::vector<std::string> names;
6094 + for (const auto& network : ListNetworks())
6095 + {
6096 + names.push_back(network.Name);
6097 + }
6098 +
6099 + return names;
6100 + };
6101 +
6102 + auto names = listedNames();
6103 + VERIFY_ARE_NOT_EQUAL(names.end(), std::ranges::find(names, networkNameA));
6104 + VERIFY_ARE_NOT_EQUAL(names.end(), std::ranges::find(names, networkNameB));
6105 + VERIFY_ARE_NOT_EQUAL(names.end(), std::ranges::find(names, networkNameC));
6106
6107 VERIFY_SUCCEEDED(m_defaultSession->DeleteNetwork(networkNameB.c_str()));
6085 - VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(nullptr, 0, networks.addressof(), networks.size_address<ULONG>()));
6086 - VERIFY_ARE_EQUAL(2u, networks.size());
6108 +
6109 + names = listedNames();
6110 + VERIFY_ARE_NOT_EQUAL(names.end(), std::ranges::find(names, networkNameA));
6111 + VERIFY_ARE_EQUAL(names.end(), std::ranges::find(names, networkNameB));
6112 + VERIFY_ARE_NOT_EQUAL(names.end(), std::ranges::find(names, networkNameC));
6113 }
6114
6115 WSLC_TEST_METHOD(NetworkInspectTest)
test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
+1 -1
@@ -150,7 +150,7 @@ class WSLCCLIExecutionUnitTests
150 }
151 else if (dataType == Data::Networks)
152 {
153 - std::vector<WSLCNetworkInformation> networks;
153 + std::vector<wsl::windows::common::wslc_schema::NetworkListEntry> networks;
154 dataMap.Add<Data::Networks>(std::move(networks));
155 handled = true;
156 }
test/windows/wslc/e2e/WSLCE2EHelpers.cpp
+3 -3
@@ -257,7 +257,7 @@ void VerifyNetworkIsListed(const std::wstring& networkName)
257 {
258 auto result = RunWslc(L"network list --format json");
259 result.Verify({.Stderr = L"", .ExitCode = 0});
260 - auto networks = ParseNdjsonOutputAs<WSLCNetworkInformation>(result);
260 + auto networks = ParseNdjsonOutputAs<NetworkListOutput>(result);
261 for (const auto& net : networks)
262 {
263 if (net.Name == wsl::shared::string::WideToMultiByte(networkName))
@@ -273,7 +273,7 @@ void VerifyNetworkIsNotListed(const std::wstring& networkName)
273 {
274 auto result = RunWslc(L"network list --format json");
275 result.Verify({.Stderr = L"", .ExitCode = 0});
276 - auto networks = ParseNdjsonOutputAs<WSLCNetworkInformation>(result);
276 + auto networks = ParseNdjsonOutputAs<NetworkListOutput>(result);
277 for (const auto& net : networks)
278 {
279 if (net.Name == wsl::shared::string::WideToMultiByte(networkName))
@@ -526,7 +526,7 @@ void EnsureNetworkDoesNotExist(const std::wstring& networkName)
526 {
527 auto result = RunWslc(L"network list --format json");
528 result.Verify({.Stderr = L"", .ExitCode = 0});
529 - auto networks = ParseNdjsonOutputAs<WSLCNetworkInformation>(result);
529 + auto networks = ParseNdjsonOutputAs<NetworkListOutput>(result);
530 for (const auto& net : networks)
531 {
532 if (net.Name == wsl::shared::string::WideToMultiByte(networkName))
test/windows/wslc/e2e/WSLCE2EHelpers.h
+16
@@ -62,6 +62,22 @@ namespace VT {
62 }
63 } // namespace VT
64
65 +// The shape emitted by "network list --format json"; every value is reported as a string.
66 +struct NetworkListOutput
67 +{
68 + std::string CreatedAt;
69 + std::string Driver;
70 + std::string ID;
71 + std::string IPv4;
72 + std::string IPv6;
73 + std::string Internal;
74 + std::string Labels;
75 + std::string Name;
76 + std::string Scope;
77 +
78 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(NetworkListOutput, CreatedAt, Driver, ID, IPv4, IPv6, Internal, Labels, Name, Scope);
79 +};
80 +
81 struct TestImage
82 {
83 std::wstring Name;
test/windows/wslc/e2e/WSLCE2ENetworkCreateTests.cpp
+2 -3
@@ -206,9 +206,8 @@ class WSLCE2ENetworkCreateTests
206 VerifyNetworkIsListed(TestNetworkName);
207 auto inspect = InspectNetwork(TestNetworkName);
208 VERIFY_ARE_EQUAL("bridge", inspect.Driver);
209 - VERIFY_IS_TRUE(inspect.Options.has_value());
210 - VERIFY_ARE_EQUAL("true", (*inspect.Options)["com.docker.network.bridge.enable_icc"]);
211 - VERIFY_ARE_EQUAL("1450", (*inspect.Options)["com.docker.network.driver.mtu"]);
209 + VERIFY_ARE_EQUAL("true", inspect.Options["com.docker.network.bridge.enable_icc"]);
210 + VERIFY_ARE_EQUAL("1450", inspect.Options["com.docker.network.driver.mtu"]);
211 }
212
213 private:
test/windows/wslc/e2e/WSLCE2ENetworkInspectTests.cpp
+20
@@ -66,6 +66,26 @@ class WSLCE2ENetworkInspectTests
66
67 VERIFY_ARE_EQUAL(WideToMultiByte(TestNetworkName1), inspect.Name);
68 VERIFY_ARE_EQUAL("bridge", inspect.Driver);
69 + VERIFY_ARE_EQUAL("local", inspect.Scope);
70 + VERIFY_IS_FALSE(inspect.Created.empty());
71 + VERIFY_IS_TRUE(inspect.EnableIPv4);
72 + VERIFY_IS_FALSE(inspect.ConfigOnly);
73 + VERIFY_IS_TRUE(inspect.Containers.empty());
74 +
75 + // The label used to track wslc managed networks is an implementation detail and must not surface.
76 + VERIFY_IS_FALSE(inspect.Labels.contains("com.microsoft.wsl.network.managed"));
77 + }
78 +
79 + WSLC_TEST_METHOD(WSLCE2E_Network_Inspect_PredefinedNetwork)
80 + {
81 + // The predefined networks are not created by wslc but must still be inspectable.
82 + auto result = RunWslc(L"network inspect bridge");
83 + result.Verify({.Stderr = L"", .ExitCode = 0});
84 +
85 + auto inspectData = wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::Network>>(result.Stdout.value().c_str());
86 + VERIFY_ARE_EQUAL(1u, inspectData.size());
87 + VERIFY_ARE_EQUAL("bridge", inspectData[0].Name);
88 + VERIFY_ARE_EQUAL("bridge", inspectData[0].Driver);
89 }
90
91 WSLC_TEST_METHOD(WSLCE2E_Network_Inspect_FormatJson_IsSingleLine)
test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp
+33 -4
@@ -127,11 +127,18 @@ class WSLCE2ENetworkListTests
127 names.reserve(networks.size());
128 for (const auto& network : networks)
129 {
130 - VERIFY_ARE_EQUAL(3u, network.size());
130 + VERIFY_ARE_EQUAL(9u, network.size());
131 VERIFY_IS_TRUE(network.contains("ID"));
132 VERIFY_IS_FALSE(network.contains("Id"));
133 VERIFY_IS_TRUE(network.contains("Name"));
134 VERIFY_IS_TRUE(network.contains("Driver"));
135 + VERIFY_IS_TRUE(network.contains("Scope"));
136 + VERIFY_IS_TRUE(network.contains("CreatedAt"));
137 + VERIFY_IS_TRUE(network.contains("IPv4"));
138 + VERIFY_IS_TRUE(network.contains("IPv6"));
139 + VERIFY_IS_TRUE(network.contains("Internal"));
140 + VERIFY_IS_TRUE(network.contains("Labels"));
141 + VERIFY_IS_TRUE(network.at("CreatedAt").get<std::string>().ends_with(" +0000 UTC"));
142 VerifyIdOutput(MultiByteToWide(network.at("ID").get<std::string>()), true);
143 names.push_back(network.at("Name").get<std::string>());
144 }
@@ -163,6 +170,28 @@ class WSLCE2ENetworkListTests
170 }
171 }
172
173 + WSLC_TEST_METHOD(WSLCE2E_Network_List_IncludesPredefinedNetworks)
174 + {
175 + // The predefined networks are not created by wslc but are still reported by list.
176 + auto result = RunWslc(L"network list --format json");
177 + result.Verify({.Stderr = L"", .ExitCode = 0});
178 +
179 + std::set<std::string> names;
180 + for (const auto& network : ParseNdjsonOutputAs<NetworkListOutput>(result))
181 + {
182 + names.insert(network.Name);
183 + }
184 +
185 + VERIFY_IS_TRUE(names.contains("bridge"));
186 + VERIFY_IS_TRUE(names.contains("host"));
187 + VERIFY_IS_TRUE(names.contains("none"));
188 +
189 + result = RunWslc(L"network list");
190 + result.Verify({.Stderr = L"", .ExitCode = 0});
191 + VERIFY_IS_TRUE(result.StdoutContainsSubstring(L"NETWORK ID"));
192 + VERIFY_IS_TRUE(result.StdoutContainsSubstring(L"SCOPE"));
193 + }
194 +
195 WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_MalformedValue)
196 {
197 // Filter values must be of the form key=value; bare keys are rejected by the CLI.
@@ -197,7 +226,7 @@ class WSLCE2ENetworkListTests
226 auto listNames = [&](const std::wstring& filterArgs) {
227 auto r = RunWslc(std::format(L"network list --format json {}", filterArgs));
228 r.Verify({.Stderr = L"", .ExitCode = 0});
200 - const auto networks = ParseNdjsonOutputAs<WSLCNetworkInformation>(r);
229 + const auto networks = ParseNdjsonOutputAs<NetworkListOutput>(r);
230 std::set<std::string> names;
231 for (const auto& n : networks)
232 {
@@ -243,7 +272,7 @@ class WSLCE2ENetworkListTests
272 auto listNames = [&](const std::wstring& filterArgs) {
273 auto r = RunWslc(std::format(L"network list --format json {}", filterArgs));
274 r.Verify({.Stderr = L"", .ExitCode = 0});
246 - const auto networks = ParseNdjsonOutputAs<WSLCNetworkInformation>(r);
275 + const auto networks = ParseNdjsonOutputAs<NetworkListOutput>(r);
276 std::set<std::string> names;
277 for (const auto& n : networks)
278 {
@@ -304,7 +333,7 @@ class WSLCE2ENetworkListTests
333 auto listNames = [&](const std::wstring& filterArgs) {
334 auto r = RunWslc(std::format(L"network list --format json {}", filterArgs));
335 r.Verify({.Stderr = L"", .ExitCode = 0});
307 - const auto networks = ParseNdjsonOutputAs<WSLCNetworkInformation>(r);
336 + const auto networks = ParseNdjsonOutputAs<NetworkListOutput>(r);
337 std::set<std::string> names;
338 for (const auto& n : networks)
339 {
test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp
+15 -19
@@ -56,8 +56,8 @@ class WSLCE2ENetworkPruneTests
56 const auto result = RunWslc(L"network prune");
57 result.Verify({.Stderr = L"", .ExitCode = 0});
58
59 - VERIFY_IS_FALSE(result.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName)));
60 - VERIFY_IS_FALSE(result.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName2)));
59 + VERIFY_IS_FALSE(result.StdoutContainsLine(TestNetworkName));
60 + VERIFY_IS_FALSE(result.StdoutContainsLine(TestNetworkName2));
61 }
62
63 WSLC_TEST_METHOD(WSLCE2E_Network_Prune_RemovesUnusedNetwork)
@@ -71,8 +71,10 @@ class WSLCE2ENetworkPruneTests
71 result.Verify({.Stderr = L"", .ExitCode = 0});
72
73 auto output = result.GetStdoutLines();
74 - VERIFY_ARE_EQUAL(1u, output.size());
75 - VERIFY_ARE_NOT_EQUAL(std::wstring::npos, output[0].find(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName)));
74 + VERIFY_ARE_EQUAL(3u, output.size());
75 + VERIFY_ARE_EQUAL(Localization::WSLCCLI_NetworkPruneDeletedHeader(), output[0]);
76 + VERIFY_ARE_EQUAL(TestNetworkName, output[1]);
77 + VERIFY_ARE_EQUAL(std::wstring{}, output[2]);
78
79 VerifyNetworkIsNotListed(TestNetworkName);
80 }
@@ -92,8 +94,8 @@ class WSLCE2ENetworkPruneTests
94 const auto result = RunWslc(L"network prune");
95 result.Verify({.Stderr = L"", .ExitCode = 0});
96
95 - VERIFY_IS_TRUE(result.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName)));
96 - VERIFY_IS_TRUE(result.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName2)));
97 + VERIFY_IS_TRUE(result.StdoutContainsLine(TestNetworkName));
98 + VERIFY_IS_TRUE(result.StdoutContainsLine(TestNetworkName2));
99
100 VerifyNetworkIsNotListed(TestNetworkName);
101 VerifyNetworkIsNotListed(TestNetworkName2);
@@ -117,9 +119,7 @@ class WSLCE2ENetworkPruneTests
119 const auto result = RunWslc(L"network prune");
120 result.Verify({.Stderr = L"", .ExitCode = 0});
121
120 - VERIFY_IS_FALSE(
121 - result.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName)),
122 - L"Network in use by a running container must not be pruned");
122 + VERIFY_IS_FALSE(result.StdoutContainsLine(TestNetworkName), L"Network in use by a running container must not be pruned");
123
124 VerifyNetworkIsListed(TestNetworkName);
125 }
@@ -135,15 +135,14 @@ class WSLCE2ENetworkPruneTests
135 const auto filteredPrune = RunWslc(L"network prune --filter label=wslc.test.never=present");
136 filteredPrune.Verify({.Stderr = L"", .ExitCode = 0});
137 VERIFY_IS_FALSE(
138 - filteredPrune.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName)),
139 - L"Filtered prune should not have deleted the non-matching network");
138 + filteredPrune.StdoutContainsLine(TestNetworkName), L"Filtered prune should not have deleted the non-matching network");
139 VerifyNetworkIsListed(TestNetworkName);
140
141 // A subsequent unfiltered prune should still remove it, proving the filter
142 // was the reason it survived.
143 const auto unfilteredPrune = RunWslc(L"network prune");
144 unfilteredPrune.Verify({.Stderr = L"", .ExitCode = 0});
146 - VERIFY_IS_TRUE(unfilteredPrune.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName)));
145 + VERIFY_IS_TRUE(unfilteredPrune.StdoutContainsLine(TestNetworkName));
146 VerifyNetworkIsNotListed(TestNetworkName);
147 }
148
@@ -162,10 +161,8 @@ class WSLCE2ENetworkPruneTests
161 const auto result = RunWslc(L"network prune --filter label=wslc.test.prune=keep");
162 result.Verify({.Stderr = L"", .ExitCode = 0});
163
165 - VERIFY_IS_TRUE(result.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName)));
166 - VERIFY_IS_FALSE(
167 - result.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName2)),
168 - L"Network without the matching label must not be deleted");
164 + VERIFY_IS_TRUE(result.StdoutContainsLine(TestNetworkName));
165 + VERIFY_IS_FALSE(result.StdoutContainsLine(TestNetworkName2), L"Network without the matching label must not be deleted");
166
167 VerifyNetworkIsNotListed(TestNetworkName);
168 VerifyNetworkIsListed(TestNetworkName2);
@@ -186,10 +183,9 @@ class WSLCE2ENetworkPruneTests
183 const auto result = RunWslc(L"network prune --filter label!=wslc.test.keep");
184 result.Verify({.Stderr = L"", .ExitCode = 0});
185
189 - VERIFY_IS_TRUE(result.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName2)));
186 + VERIFY_IS_TRUE(result.StdoutContainsLine(TestNetworkName2));
187 VERIFY_IS_FALSE(
191 - result.StdoutContainsLine(Localization::WSLCCLI_NetworkPruneDeleted(TestNetworkName)),
192 - L"Labeled network must be preserved when prune negates that label");
188 + result.StdoutContainsLine(TestNetworkName), L"Labeled network must be preserved when prune negates that label");
189
190 VerifyNetworkIsListed(TestNetworkName);
191 VerifyNetworkIsNotListed(TestNetworkName2);