use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use crate::{canonical_label, canonical_name, ContainerRunSpec};
pub const DEFAULT_WARDEN_IMAGE: &str = "ghcr.io/yah-ai/yah-yubaba:latest";
pub const DEFAULT_DOCKER_SOCKET_PATH: &str = "/var/run/docker.sock";
pub const DOCKER_SOCKET_CONTAINER_PATH: &str = "/var/run/docker.sock";
pub const WARDEN_STATE_CONTAINER_PATH: &str = "/var/lib/yah-yubaba";
pub const DEFAULT_WARDEN_HTTP_PORT: u16 = 8800;
pub const WARDEN_SLOT: &str = "yubaba";
pub const WARDEN_CAMP_SCOPE: &str = "camp";
pub fn warden_container_name(env: &str) -> String {
canonical_name(WARDEN_CAMP_SCOPE, env, WARDEN_SLOT)
}
pub fn warden_container_label(env: &str) -> String {
canonical_label(WARDEN_CAMP_SCOPE, env, WARDEN_SLOT)
}
#[derive(Debug, Clone)]
pub struct WardenContainerSpec {
pub image: String,
pub env: String,
pub http_port: u16,
pub docker_socket_path: PathBuf,
pub state_dir: PathBuf,
pub pond_state_root: Option<PathBuf>,
pub extra_env: BTreeMap<String, String>,
}
impl WardenContainerSpec {
pub fn new(env: impl Into<String>, state_dir: PathBuf) -> Self {
Self {
image: DEFAULT_WARDEN_IMAGE.into(),
env: env.into(),
http_port: 0,
docker_socket_path: PathBuf::from(DEFAULT_DOCKER_SOCKET_PATH),
state_dir,
pond_state_root: None,
extra_env: BTreeMap::new(),
}
}
}
pub const DEFAULT_WARDEN_ARGS: &str = "--bind 0.0.0.0:8800 \
--state /var/lib/yah-yubaba/identity.json \
--kamaji-socket /run/kamaji/kamaji.sock";
const KAMAJI_DOCKER_ENV_VALUE: &str = "unix:///var/run/docker.sock";
pub fn build_warden_run_spec(spec: &WardenContainerSpec) -> ContainerRunSpec {
let mut env = spec.extra_env.clone();
env.entry("YAH_WARDEN_ARGS".into())
.or_insert_with(|| DEFAULT_WARDEN_ARGS.into());
env.entry(crate::POND_PROBE_HOST_ENV.into())
.or_insert_with(|| "host.docker.internal".into());
env.entry("KAMAJI_DOCKER".into())
.or_insert_with(|| KAMAJI_DOCKER_ENV_VALUE.into());
let mut volumes = vec![
(
spec.docker_socket_path.clone(),
DOCKER_SOCKET_CONTAINER_PATH.into(),
),
(spec.state_dir.clone(), WARDEN_STATE_CONTAINER_PATH.into()),
];
if let Some(root) = &spec.pond_state_root {
volumes.push((root.clone(), root.display().to_string()));
}
ContainerRunSpec {
name: warden_container_name(&spec.env),
image: spec.image.clone(),
label: warden_container_label(&spec.env),
ports: vec![(spec.http_port, DEFAULT_WARDEN_HTTP_PORT)],
env,
volumes,
cmd: vec![],
cap_add: vec!["SYS_ADMIN".into()],
cgroupns: Some("private".into()),
network: None,
network_aliases: vec![],
extra_hosts: vec!["host.docker.internal:host-gateway".into()],
}
}
pub fn looks_like_docker_socket(host_socket_path: &Path) -> bool {
let s = host_socket_path.to_string_lossy();
s == "/var/run/docker.sock"
|| s.ends_with("/docker.sock")
|| s.ends_with(".sock")
}
#[cfg(test)]
mod tests {
use super::*;
fn spec_for_test() -> WardenContainerSpec {
let mut s = WardenContainerSpec::new("pond", PathBuf::from("/tmp/state"));
s.http_port = 8800;
s
}
#[test]
fn run_spec_has_camp_scoped_name_and_label() {
let crs = build_warden_run_spec(&spec_for_test());
assert_eq!(crs.name, "yah-pond-camp-pond-yubaba");
assert_eq!(crs.name, warden_container_name("pond"));
assert!(crs.label.contains("camp:pond:yubaba"));
assert_eq!(crs.label, warden_container_label("pond"));
}
#[test]
fn run_spec_mounts_docker_socket() {
let crs = build_warden_run_spec(&spec_for_test());
let has_socket = crs.volumes.iter().any(|(h, c)| {
h == &PathBuf::from(DEFAULT_DOCKER_SOCKET_PATH) && c == DOCKER_SOCKET_CONTAINER_PATH
});
assert!(has_socket, "expected docker socket mount, got {:?}", crs.volumes);
}
#[test]
fn run_spec_mounts_state_dir() {
let crs = build_warden_run_spec(&spec_for_test());
let has_state = crs.volumes.iter().any(|(h, c)| {
h == &PathBuf::from("/tmp/state") && c == WARDEN_STATE_CONTAINER_PATH
});
assert!(has_state, "expected state-dir mount, got {:?}", crs.volumes);
}
#[test]
fn run_spec_requests_private_cgroupns_and_sys_admin() {
let crs = build_warden_run_spec(&spec_for_test());
assert_eq!(crs.cgroupns.as_deref(), Some("private"));
assert!(crs.cap_add.iter().any(|c| c == "SYS_ADMIN"));
}
#[test]
fn run_spec_exposes_http_port() {
let mut spec = spec_for_test();
spec.http_port = 14321;
let crs = build_warden_run_spec(&spec);
assert_eq!(crs.ports, vec![(14321, DEFAULT_WARDEN_HTTP_PORT)]);
}
#[test]
fn run_spec_injects_default_warden_args() {
let crs = build_warden_run_spec(&spec_for_test());
assert_eq!(
crs.env.get("YAH_WARDEN_ARGS").map(String::as_str),
Some(DEFAULT_WARDEN_ARGS),
"build_warden_run_spec must inject YAH_WARDEN_ARGS so pond-supervise.sh \
starts `yah-yubaba serve` on the expected port"
);
}
#[test]
fn warden_wiring_routes_pond_through_kamaji() {
let crs = build_warden_run_spec(&spec_for_test());
let args = crs.env.get("YAH_WARDEN_ARGS").map(String::as_str).unwrap();
assert!(
args.contains("--kamaji-socket /run/kamaji/kamaji.sock"),
"yubaba must dial the kamaji sibling: {args:?}"
);
assert_eq!(
crs.env.get("KAMAJI_DOCKER").map(String::as_str),
Some("unix:///var/run/docker.sock"),
);
}
#[test]
fn kamaji_docker_points_at_the_mounted_socket() {
assert_eq!(
KAMAJI_DOCKER_ENV_VALUE,
format!("unix://{DOCKER_SOCKET_CONTAINER_PATH}"),
);
}
#[test]
fn default_warden_args_must_not_repeat_serve_subcommand() {
let crs = build_warden_run_spec(&spec_for_test());
let args = crs.env.get("YAH_WARDEN_ARGS").map(String::as_str).unwrap();
let first = args.split_whitespace().next();
assert_ne!(
first,
Some("serve"),
"YAH_WARDEN_ARGS must be flags-only; pond-supervise.sh owns the `serve` \
token. Leading `serve` here doubles the subcommand and crash-loops \
the container (R471 redux). Got: {args:?}"
);
}
#[test]
fn run_spec_extra_env_overrides_warden_args() {
let mut spec = spec_for_test();
spec.extra_env.insert("YAH_WARDEN_ARGS".into(), "--bind 0.0.0.0:9900".into());
let crs = build_warden_run_spec(&spec);
assert_eq!(
crs.env.get("YAH_WARDEN_ARGS").map(String::as_str),
Some("--bind 0.0.0.0:9900"),
);
}
#[test]
fn run_spec_forwards_extra_env() {
let mut spec = spec_for_test();
spec.extra_env.insert("RUST_LOG".into(), "yubaba=debug".into());
let crs = build_warden_run_spec(&spec);
assert_eq!(crs.env.get("RUST_LOG").map(String::as_str), Some("yubaba=debug"));
}
#[test]
fn docker_run_argv_carries_cgroupns_and_cap_add() {
let crs = build_warden_run_spec(&spec_for_test());
let argv = crs.docker_run_args();
assert!(
argv.iter().any(|a| a == "--cgroupns=private"),
"missing --cgroupns=private in {argv:?}",
);
let cap_idx = argv.iter().position(|a| a == "--cap-add");
assert!(cap_idx.is_some(), "missing --cap-add in {argv:?}");
assert_eq!(argv.get(cap_idx.unwrap() + 1).map(String::as_str), Some("SYS_ADMIN"));
}
#[test]
fn looks_like_docker_socket_classifies_common_paths() {
assert!(looks_like_docker_socket(Path::new("/var/run/docker.sock")));
assert!(looks_like_docker_socket(Path::new(
"/Users/user/.orbstack/run/docker.sock"
)));
assert!(looks_like_docker_socket(Path::new(
"/run/user/501/docker.sock"
)));
assert!(looks_like_docker_socket(Path::new("/tmp/colima.sock")));
assert!(!looks_like_docker_socket(Path::new("/var/run/foo")));
}
}