@samitouri / QOSAMI-WSL / commits / 146f24e6

CLI: Fix image import to output the image ID, allow untagged images, and support --no-trunc (#40856)

David Bennett committed Jun 22, 2026 at 14:06 UTC 146f24e652c3617f955a0ca10c86c034e12d11fe
13 files changed +195 -78
src/windows/WslcSDK/wslcsdk.cpp
+3 -1
@@ -1400,13 +1400,15 @@ static HRESULT WslcImportSessionImageImpl(
1400 WslcSessionImpl* internalSession, PCSTR imageName, const WslcImportImageOptions* options, ErrorInfoWrapper& errorInfoWrapper, const ImageFileResolver& imageFile)
1401 {
1402 auto progressCallback = ProgressCallback::CreateIf(options);
1403 + wil::unique_cotaskmem_ansistring imageId;
1404
1405 return errorInfoWrapper.CaptureResult(internalSession->session->ImportImage(
1406 wsl::windows::common::apicompat::Convert(ToCOMInputHandle(imageFile.Handle())),
1407 imageName,
1408 progressCallback.get(),
1409 imageFile.Length(),
1409 - nullptr));
1410 + nullptr,
1411 + &imageId));
1412 }
1413
1414 STDAPI WslcImportSessionImage(
src/windows/service/inc/WSLCCompat.idl
+1 -1
@@ -318,7 +318,7 @@ interface IWSLCCompatSession : IUnknown
318 // Image management.
319 HRESULT PullImage([in] LPCSTR Image, [in, unique] LPCSTR RegistryAuthenticationInformation, [in, unique] IWSLCCompatProgressCallback* ProgressCallback, [in, unique] IWSLCCompatWarningCallback* WarningCallback);
320 HRESULT LoadImage([in] WSLCCompatHandle ImageHandle, [in, unique] IWSLCCompatProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength, [in, unique] IWSLCCompatWarningCallback* WarningCallback);
321 - HRESULT ImportImage([in] WSLCCompatHandle ImageHandle, [in] LPCSTR ImageName, [in, unique] IWSLCCompatProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength, [in, unique] IWSLCCompatWarningCallback* WarningCallback);
321 + HRESULT ImportImage([in] WSLCCompatHandle ImageHandle, [in, unique] LPCSTR ImageName, [in, unique] IWSLCCompatProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength, [in, unique] IWSLCCompatWarningCallback* WarningCallback, [out] LPSTR* ImageId);
322 HRESULT ListImages([in, unique] const WSLCCompatListImagesOptions* Options, [out, size_is(, *Count)] WSLCCompatImageInformation** Images, [out] ULONG* Count);
323 HRESULT DeleteImage([in] const WSLCCompatDeleteImageOptions* Options, [out, size_is(, *Count)] WSLCCompatDeletedImageInformation** DeletedImages, [out] ULONG* Count);
324 HRESULT TagImage([in] const WSLCCompatTagImageOptions* Options);
src/windows/service/inc/wslc.idl
+1 -1
@@ -580,7 +580,7 @@ interface IWSLCSession : IUnknown
580 HRESULT PullImage([in] LPCSTR Image, [in, unique] LPCSTR RegistryAuthenticationInformation, [in, unique] IProgressCallback* ProgressCallback, [in, unique] IWarningCallback* WarningCallback);
581 HRESULT BuildImage([in] const WSLCBuildImageOptions* Options, [in, unique] IProgressCallback* ProgressCallback, [in, unique, system_handle(sh_event)] HANDLE CancelEvent);
582 HRESULT LoadImage([in] WSLCHandle ImageHandle, [in, unique] IProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength, [in, unique] IWarningCallback* WarningCallback);
583 - HRESULT ImportImage([in] WSLCHandle ImageHandle, [in] LPCSTR ImageName, [in, unique] IProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength, [in, unique] IWarningCallback* WarningCallback);
583 + HRESULT ImportImage([in] WSLCHandle ImageHandle, [in, unique] LPCSTR ImageName, [in, unique] IProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength, [in, unique] IWarningCallback* WarningCallback, [out] LPSTR* ImageId);
584 HRESULT SaveImage([in] WSLCHandle OutputHandle, [in] LPCSTR ImageNameOrID, [in, unique] IProgressCallback * ProgressCallback, [in, unique, system_handle(sh_event)] HANDLE CancelEvent);
585 HRESULT SaveImages([in] WSLCHandle OutputHandle, [in] const WSLCStringArray* ImageNames, [in, unique] IProgressCallback * ProgressCallback, [in, unique, system_handle(sh_event)] HANDLE CancelEvent);
586 HRESULT ListImages([in, unique] const WSLCListImagesOptions* Options, [out, size_is(, *Count)] WSLCImageInformation** Images, [out] ULONG* Count);
src/windows/wslc/commands/ImageImportCommand.cpp
+1
@@ -29,6 +29,7 @@ std::vector<Argument> ImageImportCommand::GetArguments() const
29 return {
30 Argument::Create(ArgType::ImportFile, true),
31 Argument::Create(ArgType::ImageId),
32 + Argument::Create(ArgType::NoTrunc),
33 };
34 }
35
src/windows/wslc/services/ImageService.cpp
+5 -2
@@ -232,16 +232,19 @@ void ImageService::Load(wsl::windows::wslc::models::Session& session, const std:
232 THROW_IF_FAILED(session.Get()->LoadImage(ToCOMInputHandle(source.Handle.Get()), nullptr, source.ContentLength, warningCallback.Get()));
233 }
234
235 -void ImageService::Import(wsl::windows::wslc::models::Session& session, const std::wstring& input, const std::string& imageName)
235 +std::string ImageService::Import(wsl::windows::wslc::models::Session& session, const std::wstring& input, const std::string& imageName)
236 {
237 auto source = OpenImageInput(input);
238 auto warningCallback = Microsoft::WRL::Make<WarningCallback>();
239 + wil::unique_cotaskmem_ansistring imageId;
240 THROW_IF_FAILED(session.Get()->ImportImage(
241 ToCOMInputHandle(source.Handle.Get()),
242 imageName.empty() ? nullptr : imageName.c_str(),
243 nullptr,
244 source.ContentLength,
244 - warningCallback.Get()));
245 + warningCallback.Get(),
246 + &imageId));
247 + return imageId.get() ? std::string(imageId.get()) : std::string();
248 }
249
250 void ImageService::Delete(wsl::windows::wslc::models::Session& session, const std::string& image, bool force, bool noPrune)
src/windows/wslc/services/ImageService.h
+1 -1
@@ -35,7 +35,7 @@ public:
35 static std::vector<wsl::windows::wslc::models::ImageInformation> List(
36 wsl::windows::wslc::models::Session& session, const std::vector<std::pair<std::string, std::string>>& filters = {});
37 static void Load(wsl::windows::wslc::models::Session& session, const std::wstring& input);
38 - static void Import(wsl::windows::wslc::models::Session& session, const std::wstring& input, const std::string& imageName);
38 + static std::string Import(wsl::windows::wslc::models::Session& session, const std::wstring& input, const std::string& imageName);
39 static void Delete(wsl::windows::wslc::models::Session& session, const std::string& image, bool force, bool noPrune);
40 static wsl::windows::common::wslc_schema::InspectImage Inspect(wsl::windows::wslc::models::Session& session, const std::string& image);
41 static void Pull(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback);
src/windows/wslc/tasks/ImageTasks.cpp
+6 -1
@@ -239,7 +239,12 @@ void ImportImage(CLIExecutionContext& context)
239 }
240
241 auto& input = context.Args.Get<ArgType::ImportFile>();
242 - services::ImageService::Import(session, input, imageName);
242 + auto imageId = services::ImageService::Import(session, input, imageName);
243 + if (!imageId.empty())
244 + {
245 + bool trunc = !context.Args.Contains(ArgType::NoTrunc);
246 + context.Reporter.Output(L"{}\n", MultiByteToWide(TruncateId(imageId, trunc)));
247 + }
248 }
249
250 void InspectImages(CLIExecutionContext& context)
src/windows/wslcsession/WSLCSession.cpp
+46 -12
@@ -1210,40 +1210,61 @@ try
1210
1211 auto requestContext = m_dockerClient->LoadImage(ContentSize);
1212
1213 - ImportImageImpl(*requestContext, ImageHandle);
1213 + std::ignore = ImportImageImpl(*requestContext, ImageHandle);
1214
1215 return S_OK;
1216 }
1217 CATCH_RETURN();
1218
1219 -HRESULT WSLCSession::ImportImage(const WSLCHandle ImageHandle, LPCSTR ImageName, IProgressCallback* ProgressCallback, ULONGLONG ContentSize, IWarningCallback* WarningCallback)
1219 +HRESULT WSLCSession::ImportImage(
1220 + const WSLCHandle ImageHandle, LPCSTR ImageName, IProgressCallback* ProgressCallback, ULONGLONG ContentSize, IWarningCallback* WarningCallback, LPSTR* ImageId)
1221 try
1222 {
1223 UNREFERENCED_PARAMETER(ProgressCallback);
1224
1225 WSLCExecutionContext context(this, WarningCallback);
1226
1226 - RETURN_HR_IF_NULL(E_POINTER, ImageName);
1227 - RETURN_HR_IF(E_INVALIDARG, strlen(ImageName) > WSLC_MAX_IMAGE_NAME_LENGTH);
1227 + RETURN_HR_IF_NULL(E_POINTER, ImageId);
1228 + *ImageId = nullptr;
1229 +
1230 + std::string repo;
1231 + std::string tag;
1232
1229 - auto [repo, tagOrDigest] = wslutil::ParseImage(ImageName);
1233 + if (ImageName != nullptr)
1234 + {
1235 + RETURN_HR_IF(E_INVALIDARG, strlen(ImageName) > WSLC_MAX_IMAGE_NAME_LENGTH);
1236
1231 - THROW_HR_IF_MSG(E_INVALIDARG, !tagOrDigest.has_value(), "Expected tag for image import: %hs", ImageName);
1237 + auto [parsedRepo, tagOrDigest] = wslutil::ParseImage(ImageName);
1238 + THROW_HR_IF_MSG(E_INVALIDARG, !tagOrDigest.has_value(), "Expected tag for image import: %hs", ImageName);
1239 + repo = parsedRepo;
1240 + tag = tagOrDigest.value();
1241 + }
1242
1243 auto lock = m_lock.lock_shared();
1244
1245 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1246
1237 - auto requestContext = m_dockerClient->ImportImage(repo, tagOrDigest.value(), ContentSize);
1247 + auto requestContext = m_dockerClient->ImportImage(repo, tag, ContentSize);
1248 +
1249 + auto imageId = ImportImageImpl(*requestContext, ImageHandle);
1250 + THROW_HR_IF_MSG(E_UNEXPECTED, !imageId.has_value(), "Docker import succeeded but did not return an image ID");
1251 +
1252 + if (ImageName != nullptr && strlen(ImageName) > 0)
1253 + {
1254 + OnImageCreated(ImageName);
1255 + }
1256 + else
1257 + {
1258 + OnImageCreated(imageId->c_str());
1259 + }
1260
1239 - ImportImageImpl(*requestContext, ImageHandle);
1261 + *ImageId = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(imageId->c_str()).release();
1262
1241 - OnImageCreated(ImageName);
1263 return S_OK;
1264 }
1265 CATCH_RETURN();
1266
1246 -void WSLCSession::ImportImageImpl(DockerHTTPClient::HTTPRequestContext& Request, const WSLCHandle ImageHandle)
1267 +std::optional<std::string> WSLCSession::ImportImageImpl(DockerHTTPClient::HTTPRequestContext& Request, const WSLCHandle ImageHandle)
1268 {
1269 auto userHandle = OpenUserHandle(ImageHandle);
1270
@@ -1252,6 +1273,7 @@ void WSLCSession::ImportImageImpl(DockerHTTPClient::HTTPRequestContext& Request,
1273 auto io = CreateIOContext();
1274
1275 std::optional<std::string> pendingErrorJson;
1276 + std::optional<std::string> imageId;
1277 auto onHttpResponse = [&](const boost::beast::http::message<false, boost::beast::http::buffer_body>& response) {
1278 WSL_LOG("ImageImportHttpResponse", TraceLoggingValue(static_cast<int>(response.result()), "StatusCode"));
1279
@@ -1301,6 +1323,11 @@ void WSLCSession::ImportImageImpl(DockerHTTPClient::HTTPRequestContext& Request,
1323 else if (parsed.status.has_value())
1324 {
1325 WSL_LOG("ImageImportProgress", TraceLoggingValue(parsed.status->c_str(), "Status"));
1326 + if (parsed.status->starts_with("sha256:"))
1327 + {
1328 + THROW_HR_IF_MSG(E_UNEXPECTED, imageId.has_value(), "Received duplicate image ID in import status");
1329 + imageId = *parsed.status;
1330 + }
1331 }
1332 else
1333 {
@@ -1332,6 +1359,8 @@ void WSLCSession::ImportImageImpl(DockerHTTPClient::HTTPRequestContext& Request,
1359
1360 // Otherwise look for an error message returned via the progress stream (HTTP 200 followed by a stream error).
1361 THROW_HR_WITH_USER_ERROR_IF(E_FAIL, errorMessage.value(), errorMessage.has_value());
1362 +
1363 + return imageId;
1364 }
1365
1366 HRESULT WSLCSession::SaveImage(WSLCHandle OutHandle, LPCSTR ImageNameOrID, IProgressCallback* ProgressCallback, HANDLE CancelEvent)
@@ -3050,13 +3079,18 @@ HRESULT WSLCSession::LoadImage(WSLCCompatHandle ImageHandle, IWSLCCompatProgress
3079 }
3080
3081 HRESULT WSLCSession::ImportImage(
3053 - WSLCCompatHandle ImageHandle, LPCSTR ImageName, IWSLCCompatProgressCallback* ProgressCallback, ULONGLONG ContentLength, IWSLCCompatWarningCallback* WarningCallback)
3082 + WSLCCompatHandle ImageHandle,
3083 + LPCSTR ImageName,
3084 + IWSLCCompatProgressCallback* ProgressCallback,
3085 + ULONGLONG ContentLength,
3086 + IWSLCCompatWarningCallback* WarningCallback,
3087 + LPSTR* ImageId)
3088 {
3089 const auto handle = apicompat::Convert(ImageHandle);
3090 const auto progress = apicompat::Convert(ProgressCallback);
3091 const auto warning = apicompat::Convert(WarningCallback);
3092
3059 - return ImportImage(handle, ImageName, progress.Get(), ContentLength, warning.Get());
3093 + return ImportImage(handle, ImageName, progress.Get(), ContentLength, warning.Get(), ImageId);
3094 }
3095
3096 HRESULT WSLCSession::ListImages(const WSLCCompatListImagesOptions* Options, WSLCCompatImageInformation** Images, ULONG* Count)
src/windows/wslcsession/WSLCSession.h
+7 -5
@@ -113,10 +113,11 @@ public:
113 IFACEMETHOD(LoadImage)(_In_ const WSLCHandle ImageHandle, _In_ IProgressCallback* ProgressCallback, _In_ ULONGLONG ContentLength, _In_opt_ IWarningCallback* WarningCallback) override;
114 IFACEMETHOD(ImportImage)(
115 _In_ const WSLCHandle ImageHandle,
116 - _In_ LPCSTR ImageName,
116 + _In_opt_ LPCSTR ImageName,
117 _In_ IProgressCallback* ProgressCallback,
118 _In_ ULONGLONG ContentLength,
119 - _In_opt_ IWarningCallback* WarningCallback) override;
119 + _In_opt_ IWarningCallback* WarningCallback,
120 + _Out_ LPSTR* ImageId) override;
121 IFACEMETHOD(SaveImage)(_In_ WSLCHandle OutputHandle, _In_ LPCSTR ImageNameOrID, _In_ IProgressCallback* ProgressCallback, _In_opt_ HANDLE CancelEvent) override;
122 IFACEMETHOD(SaveImages)(_In_ WSLCHandle OutputHandle, _In_ const WSLCStringArray* ImageNames, _In_ IProgressCallback* ProgressCallback, _In_opt_ HANDLE CancelEvent) override;
123 IFACEMETHOD(ListImages)(_In_opt_ const WSLCListImagesOptions* Options, _Out_ WSLCImageInformation** Images, _Out_ ULONG* Count) override;
@@ -215,10 +216,11 @@ public:
216 _In_opt_ IWSLCCompatWarningCallback* WarningCallback) override;
217 IFACEMETHOD(ImportImage)(
218 _In_ WSLCCompatHandle ImageHandle,
218 - _In_ LPCSTR ImageName,
219 + _In_opt_ LPCSTR ImageName,
220 _In_opt_ IWSLCCompatProgressCallback* ProgressCallback,
221 _In_ ULONGLONG ContentLength,
221 - _In_opt_ IWSLCCompatWarningCallback* WarningCallback) override;
222 + _In_opt_ IWSLCCompatWarningCallback* WarningCallback,
223 + _Out_ LPSTR* ImageId) override;
224 IFACEMETHOD(ListImages)(_In_opt_ const WSLCCompatListImagesOptions* Options, _Out_ WSLCCompatImageInformation** Images, _Out_ ULONG* Count) override;
225 IFACEMETHOD(DeleteImage)(_In_ const WSLCCompatDeleteImageOptions* Options, _Out_ WSLCCompatDeletedImageInformation** DeletedImages, _Out_ ULONG* Count) override;
226 IFACEMETHOD(TagImage)(_In_ const WSLCCompatTagImageOptions* Options) override;
@@ -287,7 +289,7 @@ private:
289 void StartContainerd();
290 void StartDockerd();
291 int StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs);
290 - void ImportImageImpl(DockerHTTPClient::HTTPRequestContext& Request, const WSLCHandle ImageHandle);
292 + std::optional<std::string> ImportImageImpl(DockerHTTPClient::HTTPRequestContext& Request, const WSLCHandle ImageHandle);
293 void RecoverExistingContainers();
294 void RecoverExistingNetworks();
295
test/windows/WSLCTests.cpp
+15 -8
@@ -1288,8 +1288,9 @@ class WSLCTests
1288 LARGE_INTEGER fileSize{};
1289 VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
1290
1291 + wil::unique_cotaskmem_ansistring imageId;
1292 VERIFY_SUCCEEDED(m_defaultSession->ImportImage(
1292 - ToCOMInputHandle(imageTarFileHandle.get()), "my-hello-world:test", nullptr, fileSize.QuadPart, nullptr));
1293 + ToCOMInputHandle(imageTarFileHandle.get()), "my-hello-world:test", nullptr, fileSize.QuadPart, nullptr, &imageId));
1294
1295 ExpectImagePresent(*m_defaultSession, "my-hello-world:test");
1296
@@ -1307,7 +1308,8 @@ class WSLCTests
1308 // Validate that ImportImage fails if no tag is passed
1309 {
1310 VERIFY_ARE_EQUAL(
1310 - m_defaultSession->ImportImage(ToCOMInputHandle(imageTarFileHandle.get()), "my-hello-world", nullptr, fileSize.QuadPart, nullptr),
1311 + m_defaultSession->ImportImage(
1312 + ToCOMInputHandle(imageTarFileHandle.get()), "my-hello-world", nullptr, fileSize.QuadPart, nullptr, &imageId),
1313 E_INVALIDARG);
1314 }
1315
@@ -1319,7 +1321,7 @@ class WSLCTests
1321
1322 VERIFY_ARE_EQUAL(
1323 m_defaultSession->ImportImage(
1322 - ToCOMInputHandle(currentExecutableHandle.get()), "invalid-image:test", nullptr, fileSize.QuadPart, nullptr),
1324 + ToCOMInputHandle(currentExecutableHandle.get()), "invalid-image:test", nullptr, fileSize.QuadPart, nullptr, &imageId),
1325 E_FAIL);
1326
1327 ValidateCOMErrorMessage(L"archive/tar: invalid tar header");
@@ -1333,8 +1335,9 @@ class WSLCTests
1335
1336 std::promise<HRESULT> importResult;
1337 std::thread operationThread([&]() {
1336 - importResult.set_value(
1337 - m_defaultSession->ImportImage(ToCOMInputHandle(pipeRead.get()), "broken-read:eof", nullptr, 1024 * 1024, nullptr));
1338 + wil::unique_cotaskmem_ansistring id;
1339 + importResult.set_value(m_defaultSession->ImportImage(
1340 + ToCOMInputHandle(pipeRead.get()), "broken-read:eof", nullptr, 1024 * 1024, nullptr, &id));
1341 });
1342
1343 auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); });
@@ -1358,8 +1361,9 @@ class WSLCTests
1361 std::promise<HRESULT> terminateResult;
1362 wil::unique_event testCompleted{wil::EventOptions::ManualReset};
1363 std::thread operationThread([&]() {
1364 + wil::unique_cotaskmem_ansistring id;
1365 terminateResult.set_value(m_defaultSession->ImportImage(
1362 - ToCOMInputHandle(pipeRead.get()), "session-terminate:test", nullptr, 1024 * 1024, nullptr));
1366 + ToCOMInputHandle(pipeRead.get()), "session-terminate:test", nullptr, 1024 * 1024, nullptr, &id));
1367 WI_ASSERT(testCompleted.is_signaled());
1368 });
1369
@@ -2718,7 +2722,9 @@ class WSLCTests
2722
2723 wil::unique_event testCompleted{wil::EventOptions::ManualReset};
2724 std::thread operationThread([&]() {
2721 - result.set_value(m_defaultSession->ImportImage(ToCOMInputHandle(pipeRead.get()), "dummy:latest", nullptr, 1024 * 1024, nullptr));
2725 + wil::unique_cotaskmem_ansistring id;
2726 + result.set_value(
2727 + m_defaultSession->ImportImage(ToCOMInputHandle(pipeRead.get()), "dummy:latest", nullptr, 1024 * 1024, nullptr, &id));
2728
2729 WI_ASSERT(testCompleted.is_signaled()); // Sanity check.
2730 });
@@ -2797,8 +2803,9 @@ class WSLCTests
2803 LOG_IF_FAILED(DeleteImageNoThrow("test-imported-container:latest", WSLCDeleteImageFlagsNone).first);
2804 });
2805
2806 + wil::unique_cotaskmem_ansistring importedImageId;
2807 VERIFY_SUCCEEDED(m_defaultSession->ImportImage(
2801 - ToCOMInputHandle(containerTarFileHandle.get()), "test-imported-container:latest", nullptr, fileSize.QuadPart, nullptr));
2808 + ToCOMInputHandle(containerTarFileHandle.get()), "test-imported-container:latest", nullptr, fileSize.QuadPart, nullptr, &importedImageId));
2809
2810 // Verify that the image is in the list of images.
2811 ExpectImagePresent(*m_defaultSession, "test-imported-container:latest");
test/windows/wslc/e2e/WSLCE2EHelpers.cpp
+22
@@ -375,6 +375,28 @@ void EnsureImageIsDeleted(const TestImage& image)
375 }
376 }
377
378 +void EnsureNoUntaggedImages()
379 +{
380 + auto result = RunWslc(L"image list --format json --filter dangling=true");
381 + result.Verify({.Stderr = L"", .ExitCode = 0});
382 +
383 + const auto images = wsl::shared::FromJson<std::vector<wsl::windows::wslc::models::ImageInformation>>(result.Stdout.value().c_str());
384 +
385 + for (const auto& image : images)
386 + {
387 + const auto id = wsl::shared::string::MultiByteToWide(GetHashId(image.Id, true));
388 + auto deleteResult = RunWslc(std::format(L"image delete --force {}", id));
389 +
390 + // Tolerate WSLC_E_IMAGE_NOT_FOUND - an untagged image may already be gone if it was a
391 + // parent/child of another untagged image deleted earlier in this loop.
392 + if (deleteResult.ExitCode != 0 &&
393 + (!deleteResult.Stderr.has_value() || deleteResult.Stderr.value().find(L"WSLC_E_IMAGE_NOT_FOUND") == std::wstring::npos))
394 + {
395 + deleteResult.Verify({.Stderr = L"", .ExitCode = 0});
396 + }
397 + }
398 +}
399 +
400 void EnsureImageIsLoaded(const TestImage& image, const std::wstring& sessionName)
401 {
402 std::wstring listCommand = L"image list --format json";
test/windows/wslc/e2e/WSLCE2EHelpers.h
+26
@@ -128,6 +128,7 @@ void EnsureContainerDoesNotExist(const std::wstring& containerName);
128 void EnsureImageIsLoaded(const TestImage& image, const std::wstring& sessionName = L"");
129 void EnsureImageIsDeleted(const TestImage& image);
130 void EnsureImageContainersAreDeleted(const TestImage& image);
131 +void EnsureNoUntaggedImages();
132 void EnsureSessionIsTerminated(const std::wstring& sessionName = L"");
133 void EnsureVolumeDoesNotExist(const std::wstring& volumeName);
134 void EnsureNetworkDoesNotExist(const std::wstring& networkName);
@@ -215,4 +216,29 @@ std::pair<wsl::windows::common::RunningWSLCContainer, std::string> StartLocalReg
216 // Tags an image for a registry and returns the full registry image reference (e.g. "127.0.0.1:PORT/debian:latest").
217 std::wstring TagImageForRegistry(const std::wstring& imageName, const std::wstring& registryAddress);
218
219 +// Verifies that a string is a valid hex ID output.
220 +// truncated=true expects 12 hex chars, truncated=false expects 64 hex chars.
221 +inline void VerifyIdOutput(const std::wstring& id, bool truncated)
222 +{
223 + constexpr size_t c_truncatedLength = 12;
224 + constexpr size_t c_fullLength = 64;
225 +
226 + const size_t expectedLength = truncated ? c_truncatedLength : c_fullLength;
227 +
228 + VERIFY_ARE_EQUAL(id.size(), expectedLength);
229 +
230 + bool allHex = true;
231 + for (size_t i = 0; i < expectedLength; i++)
232 + {
233 + const auto ch = id[i];
234 + if (!((ch >= L'0' && ch <= L'9') || (ch >= L'a' && ch <= L'f')))
235 + {
236 + allHex = false;
237 + break;
238 + }
239 + }
240 +
241 + VERIFY_IS_TRUE(allHex, WEX::Common::String().Format(L"ID is not a valid hex string: '%ls'", id.c_str()));
242 +}
243 +
244 } // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp
+61 -46
@@ -15,6 +15,7 @@ Abstract:
15 #include "windows/Common.h"
16 #include "WSLCExecutor.h"
17 #include "WSLCE2EHelpers.h"
18 +#include "ImageModel.h"
19
20 namespace WSLCE2ETests {
21 using namespace wsl::shared;
@@ -27,6 +28,7 @@ class WSLCE2EImageImportTests
28 {
29 EnsureImageIsDeleted(DebianImage);
30 EnsureImageIsDeleted(ImportedImage);
31 + EnsureNoUntaggedImages();
32 return true;
33 }
34
@@ -34,6 +36,7 @@ class WSLCE2EImageImportTests
36 {
37 EnsureImageIsLoaded(DebianImage);
38 EnsureImageIsDeleted(ImportedImage);
39 + EnsureNoUntaggedImages();
40 SavedArchivePath = wsl::windows::common::filesystem::GetTempFilename();
41 return true;
42 }
@@ -47,13 +50,16 @@ class WSLCE2EImageImportTests
50 WSLC_TEST_METHOD(WSLCE2E_Image_Import_HelpCommand)
51 {
52 auto result = RunWslc(L"image import --help");
50 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
53 + result.Verify({.Stderr = L"", .ExitCode = 0});
54 + VERIFY_IS_FALSE(result.Stdout.value().empty());
55 }
56
57 WSLC_TEST_METHOD(WSLCE2E_Image_Import_MissingFile)
58 {
59 const auto result = RunWslc(L"image import");
56 - result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'file'\r\n", .ExitCode = 1});
60 + result.Verify({.ExitCode = 1});
61 + VERIFY_IS_TRUE(result.Stderr.has_value());
62 + VERIFY_IS_TRUE(result.Stderr->find(L"Required argument not provided: 'file'") != std::wstring::npos);
63 }
64
65 WSLC_TEST_METHOD(WSLCE2E_Image_Import_Success)
@@ -66,22 +72,71 @@ class WSLCE2EImageImportTests
72 auto importResult = RunWslc(std::format(L"image import \"{}\" {}", SavedArchivePath.wstring(), ImportedImage.NameAndTag()));
73 importResult.Verify({.Stderr = L"", .ExitCode = 0});
74
75 + VerifyIdOutput(importResult.GetStdoutOneLine(), true);
76 +
77 // Verify the imported image is listed
78 VerifyImageIsListed(ImportedImage);
79 }
80
73 - WSLC_TEST_METHOD(WSLCE2E_Image_Import_WithoutTag)
81 + WSLC_TEST_METHOD(WSLCE2E_Image_Import_Success_NoTrunc)
82 {
75 - // TODO: http://task.ms/62249460
76 - SKIP_TEST_UNSTABLE();
83 + // Save image as a tarball
84 + auto saveResult = RunWslc(std::format(L"image save --output \"{}\" {}", SavedArchivePath.wstring(), DebianImage.NameAndTag()));
85 + saveResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
86 +
87 + // Import with --no-trunc
88 + auto importResult =
89 + RunWslc(std::format(L"image import --no-trunc \"{}\" {}", SavedArchivePath.wstring(), ImportedImage.NameAndTag()));
90 + importResult.Verify({.Stderr = L"", .ExitCode = 0});
91 +
92 + VerifyIdOutput(importResult.GetStdoutOneLine(), false);
93
94 + // Verify the imported image is listed
95 + VerifyImageIsListed(ImportedImage);
96 + }
97 +
98 + WSLC_TEST_METHOD(WSLCE2E_Image_Import_WithoutTag)
99 + {
100 // Save image as a tarball
101 auto saveResult = RunWslc(std::format(L"image save --output \"{}\" {}", SavedArchivePath.wstring(), DebianImage.NameAndTag()));
102 saveResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
103
82 - // Import without specifying an image name
104 + auto countUntaggedImages = [&]() {
105 + auto result = RunWslc(L"image list --format json");
106 + result.Verify({.Stderr = L"", .ExitCode = 0});
107 + auto images = FromJson<std::vector<wsl::windows::wslc::models::ImageInformation>>(result.Stdout.value().c_str());
108 + size_t count = 0;
109 + for (const auto& img : images)
110 + {
111 + if (!img.Repository.has_value() || img.Repository.value() == "<none>")
112 + {
113 + count++;
114 + }
115 + }
116 + return count;
117 + };
118 +
119 + auto untaggedBefore = countUntaggedImages();
120 +
121 + // Import without specifying an image name — creates an untagged image
122 auto importResult = RunWslc(std::format(L"image import \"{}\"", SavedArchivePath.wstring()));
123 +
124 + // Extract the returned ID, validate its format, and use it for cleanup
125 + auto imageId = importResult.GetStdoutOneLine();
126 +
127 + // As soon as we have the image ID, set up cleanup.
128 + auto cleanup = wil::scope_exit([&] {
129 + auto deleteResult = RunWslc(std::format(L"image rm {}", imageId));
130 + deleteResult.Verify({.ExitCode = 0});
131 + });
132 +
133 + // Import and image id verification is intentionally after the scope exit is created
134 + // for best-effort cleanup if verification fails.
135 importResult.Verify({.Stderr = L"", .ExitCode = 0});
136 + VerifyIdOutput(imageId, true);
137 +
138 + // Verify that there is now one more untagged image
139 + VERIFY_ARE_EQUAL(countUntaggedImages(), untaggedBefore + 1);
140 }
141
142 WSLC_TEST_METHOD(WSLCE2E_Image_Import_FromStdin_Success)
@@ -102,45 +157,5 @@ private:
157 const TestImage ImportedImage{L"wslc-test-imported", L"latest", L""};
158
159 std::filesystem::path SavedArchivePath{};
105 -
106 - std::wstring GetHelpMessage() const
107 - {
108 - std::wstringstream output;
109 - output << GetWslcHeader() //
110 - << GetDescription() //
111 - << GetUsage() //
112 - << GetAvailableCommands() //
113 - << GetAvailableOptions();
114 - return output.str();
115 - }
116 -
117 - std::wstring GetDescription() const
118 - {
119 - return Localization::WSLCCLI_ImageImportLongDesc() + L"\r\n\r\n";
120 - }
121 -
122 - std::wstring GetUsage() const
123 - {
124 - return L"Usage: wslc image import [<options>] <file> [<image>]\r\n\r\n";
125 - }
126 -
127 - std::wstring GetAvailableCommands() const
128 - {
129 - std::wstringstream commands;
130 - commands << L"The following arguments are available:\r\n" //
131 - << L" file " << Localization::WSLCCLI_ImportFileArgDescription() << L"\r\n" //
132 - << L" image " << Localization::WSLCCLI_ImageIdArgDescription() << L"\r\n" //
133 - << L"\r\n";
134 - return commands.str();
135 - }
136 -
137 - std::wstring GetAvailableOptions() const
138 - {
139 - std::wstringstream options;
140 - options << L"The following options are available:\r\n" //
141 - << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n" //
142 - << L"\r\n";
143 - return options.str();
144 - }
160 };
161 } // namespace WSLCE2ETests