Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 additions & 0 deletions src/windows/inc/docker_schema.h
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,22 @@ struct Network
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Network, Id, Name, Driver, Scope, Internal, IPAM, Labels);
};

struct ConnectNetworkRequest
{
using TResponse = void;
std::string Container;

NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(ConnectNetworkRequest, Container);
};

struct DisconnectNetworkRequest
{
using TResponse = void;
std::string Container;

NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(DisconnectNetworkRequest, Container);
};

struct EmptyObject
{
};
Expand Down
2 changes: 2 additions & 0 deletions src/windows/service/inc/wslc.idl
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,8 @@ interface IWSLCSession : IUnknown
HRESULT DeleteNetwork([in] LPCSTR Name);
HRESULT ListNetworks([out, size_is(, *Count)] WSLCNetworkInformation** Networks, [out] ULONG* Count);
HRESULT InspectNetwork([in] LPCSTR Name, [out] LPSTR* Output);
HRESULT AttachContainerToNetwork([in] LPCSTR ContainerId, [in] const WSLCNetworkAttachment* Attachment);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend moving those methods to WSLCContainer instead. That way we don't need to perform the container lookup in the method's implementation. That will also take care of container names and id prefixes for us

HRESULT DetachContainerFromNetwork([in] LPCSTR ContainerId, [in] LPCSTR NetworkName);
}

//
Expand Down
10 changes: 10 additions & 0 deletions src/windows/wslcsession/DockerHTTPClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,16 @@ void DockerHTTPClient::RemoveNetwork(const std::string& Name)
Transaction(verb::delete_, URL::Create("/networks/{}", Name));
}

void DockerHTTPClient::ConnectContainerToNetwork(const std::string& NetworkName, const docker_schema::ConnectNetworkRequest& Request)
{
Transaction(verb::post, URL::Create("/networks/{}/connect", NetworkName), Request);
}

void DockerHTTPClient::DisconnectContainerFromNetwork(const std::string& NetworkName, const docker_schema::DisconnectNetworkRequest& Request)
{
Transaction(verb::post, URL::Create("/networks/{}/disconnect", NetworkName), Request);
}

std::vector<docker_schema::Network> DockerHTTPClient::ListNetworks()
{
return Transaction<docker_schema::EmptyRequest, std::vector<docker_schema::Network>>(verb::get, URL::Create("/networks"));
Expand Down
2 changes: 2 additions & 0 deletions src/windows/wslcsession/DockerHTTPClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ class DockerHTTPClient
void RemoveNetwork(const std::string& Name);
std::vector<common::docker_schema::Network> ListNetworks();
common::docker_schema::Network InspectNetwork(const std::string& Name);
void ConnectContainerToNetwork(const std::string& NetworkName, const common::docker_schema::ConnectNetworkRequest& Request);
void DisconnectContainerFromNetwork(const std::string& NetworkName, const common::docker_schema::DisconnectNetworkRequest& Request);

// Image management.
struct ListImagesFilters
Expand Down
5 changes: 5 additions & 0 deletions src/windows/wslcsession/WSLCContainer.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ class WSLCContainerImpl
return m_containerFlags;
}

WSLCContainerNetworkType NetworkMode() const noexcept
{
return m_networkingMode;
}

static std::unique_ptr<WSLCContainerImpl> Create(
const WSLCContainerOptions& Options,
const std::string& Name,
Expand Down
109 changes: 109 additions & 0 deletions src/windows/wslcsession/WSLCSession.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2295,6 +2295,115 @@ try
}
CATCH_RETURN();

HRESULT WSLCSession::AttachContainerToNetwork(LPCSTR ContainerId, const WSLCNetworkAttachment* Attachment)
try
{
COMServiceExecutionContext context;

RETURN_HR_IF_NULL(E_POINTER, ContainerId);
RETURN_HR_IF_NULL(E_POINTER, Attachment);

THROW_HR_WITH_USER_ERROR_IF(E_NOTIMPL, Localization::MessageWslcContainerIpAddressNotSupported(), Attachment->ContainerIpAddress != nullptr);

THROW_HR_WITH_USER_ERROR_IF(
E_INVALIDARG, Localization::MessageWslcNetworkNameRequired(), !Attachment->NetworkName || strlen(Attachment->NetworkName) == 0);

std::string containerId = ContainerId;
std::string networkName = Attachment->NetworkName;
ValidateName(containerId.c_str(), WSLC_MAX_CONTAINER_NAME_LENGTH);
ValidateName(networkName.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH);

auto lock = m_lock.lock_shared();
THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient);
THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);

