docker_wrapper/command/
compose_exec.rs1use super::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use std::collections::HashMap;
7
8#[derive(Debug, Clone)]
10#[allow(clippy::struct_excessive_bools)] pub struct ComposeExecCommand {
12 pub executor: CommandExecutor,
14 pub config: ComposeConfig,
16 pub service: String,
18 pub command: Vec<String>,
20 pub detach: bool,
22 pub no_tty: bool,
24 pub interactive: bool,
26 pub user: Option<String>,
28 pub workdir: Option<String>,
30 pub env: HashMap<String, String>,
32 pub index: Option<u32>,
34 pub privileged: bool,
36}
37
38#[derive(Debug, Clone)]
40pub struct ComposeExecResult {
41 pub stdout: String,
43 pub stderr: String,
45 pub success: bool,
47 pub exit_code: i32,
49 pub service: String,
51 pub detached: bool,
53}
54
55impl ComposeExecCommand {
56 #[must_use]
58 pub fn new(service: impl Into<String>) -> Self {
59 Self {
60 executor: CommandExecutor::new(),
61 config: ComposeConfig::new(),
62 service: service.into(),
63 command: Vec::new(),
64 detach: false,
65 no_tty: false,
66 interactive: false,
67 user: None,
68 workdir: None,
69 env: HashMap::new(),
70 index: None,
71 privileged: false,
72 }
73 }
74
75 #[must_use]
77 pub fn cmd<I, S>(mut self, command: I) -> Self
78 where
79 I: IntoIterator<Item = S>,
80 S: Into<String>,
81 {
82 self.command = command.into_iter().map(Into::into).collect();
83 self
84 }
85
86 #[must_use]
88 pub fn arg(mut self, arg: impl Into<String>) -> Self {
89 self.command.push(arg.into());
90 self
91 }
92
93 #[must_use]
95 pub fn args<I, S>(mut self, args: I) -> Self
96 where
97 I: IntoIterator<Item = S>,
98 S: Into<String>,
99 {
100 self.command.extend(args.into_iter().map(Into::into));
101 self
102 }
103
104 #[must_use]
106 pub fn detach(mut self) -> Self {
107 self.detach = true;
108 self
109 }
110
111 #[must_use]
113 pub fn no_tty(mut self) -> Self {
114 self.no_tty = true;
115 self
116 }
117
118 #[must_use]
120 pub fn interactive(mut self) -> Self {
121 self.interactive = true;
122 self
123 }
124
125 #[must_use]
127 pub fn user(mut self, user: impl Into<String>) -> Self {
128 self.user = Some(user.into());
129 self
130 }
131
132 #[must_use]
134 pub fn workdir(mut self, workdir: impl Into<String>) -> Self {
135 self.workdir = Some(workdir.into());
136 self
137 }
138
139 #[must_use]
141 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
142 self.env.insert(key.into(), value.into());
143 self
144 }
145
146 #[must_use]
148 pub fn envs(mut self, env_vars: HashMap<String, String>) -> Self {
149 self.env.extend(env_vars);
150 self
151 }
152
153 #[must_use]
155 pub fn index(mut self, index: u32) -> Self {
156 self.index = Some(index);
157 self
158 }
159
160 #[must_use]
162 pub fn privileged(mut self) -> Self {
163 self.privileged = true;
164 self
165 }
166}
167
168#[async_trait]
169impl DockerCommand for ComposeExecCommand {
170 type Output = ComposeExecResult;
171
172 fn get_executor(&self) -> &CommandExecutor {
173 &self.executor
174 }
175
176 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
177 &mut self.executor
178 }
179
180 fn build_command_args(&self) -> Vec<String> {
181 <Self as ComposeCommand>::build_command_args(self)
183 }
184
185 async fn execute(&self) -> Result<Self::Output> {
186 let args = <Self as ComposeCommand>::build_command_args(self);
187 let output = self.execute_command(args).await?;
188
189 Ok(ComposeExecResult {
190 stdout: output.stdout,
191 stderr: output.stderr,
192 success: output.success,
193 exit_code: output.exit_code,
194 service: self.service.clone(),
195 detached: self.detach,
196 })
197 }
198}
199
200impl ComposeCommand for ComposeExecCommand {
201 fn get_config(&self) -> &ComposeConfig {
202 &self.config
203 }
204
205 fn get_config_mut(&mut self) -> &mut ComposeConfig {
206 &mut self.config
207 }
208
209 fn subcommand(&self) -> &'static str {
210 "exec"
211 }
212
213 fn build_subcommand_args(&self) -> Vec<String> {
214 let mut args = Vec::new();
215
216 if self.detach {
217 args.push("--detach".to_string());
218 }
219
220 if self.no_tty {
221 args.push("--no-TTY".to_string());
222 }
223
224 if self.interactive {
225 args.push("--interactive".to_string());
226 }
227
228 if let Some(ref user) = self.user {
230 args.push("--user".to_string());
231 args.push(user.clone());
232 }
233
234 if let Some(ref workdir) = self.workdir {
236 args.push("--workdir".to_string());
237 args.push(workdir.clone());
238 }
239
240 for (key, value) in &self.env {
242 args.push("--env".to_string());
243 args.push(format!("{key}={value}"));
244 }
245
246 if let Some(index) = self.index {
248 args.push("--index".to_string());
249 args.push(index.to_string());
250 }
251
252 if self.privileged {
253 args.push("--privileged".to_string());
254 }
255
256 args.push(self.service.clone());
258
259 args.extend(self.command.clone());
261
262 args
263 }
264}
265
266impl ComposeExecResult {
267 #[must_use]
269 pub fn success(&self) -> bool {
270 self.success
271 }
272
273 #[must_use]
275 pub fn exit_code(&self) -> i32 {
276 self.exit_code
277 }
278
279 #[must_use]
281 pub fn service(&self) -> &str {
282 &self.service
283 }
284
285 #[must_use]
287 pub fn is_detached(&self) -> bool {
288 self.detached
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn test_compose_exec_basic() {
298 let cmd = ComposeExecCommand::new("web");
299 let args = cmd.build_subcommand_args();
300 assert!(args.contains(&"web".to_string()));
301
302 let full_args = ComposeCommand::build_command_args(&cmd);
303 assert_eq!(full_args[0], "compose");
304 assert!(full_args.contains(&"exec".to_string()));
305 assert!(full_args.contains(&"web".to_string()));
306 }
307
308 #[test]
309 fn test_compose_exec_with_command() {
310 let cmd = ComposeExecCommand::new("db").cmd(vec!["psql", "-U", "postgres"]);
311
312 let args = cmd.build_subcommand_args();
313 assert!(args.contains(&"db".to_string()));
314 assert!(args.contains(&"psql".to_string()));
315 assert!(args.contains(&"-U".to_string()));
316 assert!(args.contains(&"postgres".to_string()));
317 }
318
319 #[test]
320 fn test_compose_exec_with_flags() {
321 let cmd = ComposeExecCommand::new("app")
322 .detach()
323 .no_tty()
324 .interactive()
325 .privileged();
326
327 let args = cmd.build_subcommand_args();
328 assert!(args.contains(&"--detach".to_string()));
329 assert!(args.contains(&"--no-TTY".to_string()));
330 assert!(args.contains(&"--interactive".to_string()));
331 assert!(args.contains(&"--privileged".to_string()));
332 }
333
334 #[test]
335 fn test_compose_exec_with_user_and_workdir() {
336 let cmd = ComposeExecCommand::new("web")
337 .user("root")
338 .workdir("/app")
339 .cmd(vec!["bash"]);
340
341 let args = cmd.build_subcommand_args();
342 assert!(args.contains(&"--user".to_string()));
343 assert!(args.contains(&"root".to_string()));
344 assert!(args.contains(&"--workdir".to_string()));
345 assert!(args.contains(&"/app".to_string()));
346 assert!(args.contains(&"web".to_string()));
347 assert!(args.contains(&"bash".to_string()));
348 }
349
350 #[test]
351 fn test_compose_exec_with_env_vars() {
352 let cmd = ComposeExecCommand::new("worker")
353 .env("DEBUG", "1")
354 .env("NODE_ENV", "development")
355 .cmd(vec!["npm", "test"]);
356
357 let args = cmd.build_subcommand_args();
358 assert!(args.contains(&"--env".to_string()));
359 assert!(args.contains(&"DEBUG=1".to_string()));
360 assert!(args.contains(&"NODE_ENV=development".to_string()));
361 }
362
363 #[test]
364 fn test_compose_exec_with_index() {
365 let cmd = ComposeExecCommand::new("web")
366 .index(2)
367 .cmd(vec!["ps", "aux"]);
368
369 let args = cmd.build_subcommand_args();
370 assert!(args.contains(&"--index".to_string()));
371 assert!(args.contains(&"2".to_string()));
372 assert!(args.contains(&"web".to_string()));
373 assert!(args.contains(&"ps".to_string()));
374 assert!(args.contains(&"aux".to_string()));
375 }
376
377 #[test]
378 fn test_compose_exec_all_options() {
379 let cmd = ComposeExecCommand::new("api")
380 .detach()
381 .user("www-data")
382 .workdir("/var/www")
383 .env("PHP_ENV", "production")
384 .index(1)
385 .privileged()
386 .cmd(vec!["php", "-v"]);
387
388 let args = cmd.build_subcommand_args();
389
390 assert!(args.contains(&"--detach".to_string()));
392 assert!(args.contains(&"--privileged".to_string()));
393
394 assert!(args.contains(&"--user".to_string()));
396 assert!(args.contains(&"www-data".to_string()));
397 assert!(args.contains(&"--workdir".to_string()));
398 assert!(args.contains(&"/var/www".to_string()));
399 assert!(args.contains(&"--env".to_string()));
400 assert!(args.contains(&"PHP_ENV=production".to_string()));
401 assert!(args.contains(&"--index".to_string()));
402 assert!(args.contains(&"1".to_string()));
403
404 assert!(args.contains(&"api".to_string()));
406 assert!(args.contains(&"php".to_string()));
407 assert!(args.contains(&"-v".to_string()));
408 }
409
410 #[test]
411 fn test_compose_config_integration() {
412 let cmd = ComposeExecCommand::new("database")
413 .file("docker-compose.yml")
414 .project_name("my-project")
415 .user("postgres")
416 .cmd(vec!["psql", "-c", "SELECT 1"]);
417
418 let args = ComposeCommand::build_command_args(&cmd);
419 assert!(args.contains(&"--file".to_string()));
420 assert!(args.contains(&"docker-compose.yml".to_string()));
421 assert!(args.contains(&"--project-name".to_string()));
422 assert!(args.contains(&"my-project".to_string()));
423 assert!(args.contains(&"--user".to_string()));
424 assert!(args.contains(&"postgres".to_string()));
425 assert!(args.contains(&"database".to_string()));
426 assert!(args.contains(&"psql".to_string()));
427 }
428}