Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions localization/strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2776,6 +2776,13 @@ On first run, creates the file with all settings commented out at their defaults
<data name="WSLCCLI_NameArgDescription" xml:space="preserve">
<value>Name of the container</value>
</data>
<data name="WSLCCLI_NetworkArgDescription" xml:space="preserve">
<value>Connect a container to a network</value>
</data>
<data name="WSLCCLI_NetworkEmptyError" xml:space="preserve">
<value>Invalid {} value: network name cannot be empty or whitespace</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_NoCacheArgDescription" xml:space="preserve">
<value>Do not use cache when building the image</value>
</data>
Expand Down Expand Up @@ -2859,6 +2866,10 @@ On first run, creates the file with all settings commented out at their defaults
<data name="WSLCCLI_WorkingDirArgDescription" xml:space="preserve">
<value>Working directory inside the container</value>
</data>
<data name="WSLCCLI_WorkingDirEmptyError" xml:space="preserve">
<value>Invalid {} value: working directory cannot be empty or whitespace</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
<value>Write the container ID to the provided path</value>
</data>
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/arguments/ArgumentDefinitions.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ _(Label, "label", L"l", Kind::Value, L
_(Last, "last", L"n", Kind::Value, Localization::WSLCCLI_LastArgDescription()) \
_(Latest, "latest", L"l", Kind::Flag, Localization::WSLCCLI_LatestArgDescription()) \
_(Name, "name", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NameArgDescription()) \
_(Network, "network", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NetworkArgDescription()) \
_(NetworkName, "network-name", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_NetworkNameArgDescription()) \
/*_(NoDNS, "no-dns", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoDNSArgDescription())*/ \
_(NoCache, "no-cache", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoCacheArgDescription()) \
Expand Down
15 changes: 14 additions & 1 deletion src/windows/wslc/arguments/ArgumentValidation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,20 @@ void Argument::Validate(const ArgMap& execArgs) const
if (value.empty() ||
std::all_of(value.begin(), value.end(), [](wchar_t c) { return std::iswspace(static_cast<wint_t>(c)); }))
{
throw ArgumentException(std::format(L"Invalid {} argument value: working directory cannot be empty or whitespace", m_name));
throw ArgumentException(Localization::WSLCCLI_WorkingDirEmptyError(m_name));
}
break;
}

case ArgType::Network:
{
for (const auto& value : execArgs.GetAll<ArgType::Network>())
{
if (value.empty() ||
std::all_of(value.begin(), value.end(), [](wchar_t c) { return std::iswspace(static_cast<wint_t>(c)); }))
{
throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(m_name));
}
}
break;
}
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/commands/ContainerCreateCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
Argument::Create(ArgType::Interactive),
Argument::Create(ArgType::Label, false, NO_LIMIT),
Argument::Create(ArgType::Name),
Argument::Create(ArgType::Network, false, NO_LIMIT),
// Argument::Create(ArgType::NoDNS),
// Argument::Create(ArgType::Progress),
Argument::Create(ArgType::Publish, false, NO_LIMIT),
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/commands/ContainerRunCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
Argument::Create(ArgType::Interactive),
Argument::Create(ArgType::Label, false, NO_LIMIT),
Argument::Create(ArgType::Name),
Argument::Create(ArgType::Network, false, NO_LIMIT),
// Argument::Create(ArgType::NoDNS),
// Argument::Create(ArgType::Progress),
Argument::Create(ArgType::Publish, false, NO_LIMIT),
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/services/ContainerModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ struct ContainerOptions
std::vector<std::string> DnsServers;
std::vector<std::string> DnsSearchDomains;
std::vector<std::string> DnsOptions;
std::vector<std::string> Networks;
std::vector<std::string> Tmpfs;
std::vector<std::pair<std::string, std::string>> Labels;
std::optional<std::wstring> CidFile{};
Expand Down
9 changes: 8 additions & 1 deletion src/windows/wslc/services/ContainerService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,15 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(
WI_SetFlagIf(containerFlags, WSLCContainerFlagsPublishAll, options.PublishAll);
WI_SetFlagIf(containerFlags, WSLCContainerFlagsGpu, options.Gpu);

std::string networkMode = options.Networks.empty() ? std::string("bridge") : options.Networks.front();

wsl::windows::common::WSLCContainerLauncher containerLauncher(
image, options.Name, options.Arguments, options.EnvironmentVariables, "bridge", processFlags);
image, options.Name, options.Arguments, options.EnvironmentVariables, std::move(networkMode), processFlags);

