// SPDX-FileCopyrightText: 2023 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later use std::collections::HashMap; use std::process::{Command, Stdio}; #[derive(Default, Clone, Eq, PartialEq)] pub struct Docker; impl Docker { pub fn new() -> Self { Docker } } impl Docker { pub fn version(&self) -> String { let version = Command::new("docker") .arg("--version") .output() .expect("unable to obtain Docker version"); let x = String::from_utf8(version.stdout).unwrap(); let x: Vec<&str> = x.split("Docker version ").collect(); x.get(1).unwrap().trim().to_string() } pub fn run_container( &self, name: &str, img: &str, detached: bool, env: &HashMap, network: Option, pull: bool, ) { let mut env_args = Vec::with_capacity(env.len() * 2 + 6); env_args.push("run".to_string()); if detached { env_args.push("-d".to_string()); } if pull { env_args.push("--pull=always".into()); } env_args.push("--name".to_string()); env_args.push(name.to_string()); for (k, v) in env.iter() { env_args.push("-e".to_string()); env_args.push(format!("{k}={v}")); } if let Some(network) = network { env_args.push("--network".into()); env_args.push(network); } env_args.push(img.to_string()); let mut child = Command::new("docker") .args(&env_args) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .expect("unable to obtain Docker version"); child.wait().unwrap(); } pub fn get_exit_status(&self, name: &str) -> isize { let output = Command::new("docker") .args(["inspect", name, "--format={{.State.ExitCode}}"]) .output() .expect("unable to exit status"); let out = String::from_utf8(output.stdout).unwrap(); let out = out.trim(); out.parse::().unwrap() } pub fn get_logs(&self, name: &str) -> String { let output = Command::new("docker") .args(["logs", name]) .output() .expect("unable to get logs"); String::from_utf8(output.stdout).unwrap() } pub async fn block_till_container_exists(&self, name: &str, mut timeout: usize) -> bool { let args = ["container", "inspect", name, "--format={{.State.Status}}"]; loop { let out = tokio::process::Command::new("docker") .args(args) .output() .await .unwrap_or_else(|_| panic!("unable to run docker command on container {name}")); let out = String::from_utf8(out.stdout).unwrap(); let out = out.trim(); if out == "exited" { return true; } if timeout == 0 { return false; } tokio::time::sleep(std::time::Duration::new(1, 0)).await; timeout -= 1; } } pub fn rm_container(&self, name: &str, force: bool) { let args = if force { vec!["rm", "--force", name] } else { vec!["rm", name] }; Command::new("docker") .args(args) .spawn() .unwrap_or_else(|_| panic!("unable to remove docker container {name}")); } } #[cfg(test)] mod tests { use crate::utils::get_random; use super::*; #[actix_rt::test] async fn test_docker_util() { let d = Docker::new(); d.version(); let name = format!("test_sleep__{}", get_random(4)); let mut env = HashMap::new(); env.insert("FOO".to_string(), "BAR".to_string()); env.insert("BAZ".to_string(), "BOO".to_string()); d.run_container( &name, "forgeflux/ftest-docker-cmd-tester", true, &env, None, true, ); let out = Command::new("docker") .args(["container", "inspect", "-f", "'{{.State.Running}}'", &name]) .output() .unwrap(); let out = String::from_utf8(out.stdout).unwrap(); assert!(out.contains("true")); loop { if d.block_till_container_exists(&name, 13).await { break; } } let logs = d.get_logs(&name); println!("{logs}"); assert!(logs.contains("running")); assert!(logs.contains("FOO=BAR")); assert!(logs.contains("BAZ=BOO")); assert_eq!(d.get_exit_status(&name), 0); d.rm_container(&name, true); } }