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();
}