for (size_t i = 1; i < options.Networks.size(); ++i)
{
containerLauncher.AddAdditionalNetwork(options.Networks[i]);
}

// Set port options if provided
for (const auto& port : options.Ports)
Expand Down
10 changes: 10 additions & 0 deletions src/windows/wslc/tasks/ContainerTasks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,16 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
}
}

if (context.Args.Contains(ArgType::Network))
{
auto networks = context.Args.GetAll<ArgType::Network>();
options.Networks.reserve(options.Networks.size() + networks.size());
for (const auto& value : networks)
{
options.Networks.emplace_back(WideToMultiByte(value));
}
}

if (context.Args.Contains(ArgType::User))
{
options.User = WideToMultiByte(context.Args.Get<ArgType::User>());
Expand Down
100 changes: 100 additions & 0 deletions test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,106 @@ class WSLCCLIExecutionUnitTests
command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
}

TEST_METHOD(SetContainerOptionsFromArgs_WithoutNetwork_NetworksIsEmpty)
{
CLIExecutionContext context;

wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);

const auto& options = context.Data.Get<Data::ContainerOptions>();
VERIFY_IS_TRUE(options.Networks.empty());
}

TEST_METHOD(RunCommand_ParseNetworkSingleValue_SetsNetwork)
{
auto invocation = CreateInvocationFromCommandLine(L"wslc --network host ubuntu sh");

ContainerRunCommand command{L""};
CLIExecutionContext context;
command.ParseArguments(invocation, context.Args);
command.ValidateArguments(context.Args);

wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);

const auto& options = context.Data.Get<Data::ContainerOptions>();
VERIFY_ARE_EQUAL(1u, options.Networks.size());
VERIFY_ARE_EQUAL(std::string("host"), options.Networks[0]);
}

TEST_METHOD(RunCommand_ParseNetworkMultipleValues_PreservesOrder)
{
auto invocation = CreateInvocationFromCommandLine(L"wslc --network net1 --network net2 ubuntu sh");

ContainerRunCommand command{L""};
CLIExecutionContext context;
command.ParseArguments(invocation, context.Args);
command.ValidateArguments(context.Args);

wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);

const auto& options = context.Data.Get<Data::ContainerOptions>();
VERIFY_ARE_EQUAL(2u, options.Networks.size());
VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0]);
VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1]);
}

TEST_METHOD(RunCommand_ParseNetworkEmptyValue_ThrowsArgumentException)
{
auto invocation = CreateInvocationFromCommandLine(L"wslc --network \"\" ubuntu sh");

ContainerRunCommand command{L""};
CLIExecutionContext context;
command.ParseArguments(invocation, context.Args);

VERIFY_THROWS_SPECIFIC(
command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
}

TEST_METHOD(CreateCommand_ParseNetworkSingleValue_SetsNetwork)
{
auto invocation = CreateInvocationFromCommandLine(L"wslc --network host ubuntu sh");
Comment thread
AmelBawa-msft marked this conversation as resolved.
Outdated

ContainerCreateCommand command{L""};
CLIExecutionContext context;
command.ParseArguments(invocation, context.Args);
command.ValidateArguments(context.Args);

wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);

const auto& options = context.Data.Get<Data::ContainerOptions>();
VERIFY_ARE_EQUAL(1u, options.Networks.size());
VERIFY_ARE_EQUAL(std::string("host"), options.Networks[0]);
}

TEST_METHOD(CreateCommand_ParseNetworkMultipleValues_PreservesOrder)
{
auto invocation = CreateInvocationFromCommandLine(L"wslc --network net1 --network net2 ubuntu sh");

ContainerCreateCommand command{L""};
CLIExecutionContext context;
command.ParseArguments(invocation, context.Args);
command.ValidateArguments(context.Args);

wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);

const auto& options = context.Data.Get<Data::ContainerOptions>();
VERIFY_ARE_EQUAL(2u, options.Networks.size());
VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0]);
VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1]);
}

