docker_wrapper/command/
start.rs

1//! Docker start command implementation.
2//!
3//! This module provides a comprehensive implementation of the `docker start` command
4//! with support for all native options and an extensible architecture.
5
6use super::{CommandExecutor, DockerCommand};
7use crate::error::{Error, Result};
8use async_trait::async_trait;
9
10/// Docker start command builder with fluent API
11#[derive(Debug, Clone)]
12pub struct StartCommand {
13    /// Command executor for extensibility
14    pub executor: CommandExecutor,
15    /// Container IDs or names to start
16    containers: Vec<String>,
17    /// Attach STDOUT/STDERR and forward signals
18    attach: bool,
19    /// Restore from this checkpoint
20    checkpoint: Option<String>,
21    /// Use a custom checkpoint storage directory
22    checkpoint_dir: Option<String>,
23    /// Override the key sequence for detaching a container
24    detach_keys: Option<String>,
25    /// Attach container's STDIN
26    interactive: bool,
27}
28
29/// Result of a start command execution
30#[derive(Debug, Clone, PartialEq)]
31pub struct StartResult {
32    /// Raw stdout from the command
33    pub stdout: String,
34    /// Raw stderr from the command
35    pub stderr: String,
36    /// Container IDs that were started
37    pub started_containers: Vec<String>,
38}
39
40impl StartCommand {
41    /// Create a new start command for the specified container(s)
42    ///
43    /// # Examples
44    ///
45    /// ```
46    /// use docker_wrapper::StartCommand;
47    ///
48    /// let cmd = StartCommand::new("my-container");
49    /// ```
50    ///
51    /// ```
52    /// use docker_wrapper::StartCommand;
53    ///
54    /// let cmd = StartCommand::new_multiple(vec!["container1", "container2"]);
55    /// ```
56    pub fn new(container: impl Into<String>) -> Self {
57        Self {
58            executor: CommandExecutor::new(),
59            containers: vec![container.into()],
60            attach: false,
61            checkpoint: None,
62            checkpoint_dir: None,
63            detach_keys: None,
64            interactive: false,
65        }
66    }
67
68    /// Create a new start command for multiple containers
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// use docker_wrapper::StartCommand;
74    ///
75    /// let cmd = StartCommand::new_multiple(vec!["container1", "container2"]);
76    /// ```
77    pub fn new_multiple<I, S>(containers: I) -> Self
78    where
79        I: IntoIterator<Item = S>,
80        S: Into<String>,
81    {
82        Self {
83            executor: CommandExecutor::new(),
84            containers: containers.into_iter().map(Into::into).collect(),
85            attach: false,
86            checkpoint: None,
87            checkpoint_dir: None,
88            detach_keys: None,
89            interactive: false,
90        }
91    }
92
93    /// Attach STDOUT/STDERR and forward signals
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// use docker_wrapper::StartCommand;
99    ///
100    /// let cmd = StartCommand::new("my-container")
101    ///     .attach();
102    /// ```
103    #[must_use]
104    pub fn attach(mut self) -> Self {
105        self.attach = true;
106        self
107    }
108
109    /// Restore from this checkpoint
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use docker_wrapper::StartCommand;
115    ///
116    /// let cmd = StartCommand::new("my-container")
117    ///     .checkpoint("checkpoint1");
118    /// ```
119    #[must_use]
120    pub fn checkpoint(mut self, checkpoint: impl Into<String>) -> Self {
121        self.checkpoint = Some(checkpoint.into());
122        self
123    }
124
125    /// Use a custom checkpoint storage directory
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// use docker_wrapper::StartCommand;
131    ///
132    /// let cmd = StartCommand::new("my-container")
133    ///     .checkpoint_dir("/custom/checkpoint/dir");
134    /// ```
135    #[must_use]
136    pub fn checkpoint_dir(mut self, dir: impl Into<String>) -> Self {
137        self.checkpoint_dir = Some(dir.into());
138        self
139    }
140
141    /// Override the key sequence for detaching a container
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// use docker_wrapper::StartCommand;
147    ///
148    /// let cmd = StartCommand::new("my-container")
149    ///     .detach_keys("ctrl-p,ctrl-q");
150    /// ```
151    #[must_use]
152    pub fn detach_keys(mut self, keys: impl Into<String>) -> Self {
153        self.detach_keys = Some(keys.into());
154        self
155    }
156
157    /// Attach container's STDIN
158    ///
159    /// # Examples
160    ///
161    /// ```
162    /// use docker_wrapper::StartCommand;
163    ///
164    /// let cmd = StartCommand::new("my-container")
165    ///     .interactive();
166    /// ```
167    #[must_use]
168    pub fn interactive(mut self) -> Self {
169        self.interactive = true;
170        self
171    }
172
173    /// Convenience method for interactive + attach mode
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// use docker_wrapper::StartCommand;
179    ///
180    /// let cmd = StartCommand::new("my-container")
181    ///     .ai(); // attach + interactive
182    /// ```
183    #[must_use]
184    pub fn ai(self) -> Self {
185        self.attach().interactive()
186    }
187}
188
189#[async_trait]
190impl DockerCommand for StartCommand {
191    type Output = StartResult;
192
193    fn get_executor(&self) -> &CommandExecutor {
194        &self.executor
195    }
196
197    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
198        &mut self.executor
199    }
200
201    fn build_command_args(&self) -> Vec<String> {
202        let mut args = vec!["start".to_string()];
203
204        // Add attach option
205        if self.attach {
206            args.push("--attach".to_string());
207        }
208
209        // Add checkpoint option
210        if let Some(checkpoint) = &self.checkpoint {
211            args.push("--checkpoint".to_string());
212            args.push(checkpoint.clone());
213        }
214
215        // Add checkpoint-dir option
216        if let Some(checkpoint_dir) = &self.checkpoint_dir {
217            args.push("--checkpoint-dir".to_string());
218            args.push(checkpoint_dir.clone());
219        }
220
221        // Add detach-keys option
222        if let Some(detach_keys) = &self.detach_keys {
223            args.push("--detach-keys".to_string());
224            args.push(detach_keys.clone());
225        }
226
227        // Add interactive option
228        if self.interactive {
229            args.push("--interactive".to_string());
230        }
231
232        // Add container names/IDs
233        args.extend(self.containers.clone());
234
235        // Add raw arguments from executor
236        args.extend(self.executor.raw_args.clone());
237
238        args
239    }
240
241    async fn execute(&self) -> Result<Self::Output> {
242        if self.containers.is_empty() {
243            return Err(Error::invalid_config("No containers specified"));
244        }
245
246        let args = self.build_command_args();
247        let output = self.execute_command(args).await?;
248
249        // Parse the output to extract started container IDs
250        let started_containers = if output.stdout.trim().is_empty() {
251            // If no stdout, assume the containers specified were started
252            self.containers.clone()
253        } else {
254            // Parse container IDs from stdout (each line is a container ID)
255            output
256                .stdout
257                .lines()
258                .filter(|line| !line.trim().is_empty())
259                .map(|line| line.trim().to_string())
260                .collect()
261        };
262
263        Ok(StartResult {
264            stdout: output.stdout,
265            stderr: output.stderr,
266            started_containers,
267        })
268    }
269}
270
271impl StartCommand {
272    /// Get the command arguments (for testing)
273    #[must_use]
274    pub fn args(&self) -> Vec<String> {
275        self.build_command_args()
276    }
277}
278
279impl StartResult {
280    /// Check if the command was successful
281    #[must_use]
282    pub fn is_success(&self) -> bool {
283        !self.started_containers.is_empty()
284    }
285
286    /// Get the number of containers that were started
287    #[must_use]
288    pub fn container_count(&self) -> usize {
289        self.started_containers.len()
290    }
291
292    /// Get the first started container ID (useful for single container operations)
293    #[must_use]
294    pub fn first_container(&self) -> Option<&String> {
295        self.started_containers.first()
296    }
297
298    /// Check if a specific container was started
299    #[must_use]
300    pub fn contains_container(&self, container: &str) -> bool {
301        self.started_containers.iter().any(|c| c == container)
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn test_start_command_new() {
311        let cmd = StartCommand::new("test-container");
312        assert_eq!(cmd.containers, vec!["test-container"]);
313        assert!(!cmd.attach);
314        assert!(cmd.checkpoint.is_none());
315        assert!(cmd.checkpoint_dir.is_none());
316        assert!(cmd.detach_keys.is_none());
317        assert!(!cmd.interactive);
318    }
319
320    #[test]
321    fn test_start_command_new_multiple() {
322        let cmd = StartCommand::new_multiple(vec!["container1", "container2"]);
323        assert_eq!(cmd.containers, vec!["container1", "container2"]);
324    }
325
326    #[test]
327    fn test_start_command_with_attach() {
328        let cmd = StartCommand::new("test-container").attach();
329        assert!(cmd.attach);
330    }
331
332    #[test]
333    fn test_start_command_with_checkpoint() {
334        let cmd = StartCommand::new("test-container").checkpoint("checkpoint1");
335        assert_eq!(cmd.checkpoint, Some("checkpoint1".to_string()));
336    }
337
338    #[test]
339    fn test_start_command_with_checkpoint_dir() {
340        let cmd = StartCommand::new("test-container").checkpoint_dir("/custom/dir");
341        assert_eq!(cmd.checkpoint_dir, Some("/custom/dir".to_string()));
342    }
343
344    #[test]
345    fn test_start_command_with_detach_keys() {
346        let cmd = StartCommand::new("test-container").detach_keys("ctrl-p,ctrl-q");
347        assert_eq!(cmd.detach_keys, Some("ctrl-p,ctrl-q".to_string()));
348    }
349
350    #[test]
351    fn test_start_command_with_interactive() {
352        let cmd = StartCommand::new("test-container").interactive();
353        assert!(cmd.interactive);
354    }
355
356    #[test]
357    fn test_start_command_ai_convenience() {
358        let cmd = StartCommand::new("test-container").ai();
359        assert!(cmd.attach);
360        assert!(cmd.interactive);
361    }
362
363    #[test]
364    fn test_start_command_args_basic() {
365        let cmd = StartCommand::new("test-container");
366        let args = cmd.args();
367        assert_eq!(args, vec!["start", "test-container"]);
368    }
369
370    #[test]
371    fn test_start_command_args_with_options() {
372        let cmd = StartCommand::new("test-container")
373            .attach()
374            .interactive()
375            .checkpoint("checkpoint1");
376        let args = cmd.args();
377        assert_eq!(
378            args,
379            vec![
380                "start",
381                "--attach",
382                "--checkpoint",
383                "checkpoint1",
384                "--interactive",
385                "test-container"
386            ]
387        );
388    }
389
390    #[test]
391    fn test_start_command_args_multiple_containers() {
392        let cmd =
393            StartCommand::new_multiple(vec!["container1", "container2"]).detach_keys("ctrl-c");
394        let args = cmd.args();
395        assert_eq!(
396            args,
397            vec![
398                "start",
399                "--detach-keys",
400                "ctrl-c",
401                "container1",
402                "container2"
403            ]
404        );
405    }
406
407    #[test]
408    fn test_start_result_is_success() {
409        let result = StartResult {
410            stdout: "container1\n".to_string(),
411            stderr: String::new(),
412            started_containers: vec!["container1".to_string()],
413        };
414        assert!(result.is_success());
415
416        let empty_result = StartResult {
417            stdout: String::new(),
418            stderr: String::new(),
419            started_containers: vec![],
420        };
421        assert!(!empty_result.is_success());
422    }
423
424    #[test]
425    fn test_start_result_container_count() {
426        let result = StartResult {
427            stdout: String::new(),
428            stderr: String::new(),
429            started_containers: vec!["container1".to_string(), "container2".to_string()],
430        };
431        assert_eq!(result.container_count(), 2);
432    }
433
434    #[test]
435    fn test_start_result_first_container() {
436        let result = StartResult {
437            stdout: String::new(),
438            stderr: String::new(),
439            started_containers: vec!["container1".to_string(), "container2".to_string()],
440        };
441        assert_eq!(result.first_container(), Some(&"container1".to_string()));
442
443        let empty_result = StartResult {
444            stdout: String::new(),
445            stderr: String::new(),
446            started_containers: vec![],
447        };
448        assert_eq!(empty_result.first_container(), None);
449    }
450
451    #[test]
452    fn test_start_result_contains_container() {
453        let result = StartResult {
454            stdout: String::new(),
455            stderr: String::new(),
456            started_containers: vec!["container1".to_string(), "container2".to_string()],
457        };
458        assert!(result.contains_container("container1"));
459        assert!(result.contains_container("container2"));
460        assert!(!result.contains_container("container3"));
461    }
462
463    #[test]
464    fn test_command_name() {
465        let cmd = StartCommand::new("test");
466        let args = cmd.build_command_args();
467        assert_eq!(args[0], "start");
468    }
469}