From 8f0fc9ebec00c970ce6297dd6392f99d1e9c0039 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:51:28 -0700 Subject: [PATCH 1/2] Add image import command --- localization/strings/en-US/Resources.resw | 9 + .../wslc/arguments/ArgumentDefinitions.h | 1 + src/windows/wslc/commands/ImageCommand.cpp | 1 + src/windows/wslc/commands/ImageCommand.h | 16 ++ .../wslc/commands/ImageImportCommand.cpp | 52 ++++++ src/windows/wslc/commands/RootCommand.cpp | 1 + src/windows/wslc/services/ImageService.cpp | 26 +++ src/windows/wslc/services/ImageService.h | 1 + src/windows/wslc/tasks/ImageTasks.cpp | 16 ++ src/windows/wslc/tasks/ImageTasks.h | 1 + test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp | 1 + .../wslc/e2e/WSLCE2EImageImportTests.cpp | 154 ++++++++++++++++++ test/windows/wslc/e2e/WSLCE2EImageTests.cpp | 1 + 13 files changed, 280 insertions(+) create mode 100644 src/windows/wslc/commands/ImageImportCommand.cpp create mode 100644 test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index de258d29d..4e71c3f9b 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2287,6 +2287,12 @@ For privacy information about this product please visit https://aka.ms/privacy.< Inspect images. + + Import an image from a tarball. + + + Imports the contents of a tarball to create a filesystem image. Optionally tag the image with a repository and tag name. + List images. @@ -2529,6 +2535,9 @@ On first run, creates the file with all settings commented out at their defaults Image name + + File or - to read from stdin + Provides path to the tar archive file containing the image diff --git a/src/windows/wslc/arguments/ArgumentDefinitions.h b/src/windows/wslc/arguments/ArgumentDefinitions.h index a7bfe4e9b..d6456e644 100644 --- a/src/windows/wslc/arguments/ArgumentDefinitions.h +++ b/src/windows/wslc/arguments/ArgumentDefinitions.h @@ -61,6 +61,7 @@ _(Help, "help", WSLC_CLI_HELP_ARG, Kind::Flag, L _(Hostname, "hostname", L"h", Kind::Value, Localization::WSLCCLI_HostnameArgDescription()) \ _(ImageForce, "force", L"f", Kind::Flag, Localization::WSLCCLI_ImageForceArgDescription()) \ _(ImageId, "image", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ImageIdArgDescription()) \ +_(ImportFile, "file", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ImportFileArgDescription()) \ _(Input, "input", L"i", Kind::Value, Localization::WSLCCLI_InputArgDescription()) \ _(Interactive, "interactive", L"i", Kind::Flag, Localization::WSLCCLI_InteractiveArgDescription()) \ _(Label, "label", NO_ALIAS, Kind::Value, L"Volume metadata setting") \ diff --git a/src/windows/wslc/commands/ImageCommand.cpp b/src/windows/wslc/commands/ImageCommand.cpp index 5b2492745..99507f695 100644 --- a/src/windows/wslc/commands/ImageCommand.cpp +++ b/src/windows/wslc/commands/ImageCommand.cpp @@ -27,6 +27,7 @@ std::vector> ImageCommand::GetCommands() const commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); diff --git a/src/windows/wslc/commands/ImageCommand.h b/src/windows/wslc/commands/ImageCommand.h index 01476af13..604c22161 100644 --- a/src/windows/wslc/commands/ImageCommand.h +++ b/src/windows/wslc/commands/ImageCommand.h @@ -91,6 +91,22 @@ struct ImageLoadCommand final : public Command void ExecuteInternal(CLIExecutionContext& context) const override; }; +// Import Command +struct ImageImportCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"import"; + + ImageImportCommand(const std::wstring& parent) : Command(CommandName, parent) + { + } + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& context) const override; +}; + // Remove Command struct ImageRemoveCommand final : public Command { diff --git a/src/windows/wslc/commands/ImageImportCommand.cpp b/src/windows/wslc/commands/ImageImportCommand.cpp new file mode 100644 index 000000000..cbd4b8b20 --- /dev/null +++ b/src/windows/wslc/commands/ImageImportCommand.cpp @@ -0,0 +1,52 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + ImageImportCommand.cpp + +Abstract: + + Implementation of command execution logic. + +--*/ + +#include "ImageCommand.h" +#include "CLIExecutionContext.h" +#include "ImageTasks.h" +#include "SessionTasks.h" +#include "Task.h" + +using namespace wsl::windows::wslc::execution; +using namespace wsl::windows::wslc::task; +using namespace wsl::shared; + +namespace wsl::windows::wslc { +// Image Import Command +std::vector ImageImportCommand::GetArguments() const +{ + return { + Argument::Create(ArgType::ImportFile, true), + Argument::Create(ArgType::ImageId), + Argument::Create(ArgType::Session), + }; +} + +std::wstring ImageImportCommand::ShortDescription() const +{ + return Localization::WSLCCLI_ImageImportDesc(); +} + +std::wstring ImageImportCommand::LongDescription() const +{ + return Localization::WSLCCLI_ImageImportLongDesc(); +} + +void ImageImportCommand::ExecuteInternal(CLIExecutionContext& context) const +{ + context // + << CreateSession // + << ImportImage; +} +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/RootCommand.cpp b/src/windows/wslc/commands/RootCommand.cpp index 5b28bb3f4..e78d0aed2 100644 --- a/src/windows/wslc/commands/RootCommand.cpp +++ b/src/windows/wslc/commands/RootCommand.cpp @@ -41,6 +41,7 @@ std::vector> RootCommand::GetCommands() const commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName(), true)); + commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); diff --git a/src/windows/wslc/services/ImageService.cpp b/src/windows/wslc/services/ImageService.cpp index 578f2698c..182cf4964 100644 --- a/src/windows/wslc/services/ImageService.cpp +++ b/src/windows/wslc/services/ImageService.cpp @@ -187,6 +187,32 @@ void ImageService::Load(wsl::windows::wslc::models::Session& session, const std: THROW_IF_FAILED(session.Get()->LoadImage(ToCOMInputHandle(imageFile.get()), nullptr, fileSize.QuadPart)); } +void ImageService::Import(wsl::windows::wslc::models::Session& session, const std::wstring& input, const std::string& imageName) +{ + HANDLE imageHandle = nullptr; + wil::unique_hfile imageFile; + ULONGLONG contentLength = 0; + + if (input == L"-") + { + imageHandle = GetStdHandle(STD_INPUT_HANDLE); + } + else + { + imageFile.reset( + CreateFileW(input.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); + THROW_LAST_ERROR_IF(!imageFile); + + LARGE_INTEGER fileSize{}; + THROW_LAST_ERROR_IF(!GetFileSizeEx(imageFile.get(), &fileSize)); + + imageHandle = imageFile.get(); + contentLength = fileSize.QuadPart; + } + + THROW_IF_FAILED(session.Get()->ImportImage(ToCOMInputHandle(imageHandle), imageName.c_str(), nullptr, contentLength)); +} + void ImageService::Delete(wsl::windows::wslc::models::Session& session, const std::string& image, bool force, bool noPrune) { WSLCDeleteImageOptions options{}; diff --git a/src/windows/wslc/services/ImageService.h b/src/windows/wslc/services/ImageService.h index 5f3d0697e..6f2f5e36f 100644 --- a/src/windows/wslc/services/ImageService.h +++ b/src/windows/wslc/services/ImageService.h @@ -34,6 +34,7 @@ class ImageService static std::vector List(wsl::windows::wslc::models::Session& session); static void Load(wsl::windows::wslc::models::Session& session, const std::wstring& input); + static void Import(wsl::windows::wslc::models::Session& session, const std::wstring& input, const std::string& imageName); static void Delete(wsl::windows::wslc::models::Session& session, const std::string& image, bool force, bool noPrune); static wsl::windows::common::wslc_schema::InspectImage Inspect(wsl::windows::wslc::models::Session& session, const std::string& image); static void Pull(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback); diff --git a/src/windows/wslc/tasks/ImageTasks.cpp b/src/windows/wslc/tasks/ImageTasks.cpp index bba509ee4..89f4c499e 100644 --- a/src/windows/wslc/tasks/ImageTasks.cpp +++ b/src/windows/wslc/tasks/ImageTasks.cpp @@ -187,6 +187,22 @@ void LoadImage(CLIExecutionContext& context) THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_ImageLoadNoInputError()); } +void ImportImage(CLIExecutionContext& context) +{ + WI_ASSERT(context.Data.Contains(Data::Session)); + WI_ASSERT(context.Args.Contains(ArgType::ImportFile)); + auto& session = context.Data.Get(); + + std::string imageName; + if (context.Args.Contains(ArgType::ImageId)) + { + imageName = WideToMultiByte(context.Args.Get()); + } + + auto& input = context.Args.Get(); + services::ImageService::Import(session, input, imageName); +} + void InspectImages(CLIExecutionContext& context) { WI_ASSERT(context.Data.Contains(Data::Session)); diff --git a/src/windows/wslc/tasks/ImageTasks.h b/src/windows/wslc/tasks/ImageTasks.h index b95953a0d..c56c41976 100644 --- a/src/windows/wslc/tasks/ImageTasks.h +++ b/src/windows/wslc/tasks/ImageTasks.h @@ -21,6 +21,7 @@ void BuildImage(CLIExecutionContext& context); void GetImages(CLIExecutionContext& context); void ListImages(CLIExecutionContext& context); void LoadImage(CLIExecutionContext& context); +void ImportImage(CLIExecutionContext& context); void PullImage(CLIExecutionContext& context); void PushImage(CLIExecutionContext& context); void DeleteImage(CLIExecutionContext& context); diff --git a/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp b/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp index 3a59802a2..077707c31 100644 --- a/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp @@ -534,6 +534,7 @@ class WSLCE2EGlobalTests {L"create", Localization::WSLCCLI_ContainerCreateDesc()}, {L"exec", Localization::WSLCCLI_ContainerExecDesc()}, {L"images", Localization::WSLCCLI_ImageListDesc()}, + {L"import", Localization::WSLCCLI_ImageImportDesc()}, {L"inspect", Localization::WSLCCLI_InspectDesc()}, {L"kill", Localization::WSLCCLI_ContainerKillDesc()}, {L"list", Localization::WSLCCLI_ContainerListDesc()}, diff --git a/test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp b/test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp new file mode 100644 index 000000000..b91a87f8e --- /dev/null +++ b/test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp @@ -0,0 +1,154 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCE2EImageImportTests.cpp + +Abstract: + + This file contains end-to-end tests for WSLC image import. +--*/ + +#include "precomp.h" +#include "windows/Common.h" +#include "WSLCExecutor.h" +#include "WSLCE2EHelpers.h" + +namespace WSLCE2ETests { +using namespace wsl::shared; + +class WSLCE2EImageImportTests +{ + WSLC_TEST_CLASS(WSLCE2EImageImportTests) + + TEST_CLASS_CLEANUP(ClassCleanup) + { + EnsureImageIsDeleted(DebianImage); + EnsureImageIsDeleted(ImportedImage); + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + EnsureImageIsLoaded(DebianImage); + EnsureImageIsDeleted(ImportedImage); + SavedArchivePath = wsl::windows::common::filesystem::GetTempFilename(); + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + DeleteFileW(SavedArchivePath.c_str()); + return true; + } + + WSLC_TEST_METHOD(WSLCE2E_Image_Import_HelpCommand) + { + auto result = RunWslc(L"image import --help"); + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + WSLC_TEST_METHOD(WSLCE2E_Image_Import_MissingFile) + { + const auto result = RunWslc(L"image import"); + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'file'\r\n", .ExitCode = 1}); + } + + WSLC_TEST_METHOD(WSLCE2E_Image_Import_Success) + { + // Save image as a tarball + auto saveResult = RunWslc(std::format(L"image save --output \"{}\" {}", SavedArchivePath.wstring(), DebianImage.NameAndTag())); + saveResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); + + // Import the tarball as a new image with a tag + auto importResult = + RunWslc(std::format(L"image import \"{}\" {}", SavedArchivePath.wstring(), ImportedImage.NameAndTag())); + importResult.Verify({.Stderr = L"", .ExitCode = 0}); + + // Verify the imported image is listed + VerifyImageIsListed(ImportedImage.NameAndTag()); + } + + WSLC_TEST_METHOD(WSLCE2E_Image_Import_WithoutTag) + { + // Save image as a tarball + auto saveResult = RunWslc(std::format(L"image save --output \"{}\" {}", SavedArchivePath.wstring(), DebianImage.NameAndTag())); + saveResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); + + // Import without specifying an image name + auto importResult = RunWslc(std::format(L"image import \"{}\"", SavedArchivePath.wstring())); + importResult.Verify({.Stderr = L"", .ExitCode = 0}); + } + + WSLC_TEST_METHOD(WSLCE2E_Image_Import_InvalidPath) + { + const auto result = + RunWslc(std::format(L"image import \"{}\" {}", L"C:\\nonexistent\\path\\image.tar", ImportedImage.NameAndTag())); + result.Verify({.ExitCode = 1}); + } + + WSLC_TEST_METHOD(WSLCE2E_Image_Import_FromRoot) + { + // Save image as a tarball + auto saveResult = RunWslc(std::format(L"image save --output \"{}\" {}", SavedArchivePath.wstring(), DebianImage.NameAndTag())); + saveResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); + + // Import using the root 'import' alias + auto importResult = + RunWslc(std::format(L"import \"{}\" {}", SavedArchivePath.wstring(), ImportedImage.NameAndTag())); + importResult.Verify({.Stderr = L"", .ExitCode = 0}); + + // Verify the imported image is listed + VerifyImageIsListed(ImportedImage.NameAndTag()); + } + +private: + const TestImage DebianImage = DebianTestImage(); + const TestImage ImportedImage{L"wslc-test-imported", L"latest", L""}; + + std::filesystem::path SavedArchivePath{}; + + std::wstring GetHelpMessage() const + { + std::wstringstream output; + output << GetWslcHeader() // + << GetDescription() // + << GetUsage() // + << GetAvailableCommands() // + << GetAvailableOptions(); + return output.str(); + } + + std::wstring GetDescription() const + { + return Localization::WSLCCLI_ImageImportLongDesc() + L"\r\n\r\n"; + } + + std::wstring GetUsage() const + { + return L"Usage: wslc image import [] []\r\n\r\n"; + } + + std::wstring GetAvailableCommands() const + { + std::wstringstream commands; + commands << L"The following arguments are available:\r\n" // + << L" file " << Localization::WSLCCLI_ImportFileArgDescription() << L"\r\n" // + << L" image " << Localization::WSLCCLI_ImageIdArgDescription() << L"\r\n" // + << L"\r\n"; + return commands.str(); + } + + std::wstring GetAvailableOptions() const + { + std::wstringstream options; + options << L"The following options are available:\r\n" // + << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n" // + << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n" // + << L"\r\n"; + return options.str(); + } +}; +} // namespace WSLCE2ETests diff --git a/test/windows/wslc/e2e/WSLCE2EImageTests.cpp b/test/windows/wslc/e2e/WSLCE2EImageTests.cpp index 7ade427bd..466839b58 100644 --- a/test/windows/wslc/e2e/WSLCE2EImageTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EImageTests.cpp @@ -72,6 +72,7 @@ class WSLCE2EImageTests {L"inspect", Localization::WSLCCLI_ImageInspectDesc()}, {L"list", Localization::WSLCCLI_ImageListDesc()}, {L"load", Localization::WSLCCLI_ImageLoadDesc()}, + {L"import", Localization::WSLCCLI_ImageImportDesc()}, {L"prune", Localization::WSLCCLI_ImagePruneDesc()}, {L"pull", Localization::WSLCCLI_ImagePullDesc()}, {L"push", Localization::WSLCCLI_ImagePushDesc()}, From fd1c2e6fbc4241effcf51d61813dc3687981dc66 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:43:04 -0700 Subject: [PATCH 2/2] Clang format --- src/windows/wslc/services/ImageService.cpp | 3 +-- .../wslc/e2e/WSLCE2EImageImportTests.cpp | 18 ++++++++---------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/windows/wslc/services/ImageService.cpp b/src/windows/wslc/services/ImageService.cpp index 182cf4964..60be8a77c 100644 --- a/src/windows/wslc/services/ImageService.cpp +++ b/src/windows/wslc/services/ImageService.cpp @@ -199,8 +199,7 @@ void ImageService::Import(wsl::windows::wslc::models::Session& session, const st } else { - imageFile.reset( - CreateFileW(input.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); + imageFile.reset(CreateFileW(input.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); THROW_LAST_ERROR_IF(!imageFile); LARGE_INTEGER fileSize{}; diff --git a/test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp b/test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp index b91a87f8e..d46e633bb 100644 --- a/test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp @@ -63,8 +63,7 @@ class WSLCE2EImageImportTests saveResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); // Import the tarball as a new image with a tag - auto importResult = - RunWslc(std::format(L"image import \"{}\" {}", SavedArchivePath.wstring(), ImportedImage.NameAndTag())); + auto importResult = RunWslc(std::format(L"image import \"{}\" {}", SavedArchivePath.wstring(), ImportedImage.NameAndTag())); importResult.Verify({.Stderr = L"", .ExitCode = 0}); // Verify the imported image is listed @@ -96,8 +95,7 @@ class WSLCE2EImageImportTests saveResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); // Import using the root 'import' alias - auto importResult = - RunWslc(std::format(L"import \"{}\" {}", SavedArchivePath.wstring(), ImportedImage.NameAndTag())); + auto importResult = RunWslc(std::format(L"import \"{}\" {}", SavedArchivePath.wstring(), ImportedImage.NameAndTag())); importResult.Verify({.Stderr = L"", .ExitCode = 0}); // Verify the imported image is listed @@ -134,9 +132,9 @@ class WSLCE2EImageImportTests std::wstring GetAvailableCommands() const { std::wstringstream commands; - commands << L"The following arguments are available:\r\n" // - << L" file " << Localization::WSLCCLI_ImportFileArgDescription() << L"\r\n" // - << L" image " << Localization::WSLCCLI_ImageIdArgDescription() << L"\r\n" // + commands << L"The following arguments are available:\r\n" // + << L" file " << Localization::WSLCCLI_ImportFileArgDescription() << L"\r\n" // + << L" image " << Localization::WSLCCLI_ImageIdArgDescription() << L"\r\n" // << L"\r\n"; return commands.str(); } @@ -144,9 +142,9 @@ class WSLCE2EImageImportTests std::wstring GetAvailableOptions() const { std::wstringstream options; - options << L"The following options are available:\r\n" // - << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n" // - << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n" // + options << L"The following options are available:\r\n" // + << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n" // + << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n" // << L"\r\n"; return options.str(); }