Add --volumes option to wslc container rm command (#41265)

AmirMS committed Aug 6, 2026 at 20:37 UTC 157b997b136561a685c884038034058ce669420d
8 files changed +142 -4
localization/strings/en-US/Resources.resw
+3
@@ -3178,6 +3178,9 @@ On first run, creates the file with all settings commented out at their defaults
3178 <data name="WSLCCLI_VolumeForceArgDescription" xml:space="preserve">
3179 <value>Do not error if the volume does not exist</value>
3180 </data>
3181 + <data name="WSLCCLI_RemoveVolumesArgDescription" xml:space="preserve">
3182 + <value>Remove anonymous volumes associated with the container</value>
3183 + </data>
3184 <data name="WSLCCLI_WorkingDirArgDescription" xml:space="preserve">
3185 <value>Working directory inside the container</value>
3186 </data>
src/windows/wslc/arguments/ArgumentDefinitions.h
+1
@@ -136,5 +136,6 @@ _(Version, "version", L"v", Kind::Flag, L
136 /*_(Virtual, "virtualization", NO_ALIAS, Kind::Value, Localization::WSLCCLI_VirtualArgDescription())*/ \
137 _(Volume, "volume", L"v", Kind::Value, Localization::WSLCCLI_VolumeArgDescription()) \
138 _(VolumeName, "volume-name", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_VolumeNameArgDescription()) \
139 +_(Volumes, "volumes", L"v", Kind::Flag, Localization::WSLCCLI_RemoveVolumesArgDescription()) \
140 _(WorkDir, "workdir", L"w", Kind::Value, Localization::WSLCCLI_WorkingDirArgDescription()) \
141 // clang-format on
src/windows/wslc/commands/ContainerRemoveCommand.cpp
+1
@@ -29,6 +29,7 @@ std::vector<Argument> ContainerRemoveCommand::GetArguments() const
29 return {
30 Argument::Create(ArgType::ContainerId, true, Limit::Unlimited),
31 Argument::Create(ArgType::Force),
32 + Argument::Create(ArgType::Volumes),
33 };
34 }
35
src/windows/wslc/services/ContainerService.cpp
+6 -2
@@ -533,12 +533,16 @@ void ContainerService::Kill(Session& session, const std::string& id, WSLCSignal
533 THROW_IF_FAILED(container->Kill(signal));
534 }
535
536 -void ContainerService::Delete(Session& session, const std::string& id, bool force)
536 +void ContainerService::Delete(Session& session, const std::string& id, bool force, bool deleteVolumes)
537 {
538 [[maybe_unused]] auto operation = session.BeginContainerOperation();
539 wil::com_ptr<IWSLCContainer> container;
540 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
541 - THROW_IF_FAILED(container->Delete(force ? WSLCDeleteFlagsForce : WSLCDeleteFlagsNone));
541 +
542 + auto flags = WSLCDeleteFlagsNone;
543 + WI_SetFlagIf(flags, WSLCDeleteFlagsForce, force);
544 + WI_SetFlagIf(flags, WSLCDeleteFlagsDeleteVolumes, deleteVolumes);
545 + THROW_IF_FAILED(container->Delete(flags));
546 }
547
548 std::vector<ContainerInformation> ContainerService::List(
src/windows/wslc/services/ContainerService.h
+1 -1
@@ -31,7 +31,7 @@ struct ContainerService
31 static int Start(Reporter& reporter, models::Session& session, const std::string& id, bool attach = false);
32 static void Stop(models::Session& session, const std::string& id, models::StopContainerOptions options);
33 static void Kill(models::Session& session, const std::string& id, WSLCSignal signal = WSLCSignalSIGKILL);
34 - static void Delete(models::Session& session, const std::string& id, bool force);
34 + static void Delete(models::Session& session, const std::string& id, bool force, bool deleteVolumes = false);
35 static std::vector<models::ContainerInformation> List(
36 models::Session& session, bool all = false, int limit = -1, const std::vector<std::pair<std::string, std::string>>& filters = {});
37
src/windows/wslc/tasks/ContainerTasks.cpp
+2 -1
@@ -626,9 +626,10 @@ void RemoveContainers(CLIExecutionContext& context)
626 auto& session = context.Data.Get<Data::Session>();
627 auto containerIds = context.Args.GetAll<ArgType::ContainerId>();
628 bool force = context.Args.GetFlag<ArgType::Force>();
629 + bool deleteVolumes = context.Args.GetFlag<ArgType::Volumes>();
630 for (const auto& id : containerIds)
631 {
631 - ContainerService::Delete(session, WideToMultiByte(id), force);
632 + ContainerService::Delete(session, WideToMultiByte(id), force, deleteVolumes);
633 context.Reporter.Output(L"{}\n", id);
634 }
635 }
test/windows/wslc/CommandLineTestCases.h
+5
@@ -195,6 +195,11 @@ COMMAND_LINE_TEST_CASE(L"remove cont1", L"remove", true)
195 COMMAND_LINE_TEST_CASE(L"container remove cont1 cont2", L"remove", true)
196 COMMAND_LINE_TEST_CASE(L"rm cont1", L"remove", true)
197 COMMAND_LINE_TEST_CASE(L"container rm cont1 cont2", L"remove", true)
198 +COMMAND_LINE_TEST_CASE(L"container rm --volumes cont1", L"remove", true)
199 +COMMAND_LINE_TEST_CASE(L"container rm -v cont1 cont2", L"remove", true)
200 +COMMAND_LINE_TEST_CASE(L"container rm --force --volumes cont1", L"remove", true)
201 +COMMAND_LINE_TEST_CASE(L"container rm -fv cont1", L"remove", true) // Combined short flags
202 +COMMAND_LINE_TEST_CASE(L"container rm -v", L"remove", false) // Missing required container-id positional
203 COMMAND_LINE_TEST_CASE(L"container attach cont", L"attach", true)
204 COMMAND_LINE_TEST_CASE(L"container attach", L"attach", false)
205 // Stats command tests
test/windows/wslc/e2e/WSLCE2EContainerRemoveTests.cpp
+123
@@ -26,6 +26,7 @@ class WSLCE2EContainerRemoveTests
26 TEST_CLASS_SETUP(ClassSetup)
27 {
28 EnsureImageIsLoaded(DebianImage);
29 + BuildAnonymousVolumeImage();
30 return true;
31 }
32
@@ -33,6 +34,8 @@ class WSLCE2EContainerRemoveTests
34 {
35 EnsureContainerDoesNotExist(WslcContainerName);
36 EnsureContainerDoesNotExist(WslcContainerName2);
37 + EnsureVolumeDoesNotExist(TestVolumeName);
38 + EnsureImageIsDeleted(AnonymousVolumeImage);
39 EnsureImageIsDeleted(DebianImage);
40 return true;
41 }
@@ -41,6 +44,7 @@ class WSLCE2EContainerRemoveTests
44 {
45 EnsureContainerDoesNotExist(WslcContainerName);
46 EnsureContainerDoesNotExist(WslcContainerName2);
47 + EnsureVolumeDoesNotExist(TestVolumeName);
48 return true;
49 }
50
@@ -157,9 +161,128 @@ class WSLCE2EContainerRemoveTests
161 VerifyContainerIsNotListed(WslcContainerName2);
162 }
163
164 + WSLC_TEST_METHOD(WSLCE2E_Container_Remove_Volumes_RemovesAnonymousVolume)
165 + {
166 + const auto volumesBefore = ListVolumeNames();
167 +
168 + auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, AnonymousVolumeImage.NameAndTag()));
169 + result.Verify({.Stderr = L"", .ExitCode = 0});
170 +
171 + const auto anonymousVolume = GetNewAnonymousVolumeName(volumesBefore);
172 + VerifyVolumeIsListed(anonymousVolume);
173 +
174 + result = RunWslc(std::format(L"container remove --volumes {}", WslcContainerName));
175 + result.Verify({.Stdout = std::format(L"{}\r\n", WslcContainerName), .Stderr = L"", .ExitCode = 0});
176 +
177 + VerifyContainerIsNotListed(WslcContainerName);
178 + VerifyVolumeIsNotListed(anonymousVolume);
179 + }
180 +
181 + WSLC_TEST_METHOD(WSLCE2E_Container_Remove_WithoutVolumes_KeepsAnonymousVolume)
182 + {
183 + const auto volumesBefore = ListVolumeNames();
184 +
185 + auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, AnonymousVolumeImage.NameAndTag()));
186 + result.Verify({.Stderr = L"", .ExitCode = 0});
187 +
188 + const auto anonymousVolume = GetNewAnonymousVolumeName(volumesBefore);
189 + auto cleanup = wil::scope_exit([&]() { EnsureVolumeDoesNotExist(anonymousVolume); });
190 + VerifyVolumeIsListed(anonymousVolume);
191 +
192 + result = RunWslc(std::format(L"container remove {}", WslcContainerName));
193 + result.Verify({.Stdout = std::format(L"{}\r\n", WslcContainerName), .Stderr = L"", .ExitCode = 0});
194 +
195 + VerifyContainerIsNotListed(WslcContainerName);
196 + VerifyVolumeIsListed(anonymousVolume);
197 + }
198 +
199 + WSLC_TEST_METHOD(WSLCE2E_Container_Remove_Volumes_KeepsNamedVolume)
200 + {
201 + auto result = RunWslc(std::format(L"volume create {}", TestVolumeName));
202 + result.Verify({.Stderr = L"", .ExitCode = 0});
203 +
204 + result = RunWslc(std::format(L"container create --name {} -v {}:/data {}", WslcContainerName, TestVolumeName, DebianImage.NameAndTag()));
205 + result.Verify({.Stderr = L"", .ExitCode = 0});
206 +
207 + result = RunWslc(std::format(L"container remove --volumes {}", WslcContainerName));
208 + result.Verify({.Stdout = std::format(L"{}\r\n", WslcContainerName), .Stderr = L"", .ExitCode = 0});
209 +
210 + VerifyContainerIsNotListed(WslcContainerName);
211 + VerifyVolumeIsListed(TestVolumeName);
212 + }
213 +
214 + WSLC_TEST_METHOD(WSLCE2E_Container_Remove_Volumes_Force_RunningContainer)
215 + {
216 + const auto volumesBefore = ListVolumeNames();
217 +
218 + auto result =
219 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, AnonymousVolumeImage.NameAndTag()));
220 + result.Verify({.Stderr = L"", .ExitCode = 0});
221 + const auto containerId = result.GetStdoutOneLine();
222 + VERIFY_IS_FALSE(containerId.empty());
223 +
224 + const auto anonymousVolume = GetNewAnonymousVolumeName(volumesBefore);
225 + VerifyVolumeIsListed(anonymousVolume);
226 + VerifyContainerIsListed(containerId, L"running");
227 +
228 + // -fv exercises the combined short form.
229 + result = RunWslc(std::format(L"container rm -fv {}", containerId));
230 + result.Verify({.Stdout = std::format(L"{}\r\n", containerId), .Stderr = L"", .ExitCode = 0});
231 +
232 + VerifyContainerIsNotListed(containerId);
233 + VerifyVolumeIsNotListed(anonymousVolume);
234 + }
235 +
236 private:
237 + // The VOLUME directive is the only way to get an anonymous volume: `-v` requires source:target.
238 + void BuildAnonymousVolumeImage()
239 + {
240 + auto testRoot = std::filesystem::current_path() / L"wslc-e2e-container-remove-volume-image";
241 + auto cleanup = SetupTestDirectory(testRoot);
242 +
243 + auto dockerfilePath = testRoot / L"Dockerfile";
244 + WriteTestFileContent(
245 + dockerfilePath, std::format("FROM {}\nVOLUME /data\n", wsl::shared::string::WideToMultiByte(DebianImage.NameAndTag())));
246 +
247 + auto result = RunWslc(std::format(
248 + L"build \"{}\" -f \"{}\" -t {}", testRoot.wstring(), dockerfilePath.wstring(), AnonymousVolumeImage.NameAndTag()));
249 + result.Verify({.ExitCode = 0});
250 + }
251 +
252 + static std::vector<std::wstring> ListVolumeNames()
253 + {
254 + auto result = RunWslc(L"volume list --quiet");
255 + result.Verify({.Stderr = L"", .ExitCode = 0});
256 + return result.GetStdoutLines();
257 + }
258 +
259 + // A before/after diff is the only option: `volume list` has no --filter and reports no labels.
260 + static std::wstring GetNewAnonymousVolumeName(const std::vector<std::wstring>& before)
261 + {
262 + std::vector<std::wstring> added;
263 + for (const auto& name : ListVolumeNames())
264 + {
265 + if (std::find(before.begin(), before.end(), name) == before.end())
266 + {
267 + added.push_back(name);
268 + }
269 + }
270 +
271 + VERIFY_ARE_EQUAL(static_cast<size_t>(1), added.size());
272 +
273 + VERIFY_IS_TRUE(
274 + InspectVolume(added.front()).Labels.contains("com.docker.volume.anonymous"),
275 + L"The volume created by the container is not an anonymous volume");
276 +
277 + return added.front();
278 + }
279 +
280 const std::wstring WslcContainerName = L"wslc-test-container";
281 const std::wstring WslcContainerName2 = L"wslc-test-container-2";
282 + const std::wstring TestVolumeName = L"wslc-e2e-container-remove-volume";
283 const TestImage& DebianImage = DebianTestImage();
284 +
285 + // Derived from DebianImage, so it must be deleted first in ClassCleanup.
286 + const TestImage AnonymousVolumeImage{.Name = L"wslc-e2e-container-remove-anon", .Tag = L"latest"};
287 };
288 } // namespace WSLCE2ETests
\ No newline at end of file