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