docker_wrapper/command/
exec.rs1use super::{CommandExecutor, DockerCommand, EnvironmentBuilder};
7use crate::error::Result;
8use async_trait::async_trait;
9use std::path::PathBuf;
10
11#[derive(Debug, Clone)]
13#[allow(clippy::struct_excessive_bools)]
14pub struct ExecCommand {
15 container: String,
17 command: Vec<String>,
19 pub executor: CommandExecutor,
21 detach: bool,
23 detach_keys: Option<String>,
25 environment: EnvironmentBuilder,
27 env_files: Vec<String>,
29 interactive: bool,
31 privileged: bool,
33 tty: bool,
35 user: Option<String>,
37 workdir: Option<PathBuf>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ExecOutput {
44 pub stdout: String,
46 pub stderr: String,
48 pub exit_code: i32,
50}
51
52impl ExecOutput {
53 #[must_use]
55 pub fn success(&self) -> bool {
56 self.exit_code == 0
57 }
58
59 #[must_use]
61 pub fn combined_output(&self) -> String {
62 if self.stderr.is_empty() {
63 self.stdout.clone()
64 } else if self.stdout.is_empty() {
65 self.stderr.clone()
66 } else {
67 format!("{}\n{}", self.stdout, self.stderr)
68 }
69 }
70
71 #[must_use]
73 pub fn stdout_is_empty(&self) -> bool {
74 self.stdout.trim().is_empty()
75 }
76
77 #[must_use]
79 pub fn stderr_is_empty(&self) -> bool {
80 self.stderr.trim().is_empty()
81 }
82}
83
84impl ExecCommand {
85 pub fn new(container: impl Into<String>, command: Vec<String>) -> Self {
95 Self {
96 container: container.into(),
97 command,
98 executor: CommandExecutor::new(),
99 detach: false,
100 detach_keys: None,
101 environment: EnvironmentBuilder::new(),
102 env_files: Vec::new(),
103 interactive: false,
104 privileged: false,
105 tty: false,
106 user: None,
107 workdir: None,
108 }
109 }
110
111 #[must_use]
122 pub fn detach(mut self) -> Self {
123 self.detach = true;
124 self
125 }
126
127 #[must_use]
138 pub fn detach_keys(mut self, keys: impl Into<String>) -> Self {
139 self.detach_keys = Some(keys.into());
140 self
141 }
142
143 #[must_use]
155 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
156 self.environment = self.environment.var(key, value);
157 self
158 }
159
160 #[must_use]
176 pub fn envs(mut self, vars: std::collections::HashMap<String, String>) -> Self {
177 self.environment = self.environment.vars(vars);
178 self
179 }
180
181 #[must_use]
192 pub fn env_file(mut self, file: impl Into<String>) -> Self {
193 self.env_files.push(file.into());
194 self
195 }
196
197 #[must_use]
208 pub fn interactive(mut self) -> Self {
209 self.interactive = true;
210 self
211 }
212
213 #[must_use]
224 pub fn privileged(mut self) -> Self {
225 self.privileged = true;
226 self
227 }
228
229 #[must_use]
240 pub fn tty(mut self) -> Self {
241 self.tty = true;
242 self
243 }
244
245 #[must_use]
259 pub fn user(mut self, user: impl Into<String>) -> Self {
260 self.user = Some(user.into());
261 self
262 }
263
264 #[must_use]
275 pub fn workdir(mut self, workdir: impl Into<PathBuf>) -> Self {
276 self.workdir = Some(workdir.into());
277 self
278 }
279
280 #[must_use]
291 pub fn it(self) -> Self {
292 self.interactive().tty()
293 }
294}
295
296#[async_trait]
297impl DockerCommand for ExecCommand {
298 type Output = ExecOutput;
299
300 fn get_executor(&self) -> &CommandExecutor {
301 &self.executor
302 }
303
304 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
305 &mut self.executor
306 }
307
308 fn build_command_args(&self) -> Vec<String> {
309 let mut args = vec!["exec".to_string()];
310
311 if self.detach {
313 args.push("--detach".to_string());
314 }
315
316 if let Some(ref keys) = self.detach_keys {
317 args.push("--detach-keys".to_string());
318 args.push(keys.clone());
319 }
320
321 for (key, value) in self.environment.as_map() {
323 args.push("--env".to_string());
324 args.push(format!("{key}={value}"));
325 }
326
327 for env_file in &self.env_files {
329 args.push("--env-file".to_string());
330 args.push(env_file.clone());
331 }
332
333 if self.interactive {
334 args.push("--interactive".to_string());
335 }
336
337 if self.privileged {
338 args.push("--privileged".to_string());
339 }
340
341 if self.tty {
342 args.push("--tty".to_string());
343 }
344
345 if let Some(ref user) = self.user {
346 args.push("--user".to_string());
347 args.push(user.clone());
348 }
349
350 if let Some(ref workdir) = self.workdir {
351 args.push("--workdir".to_string());
352 args.push(workdir.to_string_lossy().to_string());
353 }
354
355 args.extend(self.executor.raw_args.clone());
357
358 args.push(self.container.clone());
360
361 args.extend(self.command.clone());
363
364 args
365 }
366
367 async fn execute(&self) -> Result<Self::Output> {
368 let args = self.build_command_args();
369 let output = self.execute_command(args).await?;
370
371 Ok(ExecOutput {
372 stdout: output.stdout,
373 stderr: output.stderr,
374 exit_code: output.exit_code,
375 })
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
384 fn test_exec_command_builder() {
385 let cmd = ExecCommand::new("test-container", vec!["ls".to_string(), "-la".to_string()])
386 .interactive()
387 .tty()
388 .env("DEBUG", "1")
389 .user("root")
390 .workdir("/app");
391
392 let args = cmd.build_command_args();
393
394 assert!(args.contains(&"--interactive".to_string()));
395 assert!(args.contains(&"--tty".to_string()));
396 assert!(args.contains(&"--env".to_string()));
397 assert!(args.contains(&"DEBUG=1".to_string()));
398 assert!(args.contains(&"--user".to_string()));
399 assert!(args.contains(&"root".to_string()));
400 assert!(args.contains(&"--workdir".to_string()));
401 assert!(args.contains(&"/app".to_string()));
402 assert!(args.contains(&"test-container".to_string()));
403 assert!(args.contains(&"ls".to_string()));
404 assert!(args.contains(&"-la".to_string()));
405 }
406
407 #[test]
408 fn test_exec_command_detach() {
409 let cmd = ExecCommand::new(
410 "test-container",
411 vec!["sleep".to_string(), "10".to_string()],
412 )
413 .detach()
414 .detach_keys("ctrl-p,ctrl-q");
415
416 let args = cmd.build_command_args();
417
418 assert!(args.contains(&"--detach".to_string()));
419 assert!(args.contains(&"--detach-keys".to_string()));
420 assert!(args.contains(&"ctrl-p,ctrl-q".to_string()));
421 }
422
423 #[test]
424 fn test_exec_command_privileged() {
425 let cmd = ExecCommand::new("test-container", vec!["mount".to_string()]).privileged();
426
427 let args = cmd.build_command_args();
428
429 assert!(args.contains(&"--privileged".to_string()));
430 }
431
432 #[test]
433 fn test_exec_command_env_file() {
434 let cmd = ExecCommand::new("test-container", vec!["env".to_string()])
435 .env_file("/path/to/env.file")
436 .env_file("/another/env.file");
437
438 let args = cmd.build_command_args();
439
440 assert!(args.contains(&"--env-file".to_string()));
441 assert!(args.contains(&"/path/to/env.file".to_string()));
442 assert!(args.contains(&"/another/env.file".to_string()));
443 }
444
445 #[test]
446 fn test_it_convenience_method() {
447 let cmd = ExecCommand::new("test-container", vec!["bash".to_string()]).it();
448
449 let args = cmd.build_command_args();
450
451 assert!(args.contains(&"--interactive".to_string()));
452 assert!(args.contains(&"--tty".to_string()));
453 }
454
455 #[test]
456 fn test_exec_output_helpers() {
457 let output_success = ExecOutput {
458 stdout: "Hello World".to_string(),
459 stderr: String::new(),
460 exit_code: 0,
461 };
462
463 assert!(output_success.success());
464 assert!(!output_success.stdout_is_empty());
465 assert!(output_success.stderr_is_empty());
466 assert_eq!(output_success.combined_output(), "Hello World");
467
468 let output_error = ExecOutput {
469 stdout: String::new(),
470 stderr: "Error occurred".to_string(),
471 exit_code: 1,
472 };
473
474 assert!(!output_error.success());
475 assert!(output_error.stdout_is_empty());
476 assert!(!output_error.stderr_is_empty());
477 assert_eq!(output_error.combined_output(), "Error occurred");
478
479 let output_combined = ExecOutput {
480 stdout: "Output".to_string(),
481 stderr: "Warning".to_string(),
482 exit_code: 0,
483 };
484
485 assert_eq!(output_combined.combined_output(), "Output\nWarning");
486 }
487
488 #[test]
489 fn test_exec_command_extensibility() {
490 let mut cmd = ExecCommand::new("test-container", vec!["test".to_string()]);
491
492 cmd.flag("--some-flag");
494 cmd.option("--some-option", "value");
495 cmd.arg("extra-arg");
496
497 let args = cmd.build_command_args();
498
499 assert!(args.contains(&"--some-flag".to_string()));
500 assert!(args.contains(&"--some-option".to_string()));
501 assert!(args.contains(&"value".to_string()));
502 assert!(args.contains(&"extra-arg".to_string()));
503 }
504}