@samitouri / QOSAMI-WSL / commits / 0f8d1d04

CLI: Add container prune command (#40547)

* CLI: Add container prune command Implement the 'wslc container prune' command to remove all stopped containers. The backend IWSLCSession::PruneContainers API already exists; this adds the CLI frontend. Changes: - ContainerPruneCommand: new command class with --session arg - ContainerService::Prune(): service layer using RAII PruneResult - PruneContainers task: prints pruned container IDs and reclaimed space - PruneContainersResult model struct - 3 localization strings (desc, long desc, space reclaimed) - CLI parsing unit tests in CommandLineTestCases.h - E2E tests: help, no-stopped, stopped, running-preserved, multi-stopped - Updated container help output test to include prune subcommand Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review: use shared helpers in prune tests - Add StdoutContainsSubstring() to WSLCExecutionResult for reusable substring matching (per reviewer suggestion to move to WSLCExecutor) - Replace manual container list loops with VerifyContainerIsNotListed() and VerifyContainerIsListed() helpers - Use exact localized string match for zero-reclaimed-space assertion - Remove private VerifyStdoutContains helper (now in WSLCExecutor) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Container Prune e2 test: Fix clang formatting errors * Fix PruneContainers call to match 3-parameter interface Remove extra argument that doesn't match the IWSLCSession::PruneContainers signature (Filters, FiltersCount, Result). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address Copilot review feedback - Rename Containers -> PrunedContainers in PruneContainersResult for clarity - Add reserve() before loop to avoid repeated reallocations - Add per-test cleanup in ClassSetup to prevent flaky tests from leftovers - Assert pruned container IDs appear in prune output - Remove hardcoded English strings; use localization API for assertions - Simplify StdoutContainsSubstring to search raw buffer directly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Pooja Trivedi <trivedipooja@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Pooja Trivedi committed May 19, 2026 at 19:57 UTC 0f8d1d0498e4a29d44aa1439f58c4dff7348b96e
14 files changed +284
localization/strings/en-US/Resources.resw
+10
@@ -2421,6 +2421,16 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2421 <data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
2422 <value>View logs for a container.</value>
2423 </data>
2424 + <data name="WSLCCLI_ContainerPruneDesc" xml:space="preserve">
2425 + <value>Remove all stopped containers.</value>
2426 + </data>
2427 + <data name="WSLCCLI_ContainerPruneLongDesc" xml:space="preserve">
2428 + <value>Removes all stopped containers.</value>
2429 + </data>
2430 + <data name="WSLCCLI_ContainerPruneSpaceReclaimed" xml:space="preserve">
2431 + <value>Total reclaimed space: {:.2f} MB</value>
2432 + <comment>{FixedPlaceholder="{:.2f}"}Command line arguments, file names and string inserts should not be translated</comment>
2433 + </data>
2434 <data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
2435 <value>Remove containers.</value>
2436 </data>
src/windows/wslc/commands/ContainerCommand.cpp
+1
@@ -29,6 +29,7 @@ std::vector<std::unique_ptr<Command>> ContainerCommand::GetCommands() const
29 commands.push_back(std::make_unique<ContainerKillCommand>(FullName()));
30 commands.push_back(std::make_unique<ContainerLogsCommand>(FullName()));
31 commands.push_back(std::make_unique<ContainerListCommand>(FullName()));
32 + commands.push_back(std::make_unique<ContainerPruneCommand>(FullName()));
33 commands.push_back(std::make_unique<ContainerRemoveCommand>(FullName()));
34 commands.push_back(std::make_unique<ContainerRunCommand>(FullName()));
35 commands.push_back(std::make_unique<ContainerStartCommand>(FullName()));
src/windows/wslc/commands/ContainerCommand.h
+15
@@ -210,6 +210,21 @@ struct ContainerStopCommand final : public Command
210 std::wstring ShortDescription() const override;
211 std::wstring LongDescription() const override;
212
213 +protected:
214 + void ExecuteInternal(CLIExecutionContext& context) const override;
215 +};
216 +
217 +// Prune Command
218 +struct ContainerPruneCommand final : public Command
219 +{
220 + constexpr static std::wstring_view CommandName = L"prune";
221 + ContainerPruneCommand(const std::wstring& parent) : Command(CommandName, parent)
222 + {
223 + }
224 + std::vector<Argument> GetArguments() const override;
225 + std::wstring ShortDescription() const override;
226 + std::wstring LongDescription() const override;
227 +
228 protected:
229 void ExecuteInternal(CLIExecutionContext& context) const override;
230 };
src/windows/wslc/commands/ContainerPruneCommand.cpp new
+50
@@ -0,0 +1,50 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerPruneCommand.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 Prune Command
27 +std::vector<Argument> ContainerPruneCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::Session),
31 + };
32 +}
33 +
34 +std::wstring ContainerPruneCommand::ShortDescription() const
35 +{
36 + return Localization::WSLCCLI_ContainerPruneDesc();
37 +}
38 +
39 +std::wstring ContainerPruneCommand::LongDescription() const
40 +{
41 + return Localization::WSLCCLI_ContainerPruneLongDesc();
42 +}
43 +
44 +void ContainerPruneCommand::ExecuteInternal(CLIExecutionContext& context) const
45 +{
46 + context //
47 + << CreateSession //
48 + << PruneContainers;
49 +}
50 +} // namespace wsl::windows::wslc
src/windows/wslc/services/ContainerModel.h
+6
@@ -68,6 +68,12 @@ struct StopContainerOptions
68 LONG Timeout = DefaultTimeout;
69 };
70
71 +struct PruneContainersResult
72 +{
73 + std::vector<std::string> PrunedContainers;
74 + ULONGLONG SpaceReclaimed{};
75 +};
76 +
77 struct KillContainerOptions
78 {
79 int Signal = WSLCSignalSIGKILL;
src/windows/wslc/services/ContainerService.cpp
+16
@@ -543,4 +543,20 @@ wsl::windows::common::docker_schema::ContainerStats ContainerService::Stats(Sess
543 THROW_IF_FAILED(container->Stats(&output));
544 return wsl::shared::FromJson<wsl::windows::common::docker_schema::ContainerStats>(output.get());
545 }
546 +
547 +PruneContainersResult ContainerService::Prune(Session& session)
548 +{
549 + PruneResult result;
550 + THROW_IF_FAILED(session.Get()->PruneContainers(nullptr, 0, &result.result));
551 +
552 + PruneContainersResult pruneResult;
553 + pruneResult.SpaceReclaimed = result.result.SpaceReclaimed;
554 + pruneResult.PrunedContainers.reserve(result.result.ContainersCount);
555 + for (ULONG i = 0; i < result.result.ContainersCount; i++)
556 + {
557 + pruneResult.PrunedContainers.push_back(result.result.Containers[i]);
558 + }
559 +
560 + return pruneResult;
561 +}
562 } // namespace wsl::windows::wslc::services
src/windows/wslc/services/ContainerService.h
+1
@@ -37,5 +37,6 @@ struct ContainerService
37 static wsl::windows::common::wslc_schema::InspectContainer Inspect(models::Session& session, const std::string& id);
38 static void Logs(models::Session& session, const std::string& id, bool follow, ULONGLONG tail = 0);
39 static wsl::windows::common::docker_schema::ContainerStats Stats(models::Session& session, const std::string& id);
40 + static models::PruneContainersResult Prune(models::Session& session);
41 };
42 } // namespace wsl::windows::wslc::services
src/windows/wslc/tasks/ContainerTasks.cpp
+17
@@ -17,6 +17,7 @@ Abstract:
17 #include "ContainerModel.h"
18 #include "ContainerService.h"
19 #include "ContainerTasks.h"
20 +#include "ImageModel.h"
21 #include "SessionModel.h"
22 #include "SessionService.h"
23 #include "TableOutput.h"
@@ -694,4 +695,20 @@ void ViewContainerLogs(CLIExecutionContext& context)
695
696 ContainerService::Logs(session, WideToMultiByte(containerId), follow, tail);
697 }
698 +
699 +void PruneContainers(CLIExecutionContext& context)
700 +{
701 + WI_ASSERT(context.Data.Contains(Data::Session));
702 + auto& session = context.Data.Get<Data::Session>();
703 +
704 + auto result = ContainerService::Prune(session);
705 +
706 + for (const auto& containerId : result.PrunedContainers)
707 + {
708 + PrintMessage(MultiByteToWide(containerId));
709 + }
710 +
711 + PrintMessage(L"");
712 + PrintMessage(Localization::WSLCCLI_ContainerPruneSpaceReclaimed(static_cast<double>(result.SpaceReclaimed) / WSLC_IMAGE_1MB));
713 +}
714 } // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/ContainerTasks.h
+1
@@ -36,6 +36,7 @@ void GetContainers(CLIExecutionContext& context);
36 void InspectContainers(CLIExecutionContext& context);
37 void KillContainers(CLIExecutionContext& context);
38 void ListContainers(CLIExecutionContext& context);
39 +void PruneContainers(CLIExecutionContext& context);
40 void RemoveContainers(CLIExecutionContext& context);
41 void RunContainer(CLIExecutionContext& context);
42 void SetContainerOptionsFromArgs(CLIExecutionContext& context);
test/windows/wslc/CommandLineTestCases.h
+2
@@ -56,6 +56,8 @@ COMMAND_LINE_TEST_CASE(L"container list -qa", L"list", true)
56 COMMAND_LINE_TEST_CASE(L"container list --format json", L"list", true)
57 COMMAND_LINE_TEST_CASE(L"container list --format table", L"list", true)
58 COMMAND_LINE_TEST_CASE(L"container list --format badformat", L"list", false)
59 +COMMAND_LINE_TEST_CASE(L"container prune", L"prune", true)
60 +COMMAND_LINE_TEST_CASE(L"container prune --session foo", L"prune", true)
61 COMMAND_LINE_TEST_CASE(L"run ubuntu", L"run", true)
62 COMMAND_LINE_TEST_CASE(L"run --rm -it --entrypoint bash archlinux:latest -c \"echo 123\"", L"run", true)
63 COMMAND_LINE_TEST_CASE(L"run --rm --entrypoint /bin/bash debian:latest -c ls", L"run", true)
test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp new
+157
@@ -0,0 +1,157 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EContainerPruneTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC container prune command.
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 WSLCE2EContainerPruneTests
23 +{
24 + WSLC_TEST_CLASS(WSLCE2EContainerPruneTests)
25 +
26 + TEST_CLASS_SETUP(ClassSetup)
27 + {
28 + EnsureImageIsLoaded(DebianImage);
29 +
30 + // Clean up any leftover containers from previous failed runs
31 + EnsureContainerDoesNotExist(L"prune-test-container");
32 + EnsureContainerDoesNotExist(L"prune-running-test");
33 + EnsureContainerDoesNotExist(L"prune-multi-1");
34 + EnsureContainerDoesNotExist(L"prune-multi-2");
35 + return true;
36 + }
37 +
38 + TEST_CLASS_CLEANUP(ClassCleanup)
39 + {
40 + // Clean up any leftover containers
41 + RunWslc(L"container prune");
42 + return true;
43 + }
44 +
45 + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_HelpCommand)
46 + {
47 + const auto result = RunWslc(L"container prune --help");
48 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
49 + }
50 +
51 + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_NoStoppedContainers)
52 + {
53 + // Prune when no stopped containers exist should succeed with zero reclaimed space
54 + const auto result = RunWslc(L"container prune");
55 + result.Verify({.Stderr = L"", .ExitCode = 0});
56 +
57 + VERIFY_IS_TRUE(result.StdoutContainsSubstring(Localization::WSLCCLI_ContainerPruneSpaceReclaimed(0.0)));
58 + }
59 +
60 + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_StoppedContainer)
61 + {
62 + // Create and stop a container, then prune it
63 + auto createResult = RunWslc(std::format(L"container create --name prune-test-container {}", DebianImage.NameAndTag()));
64 + createResult.Verify({.Stderr = L"", .ExitCode = 0});
65 + auto containerId = createResult.GetStdoutOneLine();
66 +
67 + auto cleanup = wil::scope_exit([&]() { RunWslc(L"container prune"); });
68 +
69 + // The created container is in stopped state, so prune should remove it
70 + const auto result = RunWslc(L"container prune");
71 + result.Verify({.Stderr = L"", .ExitCode = 0});
72 +
73 + // Verify pruned container ID is in output
74 + VERIFY_IS_TRUE(result.StdoutContainsSubstring(containerId));
75 +
76 + // Verify the container is actually removed
77 + VerifyContainerIsNotListed(L"prune-test-container");
78 + }
79 +
80 + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_RunningContainerNotPruned)
81 + {
82 + // Start a running container, verify prune does NOT remove it
83 + auto runResult = RunWslc(std::format(L"container run --detach --name prune-running-test {} sleep 300", DebianImage.NameAndTag()));
84 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
85 +
86 + auto cleanup = wil::scope_exit([&]() {
87 + RunWslc(L"container kill prune-running-test");
88 + RunWslc(L"container remove --force prune-running-test");
89 + });
90 +
91 + // Prune should not remove a running container
92 + const auto pruneResult = RunWslc(L"container prune");
93 + pruneResult.Verify({.Stderr = L"", .ExitCode = 0});
94 +
95 + // Verify the running container is still present
96 + VerifyContainerIsListed(L"prune-running-test", L"running");
97 + }
98 +
99 + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_MultipleStopped)
100 + {
101 + // Create multiple stopped containers and verify all are pruned
102 + auto create1 = RunWslc(std::format(L"container create --name prune-multi-1 {}", DebianImage.NameAndTag()));
103 + create1.Verify({.Stderr = L"", .ExitCode = 0});
104 + auto containerId1 = create1.GetStdoutOneLine();
105 +
106 + auto create2 = RunWslc(std::format(L"container create --name prune-multi-2 {}", DebianImage.NameAndTag()));
107 + create2.Verify({.Stderr = L"", .ExitCode = 0});
108 + auto containerId2 = create2.GetStdoutOneLine();
109 +
110 + auto cleanup = wil::scope_exit([&]() { RunWslc(L"container prune"); });
111 +
112 + const auto result = RunWslc(L"container prune");
113 + result.Verify({.Stderr = L"", .ExitCode = 0});
114 +
115 + // Verify pruned container IDs are in output
116 + VERIFY_IS_TRUE(result.StdoutContainsSubstring(containerId1));
117 + VERIFY_IS_TRUE(result.StdoutContainsSubstring(containerId2));
118 +
119 + // Verify both containers are removed
120 + VerifyContainerIsNotListed(L"prune-multi-1");
121 + VerifyContainerIsNotListed(L"prune-multi-2");
122 + }
123 +
124 +private:
125 + const TestImage& DebianImage = DebianTestImage();
126 +
127 + std::wstring GetHelpMessage() const
128 + {
129 + std::wstringstream output;
130 + output << GetWslcHeader() //
131 + << GetDescription() //
132 + << GetUsage() //
133 + << GetAvailableOptions();
134 + return output.str();
135 + }
136 +
137 + std::wstring GetDescription() const
138 + {
139 + return Localization::WSLCCLI_ContainerPruneLongDesc() + L"\r\n\r\n";
140 + }
141 +
142 + std::wstring GetUsage() const
143 + {
144 + return L"Usage: wslc container prune [<options>]\r\n\r\n";
145 + }
146 +
147 + std::wstring GetAvailableOptions() const
148 + {
149 + std::wstringstream options;
150 + options << L"The following options are available:\r\n"
151 + << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n"
152 + << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
153 + << L"\r\n";
154 + return options.str();
155 + }
156 +};
157 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EContainerTests.cpp
+1
@@ -76,6 +76,7 @@ private:
76 {L"kill", Localization::WSLCCLI_ContainerKillDesc()},
77 {L"logs", Localization::WSLCCLI_ContainerLogsDesc()},
78 {L"list", Localization::WSLCCLI_ContainerListDesc()},
79 + {L"prune", Localization::WSLCCLI_ContainerPruneDesc()},
80 {L"remove", Localization::WSLCCLI_ContainerRemoveDesc()},
81 {L"run", Localization::WSLCCLI_ContainerRunDesc()},
82 {L"start", Localization::WSLCCLI_ContainerStartDesc()},
test/windows/wslc/e2e/WSLCExecutor.cpp
+6
@@ -141,6 +141,12 @@ bool WSLCExecutionResult::StdoutContainsLine(const std::wstring& expectedLine) c
141 return false;
142 }
143
144 +bool WSLCExecutionResult::StdoutContainsSubstring(const std::wstring& substring) const
145 +{
146 + VERIFY_IS_TRUE(Stdout.has_value());
147 + return Stdout.value().find(substring) != std::wstring::npos;
148 +}
149 +
150 WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType elevationType)
151 {
152 auto cmd = L"\"" + GetWslcPath() + L"\" " + commandLine;
test/windows/wslc/e2e/WSLCExecutor.h
+1
@@ -44,6 +44,7 @@ struct WSLCExecutionResult
44 std::vector<std::wstring> GetStdoutLines() const;
45 std::wstring GetStdoutOneLine() const;
46 bool StdoutContainsLine(const std::wstring& expectedLine) const;
47 + bool StdoutContainsSubstring(const std::wstring& substring) const;
48 };
49
50 // Interactive session for testing wslc commands that require stdin/stdout interaction.