use crate::manifest::AppManifest; use crate::podman_client::{ContainerState, ContainerStatus, PodmanClient}; use anyhow::{Context, Result}; use async_trait::async_trait; use std::process::Command; use tokio::process::Command as TokioCommand; #[async_trait] pub trait ContainerRuntime: Send + Sync { async fn pull_image(&self, image: &str, signature: Option<&str>) -> Result<()>; async fn create_container( &self, manifest: &AppManifest, name: &str, port_offset: u16, ) -> Result; async fn start_container(&self, name: &str) -> Result<()>; async fn stop_container(&self, name: &str) -> Result<()>; async fn remove_container(&self, name: &str) -> Result<()>; async fn get_container_status(&self, name: &str) -> Result; async fn get_container_logs(&self, name: &str, lines: u32) -> Result>; async fn list_containers(&self) -> Result>; } pub struct PodmanRuntime { client: PodmanClient, } impl PodmanRuntime { pub fn new(user: String) -> Self { Self { client: PodmanClient::new(user), } } } #[async_trait] impl ContainerRuntime for PodmanRuntime { async fn pull_image(&self, image: &str, signature: Option<&str>) -> Result<()> { self.client.pull_image(image, signature).await } async fn create_container( &self, manifest: &AppManifest, name: &str, port_offset: u16, ) -> Result { // Apply port offset to manifest ports let mut dev_manifest = manifest.clone(); for port in &mut dev_manifest.app.ports { port.host += port_offset; } // PodmanClient doesn't take port_offset, so we use the modified manifest self.client.create_container(&dev_manifest, name).await } async fn start_container(&self, name: &str) -> Result<()> { self.client.start_container(name).await } async fn stop_container(&self, name: &str) -> Result<()> { self.client.stop_container(name).await } async fn remove_container(&self, name: &str) -> Result<()> { self.client.remove_container(name).await } async fn get_container_status(&self, name: &str) -> Result { self.client.get_container_status(name).await } async fn get_container_logs(&self, name: &str, lines: u32) -> Result> { self.client.get_container_logs(name, lines).await } async fn list_containers(&self) -> Result> { self.client.list_containers().await } } pub struct DockerRuntime { _user: String, } impl DockerRuntime { pub fn new(user: String) -> Self { Self { _user: user } } fn docker_async(&self) -> TokioCommand { let mut cmd = TokioCommand::new("docker"); // Use actual HOME environment variable instead of hardcoded /home if let Ok(home) = std::env::var("HOME") { cmd.env("HOME", home); } cmd } } #[async_trait] impl ContainerRuntime for DockerRuntime { async fn pull_image(&self, image: &str, _signature: Option<&str>) -> Result<()> { let mut cmd = self.docker_async(); cmd.arg("pull").arg(image); let output = cmd .output() .await .context("Failed to execute docker pull")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to pull image: {}", stderr)); } Ok(()) } async fn create_container( &self, manifest: &AppManifest, name: &str, port_offset: u16, ) -> Result { let mut cmd = self.docker_async(); cmd.arg("create"); cmd.arg("--name").arg(name); if manifest.app.security.readonly_root { cmd.arg("--read-only"); } match manifest.app.security.network_policy.as_str() { "host" => { cmd.arg("--network").arg("host"); } "isolated" => { // Docker uses bridge network by default } _ => { cmd.arg("--network") .arg(&manifest.app.security.network_policy); } } // Port mappings with offset for port in &manifest.app.ports { let host_port = port.host + port_offset; cmd.arg("-p") .arg(format!("{}:{}", host_port, port.container)); } // Volumes for volume in &manifest.app.volumes { let mut mount = format!("{}:{}", volume.source, volume.target); if !volume.options.is_empty() { mount.push_str(&format!(":{}", volume.options.join(","))); } cmd.arg("-v").arg(mount); } // Devices for device in &manifest.app.devices { cmd.arg("--device").arg(device); } // Environment variables for env in &manifest.app.environment { cmd.arg("-e").arg(env); } // Resource limits if let Some(cpu) = manifest.app.resources.cpu_limit { cmd.arg("--cpus").arg(cpu.to_string()); } if let Some(memory) = &manifest.app.resources.memory_limit { cmd.arg("--memory").arg(memory); } // Capabilities cmd.arg("--cap-drop").arg("ALL"); for cap in &manifest.app.security.capabilities { cmd.arg("--cap-add").arg(cap); } cmd.arg(&manifest.app.container.image); let output = cmd.output().await.context("Failed to create container")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to create container: {}", stderr)); } let container_id = String::from_utf8_lossy(&output.stdout).trim().to_string(); Ok(container_id) } async fn start_container(&self, name: &str) -> Result<()> { let mut cmd = self.docker_async(); cmd.arg("start").arg(name); let output = cmd.output().await.context("Failed to start container")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to start container: {}", stderr)); } Ok(()) } async fn stop_container(&self, name: &str) -> Result<()> { let mut cmd = self.docker_async(); cmd.arg("stop").arg(name); let output = cmd.output().await.context("Failed to stop container")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to stop container: {}", stderr)); } Ok(()) } async fn remove_container(&self, name: &str) -> Result<()> { let mut cmd = self.docker_async(); cmd.arg("rm").arg("-f").arg(name); let output = cmd.output().await.context("Failed to remove container")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to remove container: {}", stderr)); } Ok(()) } async fn get_container_status(&self, name: &str) -> Result { let mut cmd = self.docker_async(); cmd.arg("inspect") .arg("--format") .arg("{{.Id}}|{{.Name}}|{{.State.Status}}|{{.Config.Image}}|{{.Created}}|{{.NetworkSettings.Ports}}") .arg(name); let output = cmd.output().await.context("Failed to inspect container")?; if !output.status.success() { return Err(anyhow::anyhow!("Container not found: {}", name)); } let info = String::from_utf8_lossy(&output.stdout); let parts: Vec<&str> = info.trim().split('|').collect(); if parts.len() < 5 { return Err(anyhow::anyhow!("Invalid container inspect output")); } Ok(ContainerStatus { id: parts[0].to_string(), name: parts[1].to_string(), state: crate::podman_client::ContainerState::from(parts[2]), health: None, exit_code: None, started_at: None, image: parts[3].to_string(), created: parts[4].to_string(), ports: vec![], lan_address: None, }) } async fn get_container_logs(&self, name: &str, lines: u32) -> Result> { let mut cmd = self.docker_async(); cmd.arg("logs") .arg("--tail") .arg(lines.to_string()) .arg(name); let output = cmd.output().await.context("Failed to get container logs")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to get logs: {}", stderr)); } let logs = String::from_utf8_lossy(&output.stdout); Ok(logs.lines().map(|s| s.to_string()).collect()) } async fn list_containers(&self) -> Result> { let mut cmd = self.docker_async(); cmd.arg("ps").arg("-a").arg("--format").arg("json"); let output = cmd.output().await.context("Failed to list containers")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to list containers: {}", stderr)); } let json = String::from_utf8_lossy(&output.stdout); let mut result = Vec::new(); // Docker returns NDJSON (newline-delimited JSON), not a JSON array for line in json.lines() { if line.trim().is_empty() { continue; } let container: serde_json::Value = serde_json::from_str(line) .context(format!("Failed to parse container JSON: {}", line))?; // Extract ports from JSON let ports_value = &container["Ports"]; let ports_str = ports_value.as_str().unwrap_or(""); let ports: Vec = if !ports_str.is_empty() { ports_str.split(", ").map(|s| s.to_string()).collect() } else { vec![] }; result.push(ContainerStatus { id: container["ID"].as_str().unwrap_or("").to_string(), name: container["Names"].as_str().unwrap_or("").to_string(), state: ContainerState::from(container["State"].as_str().unwrap_or("unknown")), health: None, exit_code: container["ExitCode"].as_i64().map(|c| c as i32), started_at: None, image: container["Image"].as_str().unwrap_or("").to_string(), created: container["CreatedAt"].as_str().unwrap_or("").to_string(), ports, lan_address: None, }); } Ok(result) } } pub struct AutoRuntime { runtime: Box, } impl AutoRuntime { pub async fn new(user: String) -> Result { // Try Podman first if Self::check_podman_available() { Ok(Self { runtime: Box::new(PodmanRuntime::new(user)), }) } else if Self::check_docker_available() { Ok(Self { runtime: Box::new(DockerRuntime::new(user)), }) } else { Err(anyhow::anyhow!("Neither Podman nor Docker is available")) } } fn check_podman_available() -> bool { Command::new("podman").arg("--version").output().is_ok() } fn check_docker_available() -> bool { Command::new("docker").arg("--version").output().is_ok() } } #[async_trait] impl ContainerRuntime for AutoRuntime { async fn pull_image(&self, image: &str, signature: Option<&str>) -> Result<()> { self.runtime.pull_image(image, signature).await } async fn create_container( &self, manifest: &AppManifest, name: &str, port_offset: u16, ) -> Result { self.runtime .create_container(manifest, name, port_offset) .await } async fn start_container(&self, name: &str) -> Result<()> { self.runtime.start_container(name).await } async fn stop_container(&self, name: &str) -> Result<()> { self.runtime.stop_container(name).await } async fn remove_container(&self, name: &str) -> Result<()> { self.runtime.remove_container(name).await } async fn get_container_status(&self, name: &str) -> Result { self.runtime.get_container_status(name).await } async fn get_container_logs(&self, name: &str, lines: u32) -> Result> { self.runtime.get_container_logs(name, lines).await } async fn list_containers(&self) -> Result> { self.runtime.list_containers().await } } // Runtime factory functions will be provided by the archipelago crate // that imports this library and has access to Config