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";