use anyhow::{Context, Result, bail};
use std::process::Command;
use crate::permissions::Permissions;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContainerRuntime {
Docker,
Podman,
}
impl ContainerRuntime {
pub fn cmd(&self) -> &'static str {
match self {
ContainerRuntime::Docker => "docker",
ContainerRuntime::Podman => "podman",
}
}
}
pub fn detect_container_runtime() -> Option<ContainerRuntime> {
if podman_available() {
Some(ContainerRuntime::Podman)
} else if docker_available() {
Some(ContainerRuntime::Docker)
} else {
None
}
}
pub struct ContainerSandbox {
pub name: String,
runtime: ContainerRuntime,
container_id: Option<String>,
}
#[allow(dead_code)]
pub type DockerSandbox = ContainerSandbox;
impl ContainerSandbox {
#[allow(dead_code)]
pub fn new(name: &str) -> Self {
let runtime = detect_container_runtime().unwrap_or(ContainerRuntime::Docker);
Self {
name: name.to_string(),
runtime,
container_id: None,
}
}
pub fn with_runtime(name: &str, runtime: ContainerRuntime) -> Self {
Self {
name: name.to_string(),
runtime,
container_id: None,
}
}
#[allow(dead_code)]
pub fn runtime(&self) -> ContainerRuntime {
self.runtime
}
#[allow(dead_code)]
pub async fn start(&mut self, image: &str) -> Result<()> {
self.start_with_permissions(image, &Permissions::default())
.await
}
pub async fn start_with_permissions(&mut self, image: &str, perms: &Permissions) -> Result<()> {
let cmd = self.runtime.cmd();
let container_name = format!("agentkernel-{}", self.name);
let _ = Command::new(cmd)
.args(["rm", "-f", &container_name])
.output();
let mut args = vec![
"run".to_string(),
"-d".to_string(),
"--rm".to_string(), "--name".to_string(),
format!("agentkernel-{}", self.name),
"--hostname".to_string(),
"agentkernel".to_string(),
];
args.extend(perms.to_docker_args());
args.extend(perms.get_env_args());
args.extend(perms.get_mount_args(None));
args.extend([
"--entrypoint".to_string(),
"sh".to_string(),
image.to_string(),
"-c".to_string(),
"while true; do sleep 3600; done".to_string(),
]);
let output = Command::new(cmd)
.args(&args)
.output()
.context("Failed to start container")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Failed to start container: {}", stderr);
}
let container_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
self.container_id = Some(container_id);
Ok(())
}
#[allow(dead_code)]
pub async fn exec(&self, cmd: &[String]) -> Result<String> {
let runtime_cmd = self.runtime.cmd();
let container_name = format!("agentkernel-{}", self.name);
let mut args = vec!["exec", &container_name];
let cmd_refs: Vec<&str> = cmd.iter().map(|s| s.as_str()).collect();
args.extend(cmd_refs);
let output = Command::new(runtime_cmd)
.args(&args)
.output()
.context("Failed to execute command in container")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Command failed: {}", stderr);
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
#[allow(dead_code)]
pub async fn stop(&mut self) -> Result<()> {
let container_name = format!("agentkernel-{}", self.name);
let _ = Command::new(self.runtime.cmd())
.args(["rm", "-f", &container_name])
.output();
self.container_id = None;
Ok(())
}
#[allow(dead_code)]
pub async fn remove(&mut self) -> Result<()> {
let container_name = format!("agentkernel-{}", self.name);
let _ = Command::new(self.runtime.cmd())
.args(["rm", "-f", &container_name])
.output();
self.container_id = None;
Ok(())
}
#[allow(dead_code)]
pub fn is_running(&self) -> bool {
let container_name = format!("agentkernel-{}", self.name);
if let Ok(output) = Command::new(self.runtime.cmd())
.args(["ps", "-q", "-f", &format!("name={}", container_name)])
.output()
{
!String::from_utf8_lossy(&output.stdout).trim().is_empty()
} else {
false
}
}
pub fn run_ephemeral_cmd(
runtime: ContainerRuntime,
image: &str,
cmd: &[String],
perms: &Permissions,
) -> Result<(i32, String, String)> {
let runtime_cmd = runtime.cmd();
let mut args = vec![
"run".to_string(),
"--rm".to_string(), ];
if let Some(cpu) = perms.max_cpu_percent {
args.push(format!("--cpus={}", cpu as f32 / 100.0));
}
if let Some(mem) = perms.max_memory_mb {
args.push(format!("--memory={}m", mem));
}
if !perms.network {
args.push("--network=none".to_string());
}
if perms.mount_cwd
&& let Ok(cwd) = std::env::current_dir()
{
args.push("-v".to_string());
args.push(format!("{}:/workspace", cwd.display()));
args.push("-w".to_string());
args.push("/workspace".to_string());
}
if perms.mount_home
&& let Some(home) = std::env::var_os("HOME")
{
args.push("-v".to_string());
args.push(format!("{}:/home/user:ro", home.to_string_lossy()));
}
if perms.read_only_root {
args.push("--read-only".to_string());
}
if perms.pass_env {
for var in ["PATH", "HOME", "USER", "LANG", "LC_ALL", "TERM"] {
if let Ok(val) = std::env::var(var) {
args.push("-e".to_string());
args.push(format!("{}={}", var, val));
}
}
}
if let Some(seccomp_path) = perms.resolve_seccomp_path() {
args.push(format!("--security-opt=seccomp={}", seccomp_path.display()));
}
args.push(image.to_string());
args.extend(cmd.iter().cloned());
let output = Command::new(runtime_cmd)
.args(&args)
.output()
.context("Failed to run container")?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(-1);
Ok((exit_code, stdout, stderr))
}
}
pub fn docker_available() -> bool {
Command::new("docker")
.arg("version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn podman_available() -> bool {
Command::new("podman")
.arg("version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[allow(dead_code)]
pub fn container_runtime_available() -> bool {
docker_available() || podman_available()
}