docker_wrapper/command/
rm.rs

1//! Docker rm command implementation.
2//!
3//! This module provides the `docker rm` command for removing stopped containers.
4//!
5//! # Example
6//!
7//! ```rust,no_run
8//! use docker_wrapper::{DockerCommand, RmCommand, RunCommand, StopCommand};
9//!
10//! # #[tokio::main]
11//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
12//! // Create and run a container
13//! let output = RunCommand::new("alpine:latest")
14//!     .name("test-rm")
15//!     .detach()
16//!     .cmd(vec!["sleep".to_string(), "10".to_string()])
17//!     .execute()
18//!     .await?;
19//!
20//! // Stop the container first
21//! StopCommand::new("test-rm")
22//!     .execute()
23//!     .await?;
24//!
25//! // Remove the stopped container
26//! RmCommand::new("test-rm")
27//!     .execute()
28//!     .await?;
29//!
30//! // Force remove a running container (stops and removes)
31//! let output2 = RunCommand::new("nginx:alpine")
32//!     .name("test-force-rm")
33//!     .detach()
34//!     .execute()
35//!     .await?;
36//!
37//! RmCommand::new("test-force-rm")
38//!     .force()  // Force removal even if running
39//!     .volumes()  // Also remove associated volumes
40//!     .execute()
41//!     .await?;
42//!
43//! // Remove multiple containers
44//! RmCommand::new_multiple(vec!["container1", "container2", "container3"])
45//!     .force()
46//!     .execute()
47//!     .await?;
48//! # Ok(())
49//! # }
50//! ```
51
52use super::{CommandExecutor, CommandOutput, DockerCommand};
53use crate::error::{Error, Result};
54use async_trait::async_trait;
55
56/// Docker rm command builder
57#[derive(Debug, Clone)]
58pub struct RmCommand {
59    /// Container names or IDs to remove
60    containers: Vec<String>,
61    /// Force removal of running containers
62    force: bool,
63    /// Remove anonymous volumes associated with the container
64    volumes: bool,
65    /// Remove the specified link
66    link: bool,
67    /// Command executor
68    pub executor: CommandExecutor,
69}
70
71impl RmCommand {
72    /// Create a new rm command for a single container
73    #[must_use]
74    pub fn new(container: impl Into<String>) -> Self {
75        Self {
76            containers: vec![container.into()],
77            force: false,
78            volumes: false,
79            link: false,
80            executor: CommandExecutor::new(),
81        }
82    }
83
84    /// Create a new rm command for multiple containers
85    #[must_use]
86    pub fn new_multiple(containers: Vec<impl Into<String>>) -> Self {
87        Self {
88            containers: containers.into_iter().map(Into::into).collect(),
89            force: false,
90            volumes: false,
91            link: false,
92            executor: CommandExecutor::new(),
93        }
94    }
95
96    /// Add another container to remove
97    #[must_use]
98    pub fn container(mut self, container: impl Into<String>) -> Self {
99        self.containers.push(container.into());
100        self
101    }
102
103    /// Force removal of running containers (uses SIGKILL)
104    #[must_use]
105    pub fn force(mut self) -> Self {
106        self.force = true;
107        self
108    }
109
110    /// Remove anonymous volumes associated with the container
111    #[must_use]
112    pub fn volumes(mut self) -> Self {
113        self.volumes = true;
114        self
115    }
116
117    /// Remove the specified link
118    #[must_use]
119    pub fn link(mut self) -> Self {
120        self.link = true;
121        self
122    }
123
124    /// Execute the rm command
125    ///
126    /// # Errors
127    /// Returns an error if:
128    /// - No containers are specified
129    /// - The Docker daemon is not running
130    /// - Any of the specified containers don't exist
131    /// - A container is running and force flag is not set
132    pub async fn run(&self) -> Result<RmResult> {
133        let output = self.execute().await?;
134
135        // Parse removed container IDs from output
136        let removed: Vec<String> = output
137            .stdout
138            .lines()
139            .filter(|line| !line.is_empty())
140            .map(String::from)
141            .collect();
142
143        Ok(RmResult { removed, output })
144    }
145}
146
147#[async_trait]
148impl DockerCommand for RmCommand {
149    type Output = CommandOutput;
150
151    fn get_executor(&self) -> &CommandExecutor {
152        &self.executor
153    }
154
155    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
156        &mut self.executor
157    }
158
159    fn build_command_args(&self) -> Vec<String> {
160        let mut args = vec!["rm".to_string()];
161
162        if self.force {
163            args.push("--force".to_string());
164        }
165
166        if self.volumes {
167            args.push("--volumes".to_string());
168        }
169
170        if self.link {
171            args.push("--link".to_string());
172        }
173
174        // Add container names/IDs
175        args.extend(self.containers.clone());
176        args.extend(self.executor.raw_args.clone());
177
178        args
179    }
180
181    async fn execute(&self) -> Result<Self::Output> {
182        if self.containers.is_empty() {
183            return Err(Error::invalid_config("No containers specified for removal"));
184        }
185
186        let args = self.build_command_args();
187        let command_name = args[0].clone();
188        let command_args = args[1..].to_vec();
189        self.executor
190            .execute_command(&command_name, command_args)
191            .await
192    }
193}
194
195/// Result from the rm command
196#[derive(Debug, Clone)]
197pub struct RmResult {
198    /// List of removed container IDs
199    pub removed: Vec<String>,
200    /// Raw command output
201    pub output: CommandOutput,
202}
203
204impl RmResult {
205    /// Check if all containers were removed successfully
206    #[must_use]
207    pub fn all_removed(&self) -> bool {
208        self.output.success
209    }
210
211    /// Get the number of containers removed
212    #[must_use]
213    pub fn count(&self) -> usize {
214        self.removed.len()
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn test_rm_single_container() {
224        let cmd = RmCommand::new("test-container");
225        let args = cmd.build_command_args();
226        assert_eq!(args, vec!["rm", "test-container"]);
227    }
228
229    #[test]
230    fn test_rm_multiple_containers() {
231        let cmd = RmCommand::new_multiple(vec!["container1", "container2", "container3"]);
232        let args = cmd.build_command_args();
233        assert_eq!(args, vec!["rm", "container1", "container2", "container3"]);
234    }
235
236    #[test]
237    fn test_rm_with_force() {
238        let cmd = RmCommand::new("test-container").force();
239        let args = cmd.build_command_args();
240        assert_eq!(args, vec!["rm", "--force", "test-container"]);
241    }
242
243    #[test]
244    fn test_rm_with_volumes() {
245        let cmd = RmCommand::new("test-container").volumes();
246        let args = cmd.build_command_args();
247        assert_eq!(args, vec!["rm", "--volumes", "test-container"]);
248    }
249
250    #[test]
251    fn test_rm_with_all_options() {
252        let cmd = RmCommand::new("test-container").force().volumes().link();
253        let args = cmd.build_command_args();
254        assert_eq!(
255            args,
256            vec!["rm", "--force", "--volumes", "--link", "test-container"]
257        );
258    }
259
260    #[test]
261    fn test_rm_builder_chain() {
262        let cmd = RmCommand::new("container1")
263            .container("container2")
264            .container("container3")
265            .force()
266            .volumes();
267        let args = cmd.build_command_args();
268        assert_eq!(
269            args,
270            vec![
271                "rm",
272                "--force",
273                "--volumes",
274                "container1",
275                "container2",
276                "container3"
277            ]
278        );
279    }
280}