use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DockerBuildInvocation {
pub program: String,
pub args: Vec<String>,
}
impl DockerBuildInvocation {
#[must_use]
pub fn build(
dockerfile_path: &Path,
context_dir: &Path,
image_tag: &str,
build_args: &std::collections::BTreeMap<String, String>,
) -> Self {
let mut args = vec![
"build".to_string(),
"-f".to_string(),
dockerfile_path.display().to_string(),
"-t".to_string(),
image_tag.to_string(),
];
for (k, v) in build_args {
args.push("--build-arg".to_string());
let mut kv = k.clone();
kv.push('=');
kv.push_str(v);
args.push(kv);
}
args.push(context_dir.display().to_string());
Self { program: "docker".to_string(), args }
}
#[must_use]
pub fn pull(image_ref: &str) -> Self {
Self {
program: "docker".to_string(),
args: vec!["pull".to_string(), image_ref.to_string()],
}
}
#[must_use]
pub fn history(image_ref: &str) -> Self {
Self {
program: "docker".to_string(),
args: vec![
"history".to_string(),
"--no-trunc".to_string(),
"--format".to_string(),
"{{.CreatedBy}}".to_string(),
image_ref.to_string(),
],
}
}
#[must_use]
pub fn save(image_ref: &str, output_path: &Path) -> Self {
Self {
program: "docker".to_string(),
args: vec![
"save".to_string(),
"-o".to_string(),
output_path.display().to_string(),
image_ref.to_string(),
],
}
}
#[must_use]
pub fn push(image_ref: &str) -> Self {
Self { program: "docker".to_string(), args: vec!["push".to_string(), image_ref.to_string()] }
}
#[must_use]
pub fn tag(source_ref: &str, target_ref: &str) -> Self {
Self {
program: "docker".to_string(),
args: vec!["tag".to_string(), source_ref.to_string(), target_ref.to_string()],
}
}
#[must_use]
pub fn to_std_command(&self) -> Command {
let mut cmd = Command::new(&self.program);
cmd.args(&self.args);
cmd
}
}
#[derive(Debug, Clone)]
pub struct CommandOutcome {
pub success: bool,
pub exit_code: Option<i32>,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
impl CommandOutcome {
#[must_use]
pub fn stderr_tail(&self, n: usize) -> String {
let start = self.stderr.len().saturating_sub(n);
String::from_utf8_lossy(&self.stderr[start..]).into_owned()
}
}
#[derive(Debug, thiserror::Error)]
pub enum CommandRunError {
#[error("failed to spawn `{program}`: {source}")]
Spawn {
program: String,
#[source]
source: std::io::Error,
},
}
pub trait CommandRunner {
fn run(&self, invocation: &DockerBuildInvocation) -> Result<CommandOutcome, CommandRunError>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct RealCommandRunner;
impl CommandRunner for RealCommandRunner {
fn run(&self, invocation: &DockerBuildInvocation) -> Result<CommandOutcome, CommandRunError> {
let output = invocation
.to_std_command()
.output()
.map_err(|source| CommandRunError::Spawn { program: invocation.program.clone(), source })?;
Ok(CommandOutcome {
success: output.status.success(),
exit_code: output.status.code(),
stdout: output.stdout,
stderr: output.stderr,
})
}
}
#[derive(Debug, Default)]
pub struct MockCommandRunner {
pub invocations: std::sync::Mutex<Vec<DockerBuildInvocation>>,
pub scripted_outcome: Option<CommandOutcome>,
}
impl MockCommandRunner {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_outcome(outcome: CommandOutcome) -> Self {
Self { invocations: std::sync::Mutex::new(Vec::new()), scripted_outcome: Some(outcome) }
}
#[must_use]
pub fn recorded(&self) -> Vec<DockerBuildInvocation> {
self.invocations.lock().expect("mock mutex poisoned").clone()
}
}
impl CommandRunner for MockCommandRunner {
fn run(&self, invocation: &DockerBuildInvocation) -> Result<CommandOutcome, CommandRunError> {
self.invocations
.lock()
.expect("mock mutex poisoned")
.push(invocation.clone());
Ok(self.scripted_outcome.clone().unwrap_or(CommandOutcome {
success: true,
exit_code: Some(0),
stdout: Vec::new(),
stderr: Vec::new(),
}))
}
}