Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/hive-c0re/src/lifecycle.rs

127 lines
4.2 KiB
Rust

//! Thin async wrappers over `nixos-container`.
use std::path::Path;
use anyhow::{Context, Result, bail};
use tokio::process::Command;
/// Sub-agent container prefix. `nixos-container` caps the total container name
/// at 11 chars (it gets encoded into network interface names), so the agent
/// name itself can be at most `MAX_AGENT_NAME` chars.
pub const AGENT_PREFIX: &str = "h-";
pub const MAX_AGENT_NAME: usize = 9;
/// Container name of the manager (a separate slot from sub-agents).
pub const MANAGER_NAME: &str = "hm1nd";
/// Mount point of the per-agent runtime directory inside the container.
pub const CONTAINER_RUNTIME_MOUNT: &str = "/run/hive";
pub fn container_name(name: &str) -> String {
format!("{AGENT_PREFIX}{name}")
}
fn validate(name: &str) -> Result<()> {
if name.is_empty() {
bail!("agent name must not be empty");
}
if name.len() > MAX_AGENT_NAME {
bail!(
"agent name '{name}' is too long ({} chars); max {MAX_AGENT_NAME}",
name.len()
);
}
Ok(())
}
pub async fn spawn(name: &str, agent_flake: &str, agent_dir: &Path) -> Result<()> {
validate(name)?;
let container = container_name(name);
run(&["create", &container, "--flake", agent_flake]).await?;
set_bind_flag(&container, agent_dir)?;
run(&["start", &container]).await
}
/// `nixos-container` doesn't expose `--bind` on the CLI, but its start script
/// expands `$EXTRA_NSPAWN_FLAGS` (from `/etc/nixos-containers/<name>.conf`)
/// unquoted into the `systemd-nspawn` invocation. Idempotently replace the
/// `EXTRA_NSPAWN_FLAGS` line with the bind we want.
fn set_bind_flag(container: &str, agent_dir: &Path) -> Result<()> {
let path = format!("/etc/nixos-containers/{container}.conf");
let original = std::fs::read_to_string(&path).with_context(|| format!("read {path}"))?;
let mut lines: Vec<String> = original
.lines()
.filter(|line| !line.trim_start().starts_with("EXTRA_NSPAWN_FLAGS="))
.map(str::to_owned)
.collect();
lines.push(format!(
"EXTRA_NSPAWN_FLAGS=\"--bind={}:{CONTAINER_RUNTIME_MOUNT}\"",
agent_dir.display()
));
let mut content = lines.join("\n");
content.push('\n');
std::fs::write(&path, content).with_context(|| format!("write {path}"))?;
tracing::info!(%path, "set EXTRA_NSPAWN_FLAGS for bind mount");
Ok(())
}
pub async fn kill(name: &str) -> Result<()> {
validate(name)?;
let container = container_name(name);
run(&["stop", &container]).await
}
pub async fn rebuild(name: &str, agent_flake: &str, agent_dir: &Path) -> Result<()> {
validate(name)?;
let container = container_name(name);
set_bind_flag(&container, agent_dir)?;
run(&["update", &container, "--flake", agent_flake]).await?;
// Restart so any nspawn-level changes (bind mounts, networking, etc.) apply.
run(&["stop", &container]).await?;
run(&["start", &container]).await
}
pub async fn list() -> Result<Vec<String>> {
let out = Command::new("nixos-container")
.arg("list")
.output()
.await
.context("invoke nixos-container list")?;
if !out.status.success() {
bail!(
"nixos-container list exited with status {}: {}",
out.status,
String::from_utf8_lossy(&out.stderr).trim()
);
}
Ok(String::from_utf8_lossy(&out.stdout)
.lines()
.map(str::trim)
.filter(|line| line.starts_with(AGENT_PREFIX) || *line == MANAGER_NAME)
.map(str::to_owned)
.collect())
}
async fn run(args: &[&str]) -> Result<()> {
let out = Command::new("nixos-container")
.args(args)
.output()
.await
.with_context(|| format!("invoke nixos-container {}", args.join(" ")))?;
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
if !stdout.trim().is_empty() {
tracing::info!(target: "nixos-container", "{}", stdout.trim());
}
if !stderr.trim().is_empty() {
tracing::warn!(target: "nixos-container", "{}", stderr.trim());
}
if !out.status.success() {
bail!(
"nixos-container {} failed ({}): {}",
args.join(" "),
out.status,
stderr.trim()
);
}
Ok(())
}