std::scoped_lock locks(m_containersLock, m_networksLock);
std::erase_if(m_containers, [](const auto& entry) { return entry.second->State() == WslcContainerStateDeleted; });

auto containerIt = m_containers.find(containerId);
THROW_HR_WITH_USER_ERROR_IF(
WSLC_E_CONTAINER_NOT_FOUND, Localization::MessageWslcContainerNotFound(containerId), containerIt == m_containers.end());

auto networkIt = m_networks.find(networkName);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we just let docker handle the network not found error for us ? That would allow us to easily support the container:<id> case

THROW_HR_WITH_USER_ERROR_IF(
WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(networkName), networkIt == m_networks.end());

const auto networkMode = containerIt->second->NetworkMode();
THROW_HR_WITH_USER_ERROR_IF(
E_INVALIDARG,
Localization::MessageWslcAdditionalNetworksRequirePrimary(),
networkMode == WSLCContainerNetworkTypeHost || networkMode == WSLCContainerNetworkTypeNone);

docker_schema::ConnectNetworkRequest request{};
request.Container = containerId;

try
{
m_dockerClient->ConnectContainerToNetwork(networkName, request);
}
catch (const DockerHTTPException& e)
{
THROW_DOCKER_USER_ERROR_MSG(e, "Failed to attach container '%hs' to network '%hs'", containerId.c_str(), networkName.c_str());
}

WSL_LOG(
"ContainerAttachedToNetwork",
TraceLoggingValue(containerId.c_str(), "ContainerId"),
TraceLoggingValue(networkName.c_str(), "NetworkName"));

return S_OK;
}
CATCH_RETURN();

HRESULT WSLCSession::DetachContainerFromNetwork(LPCSTR ContainerId, LPCSTR NetworkName)
try
{
COMServiceExecutionContext context;

RETURN_HR_IF_NULL(E_POINTER, ContainerId);
RETURN_HR_IF_NULL(E_POINTER, NetworkName);

std::string containerId = ContainerId;
std::string networkName = NetworkName;
ValidateName(containerId.c_str(), WSLC_MAX_CONTAINER_NAME_LENGTH);
ValidateName(networkName.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH);

auto lock = m_lock.lock_shared();
THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient);
THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);

std::scoped_lock locks(m_containersLock, m_networksLock);
std::erase_if(m_containers, [](const auto& entry) { return entry.second->State() == WslcContainerStateDeleted; });

auto containerIt = m_containers.find(containerId);
THROW_HR_WITH_USER_ERROR_IF(
WSLC_E_CONTAINER_NOT_FOUND, Localization::MessageWslcContainerNotFound(containerId), containerIt == m_containers.end());

auto networkIt = m_networks.find(networkName);
THROW_HR_WITH_USER_ERROR_IF(
WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(networkName), networkIt == m_networks.end());

docker_schema::DisconnectNetworkRequest request{};
request.Container = containerId;

try
{
m_dockerClient->DisconnectContainerFromNetwork(networkName, request);
}
catch (const DockerHTTPException& e)
{
THROW_DOCKER_USER_ERROR_MSG(e, "Failed to detach container '%hs' from network '%hs'", containerId.c_str(), networkName.c_str());
}

WSL_LOG(
"ContainerDetachedFromNetwork",
TraceLoggingValue(containerId.c_str(), "ContainerId"),
TraceLoggingValue(networkName.c_str(), "NetworkName"));

return S_OK;
}
CATCH_RETURN();

HRESULT WSLCSession::ListNetworks(WSLCNetworkInformation** Networks, ULONG* Count)
try
{
Expand Down
2 changes: 2 additions & 0 deletions src/windows/wslcsession/WSLCSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession
IFACEMETHOD(DeleteNetwork)(_In_ LPCSTR Name) override;
IFACEMETHOD(ListNetworks)(_Out_ WSLCNetworkInformation** Networks, _Out_ ULONG* Count) override;
IFACEMETHOD(InspectNetwork)(_In_ LPCSTR Name, _Out_ LPSTR* Output) override;
IFACEMETHOD(AttachContainerToNetwork)(_In_ LPCSTR ContainerId, _In_ const WSLCNetworkAttachment* Attachment) override;
IFACEMETHOD(DetachContainerFromNetwork)(_In_ LPCSTR ContainerId, _In_ LPCSTR NetworkName) override;

IFACEMETHOD(Terminate()) override;

Expand Down
101 changes: 100 additions & 1 deletion test/windows/WSLCTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6248,6 +6248,106 @@ class WSLCTests
ValidateCOMErrorMessage(L"Container network name is required for custom network type.");
}

