127 lines
4.2 KiB
Rust
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(())
|
|
}
|