docker_wrapper/command/
compose_attach.rs

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