Return non vhd volumes in container inspect (#41335)

* Return non-vhd volumes in container inspect * Prepare for PR * Use an empty string for non applicable sources

Blue committed Aug 14, 2026 at 19:39 UTC c009deb77c2549c8a55f8b8c084ecabac495a67e
5 files changed +114 -43
src/windows/inc/docker_schema.h
+4 -2
@@ -438,11 +438,12 @@ struct ContainerConfig
438 struct InspectMount
439 {
440 std::string Type;
441 + std::string Name;
442 std::string Source;
443 std::string Destination;
444 bool RW{};
445
445 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectMount, Type, Source, Destination, RW);
446 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectMount, Type, Name, Source, Destination, RW);
447 };
448
449 struct InspectContainer
@@ -454,9 +455,10 @@ struct InspectContainer
455 ContainerInspectState State;
456 ContainerConfig Config;
457 HostConfig HostConfig;
458 + std::vector<InspectMount> Mounts;
459 NetworkSettings NetworkSettings;
460
459 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectContainer, Id, Name, Created, Image, State, Config, HostConfig, NetworkSettings);
461 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectContainer, Id, Name, Created, Image, State, Config, HostConfig, Mounts, NetworkSettings);
462 };
463
464 struct InspectExec
src/windows/inc/wslc_schema.h
+2 -2
@@ -30,13 +30,13 @@ struct InspectPortBinding
30
31 struct InspectMount
32 {
33 - // TODO: Support different mount types (plan9/VHD) when VHD volumes are implemented.
33 std::string Type;
34 + std::string Name;
35 std::string Source;
36 std::string Destination;
37 bool ReadWrite{};
38
39 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectMount, Type, Source, Destination, ReadWrite);
39 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectMount, Type, Name, Source, Destination, ReadWrite);
40 };
41
42 struct HealthcheckResult
src/windows/wslcsession/WSLCContainer.cpp
+24 -3
@@ -1610,12 +1610,11 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec
1610 wslcInspect.Ports[portKey].push_back(std::move(portBinding));
1611 }
1612
1613 - // Map volume mounts using WSLC's host-side data.
1614 - wslcInspect.Mounts.reserve(m_mountedVolumes.size() + dockerInspect.HostConfig.Tmpfs.size());
1613 + // Map mounts without exposing Linux paths from the utility VM.
1614 + wslcInspect.Mounts.reserve(m_mountedVolumes.size() + dockerInspect.Mounts.size() + dockerInspect.HostConfig.Tmpfs.size());
1615 for (const auto& volume : m_mountedVolumes)
1616 {
1617 wslc_schema::InspectMount mountInfo{};
1618 - // TODO: Support different mount types (plan9/VHD) when VHD volumes are implemented.
1618 mountInfo.Type = "bind";
1619
1620 // For file mounts, reconstruct the original host path from the parent directory and filename.
@@ -1635,6 +1634,28 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec
1634 wslcInspect.Mounts.push_back(std::move(mountInfo));
1635 }
1636
1637 + for (const auto& volume : dockerInspect.Mounts)
1638 + {
1639 + // This block covers non-vhd volumes. This includes:
1640 + // - Guest volumes mounted via -v
1641 + // - Volumes mounted as part of the image (via VOLUME)
1642 + //
1643 + // TODO: Return mounts once --mount is implemented.
1644 +
1645 + if (volume.Type != "volume")
1646 + {
1647 + continue;
1648 + }
1649 +
1650 + wslc_schema::InspectMount mountInfo{};
1651 + mountInfo.Type = volume.Type;
1652 + mountInfo.Name = volume.Name;
1653 + mountInfo.Destination = volume.Destination;
1654 + mountInfo.ReadWrite = volume.RW;
1655 +
1656 + wslcInspect.Mounts.push_back(std::move(mountInfo));
1657 + }
1658 +
1659 // Map tmpfs mounts from Docker inspect data.
1660 for (const auto& entry : dockerInspect.HostConfig.Tmpfs)
1661 {
test/windows/WSLCTests.cpp
+70 -5
@@ -2389,12 +2389,21 @@ class WSLCTests
2389 ExpectImagePresent(*m_defaultSession, "wslc-test-build:latest");
2390
2391 const std::vector<WSLCFilter> anonymousVolumeFilters = {{"driver", "guest"}, {"label", "com.docker.volume.anonymous="}};
2392 + auto verifyAnonymousVolumeMount = [](const auto& inspect) {
2393 + VERIFY_ARE_EQUAL(inspect.Mounts.size(), 1u);
2394 + VERIFY_ARE_EQUAL(inspect.Mounts[0].Type, "volume");
2395 + VERIFY_IS_FALSE(inspect.Mounts[0].Name.empty());
2396 + VERIFY_IS_TRUE(inspect.Mounts[0].Source.empty());
2397 + VERIFY_ARE_EQUAL(inspect.Mounts[0].Destination, "/volume");
2398 + VERIFY_IS_TRUE(inspect.Mounts[0].ReadWrite);
2399 + };
2400
2401 // Session-restart scenario: an anonymous volume-backed container survives a session reset.
2402 {
2403 WSLCContainerLauncher launcher("wslc-test-build:latest", "wslc-test-anonymous-volume", {"test", "-d", "/volume"});
2404 auto container = launcher.Launch(*m_defaultSession);
2405 container.SetDeleteOnClose(false);
2406 + verifyAnonymousVolumeMount(container.Inspect());
2407
2408 auto containerId = container.Id();
2409
@@ -2416,6 +2425,10 @@ class WSLCTests
2425
2426 VERIFY_ARE_EQUAL(containers.size(), 1);
2427 VERIFY_ARE_EQUAL(containers[0].Id, containerId);
2428 +
2429 + auto recoveredContainer = OpenContainer(m_defaultSession.get(), containerId);
2430 + recoveredContainer.SetDeleteOnClose(false);
2431 + verifyAnonymousVolumeMount(recoveredContainer.Inspect());
2432 }
2433
2434 // Delete container without WSLCDeleteFlagsDeleteVolumes -> anonymous volume is leaked.
@@ -2467,6 +2480,43 @@ class WSLCTests
2480 }
2481 }
2482
2483 + WSLC_TEST_METHOD(ContainerInspectDockerfileVolumes)
2484 + {
2485 + const auto contextDir = std::filesystem::current_path() / "container-inspect-volume-build-context";
2486 + constexpr auto imageName = "wslc-test-container-inspect-volume:latest";
2487 + std::filesystem::create_directories(contextDir);
2488 +
2489 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2490 + std::error_code ec;
2491 + std::filesystem::remove_all(contextDir, ec);
2492 + LOG_IF_FAILED(DeleteImageNoThrow(imageName, WSLCDeleteImageFlagsForce).first);
2493 + });
2494 +
2495 + {
2496 + std::ofstream dockerfile(contextDir / "Dockerfile");
2497 + dockerfile << "FROM debian:latest\n";
2498 + dockerfile << "VOLUME [\"/volume-a\", \"/volume-b\"]\n";
2499 + }
2500 +
2501 + VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, imageName));
2502 +
2503 + WSLCContainerLauncher launcher(imageName, "wslc-test-container-inspect-volume");
2504 + auto container = launcher.Create(*m_defaultSession);
2505 + const auto inspect = container.Inspect();
2506 +
2507 + VERIFY_ARE_EQUAL(inspect.Mounts.size(), 2u);
2508 + for (const auto* destination : {"/volume-a", "/volume-b"})
2509 + {
2510 + const auto mount =
2511 + std::ranges::find_if(inspect.Mounts, [&](const auto& entry) { return entry.Destination == destination; });
2512 + VERIFY_IS_TRUE(mount != inspect.Mounts.end());
2513 + VERIFY_ARE_EQUAL(mount->Type, "volume");
2514 + VERIFY_IS_FALSE(mount->Name.empty());
2515 + VERIFY_IS_TRUE(mount->Source.empty());
2516 + VERIFY_IS_TRUE(mount->ReadWrite);
2517 + }
2518 + }
2519 +
2520 WSLC_TEST_METHOD(TagImage)
2521 {
2522 auto runTagImage = [&](LPCSTR Image, LPCSTR Repo, LPCSTR Tag) {
@@ -8395,10 +8445,11 @@ class WSLCTests
8445 };
8446
8447 // Helper to verify mounts.
8398 - auto expectMounts = [&](const auto& actualMounts, const std::vector<std::tuple<std::string, std::string, bool>>& expectedMounts) {
8448 + auto expectMounts = [&](const auto& actualMounts,
8449 + const std::vector<std::tuple<std::string, std::string, std::optional<std::filesystem::path>, bool>>& expectedMounts) {
8450 VERIFY_ARE_EQUAL(actualMounts.size(), expectedMounts.size());
8451
8401 - for (const auto& [expectedDest, expectedType, expectedReadWrite] : expectedMounts)
8452 + for (const auto& [expectedDest, expectedType, expectedSource, expectedReadWrite] : expectedMounts)
8453 {
8454 auto it = std::ranges::find_if(actualMounts, [&](const auto& mount) { return mount.Destination == expectedDest; });
8455 if (it == actualMounts.end())
@@ -8410,9 +8461,15 @@ class WSLCTests
8461 VERIFY_IS_FALSE(it->Type.empty());
8462 VERIFY_ARE_EQUAL(it->Type, expectedType);
8463
8413 - if (expectedType != "tmpfs")
8464 + if (expectedSource.has_value())
8465 + {
8466 + const std::filesystem::path actualSource(it->Source);
8467 + VERIFY_IS_TRUE(actualSource.is_absolute());
8468 + VERIFY_IS_TRUE(std::filesystem::equivalent(actualSource, expectedSource.value()));
8469 + }
8470 + else
8471 {
8415 - VERIFY_IS_FALSE(it->Source.empty());
8472 + VERIFY_IS_TRUE(it->Source.empty());
8473 }
8474 VERIFY_ARE_EQUAL(it->ReadWrite, expectedReadWrite);
8475 }
@@ -8422,6 +8479,7 @@ class WSLCTests
8479 {
8480 auto testFolder = std::filesystem::current_path() / "test-inspect-volume";
8481 auto testFolderReadOnly = std::filesystem::current_path() / "test-inspect-volume-ro";
8482 + const std::string guestVolumeName = "test-container-inspect-guest-volume";
8483
8484 std::filesystem::create_directories(testFolder);
8485 std::filesystem::create_directories(testFolderReadOnly);
@@ -8430,8 +8488,11 @@ class WSLCTests
8488 std::error_code ec;
8489 std::filesystem::remove_all(testFolder, ec);
8490 std::filesystem::remove_all(testFolderReadOnly, ec);
8491 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(guestVolumeName.c_str()));
8492 });
8493
8494 + CreateNamedVolume(guestVolumeName, "guest");
8495 +
8496 WSLCContainerLauncher launcher("debian:latest", "test-container-inspect", {"sleep", "99999"}, {}, "bridge");
8497
8498 launcher.AddPort(1234, 8000, AF_INET);
@@ -8439,6 +8500,7 @@ class WSLCTests
8500 launcher.AddPort(1236, 8001, AF_INET);
8501 launcher.AddVolume(testFolder.wstring(), "/test-volume", false);
8502 launcher.AddVolume(testFolderReadOnly.wstring(), "/test-volume-ro", true);
8503 + launcher.AddNamedVolume(guestVolumeName, "/test-guest-volume", false);
8504 launcher.AddTmpfs("/mnt/wslc-tmpfs-inspect", "");
8505
8506 auto container = launcher.Launch(*m_defaultSession);
@@ -8466,7 +8528,10 @@ class WSLCTests
8528 // Verify mounts match what we configured.
8529 expectMounts(
8530 details.Mounts,
8469 - {{"/test-volume", "bind", true}, {"/test-volume-ro", "bind", false}, {"/mnt/wslc-tmpfs-inspect", "tmpfs", true}});
8531 + {{"/test-volume", "bind", testFolder, true},
8532 + {"/test-volume-ro", "bind", testFolderReadOnly, false},
8533 + {"/test-guest-volume", "volume", std::nullopt, true},
8534 + {"/mnt/wslc-tmpfs-inspect", "tmpfs", std::nullopt, true}});
8535
8536 VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
8537 VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
test/windows/wslc/e2e/WSLCE2EContainerRemoveTests.cpp
+14 -31
@@ -163,12 +163,10 @@ class WSLCE2EContainerRemoveTests
163
164 WSLC_TEST_METHOD(WSLCE2E_Container_Remove_Volumes_RemovesAnonymousVolume)
165 {
166 - const auto volumesBefore = ListVolumeNames();
167 -
166 auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, AnonymousVolumeImage.NameAndTag()));
167 result.Verify({.Stderr = L"", .ExitCode = 0});
168
171 - const auto anonymousVolume = GetNewAnonymousVolumeName(volumesBefore);
169 + const auto anonymousVolume = GetAnonymousVolumeName(WslcContainerName);
170 VerifyVolumeIsListed(anonymousVolume);
171
172 result = RunWslc(std::format(L"container remove --volumes {}", WslcContainerName));
@@ -180,12 +178,10 @@ class WSLCE2EContainerRemoveTests
178
179 WSLC_TEST_METHOD(WSLCE2E_Container_Remove_WithoutVolumes_KeepsAnonymousVolume)
180 {
183 - const auto volumesBefore = ListVolumeNames();
184 -
181 auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, AnonymousVolumeImage.NameAndTag()));
182 result.Verify({.Stderr = L"", .ExitCode = 0});
183
188 - const auto anonymousVolume = GetNewAnonymousVolumeName(volumesBefore);
184 + const auto anonymousVolume = GetAnonymousVolumeName(WslcContainerName);
185 auto cleanup = wil::scope_exit([&]() { EnsureVolumeDoesNotExist(anonymousVolume); });
186 VerifyVolumeIsListed(anonymousVolume);
187
@@ -213,15 +209,13 @@ class WSLCE2EContainerRemoveTests
209
210 WSLC_TEST_METHOD(WSLCE2E_Container_Remove_Volumes_Force_RunningContainer)
211 {
216 - const auto volumesBefore = ListVolumeNames();
217 -
212 auto result =
213 RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, AnonymousVolumeImage.NameAndTag()));
214 result.Verify({.Stderr = L"", .ExitCode = 0});
215 const auto containerId = result.GetStdoutOneLine();
216 VERIFY_IS_FALSE(containerId.empty());
217
224 - const auto anonymousVolume = GetNewAnonymousVolumeName(volumesBefore);
218 + const auto anonymousVolume = GetAnonymousVolumeName(WslcContainerName);
219 VerifyVolumeIsListed(anonymousVolume);
220 VerifyContainerIsListed(containerId, L"running");
221
@@ -249,32 +243,21 @@ private:
243 result.Verify({.ExitCode = 0});
244 }
245
252 - static std::vector<std::wstring> ListVolumeNames()
253 - {
254 - auto result = RunWslc(L"volume list --quiet");
255 - result.Verify({.Stderr = L"", .ExitCode = 0});
256 - return result.GetStdoutLines();
257 - }
258 -
259 - // A before/after diff is the only option: `volume list` has no --filter and reports no labels.
260 - static std::wstring GetNewAnonymousVolumeName(const std::vector<std::wstring>& before)
246 + static std::wstring GetAnonymousVolumeName(const std::wstring& containerName)
247 {
262 - std::vector<std::wstring> added;
263 - for (const auto& name : ListVolumeNames())
264 - {
265 - if (std::find(before.begin(), before.end(), name) == before.end())
266 - {
267 - added.push_back(name);
268 - }
269 - }
248 + const auto inspect = InspectContainer(containerName);
249 + const auto mount = std::ranges::find_if(inspect.Mounts, [](const auto& entry) { return entry.Type == "volume"; });
250
271 - VERIFY_ARE_EQUAL(static_cast<size_t>(1), added.size());
251 + VERIFY_IS_TRUE(mount != inspect.Mounts.end(), L"Container inspect did not return the anonymous volume mount");
252 + VERIFY_IS_FALSE(mount->Name.empty(), L"Container inspect returned an empty anonymous volume name");
253 + VERIFY_IS_TRUE(mount->Source.empty(), L"Anonymous volume source must be empty");
254 + VERIFY_ARE_EQUAL(mount->Destination, "/data");
255
256 + const auto volumeName = wsl::shared::string::MultiByteToWide(mount->Name);
257 VERIFY_IS_TRUE(
274 - InspectVolume(added.front()).Labels.contains("com.docker.volume.anonymous"),
275 - L"The volume created by the container is not an anonymous volume");
276 -
277 - return added.front();
258 + InspectVolume(volumeName).Labels.contains("com.docker.volume.anonymous"),
259 + L"The volume returned by container inspect is not anonymous");
260 + return volumeName;
261 }
262
263 const std::wstring WslcContainerName = L"wslc-test-container";