use crate::{runner::command::Command, StepLog};
use std::{collections::HashMap, process::ExitStatus};
use tokio::sync::broadcast;
#[derive(Debug, Clone)]
pub struct Docker {
pub image: Option<String>,
pub name: String,
pub environments: HashMap<String, String>,
pub working_dir: Option<String>,
pub entrypoint: Option<String>,
pub volumes: Vec<String>,
pub auto_remove: bool,
pub security_opts: Vec<String>,
log_sender: broadcast::Sender<StepLog>,
}
impl Docker {
pub fn new(name: impl Into<String>) -> Self {
let (sender, _) = broadcast::channel(10000);
Self {
image: None,
name: name.into(),
environments: HashMap::new(),
working_dir: None,
entrypoint: None,
volumes: Vec::new(),
security_opts: Vec::new(),
auto_remove: true,
log_sender: sender,
}
}
pub fn image(mut self, image: String) -> Self {
self.image = Some(image);
self
}
pub fn auto_remove(mut self, auto_remove: bool) -> Self {
self.auto_remove = auto_remove;
self
}
pub fn environment(mut self, key: String, value: String) -> Self {
self.environments.insert(key, value);
self
}
pub fn working_dir(mut self, working_dir: String) -> Self {
self.working_dir = Some(working_dir);
self
}
pub fn entrypoint(mut self, entrypoint: String) -> Self {
self.entrypoint = Some(entrypoint);
self
}
pub fn security_opt(mut self, security_opt: String) -> Self {
self.security_opts.push(security_opt);
self
}
pub fn volume(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
self
.volumes
.push(format!("\"{}:{}\"", from.into(), to.into()));
self
}
pub async fn run(&self) -> anyhow::Result<ExitStatus> {
let command = self.generate_docker_command()?;
let mut command = Command::new(command);
command.set_log_sender(self.log_sender.clone());
command.run().await
}
pub async fn kill(&self) -> anyhow::Result<()> {
let name = self.name.clone();
Command::new("docker").arg("kill").arg(name).exec().await?;
Ok(())
}
pub fn subscribe_logs(&self) -> broadcast::Receiver<StepLog> {
self.log_sender.subscribe()
}
fn generate_docker_command(&self) -> anyhow::Result<String> {
let image = match &self.image {
Some(image) => image.clone(),
None => return Err(anyhow::anyhow!("image is required")),
};
let mut docker_command: Vec<String> = vec!["docker", "run", "--tty"]
.iter()
.map(|item| item.to_string())
.collect();
if self.auto_remove {
docker_command.push("--rm".to_string());
}
for security_opt in &self.security_opts {
docker_command.push("--security-opt".to_string());
docker_command.push(security_opt.to_string());
}
for volume in &self.volumes {
docker_command.push("-v".to_string());
docker_command.push(volume.to_string());
}
for (key, value) in &self.environments {
docker_command.push("-e".to_string());
docker_command.push(format!("{}=\"{}\"", key, value));
}
if let Some(working_dir) = &self.working_dir {
docker_command.push("-w".to_string());
docker_command.push(working_dir.to_string());
}
if let Some(entrypoint) = &self.entrypoint {
docker_command.push("--entrypoint".to_string());
docker_command.push(entrypoint.to_string());
}
docker_command.push("--name".to_string());
docker_command.push(self.name.clone());
docker_command.push(image);
let command = docker_command.join(" ");
Ok(command)
}
}
#[cfg(test)]
mod tests {
use super::Docker;
#[test]
fn test_generate_docker_command() {
let common = Docker::new("test")
.image("ubuntu".to_string())
.environment("key".to_string(), "value".to_string())
.working_dir("/home/runner/work".to_string())
.entrypoint("entrypoint".to_string())
.volume("/app".to_string(), "/home/runner/work".to_string())
.generate_docker_command();
assert_eq!(
common.unwrap(),
"docker run --tty --rm -v \"/app:/home/runner/work\" -e key=\"value\" -w /home/runner/work --entrypoint entrypoint --name test ubuntu"
);
}
}