@samitouri / QOSAMI-WSL / commits / 441ec875

Inherit image labels on containers  (#41246)

beena352 committed Aug 6, 2026 at 14:43 UTC 441ec87521aa912f8461ff1ccebaa0c7aa561d72
5 files changed +192 -12
src/windows/inc/docker_schema.h
+3 -1
@@ -429,8 +429,10 @@ struct ContainerConfig
429 std::optional<std::string> StopSignal;
430 std::optional<int> StopTimeout;
431 std::optional<HealthConfig> Healthcheck;
432 + // Optional because dockerd may emit `"Labels": null` for containers with no merged labels.
433 + std::optional<std::map<std::string, std::string>> Labels;
434
433 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Image, User, WorkingDir, Env, Cmd, Entrypoint, StopSignal, StopTimeout, Healthcheck);
435 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Image, User, WorkingDir, Env, Cmd, Entrypoint, StopSignal, StopTimeout, Healthcheck, Labels);
436 };
437
438 struct InspectMount
src/windows/inc/wslc_schema.h
+2 -1
@@ -109,8 +109,9 @@ struct ContainerConfig
109 std::string WorkingDir;
110 std::optional<int> StopTimeout;
111 std::optional<HealthConfig> Healthcheck;
112 + std::map<std::string, std::string> Labels;
113
113 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Env, Cmd, Entrypoint, User, WorkingDir, StopTimeout, Healthcheck);
114 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Env, Cmd, Entrypoint, User, WorkingDir, StopTimeout, Healthcheck, Labels);
115 };
116
117 struct InspectEndpointIPAMConfig
src/windows/wslcsession/WSLCContainer.cpp
+22 -8
@@ -49,6 +49,7 @@ using wsl::windows::service::wslc::VMPortMapping;
49 using wsl::windows::service::wslc::WSLCContainer;
50 using wsl::windows::service::wslc::WSLCContainerImpl;
51 using wsl::windows::service::wslc::WSLCContainerMetadata;
52 +using wsl::windows::service::wslc::WSLCContainerMetadataLabel;
53 using wsl::windows::service::wslc::WSLCContainerMetadataV1;
54 using wsl::windows::service::wslc::WSLCExecutionContext;
55 using wsl::windows::service::wslc::WSLCPortMapping;
@@ -509,6 +510,17 @@ std::string SerializeContainerMetadata(const WSLCContainerMetadataV1& metadata)
510 return wsl::shared::ToJson(wrapper);
511 }
512
513 +std::map<std::string, std::string> StripInternalLabels(std::map<std::string, std::string> labels)
514 +{
515 + labels.erase(WSLCContainerMetadataLabel);
516 + return labels;
517 +}
518 +
519 +std::map<std::string, std::string> StripInternalLabels(std::optional<std::map<std::string, std::string>>&& labels)
520 +{
521 + return StripInternalLabels(std::move(labels).value_or(std::map<std::string, std::string>{}));
522 +}
523 +
524 void ProcessNamedVolumes(const WSLCContainerOptions& containerOptions, wsl::windows::common::docker_schema::CreateContainer& request)
525 {
526 THROW_HR_IF(E_INVALIDARG, containerOptions.NamedVolumesCount > 0 && containerOptions.NamedVolumes == nullptr);
@@ -1624,7 +1636,8 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec
1636 wslcInspect.Mounts.push_back(std::move(mountInfo));
1637 }
1638
1627 - // Map labels. m_labels should already exclude internal metadata labels.
1639 + // Config.Labels is the Docker-shape location; top-level Labels is a legacy alias.
1640 + wslcInspect.Config.Labels = m_labels;
1641 wslcInspect.Labels = m_labels;
1642
1643 // Map per-endpoint network settings from Docker inspect data.
@@ -2029,7 +2042,7 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2042 .HostIp = e.VmMapping.IsIPv6() ? "::" : "0.0.0.0", .HostPort = std::to_string(hostPort)});
2043 }
2044
2032 - auto labels = ParseKeyValuePairs(containerOptions.Labels, containerOptions.LabelsCount, WSLCContainerMetadataLabel);
2045 + auto requestedLabels = ParseKeyValuePairs(containerOptions.Labels, containerOptions.LabelsCount, WSLCContainerMetadataLabel);
2046
2047 // Build WSLC metadata to store in a label for recovery on Open().
2048 WSLCContainerMetadataV1 metadata;
@@ -2043,7 +2056,7 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2056 }
2057
2058 request.Labels[WSLCContainerMetadataLabel] = SerializeContainerMetadata(metadata);
2046 - request.Labels.insert(labels.begin(), labels.end());
2059 + request.Labels.insert(requestedLabels.begin(), requestedLabels.end());
2060
2061 // Send the request to docker.
2062 auto result = DockerClient.CreateContainer(request, containerName);
@@ -2106,6 +2119,8 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2119 namedVolumes.emplace_back(containerOptions.NamedVolumes[i].Name);
2120 }
2121
2122 + auto mergedLabels = StripInternalLabels(std::move(inspectData.Config.Labels));
2123 +
2124 auto container = std::make_shared<WSLCContainerImpl>(
2125 wslcSession,
2126 runtime,
@@ -2117,7 +2132,7 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2132 std::move(volumes),
2133 std::move(namedVolumes),
2134 std::move(mappedPorts),
2120 - std::move(labels),
2135 + std::move(mergedLabels),
2136 std::move(OnDeleted),
2137 WslcContainerStateCreated,
2138 ParseDockerTimestamp(inspectData.Created),
@@ -2153,19 +2168,18 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
2168 }
2169 }
2170
2156 - auto labels(dockerContainer.Labels);
2157 - auto metadataIt = labels.find(WSLCContainerMetadataLabel);
2171 + auto metadataIt = dockerContainer.Labels.find(WSLCContainerMetadataLabel);
2172
2173 THROW_HR_IF_MSG(
2174 E_INVALIDARG,
2161 - metadataIt == labels.end(),
2175 + metadataIt == dockerContainer.Labels.end(),
2176 "Cannot open WSLC container %hs: missing WSLC metadata label",
2177 dockerContainer.Id.c_str());
2178
2179 WI_ASSERT(dockerContainer.State != ContainerState::Running);
2180
2181 auto metadata = ParseContainerMetadata(metadataIt->second.c_str());
2168 - labels.erase(metadataIt);
2182 + auto labels = StripInternalLabels(dockerContainer.Labels);
2183
2184 // Docker treats empty NetworkMode as the default (bridge).
2185 std::string networkMode = dockerContainer.HostConfig.NetworkMode.empty() ? std::string{"bridge"} : dockerContainer.HostConfig.NetworkMode;
test/windows/WSLCTests.cpp
+119 -2
@@ -10654,6 +10654,19 @@ class WSLCTests
10654 // Docker labels do not have a size limit, so test with a very large label value to validate that the API can handle it.
10655 std::map<std::string, std::string> labels = {{"key1", "value1"}, {"key2", std::string(10000, 'a')}};
10656
10657 + // Contains-style rather than exact-equality so the test stays green if the base image ever ships with its own labels.
10658 + auto verifyUserLabelsPresent = [&](const std::map<std::string, std::string>& observed) {
10659 + for (const auto& [key, value] : labels)
10660 + {
10661 + auto it = observed.find(key);
10662 + VERIFY_IS_TRUE(it != observed.end());
10663 + if (it != observed.end())
10664 + {
10665 + VERIFY_ARE_EQUAL(value, it->second);
10666 + }
10667 + }
10668 + };
10669 +
10670 // Test valid labels
10671 {
10672 WSLCContainerLauncher launcher("debian:latest", "test-labels", {"echo", "OK"});
@@ -10664,7 +10677,9 @@ class WSLCTests
10677 }
10678
10679 auto container = launcher.Launch(*m_defaultSession);
10667 - VERIFY_ARE_EQUAL(labels, container.Labels());
10680 + const auto containerLabels = container.Labels();
10681 + verifyUserLabelsPresent(containerLabels);
10682 + VERIFY_IS_TRUE(containerLabels.find("com.microsoft.wsl.container.metadata") == containerLabels.end());
10683
10684 // Keep the container alive after the handle is dropped so we can validate labels are persisted across sessions.
10685 container.SetDeleteOnClose(false);
@@ -10676,7 +10691,16 @@ class WSLCTests
10691
10692 // Validate that labels are correctly loaded.
10693 auto container = OpenContainer(m_defaultSession.get(), "test-labels");
10679 - VERIFY_ARE_EQUAL(labels, container.Labels());
10694 + const auto containerLabels = container.Labels();
10695 + verifyUserLabelsPresent(containerLabels);
10696 +
10697 + const std::string c_metadataLabel = "com.microsoft.wsl.container.metadata";
10698 + VERIFY_IS_TRUE(containerLabels.find(c_metadataLabel) == containerLabels.end());
10699 + const auto inspect = container.Inspect();
10700 + verifyUserLabelsPresent(inspect.Config.Labels);
10701 + verifyUserLabelsPresent(inspect.Labels);
10702 + VERIFY_ARE_EQUAL(inspect.Config.Labels, inspect.Labels);
10703 + VERIFY_IS_TRUE(inspect.Config.Labels.find(c_metadataLabel) == inspect.Config.Labels.end());
10704 }
10705
10706 // Test nullptr key
@@ -10736,6 +10760,99 @@ class WSLCTests
10760 }
10761 }
10762
10763 + // Regression: containers must inherit their base image's LABEL entries (Docker parity), with user --label
10764 + // winning on key conflict. The dockerd daemon does the merge; wslc reads it back from InspectContainer.
10765 + WSLC_TEST_METHOD(ContainerLabelsInheritedFromImage)
10766 + {
10767 + const std::string c_imageTag = "wslc-test-labels-inherited:latest";
10768 + const std::string c_imageLabelKey = "com.microsoft.wsl.test.image-label";
10769 + const std::string c_imageLabelValue = "from-image";
10770 + const std::string c_sharedLabelKey = "com.microsoft.wsl.test.shared";
10771 + const std::string c_sharedImageValue = "image-wins-if-no-override";
10772 + const std::string c_sharedUserValue = "user-wins";
10773 + const std::string c_userOnlyLabelKey = "com.microsoft.wsl.test.user-only";
10774 + const std::string c_userOnlyLabelValue = "from-user";
10775 + const std::string c_metadataLabel = "com.microsoft.wsl.container.metadata";
10776 + const std::string c_userOverrideContainerName = "test-labels-inherited-user-override";
10777 +
10778 + auto contextDir = std::filesystem::current_path() / "build-context-labels-inherited";
10779 + std::filesystem::create_directories(contextDir);
10780 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
10781 + LOG_IF_FAILED(DeleteImageNoThrow(c_imageTag.c_str(), WSLCDeleteImageFlagsForce).first);
10782 +
10783 + std::error_code ec;
10784 + std::filesystem::remove_all(contextDir, ec);
10785 + });
10786 +
10787 + {
10788 + std::ofstream dockerfile(contextDir / "Dockerfile");
10789 + dockerfile << "FROM debian:latest\n";
10790 + dockerfile << "LABEL " << c_imageLabelKey << "=" << c_imageLabelValue << "\n";
10791 + dockerfile << "LABEL " << c_sharedLabelKey << "=" << c_sharedImageValue << "\n";
10792 + }
10793 +
10794 + VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, c_imageTag.c_str()));
10795 + ExpectImagePresent(*m_defaultSession, c_imageTag.c_str());
10796 +
10797 + // Image-only label survives on the container (bug repro).
10798 + {
10799 + WSLCContainerLauncher launcher(c_imageTag.c_str(), "test-labels-inherited-image-only", {"echo", "OK"});
10800 + auto container = launcher.Launch(*m_defaultSession);
10801 +
10802 + const auto containerLabels = container.Labels();
10803 + const auto inspect = container.Inspect();
10804 +
10805 + VERIFY_IS_TRUE(containerLabels.contains(c_imageLabelKey));
10806 + VERIFY_ARE_EQUAL(c_imageLabelValue, containerLabels.at(c_imageLabelKey));
10807 + VERIFY_IS_TRUE(inspect.Config.Labels.contains(c_imageLabelKey));
10808 + VERIFY_ARE_EQUAL(c_imageLabelValue, inspect.Config.Labels.at(c_imageLabelKey));
10809 +
10810 + VERIFY_IS_TRUE(containerLabels.find(c_metadataLabel) == containerLabels.end());
10811 + }
10812 +
10813 + // Persist across a session reset so the second block exercises the Open() codepath, which reads labels
10814 + // from the /containers/json list-API — a different deserialization than InspectContainer.Config.Labels.
10815 + {
10816 + WSLCContainerLauncher launcher(c_imageTag.c_str(), c_userOverrideContainerName.c_str(), {"echo", "OK"});
10817 + launcher.AddLabel(c_sharedLabelKey, c_sharedUserValue);
10818 + launcher.AddLabel(c_userOnlyLabelKey, c_userOnlyLabelValue);
10819 + auto container = launcher.Launch(*m_defaultSession);
10820 +
10821 + const auto containerLabels = container.Labels();
10822 +
10823 + VERIFY_IS_TRUE(containerLabels.contains(c_imageLabelKey));
10824 + VERIFY_ARE_EQUAL(c_imageLabelValue, containerLabels.at(c_imageLabelKey));
10825 +
10826 + VERIFY_IS_TRUE(containerLabels.contains(c_sharedLabelKey));
10827 + VERIFY_ARE_EQUAL(c_sharedUserValue, containerLabels.at(c_sharedLabelKey));
10828 +
10829 + VERIFY_IS_TRUE(containerLabels.contains(c_userOnlyLabelKey));
10830 + VERIFY_ARE_EQUAL(c_userOnlyLabelValue, containerLabels.at(c_userOnlyLabelKey));
10831 +
10832 + container.SetDeleteOnClose(false);
10833 + }
10834 +
10835 + {
10836 + ResetTestSession();
10837 +
10838 + auto reopened = OpenContainer(m_defaultSession.get(), c_userOverrideContainerName.c_str());
10839 + const auto reopenedLabels = reopened.Labels();
10840 +
10841 + VERIFY_IS_TRUE(reopenedLabels.contains(c_imageLabelKey));
10842 + VERIFY_ARE_EQUAL(c_imageLabelValue, reopenedLabels.at(c_imageLabelKey));
10843 + VERIFY_IS_TRUE(reopenedLabels.contains(c_sharedLabelKey));
10844 + VERIFY_ARE_EQUAL(c_sharedUserValue, reopenedLabels.at(c_sharedLabelKey));
10845 + VERIFY_IS_TRUE(reopenedLabels.contains(c_userOnlyLabelKey));
10846 + VERIFY_ARE_EQUAL(c_userOnlyLabelValue, reopenedLabels.at(c_userOnlyLabelKey));
10847 +
10848 + VERIFY_IS_TRUE(reopenedLabels.find(c_metadataLabel) == reopenedLabels.end());
10849 +
10850 + const auto reopenedInspect = reopened.Inspect();
10851 + VERIFY_IS_TRUE(reopenedInspect.Config.Labels.contains(c_imageLabelKey));
10852 + VERIFY_ARE_EQUAL(c_imageLabelValue, reopenedInspect.Config.Labels.at(c_imageLabelKey));
10853 + }
10854 + }
10855 +
10856 WSLC_TEST_METHOD(ContainerResourceLimits)
10857 {
10858 // Validate per-container memory limit is applied (cgroup v2: /sys/fs/cgroup/memory.max).
test/windows/wslc/e2e/WSLCE2EInspectTests.cpp
+46
@@ -155,6 +155,51 @@ class WSLCE2EInspectTests
155 wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectContainer>>(result.Stdout.value().c_str());
156 VERIFY_ARE_EQUAL(1u, inspectData.size());
157 VERIFY_ARE_EQUAL(WslcContainerName, wsl::shared::string::MultiByteToWide(inspectData[0].Name));
158 +
159 + // Config.Labels must be present in the emitted JSON even when empty.
160 + auto json = nlohmann::json::parse(wsl::shared::string::WideToMultiByte(result.Stdout.value()));
161 + VERIFY_IS_TRUE(json.is_array() && !json.empty());
162 + VERIFY_IS_TRUE(json[0].contains("Config") && json[0]["Config"].contains("Labels"));
163 + }
164 +
165 + WSLC_TEST_METHOD(WSLCE2E_Inspect_Container_InheritsImageLabels)
166 + {
167 + auto imageCleanup = wil::scope_exit([&]() { EnsureImageIsDeleted(LabelInheritImage); });
168 + auto testRoot = std::filesystem::current_path() / L"wslc-e2e-inspect-inherit-labels";
169 + auto cleanup = SetupTestDirectory(testRoot);
170 +
171 + auto contextDir = testRoot / L"context";
172 + std::error_code ec;
173 + std::filesystem::create_directories(contextDir, ec);
174 + THROW_HR_IF(E_FAIL, ec.value() != 0 || !std::filesystem::exists(contextDir));
175 +
176 + auto dockerfilePath = testRoot / L"Dockerfile";
177 + WriteTestFileContent(
178 + dockerfilePath,
179 + "FROM debian:latest\n"
180 + "LABEL com.microsoft.wsl.test.inherit-me=from-image\n"
181 + "CMD [\"echo\", \"ok\"]\n");
182 +
183 + auto buildResult = RunWslc(std::format(
184 + L"build \"{}\" -f \"{}\" -t {}", contextDir.wstring(), dockerfilePath.wstring(), LabelInheritImage.NameAndTag()));
185 + buildResult.Verify({.Stdout = L"", .ExitCode = 0});
186 +
187 + EnsureContainerDoesNotExist(WslcContainerName);
188 + auto createResult = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, LabelInheritImage.NameAndTag()));
189 + createResult.Verify({.Stderr = L"", .ExitCode = 0});
190 +
191 + auto result = RunWslc(std::format(L"inspect {}", WslcContainerName));
192 + result.Verify({.Stderr = L"", .ExitCode = 0});
193 + auto inspectData =
194 + wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectContainer>>(result.Stdout.value().c_str());
195 + VERIFY_ARE_EQUAL(1u, inspectData.size());
196 +
197 + const auto& configLabels = inspectData[0].Config.Labels;
198 + auto inheritedIt = configLabels.find("com.microsoft.wsl.test.inherit-me");
199 + VERIFY_IS_TRUE(inheritedIt != configLabels.end());
200 + VERIFY_ARE_EQUAL(std::string("from-image"), inheritedIt->second);
201 +
202 + VERIFY_IS_TRUE(configLabels.find("com.microsoft.wsl.container.metadata") == configLabels.end());
203 }
204
205 WSLC_TEST_METHOD(WSLCE2E_Inspect_Volume_Success)
@@ -362,5 +407,6 @@ private:
407 const TestImage& InvalidImage = InvalidTestImage();
408 const std::wstring WslcVolumeName = L"wslc-inspect-test-volume";
409 const std::wstring WslcNetworkName = L"wslc-inspect-test-network";
410 + const TestImage LabelInheritImage{L"wslc-e2e-inspect-inherit-labels", L"latest", L""};
411 };
412 } // namespace WSLCE2ETests