docker_wrapper/command/
kill.rs

1//! Docker kill command implementation.
2//!
3//! This module provides the `docker kill` command for sending signals to running containers.
4//!
5//! # Example
6//!
7//! ```rust,no_run
8//! use docker_wrapper::{DockerCommand, KillCommand, RunCommand};
9//!
10//! # #[tokio::main]
11//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
12//! // Start a long-running container
13//! let output = RunCommand::new("alpine:latest")
14//!     .name("test-kill")
15//!     .detach()
16//!     .cmd(vec!["sleep".to_string(), "300".to_string()])
17//!     .execute()
18//!     .await?;
19//!
20//! // Kill the container with default signal (SIGKILL)
21//! KillCommand::new("test-kill")
22//!     .execute()
23//!     .await?;
24//!
25//! // You can also send specific signals
26//! let output2 = RunCommand::new("nginx:alpine")
27//!     .name("test-nginx")
28//!     .detach()
29//!     .execute()
30//!     .await?;
31//!
32//! // Gracefully stop with SIGTERM
33//! KillCommand::new("test-nginx")
34//!     .signal("SIGTERM")
35//!     .execute()
36//!     .await?;
37//!
38//! // Kill multiple containers at once
39//! KillCommand::new_multiple(vec!["container1", "container2"])
40//!     .signal("9")  // Numeric signal
41//!     .execute()
42//!     .await?;
43//! # Ok(())
44//! # }
45//! ```
46
47use super::{CommandExecutor, CommandOutput, DockerCommand};
48use crate::error::{Error, Result};
49use async_trait::async_trait;
50
51/// Docker kill command builder
52#[derive(Debug, Clone)]
53pub struct KillCommand {
54    /// Container names or IDs to kill
55    containers: Vec<String>,
56    /// Signal to send (default: SIGKILL)
57    signal: Option<String>,
58    /// Command executor
59    pub executor: CommandExecutor,
60}
61
62impl KillCommand {
63    /// Create a new kill command for a single container
64    #[must_use]
65    pub fn new(container: impl Into<String>) -> Self {
66        Self {
67            containers: vec![container.into()],
68            signal: None,
69            executor: CommandExecutor::new(),
70        }
71    }
72
73    /// Create a new kill command for multiple containers
74    #[must_use]
75    pub fn new_multiple(containers: Vec<impl Into<String>>) -> Self {
76        Self {
77            containers: containers.into_iter().map(Into::into).collect(),
78            signal: None,
79            executor: CommandExecutor::new(),
80        }
81    }
82
83    /// Add another container to kill
84    #[must_use]
85    pub fn container(mut self, container: impl Into<String>) -> Self {
86        self.containers.push(container.into());
87        self
88    }
89
90    /// Set the signal to send (e.g., "SIGTERM", "SIGKILL", "9")
91    #[must_use]
92    pub fn signal(mut self, signal: impl Into<String>) -> Self {
93        self.signal = Some(signal.into());
94        self
95    }
96
97    /// Execute the kill command
98    ///
99    /// # Errors
100    /// Returns an error if:
101    /// - No containers are specified
102    /// - The Docker daemon is not running
103    /// - Any of the specified containers don't exist
104    /// - The signal is invalid
105    pub async fn run(&self) -> Result<KillResult> {
106        let output = self.execute().await?;
107
108        // Parse killed container IDs from output
109        let killed: Vec<String> = output
110            .stdout
111            .lines()
112            .filter(|line| !line.is_empty())
113            .map(String::from)
114            .collect();
115
116        Ok(KillResult {
117            killed,
118            signal: self.signal.clone(),
119            output,
120        })
121    }
122}
123
124#[async_trait]
125impl DockerCommand for KillCommand {
126    type Output = CommandOutput;
127
128    fn build_command_args(&self) -> Vec<String> {
129        let mut args = vec!["kill".to_string()];
130
131        if let Some(ref sig) = self.signal {
132            args.push("--signal".to_string());
133            args.push(sig.clone());
134        }
135
136        // Add container names/IDs
137        args.extend(self.containers.clone());
138
139        args.extend(self.executor.raw_args.clone());
140        args
141    }
142
143    fn get_executor(&self) -> &CommandExecutor {
144        &self.executor
145    }
146
147    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
148        &mut self.executor
149    }
150
151    async fn execute(&self) -> Result<Self::Output> {
152        if self.containers.is_empty() {
153            return Err(Error::invalid_config("No containers specified for kill"));
154        }
155
156        let args = self.build_command_args();
157        let command_name = args[0].clone();
158        let command_args = args[1..].to_vec();
159        self.executor
160            .execute_command(&command_name, command_args)
161            .await
162    }
163}
164
165/// Result from the kill command
166#[derive(Debug, Clone)]
167pub struct KillResult {
168    /// List of killed container IDs
169    pub killed: Vec<String>,
170    /// Signal that was sent
171    pub signal: Option<String>,
172    /// Raw command output
173    pub output: CommandOutput,
174}
175
176impl KillResult {
177    /// Check if all containers were killed successfully
178    #[must_use]
179    pub fn all_killed(&self) -> bool {
180        self.output.success
181    }
182
183    /// Get the number of containers killed
184    #[must_use]
185    pub fn count(&self) -> usize {
186        self.killed.len()
187    }
188
189    /// Get the signal that was sent
190    #[must_use]
191    pub fn signal_sent(&self) -> &str {
192        self.signal.as_deref().unwrap_or("SIGKILL")
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_kill_single_container() {
202        let cmd = KillCommand::new("test-container");
203        let args = cmd.build_command_args();
204        assert_eq!(args, vec!["kill", "test-container"]);
205    }
206
207    #[test]
208    fn test_kill_multiple_containers() {
209        let cmd = KillCommand::new_multiple(vec!["container1", "container2", "container3"]);
210        let args = cmd.build_command_args();
211        assert_eq!(args, vec!["kill", "container1", "container2", "container3"]);
212    }
213
214    #[test]
215    fn test_kill_with_signal() {
216        let cmd = KillCommand::new("test-container").signal("SIGTERM");
217        let args = cmd.build_command_args();
218        assert_eq!(args, vec!["kill", "--signal", "SIGTERM", "test-container"]);
219    }
220
221    #[test]
222    fn test_kill_with_numeric_signal() {
223        let cmd = KillCommand::new("test-container").signal("9");
224        let args = cmd.build_command_args();
225        assert_eq!(args, vec!["kill", "--signal", "9", "test-container"]);
226    }
227
228    #[test]
229    fn test_kill_builder_chain() {
230        let cmd = KillCommand::new("container1")
231            .container("container2")
232            .container("container3")
233            .signal("SIGTERM");
234        let args = cmd.build_command_args();
235        assert_eq!(
236            args,
237            vec![
238                "kill",
239                "--signal",
240                "SIGTERM",
241                "container1",
242                "container2",
243                "container3"
244            ]
245        );
246    }
247}