use std::path::PathBuf;
#[cfg(feature = "oci-isolation")]
use crate::error::IsolationError;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct OciConfig {
pub state_root: PathBuf,
pub bundle_root: PathBuf,
pub use_systemd: bool,
}
impl Default for OciConfig {
fn default() -> Self {
Self {
state_root: PathBuf::from("/run/axocoatl/oci"),
bundle_root: PathBuf::from("/var/axocoatl/bundles"),
use_systemd: false,
}
}
}
#[cfg(feature = "oci-isolation")]
pub struct OciSandbox {
config: OciConfig,
}
#[cfg(feature = "oci-isolation")]
impl OciSandbox {
pub fn new(config: OciConfig) -> Result<Self, IsolationError> {
std::fs::create_dir_all(&config.state_root).map_err(|e| {
IsolationError::OciSetupFailed(format!(
"Failed to create state root {}: {e}",
config.state_root.display()
))
})?;
std::fs::create_dir_all(&config.bundle_root).map_err(|e| {
IsolationError::OciSetupFailed(format!(
"Failed to create bundle root {}: {e}",
config.bundle_root.display()
))
})?;
Ok(Self { config })
}
pub async fn execute(
&self,
tool_name: &str,
input: serde_json::Value,
timeout: std::time::Duration,
) -> Result<serde_json::Value, IsolationError> {
let container_id = format!("axocoatl-{}-{}", tool_name, uuid::Uuid::new_v4());
let bundle_path = self.config.bundle_root.join(tool_name);
if !bundle_path.exists() {
return Err(IsolationError::ToolNotFound(tool_name.to_string()));
}
tracing::debug!(
container_id = %container_id,
tool = %tool_name,
"Starting OCI container for tool execution"
);
let io_dir = self.config.state_root.join(&container_id);
tokio::fs::create_dir_all(&io_dir).await?;
let input_path = io_dir.join("input.json");
let output_path = io_dir.join("output.json");
tokio::fs::write(&input_path, serde_json::to_vec(&input)?).await?;
let state_root = self.config.state_root.clone();
let use_systemd = self.config.use_systemd;
let cid = container_id.clone();
let bp = bundle_path.clone();
let handle = tokio::task::spawn_blocking(move || {
Self::run_container_sync(&cid, &bp, &state_root, use_systemd)
});
let result = match tokio::time::timeout(timeout, handle).await {
Ok(Ok(Ok(()))) => {
let output_bytes = tokio::fs::read(&output_path)
.await
.map_err(|_| IsolationError::OutputReadFailed)?;
serde_json::from_slice(&output_bytes).map_err(IsolationError::Serialization)
}
Ok(Ok(Err(e))) => Err(e),
Ok(Err(e)) => Err(IsolationError::OciContainerFailed(format!(
"Container task panicked: {e}"
))),
Err(_) => {
tracing::warn!(
container_id = %container_id,
"OCI container timed out — cleanup may be needed"
);
Err(IsolationError::Timeout(timeout))
}
};
let _ = tokio::fs::remove_dir_all(&io_dir).await;
result
}
fn run_container_sync(
container_id: &str,
bundle_path: &std::path::Path,
state_root: &std::path::Path,
use_systemd: bool,
) -> Result<(), IsolationError> {
use libcontainer::container::builder::ContainerBuilder;
use libcontainer::syscall::syscall::SyscallType;
let mut container = ContainerBuilder::new(container_id.to_string(), SyscallType::default())
.with_root_path(state_root)
.map_err(|e| IsolationError::OciSetupFailed(format!("Root path: {e}")))?
.with_executor(libcontainer::workload::default::DefaultExecutor {})
.validate_id()
.map_err(|e| IsolationError::OciSetupFailed(format!("Invalid container ID: {e}")))?
.as_init(bundle_path)
.with_systemd(use_systemd)
.with_detach(false)
.build()
.map_err(|e| IsolationError::OciContainerFailed(format!("Container create: {e}")))?;
container
.start()
.map_err(|e| IsolationError::OciContainerFailed(format!("Container start: {e}")))?;
tracing::debug!(container_id = %container_id, "OCI container completed");
container
.delete(false)
.map_err(|e| IsolationError::OciContainerFailed(format!("Container delete: {e}")))?;
Ok(())
}
pub fn config(&self) -> &OciConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config() {
let config = OciConfig::default();
assert_eq!(config.state_root, PathBuf::from("/run/axocoatl/oci"));
assert_eq!(config.bundle_root, PathBuf::from("/var/axocoatl/bundles"));
assert!(!config.use_systemd);
}
#[test]
fn config_serde_roundtrip() {
let config = OciConfig::default();
let json = serde_json::to_string(&config).unwrap();
let back: OciConfig = serde_json::from_str(&json).unwrap();
assert_eq!(back.state_root, config.state_root);
assert_eq!(back.bundle_root, config.bundle_root);
}
}