Support Docker-style network aliases in wslc (#40972)

* Support Docker-style network aliases Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Support network option ordering Treat any --network value containing key/value separators as Docker-style advanced network syntax so aliases can appear before name=. Add run/create parser coverage for alias-before-name ordering. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Localize network validation errors Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Fix network option localization placeholder comment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Move network argument parsing to validation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Remove network parser string helper Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Group network argument parser with parse helpers Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Clarify network alias mode check Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Add WSLC network validation tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Validate aliases on additional network modes Reject endpoint aliases on non-user-defined additional network names before the network lookup so built-in modes get the same user-facing error as primary network aliases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address network alias parser feedback Move reusable string helpers to shared code and make network argument parsing throw validation errors directly while keeping aliased network options typed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Pass network argument name in task parsing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix clang-format issues Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Validate network names before conversion Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Fix network argument cache test Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f013920a-4281-4fe8-97d2-5e2497753a51 --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f013920a-4281-4fe8-97d2-5e2497753a51 Copilot-Session: 1771dae2-0542-488e-a19d-5605782222de

David Negstad committed Aug 13, 2026 at 12:22 UTC 084f968cabeab330ededb858b043d7652fcaaa68
20 files changed +579 -44
localization/strings/en-US/Resources.resw
+8
@@ -3070,6 +3070,14 @@ On first run, creates the file with all settings commented out at their defaults
3070 <value>Invalid {} value: network name cannot be empty or whitespace</value>
3071 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3072 </data>
3073 + <data name="WSLCCLI_NetworkDuplicateNameError" xml:space="preserve">
3074 + <value>Invalid {} value: network name can only be specified once</value>
3075 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3076 + </data>
3077 + <data name="WSLCCLI_NetworkUnsupportedOptionError" xml:space="preserve">
3078 + <value>Invalid {} value: unsupported network option '{}'</value>
3079 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3080 + </data>
3081 <data name="WSLCCLI_NetworkHostModeNotSupportedError" xml:space="preserve">
3082 <value>host mode networking is not supported</value>
3083 </data>
src/shared/inc/stringshared.h
+41
@@ -13,15 +13,20 @@ Abstract:
13 --*/
14
15 #pragma once
16 +#include <algorithm>
17 +#include <cctype>
18 +#include <cwctype>
19 #include <set>
20 #include <vector>
21 #include <string>
22 +#include <string_view>
23 #include <sstream>
24 #include <fstream>
25 #include <optional>
26 #include <gsl/gsl>
27 #include <format>
28 #include <source_location>
29 +#include <type_traits>
30
31 #ifndef WIN32
32 #include <string.h>
@@ -136,6 +141,27 @@ inline std::vector<std::basic_string<T>> Split(const std::basic_string<T>& Strin
141 return Output;
142 }
143
144 +template <class T>
145 +inline std::vector<std::basic_string_view<T>> SplitPreserveEmpty(const std::basic_string_view<T> String, T Separator)
146 +{
147 + std::vector<std::basic_string_view<T>> Output;
148 + size_t Start = 0;
149 + while (Start <= String.size())
150 + {
151 + const auto End = String.find(Separator, Start);
152 + if (End == std::basic_string_view<T>::npos)
153 + {
154 + Output.emplace_back(String.substr(Start));
155 + break;
156 + }
157 +
158 + Output.emplace_back(String.substr(Start, End - Start));
159 + Start = End + 1;
160 + }
161 +
162 + return Output;
163 +}
164 +
165 template <class T>
166 inline std::vector<std::basic_string<T>> SplitByMultipleSeparators(const std::basic_string<T>& String, const std::basic_string<T>& Separators)
167 {
@@ -492,6 +518,21 @@ inline bool IsEqual(const std::wstring_view String1, const std::wstring_view Str
518 return (Compare(String1, String2, CaseInsensitive) == String1.size());
519 }
520
521 +template <class T>
522 +inline bool IsEmptyOrWhitespace(const std::basic_string_view<T> String)
523 +{
524 + return String.empty() || std::all_of(String.begin(), String.end(), [](T Ch) {
525 + if constexpr (std::is_same_v<T, wchar_t>)
526 + {
527 + return std::iswspace(static_cast<wint_t>(Ch));
528 + }
529 + else
530 + {
531 + return std::isspace(static_cast<unsigned char>(Ch));
532 + }
533 + });
534 +}
535 +
536 // Parses a boolean from a string. By default only "1"/"0" and "true"/"false"
537 // (case-insensitive) are recognized. When AllowExtendedForms is true the single
538 // character forms "t"/"f" (case-insensitive) are also accepted, matching the full
src/windows/common/WSLCContainerLauncher.cpp
+25 -7
@@ -282,7 +282,12 @@ void wsl::windows::common::WSLCContainerLauncher::AddTmpfs(const std::string& Co
282
283 void wsl::windows::common::WSLCContainerLauncher::AddAdditionalNetwork(const std::string& Name)
284 {
285 - m_additionalNetworks.push_back(Name);
285 + AddAdditionalNetwork(Name, {});
286 +}
287 +
288 +void wsl::windows::common::WSLCContainerLauncher::AddAdditionalNetwork(const std::string& Name, const std::vector<std::string>& Aliases)
289 +{
290 + m_additionalNetworks.push_back({.Name = Name, .Aliases = Aliases});
291 }
292
293 void wsl::windows::common::WSLCContainerLauncher::AddPrimaryNetworkAlias(const std::string& Alias)
@@ -424,24 +429,37 @@ std::pair<HRESULT, std::optional<RunningWSLCContainer>> WSLCContainerLauncher::C
429 // Each additional network becomes an entry in NetworkingConfig.EndpointsConfig.
430 std::vector<WSLCNetworkConnection> connections;
431 connections.reserve(m_additionalNetworks.size());
432 + std::vector<std::vector<KeyValuePair>> connectionSettings;
433 + connectionSettings.reserve(m_additionalNetworks.size());
434 for (const auto& e : m_additionalNetworks)
435 {
429 - connections.push_back({.NetworkName = e.c_str()});
436 + auto& settings = connectionSettings.emplace_back();
437 + settings.reserve(e.Aliases.size());
438 + for (const auto& alias : e.Aliases)
439 + {
440 + settings.push_back({.Key = "Aliases", .Value = alias.c_str()});
441 + }
442 +
443 + connections.push_back({
444 + .NetworkName = e.Name.c_str(),
445 + .Settings = settings.empty() ? nullptr : settings.data(),
446 + .SettingsCount = static_cast<ULONG>(settings.size()),
447 + });
448 }
449
450 options.ContainerNetwork.Networks = connections.empty() ? nullptr : connections.data();
451 options.ContainerNetwork.NetworksCount = static_cast<ULONG>(connections.size());
452
453 // Aliases for the primary endpoint.
436 - std::vector<KeyValuePair> aliasKvps;
437 - aliasKvps.reserve(m_primaryNetworkAliases.size());
454 + std::vector<KeyValuePair> primarySettings;
455 + primarySettings.reserve(m_primaryNetworkAliases.size());
456 for (const auto& alias : m_primaryNetworkAliases)
457 {
440 - aliasKvps.push_back({.Key = "Aliases", .Value = alias.c_str()});
458 + primarySettings.push_back({.Key = "Aliases", .Value = alias.c_str()});
459 }
460
443 - options.ContainerNetwork.Settings = aliasKvps.empty() ? nullptr : aliasKvps.data();
444 - options.ContainerNetwork.SettingsCount = static_cast<ULONG>(aliasKvps.size());
461 + options.ContainerNetwork.Settings = primarySettings.empty() ? nullptr : primarySettings.data();
462 + options.ContainerNetwork.SettingsCount = static_cast<ULONG>(primarySettings.size());
463
464 options.MemoryBytes = m_memoryBytes;
465 options.NanoCpus = m_nanoCpus;
src/windows/common/WSLCContainerLauncher.h
+8 -1
@@ -63,6 +63,7 @@ public:
63 void AddLabel(const std::string& Key, const std::string& Value);
64 void AddTmpfs(const std::string& ContainerPath, const std::string& Options);
65 void AddAdditionalNetwork(const std::string& Name);
66 + void AddAdditionalNetwork(const std::string& Name, const std::vector<std::string>& Aliases);
67 void AddPrimaryNetworkAlias(const std::string& Alias);
68
69 std::pair<HRESULT, std::optional<RunningWSLCContainer>> CreateNoThrow(IWSLCSession& Session, IWarningCallback* WarningCallback = nullptr);
@@ -99,6 +100,12 @@ public:
100 using WSLCProcessLauncher::SetWorkingDirectory;
101
102 private:
103 + struct NetworkConnection
104 + {
105 + std::string Name;
106 + std::vector<std::string> Aliases;
107 + };
108 +
109 std::string m_image;
110 std::string m_name;
111 std::vector<WSLCPortMapping> m_ports;
@@ -123,7 +130,7 @@ private:
130 std::vector<std::string> m_dnsServers;
131 std::vector<std::string> m_dnsSearchDomains;
132 std::vector<std::string> m_dnsOptions;
126 - std::vector<std::string> m_additionalNetworks;
133 + std::vector<NetworkConnection> m_additionalNetworks;
134 std::vector<std::string> m_primaryNetworkAliases;
135 std::vector<WSLCLabel> m_labels;
136 std::deque<std::string> m_labelKeys;
src/windows/service/inc/wslc.idl
+3
@@ -255,6 +255,9 @@ typedef struct _WSLCUlimit
255 typedef struct _WSLCNetworkConnection
256 {
257 [string] LPCSTR NetworkName;
258 +
259 + // Settings for this endpoint.
260 + // KVP-encoded; duplicate keys are allowed (e.g., multiple "Aliases" entries).
261 [unique, size_is(SettingsCount)] const KeyValuePair* Settings;
262 ULONG SettingsCount;
263 } WSLCNetworkConnection;
src/windows/wslc/arguments/ArgumentConvertedTypes.h
+2
@@ -18,6 +18,7 @@ Abstract:
18 #include "ArgumentTypes.h"
19 #include "ContainerModel.h"
20 #include "InspectModel.h"
21 +#include "SpecParsing.h"
22
23 #include <cstdint>
24 #include <string>
@@ -40,6 +41,7 @@ using InspectType = wsl::windows::wslc::models::InspectType;
41 using JsonIndent = int;
42 using ProgressMode = wsl::windows::wslc::models::ProgressMode;
43 using PullPolicy = wsl::windows::wslc::models::PullPolicy;
44 +using ParsedNetworkArgument = wsl::windows::wslc::validation::ParsedNetworkArgument;
45 using WSLCSignal = ::WSLCSignal;
46 using UlimitValue = std::tuple<std::string, int64_t, int64_t>;
47 using KeyValuePair = std::pair<std::string, std::string>;
src/windows/wslc/arguments/ArgumentDefinitions.h
+1 -1
@@ -94,7 +94,7 @@ _(Link, "link", NO_ALIAS, Kind::Value,
94 _(LinkLocalIp, "link-local-ip", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_LinkLocalIpArgDescription()) \
95 _(Memory, "memory", L"m", Kind::Value, int64_t, Localization::WSLCCLI_MemoryArgDescription()) \
96 _(Name, "name", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_NameArgDescription()) \
97 -_(Network, "network", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_NetworkArgDescription()) \
97 +_(Network, "network", NO_ALIAS, Kind::Value, ParsedNetworkArgument, Localization::WSLCCLI_NetworkArgDescription()) \
98 _(NetworkAlias, "network-alias", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_NetworkAliasArgDescription()) \
99 _(NetworkName, "network-name", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_NetworkNameArgDescription()) \
100 /*_(NoDNS, "no-dns", NO_ALIAS, Kind::Flag, NoConversion, Localization::WSLCCLI_NoDNSArgDescription())*/ \
src/windows/wslc/arguments/ArgumentValidation.cpp
+6 -10
@@ -236,19 +236,15 @@ void Argument::Validate(ArgMap& execArgs) const
236
237 case ArgType::Network:
238 {
239 - for (const auto& value : RawArgMapAccess::GetAll<ArgType::Network>(execArgs))
240 - {
241 - if (value.empty() ||
242 - std::all_of(value.begin(), value.end(), [](wchar_t c) { return std::iswspace(static_cast<wint_t>(c)); }))
243 - {
244 - throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(m_name));
245 - }
246 -
247 - if (IsEqual(value, L"host", true))
239 + CacheConverted<ArgType::Network>(execArgs, m_name, [](const std::wstring& value, const std::wstring& name) {
240 + auto parsed = validation::ParseNetworkArgument(value, name);
241 + if (IsEqual(parsed.Name, "host", true))
242 {
243 throw ArgumentException(Localization::WSLCCLI_NetworkHostModeNotSupportedError());
244 }
251 - }
245 +
246 + return parsed;
247 + });
248 break;
249 }
250
src/windows/wslc/arguments/ArgumentValidation.h
+1
@@ -19,6 +19,7 @@ Abstract:
19 #include "ArgumentConvertedTypes.h"
20 #include "SpecParsing.h"
21 #include <string>
22 +#include <string_view>
23 #include <tuple>
24 #include <vector>
25 #include <charconv>
src/windows/wslc/arguments/SpecParsing.cpp
+74
@@ -458,6 +458,80 @@ std::pair<std::string, std::string> ParseFilter(const std::wstring& value)
458 return {WideToMultiByte(kv.Key), WideToMultiByte(kv.Value)};
459 }
460
461 +ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value, const std::wstring& argName)
462 +{
463 + ParsedNetworkArgument result;
464 +
465 + auto parseOptions = [&](std::wstring_view options, bool requireName) {
466 + bool parsedName = false;
467 + for (const auto part : SplitPreserveEmpty(options, L','))
468 + {
469 + const auto separator = part.find(L'=');
470 + if (separator == std::wstring_view::npos || separator == 0)
471 + {
472 + throw ArgumentException(Localization::WSLCCLI_NetworkUnsupportedOptionError(argName, std::wstring{part}));
473 + }
474 +
475 + const auto key = part.substr(0, separator);
476 + const auto optionValue = part.substr(separator + 1);
477 + if (key == L"name")
478 + {
479 + if (IsEmptyOrWhitespace(optionValue))
480 + {
481 + throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(argName));
482 + }
483 +
484 + if (parsedName)
485 + {
486 + throw ArgumentException(Localization::WSLCCLI_NetworkDuplicateNameError(argName));
487 + }
488 +
489 + parsedName = true;
490 + result.Name = WideToMultiByte(std::wstring{optionValue});
491 + }
492 + else if (key == L"alias")
493 + {
494 + if (IsEmptyOrWhitespace(optionValue))
495 + {
496 + throw ArgumentException(Localization::WSLCCLI_NetworkAliasEmptyError(argName));
497 + }
498 +
499 + result.Aliases.emplace_back(WideToMultiByte(std::wstring{optionValue}));
500 + }
501 + else
502 + {
503 + throw ArgumentException(Localization::WSLCCLI_NetworkUnsupportedOptionError(argName, std::wstring{key}));
504 + }
505 + }
506 +
507 + if (requireName && !parsedName)
508 + {
509 + throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(argName));
510 + }
511 + };
512 +
513 + if (value.find(L'=') != std::wstring_view::npos)
514 + {
515 + parseOptions(value, true);
516 + }
517 + else
518 + {
519 + if (IsEmptyOrWhitespace(value))
520 + {
521 + throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(argName));
522 + }
523 +
524 + result.Name = WideToMultiByte(std::wstring{value});
525 + }
526 +
527 + if (result.Name.empty())
528 + {
529 + throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(argName));
530 + }
531 +
532 + return result;
533 +}
534 +
535 // Map of signal names to WSLCSignal enum values
536 static const std::unordered_map<std::wstring, WSLCSignal> SignalMap = {
537 {L"SIGHUP", WSLCSignalSIGHUP}, {L"SIGINT", WSLCSignalSIGINT}, {L"SIGQUIT", WSLCSignalSIGQUIT},
src/windows/wslc/arguments/SpecParsing.h
+11
@@ -17,8 +17,10 @@ Abstract:
17 #include "ContainerModel.h"
18 #include "InspectModel.h"
19 #include <string>
20 +#include <string_view>
21 #include <tuple>
22 #include <utility>
23 +#include <vector>
24 #include <wslc.h>
25
26 namespace wsl::windows::wslc::services {
@@ -72,6 +74,15 @@ std::pair<std::string, std::string> ParseDriverOption(const std::wstring& value)
74 // Parses a --filter spec ("key=value"); the separator is required.
75 std::pair<std::string, std::string> ParseFilter(const std::wstring& value);
76
77 +struct ParsedNetworkArgument
78 +{
79 + std::string Name;
80 + std::vector<std::string> Aliases;
81 +};
82 +
83 +// Parses a --network spec ("network" or "name=network,alias=alias").
84 +ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value, const std::wstring& argName = {});
85 +
86 // Parses a signal by name ("SIGKILL"/"KILL", case-insensitive) or number ("9") into a WSLCSignal.
87 WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring& argName = {});
88
src/windows/wslc/services/ContainerModel.h
+9 -1
@@ -16,7 +16,9 @@ Abstract:
16
17 #include <wslservice.h>
18 #include <wslc.h>
19 +#include <optional>
20 #include <string>
21 +#include <vector>
22
23 namespace wsl::windows::wslc::models {
24
@@ -27,6 +29,12 @@ enum class FormatType
29 Json,
30 };
31
32 +struct ContainerNetwork
33 +{
34 + std::string Name;
35 + std::vector<std::string> Aliases;
36 +};
37 +
38 enum class PullPolicy
39 {
40 Missing,
@@ -74,7 +82,7 @@ struct ContainerOptions
82 std::vector<std::string> DnsServers;
83 std::vector<std::string> DnsSearchDomains;
84 std::vector<std::string> DnsOptions;
77 - std::vector<std::string> Networks;
85 + std::vector<ContainerNetwork> Networks;
86 std::vector<std::string> NetworkAliases;
87 std::vector<std::string> Tmpfs;
88 std::vector<std::pair<std::string, std::string>> Labels;
src/windows/wslc/services/ContainerService.cpp
+27 -4
@@ -41,6 +41,12 @@ static void SetContainerArguments(WSLCProcessOptions& options, std::vector<const
41 options.CommandLine = {.Values = argsStorage.data(), .Count = static_cast<ULONG>(argsStorage.size())};
42 }
43
44 +static bool SupportsNetworkAliases(std::string_view network)
45 +{
46 + // Aliases are only supported for user-defined networks, not built-in or container-sourced network modes.
47 + return network != "bridge" && network != "host" && network != "none" && !network.starts_with("container:");
48 +}
49 +
50 static void PullImage(Terminal& terminal, Session& session, const std::string& image)
51 {
52 ImageProgressCallback callback(terminal, Terminal::Level::Info);
@@ -66,14 +72,20 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(Terminal& termi
72 WI_SetFlagIf(containerFlags, WSLCContainerFlagsPublishAll, options.PublishAll);
73 WI_SetFlagIf(containerFlags, WSLCContainerFlagsGpu, options.Gpu);
74
69 - std::string networkMode = options.Networks.empty() ? std::string("bridge") : options.Networks.front();
75 + std::string networkMode = options.Networks.empty() ? std::string("bridge") : options.Networks.front().Name;
76
77 wsl::windows::common::WSLCContainerLauncher containerLauncher(
78 image, options.Name, options.Arguments, options.EnvironmentVariables, std::move(networkMode), processFlags);
79
80 for (size_t i = 1; i < options.Networks.size(); ++i)
81 {
76 - containerLauncher.AddAdditionalNetwork(options.Networks[i]);
82 + const auto& network = options.Networks[i];
83 + THROW_HR_WITH_USER_ERROR_IF(
84 + E_INVALIDARG,
85 + Localization::MessageWslcAliasRequiresUserDefinedNetwork(),
86 + !network.Aliases.empty() && !SupportsNetworkAliases(network.Name));
87 +
88 + containerLauncher.AddAdditionalNetwork(network.Name, network.Aliases);
89 }
90
91 if (!options.NetworkAliases.empty())
@@ -82,13 +94,24 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(Terminal& termi
94
95 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcAliasAmbiguousWithMultipleNetworks(), options.Networks.size() > 1);
96
97 + const auto& primary = options.Networks.front().Name;
98 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcAliasRequiresUserDefinedNetwork(), !SupportsNetworkAliases(primary));
99 +
100 + for (const auto& alias : options.NetworkAliases)
101 + {
102 + containerLauncher.AddPrimaryNetworkAlias(alias);
103 + }
104 + }
105 +
106 + if (!options.Networks.empty())
107 + {
108 const auto& primary = options.Networks.front();
109 THROW_HR_WITH_USER_ERROR_IF(
110 E_INVALIDARG,
111 Localization::MessageWslcAliasRequiresUserDefinedNetwork(),
89 - primary == "bridge" || primary == "host" || primary == "none" || primary.starts_with("container:"));
112 + !primary.Aliases.empty() && !SupportsNetworkAliases(primary.Name));
113
91 - for (const auto& alias : options.NetworkAliases)
114 + for (const auto& alias : primary.Aliases)
115 {
116 containerLauncher.AddPrimaryNetworkAlias(alias);
117 }
src/windows/wslc/tasks/ContainerTasks.cpp
+4 -2
@@ -825,9 +825,11 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
825 {
826 auto networks = context.Args.GetAllValues<ArgType::Network>();
827 options.Networks.reserve(options.Networks.size() + networks.size());
828 - for (const auto& value : networks)
828 + for (auto& parsed : networks)
829 {
830 - options.Networks.emplace_back(WideToMultiByte(value));
830 + auto& network = options.Networks.emplace_back();
831 + network.Name = std::move(parsed.Name);
832 + network.Aliases = std::move(parsed.Aliases);
833 }
834 }
835
src/windows/wslcsession/WSLCContainer.cpp
+12 -2
@@ -173,6 +173,11 @@ bool NetworkModeAllocatesVmPorts(std::string_view mode) noexcept
173 return mode != "host" && mode != "none" && !mode.starts_with(c_containerNetworkPrefix);
174 }
175
176 +bool NetworkSupportsAliases(std::string_view mode) noexcept
177 +{
178 + return mode != "bridge" && NetworkModeAllocatesVmPorts(mode);
179 +}
180 +
181 // Reject `<prefix>:<value>` strings whose prefix isn't `container:`. Docker treats colon-prefixed
182 // modes (`service:`, `ns:`, ...) as special, but WSLC only supports `container:`. Surface the
183 // rejection here so both Create() and Open() recovery paths share the same gate.
@@ -267,6 +272,7 @@ EndpointConfig ResolveEndpointConfig(const KeyValuePair* settings, ULONG count,
272 {
273 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcAliasEmpty(), isBlank(alias));
274 }
275 +
276 config.Aliases = std::move(it->second);
277 }
278
@@ -350,13 +356,17 @@ std::map<std::string, EndpointConfig> ResolveEndpoints(
356 auto [it, inserted] = resolved.try_emplace(name);
357 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcDuplicateNetwork(name), !inserted);
358
359 + auto config = ResolveEndpointConfig(connections[i].Settings, connections[i].SettingsCount, name);
360 + THROW_HR_WITH_USER_ERROR_IF(
361 + E_INVALIDARG, Localization::MessageWslcAliasRequiresUserDefinedNetwork(), config.Aliases.has_value() && !NetworkSupportsAliases(name));
362 +
363 if (name != "bridge")
364 {
365 THROW_HR_WITH_USER_ERROR_IF(
366 WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(name), !sessionNetworks.contains(name));
367 }
368
359 - it->second = ResolveEndpointConfig(connections[i].Settings, connections[i].SettingsCount, name);
369 + it->second = std::move(config);
370 }
371 return resolved;
372 }
@@ -2004,7 +2014,7 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2014 THROW_HR_WITH_USER_ERROR_IF(
2015 E_INVALIDARG,
2016 Localization::MessageWslcAliasRequiresUserDefinedNetwork(),
2007 - primaryConfig.Aliases.has_value() && (networkMode == "bridge" || !NetworkModeAllocatesVmPorts(networkMode)));
2017 + primaryConfig.Aliases.has_value() && !NetworkSupportsAliases(networkMode));
2018
2019 const bool hasNonAliasEndpointSettings =
2020 primaryConfig.IPAMConfig.has_value() || primaryConfig.Links.has_value() || primaryConfig.DriverOpts.has_value();
test/windows/WSLCTests.cpp
+47
@@ -8055,6 +8055,53 @@ class WSLCTests
8055 VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "backup") != endpoint.Aliases.end());
8056 }
8057
8058 + // Aliases on primary and additional user-defined networks — all present.
8059 + {
8060 + const std::string primaryNetworkName = "alias-net-primary";
8061 + const std::string additionalNetworkName = "alias-net-additional";
8062 + createNetwork(primaryNetworkName, "172.64.0.0/16");
8063 + createNetwork(additionalNetworkName, "172.65.0.0/16");
8064 + auto primaryNetCleanup =
8065 + wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(primaryNetworkName.c_str())); });
8066 + auto additionalNetCleanup =
8067 + wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(additionalNetworkName.c_str())); });
8068 +
8069 + WSLCContainerLauncher launcher("debian:latest", "alias-ctr-additional", {"sleep", "99999"}, {}, primaryNetworkName);
8070 + launcher.AddPrimaryNetworkAlias("db");
8071 + launcher.AddAdditionalNetwork(additionalNetworkName, {"cache", "replica"});
8072 + auto container = launcher.Launch(*m_defaultSession);
8073 +
8074 + auto inspect = container.Inspect();
8075 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(primaryNetworkName));
8076 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(additionalNetworkName));
8077 + const auto& primaryEndpoint = inspect.NetworkSettings.Networks.at(primaryNetworkName);
8078 + const auto& additionalEndpoint = inspect.NetworkSettings.Networks.at(additionalNetworkName);
8079 + VERIFY_IS_TRUE(std::ranges::find(primaryEndpoint.Aliases, "db") != primaryEndpoint.Aliases.end());
8080 + VERIFY_IS_TRUE(std::ranges::find(additionalEndpoint.Aliases, "cache") != additionalEndpoint.Aliases.end());
8081 + VERIFY_IS_TRUE(std::ranges::find(additionalEndpoint.Aliases, "replica") != additionalEndpoint.Aliases.end());
8082 + }
8083 +
8084 + // Aliases on additional built-in/non-user-defined networks — rejected before network lookup.
8085 + {
8086 + const std::string primaryNetworkName = "alias-net-invalid-additional";
8087 + createNetwork(primaryNetworkName, "172.66.0.0/16");
8088 + auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(primaryNetworkName.c_str())); });
8089 +
8090 + auto expectAdditionalNetworkAliasError = [&](const std::string& containerName, const std::string& additionalNetworkName) {
8091 + WSLCContainerLauncher launcher("debian:latest", containerName, {"sleep", "99999"}, {}, primaryNetworkName);
8092 + launcher.AddAdditionalNetwork(additionalNetworkName, {"db"});
8093 +
8094 + auto result = wil::ResultFromException([&] { launcher.Launch(*m_defaultSession); });
8095 + VERIFY_ARE_EQUAL(E_INVALIDARG, result);
8096 + ValidateCOMErrorMessage(L"Network aliases require a user-defined network. Use --network to specify one.");
8097 + };
8098 +
8099 + expectAdditionalNetworkAliasError("alias-ctr-additional-bridge", "bridge");
8100 + expectAdditionalNetworkAliasError("alias-ctr-additional-host", "host");
8101 + expectAdditionalNetworkAliasError("alias-ctr-additional-none", "none");
8102 + expectAdditionalNetworkAliasError("alias-ctr-additional-container", "container:alias-ctr-target");
8103 + }
8104 +
8105 // Alias on 'host' mode — rejected at the IDL layer.
8106 {
8107 expectError(
test/windows/wslc/WSLCCLIArgumentUnitTests.cpp
+16 -10
@@ -439,6 +439,14 @@ class WSLCCLIArgumentUnitTests
439 const std::string expected = "conv-value";
440 VERIFY_IS_TRUE(std::vector<BYTE>(expected.begin(), expected.end()) == secret.Value);
441 }
442 +
443 + // string -> ParsedNetworkArgument (docker-style network name and aliases)
444 + {
445 + auto network = ValidateAndGetCached<ArgType::Network>(L"name=custom,alias=web");
446 + VERIFY_ARE_EQUAL(network.Name, std::string("custom"));
447 + VERIFY_ARE_EQUAL(network.Aliases.size(), static_cast<size_t>(1));
448 + VERIFY_ARE_EQUAL(network.Aliases[0], std::string("web"));
449 + }
450 }
451
452 // Test: Because ArgMap is a multimap and any command may allow an argument to repeat, a single
@@ -518,7 +526,6 @@ class WSLCCLIArgumentUnitTests
526 {ArgType::Gpus, L"all"},
527 {ArgType::Volume, LR"(C:\hostPath:/containerPath)"},
528 {ArgType::WorkDir, L"/app"},
521 - {ArgType::Network, L"bridge"},
529 {ArgType::NetworkAlias, L"myalias"},
530 };
531
@@ -589,12 +596,9 @@ class WSLCCLIArgumentUnitTests
596 VERIFY_ARE_EQUAL(args.CountValidated(ArgType::Signal), static_cast<size_t>(2));
597 }
598
592 - // Test: A validate-only argument (checked during validation but not converted into a cached
593 - // value) is validated on demand when read, so a value added after the up-front pass is checked
594 - // exactly as a command-line value. These arguments have no converted cache, so the earlier
595 - // converted-path tests do not cover them; the read path still runs their range/format checks.
596 - // Network rejects "host" mode and unsupported values.
597 - TEST_METHOD(ArgumentValidate_OnDemandValidateOnlyArgIsChecked)
599 + // Test: Arguments are validated on demand when read, so a value added after the up-front pass is
600 + // checked exactly as a command-line value.
601 + TEST_METHOD(ArgumentValidate_OnDemandArgIsChecked)
602 {
603 ArgMap labels;
604 labels.Add(ArgType::BuildLabel, std::wstring(L"foo"));
@@ -608,12 +612,14 @@ class WSLCCLIArgumentUnitTests
612 invalidLabel.Add(ArgType::BuildLabel, std::wstring(L"=value"));
613 VERIFY_THROWS(invalidLabel.GetAllValues<ArgType::BuildLabel>(), wil::ResultException);
614
611 - // Valid value, no prior validation pass: the read validates on demand and returns the raw value.
615 + // Valid value, no prior validation pass: the read validates and converts on demand.
616 ArgMap valid;
613 - valid.Add(ArgType::Network, std::wstring(L"bridge"));
617 + valid.Add(ArgType::Network, std::wstring(L"name=custom,alias=web"));
618 auto networks = valid.GetAllValues<ArgType::Network>();
619 VERIFY_ARE_EQUAL(networks.size(), static_cast<size_t>(1));
616 - VERIFY_ARE_EQUAL(networks[0], std::wstring(L"bridge"));
620 + VERIFY_ARE_EQUAL(networks[0].Name, std::string("custom"));
621 + VERIFY_ARE_EQUAL(networks[0].Aliases.size(), static_cast<size_t>(1));
622 + VERIFY_ARE_EQUAL(networks[0].Aliases[0], std::string("web"));
623
624 // Invalid value, no prior validation pass: the read validates on demand and throws, matching
625 // the failure the up-front pass raises for the same value.
test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
+216 -6
@@ -21,6 +21,7 @@ Abstract:
21 #include "AsyncExecution.h"
22 #include "Command.h"
23 #include "RootCommand.h"
24 +#include "ArgumentValidation.h"
25 #include "ContainerCommand.h"
26 #include "ContainerTasks.h"
27
@@ -400,7 +401,7 @@ class WSLCCLIExecutionUnitTests
401
402 const auto& options = context.Data.Get<Data::ContainerOptions>();
403 VERIFY_ARE_EQUAL(1u, options.Networks.size());
403 - VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0]);
404 + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name);
405 }
406
407 TEST_METHOD(RunCommand_ParseNetworkMultipleValues_PreservesOrder)
@@ -416,8 +417,50 @@ class WSLCCLIExecutionUnitTests
417
418 const auto& options = context.Data.Get<Data::ContainerOptions>();
419 VERIFY_ARE_EQUAL(2u, options.Networks.size());
419 - VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0]);
420 - VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1]);
420 + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name);
421 + VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1].Name);
422 + }
423 +
424 + TEST_METHOD(RunCommand_ParseDockerNetworkAliases_SetsPerNetworkAliases)
425 + {
426 + auto invocation =
427 + CreateInvocationFromCommandLine(L"wslc --network name=net1,alias=a,alias=b --network name=net2,alias=c ubuntu sh");
428 +
429 + ContainerRunCommand command{L""};
430 + CLIExecutionContext context;
431 + command.ParseArguments(invocation, context.Args);
432 + command.ValidateArguments(context.Args);
433 +
434 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
435 +
436 + const auto& options = context.Data.Get<Data::ContainerOptions>();
437 + VERIFY_ARE_EQUAL(2u, options.Networks.size());
438 + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name);
439 + VERIFY_ARE_EQUAL(2u, options.Networks[0].Aliases.size());
440 + VERIFY_ARE_EQUAL(std::string("a"), options.Networks[0].Aliases[0]);
441 + VERIFY_ARE_EQUAL(std::string("b"), options.Networks[0].Aliases[1]);
442 + VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1].Name);
443 + VERIFY_ARE_EQUAL(1u, options.Networks[1].Aliases.size());
444 + VERIFY_ARE_EQUAL(std::string("c"), options.Networks[1].Aliases[0]);
445 + }
446 +
447 + TEST_METHOD(RunCommand_ParseDockerNetworkAliasesWithNameAfterAlias_SetsPerNetworkAliases)
448 + {
449 + auto invocation = CreateInvocationFromCommandLine(L"wslc --network alias=a,name=net1,alias=b ubuntu sh");
450 +
451 + ContainerRunCommand command{L""};
452 + CLIExecutionContext context;
453 + command.ParseArguments(invocation, context.Args);
454 + command.ValidateArguments(context.Args);
455 +
456 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
457 +
458 + const auto& options = context.Data.Get<Data::ContainerOptions>();
459 + VERIFY_ARE_EQUAL(1u, options.Networks.size());
460 + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name);
461 + VERIFY_ARE_EQUAL(2u, options.Networks[0].Aliases.size());
462 + VERIFY_ARE_EQUAL(std::string("a"), options.Networks[0].Aliases[0]);
463 + VERIFY_ARE_EQUAL(std::string("b"), options.Networks[0].Aliases[1]);
464 }
465
466 TEST_METHOD(RunCommand_ParseNetworkEmptyValue_ThrowsArgumentException)
@@ -445,7 +488,7 @@ class WSLCCLIExecutionUnitTests
488
489 const auto& options = context.Data.Get<Data::ContainerOptions>();
490 VERIFY_ARE_EQUAL(1u, options.Networks.size());
448 - VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0]);
491 + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name);
492 }
493
494 TEST_METHOD(CreateCommand_ParseNetworkMultipleValues_PreservesOrder)
@@ -461,8 +504,160 @@ class WSLCCLIExecutionUnitTests
504
505 const auto& options = context.Data.Get<Data::ContainerOptions>();
506 VERIFY_ARE_EQUAL(2u, options.Networks.size());
464 - VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0]);
465 - VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1]);
507 + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name);
508 + VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1].Name);
509 + }
510 +
511 + TEST_METHOD(CreateCommand_ParseDockerNetworkAliases_SetsPerNetworkAliases)
512 + {
513 + auto invocation =
514 + CreateInvocationFromCommandLine(L"wslc --network name=net1,alias=a,alias=b --network name=net2,alias=c ubuntu sh");
515 +
516 + ContainerCreateCommand command{L""};
517 + CLIExecutionContext context;
518 + command.ParseArguments(invocation, context.Args);
519 + command.ValidateArguments(context.Args);
520 +
521 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
522 +
523 + const auto& options = context.Data.Get<Data::ContainerOptions>();
524 + VERIFY_ARE_EQUAL(2u, options.Networks.size());
525 + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name);
526 + VERIFY_ARE_EQUAL(2u, options.Networks[0].Aliases.size());
527 + VERIFY_ARE_EQUAL(std::string("a"), options.Networks[0].Aliases[0]);
528 + VERIFY_ARE_EQUAL(std::string("b"), options.Networks[0].Aliases[1]);
529 + VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1].Name);
530 + VERIFY_ARE_EQUAL(1u, options.Networks[1].Aliases.size());
531 + VERIFY_ARE_EQUAL(std::string("c"), options.Networks[1].Aliases[0]);
532 + }
533 +
534 + TEST_METHOD(CreateCommand_ParseDockerNetworkAliasesWithNameAfterAlias_SetsPerNetworkAliases)
535 + {
536 + auto invocation = CreateInvocationFromCommandLine(L"wslc --network alias=a,name=net1,alias=b ubuntu sh");
537 +
538 + ContainerCreateCommand command{L""};
539 + CLIExecutionContext context;
540 + command.ParseArguments(invocation, context.Args);
541 + command.ValidateArguments(context.Args);
542 +
543 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
544 +
545 + const auto& options = context.Data.Get<Data::ContainerOptions>();
546 + VERIFY_ARE_EQUAL(1u, options.Networks.size());
547 + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0].Name);
548 + VERIFY_ARE_EQUAL(2u, options.Networks[0].Aliases.size());
549 + VERIFY_ARE_EQUAL(std::string("a"), options.Networks[0].Aliases[0]);
550 + VERIFY_ARE_EQUAL(std::string("b"), options.Networks[0].Aliases[1]);
551 + }
552 +
553 + TEST_METHOD(CreateCommand_ParseNetworkDuplicateNameOption_ThrowsArgumentException)
554 + {
555 + auto invocation = CreateInvocationFromCommandLine(L"wslc --network name=net1,name=net2 ubuntu sh");
556 +
557 + ContainerCreateCommand command{L""};
558 + CLIExecutionContext context;
559 + command.ParseArguments(invocation, context.Args);
560 +
561 + VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) {
562 + const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkDuplicateNameError(L"network");
563 + return exception.Message() == expectedMessage;
564 + });
565 + }
566 +
567 + TEST_METHOD(CreateCommand_ParseNetworkUnsupportedOption_ThrowsArgumentException)
568 + {
569 + auto invocation = CreateInvocationFromCommandLine(
570 + L"wslc --network name=net1,driver-opt=com.docker.network.endpoint.sysctls="
571 + L"net.ipv4.conf.IFNAME.log_martians=1 ubuntu sh");
572 +
573 + ContainerCreateCommand command{L""};
574 + CLIExecutionContext context;
575 + command.ParseArguments(invocation, context.Args);
576 +
577 + VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) {
578 + const auto expectedMessage =
579 + wsl::shared::Localization::WSLCCLI_NetworkUnsupportedOptionError(L"network", L"driver-opt");
580 + return exception.Message() == expectedMessage;
581 + });
582 + }
583 +
584 + TEST_METHOD(CreateCommand_ParseNetworkUnknownOption_ThrowsArgumentException)
585 + {
586 + auto invocation = CreateInvocationFromCommandLine(L"wslc --network name=net1,aliases=a ubuntu sh");
587 +
588 + ContainerCreateCommand command{L""};
589 + CLIExecutionContext context;
590 + command.ParseArguments(invocation, context.Args);
591 +
592 + VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) {
593 + const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkUnsupportedOptionError(L"network", L"aliases");
594 + return exception.Message() == expectedMessage;
595 + });
596 + }
597 +
598 + TEST_METHOD(CreateCommand_ParseNetworkBackendAliasesOption_ThrowsArgumentException)
599 + {
600 + auto invocation = CreateInvocationFromCommandLine(L"wslc --network name=net1,Aliases=a ubuntu sh");
601 +
602 + ContainerCreateCommand command{L""};
603 + CLIExecutionContext context;
604 + command.ParseArguments(invocation, context.Args);
605 +
606 + VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) {
607 + const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkUnsupportedOptionError(L"network", L"Aliases");
608 + return exception.Message() == expectedMessage;
609 + });
610 + }
611 +
612 + TEST_METHOD(CreateCommand_ParseNetworkAliasWithoutName_ThrowsArgumentException)
613 + {
614 + auto invocation = CreateInvocationFromCommandLine(L"wslc --network alias=a ubuntu sh");
615 +
616 + ContainerCreateCommand command{L""};
617 + CLIExecutionContext context;
618 + command.ParseArguments(invocation, context.Args);
619 +
620 + VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) {
621 + const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkEmptyError(L"network");
622 + return exception.Message() == expectedMessage;
623 + });
624 + }
625 +
626 + TEST_METHOD(CreateCommand_ParseNetworkNameWhitespaceValue_ThrowsArgumentException)
627 + {
628 + auto invocation = CreateInvocationFromCommandLine(L"wslc --network \"name= \" ubuntu sh");
629 +
630 + ContainerCreateCommand command{L""};
631 + CLIExecutionContext context;
632 + command.ParseArguments(invocation, context.Args);
633 +
634 + VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) {
635 + const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkEmptyError(L"network");
636 + return exception.Message() == expectedMessage;
637 + });
638 + }
639 +
640 + TEST_METHOD(ParseNetworkArgument_NameUnicodeWhitespaceValue_ThrowsArgumentException)
641 + {
642 + VERIFY_THROWS_SPECIFIC(
643 + validation::ParseNetworkArgument(L"name=\u3000", L"network"), wsl::windows::wslc::ArgumentException, [](const auto& exception) {
644 + const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkEmptyError(L"network");
645 + return exception.Message() == expectedMessage;
646 + });
647 + }
648 +
649 + TEST_METHOD(CreateCommand_ParseNetworkAliasEmptyValue_ThrowsArgumentException)
650 + {
651 + auto invocation = CreateInvocationFromCommandLine(L"wslc --network name=net1,alias= ubuntu sh");
652 +
653 + ContainerCreateCommand command{L""};
654 + CLIExecutionContext context;
655 + command.ParseArguments(invocation, context.Args);
656 +
657 + VERIFY_THROWS_SPECIFIC(command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto& exception) {
658 + const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkAliasEmptyError(L"network");
659 + return exception.Message() == expectedMessage;
660 + });
661 }
662
663 TEST_METHOD(CreateCommand_ParseNetworkEmptyValue_ThrowsArgumentException)
@@ -477,6 +672,21 @@ class WSLCCLIExecutionUnitTests
672 command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
673 }
674
675 + TEST_METHOD(CreateCommand_SetContainerOptionsInvalidNetwork_ThrowsArgumentExceptionWithArgumentName)
676 + {
677 + auto invocation = CreateInvocationFromCommandLine(L"wslc --network name=net1,name=net2 ubuntu sh");
678 +
679 + ContainerCreateCommand command{L""};
680 + CLIExecutionContext context;
681 + command.ParseArguments(invocation, context.Args);
682 +
683 + VERIFY_THROWS_SPECIFIC(
684 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context), wsl::windows::wslc::ArgumentException, [](const auto& exception) {
685 + const auto expectedMessage = wsl::shared::Localization::WSLCCLI_NetworkDuplicateNameError(L"network");
686 + return exception.Message() == expectedMessage;
687 + });
688 + }
689 +
690 // Test: Command Line test parsing all cases defined in CommandLineTestCases.h
691 // This test verifies the command line parsing logic used by the CLI and executes the same
692 // code as the CLI up to the point of command execution, including parsing and argument validtion.
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+33
@@ -1111,6 +1111,39 @@ class WSLCE2EContainerCreateTests
1111 VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "db") != endpoint.Aliases.end());
1112 }
1113
1114 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_NetworkAlias_DockerStyleMultiNetwork_Success)
1115 + {
1116 + const auto secondNetworkName = TestNetworkName + L"-2";
1117 + EnsureNetworkDoesNotExist(secondNetworkName);
1118 +
1119 + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
1120 + result.Verify({.Stderr = L"", .ExitCode = 0});
1121 + auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
1122 +
1123 + result = RunWslc(std::format(L"network create --driver bridge {}", secondNetworkName));
1124 + result.Verify({.Stderr = L"", .ExitCode = 0});
1125 + auto cleanupSecondNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(secondNetworkName); });
1126 +
1127 + result = RunWslc(std::format(
1128 + L"container create --name {} --network name={},alias=db,alias=primary --network name={},alias=cache {} true",
1129 + WslcContainerName,
1130 + TestNetworkName,
1131 + secondNetworkName,
1132 + DebianImage.NameAndTag()));
1133 + result.Verify({.Stderr = L"", .ExitCode = 0});
1134 +
1135 + const auto inspect = InspectContainer(WslcContainerName);
1136 + const auto networkName = wsl::shared::string::WideToMultiByte(TestNetworkName);
1137 + const auto secondNetwork = wsl::shared::string::WideToMultiByte(secondNetworkName);
1138 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
1139 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(secondNetwork));
1140 + const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
1141 + const auto& secondEndpoint = inspect.NetworkSettings.Networks.at(secondNetwork);
1142 + VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "db") != endpoint.Aliases.end());
1143 + VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "primary") != endpoint.Aliases.end());
1144 + VERIFY_IS_TRUE(std::ranges::find(secondEndpoint.Aliases, "cache") != secondEndpoint.Aliases.end());
1145 + }
1146 +
1147 WSLC_TEST_METHOD(WSLCE2E_Container_Create_NetworkAlias_NoNetwork_Rejected)
1148 {
1149 auto result =
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+35
@@ -966,6 +966,41 @@ class WSLCE2EContainerRunTests
966 VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "db") != endpoint.Aliases.end());
967 }
968
969 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_NetworkAlias_DockerStyleMultiNetwork_Success)
970 + {
971 + const auto secondNetworkName = TestNetworkName + L"-2";
972 + EnsureNetworkDoesNotExist(secondNetworkName);
973 +
974 + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
975 + result.Verify({.Stderr = L"", .ExitCode = 0});
976 + auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
977 +
978 + result = RunWslc(std::format(L"network create --driver bridge {}", secondNetworkName));
979 + result.Verify({.Stderr = L"", .ExitCode = 0});
980 + auto cleanupSecondNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(secondNetworkName); });
981 +
982 + result = RunWslc(std::format(
983 + L"container run --name {} --network name={},alias=db,alias=primary "
984 + L"--network name={},alias=cache,alias=replica {} true",
985 + WslcContainerName,
986 + TestNetworkName,
987 + secondNetworkName,
988 + DebianImage.NameAndTag()));
989 + result.Verify({.Stderr = L"", .ExitCode = 0});
990 +
991 + const auto inspect = InspectContainer(WslcContainerName);
992 + const auto networkName = wsl::shared::string::WideToMultiByte(TestNetworkName);
993 + const auto secondNetwork = wsl::shared::string::WideToMultiByte(secondNetworkName);
994 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
995 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(secondNetwork));
996 + const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
997 + const auto& secondEndpoint = inspect.NetworkSettings.Networks.at(secondNetwork);
998 + VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "db") != endpoint.Aliases.end());
999 + VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, "primary") != endpoint.Aliases.end());
1000 + VERIFY_IS_TRUE(std::ranges::find(secondEndpoint.Aliases, "cache") != secondEndpoint.Aliases.end());
1001 + VERIFY_IS_TRUE(std::ranges::find(secondEndpoint.Aliases, "replica") != secondEndpoint.Aliases.end());
1002 + }
1003 +
1004 WSLC_TEST_METHOD(WSLCE2E_Container_Run_NetworkAlias_NoNetwork_Rejected)
1005 {
1006 auto result =