use async_trait::async_trait;
use std::process::Stdio;
use std::time::Duration;
use tokio::process::Command;
use tracing::warn;
use super::types::{CommandOutput, ContainerConfig, ContainerRuntime, RuntimeError, RuntimeResult};
#[derive(Debug, Clone, Default)]
pub struct AppleContainerRuntime {
image: Option<String>,
extra_mounts: Vec<String>,
}
impl AppleContainerRuntime {
pub fn new() -> Self {
Self::default()
}
pub fn with_image(image: &str) -> Self {
Self {
image: Some(image.to_string()),
extra_mounts: Vec::new(),
}
}
pub fn with_extra_mounts(mut self, mounts: Vec<String>) -> Self {
self.extra_mounts = mounts;
self
}
}
#[async_trait]
impl ContainerRuntime for AppleContainerRuntime {
fn name(&self) -> &str {
"apple"
}
async fn is_available(&self) -> bool {
if !cfg!(target_os = "macos") {
return false;
}
let version_check = Command::new("container")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await
.map(|s| s.success())
.unwrap_or(false);
if !version_check {
return false;
}
let syntax_check = Command::new("container")
.args(["run", "--help"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await
.map(|s| s.success())
.unwrap_or(false);
if !syntax_check {
warn!(
"Apple Container tool found but 'container run --help' failed. \
CLI syntax may be incompatible with this implementation."
);
return false;
}
true
}
async fn execute(
&self,
command: &str,
config: &ContainerConfig,
) -> RuntimeResult<CommandOutput> {
warn!(
"Apple Container runtime is EXPERIMENTAL. \
CLI interface may not match actual Apple Container tool. \
Test thoroughly before production use."
);
let mut args = vec!["run".to_string()];
if let Some(ref image) = self.image {
args.push("--image".to_string());
args.push(image.clone());
}
if let Some(ref workdir) = config.workdir {
args.push("--workdir".to_string());
args.push(workdir.to_string_lossy().to_string());
}
for (host, container, readonly) in &config.mounts {
args.push("--mount".to_string());
let mount_spec = if *readonly {
format!(
"type=bind,source={},target={},readonly",
host.to_string_lossy(),
container.to_string_lossy()
)
} else {
format!(
"type=bind,source={},target={}",
host.to_string_lossy(),
container.to_string_lossy()
)
};
args.push(mount_spec);
}
for mount in &self.extra_mounts {
args.push("--mount".to_string());
let parts: Vec<&str> = mount.split(':').collect();
let mount_spec = match parts.len() {
2 => format!("type=bind,source={},target={}", parts[0], parts[1]),
3 if parts[2] == "ro" => {
format!("type=bind,source={},target={},readonly", parts[0], parts[1])
}
_ => {
warn!("Invalid mount format '{}', skipping", mount);
continue;
}
};
args.push(mount_spec);
}
for (key, value) in &config.env {
args.push("--env".to_string());
args.push(format!("{}={}", key, value));
}
args.push("--".to_string());
args.push("sh".to_string());
args.push("-c".to_string());
args.push(command.to_string());
let mut cmd = Command::new("container");
cmd.args(&args)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let output = tokio::time::timeout(Duration::from_secs(config.timeout_secs), cmd.output())
.await
.map_err(|_| RuntimeError::Timeout(config.timeout_secs))?
.map_err(|e| RuntimeError::ExecutionFailed(e.to_string()))?;
Ok(CommandOutput::new(
String::from_utf8_lossy(&output.stdout).to_string(),
String::from_utf8_lossy(&output.stderr).to_string(),
output.status.code(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_apple_runtime_creation() {
let runtime = AppleContainerRuntime::new();
assert_eq!(runtime.name(), "apple");
assert!(runtime.image.is_none());
}
#[test]
fn test_apple_runtime_with_image() {
let runtime = AppleContainerRuntime::with_image("/path/to/image");
assert_eq!(runtime.image, Some("/path/to/image".to_string()));
}
#[test]
fn test_apple_runtime_default() {
let runtime = AppleContainerRuntime::default();
assert!(runtime.image.is_none());
}
#[cfg(target_os = "macos")]
#[tokio::test]
#[ignore = "requires Apple Container framework"]
async fn test_apple_runtime_available() {
let runtime = AppleContainerRuntime::new();
println!(
"Apple Container available: {}",
runtime.is_available().await
);
}
#[cfg(not(target_os = "macos"))]
#[tokio::test]
async fn test_apple_runtime_not_available_on_non_macos() {
let runtime = AppleContainerRuntime::new();
assert!(!runtime.is_available().await);
}
}