use std::process::Command;
use std::time::Duration;
const NETWORK: &str = "zk-mesh";
const BROKER: &str = "zk-broker";
const DASH_BASE: u16 = 8090; const BROKER_PORT: u16 = 9000;
const WORKER_PORT: u16 = 3960;
fn node_image() -> String {
std::env::var("ZAKURO_MESH_IMAGE").unwrap_or_else(|_| "zakuro-image-compute:latest".to_string())
}
fn broker_image() -> String {
std::env::var("ZAKURO_MESH_BROKER_IMAGE").unwrap_or_else(|_| "zakuroai/zc:latest".to_string())
}
fn platform() -> Option<String> {
match std::env::var("ZAKURO_MESH_PLATFORM") {
Ok(p) if p.trim().is_empty() => None,
Ok(p) => Some(p),
Err(_) => Some("linux/amd64".to_string()),
}
}
fn cpus() -> String {
std::env::var("ZAKURO_MESH_CPUS").unwrap_or_else(|_| "2".to_string())
}
fn memory() -> String {
std::env::var("ZAKURO_MESH_MEM").unwrap_or_else(|_| "2g".to_string())
}
fn peer_key() -> String {
std::env::var("ZAKURO_MESH_PEER_KEY").unwrap_or_else(|_| "zakuro-mesh".to_string())
}
fn p2p_enabled() -> bool {
matches!(
std::env::var("ZAKURO_MESH_P2P").as_deref(),
Ok("1") | Ok("true")
)
}
fn docker(args: &[String]) -> Result<String, String> {
let out = Command::new("docker")
.args(args)
.output()
.map_err(|e| format!("docker not available: {}", e))?;
if out.status.success() {
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
} else {
Err(String::from_utf8_lossy(&out.stderr).trim().to_string())
}
}
fn d(args: &[&str]) -> Result<String, String> {
docker(&args.iter().map(|s| s.to_string()).collect::<Vec<_>>())
}
fn node_name(i: usize) -> String {
format!("zk{}", i)
}
pub fn up(nodes: usize) {
let nodes = nodes.clamp(1, 16);
let image = node_image();
let plat = platform();
let p2p = p2p_enabled();
if p2p {
println!(" zc mesh — P2P mode (ZAKURO_P2P=true, peer-key set)");
}
println!(
" zc mesh — launching {} node(s) from {} ({} cpu / {} mem each)",
nodes,
image,
cpus(),
memory()
);
let _ = d(&["network", "create", NETWORK]);
for i in 0..nodes {
let name = node_name(i);
let dash = (DASH_BASE + i as u16).to_string();
let _ = d(&["rm", "-f", &name]);
let mut args: Vec<String> = vec!["run".into(), "-d".into(), "--name".into(), name.clone()];
if let Some(p) = &plat {
args.push("--platform".into());
args.push(p.clone());
}
args.extend(["--cpus".into(), cpus(), "--memory".into(), memory()]);
args.extend([
"--hostname".into(),
name.clone(),
"--network".into(),
NETWORK.into(),
"--network-alias".into(),
name.clone(),
"-p".into(),
format!("{}:8080", dash),
]);
if i == 0 {
for pm in [
"8265:8265",
"8787:8787",
"7077:7077",
"8082:8082",
"8888:8888",
] {
args.push("-p".into());
args.push(pm.into());
}
}
args.extend([
"-e".into(),
"RAY_DASHBOARD_HOST=0.0.0.0".into(),
"-e".into(),
"SPARK_MASTER_WEBUI_PORT=8082".into(),
"-e".into(),
"JUPYTER_DISABLE_TOKEN=1".into(),
"-e".into(),
"SPARK_DAEMON_MEMORY=512m".into(),
]);
if p2p {
args.extend([
"-e".into(),
"ZAKURO_P2P=true".into(),
"-e".into(),
format!("ZAKURO_PEER_KEY={}", peer_key()),
]);
}
args.push(image.clone());
match docker(&args) {
Ok(_) => println!(" • node {} up (dashboard :{})", name, dash),
Err(e) => {
eprintln!(" ✗ failed to start {}: {}", name, e);
return;
}
}
let cmd = format!(
"ZAKURO_WORKER_NAME={n} python -m zakuro.worker.server --host 0.0.0.0 --port {p} --worker-name {n} >/tmp/worker.log 2>&1",
n = name,
p = WORKER_PORT
);
let _ = d(&["exec", "-d", &name, "sh", "-c", &cmd]);
}
let _ = d(&["rm", "-f", BROKER]);
let peers = (0..nodes)
.map(|i| format!("{}:{}", node_name(i), WORKER_PORT))
.collect::<Vec<_>>()
.join(",");
let mut bargs: Vec<String> = vec!["run".into(), "-d".into(), "--name".into(), BROKER.into()];
if let Some(p) = &plat {
bargs.push("--platform".into());
bargs.push(p.clone());
}
bargs.extend([
"--network".into(),
NETWORK.into(),
"--network-alias".into(),
"broker".into(),
"-p".into(),
format!("{}:9000", BROKER_PORT),
"-e".into(),
format!("ZAKURO_PEERS={}", peers),
"-e".into(),
"ZAKURO_SCAN_INTERVAL=5".into(),
]);
if p2p {
bargs.extend([
"-e".into(),
"ZAKURO_P2P=true".into(),
"-e".into(),
format!("ZAKURO_PEER_KEY={}", peer_key()),
]);
}
bargs.extend([broker_image(), "broker".into()]);
match docker(&bargs) {
Ok(_) => println!(" • broker up (:{})", BROKER_PORT),
Err(e) => {
eprintln!(" ✗ failed to start broker: {}", e);
return;
}
}
print!(" waiting for fleet");
let _ = std::io::Write::flush(&mut std::io::stdout());
let url = format!("http://localhost:{}/workers", BROKER_PORT);
let mut found = 0;
for _ in 0..30 {
std::thread::sleep(Duration::from_secs(2));
print!(".");
let _ = std::io::Write::flush(&mut std::io::stdout());
if let Ok(resp) = ureq::get(&url)
.config()
.timeout_global(Some(Duration::from_secs(3)))
.build()
.call()
{
if let Ok(body) = resp.into_body().read_to_string() {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&body) {
found = json["total"].as_u64().unwrap_or(0) as usize;
if found >= nodes {
break;
}
}
}
}
}
println!();
println!(
" ✓ mesh up: {}/{} workers registered · dashboards :{}-:{} · broker :{}",
found,
nodes,
DASH_BASE,
DASH_BASE + (nodes as u16 - 1),
BROKER_PORT
);
println!(" Run `zc workers` (or `/discovery` in the shell) to list the fleet.");
}
pub fn down() {
println!(" zc mesh — tearing down");
let mut names: Vec<String> = (0..16).map(node_name).collect();
names.push(BROKER.to_string());
let mut removed = 0;
for name in &names {
if d(&["rm", "-f", name])
.map(|s| !s.is_empty())
.unwrap_or(false)
{
removed += 1;
}
}
println!(" ✓ removed {} container(s)", removed);
}
pub fn status() {
let out = d(&[
"ps",
"--filter",
&format!("network={}", NETWORK),
"--format",
"{{.Names}}|{{.Status}}|{{.Ports}}",
]);
let out = match out {
Ok(o) => o,
Err(e) => {
eprintln!(" mesh status error: {}", e);
return;
}
};
if out.is_empty() {
println!(" zc mesh — no nodes running (try `zc mesh up`)");
return;
}
println!(" zc mesh — running containers:");
println!(" {:<11} {:<10} PORTS", "NAME", "STATE");
for line in out.lines() {
let p: Vec<&str> = line.splitn(3, '|').collect();
let name = p.first().copied().unwrap_or("-");
let raw_state = p.get(1).copied().unwrap_or("");
let state = if raw_state.contains("healthy") {
"healthy"
} else if raw_state.starts_with("Up") {
"up"
} else {
raw_state.split_whitespace().next().unwrap_or("-")
};
let ports = p.get(2).copied().unwrap_or("");
let mut host_ports: Vec<String> = ports
.split(',')
.filter_map(|seg| {
let seg = seg.trim();
seg.rsplit_once("->")
.and_then(|(left, _)| left.rsplit(':').next())
.map(|hp| hp.to_string())
})
.collect();
host_ports.sort();
host_ports.dedup();
println!(" {:<11} {:<10} {}", name, state, host_ports.join(", "));
}
}