Add support for saving multiple images in a single tar (#40694)
* Add support for saving multiple images in a single tar * Restore the alpine:latest image at the end of DeleteImage * Don't delete the image in ListImages * Prune after SaveImages test * Merge
Blue committed
Jun 5, 2026 at 12:38 UTC
c5351e0a296313a34ed1a96d399420204c30a0c8
12 files changed
+279
-21
localization/strings/en-US/Resources.resw
+3
@@ -2365,6 +2365,9 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2365
<value>Invalid image tag format: '{}'. Expected format is 'name:tag'</value>
2366
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2367
</data>
2368
+ <data name="MessageWslcSaveInProgress" xml:space="preserve">
2369
+ <value>Save in progress.</value>
2370
+ </data>
2371
<data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2372
<value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2373
<comment>{Locked="WSLC"}Product names should not be translated</comment>
src/windows/service/inc/wslc.idl
+3
@@ -27,6 +27,7 @@ cpp_quote("#endif")
27
#define WSLC_MAX_NETWORK_NAME_LENGTH 255
28
#define WSLC_CONTAINER_ID_LENGTH 64
29
#define WSLC_MAX_BINDING_ADDRESS_LENGTH 45
30
+#define WSLC_MAX_SAVE_IMAGES_COUNT 256
31
32
cpp_quote("#define WSLC_MAX_CONTAINER_NAME_LENGTH 255")
33
cpp_quote("#define WSLC_MAX_IMAGE_NAME_LENGTH 255")
@@ -35,6 +36,7 @@ cpp_quote("#define WSLC_MAX_VOLUME_DRIVER_LENGTH 255")
36
cpp_quote("#define WSLC_MAX_NETWORK_NAME_LENGTH 255")
37
cpp_quote("#define WSLC_CONTAINER_ID_LENGTH 64")
38
cpp_quote("#define WSLC_MAX_BINDING_ADDRESS_LENGTH 45")
39
+cpp_quote("#define WSLC_MAX_SAVE_IMAGES_COUNT 256")
40
cpp_quote("#define WSLC_EPHEMERAL_PORT 0")
41
42
typedef
@@ -739,6 +741,7 @@ interface IWSLCSession : IUnknown
741
HRESULT LoadImage([in] WSLCHandle ImageHandle, [in, unique] IProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength, [in, unique] IWarningCallback* WarningCallback);
742
HRESULT ImportImage([in] WSLCHandle ImageHandle, [in] LPCSTR ImageName, [in, unique] IProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength, [in, unique] IWarningCallback* WarningCallback);
743
HRESULT SaveImage([in] WSLCHandle OutputHandle, [in] LPCSTR ImageNameOrID, [in, unique] IProgressCallback * ProgressCallback, [in, unique, system_handle(sh_event)] HANDLE CancelEvent);
744
+ HRESULT SaveImages([in] WSLCHandle OutputHandle, [in] const WSLCStringArray* ImageNames, [in, unique] IProgressCallback * ProgressCallback, [in, unique, system_handle(sh_event)] HANDLE CancelEvent);
745
HRESULT ListImages([in, unique] const WSLCListImagesOptions* Options, [out, size_is(, *Count)] WSLCImageInformation** Images, [out] ULONG* Count);
746
HRESULT DeleteImage([in] const WSLCDeleteImageOptions* Options, [out, size_is(, *Count)] WSLCDeletedImageInformation** DeletedImages, [out] ULONG* Count);
747
HRESULT TagImage([in] const WSLCTagImageOptions* Options);
src/windows/wslc/commands/ImageSaveCommand.cpp
+1
-1
@@ -27,7 +27,7 @@ namespace wsl::windows::wslc {
27
std::vector<Argument> ImageSaveCommand::GetArguments() const
28
{
29
return {
30
- Argument::Create(ArgType::ImageId, true),
30
+ Argument::Create(ArgType::ImageId, true, NO_LIMIT),
31
Argument::Create(ArgType::Output),
32
Argument::Create(ArgType::Session),
33
};
src/windows/wslc/services/ImageService.cpp
+27
-5
@@ -303,20 +303,42 @@ void ImageService::Push(wsl::windows::wslc::models::Session& session, const std:
303
THROW_IF_FAILED(session.Get()->PushImage(image.c_str(), auth.c_str(), callback, warningCallback.Get()));
304
}
305
306
-void ImageService::Save(wsl::windows::wslc::models::Session& session, const std::string& image, const std::wstring& output, HANDLE cancelEvent)
306
+void ImageService::Save(wsl::windows::wslc::models::Session& session, const std::vector<std::string>& images, const std::wstring& output, HANDLE cancelEvent)
307
{
308
wil::unique_hfile outputFile{
309
CreateFileW(output.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
310
THROW_LAST_ERROR_IF(!outputFile);
311
312
- Save(session, image, outputFile.get(), cancelEvent);
312
+ Save(session, images, outputFile.get(), cancelEvent);
313
}
314
315
-void ImageService::Save(wsl::windows::wslc::models::Session& session, const std::string& image, HANDLE outputHandle, HANDLE cancelEvent)
315
+void ImageService::Save(wsl::windows::wslc::models::Session& session, const std::vector<std::string>& images, HANDLE outputHandle, HANDLE cancelEvent)
316
{
317
+ WI_ASSERT(!images.empty());
318
+
319
wsl::windows::common::HandleConsoleProgressBar progressBar(
318
- outputHandle, L"Save in progress.", wsl::windows::common::HandleConsoleProgressBar::Format::FileSize);
319
- THROW_IF_FAILED(session.Get()->SaveImage(ToCOMInputHandle(outputHandle), image.c_str(), nullptr, cancelEvent));
320
+ outputHandle, Localization::MessageWslcSaveInProgress(), wsl::windows::common::HandleConsoleProgressBar::Format::FileSize);
321
+
322
+ if (images.size() == 1)
323
+ {
324
+ THROW_IF_FAILED(session.Get()->SaveImage(ToCOMInputHandle(outputHandle), images[0].c_str(), nullptr, cancelEvent));
325
+ }
326
+ else
327
+ {
328
+ std::vector<LPCSTR> imagePointers;
329
+ imagePointers.reserve(images.size());
330
+ for (const auto& image : images)
331
+ {
332
+ imagePointers.push_back(image.c_str());
333
+ }
334
+
335
+ WSLCStringArray imageArray{
336
+ .Values = imagePointers.data(),
337
+ .Count = static_cast<ULONG>(imagePointers.size()),
338
+ };
339
+
340
+ THROW_IF_FAILED(session.Get()->SaveImages(ToCOMInputHandle(outputHandle), &imageArray, nullptr, cancelEvent));
341
+ }
342
}
343
344
wsl::windows::wslc::models::PruneImagesResult ImageService::Prune(
src/windows/wslc/services/ImageService.h
+2
-2
@@ -40,8 +40,8 @@ public:
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);
42
static void Push(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback);
43
- static void Save(wsl::windows::wslc::models::Session& session, const std::string& image, const std::wstring& output, HANDLE cancelEvent = nullptr);
44
- static void Save(wsl::windows::wslc::models::Session& session, const std::string& image, HANDLE outputHandle, HANDLE cancelEvent = nullptr);
43
+ static void Save(wsl::windows::wslc::models::Session& session, const std::vector<std::string>& images, const std::wstring& output, HANDLE cancelEvent = nullptr);
44
+ static void Save(wsl::windows::wslc::models::Session& session, const std::vector<std::string>& images, HANDLE outputHandle, HANDLE cancelEvent = nullptr);
45
static void Tag(wsl::windows::wslc::models::Session& session, const std::string& sourceImage, const std::string& targetImage);
46
static wsl::windows::wslc::models::PruneImagesResult Prune(
47
wsl::windows::wslc::models::Session& session, bool all, const std::vector<std::pair<std::string, std::string>>& filters = {});
src/windows/wslc/tasks/ImageTasks.cpp
+10
-3
@@ -272,12 +272,19 @@ void SaveImage(CLIExecutionContext& context)
272
WI_ASSERT(context.Data.Contains(Data::Session));
273
WI_ASSERT(context.Args.Contains(ArgType::ImageId));
274
auto& session = context.Data.Get<Data::Session>();
275
- auto& imageId = context.Args.Get<ArgType::ImageId>();
275
+ auto imageIds = context.Args.GetAll<ArgType::ImageId>();
276
+
277
+ std::vector<std::string> images;
278
+ images.reserve(imageIds.size());
279
+ for (const auto& id : imageIds)
280
+ {
281
+ images.push_back(WideToMultiByte(id));
282
+ }
283
284
if (context.Args.Contains(ArgType::Output))
285
{
286
auto& output = context.Args.Get<ArgType::Output>();
280
- services::ImageService::Save(session, WideToMultiByte(imageId), output, context.CreateCancelEvent());
287
+ services::ImageService::Save(session, images, output, context.CreateCancelEvent());
288
}
289
else
290
{
@@ -287,7 +294,7 @@ void SaveImage(CLIExecutionContext& context)
294
THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_ImageSaveStdoutIsTerminalError());
295
}
296
290
- services::ImageService::Save(session, WideToMultiByte(imageId), stdoutHandle, context.CreateCancelEvent());
297
+ services::ImageService::Save(session, images, stdoutHandle, context.CreateCancelEvent());
298
}
299
}
300
src/windows/wslcsession/DockerHTTPClient.cpp
+15
@@ -227,6 +227,21 @@ std::pair<uint32_t, wil::unique_socket> DockerHTTPClient::SaveImage(const std::s
227
return {response.result_int(), std::move(socket)};
228
}
229
230
+std::pair<uint32_t, wil::unique_socket> DockerHTTPClient::SaveImages(const std::vector<std::string>& NamesOrIds)
231
+{
232
+ auto url = URL::Create("/images/get");
233
+ for (const auto& name : NamesOrIds)
234
+ {
235
+ // 'names' is a repeated query parameter.
236
+ // See: https://docs.docker.com/reference/api/engine/version/v1.52/#tag/Image/operation/ImageGet
237
+ url.SetParameter("names", name);
238
+ }
239
+
240
+ auto [response, socket] = SendRequest(verb::get, url, {}, {});
241
+
242
+ return {response.result_int(), std::move(socket)};
243
+}
244
+
245
docker_schema::PruneImageResult DockerHTTPClient::PruneImages(const std::map<std::string, std::vector<std::string>>& filters)
246
{
247
auto url = URL::Create("/images/prune");
src/windows/wslcsession/DockerHTTPClient.h
+2
-1
@@ -164,6 +164,7 @@ public:
164
common::docker_schema::InspectImage InspectImage(const std::string& NameOrId);
165
std::vector<common::docker_schema::DeletedImage> DeleteImage(const char* Image, bool Force, bool NoPrune); // Image can be ID or Repo:Tag.
166
std::pair<uint32_t, wil::unique_socket> SaveImage(const std::string& NameOrId);
167
+ std::pair<uint32_t, wil::unique_socket> SaveImages(const std::vector<std::string>& NamesOrIds);
168
common::docker_schema::PruneImageResult PruneImages(const std::map<std::string, std::vector<std::string>>& filters = {});
169
170
// Exec.
@@ -224,7 +225,7 @@ private:
225
static std::string Escape(const std::string& Value);
226
227
std::string m_path;
227
- std::map<std::string, std::string> m_parameters;
228
+ std::multimap<std::string, std::string> m_parameters;
229
};
230
231
wil::unique_socket ConnectSocket();
src/windows/wslcsession/WSLCSession.cpp
+33
@@ -1200,6 +1200,39 @@ try
1200
}
1201
CATCH_RETURN();
1202
1203
+HRESULT WSLCSession::SaveImages(WSLCHandle OutHandle, const WSLCStringArray* ImageNames, IProgressCallback* ProgressCallback, HANDLE CancelEvent)
1204
+try
1205
+{
1206
+ UNREFERENCED_PARAMETER(ProgressCallback);
1207
+
1208
+ COMServiceExecutionContext context;
1209
+
1210
+ RETURN_HR_IF_NULL(E_POINTER, ImageNames);
1211
+ RETURN_HR_IF(E_INVALIDARG, ImageNames->Count == 0);
1212
+ RETURN_HR_IF(E_INVALIDARG, ImageNames->Count > WSLC_MAX_SAVE_IMAGES_COUNT);
1213
+ RETURN_HR_IF_NULL(E_INVALIDARG, ImageNames->Values);
1214
+
1215
+ std::vector<std::string> names;
1216
+ names.reserve(ImageNames->Count);
1217
+ for (ULONG i = 0; i < ImageNames->Count; i += 1)
1218
+ {
1219
+ RETURN_HR_IF_NULL(E_INVALIDARG, ImageNames->Values[i]);
1220
+ const size_t length = strlen(ImageNames->Values[i]);
1221
+ RETURN_HR_IF(E_INVALIDARG, length == 0);
1222
+ RETURN_HR_IF(E_INVALIDARG, length > WSLC_MAX_IMAGE_NAME_LENGTH);
1223
+ names.emplace_back(ImageNames->Values[i]);
1224
+ }
1225
+
1226
+ auto lock = m_lock.lock_shared();
1227
+
1228
+ THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1229
+
1230
+ auto retVal = m_dockerClient->SaveImages(names);
1231
+ SaveImageImpl(retVal, OutHandle, CancelEvent);
1232
+ return S_OK;
1233
+}
1234
+CATCH_RETURN();
1235
+
1236
void WSLCSession::SaveImageImpl(std::pair<uint32_t, wil::unique_socket>& SocketCodePair, WSLCHandle OutputHandle, HANDLE CancelEvent)
1237
{
1238
auto userHandle = OpenUserHandle(OutputHandle);
src/windows/wslcsession/WSLCSession.h
+1
@@ -109,6 +109,7 @@ public:
109
_In_ ULONGLONG ContentLength,
110
_In_opt_ IWarningCallback* WarningCallback) override;
111
IFACEMETHOD(SaveImage)(_In_ WSLCHandle OutputHandle, _In_ LPCSTR ImageNameOrID, _In_ IProgressCallback* ProgressCallback, _In_opt_ HANDLE CancelEvent) override;
112
+ IFACEMETHOD(SaveImages)(_In_ WSLCHandle OutputHandle, _In_ const WSLCStringArray* ImageNames, _In_ IProgressCallback* ProgressCallback, _In_opt_ HANDLE CancelEvent) override;
113
IFACEMETHOD(ListImages)(_In_opt_ const WSLCListImagesOptions* Options, _Out_ WSLCImageInformation** Images, _Out_ ULONG* Count) override;
114
IFACEMETHOD(DeleteImage)(_In_ const WSLCDeleteImageOptions* Options, _Out_ WSLCDeletedImageInformation** DeletedImages, _Out_ ULONG* Count) override;
115
IFACEMETHOD(TagImage)(_In_ const WSLCTagImageOptions* Options) override;
test/windows/WSLCTests.cpp
+139
-8
@@ -1022,17 +1022,13 @@ class WSLCTests
1022
LogInfo("Test: Dangling filter");
1023
{
1024
// Setup a dangling image
1025
- LoadTestImage(*m_defaultSession, "alpine:latest");
1025
WSLCTagImageOptions tagOptions{};
1026
tagOptions.Image = "debian:latest";
1027
tagOptions.Repo = "alpine";
1028
tagOptions.Tag = "latest";
1029
VERIFY_SUCCEEDED(m_defaultSession->TagImage(&tagOptions));
1030
1032
- auto alpineCleanup = wil::scope_exit([&]() {
1033
- RunCommand(m_defaultSession.get(), {"/usr/bin/docker", "image", "prune", "-f"});
1034
- LOG_IF_FAILED(DeleteImageNoThrow("alpine:latest", WSLCDeleteImageFlagsNone).first);
1035
- });
1031
+ auto restore = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LoadTestImage(*m_defaultSession, "alpine:latest"); });
1032
1033
// List only dangling images
1034
WSLCFilter danglingTrueFilter{.Key = "dangling", .Value = "true"};
@@ -1305,12 +1301,11 @@ class WSLCTests
1301
1302
WSLC_TEST_METHOD(DeleteImage)
1303
{
1308
- // Prepare alpine image to delete.
1309
- LoadTestImage(*m_defaultSession, "alpine:latest");
1310
-
1304
// Verify that the image is in the list of images.
1305
ExpectImagePresent(*m_defaultSession, "alpine:latest");
1306
1307
+ auto restore = wil::scope_exit([&]() { LoadTestImage(*m_defaultSession, "alpine:latest"); });
1308
+
1309
// Launch a container to ensure that image deletion fails when in use.
1310
WSLCContainerLauncher launcher("alpine:latest", "test-delete-container-in-use", {"sleep", "99999"}, {}, "host");
1311
@@ -2492,6 +2487,142 @@ class WSLCTests
2487
}
2488
}
2489
2490
+ WSLC_TEST_METHOD(SaveImages)
2491
+ {
2492
+ auto BuildStringArray = [](const std::vector<LPCSTR>& values) -> WSLCStringArray {
2493
+ return WSLCStringArray{.Values = values.empty() ? nullptr : values.data(), .Count = static_cast<ULONG>(values.size())};
2494
+ };
2495
+
2496
+ // Save multiple images to a single tar, delete one, then load back and verify.
2497
+ {
2498
+ std::filesystem::path imageTar = L"MultiImageExport.tar";
2499
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2500
+ DeleteFileW(imageTar.c_str());
2501
+
2502
+ wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
2503
+ ULONGLONG spaceReclaimed = 0;
2504
+
2505
+ LOG_IF_FAILED(m_defaultSession->PruneImages(
2506
+ nullptr, 0, deletedImages.addressof(), deletedImages.size_address<ULONG>(), &spaceReclaimed));
2507
+ });
2508
+
2509
+ {
2510
+ wil::unique_handle imageTarFileHandle{CreateFileW(
2511
+ imageTar.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
2512
+ VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
2513
+
2514
+ std::vector<LPCSTR> names = {"hello-world:latest", "alpine:latest"};
2515
+ WSLCStringArray array = BuildStringArray(names);
2516
+ VERIFY_SUCCEEDED(m_defaultSession->SaveImages(ToCOMInputHandle(imageTarFileHandle.get()), &array, nullptr, nullptr));
2517
+
2518
+ LARGE_INTEGER fileSize{};
2519
+ VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2520
+ VERIFY_IS_TRUE(fileSize.QuadPart > 0);
2521
+ }
2522
+
2523
+ // Delete hello-world:latest and verify it's gone.
2524
+ wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deleted;
2525
+ WSLCDeleteImageOptions delOpts{};
2526
+ delOpts.Image = "hello-world:latest";
2527
+ delOpts.Flags = WSLCDeleteImageFlagsForce;
2528
+ VERIFY_SUCCEEDED(m_defaultSession->DeleteImage(&delOpts, &deleted, deleted.size_address<ULONG>()));
2529
+ ExpectImagePresent(*m_defaultSession, "hello-world:latest", false);
2530
+
2531
+ // Load it back from the multi-image tar — hello-world should reappear and alpine should still be present.
2532
+ {
2533
+ wil::unique_handle imageTarFileHandle{CreateFileW(
2534
+ imageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
2535
+ VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
2536
+ LARGE_INTEGER fileSize{};
2537
+ VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2538
+ VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), nullptr, fileSize.QuadPart, nullptr));
2539
+ }
2540
+
2541
+ ExpectImagePresent(*m_defaultSession, "hello-world:latest");
2542
+ ExpectImagePresent(*m_defaultSession, "alpine:latest");
2543
+
2544
+ // Sanity check that the loaded hello-world image is functional.
2545
+ WSLCContainerLauncher launcher("hello-world:latest", "wslc-multi-save-container");
2546
+ auto container = launcher.Launch(*m_defaultSession);
2547
+
2548
+ auto output = container.GetInitProcess().WaitAndCaptureOutput();
2549
+ VERIFY_ARE_EQUAL(0, output.Code);
2550
+ VERIFY_IS_TRUE(output.Output[1].find("Hello from Docker!") != std::string::npos);
2551
+ }
2552
+
2553
+ // Single image via SaveImages — must produce a valid tar archive.
2554
+ {
2555
+ std::filesystem::path imageTar = L"MultiImageSingle.tar";
2556
+ auto cleanup =
2557
+ wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(imageTar.c_str())); });
2558
+
2559
+ wil::unique_handle imageTarFileHandle{
2560
+ CreateFileW(imageTar.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
2561
+ VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
2562
+
2563
+ std::vector<LPCSTR> names = {"hello-world:latest"};
2564
+ WSLCStringArray array = BuildStringArray(names);
2565
+ VERIFY_SUCCEEDED(m_defaultSession->SaveImages(ToCOMInputHandle(imageTarFileHandle.get()), &array, nullptr, nullptr));
2566
+
2567
+ LARGE_INTEGER fileSize{};
2568
+ VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2569
+ VERIFY_IS_TRUE(fileSize.QuadPart > 0);
2570
+ }
2571
+
2572
+ // Validate that invalid input parameters are rejected.
2573
+ {
2574
+ // Use a real temp file so ToCOMInputHandle doesn't throw before SaveImages runs.
2575
+ std::filesystem::path placeholderTar = L"MultiImageValidation.tar";
2576
+ auto placeholderCleanup =
2577
+ wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(placeholderTar.c_str())); });
2578
+
2579
+ wil::unique_handle placeholder{CreateFileW(
2580
+ placeholderTar.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
2581
+ VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == placeholder.get());
2582
+ HANDLE phHandle = placeholder.get();
2583
+
2584
+ // Empty array (Count=0).
2585
+ WSLCStringArray emptyArray{.Values = nullptr, .Count = 0};
2586
+ VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->SaveImages(ToCOMInputHandle(phHandle), &emptyArray, nullptr, nullptr));
2587
+
2588
+ // Empty string entry.
2589
+ LPCSTR emptyEntry[] = {""};
2590
+ WSLCStringArray emptyEntryArray{.Values = emptyEntry, .Count = 1};
2591
+ VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->SaveImages(ToCOMInputHandle(phHandle), &emptyEntryArray, nullptr, nullptr));
2592
+
2593
+ // Name longer than WSLC_MAX_IMAGE_NAME_LENGTH.
2594
+ std::string longName(WSLC_MAX_IMAGE_NAME_LENGTH + 1, 'a');
2595
+ LPCSTR longEntry[] = {longName.c_str()};
2596
+ WSLCStringArray longEntryArray{.Values = longEntry, .Count = 1};
2597
+ VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->SaveImages(ToCOMInputHandle(phHandle), &longEntryArray, nullptr, nullptr));
2598
+
2599
+ // Too many images.
2600
+ std::vector<LPCSTR> names(WSLC_MAX_SAVE_IMAGES_COUNT + 1, "foo");
2601
+ WSLCStringArray tooManyArray = BuildStringArray(names);
2602
+ VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->SaveImages(ToCOMInputHandle(phHandle), &tooManyArray, nullptr, nullptr));
2603
+ }
2604
+
2605
+ // Try to save with one of the images not found — must fail
2606
+ {
2607
+ std::filesystem::path imageTar = L"MultiImageError.tar";
2608
+ auto cleanup =
2609
+ wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(imageTar.c_str())); });
2610
+
2611
+ wil::unique_handle imageTarFileHandle{CreateFileW(
2612
+ imageTar.c_str(), GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
2613
+ VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
2614
+
2615
+ std::vector<LPCSTR> names = {"alpine:latest", "not-found"};
2616
+ WSLCStringArray array = BuildStringArray(names);
2617
+ VERIFY_FAILED(m_defaultSession->SaveImages(ToCOMInputHandle(imageTarFileHandle.get()), &array, nullptr, nullptr));
2618
+
2619
+ ValidateCOMErrorMessage(L"No such image: not-found");
2620
+ LARGE_INTEGER fileSize{};
2621
+ VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2622
+ VERIFY_ARE_EQUAL(0ull, static_cast<ULONGLONG>(fileSize.QuadPart));
2623
+ }
2624
+ }
2625
+
2626
WSLC_TEST_METHOD(SynchronousIoCancellation)
2627
{
2628
// Create a blocked operation that will cause the service to get stuck on a ReadFile() call.
test/windows/wslc/e2e/WSLCE2EImageSaveTests.cpp
+43
-1
@@ -26,6 +26,7 @@ class WSLCE2EImageSaveTests
26
TEST_CLASS_CLEANUP(ClassCleanup)
27
{
28
EnsureImageIsDeleted(DebianImage);
29
+ EnsureImageIsDeleted(AlpineImage);
30
return true;
31
}
32
@@ -128,9 +129,50 @@ class WSLCE2EImageSaveTests
129
auto runResult = RunWslc(std::format(L"container run --rm {} echo Hello from saved image!", DebianImage.NameAndTag()));
130
runResult.Verify({.Stdout = L"Hello from saved image!\n", .Stderr = L"", .ExitCode = 0});
131
}
132
+ WSLC_TEST_METHOD(WSLCE2E_Image_Save_MultipleImages_Load)
133
+ {
134
+ EnsureImageIsLoaded(AlpineImage);
135
+
136
+ // Force a pristine re-load of DebianImage at the end so subsequent tests (in fast mode)
137
+ // see the same on-disk tar as DebianImage.Path. Without this, reloading from a
138
+ // multi-image archive can produce a slightly different on-disk representation that
139
+ // breaks byte-exact size checks in WSLCE2E_Image_Save_Success.
140
+ auto restoreDebian = wil::scope_exit([&]() { EnsureImageIsDeleted(DebianImage); });
141
+
142
+ // Save both images into a single archive.
143
+ const auto saveResult = RunWslc(std::format(
144
+ L"image save --output \"{}\" {} {}", SavedArchivePath.wstring(), DebianImage.NameAndTag(), AlpineImage.NameAndTag()));
145
+ saveResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
146
+
147
+ // Delete both source images.
148
+ EnsureImageIsDeleted(DebianImage);
149
+ EnsureImageIsDeleted(AlpineImage);
150
+
151
+ // Load both images back from the single archive.
152
+ const auto loadResult = RunWslc(std::format(L"image load --input \"{}\"", SavedArchivePath.wstring()));
153
+ loadResult.Verify({.Stderr = L"", .ExitCode = 0});
154
+
155
+ // Run a container from each loaded image to confirm both are restored and runnable.
156
+ const auto runDebian = RunWslc(std::format(L"container run --rm {} echo ok!", DebianImage.NameAndTag()));
157
+ runDebian.Verify({.Stdout = std::format(L"ok!\n"), .Stderr = L"", .ExitCode = 0});
158
+
159
+ const auto runAlpine = RunWslc(std::format(L"container run --rm {} echo ok!", AlpineImage.NameAndTag()));
160
+ runAlpine.Verify({.Stdout = std::format(L"ok!\n"), .Stderr = L"", .ExitCode = 0});
161
+ }
162
+
163
+ WSLC_TEST_METHOD(WSLCE2E_Image_Save_MultipleImages_InvalidImage)
164
+ {
165
+ const auto result = RunWslc(std::format(
166
+ L"image save --output \"{}\" {} {}", SavedArchivePath.wstring(), DebianImage.NameAndTag(), InvalidImage.NameAndTag()));
167
+ VERIFY_IS_TRUE(result.ExitCode.has_value());
168
+ VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
169
+ VERIFY_IS_TRUE(result.Stderr.has_value());
170
+ VERIFY_ARE_NOT_EQUAL(0u, result.Stderr.value().size());
171
+ }
172
173
private:
174
const TestImage DebianImage = DebianTestImage();
175
+ const TestImage& AlpineImage = AlpineTestImage();
176
const TestImage& InvalidImage = InvalidTestImage();
177
178
std::filesystem::path SavedArchivePath{};
@@ -176,4 +218,4 @@ private:
218
return options.str();
219
}
220
};
179
-} // namespace WSLCE2ETests
\ No newline at end of file
221
+} // namespace WSLCE2ETests