@samitouri / QOSAMI-WSL / commits / 2be22d91

Add --force option to wslc volume rm and network rm commands (#40834)

* Add --force option to wslc volume rm and network rm commands Implement --force/-f flag for 'wslc volume rm' and 'wslc network rm' commands following Docker CLI semantics. When --force is specified, 'not found' errors are silently suppressed and the command exits with code 0 instead of 1. Changes: - Add VolumeForce and NetworkForce arg types in ArgumentDefinitions.h - Register --force argument in VolumeRemoveCommand and NetworkRemoveCommand - Update DeleteVolumes/DeleteNetworks tasks to read force flag and suppress not-found errors when set - Add localization strings for the new argument descriptions - Add e2e tests for --force with not-found, valid removal, and mixed found/not-found scenarios - Update help message expectations in existing tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix clang format errors * Address code review feedback - Reuse existing ArgType::Force with description override instead of custom VolumeForce/NetworkForce arg types (per @dkbennett) - Move --force arg after positional args in GetArguments() to match convention of positional/required args first, optional args after - Add WSLCE2E_Volume_Remove_Force_VolumeInUse_Fail test to verify --force does not bypass in-use safety checks (per @Copilot) 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 Jun 18, 2026 at 15:13 UTC 2be22d91ecf3e9fb91a5b48cb7c1cffc93b2368b
7 files changed +117 -12
localization/strings/en-US/Resources.resw
+6
@@ -2844,6 +2844,9 @@ On first run, creates the file with all settings commented out at their defaults
2844 <data name="WSLCCLI_NetworkArgDescription" xml:space="preserve">
2845 <value>Connect a container to a network</value>
2846 </data>
2847 + <data name="WSLCCLI_NetworkForceArgDescription" xml:space="preserve">
2848 + <value>Do not error if the network does not exist</value>
2849 + </data>
2850 <data name="WSLCCLI_NetworkEmptyError" xml:space="preserve">
2851 <value>Invalid {} value: network name cannot be empty or whitespace</value>
2852 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
@@ -2938,6 +2941,9 @@ On first run, creates the file with all settings commented out at their defaults
2941 <data name="WSLCCLI_VolumeArgDescription" xml:space="preserve">
2942 <value>Bind mount a volume to the container</value>
2943 </data>
2944 + <data name="WSLCCLI_VolumeForceArgDescription" xml:space="preserve">
2945 + <value>Do not error if the volume does not exist</value>
2946 + </data>
2947 <data name="WSLCCLI_WorkingDirArgDescription" xml:space="preserve">
2948 <value>Working directory inside the container</value>
2949 </data>
src/windows/wslc/commands/NetworkRemoveCommand.cpp
+1
@@ -28,6 +28,7 @@ std::vector<Argument> NetworkRemoveCommand::GetArguments() const
28 {
29 return {
30 Argument::Create(ArgType::NetworkName, true, NO_LIMIT),
31 + Argument::Create(ArgType::Force, std::nullopt, std::nullopt, Localization::WSLCCLI_NetworkForceArgDescription()),
32 };
33 }
34
src/windows/wslc/commands/VolumeRemoveCommand.cpp
+1
@@ -28,6 +28,7 @@ std::vector<Argument> VolumeRemoveCommand::GetArguments() const
28 {
29 return {
30 Argument::Create(ArgType::VolumeName, true, NO_LIMIT),
31 + Argument::Create(ArgType::Force, std::nullopt, std::nullopt, Localization::WSLCCLI_VolumeForceArgDescription()),
32 };
33 }
34
src/windows/wslc/tasks/NetworkTasks.cpp
+9 -4
@@ -49,7 +49,7 @@ static bool TryInspectNetwork(Session& session, const std::string& networkName,
49 }
50 }
51
52 -static bool TryDeleteNetwork(Session& session, const std::string& networkName)
52 +static bool TryDeleteNetwork(Session& session, const std::string& networkName, bool force)
53 {
54 try
55 {
@@ -60,7 +60,11 @@ static bool TryDeleteNetwork(Session& session, const std::string& networkName)
60 {
61 if (ex.GetErrorCode() == WSLC_E_NETWORK_NOT_FOUND)
62 {
63 - PrintMessage(Localization::MessageWslcNetworkNotFound(networkName.c_str()), stderr);
63 + if (!force)
64 + {
65 + PrintMessage(Localization::MessageWslcNetworkNotFound(networkName.c_str()), stderr);
66 + }
67 +
68 return false;
69 }
70
@@ -100,13 +104,14 @@ void DeleteNetworks(CLIExecutionContext& context)
104 WI_ASSERT(context.Data.Contains(Data::Session));
105 auto& session = context.Data.Get<Data::Session>();
106 auto networkNames = context.Args.GetAll<ArgType::NetworkName>();
107 + const bool force = context.Args.Contains(ArgType::Force);
108 for (const auto& name : networkNames)
109 {
105 - if (TryDeleteNetwork(session, WideToMultiByte(name)))
110 + if (TryDeleteNetwork(session, WideToMultiByte(name), force))
111 {
112 PrintMessage(name);
113 }
109 - else
114 + else if (!force)
115 {
116 context.ExitCode = 1;
117 }
src/windows/wslc/tasks/VolumeTasks.cpp
+9 -4
@@ -49,7 +49,7 @@ static bool TryInspectVolume(Session& session, const std::string& volumeName, st
49 }
50 }
51
52 -static bool TryDeleteVolume(Session& session, const std::string& volumeName)
52 +static bool TryDeleteVolume(Session& session, const std::string& volumeName, bool force)
53 {
54 try
55 {
@@ -60,7 +60,11 @@ static bool TryDeleteVolume(Session& session, const std::string& volumeName)
60 {
61 if (ex.GetErrorCode() == WSLC_E_VOLUME_NOT_FOUND)
62 {
63 - PrintMessage(Localization::MessageWslcVolumeNotFound(volumeName.c_str()), stderr);
63 + if (!force)
64 + {
65 + PrintMessage(Localization::MessageWslcVolumeNotFound(volumeName.c_str()), stderr);
66 + }
67 +
68 return false;
69 }
70
@@ -104,13 +108,14 @@ void DeleteVolumes(CLIExecutionContext& context)
108 WI_ASSERT(context.Data.Contains(Data::Session));
109 auto& session = context.Data.Get<Data::Session>();
110 auto volumeNames = context.Args.GetAll<ArgType::VolumeName>();
111 + const bool force = context.Args.Contains(ArgType::Force);
112 for (const auto& name : volumeNames)
113 {
109 - if (TryDeleteVolume(session, WideToMultiByte(name)))
114 + if (TryDeleteVolume(session, WideToMultiByte(name), force))
115 {
116 PrintMessage(name);
117 }
113 - else
118 + else if (!force)
119 {
120 context.ExitCode = 1;
121 }
test/windows/wslc/e2e/WSLCE2ENetworkRemoveTests.cpp
+33 -2
@@ -99,6 +99,36 @@ class WSLCE2ENetworkRemoveTests
99 VerifyNetworkIsNotListed(TestNetworkName);
100 }
101
102 + WSLC_TEST_METHOD(WSLCE2E_Network_Remove_Force_NotFound)
103 + {
104 + auto result = RunWslc(std::format(L"network remove --force {}", TestNetworkName));
105 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
106 + }
107 +
108 + WSLC_TEST_METHOD(WSLCE2E_Network_Remove_Force_Valid)
109 + {
110 + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
111 + result.Verify({.Stderr = L"", .ExitCode = 0});
112 +
113 + VerifyNetworkIsListed(TestNetworkName);
114 +
115 + result = RunWslc(std::format(L"network remove --force {}", TestNetworkName));
116 + result.Verify({.Stdout = std::format(L"{}\r\n", TestNetworkName), .Stderr = L"", .ExitCode = 0});
117 +
118 + VerifyNetworkIsNotListed(TestNetworkName);
119 + }
120 +
121 + WSLC_TEST_METHOD(WSLCE2E_Network_Remove_Force_MixedFoundNotFound)
122 + {
123 + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
124 + result.Verify({.Stderr = L"", .ExitCode = 0});
125 + VerifyNetworkIsListed(TestNetworkName);
126 +
127 + result = RunWslc(std::format(L"network remove --force {} {}", TestNetworkName, TestNetworkName2));
128 + result.Verify({.Stdout = std::format(L"{}\r\n", TestNetworkName), .Stderr = L"", .ExitCode = 0});
129 + VerifyNetworkIsNotListed(TestNetworkName);
130 + }
131 +
132 private:
133 const std::wstring TestNetworkName = L"wslc-e2e-network-remove";
134 const std::wstring TestNetworkName2 = L"wslc-e2e-network-remove-2";
@@ -142,8 +172,9 @@ private:
172 std::wstring GetAvailableOptions() const
173 {
174 std::wstringstream options;
145 - options << L"The following options are available:\r\n" //
146 - << L" -?,--help Shows help about the selected command\r\n" //
175 + options << L"The following options are available:\r\n" //
176 + << L" -f,--force Do not error if the network does not exist\r\n" //
177 + << L" -?,--help Shows help about the selected command\r\n" //
178 << L"\r\n";
179 return options.str();
180 }
test/windows/wslc/e2e/WSLCE2EVolumeRemoveTests.cpp
+58 -2
@@ -131,6 +131,61 @@ class WSLCE2EVolumeRemoveTests
131 VerifyVolumeIsListed(TestVolumeName);
132 }
133
134 + WSLC_TEST_METHOD(WSLCE2E_Volume_Remove_Force_NotFound)
135 + {
136 + auto result = RunWslc(std::format(L"volume remove --force {}", TestVolumeName));
137 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
138 + }
139 +
140 + WSLC_TEST_METHOD(WSLCE2E_Volume_Remove_Force_Valid)
141 + {
142 + auto result = RunWslc(std::format(L"volume create {}", TestVolumeName));
143 + result.Verify({.Stderr = L"", .ExitCode = 0});
144 +
145 + VerifyVolumeIsListed(TestVolumeName);
146 +
147 + result = RunWslc(std::format(L"volume remove --force {}", TestVolumeName));
148 + result.Verify({.Stdout = std::format(L"{}\r\n", TestVolumeName), .Stderr = L"", .ExitCode = 0});
149 +
150 + VerifyVolumeIsNotListed(TestVolumeName);
151 + }
152 +
153 + WSLC_TEST_METHOD(WSLCE2E_Volume_Remove_Force_MixedFoundNotFound)
154 + {
155 + auto result = RunWslc(std::format(L"volume create {}", TestVolumeName));
156 + result.Verify({.Stderr = L"", .ExitCode = 0});
157 + VerifyVolumeIsListed(TestVolumeName);
158 +
159 + result = RunWslc(std::format(L"volume remove --force {} {}", TestVolumeName, TestVolumeName2));
160 + result.Verify({.Stdout = std::format(L"{}\r\n", TestVolumeName), .Stderr = L"", .ExitCode = 0});
161 + VerifyVolumeIsNotListed(TestVolumeName);
162 + }
163 +
164 + WSLC_TEST_METHOD(WSLCE2E_Volume_Remove_Force_VolumeInUse_Fail)
165 + {
166 + auto result = RunWslc(std::format(L"volume create {}", TestVolumeName));
167 + result.Verify({.Stderr = L"", .ExitCode = 0});
168 + VerifyVolumeIsListed(TestVolumeName);
169 +
170 + // Create a container that uses the volume to ensure it's in use
171 + result = RunWslc(std::format(
172 + L"container run -d --name {} -v {}:/data {} sh -c \"echo -n 'WSLC Volume In Use Test' > /data/test.txt && sleep "
173 + L"infinity\"",
174 + WslcContainerName,
175 + TestVolumeName,
176 + DebianImage.NameAndTag()));
177 + result.Verify({.Stderr = L"", .ExitCode = 0});
178 +
179 + // --force does not bypass in-use checks, volume should still fail to be removed
180 + result = RunWslc(std::format(L"volume remove --force {}", TestVolumeName));
181 + result.Verify(
182 + {.Stdout = L"",
183 + .Stderr = std::format(L"Volume '{}' is in use.\r\nError code: ERROR_SHARING_VIOLATION\r\n", TestVolumeName),
184 + .ExitCode = 1});
185 +
186 + VerifyVolumeIsListed(TestVolumeName);
187 + }
188 +
189 private:
190 const std::wstring WslcContainerName = L"wslc-test-container";
191 const TestImage& DebianImage = DebianTestImage();
@@ -176,8 +231,9 @@ private:
231 std::wstring GetAvailableOptions() const
232 {
233 std::wstringstream options;
179 - options << L"The following options are available:\r\n" //
180 - << L" -?,--help Shows help about the selected command\r\n" //
234 + options << L"The following options are available:\r\n" //
235 + << L" -f,--force Do not error if the volume does not exist\r\n" //
236 + << L" -?,--help Shows help about the selected command\r\n" //
237 << L"\r\n";
238 return options.str();
239 }