Add --cpus, --memory, and --ulimit options to wslc container create/run (#40727)
* Init resources * Fix test * Clang format * Simplify parse methods * Resolve copilot comment
AmirMS committed
Jun 8, 2026 at 12:41 UTC
e807fd7f41d254f2a73c98e109cc112e19e05b04
12 files changed
+366
localization/strings/en-US/Resources.resw
+20
@@ -2708,6 +2708,14 @@ On first run, creates the file with all settings commented out at their defaults
2708
<data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2709
<value>The command to run</value>
2710
</data>
2711
+ <data name="WSLCCLI_CpusArgDescription" xml:space="preserve">
2712
+ <value>Number of CPUs (e.g. 0.5, 1, 2.5)</value>
2713
+ <comment>{Locked="0.5"}{Locked="1"}{Locked="2.5"}Command line argument example values should not be translated</comment>
2714
+ </data>
2715
+ <data name="WSLCCLI_InvalidCpusError" xml:space="preserve">
2716
+ <value>Invalid {} argument value: '{}'. Expected a positive number of CPUs (e.g. 0.5, 1, 2)</value>
2717
+ <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="0.5"}{Locked="1"}{Locked="2"}</comment>
2718
+ </data>
2719
<data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2720
<value>Delete containers even if they are running</value>
2721
</data>
@@ -2777,6 +2785,10 @@ On first run, creates the file with all settings commented out at their defaults
2785
<data name="WSLCCLI_LatestArgDescription" xml:space="preserve">
2786
<value>Show the latest created container (includes all states)</value>
2787
</data>
2788
+ <data name="WSLCCLI_MemoryArgDescription" xml:space="preserve">
2789
+ <value>Memory limit (e.g. 512M, 1G)</value>
2790
+ <comment>{Locked="512M"}{Locked="1G"}Command line argument example values should not be translated</comment>
2791
+ </data>
2792
<data name="WSLCCLI_HostnameArgDescription" xml:space="preserve">
2793
<value>Container host name</value>
2794
</data>
@@ -2910,6 +2922,14 @@ On first run, creates the file with all settings commented out at their defaults
2922
<value>Mount tmpfs to the container at the given path</value>
2923
<comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2924
</data>
2925
+ <data name="WSLCCLI_UlimitArgDescription" xml:space="preserve">
2926
+ <value>Ulimit options (format: <name>=<soft>[:<hard>], use -1 for unlimited)</value>
2927
+ <comment>{Locked="-1"}{Locked="<name>=<soft>[:<hard>]"}Command line arguments should not be translated</comment>
2928
+ </data>
2929
+ <data name="WSLCCLI_InvalidUlimitError" xml:space="preserve">
2930
+ <value>Invalid {} argument value: '{}'. Expected <name>=<soft>[:<hard>] (use -1 for unlimited)</value>
2931
+ <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="-1"}{Locked="<name>=<soft>[:<hard>]"}</comment>
2932
+ </data>
2933
<data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2934
<value>User ID for the process (name|uid|uid:gid)</value>
2935
<comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
src/windows/wslc/arguments/ArgumentDefinitions.h
+3
@@ -41,6 +41,7 @@ _(BuildTarget, "target", NO_ALIAS, Kind::Value, L
41
_(CIDFile, "cidfile", NO_ALIAS, Kind::Value, Localization::WSLCCLI_CIDFileArgDescription()) \
42
_(Command, "command", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_CommandArgDescription()) \
43
_(ContainerId, "container-id", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ContainerIdArgDescription()) \
44
+_(Cpus, "cpus", NO_ALIAS, Kind::Value, Localization::WSLCCLI_CpusArgDescription()) \
45
_(Force, "force", L"f", Kind::Flag, Localization::WSLCCLI_ForceArgDescription()) \
46
_(Detach, "detach", L"d", Kind::Flag, Localization::WSLCCLI_DetachArgDescription()) \
47
_(DNS, "dns", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DNSArgDescription()) \
@@ -72,6 +73,7 @@ _(Interactive, "interactive", L"i", Kind::Flag, L
73
_(Label, "label", L"l", Kind::Value, Localization::WSLCCLI_LabelArgDescription()) \
74
_(Last, "last", L"n", Kind::Value, Localization::WSLCCLI_LastArgDescription()) \
75
_(Latest, "latest", L"l", Kind::Flag, Localization::WSLCCLI_LatestArgDescription()) \
76
+_(Memory, "memory", L"m", Kind::Value, Localization::WSLCCLI_MemoryArgDescription()) \
77
_(Name, "name", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NameArgDescription()) \
78
_(NetworkName, "network-name", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_NetworkNameArgDescription()) \
79
/*_(NoDNS, "no-dns", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoDNSArgDescription())*/ \
@@ -106,6 +108,7 @@ _(Time, "time", L"t", Kind::Value, L
108
_(TMPFS, "tmpfs", NO_ALIAS, Kind::Value, Localization::WSLCCLI_TMPFSArgDescription()) \
109
_(TTY, "tty", L"t", Kind::Flag, Localization::WSLCCLI_TTYArgDescription()) \
110
_(Type, "type", L"t", Kind::Value, Localization::WSLCCLI_TypeArgDescription()) \
111
+_(Ulimit, "ulimit", NO_ALIAS, Kind::Value, Localization::WSLCCLI_UlimitArgDescription()) \
112
_(User, "user", L"u", Kind::Value, Localization::WSLCCLI_UserArgDescription()) \
113
_(Username, "username", L"u", Kind::Value, Localization::WSLCCLI_LoginUsernameArgDescription()) \
114
_(Verbose, "verbose", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_VerboseArgDescription()) \
src/windows/wslc/arguments/ArgumentValidation.cpp
+91
@@ -11,6 +11,8 @@ Abstract:
11
Implementation of the Argument Validation.
12
13
--*/
14
+
15
+#include "precomp.h"
16
#include "Argument.h"
17
#include "ArgumentTypes.h"
18
#include "ArgumentValidation.h"
@@ -48,6 +50,18 @@ void Argument::Validate(const ArgMap& execArgs) const
50
validation::ValidateMemorySize(execArgs.GetAll<ArgType::ShmSize>(), m_name);
51
break;
52
53
+ case ArgType::Memory:
54
+ validation::ValidateMemorySize(execArgs.GetAll<ArgType::Memory>(), m_name);
55
+ break;
56
+
57
+ case ArgType::Cpus:
58
+ validation::ValidateNanoCpus(execArgs.GetAll<ArgType::Cpus>(), m_name);
59
+ break;
60
+
61
+ case ArgType::Ulimit:
62
+ validation::ValidateUlimit(execArgs.GetAll<ArgType::Ulimit>(), m_name);
63
+ break;
64
+
65
case ArgType::Tail:
66
validation::ValidateIntegerFromString<ULONGLONG>(
67
execArgs.GetAll<ArgType::Tail>(), m_name, [](auto value) { return value != 0; });
@@ -271,6 +285,83 @@ int64_t GetMemorySizeFromString(const std::wstring& input, const std::wstring& a
285
return static_cast<int64_t>(parsed.value());
286
}
287
288
+void ValidateNanoCpus(const std::vector<std::wstring>& values, const std::wstring& argName)
289
+{
290
+ for (const auto& value : values)
291
+ {
292
+ std::ignore = GetNanoCpusFromString(value, argName);
293
+ }
294
+}
295
+
296
+int64_t GetNanoCpusFromString(const std::wstring& input, const std::wstring& argName)
297
+{
298
+ constexpr double NanosPerCpu = 1'000'000'000.0;
299
+ constexpr double MaxCpus = static_cast<double>(std::numeric_limits<int64_t>::max()) / NanosPerCpu;
300
+
301
+ const std::string narrow = WideToMultiByte(input);
302
+ const char* begin = narrow.c_str();
303
+ const char* end = begin + narrow.size();
304
+
305
+ double cpus{};
306
+ const auto result = std::from_chars(begin, end, cpus, std::chars_format::fixed);
307
+ if (result.ec != std::errc() || result.ptr != end || cpus <= 0.0 || cpus > MaxCpus)
308
+ {
309
+ throw ArgumentException(Localization::WSLCCLI_InvalidCpusError(argName, input));
310
+ }
311
+
312
+ return static_cast<int64_t>(cpus * NanosPerCpu);
313
+}
314
+
315
+void ValidateUlimit(const std::vector<std::wstring>& values, const std::wstring& argName)
316
+{
317
+ for (const auto& value : values)
318
+ {
319
+ std::ignore = ParseUlimit(value, argName);
320
+ }
321
+}
322
+
323
+std::tuple<std::string, int64_t, int64_t> ParseUlimit(const std::wstring& input, const std::wstring& argName)
324
+{
325
+ // Accepts <name>=<soft>[:<hard>]; if hard is omitted hard = soft. -1 means unlimited.
326
+ const auto equalsPos = input.find(L'=');
327
+ if (equalsPos == std::wstring::npos || equalsPos == 0)
328
+ {
329
+ throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input));
330
+ }
331
+
332
+ const std::wstring valuesPart = input.substr(equalsPos + 1);
333
+ const auto colonPos = valuesPart.find(L':');
334
+
335
+ auto parseLimit = [&](const std::wstring& limitStr) -> int64_t {
336
+ if (limitStr.empty())
337
+ {
338
+ throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input));
339
+ }
340
+
341
+ try
342
+ {
343
+ return GetIntegerFromString<int64_t>(limitStr, argName, [](int64_t v) { return v >= -1; });
344
+ }
345
+ catch (const ArgumentException&)
346
+ {
347
+ // Re-throw with the ulimit-specific error message so the user sees the full input.
348
+ throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input));
349
+ }
350
+ };
351
+
352
+ const int64_t soft = parseLimit(colonPos == std::wstring::npos ? valuesPart : valuesPart.substr(0, colonPos));
353
+ const int64_t hard = colonPos == std::wstring::npos ? soft : parseLimit(valuesPart.substr(colonPos + 1));
354
+
355
+ // This rejects "-1:1024" and "-1:<finite>" while allowing "<finite>:-1", "-1:-1", and "-1".
356
+ const bool invalidRange = (soft == -1) ? (hard != -1) : (hard != -1 && hard < soft);
357
+ if (invalidRange)
358
+ {
359
+ throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input));
360
+ }
361
+
362
+ return {WideToMultiByte(input.substr(0, equalsPos)), soft, hard};
363
+}
364
+
365
std::pair<std::string, std::string> ParseLabel(const std::wstring& value)
366
{
367
std::pair<std::string, std::string> result{};
src/windows/wslc/arguments/ArgumentValidation.h
+7
@@ -17,6 +17,7 @@ Abstract:
17
#include "ContainerModel.h"
18
#include "InspectModel.h"
19
#include <string>
20
+#include <tuple>
21
#include <vector>
22
#include <charconv>
23
#include <wslc.h>
@@ -65,6 +66,12 @@ WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring
66
void ValidateMemorySize(const std::vector<std::wstring>& values, const std::wstring& argName);
67
int64_t GetMemorySizeFromString(const std::wstring& input, const std::wstring& argName = {});
68
69
+void ValidateNanoCpus(const std::vector<std::wstring>& values, const std::wstring& argName);
70
+int64_t GetNanoCpusFromString(const std::wstring& input, const std::wstring& argName = {});
71
+
72
+void ValidateUlimit(const std::vector<std::wstring>& values, const std::wstring& argName);
73
+std::tuple<std::string, int64_t, int64_t> ParseUlimit(const std::wstring& input, const std::wstring& argName = {});
74
+
75
void ValidateFormatTypeFromString(const std::vector<std::wstring>& values, const std::wstring& argName);
76
FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring& argName = {});
77
src/windows/wslc/commands/ContainerCreateCommand.cpp
+3
@@ -32,6 +32,7 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
32
Argument::Create(ArgType::Command),
33
Argument::Create(ArgType::ForwardArgs),
34
Argument::Create(ArgType::CIDFile),
35
+ Argument::Create(ArgType::Cpus),
36
Argument::Create(ArgType::DNS, false, NO_LIMIT),
37
// Argument::Create(ArgType::DNSDomain),
38
Argument::Create(ArgType::DNSOption, false, NO_LIMIT),
@@ -45,6 +46,7 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
46
Argument::Create(ArgType::Hostname),
47
Argument::Create(ArgType::Interactive),
48
Argument::Create(ArgType::Label, false, NO_LIMIT),
49
+ Argument::Create(ArgType::Memory),
50
Argument::Create(ArgType::Name),
51
// Argument::Create(ArgType::NoDNS),
52
// Argument::Create(ArgType::Progress),
@@ -57,6 +59,7 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
59
Argument::Create(ArgType::StopSignal),
60
Argument::Create(ArgType::TMPFS, false, NO_LIMIT),
61
Argument::Create(ArgType::TTY),
62
+ Argument::Create(ArgType::Ulimit, false, NO_LIMIT),
63
Argument::Create(ArgType::User),
64
Argument::Create(ArgType::Volume, false, NO_LIMIT),
65
// Argument::Create(ArgType::Virtual),
src/windows/wslc/commands/ContainerRunCommand.cpp
+3
@@ -32,6 +32,7 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
32
Argument::Create(ArgType::Command),
33
Argument::Create(ArgType::ForwardArgs),
34
Argument::Create(ArgType::CIDFile),
35
+ Argument::Create(ArgType::Cpus),
36
Argument::Create(ArgType::Detach),
37
Argument::Create(ArgType::DNS, false, NO_LIMIT),
38
// Argument::Create(ArgType::DNSDomain),
@@ -45,6 +46,7 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
46
Argument::Create(ArgType::Hostname),
47
Argument::Create(ArgType::Interactive),
48
Argument::Create(ArgType::Label, false, NO_LIMIT),
49
+ Argument::Create(ArgType::Memory),
50
Argument::Create(ArgType::Name),
51
// Argument::Create(ArgType::NoDNS),
52
// Argument::Create(ArgType::Progress),
@@ -58,6 +60,7 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
60
Argument::Create(ArgType::StopSignal),
61
Argument::Create(ArgType::TMPFS, false, NO_LIMIT),
62
Argument::Create(ArgType::TTY),
63
+ Argument::Create(ArgType::Ulimit, false, NO_LIMIT),
64
Argument::Create(ArgType::User),
65
Argument::Create(ArgType::Volume, false, NO_LIMIT),
66
// Argument::Create(ArgType::Virtual),
src/windows/wslc/services/ContainerModel.h
+3
@@ -53,6 +53,9 @@ struct ContainerOptions
53
std::vector<std::string> Tmpfs;
54
std::vector<std::pair<std::string, std::string>> Labels;
55
std::optional<std::wstring> CidFile{};
56
+ std::optional<int64_t> MemoryBytes{};
57
+ std::optional<int64_t> NanoCpus{};
58
+ std::vector<std::tuple<std::string, int64_t, int64_t>> Ulimits;
59
};
60
61
struct CreateContainerResult
src/windows/wslc/services/ContainerService.cpp
+15
@@ -109,6 +109,21 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(
109
containerLauncher.SetShmSize(options.ShmSize.value());
110
}
111
112
+ if (options.MemoryBytes.has_value())
113
+ {
114
+ containerLauncher.SetMemoryLimit(options.MemoryBytes.value());
115
+ }
116
+
117
+ if (options.NanoCpus.has_value())
118
+ {
119
+ containerLauncher.SetNanoCpus(options.NanoCpus.value());
120
+ }
121
+
122
+ for (const auto& [name, soft, hard] : options.Ulimits)
123
+ {
124
+ containerLauncher.AddUlimit(name, soft, hard);
125
+ }
126
+
127
if (!options.Entrypoint.empty())
128
{
129
auto entrypoints = options.Entrypoint;
src/windows/wslc/tasks/ContainerTasks.cpp
+18
@@ -422,6 +422,24 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
422
options.ShmSize = validation::GetMemorySizeFromString(context.Args.Get<ArgType::ShmSize>());
423
}
424
425
+ if (context.Args.Contains(ArgType::Memory))
426
+ {
427
+ options.MemoryBytes = validation::GetMemorySizeFromString(context.Args.Get<ArgType::Memory>());
428
+ }
429
+
430
+ if (context.Args.Contains(ArgType::Cpus))
431
+ {
432
+ options.NanoCpus = validation::GetNanoCpusFromString(context.Args.Get<ArgType::Cpus>());
433
+ }
434
+
435
+ if (context.Args.Contains(ArgType::Ulimit))
436
+ {
437
+ for (const auto& value : context.Args.GetAll<ArgType::Ulimit>())
438
+ {
439
+ options.Ulimits.emplace_back(validation::ParseUlimit(value));
440
+ }
441
+ }
442
+
443
if (context.Args.Contains(ArgType::Command))
444
{
445
options.Arguments.emplace_back(WideToMultiByte(context.Args.Get<ArgType::Command>()));
test/windows/wslc/WSLCCLIResourceLimitsParserUnitTests.cpp
new
+129
@@ -0,0 +1,129 @@
1
+/*++
2
+
3
+Copyright (c) Microsoft. All rights reserved.
4
+
5
+Module Name:
6
+
7
+ WSLCCLIResourceLimitsParserUnitTests.cpp
8
+
9
+Abstract:
10
+
11
+ This file contains unit tests for WSLC CLI resource-limit (--cpus, --memory, --ulimit) parsing and validation.
12
+
13
+--*/
14
+
15
+#include "precomp.h"
16
+#include "windows/Common.h"
17
+#include "WSLCCLITestHelpers.h"
18
+#include "ArgumentValidation.h"
19
+
20
+using namespace wsl::windows::wslc;
21
+
22
+namespace WSLCCLIResourceLimitsParserUnitTests {
23
+
24
+class WSLCCLIResourceLimitsParserUnitTests
25
+{
26
+ WSLC_TEST_CLASS(WSLCCLIResourceLimitsParserUnitTests)
27
+
28
+ TEST_METHOD(NanoCpus_Valid)
29
+ {
30
+ // (input, expected nanoCpus)
31
+ std::vector<std::pair<std::wstring, int64_t>> valid = {
32
+ {L"1", 1'000'000'000LL},
33
+ {L"2", 2'000'000'000LL},
34
+ {L"0.5", 500'000'000LL},
35
+ {L"1.5", 1'500'000'000LL},
36
+ {L"2.5", 2'500'000'000LL},
37
+ {L"0.001", 1'000'000LL},
38
+ };
39
+
40
+ for (const auto& [input, expected] : valid)
41
+ {
42
+ const auto actual = validation::GetNanoCpusFromString(input, L"cpus");
43
+ VERIFY_ARE_EQUAL(expected, actual);
44
+ }
45
+ }
46
+
47
+ TEST_METHOD(NanoCpus_Invalid)
48
+ {
49
+ // Each value should be rejected as an invalid --cpus value.
50
+ const std::vector<std::wstring> invalid = {
51
+ L"",
52
+ L"0", // not positive
53
+ L"-1", // sign char rejected
54
+ L"-0.5", // sign char rejected
55
+ L"abc", // not numeric
56
+ L"1.5x", // trailing garbage
57
+ L" 1", // leading whitespace
58
+ L"1 ", // trailing whitespace
59
+ L"1e3", // exponent not allowed
60
+ L"+1", // sign char rejected
61
+ L"1.2.3", // multiple dots (rejected by strtod's strict end check)
62
+ L"99999999999" // overflow when multiplied by 1e9
63
+ };
64
+
65
+ for (const auto& input : invalid)
66
+ {
67
+ VERIFY_THROWS(validation::GetNanoCpusFromString(input, L"cpus"), ArgumentException);
68
+ }
69
+ }
70
+
71
+ TEST_METHOD(Ulimit_Valid)
72
+ {
73
+ // (input, expectedName, expectedSoft, expectedHard)
74
+ std::vector<std::tuple<std::wstring, std::string, int64_t, int64_t>> valid = {
75
+ {L"nofile=1024", "nofile", 1024, 1024},
76
+ {L"nofile=1024:2048", "nofile", 1024, 2048},
77
+ {L"nproc=512:512", "nproc", 512, 512},
78
+ {L"core=-1", "core", -1, -1},
79
+ {L"core=-1:-1", "core", -1, -1},
80
+ {L"memlock=0", "memlock", 0, 0},
81
+ {L"stack=8192:-1", "stack", 8192, -1},
82
+ };
83
+
84
+ for (const auto& [input, expectedName, expectedSoft, expectedHard] : valid)
85
+ {
86
+ const auto [name, soft, hard] = validation::ParseUlimit(input, L"ulimit");
87
+ VERIFY_ARE_EQUAL(expectedName, name);
88
+ VERIFY_ARE_EQUAL(expectedSoft, soft);
89
+ VERIFY_ARE_EQUAL(expectedHard, hard);
90
+ }
91
+ }
92
+
93
+ TEST_METHOD(Ulimit_Invalid)
94
+ {
95
+ const std::vector<std::wstring> invalid = {
96
+ L"",
97
+ L"=1024", // empty name
98
+ L"nofile=", // empty value
99
+ L"nofile", // missing '='
100
+ L"nofile=abc", // non-numeric soft
101
+ L"nofile=1024:", // empty hard
102
+ L"nofile=:1024", // empty soft
103
+ L"nofile=-2", // negative other than -1
104
+ L"nofile=1024:512", // hard < soft (and both positive)
105
+ L"nofile=-1:1024", // unlimited soft but limited hard
106
+ L"nofile=-1:9223372036854775807", // unlimited soft but finite (INT64_MAX) hard
107
+ L"nofile=1.5", // not integer
108
+ };
109
+
110
+ for (const auto& input : invalid)
111
+ {
112
+ VERIFY_THROWS(validation::ParseUlimit(input, L"ulimit"), ArgumentException);
113
+ }
114
+ }
115
+
116
+ TEST_METHOD(NanoCpus_Validator)
117
+ {
118
+ VERIFY_NO_THROW(validation::ValidateNanoCpus({L"0.5", L"1", L"2.5"}, L"cpus"));
119
+ VERIFY_THROWS(validation::ValidateNanoCpus({L"1", L"0"}, L"cpus"), ArgumentException);
120
+ }
121
+
122
+ TEST_METHOD(Ulimit_Validator)
123
+ {
124
+ VERIFY_NO_THROW(validation::ValidateUlimit({L"nofile=1024", L"core=-1"}, L"ulimit"));
125
+ VERIFY_THROWS(validation::ValidateUlimit({L"nofile=1024", L"bad"}, L"ulimit"), ArgumentException);
126
+ }
127
+};
128
+
129
+} // namespace WSLCCLIResourceLimitsParserUnitTests
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+3
@@ -828,6 +828,7 @@ private:
828
std::wstringstream options;
829
options << L"The following options are available:\r\n" //
830
<< L" --cidfile Write the container ID to the provided path\r\n"
831
+ << L" --cpus Number of CPUs (e.g. 0.5, 1, 2.5)\r\n"
832
<< L" --dns IP address of the DNS nameserver in resolv.conf\r\n"
833
<< L" --dns-option Set DNS options\r\n"
834
<< L" --dns-search Set DNS search domains\r\n"
@@ -839,6 +840,7 @@ private:
840
<< L" -h,--hostname Container host name\r\n"
841
<< L" -i,--interactive Attach to stdin and keep it open\r\n"
842
<< L" -l,--label Set metadata on an object\r\n"
843
+ << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n"
844
<< L" --name Name of the container\r\n"
845
<< L" -p,--publish Publish a port from a container to host\r\n"
846
<< L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
@@ -848,6 +850,7 @@ private:
850
<< L" --stop-signal Signal to stop the container\r\n"
851
<< L" --tmpfs Mount tmpfs to the container at the given path\r\n"
852
<< L" -t,--tty Open a TTY with the container process.\r\n"
853
+ << L" --ulimit Ulimit options (format: <name>=<soft>[:<hard>], use -1 for unlimited)\r\n"
854
<< L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
855
<< L" -v,--volume Bind mount a volume to the container\r\n"
856
<< L" -w,--workdir Working directory inside the container\r\n"
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+71
@@ -731,6 +731,74 @@ class WSLCE2EContainerRunTests
731
}
732
}
733
734
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Cpus)
735
+ {
736
+ auto result = RunWslc(std::format(L"container run --name {} --cpus 1.5 {} true", WslcContainerName, DebianImage.NameAndTag()));
737
+ result.Verify({.Stderr = L"", .ExitCode = 0});
738
+
739
+ const auto inspect = InspectContainer(WslcContainerName);
740
+ VERIFY_ARE_EQUAL(static_cast<int64_t>(1'500'000'000), inspect.HostConfig.NanoCpus);
741
+ }
742
+
743
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Memory)
744
+ {
745
+ auto result = RunWslc(std::format(L"container run --name {} --memory 32M {} true", WslcContainerName, DebianImage.NameAndTag()));
746
+ // Note: stderr is not asserted here because some kernels emit a swap-limit warning
747
+ // ("Your kernel does not support swap limit capabilities...") when a memory limit is set.
748
+ result.Verify({.ExitCode = 0});
749
+
750
+ const auto inspect = InspectContainer(WslcContainerName);
751
+ VERIFY_ARE_EQUAL(static_cast<int64_t>(32) * 1024 * 1024, inspect.HostConfig.Memory);
752
+ }
753
+
754
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ulimit)
755
+ {
756
+ auto result = RunWslc(std::format(
757
+ L"container run --name {} --ulimit nofile=1024:2048 --ulimit nproc=512 {} true", WslcContainerName, DebianImage.NameAndTag()));
758
+ result.Verify({.Stderr = L"", .ExitCode = 0});
759
+
760
+ const auto inspect = InspectContainer(WslcContainerName);
761
+ VERIFY_ARE_EQUAL(static_cast<size_t>(2), inspect.HostConfig.Ulimits.size());
762
+
763
+ std::map<std::string, std::pair<int64_t, int64_t>> byName;
764
+ for (const auto& ul : inspect.HostConfig.Ulimits)
765
+ {
766
+ byName[ul.Name] = {ul.Soft, ul.Hard};
767
+ }
768
+
769
+ VERIFY_IS_TRUE(byName.contains("nofile"));
770
+ VERIFY_ARE_EQUAL(static_cast<int64_t>(1024), byName["nofile"].first);
771
+ VERIFY_ARE_EQUAL(static_cast<int64_t>(2048), byName["nofile"].second);
772
+
773
+ VERIFY_IS_TRUE(byName.contains("nproc"));
774
+ VERIFY_ARE_EQUAL(static_cast<int64_t>(512), byName["nproc"].first);
775
+ VERIFY_ARE_EQUAL(static_cast<int64_t>(512), byName["nproc"].second);
776
+ }
777
+
778
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Cpus_Invalid)
779
+ {
780
+ auto result = RunWslc(std::format(L"container run --rm --cpus 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
781
+ result.Verify({.Stderr = L"Invalid cpus argument value: '0'. Expected a positive number of CPUs (e.g. 0.5, 1, 2)\r\n", .ExitCode = 1});
782
+ EnsureContainerDoesNotExist(WslcContainerName);
783
+ }
784
+
785
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Memory_Invalid)
786
+ {
787
+ auto result =
788
+ RunWslc(std::format(L"container run --rm --memory invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
789
+ result.Verify({.Stderr = L"Invalid memory argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)\r\n", .ExitCode = 1});
790
+ EnsureContainerDoesNotExist(WslcContainerName);
791
+ }
792
+
793
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ulimit_Invalid)
794
+ {
795
+ auto result =
796
+ RunWslc(std::format(L"container run --rm --ulimit nofile --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
797
+ result.Verify(
798
+ {.Stderr = L"Invalid ulimit argument value: 'nofile'. Expected <name>=<soft>[:<hard>] (use -1 for unlimited)\r\n", .ExitCode = 1});
799
+ EnsureContainerDoesNotExist(WslcContainerName);
800
+ }
801
+
802
WSLC_TEST_METHOD(WSLCE2E_Container_Run_StopSignal_Invalid)
803
{
804
{
@@ -820,6 +888,7 @@ private:
888
std::wstringstream options;
889
options << L"The following options are available:\r\n"
890
<< L" --cidfile Write the container ID to the provided path\r\n"
891
+ << L" --cpus Number of CPUs (e.g. 0.5, 1, 2.5)\r\n"
892
<< L" -d,--detach Run container in detached mode\r\n"
893
<< L" --dns IP address of the DNS nameserver in resolv.conf\r\n"
894
<< L" --dns-option Set DNS options\r\n"
@@ -832,6 +901,7 @@ private:
901
<< L" -h,--hostname Container host name\r\n"
902
<< L" -i,--interactive Attach to stdin and keep it open\r\n"
903
<< L" -l,--label Set metadata on an object\r\n"
904
+ << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n"
905
<< L" --name Name of the container\r\n"
906
<< L" -p,--publish Publish a port from a container to host\r\n"
907
<< L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
@@ -841,6 +911,7 @@ private:
911
<< L" --stop-signal Signal to stop the container\r\n"
912
<< L" --tmpfs Mount tmpfs to the container at the given path\r\n"
913
<< L" -t,--tty Open a TTY with the container process.\r\n"
914
+ << L" --ulimit Ulimit options (format: <name>=<soft>[:<hard>], use -1 for unlimited)\r\n"
915
<< L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
916
<< L" -v,--volume Bind mount a volume to the container\r\n"
917
<< L" -w,--workdir Working directory inside the container\r\n"