diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index d08df400b..02f59cb9a 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2701,6 +2701,14 @@ On first run, creates the file with all settings commented out at their defaults The command to run + + Number of CPUs (e.g. 0.5, 1, 2.5) + {Locked="0.5"}{Locked="1"}{Locked="2.5"}Command line argument example values should not be translated + + + Invalid {} argument value: '{}'. Expected a positive number of CPUs (e.g. 0.5, 1, 2) + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="0.5"}{Locked="1"}{Locked="2"} + Delete containers even if they are running @@ -2770,6 +2778,10 @@ On first run, creates the file with all settings commented out at their defaults Show the latest created container (includes all states) + + Memory limit (e.g. 512M, 1G) + {Locked="512M"}{Locked="1G"}Command line argument example values should not be translated + Container host name @@ -2903,6 +2915,14 @@ On first run, creates the file with all settings commented out at their defaults Mount tmpfs to the container at the given path {Locked="tmpfs"}Command line arguments should not be translated + + Ulimit options (format: <name>=<soft>[:<hard>], use -1 for unlimited) + {Locked="-1"}{Locked="<name>=<soft>[:<hard>]"}Command line arguments should not be translated + + + Invalid {} argument value: '{}'. Expected <name>=<soft>[:<hard>] (use -1 for unlimited) + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="-1"}{Locked="<name>=<soft>[:<hard>]"} + User ID for the process (name|uid|uid:gid) {Locked="name|uid|uid:gid"}Command line arguments should not be translated diff --git a/src/windows/wslc/arguments/ArgumentDefinitions.h b/src/windows/wslc/arguments/ArgumentDefinitions.h index 7e0a4df6e..cd4054a95 100644 --- a/src/windows/wslc/arguments/ArgumentDefinitions.h +++ b/src/windows/wslc/arguments/ArgumentDefinitions.h @@ -41,6 +41,7 @@ _(BuildTarget, "target", NO_ALIAS, Kind::Value, L _(CIDFile, "cidfile", NO_ALIAS, Kind::Value, Localization::WSLCCLI_CIDFileArgDescription()) \ _(Command, "command", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_CommandArgDescription()) \ _(ContainerId, "container-id", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ContainerIdArgDescription()) \ +_(Cpus, "cpus", NO_ALIAS, Kind::Value, Localization::WSLCCLI_CpusArgDescription()) \ _(Force, "force", L"f", Kind::Flag, Localization::WSLCCLI_ForceArgDescription()) \ _(Detach, "detach", L"d", Kind::Flag, Localization::WSLCCLI_DetachArgDescription()) \ _(DNS, "dns", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DNSArgDescription()) \ @@ -72,6 +73,7 @@ _(Interactive, "interactive", L"i", Kind::Flag, L _(Label, "label", L"l", Kind::Value, Localization::WSLCCLI_LabelArgDescription()) \ _(Last, "last", L"n", Kind::Value, Localization::WSLCCLI_LastArgDescription()) \ _(Latest, "latest", L"l", Kind::Flag, Localization::WSLCCLI_LatestArgDescription()) \ +_(Memory, "memory", L"m", Kind::Value, Localization::WSLCCLI_MemoryArgDescription()) \ _(Name, "name", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NameArgDescription()) \ _(NetworkName, "network-name", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_NetworkNameArgDescription()) \ /*_(NoDNS, "no-dns", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoDNSArgDescription())*/ \ @@ -106,6 +108,7 @@ _(Time, "time", L"t", Kind::Value, L _(TMPFS, "tmpfs", NO_ALIAS, Kind::Value, Localization::WSLCCLI_TMPFSArgDescription()) \ _(TTY, "tty", L"t", Kind::Flag, Localization::WSLCCLI_TTYArgDescription()) \ _(Type, "type", L"t", Kind::Value, Localization::WSLCCLI_TypeArgDescription()) \ +_(Ulimit, "ulimit", NO_ALIAS, Kind::Value, Localization::WSLCCLI_UlimitArgDescription()) \ _(User, "user", L"u", Kind::Value, Localization::WSLCCLI_UserArgDescription()) \ _(Username, "username", L"u", Kind::Value, Localization::WSLCCLI_LoginUsernameArgDescription()) \ _(Verbose, "verbose", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_VerboseArgDescription()) \ diff --git a/src/windows/wslc/arguments/ArgumentValidation.cpp b/src/windows/wslc/arguments/ArgumentValidation.cpp index fda2fd219..1e54f7e2e 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.cpp +++ b/src/windows/wslc/arguments/ArgumentValidation.cpp @@ -11,6 +11,8 @@ Module Name: Implementation of the Argument Validation. --*/ + +#include "precomp.h" #include "Argument.h" #include "ArgumentTypes.h" #include "ArgumentValidation.h" @@ -48,6 +50,18 @@ void Argument::Validate(const ArgMap& execArgs) const validation::ValidateMemorySize(execArgs.GetAll(), m_name); break; + case ArgType::Memory: + validation::ValidateMemorySize(execArgs.GetAll(), m_name); + break; + + case ArgType::Cpus: + validation::ValidateNanoCpus(execArgs.GetAll(), m_name); + break; + + case ArgType::Ulimit: + validation::ValidateUlimit(execArgs.GetAll(), m_name); + break; + case ArgType::Tail: validation::ValidateIntegerFromString( execArgs.GetAll(), m_name, [](auto value) { return value != 0; }); @@ -271,6 +285,83 @@ int64_t GetMemorySizeFromString(const std::wstring& input, const std::wstring& a return static_cast(parsed.value()); } +void ValidateNanoCpus(const std::vector& values, const std::wstring& argName) +{ + for (const auto& value : values) + { + std::ignore = GetNanoCpusFromString(value, argName); + } +} + +int64_t GetNanoCpusFromString(const std::wstring& input, const std::wstring& argName) +{ + constexpr double NanosPerCpu = 1'000'000'000.0; + constexpr double MaxCpus = static_cast(std::numeric_limits::max()) / NanosPerCpu; + + const std::string narrow = WideToMultiByte(input); + const char* begin = narrow.c_str(); + const char* end = begin + narrow.size(); + + double cpus{}; + const auto result = std::from_chars(begin, end, cpus, std::chars_format::fixed); + if (result.ec != std::errc() || result.ptr != end || cpus <= 0.0 || cpus > MaxCpus) + { + throw ArgumentException(Localization::WSLCCLI_InvalidCpusError(argName, input)); + } + + return static_cast(cpus * NanosPerCpu); +} + +void ValidateUlimit(const std::vector& values, const std::wstring& argName) +{ + for (const auto& value : values) + { + std::ignore = ParseUlimit(value, argName); + } +} + +std::tuple ParseUlimit(const std::wstring& input, const std::wstring& argName) +{ + // Accepts =[:]; if hard is omitted hard = soft. -1 means unlimited. + const auto equalsPos = input.find(L'='); + if (equalsPos == std::wstring::npos || equalsPos == 0) + { + throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input)); + } + + const std::wstring valuesPart = input.substr(equalsPos + 1); + const auto colonPos = valuesPart.find(L':'); + + auto parseLimit = [&](const std::wstring& limitStr) -> int64_t { + if (limitStr.empty()) + { + throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input)); + } + + try + { + return GetIntegerFromString(limitStr, argName, [](int64_t v) { return v >= -1; }); + } + catch (const ArgumentException&) + { + // Re-throw with the ulimit-specific error message so the user sees the full input. + throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input)); + } + }; + + const int64_t soft = parseLimit(colonPos == std::wstring::npos ? valuesPart : valuesPart.substr(0, colonPos)); + const int64_t hard = colonPos == std::wstring::npos ? soft : parseLimit(valuesPart.substr(colonPos + 1)); + + // This rejects "-1:1024" and "-1:" while allowing ":-1", "-1:-1", and "-1". + const bool invalidRange = (soft == -1) ? (hard != -1) : (hard != -1 && hard < soft); + if (invalidRange) + { + throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input)); + } + + return {WideToMultiByte(input.substr(0, equalsPos)), soft, hard}; +} + std::pair ParseLabel(const std::wstring& value) { std::pair result{}; diff --git a/src/windows/wslc/arguments/ArgumentValidation.h b/src/windows/wslc/arguments/ArgumentValidation.h index 563992246..2356ea2cb 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.h +++ b/src/windows/wslc/arguments/ArgumentValidation.h @@ -17,6 +17,7 @@ Module Name: #include "ContainerModel.h" #include "InspectModel.h" #include +#include #include #include #include @@ -65,6 +66,12 @@ WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring void ValidateMemorySize(const std::vector& values, const std::wstring& argName); int64_t GetMemorySizeFromString(const std::wstring& input, const std::wstring& argName = {}); +void ValidateNanoCpus(const std::vector& values, const std::wstring& argName); +int64_t GetNanoCpusFromString(const std::wstring& input, const std::wstring& argName = {}); + +void ValidateUlimit(const std::vector& values, const std::wstring& argName); +std::tuple ParseUlimit(const std::wstring& input, const std::wstring& argName = {}); + void ValidateFormatTypeFromString(const std::vector& values, const std::wstring& argName); FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring& argName = {}); diff --git a/src/windows/wslc/commands/ContainerCreateCommand.cpp b/src/windows/wslc/commands/ContainerCreateCommand.cpp index 676f8ac69..83e69c8e1 100644 --- a/src/windows/wslc/commands/ContainerCreateCommand.cpp +++ b/src/windows/wslc/commands/ContainerCreateCommand.cpp @@ -32,6 +32,7 @@ std::vector ContainerCreateCommand::GetArguments() const Argument::Create(ArgType::Command), Argument::Create(ArgType::ForwardArgs), Argument::Create(ArgType::CIDFile), + Argument::Create(ArgType::Cpus), Argument::Create(ArgType::DNS, false, NO_LIMIT), // Argument::Create(ArgType::DNSDomain), Argument::Create(ArgType::DNSOption, false, NO_LIMIT), @@ -45,6 +46,7 @@ std::vector ContainerCreateCommand::GetArguments() const Argument::Create(ArgType::Hostname), Argument::Create(ArgType::Interactive), Argument::Create(ArgType::Label, false, NO_LIMIT), + Argument::Create(ArgType::Memory), Argument::Create(ArgType::Name), // Argument::Create(ArgType::NoDNS), // Argument::Create(ArgType::Progress), @@ -57,6 +59,7 @@ std::vector ContainerCreateCommand::GetArguments() const Argument::Create(ArgType::StopSignal), Argument::Create(ArgType::TMPFS, false, NO_LIMIT), Argument::Create(ArgType::TTY), + Argument::Create(ArgType::Ulimit, false, NO_LIMIT), Argument::Create(ArgType::User), Argument::Create(ArgType::Volume, false, NO_LIMIT), // Argument::Create(ArgType::Virtual), diff --git a/src/windows/wslc/commands/ContainerRunCommand.cpp b/src/windows/wslc/commands/ContainerRunCommand.cpp index 49a54d249..88dc1a5b5 100644 --- a/src/windows/wslc/commands/ContainerRunCommand.cpp +++ b/src/windows/wslc/commands/ContainerRunCommand.cpp @@ -32,6 +32,7 @@ std::vector ContainerRunCommand::GetArguments() const Argument::Create(ArgType::Command), Argument::Create(ArgType::ForwardArgs), Argument::Create(ArgType::CIDFile), + Argument::Create(ArgType::Cpus), Argument::Create(ArgType::Detach), Argument::Create(ArgType::DNS, false, NO_LIMIT), // Argument::Create(ArgType::DNSDomain), @@ -45,6 +46,7 @@ std::vector ContainerRunCommand::GetArguments() const Argument::Create(ArgType::Hostname), Argument::Create(ArgType::Interactive), Argument::Create(ArgType::Label, false, NO_LIMIT), + Argument::Create(ArgType::Memory), Argument::Create(ArgType::Name), // Argument::Create(ArgType::NoDNS), // Argument::Create(ArgType::Progress), @@ -58,6 +60,7 @@ std::vector ContainerRunCommand::GetArguments() const Argument::Create(ArgType::StopSignal), Argument::Create(ArgType::TMPFS, false, NO_LIMIT), Argument::Create(ArgType::TTY), + Argument::Create(ArgType::Ulimit, false, NO_LIMIT), Argument::Create(ArgType::User), Argument::Create(ArgType::Volume, false, NO_LIMIT), // Argument::Create(ArgType::Virtual), diff --git a/src/windows/wslc/services/ContainerModel.h b/src/windows/wslc/services/ContainerModel.h index 94a283032..e5f876446 100644 --- a/src/windows/wslc/services/ContainerModel.h +++ b/src/windows/wslc/services/ContainerModel.h @@ -53,6 +53,9 @@ struct ContainerOptions std::vector Tmpfs; std::vector> Labels; std::optional CidFile{}; + std::optional MemoryBytes{}; + std::optional NanoCpus{}; + std::vector> Ulimits; }; struct CreateContainerResult diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index 0f2b2b776..7d9edd1d5 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -109,6 +109,21 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal( containerLauncher.SetShmSize(options.ShmSize.value()); } + if (options.MemoryBytes.has_value()) + { + containerLauncher.SetMemoryLimit(options.MemoryBytes.value()); + } + + if (options.NanoCpus.has_value()) + { + containerLauncher.SetNanoCpus(options.NanoCpus.value()); + } + + for (const auto& [name, soft, hard] : options.Ulimits) + { + containerLauncher.AddUlimit(name, soft, hard); + } + if (!options.Entrypoint.empty()) { auto entrypoints = options.Entrypoint; diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp index a98ddd9a0..86abac013 100644 --- a/src/windows/wslc/tasks/ContainerTasks.cpp +++ b/src/windows/wslc/tasks/ContainerTasks.cpp @@ -422,6 +422,24 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context) options.ShmSize = validation::GetMemorySizeFromString(context.Args.Get()); } + if (context.Args.Contains(ArgType::Memory)) + { + options.MemoryBytes = validation::GetMemorySizeFromString(context.Args.Get()); + } + + if (context.Args.Contains(ArgType::Cpus)) + { + options.NanoCpus = validation::GetNanoCpusFromString(context.Args.Get()); + } + + if (context.Args.Contains(ArgType::Ulimit)) + { + for (const auto& value : context.Args.GetAll()) + { + options.Ulimits.emplace_back(validation::ParseUlimit(value)); + } + } + if (context.Args.Contains(ArgType::Command)) { options.Arguments.emplace_back(WideToMultiByte(context.Args.Get())); diff --git a/test/windows/wslc/WSLCCLIResourceLimitsParserUnitTests.cpp b/test/windows/wslc/WSLCCLIResourceLimitsParserUnitTests.cpp new file mode 100644 index 000000000..0943f8117 --- /dev/null +++ b/test/windows/wslc/WSLCCLIResourceLimitsParserUnitTests.cpp @@ -0,0 +1,129 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCCLIResourceLimitsParserUnitTests.cpp + +Abstract: + + This file contains unit tests for WSLC CLI resource-limit (--cpus, --memory, --ulimit) parsing and validation. + +--*/ + +#include "precomp.h" +#include "windows/Common.h" +#include "WSLCCLITestHelpers.h" +#include "ArgumentValidation.h" + +using namespace wsl::windows::wslc; + +namespace WSLCCLIResourceLimitsParserUnitTests { + +class WSLCCLIResourceLimitsParserUnitTests +{ + WSLC_TEST_CLASS(WSLCCLIResourceLimitsParserUnitTests) + + TEST_METHOD(NanoCpus_Valid) + { + // (input, expected nanoCpus) + std::vector> valid = { + {L"1", 1'000'000'000LL}, + {L"2", 2'000'000'000LL}, + {L"0.5", 500'000'000LL}, + {L"1.5", 1'500'000'000LL}, + {L"2.5", 2'500'000'000LL}, + {L"0.001", 1'000'000LL}, + }; + + for (const auto& [input, expected] : valid) + { + const auto actual = validation::GetNanoCpusFromString(input, L"cpus"); + VERIFY_ARE_EQUAL(expected, actual); + } + } + + TEST_METHOD(NanoCpus_Invalid) + { + // Each value should be rejected as an invalid --cpus value. + const std::vector invalid = { + L"", + L"0", // not positive + L"-1", // sign char rejected + L"-0.5", // sign char rejected + L"abc", // not numeric + L"1.5x", // trailing garbage + L" 1", // leading whitespace + L"1 ", // trailing whitespace + L"1e3", // exponent not allowed + L"+1", // sign char rejected + L"1.2.3", // multiple dots (rejected by strtod's strict end check) + L"99999999999" // overflow when multiplied by 1e9 + }; + + for (const auto& input : invalid) + { + VERIFY_THROWS(validation::GetNanoCpusFromString(input, L"cpus"), ArgumentException); + } + } + + TEST_METHOD(Ulimit_Valid) + { + // (input, expectedName, expectedSoft, expectedHard) + std::vector> valid = { + {L"nofile=1024", "nofile", 1024, 1024}, + {L"nofile=1024:2048", "nofile", 1024, 2048}, + {L"nproc=512:512", "nproc", 512, 512}, + {L"core=-1", "core", -1, -1}, + {L"core=-1:-1", "core", -1, -1}, + {L"memlock=0", "memlock", 0, 0}, + {L"stack=8192:-1", "stack", 8192, -1}, + }; + + for (const auto& [input, expectedName, expectedSoft, expectedHard] : valid) + { + const auto [name, soft, hard] = validation::ParseUlimit(input, L"ulimit"); + VERIFY_ARE_EQUAL(expectedName, name); + VERIFY_ARE_EQUAL(expectedSoft, soft); + VERIFY_ARE_EQUAL(expectedHard, hard); + } + } + + TEST_METHOD(Ulimit_Invalid) + { + const std::vector invalid = { + L"", + L"=1024", // empty name + L"nofile=", // empty value + L"nofile", // missing '=' + L"nofile=abc", // non-numeric soft + L"nofile=1024:", // empty hard + L"nofile=:1024", // empty soft + L"nofile=-2", // negative other than -1 + L"nofile=1024:512", // hard < soft (and both positive) + L"nofile=-1:1024", // unlimited soft but limited hard + L"nofile=-1:9223372036854775807", // unlimited soft but finite (INT64_MAX) hard + L"nofile=1.5", // not integer + }; + + for (const auto& input : invalid) + { + VERIFY_THROWS(validation::ParseUlimit(input, L"ulimit"), ArgumentException); + } + } + + TEST_METHOD(NanoCpus_Validator) + { + VERIFY_NO_THROW(validation::ValidateNanoCpus({L"0.5", L"1", L"2.5"}, L"cpus")); + VERIFY_THROWS(validation::ValidateNanoCpus({L"1", L"0"}, L"cpus"), ArgumentException); + } + + TEST_METHOD(Ulimit_Validator) + { + VERIFY_NO_THROW(validation::ValidateUlimit({L"nofile=1024", L"core=-1"}, L"ulimit")); + VERIFY_THROWS(validation::ValidateUlimit({L"nofile=1024", L"bad"}, L"ulimit"), ArgumentException); + } +}; + +} // namespace WSLCCLIResourceLimitsParserUnitTests diff --git a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp index 90f65edcd..95f602f49 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp @@ -828,6 +828,7 @@ class WSLCE2EContainerCreateTests std::wstringstream options; options << L"The following options are available:\r\n" // << L" --cidfile Write the container ID to the provided path\r\n" + << L" --cpus Number of CPUs (e.g. 0.5, 1, 2.5)\r\n" << L" --dns IP address of the DNS nameserver in resolv.conf\r\n" << L" --dns-option Set DNS options\r\n" << L" --dns-search Set DNS search domains\r\n" @@ -839,6 +840,7 @@ class WSLCE2EContainerCreateTests << L" -h,--hostname Container host name\r\n" << L" -i,--interactive Attach to stdin and keep it open\r\n" << L" -l,--label Set metadata on an object\r\n" + << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n" << L" --name Name of the container\r\n" << L" -p,--publish Publish a port from a container to host\r\n" << L" -P,--publish-all Publish all exposed ports to random host ports\r\n" @@ -848,6 +850,7 @@ class WSLCE2EContainerCreateTests << L" --stop-signal Signal to stop the container\r\n" << L" --tmpfs Mount tmpfs to the container at the given path\r\n" << L" -t,--tty Open a TTY with the container process.\r\n" + << L" --ulimit Ulimit options (format: =[:], use -1 for unlimited)\r\n" << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n" << L" -v,--volume Bind mount a volume to the container\r\n" << L" -w,--workdir Working directory inside the container\r\n" diff --git a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp index 83dd35dfd..f3fbfe4bf 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp @@ -731,6 +731,74 @@ class WSLCE2EContainerRunTests } } + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Cpus) + { + auto result = RunWslc(std::format(L"container run --name {} --cpus 1.5 {} true", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + const auto inspect = InspectContainer(WslcContainerName); + VERIFY_ARE_EQUAL(static_cast(1'500'000'000), inspect.HostConfig.NanoCpus); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Memory) + { + auto result = RunWslc(std::format(L"container run --name {} --memory 32M {} true", WslcContainerName, DebianImage.NameAndTag())); + // Note: stderr is not asserted here because some kernels emit a swap-limit warning + // ("Your kernel does not support swap limit capabilities...") when a memory limit is set. + result.Verify({.ExitCode = 0}); + + const auto inspect = InspectContainer(WslcContainerName); + VERIFY_ARE_EQUAL(static_cast(32) * 1024 * 1024, inspect.HostConfig.Memory); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ulimit) + { + auto result = RunWslc(std::format( + L"container run --name {} --ulimit nofile=1024:2048 --ulimit nproc=512 {} true", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + const auto inspect = InspectContainer(WslcContainerName); + VERIFY_ARE_EQUAL(static_cast(2), inspect.HostConfig.Ulimits.size()); + + std::map> byName; + for (const auto& ul : inspect.HostConfig.Ulimits) + { + byName[ul.Name] = {ul.Soft, ul.Hard}; + } + + VERIFY_IS_TRUE(byName.contains("nofile")); + VERIFY_ARE_EQUAL(static_cast(1024), byName["nofile"].first); + VERIFY_ARE_EQUAL(static_cast(2048), byName["nofile"].second); + + VERIFY_IS_TRUE(byName.contains("nproc")); + VERIFY_ARE_EQUAL(static_cast(512), byName["nproc"].first); + VERIFY_ARE_EQUAL(static_cast(512), byName["nproc"].second); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Cpus_Invalid) + { + auto result = RunWslc(std::format(L"container run --rm --cpus 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag())); + 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}); + EnsureContainerDoesNotExist(WslcContainerName); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Memory_Invalid) + { + auto result = + RunWslc(std::format(L"container run --rm --memory invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"Invalid memory argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)\r\n", .ExitCode = 1}); + EnsureContainerDoesNotExist(WslcContainerName); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ulimit_Invalid) + { + auto result = + RunWslc(std::format(L"container run --rm --ulimit nofile --name {} {}", WslcContainerName, DebianImage.NameAndTag())); + result.Verify( + {.Stderr = L"Invalid ulimit argument value: 'nofile'. Expected =[:] (use -1 for unlimited)\r\n", .ExitCode = 1}); + EnsureContainerDoesNotExist(WslcContainerName); + } + WSLC_TEST_METHOD(WSLCE2E_Container_Run_StopSignal_Invalid) { { @@ -820,6 +888,7 @@ class WSLCE2EContainerRunTests std::wstringstream options; options << L"The following options are available:\r\n" << L" --cidfile Write the container ID to the provided path\r\n" + << L" --cpus Number of CPUs (e.g. 0.5, 1, 2.5)\r\n" << L" -d,--detach Run container in detached mode\r\n" << L" --dns IP address of the DNS nameserver in resolv.conf\r\n" << L" --dns-option Set DNS options\r\n" @@ -832,6 +901,7 @@ class WSLCE2EContainerRunTests << L" -h,--hostname Container host name\r\n" << L" -i,--interactive Attach to stdin and keep it open\r\n" << L" -l,--label Set metadata on an object\r\n" + << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n" << L" --name Name of the container\r\n" << L" -p,--publish Publish a port from a container to host\r\n" << L" -P,--publish-all Publish all exposed ports to random host ports\r\n" @@ -841,6 +911,7 @@ class WSLCE2EContainerRunTests << L" --stop-signal Signal to stop the container\r\n" << L" --tmpfs Mount tmpfs to the container at the given path\r\n" << L" -t,--tty Open a TTY with the container process.\r\n" + << L" --ulimit Ulimit options (format: =[:], use -1 for unlimited)\r\n" << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n" << L" -v,--volume Bind mount a volume to the container\r\n" << L" -w,--workdir Working directory inside the container\r\n"