diff --git a/src/windows/wslc/commands/ContainerCreateCommand.cpp b/src/windows/wslc/commands/ContainerCreateCommand.cpp index 80bf5def4..d86ba3fe3 100644 --- a/src/windows/wslc/commands/ContainerCreateCommand.cpp +++ b/src/windows/wslc/commands/ContainerCreateCommand.cpp @@ -37,7 +37,7 @@ std::vector ContainerCreateCommand::GetArguments() const Argument::Create(ArgType::DNSSearch), Argument::Create(ArgType::Entrypoint), Argument::Create(ArgType::Env, false, NO_LIMIT), - Argument::Create(ArgType::EnvFile), + Argument::Create(ArgType::EnvFile, false, NO_LIMIT), Argument::Create(ArgType::GroupId), Argument::Create(ArgType::Interactive), Argument::Create(ArgType::Name), diff --git a/src/windows/wslc/commands/ContainerExecCommand.cpp b/src/windows/wslc/commands/ContainerExecCommand.cpp index 368307b72..999d44891 100644 --- a/src/windows/wslc/commands/ContainerExecCommand.cpp +++ b/src/windows/wslc/commands/ContainerExecCommand.cpp @@ -34,8 +34,8 @@ std::vector ContainerExecCommand::GetArguments() const std::nullopt, L"Arguments to pass to the command being executed inside the container"), Argument::Create(ArgType::Detach), - Argument::Create(ArgType::Env, std::nullopt, NO_LIMIT), - Argument::Create(ArgType::EnvFile), + Argument::Create(ArgType::Env, false, NO_LIMIT), + Argument::Create(ArgType::EnvFile, false, NO_LIMIT), Argument::Create(ArgType::Interactive), Argument::Create(ArgType::Session), Argument::Create(ArgType::TTY), diff --git a/src/windows/wslc/commands/ContainerRunCommand.cpp b/src/windows/wslc/commands/ContainerRunCommand.cpp index bc8846b10..76467ef5f 100644 --- a/src/windows/wslc/commands/ContainerRunCommand.cpp +++ b/src/windows/wslc/commands/ContainerRunCommand.cpp @@ -37,8 +37,8 @@ std::vector ContainerRunCommand::GetArguments() const Argument::Create(ArgType::DNSOption), Argument::Create(ArgType::DNSSearch), Argument::Create(ArgType::Entrypoint), - Argument::Create(ArgType::Env, std::nullopt, NO_LIMIT), - Argument::Create(ArgType::EnvFile), + Argument::Create(ArgType::Env, false, NO_LIMIT), + Argument::Create(ArgType::EnvFile, false, NO_LIMIT), Argument::Create(ArgType::Interactive), Argument::Create(ArgType::Name), Argument::Create(ArgType::NoDNS), diff --git a/src/windows/wslc/services/ContainerModel.cpp b/src/windows/wslc/services/ContainerModel.cpp index 704af6474..7099d6cc6 100644 --- a/src/windows/wslc/services/ContainerModel.cpp +++ b/src/windows/wslc/services/ContainerModel.cpp @@ -9,10 +9,9 @@ Module Name: Abstract: This file contains the ContainerModel implementation - --*/ -#include +#include "precomp.h" #include "ContainerModel.h" namespace wsl::windows::wslc::models { @@ -190,4 +189,87 @@ VolumeMount VolumeMount::Parse(const std::wstring& value) vm.m_hostPath = value.substr(0, splitColon); return vm; } -} // namespace wsl::windows::wslc::models \ No newline at end of file + +static inline bool IsSpace(wchar_t ch) +{ + return std::iswspace(ch) != 0; +} + +std::optional EnvironmentVariable::Parse(const std::wstring& entry) +{ + if (entry.empty() || std::all_of(entry.begin(), entry.end(), IsSpace)) + { + return std::nullopt; + } + + std::wstring key; + std::optional value; + + auto delimiterPos = entry.find('='); + if (delimiterPos == std::wstring::npos) + { + key = entry; + } + else + { + key = entry.substr(0, delimiterPos); + value = entry.substr(delimiterPos + 1); + } + + if (key.empty()) + { + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, L"Environment variable key cannot be empty"); + } + + if (std::any_of(key.begin(), key.end(), IsSpace)) + { + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, std::format(L"Environment variable key '{}' cannot contain whitespace", key)); + } + + if (!value.has_value()) + { + wil::unique_hglobal_string envValue; + auto hr = wil::GetEnvironmentVariableW(key.c_str(), envValue); + if (FAILED(hr) || envValue.get() == nullptr) + { + return std::nullopt; + } + + value = std::wstring(envValue.get()); + } + + return std::format(L"{}={}", key, value.value()); +} + +std::vector EnvironmentVariable::ParseFile(const std::wstring& filePath) +{ + std::ifstream file(filePath); + if (!file.is_open() || !file.good()) + { + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, std::format(L"Environment file '{}' cannot be opened for reading", filePath)); + } + + // Read the file line by line + std::vector envVars; + std::string line; + while (std::getline(file, line)) + { + // Remove leading whitespace + line.erase(line.begin(), std::find_if(line.begin(), line.end(), [](unsigned char ch) { return !std::isspace(ch); })); + + // Skip empty lines and comments + if (line.empty() || line[0] == '#') + { + continue; + } + + auto envVar = Parse(wsl::shared::string::MultiByteToWide(line)); + if (envVar.has_value()) + { + envVars.push_back(std::move(envVar.value())); + } + } + + return envVars; +} +} // namespace wsl::windows::wslc::models diff --git a/src/windows/wslc/services/ContainerModel.h b/src/windows/wslc/services/ContainerModel.h index 28de2321f..9833fabbf 100644 --- a/src/windows/wslc/services/ContainerModel.h +++ b/src/windows/wslc/services/ContainerModel.h @@ -30,6 +30,7 @@ enum class FormatType struct ContainerOptions { std::vector Arguments; + std::vector EnvironmentVariables; bool Detach = false; bool Interactive = false; std::string Name; @@ -69,6 +70,12 @@ struct ContainerInformation NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(ContainerInformation, Id, Name, Image, State, StateChangedAt, CreatedAt); }; +struct EnvironmentVariable +{ + static std::optional Parse(const std::wstring& entry); + static std::vector ParseFile(const std::wstring& filePath); +}; + struct PublishPort { enum class Protocol diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index 898979681..064930c25 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -86,7 +86,7 @@ static wsl::windows::common::RunningWSLAContainer CreateInternal( WI_SetFlagIf(containerFlags, WSLAContainerFlagsRm, options.Remove); wsl::windows::common::WSLAContainerLauncher containerLauncher( - image, options.Name, options.Arguments, {}, WSLAContainerNetworkTypeBridged, processFlags); + image, options.Name, options.Arguments, options.EnvironmentVariables, WSLAContainerNetworkTypeBridged, processFlags); // Set port options if provided for (const auto& port : options.Ports) @@ -341,7 +341,7 @@ int ContainerService::Exec(Session& session, const std::string& id, ContainerOpt ConsoleService consoleService; return consoleService.AttachToCurrentConsole( - wsl::windows::common::WSLAProcessLauncher({}, options.Arguments, {}, execFlags).Launch(*container)); + wsl::windows::common::WSLAProcessLauncher({}, options.Arguments, options.EnvironmentVariables, execFlags).Launch(*container)); } InspectContainer ContainerService::Inspect(Session& session, const std::string& id) diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp index 6d6dedb9d..ba520e57c 100644 --- a/src/windows/wslc/tasks/ContainerTasks.cpp +++ b/src/windows/wslc/tasks/ContainerTasks.cpp @@ -235,6 +235,32 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context) options.Arguments.emplace_back(WideToMultiByte(context.Args.Get())); } + if (context.Args.Contains(ArgType::EnvFile)) + { + auto const& envFiles = context.Args.GetAll(); + for (const auto& envFile : envFiles) + { + auto parsedEnvVars = EnvironmentVariable::ParseFile(envFile); + for (const auto& envVar : parsedEnvVars) + { + options.EnvironmentVariables.push_back(wsl::shared::string::WideToMultiByte(envVar)); + } + } + } + + if (context.Args.Contains(ArgType::Env)) + { + auto const& envArgs = context.Args.GetAll(); + for (const auto& arg : envArgs) + { + auto envVar = EnvironmentVariable::Parse(arg); + if (envVar) + { + options.EnvironmentVariables.push_back(wsl::shared::string::WideToMultiByte(*envVar)); + } + } + } + if (context.Args.Contains(ArgType::ForwardArgs)) { auto const& forwardArgs = context.Args.Get(); diff --git a/test/windows/wslc/WSLCCLIEnvVarParserUnitTests.cpp b/test/windows/wslc/WSLCCLIEnvVarParserUnitTests.cpp new file mode 100644 index 000000000..95194876f --- /dev/null +++ b/test/windows/wslc/WSLCCLIEnvVarParserUnitTests.cpp @@ -0,0 +1,179 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCCLIEnvVarParserUnitTests.cpp + +Abstract: + + This file contains unit tests for WSLC CLI environment variable parsing and validation. + +--*/ + +#include "precomp.h" +#include "windows/Common.h" +#include "WSLCCLITestHelpers.h" +#include "ContainerModel.h" + +#include +#include + +using namespace wsl::windows::wslc; + +namespace WSLCCLIEnvVarParserUnitTests { + +class WSLCCLIEnvVarParserUnitTests +{ + WSL_TEST_CLASS(WSLCCLIEnvVarParserUnitTests) + + TEST_METHOD_SETUP(TestMethodSetup) + { + EnvTestFile = wsl::windows::common::filesystem::GetTempFilename(); + return true; + } + + TEST_METHOD_CLEANUP(TestMethodCleanup) + { + DeleteFileW(EnvTestFile.c_str()); + return true; + } + + TEST_METHOD(WSLCCLIEnvVarParser_ValidEnvVars) + { + const auto parsed = models::EnvironmentVariable::Parse(L"FOO=bar"); + VERIFY_IS_TRUE(parsed.has_value()); + VERIFY_ARE_EQUAL(L"FOO=bar", parsed.value()); + } + + TEST_METHOD(WSLCCLIEnvVarParser_UsesProcessEnvWhenValueMissing) + { + constexpr const auto key = L"WSLC_TEST_ENV_FROM_PROCESS"; + VERIFY_IS_TRUE(SetEnvironmentVariableW(key, L"process_value")); + + auto cleanup = wil::scope_exit([&] { SetEnvironmentVariableW(key, nullptr); }); + + const auto parsed = models::EnvironmentVariable::Parse(key); + VERIFY_IS_TRUE(parsed.has_value()); + VERIFY_ARE_EQUAL(L"WSLC_TEST_ENV_FROM_PROCESS=process_value", parsed.value()); + } + + TEST_METHOD(WSLCCLIEnvVarParser_NulloptForWhitespaceOrUnsetVar) + { + const auto whitespaceOnly = models::EnvironmentVariable::Parse(L" \t "); + VERIFY_IS_FALSE(whitespaceOnly.has_value()); + + SetEnvironmentVariableA("WSLC_TEST_ENV_UNSET", nullptr); + const auto missingFromProcess = models::EnvironmentVariable::Parse(L"WSLC_TEST_ENV_UNSET"); + VERIFY_IS_FALSE(missingFromProcess.has_value()); + } + + TEST_METHOD(WSLCCLIEnvVarParser_InvalidKeysThrow) + { + VERIFY_THROWS(models::EnvironmentVariable::Parse(L"=value"), std::exception); + VERIFY_THROWS(models::EnvironmentVariable::Parse(L"BAD KEY=value"), std::exception); + VERIFY_THROWS(models::EnvironmentVariable::Parse(L"BAD\tKEY=value"), std::exception); + VERIFY_THROWS(models::EnvironmentVariable::Parse(L"BAD\nKEY=value"), std::exception); + } + + TEST_METHOD(WSLCCLIEnvVarParser_ParseFileParsesAndSkipsExpectedLines) + { + constexpr const auto key = L"WSLC_TEST_ENV_FROM_FILE"; + VERIFY_IS_TRUE(SetEnvironmentVariableW(key, L"file_process_value") == TRUE); + + auto envCleanup = wil::scope_exit([&] { SetEnvironmentVariableW(key, nullptr); }); + + std::ofstream file(EnvTestFile); + VERIFY_IS_TRUE(file.is_open()); + file << "# comment\n"; + file << "\n"; + file << "KEY1=VALUE1\n"; + file << " KEY2=VALUE2\n"; + file << "WSLC_TEST_ENV_FROM_FILE\n"; + file << "WSLC_TEST_ENV_DOES_NOT_EXIST\n"; + file.close(); + + const auto parsed = models::EnvironmentVariable::ParseFile(EnvTestFile.wstring()); + + VERIFY_ARE_EQUAL(3U, parsed.size()); + VERIFY_ARE_EQUAL(L"KEY1=VALUE1", parsed[0]); + VERIFY_ARE_EQUAL(L"KEY2=VALUE2", parsed[1]); + VERIFY_ARE_EQUAL(L"WSLC_TEST_ENV_FROM_FILE=file_process_value", parsed[2]); + } + + TEST_METHOD(WSLCCLIEnvVarParser_ParseFileThrowsWhenMissing) + { + VERIFY_THROWS(models::EnvironmentVariable::ParseFile(L"ENV_FILE_NOT_FOUND"), std::exception); + } + + TEST_METHOD(WSLCCLIEnvVarParser_ExplicitEmptyValueIsValid) + { + const auto parsed = models::EnvironmentVariable::Parse(L"FOO="); + VERIFY_IS_TRUE(parsed.has_value()); + VERIFY_ARE_EQUAL(L"FOO=", parsed.value()); + } + + TEST_METHOD(WSLCCLIEnvVarParser_MultipleEqualsPreservedInValue) + { + const auto parsed = models::EnvironmentVariable::Parse(L"FOO=a=b=c"); + VERIFY_IS_TRUE(parsed.has_value()); + VERIFY_ARE_EQUAL(L"FOO=a=b=c", parsed.value()); + } + + TEST_METHOD(WSLCCLIEnvVarParser_EmptyInputReturnsNullopt) + { + const auto parsed = models::EnvironmentVariable::Parse(L""); + VERIFY_IS_FALSE(parsed.has_value()); + } + + TEST_METHOD(WSLCCLIEnvVarParser_UsesProcessEnvWhenValueIsExplicitlyEmpty) + { + constexpr const auto key = L"WSLC_TEST_ENV_EMPTY_VALUE"; + VERIFY_IS_TRUE(SetEnvironmentVariableW(key, L"")); + + auto cleanup = wil::scope_exit([&] { SetEnvironmentVariableW(key, nullptr); }); + + const auto parsed = models::EnvironmentVariable::Parse(key); + VERIFY_IS_TRUE(parsed.has_value()); + VERIFY_ARE_EQUAL(L"WSLC_TEST_ENV_EMPTY_VALUE=", parsed.value()); + } + + TEST_METHOD(WSLCCLIEnvVarParser_ParseFilePreservesTrailingWhitespaceInValue) + { + std::ofstream file(EnvTestFile); + VERIFY_IS_TRUE(file.is_open()); + file << "KEY=value \n"; + file.close(); + + const auto parsed = models::EnvironmentVariable::ParseFile(EnvTestFile.wstring()); + + VERIFY_ARE_EQUAL(1U, parsed.size()); + VERIFY_ARE_EQUAL(L"KEY=value ", parsed[0]); + } + + TEST_METHOD(WSLCCLIEnvVarParser_ParseFileThrowsOnInvalidLine) + { + std::ofstream file(EnvTestFile); + VERIFY_IS_TRUE(file.is_open()); + file << "BAD KEY=value\n"; + file.close(); + + VERIFY_THROWS(models::EnvironmentVariable::ParseFile(EnvTestFile.wstring()), std::exception); + } + + TEST_METHOD(WSLCCLIEnvVarParser_ParseFileEmptyFileReturnsEmpty) + { + std::ofstream file(EnvTestFile); + VERIFY_IS_TRUE(file.is_open()); + file.close(); + + const auto parsed = models::EnvironmentVariable::ParseFile(EnvTestFile.wstring()); + VERIFY_ARE_EQUAL(0U, parsed.size()); + } + +private: + std::filesystem::path EnvTestFile; +}; + +} // namespace WSLCCLIEnvVarParserUnitTests \ No newline at end of file diff --git a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp index 33199e100..32680c1d4 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp @@ -15,6 +15,7 @@ Module Name: #include "windows/Common.h" #include "WSLCExecutor.h" #include "WSLCE2EHelpers.h" +#include #include #include @@ -32,6 +33,10 @@ class WSLCE2EContainerCreateTests EnsureImageIsLoaded(DebianImage); EnsureImageIsLoaded(PythonImage); + VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), HostEnvVariableValue.c_str())); + VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName2.c_str(), HostEnvVariableValue2.c_str())); + VERIFY_IS_TRUE(::SetEnvironmentVariableW(MissingHostEnvVariableName.c_str(), nullptr)); + // Initialize Winsock for loopback connectivity tests WSADATA wsaData{}; const int result = WSAStartup(MAKEWORD(2, 2), &wsaData); @@ -46,6 +51,10 @@ class WSLCE2EContainerCreateTests EnsureImageIsDeleted(DebianImage); EnsureImageIsDeleted(PythonImage); + VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), nullptr)); + VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName2.c_str(), nullptr)); + VERIFY_IS_TRUE(::SetEnvironmentVariableW(MissingHostEnvVariableName.c_str(), nullptr)); + // Cleanup Winsock WSACleanup(); return true; @@ -53,6 +62,8 @@ class WSLCE2EContainerCreateTests TEST_METHOD_SETUP(TestMethodSetup) { + EnvTestFile1 = wsl::windows::common::filesystem::GetTempFilename(); + EnvTestFile2 = wsl::windows::common::filesystem::GetTempFilename(); VolumeTestFile1 = wsl::windows::common::filesystem::GetTempFilename(); VolumeTestFile2 = wsl::windows::common::filesystem::GetTempFilename(); EnsureContainerDoesNotExist(WslcContainerName); @@ -61,6 +72,8 @@ class WSLCE2EContainerCreateTests TEST_METHOD_CLEANUP(TestMethodCleanup) { + DeleteFileW(EnvTestFile1.c_str()); + DeleteFileW(EnvTestFile2.c_str()); DeleteFileW(VolumeTestFile1.c_str()); DeleteFileW(VolumeTestFile2.c_str()); return true; @@ -386,6 +399,298 @@ class WSLCE2EContainerCreateTests VerifyContainerIsNotListed(WslcContainerName); } + TEST_METHOD(WSLCE2E_Container_Run_EnvOption) + { + WSL2_TEST_ONLY(); + VerifyContainerIsNotListed(WslcContainerName); + + auto result = RunWslc(std::format( + L"container run --rm --name {} -e {}=A {} env", WslcContainerName, HostEnvVariableName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + const auto outputLines = result.GetStdoutLines(); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, std::format(L"{}=A", HostEnvVariableName))); + } + + TEST_METHOD(WSLCE2E_Container_Run_EnvOption_MultipleValues) + { + WSL2_TEST_ONLY(); + VerifyContainerIsNotListed(WslcContainerName); + + auto result = RunWslc(std::format( + L"container run --rm --name {} -e {}=A -e {}=B {} env", + WslcContainerName, + HostEnvVariableName, + HostEnvVariableName2, + DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + const auto outputLines = result.GetStdoutLines(); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, std::format(L"{}=A", HostEnvVariableName))); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, std::format(L"{}=B", HostEnvVariableName2))); + } + + TEST_METHOD(WSLCE2E_Container_Run_EnvOption_KeyOnly_UsesHostValue) + { + WSL2_TEST_ONLY(); + VerifyContainerIsNotListed(WslcContainerName); + + auto result = RunWslc(std::format( + L"container run --rm --name {} -e {} {} env", WslcContainerName, HostEnvVariableName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + const auto outputLines = result.GetStdoutLines(); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, std::format(L"{}={}", HostEnvVariableName, HostEnvVariableValue))); + } + + TEST_METHOD(WSLCE2E_Container_Run_EnvOption_KeyOnly_MultipleValues_UsesHostValues) + { + WSL2_TEST_ONLY(); + VerifyContainerIsNotListed(WslcContainerName); + + auto result = RunWslc(std::format( + L"container run --rm --name {} -e {} -e {} {} env", + WslcContainerName, + HostEnvVariableName, + HostEnvVariableName2, + DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + const auto outputLines = result.GetStdoutLines(); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, std::format(L"{}={}", HostEnvVariableName, HostEnvVariableValue))); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, std::format(L"{}={}", HostEnvVariableName2, HostEnvVariableValue2))); + } + + TEST_METHOD(WSLCE2E_Container_Run_EnvOption_EmptyValue) + { + WSL2_TEST_ONLY(); + VerifyContainerIsNotListed(WslcContainerName); + + auto result = RunWslc(std::format( + L"container run --rm --name {} -e {}= {} env", WslcContainerName, HostEnvVariableName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + const auto outputLines = result.GetStdoutLines(); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, std::format(L"{}=", HostEnvVariableName))); + } + + TEST_METHOD(WSLCE2E_Container_Run_EnvFile) + { + WSL2_TEST_ONLY(); + VerifyContainerIsNotListed(WslcContainerName); + + WriteEnvFile(EnvTestFile1, {"WSLC_TEST_ENV_FILE_A=env-file-a", "WSLC_TEST_ENV_FILE_B=env-file-b"}); + + auto result = RunWslc(std::format( + L"container run --rm --name {} --env-file {} {} env", + WslcContainerName, + EscapePath(EnvTestFile1.wstring()), + DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + const auto outputLines = result.GetStdoutLines(); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, L"WSLC_TEST_ENV_FILE_A=env-file-a")); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, L"WSLC_TEST_ENV_FILE_B=env-file-b")); + } + + TEST_METHOD(WSLCE2E_Container_Run_EnvOption_MixedWithEnvFile) + { + WSL2_TEST_ONLY(); + VerifyContainerIsNotListed(WslcContainerName); + + WriteEnvFile(EnvTestFile1, {"WSLC_TEST_ENV_MIX_FILE_A=from-file-a", "WSLC_TEST_ENV_MIX_FILE_B=from-file-b"}); + + auto result = RunWslc(std::format( + L"container run --rm --name {} -e WSLC_TEST_ENV_MIX_CLI=from-cli --env-file {} {} env", + WslcContainerName, + EscapePath(EnvTestFile1.wstring()), + DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + const auto outputLines = result.GetStdoutLines(); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, L"WSLC_TEST_ENV_MIX_FILE_A=from-file-a")); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, L"WSLC_TEST_ENV_MIX_FILE_B=from-file-b")); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, L"WSLC_TEST_ENV_MIX_CLI=from-cli")); + } + + TEST_METHOD(WSLCE2E_Container_Run_EnvFile_MultipleFiles) + { + WSL2_TEST_ONLY(); + VerifyContainerIsNotListed(WslcContainerName); + + WriteEnvFile(EnvTestFile1, {"WSLC_TEST_ENV_FILE_MULTI_A=file1-a", "WSLC_TEST_ENV_FILE_MULTI_B=file1-b"}); + + WriteEnvFile(EnvTestFile2, {"WSLC_TEST_ENV_FILE_MULTI_C=file2-c", "WSLC_TEST_ENV_FILE_MULTI_D=file2-d"}); + + auto result = RunWslc(std::format( + L"container run --rm --name {} --env-file {} --env-file {} {} env", + WslcContainerName, + EscapePath(EnvTestFile1.wstring()), + EscapePath(EnvTestFile2.wstring()), + DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + const auto outputLines = result.GetStdoutLines(); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, L"WSLC_TEST_ENV_FILE_MULTI_A=file1-a")); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, L"WSLC_TEST_ENV_FILE_MULTI_B=file1-b")); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, L"WSLC_TEST_ENV_FILE_MULTI_C=file2-c")); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, L"WSLC_TEST_ENV_FILE_MULTI_D=file2-d")); + } + + TEST_METHOD(WSLCE2E_Container_Run_EnvFile_MissingFile) + { + WSL2_TEST_ONLY(); + VerifyContainerIsNotListed(WslcContainerName); + + auto result = RunWslc(std::format( + L"container run --rm --name {} --env-file ENV_FILE_NOT_FOUND {} env", WslcContainerName, DebianImage.NameAndTag())); + result.Verify( + {.Stderr = L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1}); + } + + TEST_METHOD(WSLCE2E_Container_Run_EnvFile_InvalidContent) + { + WSL2_TEST_ONLY(); + VerifyContainerIsNotListed(WslcContainerName); + + WriteEnvFile(EnvTestFile1, {"WSLC_TEST_ENV_VALID=ok", "BAD KEY=value"}); + + auto result = RunWslc(std::format( + L"container run --rm --name {} --env-file {} {} env", + WslcContainerName, + EscapePath(EnvTestFile1.wstring()), + DebianImage.NameAndTag())); + result.Verify({.Stderr = L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1}); + } + + TEST_METHOD(WSLCE2E_Container_Run_EnvFile_DuplicateKeys_Precedence) + { + WSL2_TEST_ONLY(); + VerifyContainerIsNotListed(WslcContainerName); + + WriteEnvFile(EnvTestFile1, {"WSLC_TEST_ENV_DUP=from-file-1"}); + + WriteEnvFile(EnvTestFile2, {"WSLC_TEST_ENV_DUP=from-file-2"}); + + // Later --env-file should win over earlier --env-file for duplicate keys + auto result = RunWslc(std::format( + L"container run --rm --name {} --env-file {} --env-file {} {} env", + WslcContainerName, + EscapePath(EnvTestFile1.wstring()), + EscapePath(EnvTestFile2.wstring()), + DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + auto outputLines = result.GetStdoutLines(); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, L"WSLC_TEST_ENV_DUP=from-file-2")); + + // Explicit -e should win over env-file value for duplicate keys + result = RunWslc(std::format( + L"container run --rm --name {} -e WSLC_TEST_ENV_DUP=from-cli --env-file {} --env-file {} {} env", + WslcContainerName, + EscapePath(EnvTestFile1.wstring()), + EscapePath(EnvTestFile2.wstring()), + DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + outputLines = result.GetStdoutLines(); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, L"WSLC_TEST_ENV_DUP=from-cli")); + } + + TEST_METHOD(WSLCE2E_Container_Run_EnvFile_ValueContainsEquals) + { + WSL2_TEST_ONLY(); + VerifyContainerIsNotListed(WslcContainerName); + + WriteEnvFile(EnvTestFile1, {"WSLC_TEST_ENV_EQUALS=value=with=equals"}); + + auto result = RunWslc(std::format( + L"container run --rm --name {} --env-file {} {} env", + WslcContainerName, + EscapePath(EnvTestFile1.wstring()), + DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + const auto outputLines = result.GetStdoutLines(); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, L"WSLC_TEST_ENV_EQUALS=value=with=equals")); + } + + TEST_METHOD(WSLCE2E_Container_Exec_EnvOption) + { + WSL2_TEST_ONLY(); + + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + result = RunWslc(std::format(L"container exec -e {}=A {} env", HostEnvVariableName, WslcContainerName)); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + const auto outputLines = result.GetStdoutLines(); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, std::format(L"{}=A", HostEnvVariableName))); + } + + TEST_METHOD(WSLCE2E_Container_Exec_EnvOption_KeyOnly_UsesHostValue) + { + WSL2_TEST_ONLY(); + + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + result = RunWslc(std::format(L"container exec -e {} {} env", HostEnvVariableName, WslcContainerName)); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + const auto outputLines = result.GetStdoutLines(); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, std::format(L"{}={}", HostEnvVariableName, HostEnvVariableValue))); + } + + TEST_METHOD(WSLCE2E_Container_Exec_EnvFile) + { + WSL2_TEST_ONLY(); + + WriteEnvFile(EnvTestFile1, {"WSLC_TEST_EXEC_ENV_FILE_A=exec-env-file-a", "WSLC_TEST_EXEC_ENV_FILE_B=exec-env-file-b"}); + + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + result = RunWslc(std::format(L"container exec --env-file {} {} env", EscapePath(EnvTestFile1.wstring()), WslcContainerName)); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + const auto outputLines = result.GetStdoutLines(); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, L"WSLC_TEST_EXEC_ENV_FILE_A=exec-env-file-a")); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, L"WSLC_TEST_EXEC_ENV_FILE_B=exec-env-file-b")); + } + + TEST_METHOD(WSLCE2E_Container_Exec_EnvOption_MixedWithEnvFile) + { + WSL2_TEST_ONLY(); + + WriteEnvFile(EnvTestFile1, {"WSLC_TEST_EXEC_ENV_MIX_FILE_A=from-file-a", "WSLC_TEST_EXEC_ENV_MIX_FILE_B=from-file-b"}); + + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + result = RunWslc(std::format( + L"container exec -e WSLC_TEST_EXEC_ENV_MIX_CLI=from-cli --env-file {} {} env", EscapePath(EnvTestFile1.wstring()), WslcContainerName)); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + const auto outputLines = result.GetStdoutLines(); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, L"WSLC_TEST_EXEC_ENV_MIX_FILE_A=from-file-a")); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, L"WSLC_TEST_EXEC_ENV_MIX_FILE_B=from-file-b")); + VERIFY_IS_TRUE(ContainsOutputLine(outputLines, L"WSLC_TEST_EXEC_ENV_MIX_CLI=from-cli")); + } + + TEST_METHOD(WSLCE2E_Container_Exec_EnvFile_MissingFile) + { + WSL2_TEST_ONLY(); + + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = S_OK}); + + result = RunWslc(std::format(L"container exec --env-file ENV_FILE_NOT_FOUND {} env", WslcContainerName)); + result.Verify( + {.Stderr = L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1}); + } + TEST_METHOD(WSLCE2E_Container_RunInteractive_TTY) { WSL2_TEST_ONLY(); @@ -769,16 +1074,32 @@ class WSLCE2EContainerCreateTests } private: + // Test container name const std::wstring WslcContainerName = L"wslc-test-container"; + // Test environment variables + const std::wstring HostEnvVariableName = L"WSLC_TEST_HOST_ENV"; + const std::wstring HostEnvVariableName2 = L"WSLC_TEST_HOST_ENV2"; + const std::wstring HostEnvVariableValue = L"wslc-host-env-value"; + const std::wstring HostEnvVariableValue2 = L"wslc-host-env-value2"; + const std::wstring MissingHostEnvVariableName = L"WSLC_TEST_MISSING_HOST_ENV"; + + // Test environment variable files + std::filesystem::path EnvTestFile1; + std::filesystem::path EnvTestFile2; + + // Test images const TestImage& AlpineImage = AlpineTestImage(); const TestImage& DebianImage = DebianTestImage(); const TestImage& PythonImage = PythonTestImage(); const TestImage& InvalidImage = InvalidTestImage(); + // Test ports const uint16_t ContainerTestPort = 8080; const uint16_t HostTestPort1 = 1234; const uint16_t HostTestPort2 = 1235; + + // Test volume files std::filesystem::path VolumeTestFile1; std::filesystem::path VolumeTestFile2; @@ -843,6 +1164,30 @@ class WSLCE2EContainerCreateTests return options.str(); } + void WriteEnvFile(const std::filesystem::path& filePath, const std::vector& envVariableLines) const + { + std::ofstream envFile(filePath, std::ios::out | std::ios::trunc | std::ios::binary); + VERIFY_IS_TRUE(envFile.is_open()); + for (const auto& line : envVariableLines) + { + envFile << line << "\n"; + } + VERIFY_IS_TRUE(envFile.good()); + } + + bool ContainsOutputLine(const std::vector& outputLines, const std::wstring& expectedLine) const + { + for (const auto& line : outputLines) + { + if (line == expectedLine) + { + return true; + } + } + + return false; + } + std::wstring GetPythonHttpServerScript(uint16_t port) { return std::format(L"python3 -m http.server {}", port);