TEST_METHOD(CreateCommand_ParseNetworkEmptyValue_ThrowsArgumentException)
{
auto invocation = CreateInvocationFromCommandLine(L"wslc --network \"\" ubuntu sh");

ContainerCreateCommand command{L""};
CLIExecutionContext context;
command.ParseArguments(invocation, context.Args);

VERIFY_THROWS_SPECIFIC(
command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
}

// Test: Command Line test parsing all cases defined in CommandLineTestCases.h
// This test verifies the command line parsing logic used by the CLI and executes the same
// code as the CLI up to the point of command execution, including parsing and argument validtion.
Expand Down
54 changes: 54 additions & 0 deletions test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class WSLCE2EContainerCreateTests
EnsureContainerDoesNotExist(WslcContainerName);
EnsureImageIsDeleted(AlpineImage);
EnsureImageIsDeleted(DebianImage);
EnsureNetworkDoesNotExist(TestNetworkName);

VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), nullptr));
VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName2.c_str(), nullptr));
Expand All @@ -58,6 +59,7 @@ class WSLCE2EContainerCreateTests
VolumeTestFile1 = wsl::windows::common::filesystem::GetTempFilename();
VolumeTestFile2 = wsl::windows::common::filesystem::GetTempFilename();
EnsureContainerDoesNotExist(WslcContainerName);
EnsureNetworkDoesNotExist(TestNetworkName);
return true;
}

Expand Down Expand Up @@ -768,10 +770,61 @@ class WSLCE2EContainerCreateTests
}
}

WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_DefaultIsBridge)
{
auto result = RunWslc(std::format(L"container create --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
result.Verify({.Stderr = L"", .ExitCode = 0});

const auto inspect = InspectContainer(WslcContainerName);
VERIFY_ARE_EQUAL(std::string("bridge"), inspect.HostConfig.NetworkMode);
}

WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_HostMode)
{
auto result =
RunWslc(std::format(L"container create --name {} --network host {} true", WslcContainerName, DebianImage.NameAndTag()));
Comment thread
AmelBawa-msft marked this conversation as resolved.
result.Verify({.Stderr = L"", .ExitCode = 0});

const auto inspect = InspectContainer(WslcContainerName);
VERIFY_ARE_EQUAL(std::string("host"), inspect.HostConfig.NetworkMode);
}

WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_UserDefinedNetwork)
{
auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
result.Verify({.Stderr = L"", .ExitCode = 0});
auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });

result = RunWslc(std::format(
L"container create --name {} --network {} {} true", WslcContainerName, TestNetworkName, DebianImage.NameAndTag()));
result.Verify({.Stderr = L"", .ExitCode = 0});

const auto inspect = InspectContainer(WslcContainerName);
VERIFY_ARE_EQUAL(wsl::shared::string::WideToMultiByte(TestNetworkName), inspect.HostConfig.NetworkMode);
}

WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_EmptyValue_Rejected)
{
auto result = RunWslc(std::format(L"container create --network \"\" --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
result.Verify({.Stderr = L"Invalid network value: network name cannot be empty or whitespace\r\n", .ExitCode = 1});
VerifyContainerIsNotListed(WslcContainerName);
}

WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_NonexistentNetwork_Rejected)
{
auto result = RunWslc(
std::format(L"container create --network does-not-exist --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
result.Verify({.Stderr = L"Network not found: 'does-not-exist'\r\nError code: WSLC_E_NETWORK_NOT_FOUND\r\n", .ExitCode = 1});
VerifyContainerIsNotListed(WslcContainerName);
}

private:
// Test container name
const std::wstring WslcContainerName = L"wslc-test-container";

// Test network name
const std::wstring TestNetworkName = L"wslc-test-network";

// Test environment variables
const std::wstring HostEnvVariableName = L"WSLC_TEST_HOST_ENV";
const std::wstring HostEnvVariableName2 = L"WSLC_TEST_HOST_ENV2";
Expand Down Expand Up @@ -840,6 +893,7 @@ class WSLCE2EContainerCreateTests
<< L" -i,--interactive Attach to stdin and keep it open\r\n"
<< L" -l,--label Set metadata on an object\r\n"
<< L" --name Name of the container\r\n"
<< L" --network Connect a container to a network\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"
<< L" --rm Remove the container after it stops\r\n"
Expand Down
54 changes: 54 additions & 0 deletions test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class WSLCE2EContainerRunTests
EnsureImageIsDeleted(DebianImage);
EnsureImageIsDeleted(PythonImage);
EnsureVolumeDoesNotExist(WslcVolumeName);
EnsureNetworkDoesNotExist(TestNetworkName);

VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), nullptr));
VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName2.c_str(), nullptr));
Expand All @@ -58,6 +59,7 @@ class WSLCE2EContainerRunTests
EnsureContainerDoesNotExist(WslcContainerName);
EnsureContainerDoesNotExist(WslcContainerName2);
EnsureVolumeDoesNotExist(WslcVolumeName);
EnsureNetworkDoesNotExist(TestNetworkName);

