1
use std::io::{
2
  BufRead as _,
3
  BufReader,
4
};
5
use std::process::Command as ProcessCommand;
6
use std::{
7
  env,
8
  thread,
9
};
10

            
11
use anyhow::Context as _;
12
use serde::Deserialize;
13
use which::which;
14

            
15
use crate::defaults::{
16
  default_ignore_errors,
17
  default_verbose,
18
};
19
use crate::file::ToUtf8 as _;
20
use crate::handle_output;
21
use crate::schema::{
22
  get_output_handler,
23
  TaskContext,
24
};
25

            
26
#[derive(Debug, Deserialize)]
27
pub struct ContainerRun {
28
  /// The command to run in the container
29
  pub container_command: Vec<String>,
30

            
31
  /// The container image to use
32
  pub image: String,
33

            
34
  /// The mounted paths to bind mount into the container
35
  #[serde(default)]
36
  pub mounted_paths: Vec<String>,
37

            
38
  /// Ignore errors if the command fails
39
  #[serde(default)]
40
  pub ignore_errors: Option<bool>,
41

            
42
  /// Show verbose output
43
  #[serde(default)]
44
  pub verbose: Option<bool>,
45
}
46

            
47
impl ContainerRun {
48
  pub fn execute(&self, context: &TaskContext) -> anyhow::Result<()> {
49
    assert!(!self.image.is_empty());
50
    assert!(!self.container_command.is_empty());
51

            
52
    let ignore_errors = self.ignore_errors(context);
53
    let verbose = self.verbose(context);
54

            
55
    let stdout = get_output_handler(verbose);
56
    let stderr = get_output_handler(verbose);
57

            
58
    let container_runtime = which("docker")
59
      .or_else(|_| which("podman"))
60
      .with_context(|| "Failed to find docker or podman")?;
61

            
62
    let mut cmd = ProcessCommand::new(container_runtime);
63
    cmd.arg("run").arg("--rm").arg("-i").stdout(stdout).stderr(stderr);
64

            
65
    let current_dir = env::current_dir()?;
66
    cmd
67
      .arg("-v")
68
      .arg(format!("{}:/workdir:z", current_dir.to_utf8()?));
69
    cmd.arg("-w").arg("/workdir");
70

            
71
    for mounted_path in self.mounted_paths.clone() {
72
      cmd.arg("-v").arg(mounted_path);
73
    }
74

            
75
    // Inject environment variables in both container and command
76
    for (key, value) in context.env_vars.iter() {
77
      cmd.env(key, value);
78
      cmd.arg("-e").arg(format!("{}={}", key, value));
79
    }
80

            
81
    cmd.arg(&self.image).args(&self.container_command);
82

            
83
    log::trace!("Running command: {:?}", cmd);
84

            
85
    let mut cmd = cmd.spawn()?;
86
    if verbose {
87
      handle_output!(cmd.stdout, context);
88
      handle_output!(cmd.stderr, context);
89
    }
90

            
91
    let status = cmd.wait()?;
92
    if !status.success() && !ignore_errors {
93
      anyhow::bail!("Command failed - {}", self.container_command.join(" "));
94
    }
95

            
96
    Ok(())
97
  }
98

            
99
  fn ignore_errors(&self, context: &TaskContext) -> bool {
100
    self
101
      .ignore_errors
102
      .or(context.ignore_errors)
103
      .unwrap_or(default_ignore_errors())
104
  }
105

            
106
  fn verbose(&self, context: &TaskContext) -> bool {
107
    self.verbose.or(context.verbose).unwrap_or(default_verbose())
108
  }
109
}