Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions localization/strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2404,6 +2404,16 @@ For privacy information about this product please visit https://aka.ms/privacy.<
<data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
<value>View logs for a container.</value>
</data>
<data name="WSLCCLI_ContainerPruneDesc" xml:space="preserve">
<value>Remove all stopped containers.</value>
</data>
<data name="WSLCCLI_ContainerPruneLongDesc" xml:space="preserve">
<value>Removes all stopped containers.</value>
</data>
<data name="WSLCCLI_ContainerPruneSpaceReclaimed" xml:space="preserve">
<value>Total reclaimed space: {:.2f} MB</value>
<comment>{FixedPlaceholder="{:.2f}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
<value>Remove containers.</value>
</data>
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/commands/ContainerCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ std::vector<std::unique_ptr<Command>> ContainerCommand::GetCommands() const
commands.push_back(std::make_unique<ContainerKillCommand>(FullName()));
commands.push_back(std::make_unique<ContainerLogsCommand>(FullName()));
commands.push_back(std::make_unique<ContainerListCommand>(FullName()));
commands.push_back(std::make_unique<ContainerPruneCommand>(FullName()));
commands.push_back(std::make_unique<ContainerRemoveCommand>(FullName()));
commands.push_back(std::make_unique<ContainerRunCommand>(FullName()));
commands.push_back(std::make_unique<ContainerStartCommand>(FullName()));
Expand Down
15 changes: 15 additions & 0 deletions src/windows/wslc/commands/ContainerCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<Argument> GetArguments() const override;
std::wstring ShortDescription() const override;
std::wstring LongDescription() const override;

protected:
void ExecuteInternal(CLIExecutionContext& context) const override;
};
Expand Down
50 changes: 50 additions & 0 deletions src/windows/wslc/commands/ContainerPruneCommand.cpp
Original file line number Diff line number Diff line change
@@ -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<Argument> 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
6 changes: 6 additions & 0 deletions src/windows/wslc/services/ContainerModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ struct StopContainerOptions
LONG Timeout = DefaultTimeout;
};

struct PruneContainersResult
{
std::vector<std::string> Containers;
ULONGLONG SpaceReclaimed{};
};

struct KillContainerOptions
{
int Signal = WSLCSignalSIGKILL;
Expand Down
15 changes: 15 additions & 0 deletions src/windows/wslc/services/ContainerService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -527,4 +527,19 @@ wsl::windows::common::docker_schema::ContainerStats ContainerService::Stats(Sess
THROW_IF_FAILED(container->Stats(&output));
return wsl::shared::FromJson<wsl::windows::common::docker_schema::ContainerStats>(output.get());
}

PruneContainersResult ContainerService::Prune(Session& session)
{
PruneResult result;
THROW_IF_FAILED(session.Get()->PruneContainers(nullptr, 0, 0, &result.result));

PruneContainersResult pruneResult;
pruneResult.SpaceReclaimed = result.result.SpaceReclaimed;
for (ULONG i = 0; i < result.result.ContainersCount; i++)
{
pruneResult.Containers.push_back(result.result.Containers[i]);
}

return pruneResult;
}
} // namespace wsl::windows::wslc::services
1 change: 1 addition & 0 deletions src/windows/wslc/services/ContainerService.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,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
17 changes: 17 additions & 0 deletions src/windows/wslc/tasks/ContainerTasks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -674,4 +675,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<Data::Session>();

auto result = ContainerService::Prune(session);

for (const auto& containerId : result.Containers)
{
PrintMessage(MultiByteToWide(containerId));
}

PrintMessage(L"");
PrintMessage(Localization::WSLCCLI_ContainerPruneSpaceReclaimed(static_cast<double>(result.SpaceReclaimed) / WSLC_IMAGE_1MB));
}
} // namespace wsl::windows::wslc::task
1 change: 1 addition & 0 deletions src/windows/wslc/tasks/ContainerTasks.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions test/windows/wslc/CommandLineTestCases.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
185 changes: 185 additions & 0 deletions test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*++

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);
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});

VerifyStdoutContains(result, L"Total reclaimed space:");
Comment on lines +47 to +51
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's zero reclaimed, can we use the .Verify({.Stdout = "...: 0", ...}); instead?

}

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 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});

VerifyStdoutContains(result, L"Total reclaimed space:");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a chance we can hardcode the values here based on manual test, or they are unpredictable?


// Verify the container is actually removed
auto listResult = RunWslc(L"container list --all");
listResult.Verify({.Stderr = L"", .ExitCode = 0});
for (const auto& line : listResult.GetStdoutLines())
{
VERIFY_IS_FALSE(
line.find(L"prune-test-container") != std::wstring::npos,
L"Container 'prune-test-container' should have been pruned");
}
Comment on lines +70 to +77
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a method for this VerifyContainerIsNotListed 😊

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for the other methods

}

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
auto listResult = RunWslc(L"container list");
listResult.Verify({.Stderr = L"", .ExitCode = 0});
bool found = false;
for (const auto& line : listResult.GetStdoutLines())
{
if (line.find(L"prune-running-test") != std::wstring::npos)
{
found = true;
break;
}
}

VERIFY_IS_TRUE(found, L"Running container 'prune-running-test' should still be present after prune");
}

WSLC_TEST_METHOD(WSLCE2E_Container_Prune_MultipleStopped)
{
// Create multiple stopped containers and verify all are pruned
RunWslc(std::format(L"container create --name prune-multi-1 {}", DebianImage.NameAndTag()))
.Verify({.Stderr = L"", .ExitCode = 0});
RunWslc(std::format(L"container create --name prune-multi-2 {}", DebianImage.NameAndTag()))
.Verify({.Stderr = L"", .ExitCode = 0});

auto cleanup = wil::scope_exit([&]() { RunWslc(L"container prune"); });

const auto result = RunWslc(L"container prune");
result.Verify({.Stderr = L"", .ExitCode = 0});

VerifyStdoutContains(result, L"Total reclaimed space:");

// Verify both containers are removed
auto listResult = RunWslc(L"container list --all");
listResult.Verify({.Stderr = L"", .ExitCode = 0});
for (const auto& line : listResult.GetStdoutLines())
{
VERIFY_IS_FALSE(
line.find(L"prune-multi-1") != std::wstring::npos, L"Container 'prune-multi-1' should have been pruned");
VERIFY_IS_FALSE(
line.find(L"prune-multi-2") != std::wstring::npos, L"Container 'prune-multi-2' should have been pruned");
}
}

private:
const TestImage& DebianImage = DebianTestImage();

static void VerifyStdoutContains(const WSLCExecutionResult& result, const std::wstring& substring)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless my comment suggestion above does not help eliminate the need for this method, I would suggest moving this method to the WSLCExecutor where another method called StdoutContainsLine method exist.

{
for (const auto& line : result.GetStdoutLines())
{
if (line.find(substring) != std::wstring::npos)
{
return;
}
}

VERIFY_FAIL(std::format(L"Expected stdout to contain '{}'", substring).c_str());
}

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 [<options>]\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
1 change: 1 addition & 0 deletions test/windows/wslc/e2e/WSLCE2EContainerTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()},
Expand Down
Loading