CLI: Add container export command (#40807)

David Bennett committed Jun 15, 2026 at 12:58 UTC 88c620eea5b6c62eb557ed2c00b0a9b78133df67
13 files changed +290
localization/strings/en-US/Resources.resw
+16
@@ -2391,6 +2391,9 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2391 <data name="MessageWslcSaveInProgress" xml:space="preserve">
2392 <value>Save in progress.</value>
2393 </data>
2394 + <data name="MessageWslcExportInProgress" xml:space="preserve">
2395 + <value>Export in progress.</value>
2396 + </data>
2397 <data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
2398 <value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
2399 <comment>{Locked="WSLC"}Product names should not be translated</comment>
@@ -2424,6 +2427,16 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2427 <data name="WSLCCLI_ContainerExecLongDesc" xml:space="preserve">
2428 <value>Executes a command in a running container.</value>
2429 </data>
2430 + <data name="WSLCCLI_ContainerExportDesc" xml:space="preserve">
2431 + <value>Export a container's filesystem as a tar archive.</value>
2432 + </data>
2433 + <data name="WSLCCLI_ContainerExportLongDesc" xml:space="preserve">
2434 + <value>Exports a container's filesystem as a tar archive.</value>
2435 + </data>
2436 + <data name="WSLCCLI_ContainerExportOutputArgDescription" xml:space="preserve">
2437 + <value>Write to a file, instead of STDOUT</value>
2438 + <comment>{Locked="STDOUT"}Command line arguments, file names and string inserts should not be translated</comment>
2439 + </data>
2440 <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2441 <value>Inspect a container.</value>
2442 </data>
@@ -3134,6 +3147,9 @@ On first run, creates the file with all settings commented out at their defaults
3147 <data name="WSLCCLI_ImageSaveStdoutIsTerminalError" xml:space="preserve">
3148 <value>Cannot write image to terminal. Use the -o flag or redirect stdout.</value>
3149 </data>
3150 + <data name="WSLCCLI_ContainerExportStdoutIsTerminalError" xml:space="preserve">
3151 + <value>Cannot export container to terminal. Use the -o flag or redirect stdout.</value>
3152 + </data>
3153 <data name="WSLCCLI_VolumeFormatUsage" xml:space="preserve">
3154 <value>Expected format: &lt;host path | named volume&gt;:&lt;container path&gt;[:mode]</value>
3155 <comment>Usage string for volume mount specification.</comment>
src/windows/wslc/commands/ContainerCommand.cpp
+1
@@ -25,6 +25,7 @@ std::vector<std::unique_ptr<Command>> ContainerCommand::GetCommands() const
25 commands.push_back(std::make_unique<ContainerAttachCommand>(FullName()));
26 commands.push_back(std::make_unique<ContainerCreateCommand>(FullName()));
27 commands.push_back(std::make_unique<ContainerExecCommand>(FullName()));
28 + commands.push_back(std::make_unique<ContainerExportCommand>(FullName()));
29 commands.push_back(std::make_unique<ContainerInspectCommand>(FullName()));
30 commands.push_back(std::make_unique<ContainerKillCommand>(FullName()));
31 commands.push_back(std::make_unique<ContainerLogsCommand>(FullName()));
src/windows/wslc/commands/ContainerCommand.h
+15
@@ -77,6 +77,21 @@ protected:
77 void ExecuteInternal(CLIExecutionContext& context) const override;
78 };
79
80 +// Export Command
81 +struct ContainerExportCommand final : public Command
82 +{
83 + constexpr static std::wstring_view CommandName = L"export";
84 + ContainerExportCommand(const std::wstring& parent) : Command(CommandName, parent)
85 + {
86 + }
87 + std::vector<Argument> GetArguments() const override;
88 + std::wstring ShortDescription() const override;
89 + std::wstring LongDescription() const override;
90 +
91 +protected:
92 + void ExecuteInternal(CLIExecutionContext& context) const override;
93 +};
94 +
95 // Inspect Command
96 struct ContainerInspectCommand final : public Command
97 {
src/windows/wslc/commands/ContainerExportCommand.cpp new
+52
@@ -0,0 +1,52 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerExportCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of command execution logic.
12 +
13 +--*/
14 +
15 +#include "ContainerCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ContainerTasks.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 +// Container Export Command
27 +std::vector<Argument> ContainerExportCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::ContainerId, true),
31 + Argument::Create(ArgType::Output, std::nullopt, std::nullopt, Localization::WSLCCLI_ContainerExportOutputArgDescription()),
32 + Argument::Create(ArgType::Session),
33 + };
34 +}
35 +
36 +std::wstring ContainerExportCommand::ShortDescription() const
37 +{
38 + return Localization::WSLCCLI_ContainerExportDesc();
39 +}
40 +
41 +std::wstring ContainerExportCommand::LongDescription() const
42 +{
43 + return Localization::WSLCCLI_ContainerExportLongDesc();
44 +}
45 +
46 +void ContainerExportCommand::ExecuteInternal(CLIExecutionContext& context) const
47 +{
48 + context //
49 + << CreateSession //
50 + << ExportContainer;
51 +}
52 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/RootCommand.cpp
+1
@@ -42,6 +42,7 @@ std::vector<std::unique_ptr<Command>> RootCommand::GetCommands() const
42 commands.push_back(std::make_unique<ImageBuildCommand>(FullName()));
43 commands.push_back(std::make_unique<ContainerCreateCommand>(FullName()));
44 commands.push_back(std::make_unique<ContainerExecCommand>(FullName()));
45 + commands.push_back(std::make_unique<ContainerExportCommand>(FullName()));
46 commands.push_back(std::make_unique<ImageListCommand>(FullName(), true));
47 commands.push_back(std::make_unique<ImageImportCommand>(FullName()));
48 commands.push_back(std::make_unique<InspectCommand>(FullName()));
src/windows/wslc/services/ContainerService.cpp
+21
@@ -19,6 +19,7 @@ Abstract:
19 #include "ImageProgressCallback.h"
20 #include "WarningCallback.h"
21 #include <wslutil.h>
22 +#include <HandleConsoleProgressBar.h>
23 #include <WSLCProcessLauncher.h>
24 #include <ConsoleState.h>
25 #include <CommandLine.h>
@@ -582,6 +583,26 @@ InspectContainer ContainerService::Inspect(Session& session, const std::string&
583 return wsl::shared::FromJson<InspectContainer>(output.get());
584 }
585
586 +void ContainerService::Export(Session& session, const std::string& id, const std::wstring& outputPath)
587 +{
588 + wil::unique_hfile outputFile{
589 + CreateFileW(outputPath.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
590 + THROW_LAST_ERROR_IF(!outputFile);
591 +
592 + Export(session, id, outputFile.get());
593 +}
594 +
595 +void ContainerService::Export(Session& session, const std::string& id, HANDLE outputHandle)
596 +{
597 + wil::com_ptr<IWSLCContainer> container;
598 + THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
599 +
600 + wsl::windows::common::HandleConsoleProgressBar progressBar(
601 + outputHandle, Localization::MessageWslcExportInProgress(), wsl::windows::common::HandleConsoleProgressBar::Format::FileSize);
602 +
603 + THROW_IF_FAILED(container->Export(ToCOMInputHandle(outputHandle)));
604 +}
605 +
606 void ContainerService::Logs(Session& session, const std::string& id, bool follow, bool timestamps, ULONGLONG since, ULONGLONG until, ULONGLONG tail)
607 {
608 wil::com_ptr<IWSLCContainer> container;
src/windows/wslc/services/ContainerService.h
+2
@@ -34,6 +34,8 @@ struct ContainerService
34 models::Session& session, bool all = false, int limit = -1, const std::vector<std::pair<std::string, std::string>>& filters = {});
35
36 static int Exec(models::Session& session, const std::string& id, models::ContainerOptions options);
37 + static void Export(models::Session& session, const std::string& id, const std::wstring& outputPath);
38 + static void Export(models::Session& session, const std::string& id, HANDLE outputHandle);
39 static wsl::windows::common::wslc_schema::InspectContainer Inspect(models::Session& session, const std::string& id);
40 static void Logs(models::Session& session, const std::string& id, bool follow, bool timestamps, ULONGLONG since, ULONGLONG until, ULONGLONG tail = 0);
41 static wsl::windows::common::docker_schema::ContainerStats Stats(models::Session& session, const std::string& id);
src/windows/wslc/tasks/ContainerTasks.cpp
+24
@@ -252,6 +252,30 @@ void KillContainers(CLIExecutionContext& context)
252 }
253 }
254
255 +void ExportContainer(CLIExecutionContext& context)
256 +{
257 + WI_ASSERT(context.Data.Contains(Data::Session));
258 + WI_ASSERT(context.Args.Contains(ArgType::ContainerId));
259 + auto& session = context.Data.Get<Data::Session>();
260 + auto containerId = WideToMultiByte(context.Args.Get<ArgType::ContainerId>());
261 +
262 + if (context.Args.Contains(ArgType::Output))
263 + {
264 + auto& output = context.Args.Get<ArgType::Output>();
265 + ContainerService::Export(session, containerId, output);
266 + }
267 + else
268 + {
269 + auto stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE);
270 + if (wsl::windows::common::wslutil::IsConsoleHandle(stdoutHandle))
271 + {
272 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_ContainerExportStdoutIsTerminalError());
273 + }
274 +
275 + ContainerService::Export(session, containerId, stdoutHandle);
276 + }
277 +}
278 +
279 void ListContainers(CLIExecutionContext& context)
280 {
281 WI_ASSERT(context.Data.Contains(Data::Containers));
src/windows/wslc/tasks/ContainerTasks.h
+1
@@ -32,6 +32,7 @@ private:
32
33 void CreateContainer(CLIExecutionContext& context);
34 void ExecContainer(CLIExecutionContext& context);
35 +void ExportContainer(CLIExecutionContext& context);
36 void GetContainers(CLIExecutionContext& context);
37 void InspectContainers(CLIExecutionContext& context);
38 void KillContainers(CLIExecutionContext& context);
test/windows/wslc/CommandLineTestCases.h
+7
@@ -156,6 +156,13 @@ COMMAND_LINE_TEST_CASE(L"container stats cont1", L"stats", true)
156 COMMAND_LINE_TEST_CASE(L"container stats cont1 cont2", L"stats", true)
157 COMMAND_LINE_TEST_CASE(L"container stats --no-trunc cont1", L"stats", true)
158 COMMAND_LINE_TEST_CASE(L"container stats --all", L"stats", true)
159 +// Export command tests
160 +COMMAND_LINE_TEST_CASE(L"export cont1", L"export", true)
161 +COMMAND_LINE_TEST_CASE(L"container export cont1", L"export", true)
162 +COMMAND_LINE_TEST_CASE(L"container export --output foo cont1", L"export", true)
163 +COMMAND_LINE_TEST_CASE(L"container export -o foo cont1", L"export", true)
164 +COMMAND_LINE_TEST_CASE(L"container export cont1 --output foo", L"export", true)
165 +COMMAND_LINE_TEST_CASE(L"container export cont1 -o foo", L"export", true)
166
167 // Logs command
168 COMMAND_LINE_TEST_CASE(L"logs cont1", L"logs", true)
test/windows/wslc/e2e/WSLCE2EContainerExportTests.cpp new
+148
@@ -0,0 +1,148 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EContainerExportTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC container export.
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 WSLCE2EContainerExportTests
23 +{
24 + WSLC_TEST_CLASS(WSLCE2EContainerExportTests)
25 +
26 + TEST_CLASS_SETUP(ClassSetup)
27 + {
28 + EnsureImageIsLoaded(DebianImage);
29 + return true;
30 + }
31 +
32 + TEST_CLASS_CLEANUP(ClassCleanup)
33 + {
34 + EnsureContainerDoesNotExist(WslcContainerName);
35 + EnsureImageIsDeleted(DebianImage);
36 + return true;
37 + }
38 +
39 + TEST_METHOD_SETUP(MethodSetup)
40 + {
41 + EnsureContainerDoesNotExist(WslcContainerName);
42 + ExportPath = wsl::windows::common::filesystem::GetTempFilename();
43 + DeleteFileW(ExportPath.c_str());
44 + return true;
45 + }
46 +
47 + TEST_METHOD_CLEANUP(MethodCleanup)
48 + {
49 + EnsureContainerDoesNotExist(WslcContainerName);
50 + DeleteFileW(ExportPath.c_str());
51 + return true;
52 + }
53 +
54 + WSLC_TEST_METHOD(WSLCE2E_Container_Export_HelpCommand)
55 + {
56 + auto result = RunWslc(L"container export --help");
57 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
58 + }
59 +
60 + WSLC_TEST_METHOD(WSLCE2E_Container_Export_MissingContainerId)
61 + {
62 + const auto result = RunWslc(std::format(L"container export --output \"{}\"", ExportPath.wstring()));
63 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1});
64 + }
65 +
66 + WSLC_TEST_METHOD(WSLCE2E_Container_Export_ContainerNotFound)
67 + {
68 + const auto result = RunWslc(std::format(L"container export --output \"{}\" {}", ExportPath.wstring(), InvalidContainerName));
69 + VERIFY_IS_TRUE(result.ExitCode.has_value());
70 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
71 + VERIFY_IS_TRUE(result.Stderr.has_value());
72 + VERIFY_ARE_NOT_EQUAL(0u, result.Stderr.value().size());
73 + }
74 +
75 + WSLC_TEST_METHOD(WSLCE2E_Container_Export_ToFile_Success)
76 + {
77 + // Create a stopped container so it has a filesystem to export.
78 + const auto createResult = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
79 + createResult.Verify({.Stderr = L"", .ExitCode = 0});
80 +
81 + const auto result = RunWslc(std::format(L"container export --output \"{}\" {}", ExportPath.wstring(), WslcContainerName));
82 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
83 +
84 + VERIFY_IS_TRUE(std::filesystem::exists(ExportPath));
85 + VERIFY_ARE_NOT_EQUAL(0u, std::filesystem::file_size(ExportPath));
86 + }
87 +
88 + WSLC_TEST_METHOD(WSLCE2E_Container_Export_ToStdout_Success)
89 + {
90 + const auto createResult = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
91 + createResult.Verify({.Stderr = L"", .ExitCode = 0});
92 +
93 + const auto result = RunWslcAndRedirectToFile(std::format(L"container export {}", WslcContainerName), ExportPath);
94 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
95 +
96 + VERIFY_IS_TRUE(std::filesystem::exists(ExportPath));
97 + VERIFY_ARE_NOT_EQUAL(0u, std::filesystem::file_size(ExportPath));
98 + }
99 +
100 +private:
101 + const std::wstring WslcContainerName = L"wslc-test-container-export";
102 + const std::wstring InvalidContainerName = L"wslc-nonexistent-container-for-export";
103 + const TestImage& DebianImage = DebianTestImage();
104 +
105 + std::filesystem::path ExportPath{};
106 +
107 + std::wstring GetHelpMessage() const
108 + {
109 + std::wstringstream output;
110 + output << GetWslcHeader() //
111 + << GetDescription() //
112 + << GetUsage() //
113 + << GetAvailableCommands() //
114 + << GetAvailableOptions();
115 + return output.str();
116 + }
117 +
118 + std::wstring GetDescription() const
119 + {
120 + return Localization::WSLCCLI_ContainerExportLongDesc() + L"\r\n\r\n";
121 + }
122 +
123 + std::wstring GetUsage() const
124 + {
125 + return L"Usage: wslc container export [<options>] <container-id>\r\n\r\n";
126 + }
127 +
128 + std::wstring GetAvailableCommands() const
129 + {
130 + std::wstringstream commands;
131 + commands << L"The following arguments are available:\r\n" //
132 + << L" container-id Container ID\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" -o,--output Write to a file, instead of STDOUT\r\n" //
142 + << L" --session Specify the session to use\r\n" //
143 + << L" -?,--help Shows help about the selected command\r\n" //
144 + << L"\r\n";
145 + return options.str();
146 + }
147 +};
148 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EContainerTests.cpp
+1
@@ -72,6 +72,7 @@ private:
72 {L"attach", Localization::WSLCCLI_ContainerAttachDesc()},
73 {L"create", Localization::WSLCCLI_ContainerCreateDesc()},
74 {L"exec", Localization::WSLCCLI_ContainerExecDesc()},
75 + {L"export", Localization::WSLCCLI_ContainerExportDesc()},
76 {L"inspect", Localization::WSLCCLI_ContainerInspectDesc()},
77 {L"kill", Localization::WSLCCLI_ContainerKillDesc()},
78 {L"logs", Localization::WSLCCLI_ContainerLogsDesc()},
test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp
+1
@@ -553,6 +553,7 @@ private:
553 {L"build", Localization::WSLCCLI_ImageBuildDesc()},
554 {L"create", Localization::WSLCCLI_ContainerCreateDesc()},
555 {L"exec", Localization::WSLCCLI_ContainerExecDesc()},
556 + {L"export", Localization::WSLCCLI_ContainerExportDesc()},
557 {L"images", Localization::WSLCCLI_ImageListDesc()},
558 {L"import", Localization::WSLCCLI_ImageImportDesc()},
559 {L"inspect", Localization::WSLCCLI_InspectDesc()},