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