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