Menu
AkurAI-Build
publicLatest change a9ee1aad5871410be579b77bfd0277c1d4e891d4 - policy: enforce SDP-002..006/010 in config::parse, add fleet-audit command by Ólafur Búi Ólafsson
//! Host-side verbs: `ssh`, `ship`, `status`, `processes`/`ps`, `logs`, `ports`,
//! `allow-ssh`, `health`.
//!
//! Ported verbatim from the `akurai-ec2` bash CLI. The box-side introspection
//! (systemd, ss, nginx) is inherently shell, so those payloads run unchanged on
//! the remote; everything else (registry parsing, drift analysis, health
//! fan-out) is native Rust with the same output contract.
use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::process::{Command, Stdio};
use anyhow::{Context, Result, bail};
use serde_json::Value;
use super::{APP_POOL_HI, APP_POOL_LO, Ec2, say, shell_quote};
/// `status` payload — remote introspection, verbatim from the bash.
const STATUS_SCRIPT: &str = r#"echo "=== app services ==="
systemctl list-units --type=service --state=running | grep -iE 'akurai|golfset|mail|idp|bunfork' || true
echo "=== listening ports ==="
sudo ss -tlnp 2>/dev/null | grep -E 'LISTEN' | awk '{print $4}' | sort -u | tail -20
echo "=== nginx vhosts ==="
ls /etc/nginx/sites-enabled/
echo "=== disk ==="
df -h / | tail -1
"#;
/// `processes` remote script — unit/state/pid/mem/uptime/ports, verbatim.
const PROCESS_SCRIPT: &str = r#"pat="$1"
# pid -> listening port(s), so each unit can show what it's actually serving.
declare -A pid_ports
while read -r line; do
laddr=$(echo "$line" | awk '{print $4}'); port=${laddr##*:}
case "$port" in (*[!0-9]*|"") continue;; esac
pid=$(echo "$line" | grep -oE 'pid=[0-9]+' | head -1 | grep -oE '[0-9]+')
[ -n "$pid" ] && pid_ports[$pid]+="$port "
done < <(sudo ss -ltnpH 2>/dev/null)
printf '%-24s %-9s %-7s %-8s %-19s %s\n' UNIT STATE PID MEM SINCE PORTS
n=0
for unit in $(systemctl list-units --type=service --state=running --no-legend --plain 2>/dev/null | awk '{print $1}' | grep -iE "$pat" | sort -u); do
pid=$(systemctl show "$unit" -p MainPID --value 2>/dev/null)
sub=$(systemctl show "$unit" -p SubState --value 2>/dev/null)
memraw=$(systemctl show "$unit" -p MemoryCurrent --value 2>/dev/null)
since=$(systemctl show "$unit" -p ActiveEnterTimestamp --value 2>/dev/null | awk '{print $2" "$3}')
mem="-"
case "$memraw" in (''|'[not set]'|18446744073709551615) ;; (*) mem=$(numfmt --to=iec "$memraw" 2>/dev/null || echo "$memraw");; esac
ports="${pid_ports[$pid]:-—}"
printf '%-24s %-9s %-7s %-8s %-19s %s\n' "${unit%.service}" "$sub" "$pid" "$mem" "$since" "$ports"
n=$((n+1))
done
echo
echo "total running: $n"
"#;
/// `ports` live-state fetch — port->unit (via cgroup) and vhost->port, verbatim.
const RFETCH_SCRIPT: &str = r####"echo "### PORTS"
sudo ss -ltnpH 2>/dev/null | while read -r line; do
laddr=$(echo "$line" | awk '{print $4}'); port=${laddr##*:}
case "$port" in (*[!0-9]*|"") continue;; esac
pid=$(echo "$line" | grep -oE 'pid=[0-9]+' | head -1 | grep -oE '[0-9]+'); unit=""
[ -n "$pid" ] && unit=$(grep -aoE '[A-Za-z0-9_.@-]+\.service' /proc/$pid/cgroup 2>/dev/null | head -1)
printf '%s|%s\n' "$port" "${unit%.service}"
done | sort -t'|' -k1,1n -u
echo "### VHOSTS"
for f in /etc/nginx/sites-enabled/*; do
n=$(basename "$f")
# Resolve the ROOT "location / " upstream (trailing space avoids matching /mcp etc).
# Handle both direct (proxy_pass http://127.0.0.1:PORT) and named upstreams
# (proxy_pass http://name; upstream name { server 127.0.0.1:PORT; }).
tgt=$(grep -A4 'location / ' "$f" 2>/dev/null | grep -oE 'proxy_pass http://[^;]+' | head -1 | sed 's#proxy_pass http://##')
case "$tgt" in
127.0.0.1:*) p=${tgt##*:} ;;
"") p=static ;;
*) p=$(grep -E "upstream[[:space:]]+${tgt}[[:space:]]*\{" -A3 "$f" 2>/dev/null | grep -oE 'server[[:space:]]+127\.0\.0\.1:[0-9]+' | grep -oE '[0-9]+$' | head -1)
[ -z "$p" ] && p=static ;;
esac
printf '%s|%s\n' "$n" "${p:-static}"
done
"####;
/// `processes` default fleet match (egrep alternation).
const DEFAULT_PROCESS_PATTERN: &str = "akurai|golfset|idp|mail|router|drive|fulltrui|platform|notes|passvault|crm|vpn|monitor|bunfork";
/// `akurai-ec2 ssh '<cmd>'` — run a command on the box, inheriting stdio.
pub fn ssh(ec2: &Ec2, args: &[String]) -> Result<()> {
// ssh joins its post-host argv with spaces into the remote command; joining
// here reproduces that exactly (the self-heal lives in Ec2's ssh wrapper).
let command = args.join(" ");
let code = ec2.ssh_streaming_status(&command)?;
if code != 0 {
bail!("ssh {}: command failed (exit {code})", ec2.ssh_host);
}
Ok(())
}
/// `akurai-ec2 ship <local> <remote>` — scp a file up (creates the parent dir).
pub fn ship(ec2: &Ec2, local: &Path, remote: &str) -> Result<()> {
ec2.ship(local, remote)?;
say(&format!("shipped {} → {remote}", local.display()));
Ok(())
}
/// `akurai-ec2 status` — services, ports, nginx, disk.
pub fn status(ec2: &Ec2) -> Result<()> {
ec2.ssh_stdin_streaming("bash -s", STATUS_SCRIPT.as_bytes())
}
/// `akurai-ec2 processes [pattern]` / `ps` — the running AkurAI fleet.
pub fn processes(ec2: &Ec2, pattern: Option<&str>) -> Result<()> {
let pat = pattern.unwrap_or(DEFAULT_PROCESS_PATTERN);
say(&format!(
"AkurAI processes on {} (match: {pat})",
ec2.ssh_host
));
let command = format!("bash -s -- {}", shell_quote(pat));
ec2.ssh_stdin_streaming(&command, PROCESS_SCRIPT.as_bytes())
}
/// `akurai-ec2 logs <service>` — journalctl -u <service> -n 80.
pub fn logs(ec2: &Ec2, service: &str) -> Result<()> {
// Quoted, unlike the bash: a service name is a unit identifier, never shell
// code. Behaviour for every valid name is identical to the original.
ec2.ssh_streaming(&format!(
"journalctl -u {} -n 80 --no-pager",
shell_quote(service)
))
}
/// `akurai-ec2 ports [list|free [n]|check <port>|drift]`.
pub fn ports(ec2: &Ec2, args: &[String]) -> Result<()> {
if !ec2.ports_registry.exists() {
bail!(
"registry not found: {} (see the _VMPORTS skill)",
ec2.ports_registry.display()
);
}
let sub = args.first().map(String::as_str).unwrap_or("list");
let rest = &args[args.len().min(1)..];
let live_text = ec2.ssh(RFETCH_SCRIPT)?;
let live = parse_live(&live_text);
let registry_text = std::fs::read_to_string(&ec2.ports_registry)
.with_context(|| format!("reading {}", ec2.ports_registry.display()))?;
let reg = parse_registry(®istry_text);
match sub {
"list" => list_ports(®, &live),
"free" => free_ports_cmd(®, &live, rest),
"check" => check_port(®, &live, rest),
"drift" => drift(®, &live),
other => {
bail!("usage: akurai-ec2 ports [list|free [n]|check <port>|drift] (got {other:?})")
}
}
}
#[derive(Default)]
struct Registry {
/// Numeric registry ports, in file order.
ports: Vec<u16>,
unit: HashMap<u16, String>,
domain: HashMap<u16, String>,
pool: HashMap<u16, String>,
note: HashMap<u16, String>,
}
fn parse_registry(text: &str) -> Registry {
let mut reg = Registry::default();
for line in text.lines() {
let mut fields = line.splitn(6, '|');
let port_raw = fields.next().unwrap_or("").trim();
let unit = fields.next().unwrap_or("").trim().to_string();
let domain = fields.next().unwrap_or("").trim().to_string();
let _repo = fields.next().unwrap_or("").trim();
let pool = fields.next().unwrap_or("").trim().to_string();
// Bash `IFS='|' read` folds any further separators into the last field.
let note = fields.next().unwrap_or("").trim().to_string();
if port_raw.is_empty() || port_raw.starts_with('#') {
continue;
}
let Ok(p) = port_raw.parse::<u16>() else {
continue;
};
reg.ports.push(p);
reg.unit.insert(p, unit);
reg.domain.insert(p, domain);
reg.pool.insert(p, pool);
reg.note.insert(p, note);
}
reg
}
#[derive(Default)]
struct LiveState {
/// port -> unit (empty unit = bound by something unlabelled).
unit: HashMap<u16, String>,
/// vhost name -> port string ("static" when no upstream).
vhost: HashMap<String, String>,
}
fn parse_live(text: &str) -> LiveState {
let mut state = LiveState::default();
let mut section = "";
for line in text.lines() {
match line {
"### PORTS" => {
section = "p";
continue;
}
"### VHOSTS" => {
section = "v";
continue;
}
"" => continue,
_ => {}
}
let Some((key, value)) = line.split_once('|') else {
continue;
};
if section == "p" {
if let Ok(p) = key.parse::<u16>() {
state.unit.insert(p, value.to_string());
}
} else if section == "v" {
state.vhost.insert(key.to_string(), value.to_string());
}
}
state
}
/// Taken if claimed in the registry OR bound live.
fn is_taken(reg: &Registry, live: &LiveState, p: u16) -> bool {
reg.unit.contains_key(&p) || live.unit.contains_key(&p)
}
fn list_ports(reg: &Registry, live: &LiveState) -> Result<()> {
println!(
"\x1b[1m{:<6} {:<20} {:<7} {:<30} LIVE\x1b[0m",
"PORT", "UNIT", "POOL", "DOMAIN"
);
let mut sorted = reg.ports.clone();
sorted.sort_unstable();
for p in sorted {
let want = reg.unit.get(&p).map(String::as_str).unwrap_or("");
let got = live.unit.get(&p).map(String::as_str).unwrap_or("");
let pool = reg.pool.get(&p).map(String::as_str).unwrap_or("");
let domain = reg.domain.get(&p).map(String::as_str).unwrap_or("");
// System services carry a descriptive label, not an exact unit name —
// any live binding counts as bound for them.
let mark = if want == got || (pool == "system" && !got.is_empty()) {
"\x1b[0;32m●\x1b[0m bound".to_string()
} else if !got.is_empty() {
format!("\x1b[0;31m✗\x1b[0m {got}")
} else {
"\x1b[0;33m○\x1b[0m not bound".to_string()
};
println!("{p:<6} {want:<20} {pool:<7} {domain:<30} {mark}");
}
// Live app-pool ports with no registry row = unregistered.
let mut live_keys: Vec<u16> = live.unit.keys().copied().collect();
live_keys.sort_unstable();
let unregistered: Vec<String> = live_keys
.into_iter()
.filter(|p| (APP_POOL_LO..=APP_POOL_HI).contains(p))
.filter(|p| !reg.unit.contains_key(p))
.map(|p| format!(" {p} -> {}", live.unit[&p]))
.collect();
if !unregistered.is_empty() {
println!("\n\x1b[1;31munregistered live ports (app pool):\x1b[0m");
for line in unregistered {
println!("{line}");
}
}
Ok(())
}
fn free_ports_cmd(reg: &Registry, live: &LiveState, rest: &[String]) -> Result<()> {
let n = match rest.first() {
None => 1,
Some(raw) => raw
.parse::<u32>()
.context("ports free: count must be an integer")?,
};
let found: Vec<u16> = free_ports(reg, live).into_iter().take(n as usize).collect();
for port in &found {
println!("{port}");
}
if found.is_empty() {
bail!("no free ports in {APP_POOL_LO}-{APP_POOL_HI}");
}
Ok(())
}
fn check_port(reg: &Registry, live: &LiveState, rest: &[String]) -> Result<()> {
let Some(raw) = rest.first() else {
bail!("usage: akurai-ec2 ports check <port>");
};
say(&format!("port {raw}"));
// Non-numeric ports never match anything (bash associative-array lookup).
let p = raw.parse::<u16>().ok();
if let Some(p) = p {
let unit = reg.unit.get(&p).map(String::as_str).unwrap_or("");
if !unit.is_empty() {
let pool = reg.pool.get(&p).map(String::as_str).unwrap_or("");
let domain = reg.domain.get(&p).map(String::as_str).unwrap_or("");
let note = reg.note.get(&p).map(String::as_str).unwrap_or("");
println!(" registry: {unit} ({pool}) {domain} — {note}");
} else {
println!(" registry: (unclaimed)");
}
if let Some(live_unit) = live.unit.get(&p) {
println!(" live: bound by {live_unit}");
} else {
println!(" live: not bound");
}
if is_taken(reg, live, p) {
println!("\x1b[1;31m => TAKEN\x1b[0m");
} else {
println!("\x1b[1;32m => FREE\x1b[0m");
}
} else {
println!(" registry: (unclaimed)");
println!(" live: not bound");
println!("\x1b[1;32m => FREE\x1b[0m");
}
Ok(())
}
fn drift(reg: &Registry, live: &LiveState) -> Result<()> {
let mut issues = 0usize;
let mut flagged_domains: HashSet<&str> = HashSet::new();
// (a) Registry port expected unit U, but live differs / missing.
let mut sorted = reg.ports.clone();
sorted.sort_unstable();
for p in sorted {
if reg.pool.get(&p).map(String::as_str) == Some("system") {
continue;
}
let want = reg.unit.get(&p).map(String::as_str).unwrap_or("");
if want.is_empty() {
continue;
}
let got = live.unit.get(&p).map(String::as_str).unwrap_or("");
let note = reg.note.get(&p).map(String::as_str).unwrap_or("");
if got.is_empty() {
println!("\x1b[0;33m○ {want:<18} NOT BOUND on its port {p}\x1b[0m ({note})");
issues += 1;
} else if got != want {
println!("\x1b[0;31m✗ port {p}: expected {want}, live is {got}\x1b[0m");
issues += 1;
}
}
// (b) nginx vhost points at a port other than what the registry intends.
// A port may carry comma-separated domain aliases; a "host/path" entry is
// path-routed (e.g. notes /mcp) and is not its own vhost — skip it.
let mut domains: Vec<u16> = reg.domain.keys().copied().collect();
domains.sort_unstable();
for p in domains {
let raw = reg.domain.get(&p).map(String::as_str).unwrap_or("");
if raw == "-" {
continue;
}
let unit = reg.unit.get(&p).map(String::as_str).unwrap_or("");
for d in raw.split(',') {
if d.contains('/') {
continue;
}
let Some(lv) = live.vhost.get(d) else {
continue;
};
let flagged = flagged_domains.contains(d);
if lv == "static" {
println!(
"\x1b[0;33m○ vhost {d} is static — registry routes it to {p} ({unit})\x1b[0m"
);
flagged_domains.insert(d);
issues += 1;
} else if lv != &p.to_string() {
println!("\x1b[0;31m✗ vhost {d} -> {lv}, registry intends {p} ({unit})\x1b[0m");
flagged_domains.insert(d);
issues += 1;
} else if flagged {
// Already reported; keep the set for phase (d).
}
}
}
// (c) Live app-pool listeners with no registry row.
let mut live_keys: Vec<u16> = live.unit.keys().copied().collect();
live_keys.sort_unstable();
for &p in &live_keys {
if !(APP_POOL_LO..=APP_POOL_HI).contains(&p) {
continue;
}
if !reg.unit.contains_key(&p) {
println!(
"\x1b[0;31m✗ port {p} bound by {} — not in registry\x1b[0m",
live.unit[&p]
);
issues += 1;
}
}
// (d) Live vhost serving an app-pool port that belongs to a different app.
let mut vhosts: Vec<(&String, &String)> = live.vhost.iter().collect();
vhosts.sort_by(|a, b| a.0.cmp(b.0));
for (d, lv) in vhosts {
if flagged_domains.contains(d.as_str()) {
continue; // already reported in (b)
}
let Ok(lv_port) = lv.parse::<u16>() else {
continue;
};
if !(APP_POOL_LO..=APP_POOL_HI).contains(&lv_port) {
continue;
}
let raw = reg.domain.get(&lv_port).map(String::as_str).unwrap_or("");
if raw.is_empty() || raw == "-" {
continue;
}
let owner = raw.split(',').any(|alias| alias == d);
if !owner {
let unit = reg.unit.get(&lv_port).map(String::as_str).unwrap_or("");
println!("\x1b[0;31m✗ vhost {d} -> {lv}, but {lv} is {unit} ({raw})\x1b[0m");
issues += 1;
}
}
if issues == 0 {
say("no drift — registry matches the box");
Ok(())
} else {
bail!("{issues} drift issue(s) — see above");
}
}
/// `akurai-ec2 allow-ssh [ip]` — authorize an IP on the SG, then verify SSH.
pub fn allow_ssh(ec2: &Ec2, ip: Option<&str>) -> Result<()> {
ec2.ensure_my_ip(ip).context("could not authorize SSH")?;
say(&format!("Verifying SSH to {}…", ec2.ssh_host));
let ok = Command::new("ssh")
.arg("-F")
.arg(&ec2.ssh_config)
.arg("-o")
.arg("ConnectTimeout=15")
.arg("-o")
.arg("BatchMode=yes")
.arg(&ec2.ssh_host)
.arg("echo ok")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false);
if ok {
say(&format!("SSH OK ✓ ({})", ec2.ssh_host));
Ok(())
} else {
bail!(
"authorized, but SSH to {} still fails (route/key/DNS?) — investigate",
ec2.ssh_host
);
}
}
/// `akurai-ec2 health [--all]` — fan-out health check across AKURAI_APPS.toml.
pub fn health(_ec2: &Ec2, _all: bool) -> Result<()> {
let home = std::env::var("HOME").context("HOME is not set")?;
let apps_toml = Path::new(&home)
.join("Projects")
.join("AkurAI-Framework")
.join("AKURAI_APPS.toml");
if !apps_toml.exists() {
bail!("AKURAI_APPS.toml not found at {}", apps_toml.display());
}
println!("=== Fleet Health Check ===");
let text = std::fs::read_to_string(&apps_toml)
.with_context(|| format!("reading {}", apps_toml.display()))?;
for app in parse_apps_toml(&text) {
probe_app(&app);
}
Ok(())
}
/// One app's fleet-audit finding: exact hosted commit (when resolvable),
/// pipeline conformance violations, and evidence still missing. Pipeline
/// conformance (static SDP-002..006/010 pattern checks via
/// `config::parse`) is reported distinctly from persisted deployment
/// success (SDP-006/007/009/010 runtime evidence), which this command
/// cannot itself observe — declaring a rule satisfied in `.akurai.yml`
/// text is never accepted as proof a production deploy actually ran.
#[derive(serde::Serialize)]
struct FleetAuditEntry {
app: String,
hosted_commit: Option<String>,
violations: Vec<String>,
missing_evidence: Vec<&'static str>,
}
/// `akurai-ec2 fleet-audit [--json]` — walk every active app in
/// AKURAI_APPS.toml, resolve its hosted commit, and validate its
/// `.akurai.yml` production job(s) against the suite deployment policy
/// (docs/suite-deployment-policy.md). Retired apps are skipped.
pub fn fleet_audit(json: bool) -> Result<()> {
let home = std::env::var("HOME").context("HOME is not set")?;
let apps_toml = Path::new(&home)
.join("Projects")
.join("AkurAI-Framework")
.join("AKURAI_APPS.toml");
if !apps_toml.exists() {
bail!("AKURAI_APPS.toml not found at {}", apps_toml.display());
}
let text = std::fs::read_to_string(&apps_toml)
.with_context(|| format!("reading {}", apps_toml.display()))?;
let mut entries = Vec::new();
for app in parse_apps_toml(&text) {
if app.retired || app.name.is_empty() {
continue;
}
entries.push(audit_app(&app));
}
if json {
println!("{}", serde_json::to_string_pretty(&entries)?);
} else {
println!("=== Fleet Deployment Policy Audit ===");
for entry in &entries {
let commit = entry.hosted_commit.as_deref().unwrap_or("unresolved");
if entry.violations.is_empty() {
println!(" [\x1b[32mOK\x1b[0m] {:<20} {commit}", entry.app);
} else {
println!(" [\x1b[31mFAIL\x1b[0m] {:<20} {commit}", entry.app);
for violation in &entry.violations {
println!(" - {violation}");
}
}
if !entry.missing_evidence.is_empty() {
println!(
" missing runtime evidence: {}",
entry.missing_evidence.join(", ")
);
}
}
}
let failed = entries.iter().any(|entry| !entry.violations.is_empty());
if failed {
bail!("fleet audit found non-conforming pipelines");
}
Ok(())
}
fn audit_app(app: &AppCfg) -> FleetAuditEntry {
let mut violations = Vec::new();
let checkout = Path::new(&app.checkout);
let hosted_commit = if app.checkout.is_empty() {
None
} else {
Command::new("git")
.arg("-C")
.arg(checkout)
.args(["rev-parse", "HEAD"])
.output()
.ok()
.filter(|out| out.status.success())
.map(|out| String::from_utf8_lossy(&out.stdout).trim().to_owned())
};
let pipeline_path = checkout.join(".akurai.yml");
match std::fs::read_to_string(&pipeline_path) {
Ok(source) => {
if let Err(error) = crate::config::parse(&source) {
violations.push(format!("{error:#}"));
}
}
Err(error) => violations.push(format!(
"no .akurai.yml at {}: {error}",
pipeline_path.display()
)),
}
// The catalog `deploy` field is manual-fallback tooling, not something
// Build enforcement reads, but a raw direct-deploy command invites
// bypassing Build by hand — flag it as a policy violation too.
if !app.deploy.is_empty() && !app.deploy.contains("akurai-build:") {
violations.push(format!(
"AKURAI_APPS.toml deploy field bypasses AkurAI Build: {}",
app.deploy
));
}
// Runtime evidence (SDP-006/007/009/010: host-agent handoff actually
// executed, migration backup verified, run reached `succeeded`, health
// check passed) lives in AkurAI Build's persisted run/job-log rows,
// which this filesystem-only audit cannot read; a fleet audit is never
// a substitute for checking `akurai_run_show`/`akurai_runs` before
// calling an app deployed.
let mut missing_evidence = vec!["SDP-009 persisted succeeded run"];
if !app.db_snapshot.is_empty() {
missing_evidence.push("SDP-007 migration backup+recovery");
}
missing_evidence.push("SDP-006 host-agent handoff execution");
missing_evidence.push("SDP-010 post-deploy health check result");
FleetAuditEntry {
app: app.name.clone(),
hosted_commit,
violations,
missing_evidence,
}
}
#[derive(Default, Debug)]
struct AppCfg {
name: String,
retired: bool,
health_type: String,
dns_server: String,
dns_name: String,
dns_expect: String,
domain: String,
health_path: String,
repo: String,
checkout: String,
deploy: String,
db_snapshot: String,
}
/// Minimal TOML-subset reader for AKURAI_APPS.toml: `[[app]]` sections with
/// `key = value` lines (quoted strings or bare words). Enough for the fields
/// the health check reads; no TOML crate is in the dependency budget.
fn parse_apps_toml(text: &str) -> Vec<AppCfg> {
let mut apps = Vec::new();
let mut current: Option<AppCfg> = None;
for line in text.lines() {
let line = line.trim();
if line == "[[app]]" {
if let Some(cfg) = current.take() {
apps.push(cfg);
}
current = Some(AppCfg::default());
continue;
}
if line.is_empty() || line.starts_with('#') || line.starts_with('[') {
continue;
}
let Some((key, value)) = line.split_once('=') else {
continue;
};
let Some(cfg) = current.as_mut() else {
continue;
};
match key.trim() {
"name" => cfg.name = toml_value(value),
"retired" => cfg.retired = toml_value(value) == "true",
"health_type" => cfg.health_type = toml_value(value),
"dns_server" => cfg.dns_server = toml_value(value),
"dns_name" => cfg.dns_name = toml_value(value),
"dns_expect" => cfg.dns_expect = toml_value(value),
"domain" => cfg.domain = toml_value(value),
"health_path" => cfg.health_path = toml_value(value),
"repo" => cfg.repo = toml_value(value),
"checkout" => cfg.checkout = toml_value(value),
"deploy" => cfg.deploy = toml_value(value),
"db_snapshot" => cfg.db_snapshot = toml_value(value),
_ => {}
}
}
if let Some(cfg) = current.take() {
apps.push(cfg);
}
apps
}
fn toml_value(raw: &str) -> String {
let raw = raw.trim();
if let Some(stripped) = raw.strip_prefix('"') {
// Quoted string: up to the closing quote (trailing comment stays out).
if let Some(end) = stripped.find('"') {
return stripped[..end].to_string();
}
return stripped.to_string();
}
// Bare word / boolean / integer.
raw.split([' ', '#']).next().unwrap_or("").to_string()
}
fn probe_app(app: &AppCfg) {
let name = if app.name.is_empty() { "?" } else { &app.name };
// DNS-only services expose no HTTP endpoint; probe a TXT health record.
if app.health_type == "dns" {
let server = &app.dns_server;
let qname = &app.dns_name;
let expect = if app.dns_expect.is_empty() {
"ok"
} else {
&app.dns_expect
};
match Command::new("dig")
.arg(format!("@{server}"))
.arg(qname)
.arg("TXT")
.arg("+short")
.arg("+time=3")
.arg("+tries=1")
.output()
{
Ok(out) if out.status.success() => {
let stdout = String::from_utf8_lossy(&out.stdout);
if stdout.contains(expect) {
println!(" [\x1b[32mOK\x1b[0m] {name:<30} dns:{qname:<25} status={expect}");
} else {
println!(
" [\x1b[31mFAIL\x1b[0m] {name:<30} dig {qname} @{server} (want TXT {expect})"
);
}
}
Ok(_) => println!(
" [\x1b[31mFAIL\x1b[0m] {name:<30} dig {qname} @{server} (want TXT {expect})"
),
Err(error) => println!(" [\x1b[31mERR\x1b[0m] {name:<30} {error}"),
}
return;
}
let domain = &app.domain;
if domain.is_empty() {
return;
}
let health_url = if app.health_path == "/health" {
format!("https://{domain}/health")
} else {
format!("https://{domain}/api/health")
};
match Command::new("curl")
.args(["-fsS", "--max-time", "5", &health_url])
.output()
{
Ok(out) if out.status.success() => {
let stdout = String::from_utf8_lossy(&out.stdout);
let (label, status) = match serde_json::from_str::<Value>(&stdout) {
Ok(data) => {
let label = data
.get("app")
.or_else(|| data.get("service"))
.or_else(|| data.get("framework"))
.map(json_to_string)
.unwrap_or_else(|| name.to_string());
let status = data
.get("status")
.map(json_to_string)
.unwrap_or_else(|| "ok".to_string());
(label, status)
}
Err(_) => (name.to_string(), "ok".to_string()),
};
println!(" [\x1b[32mOK\x1b[0m] {name:<30} {label:<25} status={status}");
}
Ok(_) => println!(" [\x1b[31mFAIL\x1b[0m] {name:<30} {health_url}"),
Err(error) => println!(" [\x1b[31mERR\x1b[0m] {name:<30} {error}"),
}
}
fn json_to_string(value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
other => other.to_string(),
}
}
/// Free ports helper factored out for testing (bash `ports free [n]` core).
fn free_ports(reg: &Registry, live: &LiveState) -> Vec<u16> {
(APP_POOL_LO..=APP_POOL_HI)
.filter(|p| !is_taken(reg, live, *p))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_parsing_skips_comments_and_garbage() {
let text = "# port|unit|domain|repo|pool|note\n8094|mail|mail.olibuijr.com|akurai-mail|app|primary mail\n\n|bad|row\n8199|system-thing|-|repo|system|note with|extra pipes\n";
let reg = parse_registry(text);
assert_eq!(reg.ports, vec![8094, 8199]);
assert_eq!(reg.unit[&8094], "mail");
assert_eq!(reg.pool[&8199], "system");
assert_eq!(reg.note[&8199], "note with|extra pipes");
}
#[test]
fn live_parsing_sections() {
let live = parse_live(
"### PORTS\n8080|web\n8094|mail\n### VHOSTS\nmail.olibuijr.com|8094\nstatic.site|static\n",
);
assert_eq!(live.unit[&8080], "web");
assert_eq!(live.vhost["mail.olibuijr.com"], "8094");
assert_eq!(live.vhost["static.site"], "static");
}
#[test]
fn free_ports_skips_registered_and_live() {
let reg = parse_registry("8094|mail|mail.olibuijr.com|repo|app|note\n");
let live = parse_live("### PORTS\n8095|other\n### VHOSTS\n");
assert_eq!(
free_ports(®, &live)
.into_iter()
.take(3)
.collect::<Vec<_>>(),
vec![8096, 8097, 8098]
);
assert!(is_taken(®, &live, 8094));
assert!(is_taken(®, &live, 8095));
assert!(!is_taken(®, &live, 8096));
}
#[test]
fn apps_toml_minimal_parse() {
let text = "[[app]]\nname = \"mail\"\ndomain = \"mail.olibuijr.com\"\nhealth_path = \"/health\"\n\n[[app]]\nname = \"dns-only\"\nretired = false\nhealth_type = \"dns\"\ndns_server = \"ns1\"\ndns_name = \"_health\"\ndns_expect = \"ok\"\n";
let apps = parse_apps_toml(text);
assert_eq!(apps.len(), 2);
assert_eq!(apps[0].name, "mail");
assert_eq!(apps[1].dns_name, "_health");
}
#[test]
fn free_ports_list_matches_pool_bounds() {
assert_eq!(APP_POOL_LO, 8094);
assert_eq!(APP_POOL_HI, 8199);
}
}