@samitouri / QOSAMI-WSL / commits / 4f90788c

CLI: Add image import command (#40473)

* Add image import command * Remove import from root test * Addressed comments * Fix build * Addressed comments * Addressed comments * Added TODO * Addressed comments

AmirMS committed May 13, 2026 at 16:21 UTC 4f90788ca7f295522f6217e3890e7aaefbbe3783
13 files changed +293 -6
localization/strings/en-US/Resources.resw
+12
@@ -2331,6 +2331,9 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2331 <value>Failed to open '{}': {}</value>
2332 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2333 </data>
2334 + <data name = "MessageWslcImportPipeNotSupported" xml:space = "preserve" >
2335 + <value>Importing from a pipe is not supported. Please redirect stdin from a file.</value>
2336 + </data>
2337 <data name = "MessageWslcBuildFileNotFound" xml:space = "preserve" >
2338 <value>No Containerfile or Dockerfile found in '{}'</value>
2339 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
@@ -2446,6 +2449,12 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2449 <data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
2450 <value>Inspect images.</value>
2451 </data>
2452 + <data name="WSLCCLI_ImageImportDesc" xml:space="preserve">
2453 + <value>Import an image from a tarball.</value>
2454 + </data>
2455 + <data name="WSLCCLI_ImageImportLongDesc" xml:space="preserve">
2456 + <value>Imports the contents of a tarball to create a filesystem image. Optionally tag the image with a repository and tag name.</value>
2457 + </data>
2458 <data name="WSLCCLI_ImageListDesc" xml:space="preserve">
2459 <value>List images.</value>
2460 </data>
@@ -2702,6 +2711,9 @@ On first run, creates the file with all settings commented out at their defaults
2711 <data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
2712 <value>Image name</value>
2713 </data>
2714 + <data name="WSLCCLI_ImportFileArgDescription" xml:space="preserve">
2715 + <value>File or - to read from stdin</value>
2716 + </data>
2717 <data name="WSLCCLI_InputArgDescription" xml:space="preserve">
2718 <value>Provides path to the tar archive file containing the image</value>
2719 </data>
src/windows/wslc/arguments/ArgumentDefinitions.h
+1
@@ -62,6 +62,7 @@ _(Help, "help", WSLC_CLI_HELP_ARG, Kind::Flag, L
62 _(Hostname, "hostname", L"h", Kind::Value, Localization::WSLCCLI_HostnameArgDescription()) \
63 _(ImageForce, "force", L"f", Kind::Flag, Localization::WSLCCLI_ImageForceArgDescription()) \
64 _(ImageId, "image", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ImageIdArgDescription()) \
65 +_(ImportFile, "file", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ImportFileArgDescription()) \
66 _(Input, "input", L"i", Kind::Value, Localization::WSLCCLI_InputArgDescription()) \
67 _(Interactive, "interactive", L"i", Kind::Flag, Localization::WSLCCLI_InteractiveArgDescription()) \
68 _(Label, "label", L"l", Kind::Value, Localization::WSLCCLI_LabelArgDescription()) \
src/windows/wslc/commands/ImageCommand.cpp
+1
@@ -27,6 +27,7 @@ std::vector<std::unique_ptr<Command>> ImageCommand::GetCommands() const
27 commands.push_back(std::make_unique<ImageInspectCommand>(FullName()));
28 commands.push_back(std::make_unique<ImageListCommand>(FullName()));
29 commands.push_back(std::make_unique<ImageLoadCommand>(FullName()));
30 + commands.push_back(std::make_unique<ImageImportCommand>(FullName()));
31 commands.push_back(std::make_unique<ImagePruneCommand>(FullName()));
32 commands.push_back(std::make_unique<ImagePullCommand>(FullName()));
33 commands.push_back(std::make_unique<ImagePushCommand>(FullName()));
src/windows/wslc/commands/ImageCommand.h
+16
@@ -91,6 +91,22 @@ protected:
91 void ExecuteInternal(CLIExecutionContext& context) const override;
92 };
93
94 +// Import Command
95 +struct ImageImportCommand final : public Command
96 +{
97 + constexpr static std::wstring_view CommandName = L"import";
98 +
99 + ImageImportCommand(const std::wstring& parent) : Command(CommandName, parent)
100 + {
101 + }
102 + std::vector<Argument> GetArguments() const override;
103 + std::wstring ShortDescription() const override;
104 + std::wstring LongDescription() const override;
105 +
106 +protected:
107 + void ExecuteInternal(CLIExecutionContext& context) const override;
108 +};
109 +
110 // Remove Command
111 struct ImageRemoveCommand final : public Command
112 {
src/windows/wslc/commands/ImageImportCommand.cpp new
+52
@@ -0,0 +1,52 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ImageImportCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ImageCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ImageTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +
25 +namespace wsl::windows::wslc {
26 +// Image Import Command
27 +std::vector<Argument> ImageImportCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::ImportFile, true),
31 + Argument::Create(ArgType::ImageId),
32 + Argument::Create(ArgType::Session),
33 + };
34 +}
35 +
36 +std::wstring ImageImportCommand::ShortDescription() const
37 +{
38 + return Localization::WSLCCLI_ImageImportDesc();
39 +}
40 +
41 +std::wstring ImageImportCommand::LongDescription() const
42 +{
43 + return Localization::WSLCCLI_ImageImportLongDesc();
44 +}
45 +
46 +void ImageImportCommand::ExecuteInternal(CLIExecutionContext& context) const
47 +{
48 + context //
49 + << CreateSession //
50 + << ImportImage;
51 +}
52 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/RootCommand.cpp
+1
@@ -41,6 +41,7 @@ std::vector<std::unique_ptr<Command>> RootCommand::GetCommands() const
41 commands.push_back(std::make_unique<ContainerCreateCommand>(FullName()));
42 commands.push_back(std::make_unique<ContainerExecCommand>(FullName()));
43 commands.push_back(std::make_unique<ImageListCommand>(FullName(), true));
44 + commands.push_back(std::make_unique<ImageImportCommand>(FullName()));
45 commands.push_back(std::make_unique<InspectCommand>(FullName()));
46 commands.push_back(std::make_unique<ContainerKillCommand>(FullName()));
47 commands.push_back(std::make_unique<ContainerListCommand>(FullName()));
src/windows/wslc/services/ImageService.cpp
+43 -6
@@ -16,6 +16,7 @@ Abstract:
16 #include "SessionService.h"
17 #include <wslutil.h>
18 #include <HandleConsoleProgressBar.h>
19 +#include <relay.hpp>
20
21 using namespace wsl::shared;
22 using namespace wsl::windows::common::wslutil;
@@ -71,6 +72,40 @@ std::string GetServerFromImage(const std::string& image)
72 return server;
73 }
74
75 +struct InputSource
76 +{
77 + InputSource(wsl::windows::common::relay::HandleWrapper&& handle, ULONGLONG contentLength) :
78 + Handle(std::move(handle)), ContentLength(contentLength)
79 + {
80 + }
81 +
82 + wsl::windows::common::relay::HandleWrapper Handle;
83 + ULONGLONG ContentLength = 0;
84 +};
85 +
86 +wsl::windows::common::relay::HandleWrapper OpenInputHandle(const std::wstring& input)
87 +{
88 + if (input == L"-")
89 + {
90 + return wsl::windows::common::relay::HandleWrapper(GetStdHandle(STD_INPUT_HANDLE));
91 + }
92 +
93 + wil::unique_hfile file(CreateFileW(input.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
94 + THROW_LAST_ERROR_IF(!file);
95 +
96 + return wsl::windows::common::relay::HandleWrapper(std::move(file));
97 +}
98 +
99 +InputSource OpenImageInput(const std::wstring& input)
100 +{
101 + auto handle = OpenInputHandle(input);
102 +
103 + LARGE_INTEGER fileSize{};
104 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcImportPipeNotSupported(), !GetFileSizeEx(handle.Get(), &fileSize));
105 +
106 + return InputSource{std::move(handle), static_cast<ULONGLONG>(fileSize.QuadPart)};
107 +}
108 +
109 } // namespace
110
111 namespace wsl::windows::wslc::services {
@@ -178,13 +213,15 @@ std::vector<ImageInformation> ImageService::List(wsl::windows::wslc::models::Ses
213
214 void ImageService::Load(wsl::windows::wslc::models::Session& session, const std::wstring& input)
215 {
181 - wil::unique_hfile imageFile{CreateFileW(input.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
182 - THROW_LAST_ERROR_IF(!imageFile);
183 -
184 - LARGE_INTEGER fileSize{};
185 - THROW_LAST_ERROR_IF(!GetFileSizeEx(imageFile.get(), &fileSize));
216 + auto source = OpenImageInput(input);
217 + THROW_IF_FAILED(session.Get()->LoadImage(ToCOMInputHandle(source.Handle.Get()), nullptr, source.ContentLength));
218 +}
219
187 - THROW_IF_FAILED(session.Get()->LoadImage(ToCOMInputHandle(imageFile.get()), nullptr, fileSize.QuadPart));
220 +void ImageService::Import(wsl::windows::wslc::models::Session& session, const std::wstring& input, const std::string& imageName)
221 +{
222 + auto source = OpenImageInput(input);
223 + THROW_IF_FAILED(session.Get()->ImportImage(
224 + ToCOMInputHandle(source.Handle.Get()), imageName.empty() ? nullptr : imageName.c_str(), nullptr, source.ContentLength));
225 }
226
227 void ImageService::Delete(wsl::windows::wslc::models::Session& session, const std::string& image, bool force, bool noPrune)
src/windows/wslc/services/ImageService.h
+1
@@ -34,6 +34,7 @@ public:
34
35 static std::vector<wsl::windows::wslc::models::ImageInformation> List(wsl::windows::wslc::models::Session& session);
36 static void Load(wsl::windows::wslc::models::Session& session, const std::wstring& input);
37 + static void Import(wsl::windows::wslc::models::Session& session, const std::wstring& input, const std::string& imageName);
38 static void Delete(wsl::windows::wslc::models::Session& session, const std::string& image, bool force, bool noPrune);
39 static wsl::windows::common::wslc_schema::InspectImage Inspect(wsl::windows::wslc::models::Session& session, const std::string& image);
40 static void Pull(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback);
src/windows/wslc/tasks/ImageTasks.cpp
+16
@@ -210,6 +210,22 @@ void LoadImage(CLIExecutionContext& context)
210 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_ImageLoadNoInputError());
211 }
212
213 +void ImportImage(CLIExecutionContext& context)
214 +{
215 + WI_ASSERT(context.Data.Contains(Data::Session));
216 + WI_ASSERT(context.Args.Contains(ArgType::ImportFile));
217 + auto& session = context.Data.Get<Data::Session>();
218 +
219 + std::string imageName;
220 + if (context.Args.Contains(ArgType::ImageId))
221 + {
222 + imageName = WideToMultiByte(context.Args.Get<ArgType::ImageId>());
223 + }
224 +
225 + auto& input = context.Args.Get<ArgType::ImportFile>();
226 + services::ImageService::Import(session, input, imageName);
227 +}
228 +
229 void InspectImages(CLIExecutionContext& context)
230 {
231 WI_ASSERT(context.Data.Contains(Data::Session));
src/windows/wslc/tasks/ImageTasks.h
+1
@@ -21,6 +21,7 @@ void BuildImage(CLIExecutionContext& context);
21 void GetImages(CLIExecutionContext& context);
22 void ListImages(CLIExecutionContext& context);
23 void LoadImage(CLIExecutionContext& context);
24 +void ImportImage(CLIExecutionContext& context);
25 void PullImage(CLIExecutionContext& context);
26 void PushImage(CLIExecutionContext& context);
27 void DeleteImage(CLIExecutionContext& context);
test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp
+1
@@ -534,6 +534,7 @@ private:
534 {L"create", Localization::WSLCCLI_ContainerCreateDesc()},
535 {L"exec", Localization::WSLCCLI_ContainerExecDesc()},
536 {L"images", Localization::WSLCCLI_ImageListDesc()},
537 + {L"import", Localization::WSLCCLI_ImageImportDesc()},
538 {L"inspect", Localization::WSLCCLI_InspectDesc()},
539 {L"kill", Localization::WSLCCLI_ContainerKillDesc()},
540 {L"list", Localization::WSLCCLI_ContainerListDesc()},
test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp new
+147
@@ -0,0 +1,147 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EImageImportTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC image import.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +
19 +namespace WSLCE2ETests {
20 +using namespace wsl::shared;
21 +
22 +class WSLCE2EImageImportTests
23 +{
24 + WSLC_TEST_CLASS(WSLCE2EImageImportTests)
25 +
26 + TEST_CLASS_CLEANUP(ClassCleanup)
27 + {
28 + EnsureImageIsDeleted(DebianImage);
29 + EnsureImageIsDeleted(ImportedImage);
30 + return true;
31 + }
32 +
33 + TEST_METHOD_SETUP(MethodSetup)
34 + {
35 + EnsureImageIsLoaded(DebianImage);
36 + EnsureImageIsDeleted(ImportedImage);
37 + SavedArchivePath = wsl::windows::common::filesystem::GetTempFilename();
38 + return true;
39 + }
40 +
41 + TEST_METHOD_CLEANUP(MethodCleanup)
42 + {
43 + DeleteFileW(SavedArchivePath.c_str());
44 + return true;
45 + }
46 +
47 + WSLC_TEST_METHOD(WSLCE2E_Image_Import_HelpCommand)
48 + {
49 + auto result = RunWslc(L"image import --help");
50 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
51 + }
52 +
53 + WSLC_TEST_METHOD(WSLCE2E_Image_Import_MissingFile)
54 + {
55 + const auto result = RunWslc(L"image import");
56 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'file'\r\n", .ExitCode = 1});
57 + }
58 +
59 + WSLC_TEST_METHOD(WSLCE2E_Image_Import_Success)
60 + {
61 + // Save image as a tarball
62 + auto saveResult = RunWslc(std::format(L"image save --output \"{}\" {}", SavedArchivePath.wstring(), DebianImage.NameAndTag()));
63 + saveResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
64 +
65 + // Import the tarball as a new image with a tag
66 + auto importResult = RunWslc(std::format(L"image import \"{}\" {}", SavedArchivePath.wstring(), ImportedImage.NameAndTag()));
67 + importResult.Verify({.Stderr = L"", .ExitCode = 0});
68 +
69 + // Verify the imported image is listed
70 + VerifyImageIsListed(ImportedImage);
71 + }
72 +
73 + WSLC_TEST_METHOD(WSLCE2E_Image_Import_WithoutTag)
74 + {
75 + // TODO: http://task.ms/62249460
76 + SKIP_TEST_UNSTABLE();
77 +
78 + // Save image as a tarball
79 + auto saveResult = RunWslc(std::format(L"image save --output \"{}\" {}", SavedArchivePath.wstring(), DebianImage.NameAndTag()));
80 + saveResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
81 +
82 + // Import without specifying an image name
83 + auto importResult = RunWslc(std::format(L"image import \"{}\"", SavedArchivePath.wstring()));
84 + importResult.Verify({.Stderr = L"", .ExitCode = 0});
85 + }
86 +
87 + WSLC_TEST_METHOD(WSLCE2E_Image_Import_FromStdin_Success)
88 + {
89 + // TODO: http://task.ms/62246732
90 + SKIP_TEST_NOT_IMPL();
91 + }
92 +
93 + WSLC_TEST_METHOD(WSLCE2E_Image_Import_InvalidPath)
94 + {
95 + const auto result =
96 + RunWslc(std::format(L"image import \"{}\" {}", L"C:\\nonexistent\\path\\image.tar", ImportedImage.NameAndTag()));
97 + result.Verify({.ExitCode = 1});
98 + }
99 +
100 +private:
101 + const TestImage DebianImage = DebianTestImage();
102 + const TestImage ImportedImage{L"wslc-test-imported", L"latest", L""};
103 +
104 + 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" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n" //
142 + << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n" //
143 + << L"\r\n";
144 + return options.str();
145 + }
146 +};
147 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EImageTests.cpp
+1
@@ -72,6 +72,7 @@ private:
72 {L"inspect", Localization::WSLCCLI_ImageInspectDesc()},
73 {L"list", Localization::WSLCCLI_ImageListDesc()},
74 {L"load", Localization::WSLCCLI_ImageLoadDesc()},
75 + {L"import", Localization::WSLCCLI_ImageImportDesc()},
76 {L"prune", Localization::WSLCCLI_ImagePruneDesc()},
77 {L"pull", Localization::WSLCCLI_ImagePullDesc()},
78 {L"push", Localization::WSLCCLI_ImagePushDesc()},