@samitouri / QOSAMI-WSL / commits / 5422fc47

Add --ip flag to container create and run (#41374)

beena352 committed Aug 19, 2026 at 12:23 UTC 5422fc47a17e75464e8e14cb74f64f35befe779d
11 files changed +242 -2
localization/strings/en-US/Resources.resw
+8
@@ -2496,6 +2496,14 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2496 <value>Network aliases cannot be specified when multiple networks are requested. Use a single --network option.</value>
2497 <comment>{Locked="--network "}Command line arguments, file names and string inserts should not be translated</comment>
2498 </data>
2499 + <data name="MessageWslcIpRequiresUserDefinedNetwork" xml:space="preserve">
2500 + <value>An IP address requires a user-defined network. Use --network to specify one.</value>
2501 + <comment>{Locked="--network "}Command line arguments, file names and string inserts should not be translated</comment>
2502 + </data>
2503 + <data name="MessageWslcIpAmbiguousWithMultipleNetworks" xml:space="preserve">
2504 + <value>An IP address cannot be specified when multiple networks are requested. Use a single --network option.</value>
2505 + <comment>{Locked="--network "}Command line arguments, file names and string inserts should not be translated</comment>
2506 + </data>
2507 <data name = "MessageWslcFailedToMountVolume" xml:space = "preserve" >
2508 <value>Failed to create volume '{}': {}</value>
2509 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/windows/common/WSLCContainerLauncher.cpp
+12 -2
@@ -336,6 +336,11 @@ void wsl::windows::common::WSLCContainerLauncher::AddPrimaryNetworkAlias(const s
336 m_primaryNetworkAliases.push_back(Alias);
337 }
338
339 +void wsl::windows::common::WSLCContainerLauncher::SetPrimaryNetworkIpAddress(std::string&& Address)
340 +{
341 + m_primaryNetworkIpAddress = std::move(Address);
342 +}
343 +
344 std::pair<HRESULT, std::optional<RunningWSLCContainer>> WSLCContainerLauncher::LaunchNoThrow(
345 IWSLCSession& Session, WSLCContainerStartFlags Flags, IWarningCallback* WarningCallback)
346 {
@@ -485,14 +490,19 @@ std::pair<HRESULT, std::optional<RunningWSLCContainer>> WSLCContainerLauncher::C
490 options.ContainerNetwork.Networks = connections.empty() ? nullptr : connections.data();
491 options.ContainerNetwork.NetworksCount = static_cast<ULONG>(connections.size());
492
488 - // Aliases for the primary endpoint.
493 + // Settings for the primary endpoint.
494 std::vector<KeyValuePair> primarySettings;
490 - primarySettings.reserve(m_primaryNetworkAliases.size());
495 + primarySettings.reserve(m_primaryNetworkAliases.size() + (m_primaryNetworkIpAddress.has_value() ? 1 : 0));
496 for (const auto& alias : m_primaryNetworkAliases)
497 {
498 primarySettings.push_back({.Key = "Aliases", .Value = alias.c_str()});
499 }
500
501 + if (m_primaryNetworkIpAddress.has_value())
502 + {
503 + primarySettings.push_back({.Key = "IPAddress", .Value = m_primaryNetworkIpAddress->c_str()});
504 + }
505 +
506 options.ContainerNetwork.Settings = primarySettings.empty() ? nullptr : primarySettings.data();
507 options.ContainerNetwork.SettingsCount = static_cast<ULONG>(primarySettings.size());
508
src/windows/common/WSLCContainerLauncher.h
+2
@@ -67,6 +67,7 @@ public:
67 void AddAdditionalNetwork(const std::string& Name);
68 void AddAdditionalNetwork(const std::string& Name, const std::vector<std::string>& Aliases);
69 void AddPrimaryNetworkAlias(const std::string& Alias);
70 + void SetPrimaryNetworkIpAddress(std::string&& Address);
71
72 std::pair<HRESULT, std::optional<RunningWSLCContainer>> CreateNoThrow(IWSLCSession& Session, IWarningCallback* WarningCallback = nullptr);
73 RunningWSLCContainer Create(IWSLCSession& Session, IWarningCallback* WarningCallback = nullptr);
@@ -133,6 +134,7 @@ private:
134 std::vector<std::string> m_dnsOptions;
135 std::vector<NetworkConnection> m_additionalNetworks;
136 std::vector<std::string> m_primaryNetworkAliases;
137 + std::optional<std::string> m_primaryNetworkIpAddress;
138 std::vector<WSLCLabel> m_labels;
139 std::deque<std::string> m_labelKeys;
140 std::deque<std::string> m_labelValues;
src/windows/wslc/commands/ContainerCreateCommand.cpp
+1
@@ -50,6 +50,7 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
50 Argument::Create(ArgType::HealthTimeout),
51 Argument::Create(ArgType::Hostname),
52 Argument::Create(ArgType::Interactive),
53 + Argument::Create(ArgType::IpAddress, false),
54 Argument::Create(ArgType::Label, false, Limit::Unlimited),
55 Argument::Create(ArgType::Memory),
56 Argument::Create(ArgType::Mount, false, Limit::Unlimited),
src/windows/wslc/commands/ContainerRunCommand.cpp
+1
@@ -50,6 +50,7 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
50 Argument::Create(ArgType::HealthTimeout),
51 Argument::Create(ArgType::Hostname),
52 Argument::Create(ArgType::Interactive),
53 + Argument::Create(ArgType::IpAddress, false),
54 Argument::Create(ArgType::Label, false, Limit::Unlimited),
55 Argument::Create(ArgType::Memory),
56 Argument::Create(ArgType::Mount, false, Limit::Unlimited),
src/windows/wslc/services/ContainerModel.h
+2
@@ -87,6 +87,8 @@ struct ContainerOptions
87 std::vector<std::string> DnsOptions;
88 std::vector<ContainerNetwork> Networks;
89 std::vector<std::string> NetworkAliases;
90 + std::optional<std::string> IpAddress{};
91 + std::vector<std::string> Tmpfs;
92 std::vector<std::pair<std::string, std::string>> Labels;
93 std::optional<std::wstring> CidFile{};
94 std::optional<int64_t> MemoryBytes{};
src/windows/wslc/services/ContainerService.cpp
+12
@@ -105,6 +105,18 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(Terminal& termi
105 }
106 }
107
108 + if (options.IpAddress.has_value())
109 + {
110 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcIpRequiresUserDefinedNetwork(), options.Networks.empty());
111 +
112 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcIpAmbiguousWithMultipleNetworks(), options.Networks.size() > 1);
113 +
114 + const auto& primary = options.Networks.front().Name;
115 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcIpRequiresUserDefinedNetwork(), !SupportsNetworkAliases(primary));
116 +
117 + containerLauncher.SetPrimaryNetworkIpAddress(std::string(options.IpAddress.value()));
118 + }
119 +
120 if (!options.Networks.empty())
121 {
122 const auto& primary = options.Networks.front();
src/windows/wslc/tasks/ContainerTasks.cpp
+5
@@ -822,6 +822,11 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
822 }
823 }
824
825 + if (context.Args.Contains(ArgType::IpAddress))
826 + {
827 + options.IpAddress = WideToMultiByte(context.Args.GetValue<ArgType::IpAddress>());
828 + }
829 +
830 if (context.Args.Contains(ArgType::User))
831 {
832 options.User = WideToMultiByte(context.Args.GetValue<ArgType::User>());
test/windows/WSLCTests.cpp
+38
@@ -8355,6 +8355,44 @@ class WSLCTests
8355 VERIFY_ARE_EQUAL(driverOptValue, opt->second);
8356 }
8357
8358 + // Launcher-driven pinned IP alongside an alias — both settings survive the same KVP batch.
8359 + {
8360 + const std::string networkName = "alias-net-ip";
8361 + const std::string ipAddress = "172.67.0.42";
8362 + const std::string alias = "db";
8363 + createNetwork(networkName, "172.67.0.0/16");
8364 + auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
8365 +
8366 + WSLCContainerLauncher launcher("debian:latest", "alias-ctr-ip", {"sleep", "99999"}, {}, networkName);
8367 + launcher.AddPrimaryNetworkAlias(alias);
8368 + launcher.SetPrimaryNetworkIpAddress(std::string(ipAddress));
8369 + auto container = launcher.Launch(*m_defaultSession);
8370 +
8371 + auto inspect = container.Inspect();
8372 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
8373 + const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
8374 + VERIFY_ARE_EQUAL(ipAddress, endpoint.IPAddress);
8375 + VERIFY_IS_TRUE(endpoint.IPAMConfig.has_value());
8376 + VERIFY_ARE_EQUAL(ipAddress, endpoint.IPAMConfig->IPv4Address);
8377 + VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, alias) != endpoint.Aliases.end());
8378 + }
8379 +
8380 + // Pinned IP outside a user-defined network — rejected for callers that bypass the CLI checks.
8381 + {
8382 + auto expectEndpointSettingsError = [&](const std::string& containerName, const std::string& networkMode) {
8383 + WSLCContainerLauncher launcher("debian:latest", containerName, {"sleep", "99999"}, {}, networkMode);
8384 + launcher.SetPrimaryNetworkIpAddress("172.67.0.42");
8385 +
8386 + auto retVal = launcher.LaunchNoThrow(*m_defaultSession);
8387 + VERIFY_ARE_EQUAL(E_INVALIDARG, retVal.first);
8388 + ValidateCOMErrorMessage(std::format(
8389 + L"Endpoint settings are not supported for network mode '{}'.", std::wstring(networkMode.begin(), networkMode.end())));
8390 + };
8391 +
8392 + expectEndpointSettingsError("alias-ctr-ip-host", "host");
8393 + expectEndpointSettingsError("alias-ctr-ip-none", "none");
8394 + }
8395 +
8396 // Primary endpoint Links: launch a target container with an alias, then a source with --link at create time.
8397 {
8398 const std::string networkName = "alias-net-link";
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+79
@@ -1410,6 +1410,85 @@ class WSLCE2EContainerCreateTests
1410 VerifyContainerIsNotListed(WslcContainerName);
1411 }
1412
1413 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ip_Success)
1414 + {
1415 + const std::wstring subnet = L"172.74.0.0/16";
1416 + const std::wstring ipAddress = L"172.74.0.42";
1417 +
1418 + auto result = RunWslc(std::format(L"network create --driver bridge --subnet {} {}", subnet, TestNetworkName));
1419 + result.Verify({.Stderr = L"", .ExitCode = 0});
1420 + auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
1421 +
1422 + result = RunWslc(std::format(
1423 + L"container create --name {} --network {} --ip {} {} true", WslcContainerName, TestNetworkName, ipAddress, DebianImage.NameAndTag()));
1424 + result.Verify({.Stderr = L"", .ExitCode = 0});
1425 +
1426 + const auto inspect = InspectContainer(WslcContainerName);
1427 + const auto networkName = wsl::shared::string::WideToMultiByte(TestNetworkName);
1428 + const auto expectedIp = wsl::shared::string::WideToMultiByte(ipAddress);
1429 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
1430 + const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
1431 + VERIFY_IS_TRUE(endpoint.IPAMConfig.has_value());
1432 + VERIFY_ARE_EQUAL(expectedIp, endpoint.IPAMConfig->IPv4Address);
1433 + }
1434 +
1435 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ip_NoNetwork_Rejected)
1436 + {
1437 + const std::wstring ipAddress = L"172.74.0.42";
1438 +
1439 + auto result =
1440 + RunWslc(std::format(L"container create --ip {} --name {} {} true", ipAddress, WslcContainerName, DebianImage.NameAndTag()));
1441 + result.Verify(
1442 + {.Stderr = std::format(L"{}\r\nError code: E_INVALIDARG\r\n", wsl::shared::Localization::MessageWslcIpRequiresUserDefinedNetwork()),
1443 + .ExitCode = 1});
1444 + VerifyContainerIsNotListed(WslcContainerName);
1445 + }
1446 +
1447 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ip_BridgeMode_Rejected)
1448 + {
1449 + const std::wstring ipAddress = L"172.74.0.42";
1450 +
1451 + auto result = RunWslc(std::format(
1452 + L"container create --network bridge --ip {} --name {} {} true", ipAddress, WslcContainerName, DebianImage.NameAndTag()));
1453 + result.Verify(
1454 + {.Stderr = std::format(L"{}\r\nError code: E_INVALIDARG\r\n", wsl::shared::Localization::MessageWslcIpRequiresUserDefinedNetwork()),
1455 + .ExitCode = 1});
1456 + VerifyContainerIsNotListed(WslcContainerName);
1457 + }
1458 +
1459 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ip_MultipleNetworks_Rejected)
1460 + {
1461 + const std::wstring ipAddress = L"172.74.0.42";
1462 +
1463 + auto result = RunWslc(std::format(
1464 + L"container create --network bridge --network bridge --ip {} --name {} {} true",
1465 + ipAddress,
1466 + WslcContainerName,
1467 + DebianImage.NameAndTag()));
1468 + result.Verify({.Stdout = L"", .ExitCode = 1});
1469 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
1470 + wsl::shared::Localization::MessageWslcIpAmbiguousWithMultipleNetworks() + L"\r\nError code: E_INVALIDARG"));
1471 + VerifyContainerIsNotListed(WslcContainerName);
1472 + }
1473 +
1474 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ip_InvalidValue_Rejected)
1475 + {
1476 + const std::wstring badIp = L"not-an-ip";
1477 +
1478 + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
1479 + result.Verify({.Stderr = L"", .ExitCode = 0});
1480 + auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
1481 +
1482 + result = RunWslc(std::format(
1483 + L"container create --network {} --ip {} --name {} {} true", TestNetworkName, badIp, WslcContainerName, DebianImage.NameAndTag()));
1484 + result.Verify({.Stdout = L"", .ExitCode = 1});
1485 + VERIFY_IS_TRUE(result.Stderr.has_value());
1486 + VerifyPatternMatch(
1487 + wsl::shared::string::WideToMultiByte(result.Stderr.value()),
1488 + std::format("*Invalid IP address '{}'*", wsl::shared::string::WideToMultiByte(badIp)));
1489 + VerifyContainerIsNotListed(WslcContainerName);
1490 + }
1491 +
1492 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Cpus)
1493 {
1494 auto result = RunWslc(std::format(L"container create --name {} --cpus 0.5 {} true", WslcContainerName, DebianImage.NameAndTag()));
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+82
@@ -1041,6 +1041,88 @@ class WSLCE2EContainerRunTests
1041 result.StderrContainsSubstring(L"Invalid network-alias value: network alias cannot be empty or whitespace"));
1042 }
1043
1044 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ip_Success)
1045 + {
1046 + const std::wstring subnet = L"172.73.0.0/16";
1047 + const std::wstring ipAddress = L"172.73.0.42";
1048 +
1049 + auto result = RunWslc(std::format(L"network create --driver bridge --subnet {} {}", subnet, TestNetworkName));
1050 + result.Verify({.Stderr = L"", .ExitCode = 0});
1051 + auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
1052 +
1053 + result = RunWslc(std::format(
1054 + L"container run -d --name {} --network {} --ip {} {} sleep infinity",
1055 + WslcContainerName,
1056 + TestNetworkName,
1057 + ipAddress,
1058 + DebianImage.NameAndTag()));
1059 + result.Verify({.Stderr = L"", .ExitCode = 0});
1060 + // Registered after the network so it runs first; the network cannot be deleted while the container holds an endpoint.
1061 + auto cleanupContainer = wil::scope_exit([&] { EnsureContainerDoesNotExist(WslcContainerName); });
1062 +
1063 + const auto inspect = InspectContainer(WslcContainerName);
1064 + const auto networkName = wsl::shared::string::WideToMultiByte(TestNetworkName);
1065 + const auto expectedIp = wsl::shared::string::WideToMultiByte(ipAddress);
1066 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
1067 + const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
1068 + VERIFY_ARE_EQUAL(expectedIp, endpoint.IPAddress);
1069 + VERIFY_IS_TRUE(endpoint.IPAMConfig.has_value());
1070 + VERIFY_ARE_EQUAL(expectedIp, endpoint.IPAMConfig->IPv4Address);
1071 + }
1072 +
1073 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ip_NoNetwork_Rejected)
1074 + {
1075 + const std::wstring ipAddress = L"172.73.0.42";
1076 +
1077 + auto result =
1078 + RunWslc(std::format(L"container run --rm --ip {} --name {} {} true", ipAddress, WslcContainerName, DebianImage.NameAndTag()));
1079 + result.Verify(
1080 + {.Stderr = std::format(L"{}\r\nError code: E_INVALIDARG\r\n", wsl::shared::Localization::MessageWslcIpRequiresUserDefinedNetwork()),
1081 + .ExitCode = 1});
1082 + }
1083 +
1084 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ip_BridgeMode_Rejected)
1085 + {
1086 + const std::wstring ipAddress = L"172.73.0.42";
1087 +
1088 + auto result = RunWslc(std::format(
1089 + L"container run --rm --network bridge --ip {} --name {} {} true", ipAddress, WslcContainerName, DebianImage.NameAndTag()));
1090 + result.Verify(
1091 + {.Stderr = std::format(L"{}\r\nError code: E_INVALIDARG\r\n", wsl::shared::Localization::MessageWslcIpRequiresUserDefinedNetwork()),
1092 + .ExitCode = 1});
1093 + }
1094 +
1095 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ip_MultipleNetworks_Rejected)
1096 + {
1097 + const std::wstring ipAddress = L"172.73.0.42";
1098 +
1099 + auto result = RunWslc(std::format(
1100 + L"container run --rm --network bridge --network bridge --ip {} --name {} {} true",
1101 + ipAddress,
1102 + WslcContainerName,
1103 + DebianImage.NameAndTag()));
1104 + result.Verify({.Stdout = L"", .ExitCode = 1});
1105 + VERIFY_IS_TRUE(result.StderrContainsSubstring(
1106 + wsl::shared::Localization::MessageWslcIpAmbiguousWithMultipleNetworks() + L"\r\nError code: E_INVALIDARG"));
1107 + }
1108 +
1109 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ip_InvalidValue_Rejected)
1110 + {
1111 + const std::wstring badIp = L"not-an-ip";
1112 +
1113 + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
1114 + result.Verify({.Stderr = L"", .ExitCode = 0});
1115 + auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
1116 +
1117 + result = RunWslc(std::format(
1118 + L"container run --rm --network {} --ip {} --name {} {} true", TestNetworkName, badIp, WslcContainerName, DebianImage.NameAndTag()));
1119 + result.Verify({.Stdout = L"", .ExitCode = 1});
1120 + VERIFY_IS_TRUE(result.Stderr.has_value());
1121 + VerifyPatternMatch(
1122 + wsl::shared::string::WideToMultiByte(result.Stderr.value()),
1123 + std::format("*Invalid IP address '{}'*", wsl::shared::string::WideToMultiByte(badIp)));
1124 + }
1125 +
1126 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Volume_NamedVolume_Success)
1127 {
1128 // Create a named volume