use tokio::process::Command;
use crate::error::IsolationError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PodmanReadiness {
Ready,
NotInstalled,
MachineMissing,
MachineStopped,
}
impl PodmanReadiness {
pub fn summary(&self) -> String {
match self {
PodmanReadiness::Ready => "podman ready".to_string(),
PodmanReadiness::NotInstalled => {
format!("podman not installed — {}", manual_install_hint())
}
PodmanReadiness::MachineMissing => {
"podman installed, but no VM — run: podman machine init && podman machine start"
.to_string()
}
PodmanReadiness::MachineStopped => {
"podman installed, VM stopped — run: podman machine start".to_string()
}
}
}
}
fn needs_machine() -> bool {
cfg!(any(target_os = "macos", target_os = "windows"))
}
async fn has(bin: &str) -> bool {
Command::new(bin)
.arg("--version")
.output()
.await
.map(|o| o.status.success())
.unwrap_or(false)
}
async fn machine_state() -> (bool, bool) {
let out = match Command::new("podman")
.args(["machine", "list", "--format", "json"])
.output()
.await
{
Ok(o) if o.status.success() => o.stdout,
_ => return (false, false),
};
let machines: Vec<serde_json::Value> = serde_json::from_slice(&out).unwrap_or_default();
let running = machines
.iter()
.any(|m| m.get("Running").and_then(|v| v.as_bool()).unwrap_or(false));
(!machines.is_empty(), running)
}
pub async fn detect() -> PodmanReadiness {
if !has("podman").await {
return PodmanReadiness::NotInstalled;
}
if !needs_machine() {
return PodmanReadiness::Ready;
}
match machine_state().await {
(false, _) => PodmanReadiness::MachineMissing,
(true, false) => PodmanReadiness::MachineStopped,
(true, true) => PodmanReadiness::Ready,
}
}
pub async fn ensure_ready() -> Result<(), IsolationError> {
match detect().await {
PodmanReadiness::Ready => Ok(()),
PodmanReadiness::MachineStopped => machine_start().await,
PodmanReadiness::MachineMissing => Err(IsolationError::OciSetupFailed(
"podman is installed but has no VM — run: podman machine init && podman machine start"
.to_string(),
)),
PodmanReadiness::NotInstalled => {
tracing::info!("podman not found — attempting an automatic install");
install().await?;
match detect().await {
PodmanReadiness::Ready => Ok(()),
PodmanReadiness::MachineStopped => machine_start().await,
PodmanReadiness::MachineMissing => Err(IsolationError::OciSetupFailed(
"podman installed — now run: podman machine init && podman machine start"
.to_string(),
)),
PodmanReadiness::NotInstalled => Err(IsolationError::OciSetupFailed(format!(
"podman could not be installed automatically. {}",
manual_install_hint()
))),
}
}
}
}
async fn machine_start() -> Result<(), IsolationError> {
tracing::info!("starting the podman machine");
let out = Command::new("podman")
.args(["machine", "start"])
.output()
.await
.map_err(|e| IsolationError::OciSetupFailed(format!("podman machine start: {e}")))?;
let stderr = String::from_utf8_lossy(&out.stderr);
if out.status.success() || stderr.contains("already running") {
Ok(())
} else {
Err(IsolationError::OciSetupFailed(format!(
"could not start the podman machine: {}",
stderr.trim()
)))
}
}
async fn install() -> Result<(), IsolationError> {
#[cfg(target_os = "linux")]
{
let (pm, args): (&str, &[&str]) = if has("apt-get").await {
("apt-get", &["install", "-y", "podman"])
} else if has("dnf").await {
("dnf", &["install", "-y", "podman"])
} else if has("pacman").await {
("pacman", &["-S", "--noconfirm", "podman"])
} else if has("zypper").await {
("zypper", &["--non-interactive", "install", "podman"])
} else {
return Err(IsolationError::OciSetupFailed(format!(
"no supported package manager found. {}",
manual_install_hint()
)));
};
tracing::info!(manager = pm, "installing podman via sudo (non-interactive)");
run_install("sudo", &[&["-n", pm], args].concat()).await
}
#[cfg(target_os = "macos")]
{
if !has("brew").await {
return Err(IsolationError::OciSetupFailed(format!(
"Homebrew not found. {}",
manual_install_hint()
)));
}
run_install("brew", &["install", "podman"]).await
}
#[cfg(target_os = "windows")]
{
run_install(
"winget",
&[
"install",
"-e",
"--id",
"RedHat.Podman",
"--accept-package-agreements",
"--accept-source-agreements",
],
)
.await
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
Err(IsolationError::OciSetupFailed(
manual_install_hint().to_string(),
))
}
}
#[allow(dead_code)]
async fn run_install(program: &str, args: &[&str]) -> Result<(), IsolationError> {
let out = Command::new(program)
.args(args)
.output()
.await
.map_err(|e| {
IsolationError::OciSetupFailed(format!(
"running '{program}' failed: {e}. {}",
manual_install_hint()
))
})?;
if out.status.success() {
tracing::info!("podman installed");
Ok(())
} else {
Err(IsolationError::OciSetupFailed(format!(
"automatic podman install failed: {}. {}",
String::from_utf8_lossy(&out.stderr).trim(),
manual_install_hint()
)))
}
}
pub fn manual_install_hint() -> &'static str {
#[cfg(target_os = "linux")]
{
"install it, e.g.: sudo apt-get install -y podman (or dnf/pacman/zypper)"
}
#[cfg(target_os = "macos")]
{
"install it: brew install podman && podman machine init && podman machine start"
}
#[cfg(target_os = "windows")]
{
"install it: winget install RedHat.Podman, then: podman machine init && podman machine start"
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
"install podman — see https://podman.io/docs/installation"
}
}