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: <host path | named volume>:<container path>[: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()},