WSLC_TEST_METHOD(AttachDetachContainerNetworkRoundTripTest)
{
const std::string networkName = "test-attach-detach-net";
const std::string containerName = "test-attach-detach-ctr";

LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));

WSLCDriverOption opts[] = {{"Subnet", "172.50.0.0/16"}};
WSLCNetworkOptions netOpts{};
netOpts.Name = networkName.c_str();
netOpts.Driver = "bridge";
netOpts.DriverOpts = opts;
netOpts.DriverOptsCount = ARRAYSIZE(opts);
VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&netOpts));
auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });

WSLCContainerLauncher launcher("debian:latest", containerName, {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeBridged);
auto container = launcher.Launch(*m_defaultSession);

WSLCNetworkAttachment attachment{};
attachment.NetworkName = networkName.c_str();
VERIFY_SUCCEEDED(container.Get().AttachToNetwork(&attachment));
Comment on lines +6269 to +6272

auto inspect = container.Inspect();
VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
VERIFY_IS_FALSE(inspect.NetworkSettings.Networks.at(networkName).IPAddress.empty());

VERIFY_SUCCEEDED(container.Get().DetachFromNetwork(networkName.c_str()));

auto inspectAfter = container.Inspect();
VERIFY_IS_FALSE(inspectAfter.NetworkSettings.Networks.contains(networkName));
}

WSLC_TEST_METHOD(AttachContainerEmptyNetworkNameTest)
{
const std::string containerName = "test-attach-empty-net";

WSLCContainerLauncher launcher("debian:latest", containerName, {"sleep", "99999"}, {});
auto container = launcher.Launch(*m_defaultSession);

WSLCNetworkAttachment attachment{};
attachment.NetworkName = "";
VERIFY_ARE_EQUAL(E_INVALIDARG, container.Get().AttachToNetwork(&attachment));
ValidateCOMErrorMessage(L"Network name cannot be empty.");
}

WSLC_TEST_METHOD(AttachHostOrNoneModeContainerRejectedTest)
{
const std::string networkName = "test-attach-mode-net";
const std::string hostContainerName = "test-attach-host-ctr";
const std::string noneContainerName = "test-attach-none-ctr";

LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));

WSLCDriverOption opts[] = {{"Subnet", "172.52.0.0/16"}};
WSLCNetworkOptions netOpts{};
netOpts.Name = networkName.c_str();
netOpts.Driver = "bridge";
netOpts.DriverOpts = opts;
netOpts.DriverOptsCount = ARRAYSIZE(opts);
VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&netOpts));
auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });

{
WSLCContainerLauncher launcher(
"debian:latest", hostContainerName, {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeHost);
auto container = launcher.Launch(*m_defaultSession);

WSLCNetworkAttachment attachment{};
attachment.NetworkName = networkName.c_str();
VERIFY_ARE_EQUAL(E_INVALIDARG, container.Get().AttachToNetwork(&attachment));
ValidateCOMErrorMessage(L"Additional networks are not allowed when the primary network mode is 'host' or 'none'.");
}

{
WSLCContainerLauncher launcher(
"debian:latest", noneContainerName, {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeNone);
auto container = launcher.Launch(*m_defaultSession);

WSLCNetworkAttachment attachment{};
attachment.NetworkName = networkName.c_str();
VERIFY_ARE_EQUAL(E_INVALIDARG, container.Get().AttachToNetwork(&attachment));
ValidateCOMErrorMessage(L"Additional networks are not allowed when the primary network mode is 'host' or 'none'.");
}
}

WSLC_TEST_METHOD(AttachContainerWithIpAddressRejectedTest)
{
const std::string containerName = "test-attach-ip-ctr";

WSLCContainerLauncher launcher("debian:latest", containerName, {"sleep", "99999"}, {});
auto container = launcher.Launch(*m_defaultSession);

WSLCNetworkAttachment attachment{};
attachment.NetworkName = "bridge";
attachment.ContainerIpAddress = "10.0.0.5";
VERIFY_ARE_EQUAL(E_NOTIMPL, container.Get().AttachToNetwork(&attachment));
ValidateCOMErrorMessage(L"ContainerIpAddress is not yet supported.");
}

WSLC_TEST_METHOD(ContainerNetworkModeHappyPathTest)
{
// Start container A on the default (bridged) network, then start container B sharing A's
Expand Down Expand Up @@ -6410,7 +6510,6 @@ class WSLCTests
VERIFY_ARE_EQUAL(E_NOTIMPL, hr);
ValidateCOMErrorMessage(L"ContainerIpAddress is not yet supported.");
}

WSLC_TEST_METHOD(ContainerInspect)
{
// Helper to verify port mappings.
Expand Down
Loading