docker_wrapper/compose/
attach.rs

1//! Docker Compose attach command implementation.
2
3use crate::compose::{ComposeCommandV2 as ComposeCommand, ComposeConfig};
4use crate::error::Result;
5use async_trait::async_trait;
6
7/// Docker Compose attach command
8///
9/// Attach to a running container's output.
10#[derive(Debug, Clone, Default)]
11pub struct ComposeAttachCommand {
12    /// Base configuration
13    pub config: ComposeConfig,
14    /// Service to attach to
15    pub service: String,
16    /// Detach keys sequence
17    pub detach_keys: Option<String>,
18    /// Container index if service has multiple instances
19    pub index: Option<u32>,
20    /// Don't stream STDIN
21    pub no_stdin: bool,
22    /// Use a pseudo-TTY
23    pub sig_proxy: bool,
24}
25
26/// Result from attach command
27#[derive(Debug, Clone)]
28pub struct AttachResult {
29    /// Output from the command
30    pub output: String,
31    /// Whether the operation succeeded
32    pub success: bool,
33}
34
35impl ComposeAttachCommand {
36    /// Create a new attach command
37    #[must_use]
38    pub fn new(service: impl Into<String>) -> Self {
39        Self {
40            service: service.into(),
41            sig_proxy: true, // Default to true
42            ..Default::default()
43        }
44    }
45
46    /// Add a compose file
47    #[must_use]
48    pub fn file<P: Into<std::path::PathBuf>>(mut self, file: P) -> Self {
49        self.config.files.push(file.into());
50        self
51    }
52
53    /// Set project name
54    #[must_use]
55    pub fn project_name(mut self, name: impl Into<String>) -> Self {
56        self.config.project_name = Some(name.into());
57        self
58    }
59
60    /// Set detach keys
61    #[must_use]
62    pub fn detach_keys(mut self, keys: impl Into<String>) -> Self {
63        self.detach_keys = Some(keys.into());
64        self
65    }
66
67    /// Set container index
68    #[must_use]
69    pub fn index(mut self, index: u32) -> Self {
70        self.index = Some(index);
71        self
72    }
73
74    /// Don't attach to STDIN
75    #[must_use]
76    pub fn no_stdin(mut self) -> Self {
77        self.no_stdin = true;
78        self
79    }
80
81    /// Disable signal proxy
82    #[must_use]
83    pub fn no_sig_proxy(mut self) -> Self {
84        self.sig_proxy = false;
85        self
86    }
87
88    fn build_args(&self) -> Vec<String> {
89        let mut args = vec!["attach".to_string()];
90
91        // Add flags
92        if let Some(ref keys) = self.detach_keys {
93            args.push("--detach-keys".to_string());
94            args.push(keys.clone());
95        }
96
97        if let Some(index) = self.index {
98            args.push("--index".to_string());
99            args.push(index.to_string());
100        }
101
102        if self.no_stdin {
103            args.push("--no-stdin".to_string());
104        }
105
106        if !self.sig_proxy {
107            args.push("--sig-proxy=false".to_string());
108        }
109
110        // Add service
111        args.push(self.service.clone());
112
113        args
114    }
115}
116
117#[async_trait]
118impl ComposeCommand for ComposeAttachCommand {
119    type Output = AttachResult;
120
121    fn get_config(&self) -> &ComposeConfig {
122        &self.config
123    }
124
125    fn get_config_mut(&mut self) -> &mut ComposeConfig {
126        &mut self.config
127    }
128
129    async fn execute_compose(&self, args: Vec<String>) -> Result<Self::Output> {
130        let output = self.execute_compose_command(args).await?;
131
132        Ok(AttachResult {
133            output: output.stdout,
134            success: output.success,
135        })
136    }
137
138    async fn execute(&self) -> Result<Self::Output> {
139        let args = self.build_args();
140        self.execute_compose(args).await
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_attach_command_basic() {
150        let cmd = ComposeAttachCommand::new("web");
151        let args = cmd.build_args();
152        assert_eq!(args[0], "attach");
153        assert!(args.contains(&"web".to_string()));
154    }
155
156    #[test]
157    fn test_attach_command_with_detach_keys() {
158        let cmd = ComposeAttachCommand::new("web").detach_keys("ctrl-p,ctrl-q");
159        let args = cmd.build_args();
160        assert!(args.contains(&"--detach-keys".to_string()));
161        assert!(args.contains(&"ctrl-p,ctrl-q".to_string()));
162    }
163
164    #[test]
165    fn test_attach_command_with_index() {
166        let cmd = ComposeAttachCommand::new("web").index(2).no_stdin();
167        let args = cmd.build_args();
168        assert!(args.contains(&"--index".to_string()));
169        assert!(args.contains(&"2".to_string()));
170        assert!(args.contains(&"--no-stdin".to_string()));
171    }
172
173    #[test]
174    fn test_attach_command_with_no_sig_proxy() {
175        let cmd = ComposeAttachCommand::new("worker").no_sig_proxy();
176        let args = cmd.build_args();
177        assert!(args.contains(&"--sig-proxy=false".to_string()));
178    }
179}