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