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