diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index e81766f37..99293088d 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2421,6 +2421,16 @@ For privacy information about this product please visit https://aka.ms/privacy.< View logs for a container. + + Remove all stopped containers. + + + Removes all stopped containers. + + + Total reclaimed space: {:.2f} MB + {FixedPlaceholder="{:.2f}"}Command line arguments, file names and string inserts should not be translated + Remove containers. diff --git a/src/windows/wslc/commands/ContainerCommand.cpp b/src/windows/wslc/commands/ContainerCommand.cpp index ff919ebd5..20ca7a58f 100644 --- a/src/windows/wslc/commands/ContainerCommand.cpp +++ b/src/windows/wslc/commands/ContainerCommand.cpp @@ -29,6 +29,7 @@ std::vector> ContainerCommand::GetCommands() const commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); diff --git a/src/windows/wslc/commands/ContainerCommand.h b/src/windows/wslc/commands/ContainerCommand.h index 312816488..51865a44b 100644 --- a/src/windows/wslc/commands/ContainerCommand.h +++ b/src/windows/wslc/commands/ContainerCommand.h @@ -210,6 +210,21 @@ struct ContainerStopCommand final : public Command std::wstring ShortDescription() const override; std::wstring LongDescription() const override; +protected: + void ExecuteInternal(CLIExecutionContext& context) const override; +}; + +// Prune Command +struct ContainerPruneCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"prune"; + ContainerPruneCommand(const std::wstring& parent) : Command(CommandName, parent) + { + } + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + protected: void ExecuteInternal(CLIExecutionContext& context) const override; }; diff --git a/src/windows/wslc/commands/ContainerPruneCommand.cpp b/src/windows/wslc/commands/ContainerPruneCommand.cpp new file mode 100644 index 000000000..e9a550b5a --- /dev/null +++ b/src/windows/wslc/commands/ContainerPruneCommand.cpp @@ -0,0 +1,50 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + ContainerPruneCommand.cpp + +Abstract: + + Implementation of command execution logic. + +--*/ + +#include "ContainerCommand.h" +#include "CLIExecutionContext.h" +#include "ContainerTasks.h" +#include "SessionTasks.h" +#include "Task.h" + +using namespace wsl::windows::wslc::execution; +using namespace wsl::windows::wslc::task; +using namespace wsl::shared; + +namespace wsl::windows::wslc { +// Container Prune Command +std::vector ContainerPruneCommand::GetArguments() const +{ + return { + Argument::Create(ArgType::Session), + }; +} + +std::wstring ContainerPruneCommand::ShortDescription() const +{ + return Localization::WSLCCLI_ContainerPruneDesc(); +} + +std::wstring ContainerPruneCommand::LongDescription() const +{ + return Localization::WSLCCLI_ContainerPruneLongDesc(); +} + +void ContainerPruneCommand::ExecuteInternal(CLIExecutionContext& context) const +{ + context // + << CreateSession // + << PruneContainers; +} +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/services/ContainerModel.h b/src/windows/wslc/services/ContainerModel.h index 47e052788..94a283032 100644 --- a/src/windows/wslc/services/ContainerModel.h +++ b/src/windows/wslc/services/ContainerModel.h @@ -68,6 +68,12 @@ struct StopContainerOptions LONG Timeout = DefaultTimeout; }; +struct PruneContainersResult +{ + std::vector PrunedContainers; + ULONGLONG SpaceReclaimed{}; +}; + struct KillContainerOptions { int Signal = WSLCSignalSIGKILL; diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index 961fa467e..81b985311 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -543,4 +543,20 @@ wsl::windows::common::docker_schema::ContainerStats ContainerService::Stats(Sess THROW_IF_FAILED(container->Stats(&output)); return wsl::shared::FromJson(output.get()); } + +PruneContainersResult ContainerService::Prune(Session& session) +{ + PruneResult result; + THROW_IF_FAILED(session.Get()->PruneContainers(nullptr, 0, &result.result)); + + PruneContainersResult pruneResult; + pruneResult.SpaceReclaimed = result.result.SpaceReclaimed; + pruneResult.PrunedContainers.reserve(result.result.ContainersCount); + for (ULONG i = 0; i < result.result.ContainersCount; i++) + { + pruneResult.PrunedContainers.push_back(result.result.Containers[i]); + } + + return pruneResult; +} } // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/ContainerService.h b/src/windows/wslc/services/ContainerService.h index 87f002a7e..23b0c11bd 100644 --- a/src/windows/wslc/services/ContainerService.h +++ b/src/windows/wslc/services/ContainerService.h @@ -37,5 +37,6 @@ struct ContainerService static wsl::windows::common::wslc_schema::InspectContainer Inspect(models::Session& session, const std::string& id); static void Logs(models::Session& session, const std::string& id, bool follow, ULONGLONG tail = 0); static wsl::windows::common::docker_schema::ContainerStats Stats(models::Session& session, const std::string& id); + static models::PruneContainersResult Prune(models::Session& session); }; } // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp index 47d8b44c8..744e74964 100644 --- a/src/windows/wslc/tasks/ContainerTasks.cpp +++ b/src/windows/wslc/tasks/ContainerTasks.cpp @@ -17,6 +17,7 @@ Module Name: #include "ContainerModel.h" #include "ContainerService.h" #include "ContainerTasks.h" +#include "ImageModel.h" #include "SessionModel.h" #include "SessionService.h" #include "TableOutput.h" @@ -694,4 +695,20 @@ void ViewContainerLogs(CLIExecutionContext& context) ContainerService::Logs(session, WideToMultiByte(containerId), follow, tail); } + +void PruneContainers(CLIExecutionContext& context) +{ + WI_ASSERT(context.Data.Contains(Data::Session)); + auto& session = context.Data.Get(); + + auto result = ContainerService::Prune(session); + + for (const auto& containerId : result.PrunedContainers) + { + PrintMessage(MultiByteToWide(containerId)); + } + + PrintMessage(L""); + PrintMessage(Localization::WSLCCLI_ContainerPruneSpaceReclaimed(static_cast(result.SpaceReclaimed) / WSLC_IMAGE_1MB)); +} } // namespace wsl::windows::wslc::task diff --git a/src/windows/wslc/tasks/ContainerTasks.h b/src/windows/wslc/tasks/ContainerTasks.h index 2af19d4ae..a8fadfeb0 100644 --- a/src/windows/wslc/tasks/ContainerTasks.h +++ b/src/windows/wslc/tasks/ContainerTasks.h @@ -36,6 +36,7 @@ void GetContainers(CLIExecutionContext& context); void InspectContainers(CLIExecutionContext& context); void KillContainers(CLIExecutionContext& context); void ListContainers(CLIExecutionContext& context); +void PruneContainers(CLIExecutionContext& context); void RemoveContainers(CLIExecutionContext& context); void RunContainer(CLIExecutionContext& context); void SetContainerOptionsFromArgs(CLIExecutionContext& context); diff --git a/test/windows/wslc/CommandLineTestCases.h b/test/windows/wslc/CommandLineTestCases.h index 70a0e097c..955effad7 100644 --- a/test/windows/wslc/CommandLineTestCases.h +++ b/test/windows/wslc/CommandLineTestCases.h @@ -56,6 +56,8 @@ COMMAND_LINE_TEST_CASE(L"container list -qa", L"list", true) COMMAND_LINE_TEST_CASE(L"container list --format json", L"list", true) COMMAND_LINE_TEST_CASE(L"container list --format table", L"list", true) COMMAND_LINE_TEST_CASE(L"container list --format badformat", L"list", false) +COMMAND_LINE_TEST_CASE(L"container prune", L"prune", true) +COMMAND_LINE_TEST_CASE(L"container prune --session foo", L"prune", true) COMMAND_LINE_TEST_CASE(L"run ubuntu", L"run", true) COMMAND_LINE_TEST_CASE(L"run --rm -it --entrypoint bash archlinux:latest -c \"echo 123\"", L"run", true) COMMAND_LINE_TEST_CASE(L"run --rm --entrypoint /bin/bash debian:latest -c ls", L"run", true) diff --git a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp new file mode 100644 index 000000000..968a3f07f --- /dev/null +++ b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp @@ -0,0 +1,157 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCE2EContainerPruneTests.cpp + +Abstract: + + This file contains end-to-end tests for WSLC container prune command. +--*/ + +#include "precomp.h" +#include "windows/Common.h" +#include "WSLCExecutor.h" +#include "WSLCE2EHelpers.h" + +namespace WSLCE2ETests { +using namespace wsl::shared; + +class WSLCE2EContainerPruneTests +{ + WSLC_TEST_CLASS(WSLCE2EContainerPruneTests) + + TEST_CLASS_SETUP(ClassSetup) + { + EnsureImageIsLoaded(DebianImage); + + // Clean up any leftover containers from previous failed runs + EnsureContainerDoesNotExist(L"prune-test-container"); + EnsureContainerDoesNotExist(L"prune-running-test"); + EnsureContainerDoesNotExist(L"prune-multi-1"); + EnsureContainerDoesNotExist(L"prune-multi-2"); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + // Clean up any leftover containers + RunWslc(L"container prune"); + return true; + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_HelpCommand) + { + const auto result = RunWslc(L"container prune --help"); + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_NoStoppedContainers) + { + // Prune when no stopped containers exist should succeed with zero reclaimed space + const auto result = RunWslc(L"container prune"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VERIFY_IS_TRUE(result.StdoutContainsSubstring(Localization::WSLCCLI_ContainerPruneSpaceReclaimed(0.0))); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_StoppedContainer) + { + // Create and stop a container, then prune it + auto createResult = RunWslc(std::format(L"container create --name prune-test-container {}", DebianImage.NameAndTag())); + createResult.Verify({.Stderr = L"", .ExitCode = 0}); + auto containerId = createResult.GetStdoutOneLine(); + + auto cleanup = wil::scope_exit([&]() { RunWslc(L"container prune"); }); + + // The created container is in stopped state, so prune should remove it + const auto result = RunWslc(L"container prune"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + // Verify pruned container ID is in output + VERIFY_IS_TRUE(result.StdoutContainsSubstring(containerId)); + + // Verify the container is actually removed + VerifyContainerIsNotListed(L"prune-test-container"); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_RunningContainerNotPruned) + { + // Start a running container, verify prune does NOT remove it + auto runResult = RunWslc(std::format(L"container run --detach --name prune-running-test {} sleep 300", DebianImage.NameAndTag())); + runResult.Verify({.Stderr = L"", .ExitCode = 0}); + + auto cleanup = wil::scope_exit([&]() { + RunWslc(L"container kill prune-running-test"); + RunWslc(L"container remove --force prune-running-test"); + }); + + // Prune should not remove a running container + const auto pruneResult = RunWslc(L"container prune"); + pruneResult.Verify({.Stderr = L"", .ExitCode = 0}); + + // Verify the running container is still present + VerifyContainerIsListed(L"prune-running-test", L"running"); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_MultipleStopped) + { + // Create multiple stopped containers and verify all are pruned + auto create1 = RunWslc(std::format(L"container create --name prune-multi-1 {}", DebianImage.NameAndTag())); + create1.Verify({.Stderr = L"", .ExitCode = 0}); + auto containerId1 = create1.GetStdoutOneLine(); + + auto create2 = RunWslc(std::format(L"container create --name prune-multi-2 {}", DebianImage.NameAndTag())); + create2.Verify({.Stderr = L"", .ExitCode = 0}); + auto containerId2 = create2.GetStdoutOneLine(); + + auto cleanup = wil::scope_exit([&]() { RunWslc(L"container prune"); }); + + const auto result = RunWslc(L"container prune"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + // Verify pruned container IDs are in output + VERIFY_IS_TRUE(result.StdoutContainsSubstring(containerId1)); + VERIFY_IS_TRUE(result.StdoutContainsSubstring(containerId2)); + + // Verify both containers are removed + VerifyContainerIsNotListed(L"prune-multi-1"); + VerifyContainerIsNotListed(L"prune-multi-2"); + } + +private: + const TestImage& DebianImage = DebianTestImage(); + + std::wstring GetHelpMessage() const + { + std::wstringstream output; + output << GetWslcHeader() // + << GetDescription() // + << GetUsage() // + << GetAvailableOptions(); + return output.str(); + } + + std::wstring GetDescription() const + { + return Localization::WSLCCLI_ContainerPruneLongDesc() + L"\r\n\r\n"; + } + + std::wstring GetUsage() const + { + return L"Usage: wslc container prune []\r\n\r\n"; + } + + std::wstring GetAvailableOptions() const + { + std::wstringstream options; + options << L"The following options are available:\r\n" + << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n" + << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n" + << L"\r\n"; + return options.str(); + } +}; +} // namespace WSLCE2ETests diff --git a/test/windows/wslc/e2e/WSLCE2EContainerTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerTests.cpp index a9ef20db5..d1f5dc569 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerTests.cpp @@ -76,6 +76,7 @@ class WSLCE2EContainerTests {L"kill", Localization::WSLCCLI_ContainerKillDesc()}, {L"logs", Localization::WSLCCLI_ContainerLogsDesc()}, {L"list", Localization::WSLCCLI_ContainerListDesc()}, + {L"prune", Localization::WSLCCLI_ContainerPruneDesc()}, {L"remove", Localization::WSLCCLI_ContainerRemoveDesc()}, {L"run", Localization::WSLCCLI_ContainerRunDesc()}, {L"start", Localization::WSLCCLI_ContainerStartDesc()}, diff --git a/test/windows/wslc/e2e/WSLCExecutor.cpp b/test/windows/wslc/e2e/WSLCExecutor.cpp index 5711713ae..4a2166b21 100644 --- a/test/windows/wslc/e2e/WSLCExecutor.cpp +++ b/test/windows/wslc/e2e/WSLCExecutor.cpp @@ -141,6 +141,12 @@ bool WSLCExecutionResult::StdoutContainsLine(const std::wstring& expectedLine) c return false; } +bool WSLCExecutionResult::StdoutContainsSubstring(const std::wstring& substring) const +{ + VERIFY_IS_TRUE(Stdout.has_value()); + return Stdout.value().find(substring) != std::wstring::npos; +} + WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType elevationType) { auto cmd = L"\"" + GetWslcPath() + L"\" " + commandLine; diff --git a/test/windows/wslc/e2e/WSLCExecutor.h b/test/windows/wslc/e2e/WSLCExecutor.h index c7a4fccba..98cc45de9 100644 --- a/test/windows/wslc/e2e/WSLCExecutor.h +++ b/test/windows/wslc/e2e/WSLCExecutor.h @@ -44,6 +44,7 @@ struct WSLCExecutionResult std::vector GetStdoutLines() const; std::wstring GetStdoutOneLine() const; bool StdoutContainsLine(const std::wstring& expectedLine) const; + bool StdoutContainsSubstring(const std::wstring& substring) const; }; // Interactive session for testing wslc commands that require stdin/stdout interaction.