docker_wrapper/command/
rmi.rs1use super::{CommandExecutor, CommandOutput, DockerCommand};
6use crate::error::Result;
7use async_trait::async_trait;
8
9#[derive(Debug, Clone)]
33pub struct RmiCommand {
34 images: Vec<String>,
36 force: bool,
38 no_prune: bool,
40 pub executor: CommandExecutor,
42}
43
44impl RmiCommand {
45 #[must_use]
55 pub fn new(image: impl Into<String>) -> Self {
56 Self {
57 images: vec![image.into()],
58 force: false,
59 no_prune: false,
60 executor: CommandExecutor::new(),
61 }
62 }
63
64 #[must_use]
74 pub fn new_multiple(images: Vec<impl Into<String>>) -> Self {
75 Self {
76 images: images.into_iter().map(Into::into).collect(),
77 force: false,
78 no_prune: false,
79 executor: CommandExecutor::new(),
80 }
81 }
82
83 #[must_use]
85 pub fn image(mut self, image: impl Into<String>) -> Self {
86 self.images.push(image.into());
87 self
88 }
89
90 #[must_use]
101 pub fn force(mut self) -> Self {
102 self.force = true;
103 self
104 }
105
106 #[must_use]
108 pub fn no_prune(mut self) -> Self {
109 self.no_prune = true;
110 self
111 }
112
113 pub async fn run(&self) -> Result<RmiResult> {
138 let output = self.execute().await?;
139
140 let removed_images = Self::parse_removed_images(&output.stdout);
142
143 Ok(RmiResult {
144 output,
145 removed_images,
146 })
147 }
148
149 fn parse_removed_images(stdout: &str) -> Vec<String> {
151 let mut removed = Vec::new();
152
153 for line in stdout.lines() {
154 let line = line.trim();
155 if line.starts_with("Deleted:") {
156 if let Some(id) = line.strip_prefix("Deleted:") {
157 removed.push(id.trim().to_string());
158 }
159 } else if line.starts_with("Untagged:") {
160 if let Some(tag) = line.strip_prefix("Untagged:") {
161 removed.push(tag.trim().to_string());
162 }
163 }
164 }
165
166 removed
167 }
168}
169
170#[async_trait]
171impl DockerCommand for RmiCommand {
172 type Output = CommandOutput;
173
174 fn build_command_args(&self) -> Vec<String> {
175 let mut args = vec!["rmi".to_string()];
176
177 if self.force {
178 args.push("--force".to_string());
179 }
180
181 if self.no_prune {
182 args.push("--no-prune".to_string());
183 }
184
185 args.extend(self.images.clone());
187
188 args.extend(self.executor.raw_args.clone());
189 args
190 }
191
192 fn get_executor(&self) -> &CommandExecutor {
193 &self.executor
194 }
195
196 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
197 &mut self.executor
198 }
199
200 async fn execute(&self) -> Result<Self::Output> {
201 if self.images.is_empty() {
202 return Err(crate::error::Error::invalid_config(
203 "No images specified for removal",
204 ));
205 }
206
207 let args = self.build_command_args();
208 let command_name = args[0].clone();
209 let command_args = args[1..].to_vec();
210 self.executor
211 .execute_command(&command_name, command_args)
212 .await
213 }
214}
215
216#[derive(Debug, Clone)]
218pub struct RmiResult {
219 pub output: CommandOutput,
221 pub removed_images: Vec<String>,
223}
224
225impl RmiResult {
226 #[must_use]
228 pub fn success(&self) -> bool {
229 self.output.success
230 }
231
232 #[must_use]
234 pub fn removed_images(&self) -> &[String] {
235 &self.removed_images
236 }
237
238 #[must_use]
240 pub fn removed_count(&self) -> usize {
241 self.removed_images.len()
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn test_rmi_single_image() {
251 let cmd = RmiCommand::new("test-image:latest");
252 let args = cmd.build_command_args();
253 assert_eq!(args, vec!["rmi", "test-image:latest"]);
254 }
255
256 #[test]
257 fn test_rmi_multiple_images() {
258 let cmd = RmiCommand::new_multiple(vec!["image1:latest", "image2:v1.0", "image3"]);
259 let args = cmd.build_command_args();
260 assert_eq!(args, vec!["rmi", "image1:latest", "image2:v1.0", "image3"]);
261 }
262
263 #[test]
264 fn test_rmi_with_force() {
265 let cmd = RmiCommand::new("stubborn-image:latest").force();
266 let args = cmd.build_command_args();
267 assert_eq!(args, vec!["rmi", "--force", "stubborn-image:latest"]);
268 }
269
270 #[test]
271 fn test_rmi_with_no_prune() {
272 let cmd = RmiCommand::new("test-image:latest").no_prune();
273 let args = cmd.build_command_args();
274 assert_eq!(args, vec!["rmi", "--no-prune", "test-image:latest"]);
275 }
276
277 #[test]
278 fn test_rmi_all_options() {
279 let cmd = RmiCommand::new("test-image:latest")
280 .image("another-image:v1.0")
281 .force()
282 .no_prune();
283 let args = cmd.build_command_args();
284 assert_eq!(
285 args,
286 vec![
287 "rmi",
288 "--force",
289 "--no-prune",
290 "test-image:latest",
291 "another-image:v1.0"
292 ]
293 );
294 }
295
296 #[test]
297 fn test_parse_removed_images() {
298 let output =
299 "Untagged: test-image:latest\nDeleted: sha256:abc123def456\nDeleted: sha256:789xyz123";
300 let removed = RmiCommand::parse_removed_images(output);
301 assert_eq!(
302 removed,
303 vec![
304 "test-image:latest",
305 "sha256:abc123def456",
306 "sha256:789xyz123"
307 ]
308 );
309 }
310
311 #[test]
312 fn test_parse_removed_images_empty() {
313 let removed = RmiCommand::parse_removed_images("");
314 assert!(removed.is_empty());
315 }
316}