Fix wslc image inspect missing RootFS, ExposedPorts, Volumes, and StopSignal (#40811)
beena352 committed
Jun 17, 2026 at 13:56 UTC
26d4698fb19578aa92fa9b827a5e6738eb50abf6
8 files changed
+153
-54
src/shared/inc/JsonUtils.h
+13
@@ -34,6 +34,19 @@ namespace wsl::shared {
34
35
constexpr int c_jsonPrettyPrintIndent = 2;
36
37
+struct EmptyObject
38
+{
39
+};
40
+
41
+inline void to_json(nlohmann::json& j, const EmptyObject&)
42
+{
43
+ j = nlohmann::json::object();
44
+}
45
+
46
+inline void from_json(const nlohmann::json&, EmptyObject&)
47
+{
48
+}
49
+
50
template <typename T>
51
std::string ToJson(const T& Value, int indent = -1)
52
{
src/windows/inc/docker_schema.h
+2
-17
@@ -20,6 +20,8 @@ Abstract:
20
21
namespace wsl::windows::common::docker_schema {
22
23
+using wsl::shared::EmptyObject;
24
+
25
struct CreatedContainer
26
{
27
std::string Id;
@@ -170,23 +172,6 @@ struct ContainerNetworkRequest
172
NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(ContainerNetworkRequest, Container);
173
};
174
173
-struct EmptyObject
174
-{
175
-};
176
-
177
-inline void to_json(nlohmann::json& j, const EmptyObject& memory)
178
-{
179
- UNREFERENCED_PARAMETER(memory);
180
- j = nlohmann::json::object();
181
-}
182
-
183
-inline void from_json(const nlohmann::json& j, EmptyObject& obj)
184
-{
185
- // EmptyObject has no fields, so nothing to deserialize
186
- UNREFERENCED_PARAMETER(j);
187
- UNREFERENCED_PARAMETER(obj);
188
-}
189
-
175
struct Mount
176
{
177
std::string Name;
src/windows/inc/wslc_schema.h
+16
-2
@@ -18,6 +18,8 @@ Abstract:
18
19
namespace wsl::windows::common::wslc_schema {
20
21
+using wsl::shared::EmptyObject;
22
+
23
struct InspectPortBinding
24
{
25
// WSLC always binds to localhost. Included for Docker API compatibility.
@@ -119,11 +121,22 @@ struct ImageConfig
121
std::optional<std::vector<std::string>> Cmd;
122
std::optional<std::vector<std::string>> Entrypoint;
123
std::optional<std::vector<std::string>> Env;
124
+ std::optional<std::map<std::string, EmptyObject>> ExposedPorts;
125
std::optional<std::map<std::string, std::string>> Labels;
126
+ std::string StopSignal;
127
std::string User;
128
+ std::optional<std::map<std::string, EmptyObject>> Volumes;
129
std::string WorkingDir;
130
126
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ImageConfig, Cmd, Entrypoint, Env, Labels, User, WorkingDir);
131
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ImageConfig, Cmd, Entrypoint, Env, ExposedPorts, Labels, StopSignal, User, Volumes, WorkingDir);
132
+};
133
+
134
+struct ImageRootFS
135
+{
136
+ std::string Type;
137
+ std::optional<std::vector<std::string>> Layers;
138
+
139
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ImageRootFS, Type, Layers);
140
};
141
142
struct InspectImage
@@ -140,9 +153,10 @@ struct InspectImage
153
int64_t Size{};
154
std::optional<std::map<std::string, std::string>> Metadata;
155
std::optional<ImageConfig> Config;
156
+ std::optional<ImageRootFS> RootFS;
157
158
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(
145
- InspectImage, Id, RepoTags, RepoDigests, Parent, Comment, Created, Author, Architecture, Os, Size, Metadata, Config);
159
+ InspectImage, Id, RepoTags, RepoDigests, Parent, Comment, Created, Author, Architecture, Os, Size, Metadata, Config, RootFS);
160
};
161
162
struct InspectVolume
src/windows/wslcsession/WSLCSession.cpp
+30
@@ -127,12 +127,42 @@ wslc_schema::InspectImage ConvertInspectImage(const docker_schema::InspectImage&
127
wslcConfig.Entrypoint = dockerConfig.Entrypoint;
128
wslcConfig.Env = dockerConfig.Env;
129
wslcConfig.Labels = dockerConfig.Labels;
130
+ wslcConfig.StopSignal = dockerConfig.StopSignal;
131
wslcConfig.User = dockerConfig.User;
132
wslcConfig.WorkingDir = dockerConfig.WorkingDir;
133
134
+ if (dockerConfig.ExposedPorts.has_value())
135
+ {
136
+ std::map<std::string, wslc_schema::EmptyObject> ports;
137
+ for (const auto& [port, _] : dockerConfig.ExposedPorts.value())
138
+ {
139
+ ports.emplace(port, wslc_schema::EmptyObject{});
140
+ }
141
+ wslcConfig.ExposedPorts = std::move(ports);
142
+ }
143
+
144
+ if (dockerConfig.Volumes.has_value())
145
+ {
146
+ std::map<std::string, wslc_schema::EmptyObject> volumes;
147
+ for (const auto& [path, _] : dockerConfig.Volumes.value())
148
+ {
149
+ volumes.emplace(path, wslc_schema::EmptyObject{});
150
+ }
151
+ wslcConfig.Volumes = std::move(volumes);
152
+ }
153
+
154
wslcInspect.Config = wslcConfig;
155
}
156
157
+ if (dockerInspect.RootFS.has_value())
158
+ {
159
+ const auto& dockerRootFS = dockerInspect.RootFS.value();
160
+ wslc_schema::ImageRootFS wslcRootFS{};
161
+ wslcRootFS.Type = dockerRootFS.Type;
162
+ wslcRootFS.Layers = dockerRootFS.Layers;
163
+ wslcInspect.RootFS = std::move(wslcRootFS);
164
+ }
165
+
166
return wslcInspect;
167
}
168
test/windows/wslc/e2e/WSLCE2EHelpers.cpp
+8
@@ -583,6 +583,14 @@ void WriteTestFile(const std::filesystem::path& filePath, const std::vector<std:
583
VERIFY_IS_TRUE(file.good());
584
}
585
586
+void WriteTestFileContent(const std::filesystem::path& filePath, const std::string& content)
587
+{
588
+ std::ofstream file(filePath, std::ios::out | std::ios::trunc | std::ios::binary);
589
+ THROW_HR_IF_MSG(E_FAIL, !file.is_open(), "Failed to open %ls for writing", filePath.c_str());
590
+ file << content;
591
+ THROW_HR_IF_MSG(E_FAIL, !file.good(), "Failed to write to %ls", filePath.c_str());
592
+}
593
+
594
std::wstring GetPythonHttpServerScript(uint16_t port)
595
{
596
return std::format(L"python3 -m http.server {}", port);
test/windows/wslc/e2e/WSLCE2EHelpers.h
+18
@@ -133,6 +133,24 @@ void EnsureVolumeDoesNotExist(const std::wstring& volumeName);
133
void EnsureNetworkDoesNotExist(const std::wstring& networkName);
134
135
void WriteTestFile(const std::filesystem::path& filePath, const std::vector<std::string>& envVariableLines);
136
+void WriteTestFileContent(const std::filesystem::path& filePath, const std::string& content);
137
+
138
+// Sets up a clean test directory and returns a scope_exit to remove it.
139
+inline auto SetupTestDirectory(const std::filesystem::path& directory)
140
+{
141
+ std::error_code ec;
142
+ std::filesystem::remove_all(directory, ec);
143
+ THROW_HR_IF_MSG(E_FAIL, ec.value() != 0 && std::filesystem::exists(directory), "%hs", ec.message().c_str());
144
+
145
+ std::filesystem::create_directories(directory, ec);
146
+ THROW_HR_IF_MSG(E_FAIL, ec.value() != 0 || !std::filesystem::exists(directory), "%hs", ec.message().c_str());
147
+
148
+ return wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [directory]() {
149
+ std::error_code removeError;
150
+ std::filesystem::remove_all(directory, removeError);
151
+ });
152
+}
153
+
154
std::wstring GetPythonHttpServerScript(uint16_t port);
155
156
// Default timeout of 0 will execute once.
test/windows/wslc/e2e/WSLCE2EImageBuildTests.cpp
+10
-35
@@ -15,7 +15,6 @@ Abstract:
15
#include "windows/Common.h"
16
#include "WSLCExecutor.h"
17
#include "WSLCE2EHelpers.h"
18
-#include <fstream>
18
19
namespace WSLCE2ETests {
20
@@ -60,7 +59,7 @@ class WSLCE2EImageBuildTests
59
THROW_HR_IF(E_FAIL, ec.value() != 0 || !std::filesystem::exists(contextDir));
60
61
auto dockerfilePath = testRoot / L"Dockerfile";
63
- WriteTestFile(dockerfilePath, "FROM debian:latest\nCMD [\"echo\", \"wslc-e2e-build-ok\"]\n");
62
+ WriteTestFileContent(dockerfilePath, "FROM debian:latest\nCMD [\"echo\", \"wslc-e2e-build-ok\"]\n");
63
64
auto buildResult = RunWslc(
65
std::format(L"build \"{}\" -f \"{}\" -t {}", contextDir.wstring(), dockerfilePath.wstring(), BuiltImage.NameAndTag()));
@@ -84,10 +83,10 @@ class WSLCE2EImageBuildTests
83
84
// Create a simple file in the context directory
85
auto filePath = contextDir / L"hello.txt";
87
- WriteTestFile(filePath, "hello from wslc build\n");
86
+ WriteTestFileContent(filePath, "hello from wslc build\n");
87
88
auto dockerfilePath = testRoot / L"Dockerfile";
90
- WriteTestFile(
89
+ WriteTestFileContent(
90
dockerfilePath,
91
"FROM debian:latest\n"
92
"ARG TEST_LABEL=default_value\n"
@@ -134,7 +133,7 @@ class WSLCE2EImageBuildTests
133
THROW_HR_IF(E_FAIL, ec.value() != 0 || !std::filesystem::exists(contextDir));
134
135
auto dockerfilePath = testRoot / L"Dockerfile";
137
- WriteTestFile(dockerfilePath, "FROM debian:latest\nCMD [\"echo\", \"pull-ok\"]\n");
136
+ WriteTestFileContent(dockerfilePath, "FROM debian:latest\nCMD [\"echo\", \"pull-ok\"]\n");
137
138
// Build with --pull --verbose. When --pull causes docker to resolve the base image
139
// from the registry, the FROM step includes a @sha256: digest (e.g.
@@ -158,7 +157,7 @@ class WSLCE2EImageBuildTests
157
THROW_HR_IF(E_FAIL, ec.value() != 0 || !std::filesystem::exists(contextDir));
158
159
auto dockerfilePath = testRoot / L"Dockerfile";
161
- WriteTestFile(
160
+ WriteTestFileContent(
161
dockerfilePath,
162
"FROM debian:latest AS build-stage\n"
163
"RUN echo build > /stage.txt\n"
@@ -198,8 +197,8 @@ class WSLCE2EImageBuildTests
197
auto testRoot = std::filesystem::current_path() / L"wslc-e2e-build-both-files";
198
auto cleanup = SetupTestDirectory(testRoot);
199
201
- WriteTestFile(testRoot / L"Dockerfile", "FROM debian:latest\n");
202
- WriteTestFile(testRoot / L"Containerfile", "FROM debian:latest\n");
200
+ WriteTestFileContent(testRoot / L"Dockerfile", "FROM debian:latest\n");
201
+ WriteTestFileContent(testRoot / L"Containerfile", "FROM debian:latest\n");
202
203
auto buildResult = RunWslc(std::format(L"build \"{}\"", testRoot.wstring()));
204
buildResult.Verify(
@@ -226,7 +225,7 @@ class WSLCE2EImageBuildTests
225
auto cleanup = SetupTestDirectory(testRoot);
226
227
auto containerfilePath = testRoot / L"Containerfile";
229
- WriteTestFile(containerfilePath, "FROM debian:latest\n");
228
+ WriteTestFileContent(containerfilePath, "FROM debian:latest\n");
229
230
// Deny read access so wslc cannot open the file.
231
SetPathAccess(containerfilePath, GENERIC_READ, DENY_ACCESS);
@@ -254,7 +253,7 @@ class WSLCE2EImageBuildTests
253
// `RUN date +%N` produces a different output each invocation, so without caching the
254
// resulting layer (and therefore the image id) changes every build.
255
auto dockerfilePath = testRoot / L"Dockerfile";
257
- WriteTestFile(
256
+ WriteTestFileContent(
257
dockerfilePath,
258
"FROM debian:latest\n"
259
"RUN date +%N > /timestamp.txt\n");
@@ -296,7 +295,7 @@ private:
295
auto testRoot = std::filesystem::current_path() / image.Name;
296
auto cleanup = SetupTestDirectory(testRoot);
297
299
- WriteTestFile(testRoot / fileName, "FROM debian:latest\nCMD [\"echo\", \"build-ok\"]\n");
298
+ WriteTestFileContent(testRoot / fileName, "FROM debian:latest\nCMD [\"echo\", \"build-ok\"]\n");
299
300
auto buildResult = RunWslc(std::format(L"build \"{}\" -t {}", testRoot.wstring(), image.NameAndTag()));
301
buildResult.Verify({.Stderr = L"", .ExitCode = 0});
@@ -318,29 +317,5 @@ private:
317
EnsureImageIsDeleted(BuiltImageContainerfile);
318
EnsureImageIsDeleted(BuiltImageNoCache);
319
}
321
-
322
- static auto SetupTestDirectory(const std::filesystem::path& testRoot)
323
- {
324
- std::error_code ec;
325
- std::filesystem::remove_all(testRoot, ec);
326
- THROW_HR_IF_MSG(E_FAIL, ec.value() != 0 && std::filesystem::exists(testRoot), "%hs", ec.message().c_str());
327
-
328
- std::filesystem::create_directories(testRoot, ec);
329
- THROW_HR_IF_MSG(E_FAIL, ec.value() != 0 || !std::filesystem::exists(testRoot), "%hs", ec.message().c_str());
330
-
331
- return wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [testRoot]() {
332
- std::error_code removeError;
333
- std::filesystem::remove_all(testRoot, removeError);
334
- });
335
- }
336
-
337
- static void WriteTestFile(const std::filesystem::path& path, const std::string& content)
338
- {
339
- std::ofstream file(path);
340
- THROW_HR_IF(E_FAIL, !file.is_open());
341
- file << content;
342
- THROW_HR_IF(E_FAIL, !file.good());
343
- file.close();
344
- }
320
};
321
} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EImageInspectTests.cpp
+56
@@ -32,6 +32,7 @@ class WSLCE2EImageInspectTests
32
33
TEST_CLASS_CLEANUP(ClassCleanup)
34
{
35
+ EnsureImageIsDeleted(BuiltExposeImage);
36
EnsureImageIsDeleted(DebianImage);
37
return true;
38
}
@@ -64,12 +65,67 @@ class WSLCE2EImageInspectTests
65
VERIFY_IS_TRUE(inspectData[0].RepoTags.has_value());
66
VERIFY_ARE_EQUAL(1u, inspectData[0].RepoTags.value().size());
67
VERIFY_ARE_EQUAL(DebianImage.NameAndTag(), wsl::shared::string::MultiByteToWide(inspectData[0].RepoTags.value()[0]));
68
+
69
+ // Verify RootFS is populated.
70
+ VERIFY_IS_TRUE(inspectData[0].RootFS.has_value());
71
+ VERIFY_ARE_EQUAL(std::string{"layers"}, inspectData[0].RootFS.value().Type);
72
+ VERIFY_IS_TRUE(inspectData[0].RootFS.value().Layers.has_value());
73
+ VERIFY_IS_GREATER_THAN(inspectData[0].RootFS.value().Layers.value().size(), 0u);
74
+
75
+ // Debian has no ExposedPorts or Volumes.
76
+ VERIFY_IS_TRUE(inspectData[0].Config.has_value());
77
+ VERIFY_IS_FALSE(inspectData[0].Config.value().ExposedPorts.has_value());
78
+ VERIFY_IS_FALSE(inspectData[0].Config.value().Volumes.has_value());
79
+ }
80
+
81
+ WSLC_TEST_METHOD(WSLCE2E_Image_Inspect_ConfigExtras_Success)
82
+ {
83
+ auto testRoot = std::filesystem::current_path() / L"wslc-e2e-inspect-config-extras";
84
+ auto cleanupDir = SetupTestDirectory(testRoot);
85
+
86
+ auto contextDir = testRoot / L"context";
87
+ std::error_code ec;
88
+ std::filesystem::create_directories(contextDir, ec);
89
+ THROW_HR_IF(E_FAIL, ec.value() != 0 || !std::filesystem::exists(contextDir));
90
+
91
+ auto dockerfilePath = testRoot / L"Dockerfile";
92
+ WriteTestFileContent(
93
+ dockerfilePath,
94
+ std::format(
95
+ "FROM {}\n"
96
+ "EXPOSE 8080/tcp\n"
97
+ "EXPOSE 9090/udp\n"
98
+ "VOLUME /data\n"
99
+ "VOLUME /var/log/app\n"
100
+ "STOPSIGNAL SIGTERM\n",
101
+ wsl::shared::string::WideToMultiByte(DebianImage.NameAndTag())));
102
+
103
+ auto buildResult = RunWslc(std::format(
104
+ L"build \"{}\" -f \"{}\" -t {}", contextDir.wstring(), dockerfilePath.wstring(), BuiltExposeImage.NameAndTag()));
105
+ buildResult.Verify({.Stderr = L"", .ExitCode = 0});
106
+
107
+ auto inspectData = InspectImage(BuiltExposeImage.NameAndTag());
108
+ VERIFY_IS_TRUE(inspectData.Config.has_value());
109
+ const auto& config = inspectData.Config.value();
110
+
111
+ VERIFY_IS_TRUE(config.ExposedPorts.has_value());
112
+ const auto& ports = config.ExposedPorts.value();
113
+ VERIFY_IS_TRUE(ports.contains("8080/tcp"));
114
+ VERIFY_IS_TRUE(ports.contains("9090/udp"));
115
+
116
+ VERIFY_IS_TRUE(config.Volumes.has_value());
117
+ const auto& volumes = config.Volumes.value();
118
+ VERIFY_IS_TRUE(volumes.contains("/data"));
119
+ VERIFY_IS_TRUE(volumes.contains("/var/log/app"));
120
+
121
+ VERIFY_ARE_EQUAL(std::string{"SIGTERM"}, config.StopSignal);
122
}
123
124
private:
125
const std::wstring WslcContainerName = L"wslc-test-container";
126
const TestImage& DebianImage = DebianTestImage();
127
const TestImage& InvalidImage = InvalidTestImage();
128
+ const TestImage BuiltExposeImage{L"wslc-e2e-inspect-config-extras", L"latest", L""};
129
130
std::wstring GetHelpMessage() const
131
{