use std::process::Command;
#[derive(Debug, Clone)]
pub enum DockerStatus {
Running { version: String },
NotRunning { version: String },
NotFound,
}
impl DockerStatus {
pub fn is_ready(&self) -> bool {
matches!(self, DockerStatus::Running { .. })
}
#[allow(dead_code)]
pub fn is_installed(&self) -> bool {
!matches!(self, DockerStatus::NotFound)
}
#[allow(dead_code)]
pub fn version(&self) -> Option<&str> {
match self {
DockerStatus::Running { version } | DockerStatus::NotRunning { version } => {
Some(version)
}
DockerStatus::NotFound => None,
}
}
}
pub fn check_docker() -> DockerStatus {
let version_output = Command::new("docker").args(["--version"]).output();
let version = match version_output {
Ok(output) if output.status.success() => {
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
_ => return DockerStatus::NotFound,
};
let info_output = Command::new("docker").args(["info"]).output();
match info_output {
Ok(output) if output.status.success() => DockerStatus::Running { version },
_ => DockerStatus::NotRunning { version },
}
}
pub fn get_available_disk_space() -> Option<u64> {
let output = Command::new("df").args(["-k", "/"]).output().ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
let available_kb: u64 = stdout
.lines()
.nth(1)?
.split_whitespace()
.nth(3)?
.parse()
.ok()?;
Some(available_kb / (1024 * 1024))
}
#[derive(Debug)]
#[allow(dead_code)]
pub enum ContainerResult {
Started { container_id: String },
AlreadyRunning { container_id: String },
Restarted { container_id: String },
Failed { error: String },
}
pub fn container_exists(name: &str) -> bool {
let output = Command::new("docker")
.args([
"ps",
"-a",
"--filter",
&format!("name=^{}$", name),
"--format",
"{{.Names}}",
])
.output();
match output {
Ok(out) if out.status.success() => !String::from_utf8_lossy(&out.stdout).trim().is_empty(),
_ => false,
}
}
pub fn container_running(name: &str) -> bool {
let output = Command::new("docker")
.args([
"ps",
"--filter",
&format!("name=^{}$", name),
"--format",
"{{.Names}}",
])
.output();
match output {
Ok(out) if out.status.success() => !String::from_utf8_lossy(&out.stdout).trim().is_empty(),
_ => false,
}
}
pub fn start_container(name: &str) -> Result<String, String> {
let output = Command::new("docker")
.args(["start", name])
.output()
.map_err(|e| format!("Failed to run docker start: {}", e))?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
}
}
#[allow(dead_code)]
pub fn stop_container(name: &str) -> Result<(), String> {
let output = Command::new("docker")
.args(["stop", name])
.output()
.map_err(|e| format!("Failed to run docker stop: {}", e))?;
if output.status.success() {
Ok(())
} else {
Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
}
}
pub fn remove_container(name: &str) -> Result<(), String> {
let output = Command::new("docker")
.args(["rm", "-f", name])
.output()
.map_err(|e| format!("Failed to run docker rm: {}", e))?;
if output.status.success() {
Ok(())
} else {
Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
}
}
pub fn pull_image(image: &str) -> Result<(), String> {
let output = Command::new("docker")
.args(["pull", image])
.output()
.map_err(|e| format!("Failed to run docker pull: {}", e))?;
if output.status.success() {
Ok(())
} else {
Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
}
}
pub fn run_container(
name: &str,
image: &str,
port_mapping: Option<(&str, &str)>,
env_vars: &[(&str, &str)],
extra_args: &[&str],
) -> Result<String, String> {
let mut cmd = Command::new("docker");
cmd.args(["run", "-d", "--name", name]);
if let Some((host_port, container_port)) = port_mapping {
cmd.arg("-p")
.arg(format!("{}:{}", host_port, container_port));
}
for (key, value) in env_vars {
cmd.arg("-e").arg(format!("{}={}", key, value));
}
cmd.args(extra_args);
cmd.arg(image);
let output = cmd
.output()
.map_err(|e| format!("Failed to run docker: {}", e))?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
}
}
#[allow(dead_code)]
pub async fn wait_for_healthy(name: &str, timeout_secs: u64) -> Result<(), String> {
use std::time::{Duration, Instant};
use tokio::time::sleep;
let start = Instant::now();
let timeout = Duration::from_secs(timeout_secs);
while start.elapsed() < timeout {
if container_running(name) {
return Ok(());
}
sleep(Duration::from_millis(500)).await;
}
Err(format!(
"Container {} did not become healthy within {} seconds",
name, timeout_secs
))
}
pub fn docker_install_instructions() -> Vec<String> {
let os = std::env::consts::OS;
match os {
"macos" => vec![
"macOS: brew install --cask docker".to_string(),
" or https://docker.com/get-started".to_string(),
],
"linux" => vec!["Linux: https://docs.docker.com/engine/install".to_string()],
"windows" => vec!["Windows: https://docs.docker.com/desktop/windows".to_string()],
_ => vec!["Visit https://docker.com/get-started".to_string()],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_docker() {
let status = check_docker();
let _ = status.is_ready();
let _ = status.is_installed();
}
}