EnvTestFile1 = wsl::windows::common::filesystem::GetTempFilename();
EnvTestFile2 = wsl::windows::common::filesystem::GetTempFilename();
Expand Down Expand Up @@ -647,6 +649,54 @@ class WSLCE2EContainerRunTests
VERIFY_IS_TRUE(result.Stdout->find(L"options ndots:5 timeout:3") != std::wstring::npos);
}

WSLC_TEST_METHOD(WSLCE2E_Container_Run_Network_DefaultIsBridge)
{
auto result = RunWslc(std::format(L"container run --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
result.Verify({.Stderr = L"", .ExitCode = 0});

const auto inspect = InspectContainer(WslcContainerName);
VERIFY_ARE_EQUAL(std::string("bridge"), inspect.HostConfig.NetworkMode);
}

WSLC_TEST_METHOD(WSLCE2E_Container_Run_Network_HostMode)
{
auto result =
RunWslc(std::format(L"container run --name {} --network host {} true", WslcContainerName, DebianImage.NameAndTag()));
result.Verify({.Stderr = L"", .ExitCode = 0});

const auto inspect = InspectContainer(WslcContainerName);
VERIFY_ARE_EQUAL(std::string("host"), inspect.HostConfig.NetworkMode);
}

WSLC_TEST_METHOD(WSLCE2E_Container_Run_Network_UserDefinedNetwork)
{
auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
result.Verify({.Stderr = L"", .ExitCode = 0});
auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });

result = RunWslc(std::format(
L"container run --name {} --network {} {} true", WslcContainerName, TestNetworkName, DebianImage.NameAndTag()));
result.Verify({.Stderr = L"", .ExitCode = 0});

const auto inspect = InspectContainer(WslcContainerName);
VERIFY_ARE_EQUAL(wsl::shared::string::WideToMultiByte(TestNetworkName), inspect.HostConfig.NetworkMode);
VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(wsl::shared::string::WideToMultiByte(TestNetworkName)));
}

WSLC_TEST_METHOD(WSLCE2E_Container_Run_Network_EmptyValue_Rejected)
{
auto result =
RunWslc(std::format(L"container run --rm --network \"\" --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
result.Verify({.Stderr = L"Invalid network value: network name cannot be empty or whitespace\r\n", .ExitCode = 1});
}

WSLC_TEST_METHOD(WSLCE2E_Container_Run_Network_NonexistentNetwork_Rejected)
{
auto result = RunWslc(std::format(
L"container run --rm --network does-not-exist --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
result.Verify({.Stderr = L"Network not found: 'does-not-exist'\r\nError code: WSLC_E_NETWORK_NOT_FOUND\r\n", .ExitCode = 1});
}

WSLC_TEST_METHOD(WSLCE2E_Container_Run_Volume_NamedVolume_Success)
{
// Create a named volume
Expand Down Expand Up @@ -782,6 +832,9 @@ class WSLCE2EContainerRunTests
// Test named volume
const std::wstring WslcVolumeName = L"wslc-test-volume";

// Test user-defined network
const std::wstring TestNetworkName = L"wslc-test-network";

std::wstring GetHelpMessage() const
{
std::wstringstream output;
Expand Down Expand Up @@ -833,6 +886,7 @@ class WSLCE2EContainerRunTests
<< L" -i,--interactive Attach to stdin and keep it open\r\n"
<< L" -l,--label Set metadata on an object\r\n"
<< L" --name Name of the container\r\n"
<< L" --network Connect a container to a network\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"
<< L" --rm Remove the container after it stops\r\n"
Expand Down