use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Stdio;
use std::sync::Arc;
use async_trait::async_trait;
use tokio::process::{Child, Command};
use tokio::sync::Mutex;
use tracing::{instrument, warn};
use cellos_core::ports::{CellBackend, CellHandle, TeardownReport};
use cellos_core::{CellosError, ExecutionCellDocument};
use crate::bundle::generate_bundle_config;
const RUNSC_BIN_ENV: &str = "CELLOS_GVISOR_RUNSC_BIN";
const BUNDLE_ROOT_ENV: &str = "CELLOS_GVISOR_BUNDLE_ROOT";
struct TrackedCell {
bundle_dir: PathBuf,
child: Child,
}
pub struct GVisorCellBackend {
tracked: Arc<Mutex<HashMap<String, TrackedCell>>>,
}
impl Default for GVisorCellBackend {
fn default() -> Self {
Self::new()
}
}
impl GVisorCellBackend {
pub fn new() -> Self {
Self {
tracked: Arc::new(Mutex::new(HashMap::new())),
}
}
fn runsc_bin() -> String {
std::env::var(RUNSC_BIN_ENV).unwrap_or_else(|_| "runsc".to_string())
}
fn bundle_root() -> PathBuf {
if let Ok(s) = std::env::var(BUNDLE_ROOT_ENV) {
return PathBuf::from(s);
}
let tmp = std::env::var("TMPDIR").unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(tmp).join("cellos-gvisor")
}
}
#[async_trait]
impl CellBackend for GVisorCellBackend {
#[instrument(skip(self, spec), fields(cell_id = %spec.spec.id))]
async fn create(&self, spec: &ExecutionCellDocument) -> Result<CellHandle, CellosError> {
let cfg = generate_bundle_config(spec)
.map_err(|e| CellosError::InvalidSpec(format!("gvisor bundle: {e}")))?;
let cell_id = spec.spec.id.clone();
let bundle_dir = Self::bundle_root().join(&cell_id);
let rootfs_dir = bundle_dir.join("rootfs");
std::fs::create_dir_all(&rootfs_dir).map_err(|e| {
CellosError::Host(format!("gvisor: create bundle dir {bundle_dir:?}: {e}"))
})?;
let config_path = bundle_dir.join("config.json");
let json = serde_json::to_vec_pretty(&cfg)
.map_err(|e| CellosError::Host(format!("gvisor: serialize config.json: {e}")))?;
std::fs::write(&config_path, json)
.map_err(|e| CellosError::Host(format!("gvisor: write {config_path:?}: {e}")))?;
let mut cmd = Command::new(Self::runsc_bin());
cmd.arg("run")
.arg("--bundle")
.arg(&bundle_dir)
.arg(&cell_id)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
let child = cmd.spawn().map_err(|e| {
CellosError::Host(format!(
"gvisor: spawn `runsc run --bundle {bundle_dir:?} {cell_id}` failed: {e}"
))
})?;
self.tracked.lock().await.insert(
cell_id.clone(),
TrackedCell {
bundle_dir: bundle_dir.clone(),
child,
},
);
Ok(CellHandle {
cell_id,
cgroup_path: None,
nft_rules_applied: None,
kernel_digest_sha256: None,
rootfs_digest_sha256: None,
firecracker_digest_sha256: None,
})
}
#[instrument(skip(self))]
async fn wait_for_in_vm_exit(&self, cell_id: &str) -> Option<Result<i32, CellosError>> {
let mut tracked = self.tracked.lock().await;
let entry = tracked.remove(cell_id)?;
drop(tracked);
let TrackedCell {
bundle_dir,
mut child,
} = entry;
let status = match child.wait().await {
Ok(s) => s,
Err(e) => {
return Some(Err(CellosError::Host(format!(
"gvisor: wait for runsc child of {cell_id}: {e}"
))));
}
};
let _ = bundle_dir;
Some(Ok(status.code().unwrap_or(-1)))
}
#[instrument(skip(self, handle), fields(cell_id = %handle.cell_id))]
async fn destroy(&self, handle: &CellHandle) -> Result<TeardownReport, CellosError> {
let mut tracked = self.tracked.lock().await;
let entry = tracked.remove(&handle.cell_id);
let still_tracked = tracked.len();
drop(tracked);
let runsc = Self::runsc_bin();
for (sub, args) in [
("kill", vec!["kill", &handle.cell_id, "SIGKILL"]),
("delete", vec!["delete", &handle.cell_id]),
] {
let res = Command::new(&runsc)
.args(&args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await;
if let Err(e) = res {
warn!(error = %e, "gvisor: `runsc {sub} {}` failed (continuing)", handle.cell_id);
}
}
if let Some(t) = entry {
if let Err(e) = std::fs::remove_dir_all(&t.bundle_dir) {
warn!(error = %e, bundle = ?t.bundle_dir, "gvisor: bundle cleanup failed");
}
}
Ok(TeardownReport {
cell_id: handle.cell_id.clone(),
destroyed: true,
peers_tracked_after: still_tracked,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn runsc_bin_respects_env_override() {
let prev = std::env::var(RUNSC_BIN_ENV).ok();
std::env::set_var(RUNSC_BIN_ENV, "/usr/local/bin/my-runsc");
assert_eq!(GVisorCellBackend::runsc_bin(), "/usr/local/bin/my-runsc");
match prev {
Some(v) => std::env::set_var(RUNSC_BIN_ENV, v),
None => std::env::remove_var(RUNSC_BIN_ENV),
}
}
#[test]
fn bundle_root_respects_env_override() {
let prev = std::env::var(BUNDLE_ROOT_ENV).ok();
std::env::set_var(BUNDLE_ROOT_ENV, "/var/lib/cellos-gvisor-test");
assert_eq!(
GVisorCellBackend::bundle_root(),
PathBuf::from("/var/lib/cellos-gvisor-test")
);
match prev {
Some(v) => std::env::set_var(BUNDLE_ROOT_ENV, v),
None => std::env::remove_var(BUNDLE_ROOT_ENV),
}
}
}