ci / rust (push) Successful in 9m49s
The `RegistryClient` and the bare `reqwest::Client::new()` calls in
`policy.rs` built clients with no timeouts, so `maestro list` / `maestro
policy {show,reload}` would hang forever against a registry/daemon that
is TCP-alive but not making forward progress (SIGSTOP, swap thrash,
deadlock behind slow disk).
Centralize construction in `http::build_client()`, which applies a 5s
connect timeout and a 30s request timeout, and route every call site
through it. Covered by a test that points the client at a peer which
accepts the connection but never responds and asserts the request times
out instead of hanging.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
128 lines
3.6 KiB
Rust
128 lines
3.6 KiB
Rust
//! `maestro policy reload | show` — talk to the local daemon's
|
|
//! `/api/policy` endpoints (NAP-237).
|
|
//!
|
|
//! The local daemon's bind port is read from `[daemon].bind_addr` in the
|
|
//! unified config; the host is always `127.0.0.1` (these endpoints are
|
|
//! same-host by design — the daemon's bind may be `0.0.0.0` for attach,
|
|
//! but operators reload policy from the same machine).
|
|
|
|
use std::io::Write;
|
|
use std::path::Path;
|
|
|
|
use maestro_config::MaestroConfigFile;
|
|
use serde::Deserialize;
|
|
|
|
const DEFAULT_PORT: u16 = 7070;
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum PolicyError {
|
|
#[error("config: {0}")]
|
|
Config(#[from] maestro_config::ConfigError),
|
|
#[error("http: {0}")]
|
|
Http(#[from] reqwest::Error),
|
|
#[error("daemon returned {status}: {body}")]
|
|
DaemonError {
|
|
status: reqwest::StatusCode,
|
|
body: String,
|
|
},
|
|
#[error("io: {0}")]
|
|
Io(#[from] std::io::Error),
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct PolicyView {
|
|
pub allow_inbound: Vec<String>,
|
|
}
|
|
|
|
pub async fn run_reload(config_path: &Path, out: &mut impl Write) -> Result<(), PolicyError> {
|
|
let url = format!("{}/api/policy/reload", local_base_url(config_path)?);
|
|
let resp = crate::http::build_client().post(&url).send().await?;
|
|
let status = resp.status();
|
|
if !status.is_success() {
|
|
let body = resp.text().await.unwrap_or_default();
|
|
return Err(PolicyError::DaemonError { status, body });
|
|
}
|
|
let view: PolicyView = resp.json().await?;
|
|
writeln!(
|
|
out,
|
|
"reloaded — allow_inbound now has {} entr{}",
|
|
view.allow_inbound.len(),
|
|
if view.allow_inbound.len() == 1 {
|
|
"y"
|
|
} else {
|
|
"ies"
|
|
},
|
|
)?;
|
|
for entry in &view.allow_inbound {
|
|
writeln!(out, " {entry}")?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn run_show(
|
|
config_path: &Path,
|
|
json: bool,
|
|
out: &mut impl Write,
|
|
) -> Result<(), PolicyError> {
|
|
let url = format!("{}/api/policy", local_base_url(config_path)?);
|
|
let resp = crate::http::build_client().get(&url).send().await?;
|
|
let status = resp.status();
|
|
if !status.is_success() {
|
|
let body = resp.text().await.unwrap_or_default();
|
|
return Err(PolicyError::DaemonError { status, body });
|
|
}
|
|
let view: PolicyView = resp.json().await?;
|
|
if json {
|
|
writeln!(
|
|
out,
|
|
"{}",
|
|
serde_json::to_string(&view.allow_inbound).unwrap()
|
|
)?;
|
|
} else if view.allow_inbound.is_empty() {
|
|
writeln!(
|
|
out,
|
|
"(allow_inbound is empty — every cross-daemon attach is denied)"
|
|
)?;
|
|
} else {
|
|
for entry in &view.allow_inbound {
|
|
writeln!(out, "{entry}")?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// `http://127.0.0.1:<port>` where `<port>` is `[daemon].bind_addr`'s port
|
|
/// or `7070` if unset / file missing.
|
|
fn local_base_url(config_path: &Path) -> Result<String, PolicyError> {
|
|
let file = MaestroConfigFile::load_or_default(config_path)?;
|
|
let port = file
|
|
.daemon
|
|
.bind_addr
|
|
.map(|a| a.port())
|
|
.unwrap_or(DEFAULT_PORT);
|
|
Ok(format!("http://127.0.0.1:{port}"))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn local_base_url_uses_bind_port_or_default() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("config.toml");
|
|
// missing file → default
|
|
assert_eq!(local_base_url(&path).unwrap(), "http://127.0.0.1:7070");
|
|
|
|
std::fs::write(
|
|
&path,
|
|
r#"
|
|
[daemon]
|
|
bind_addr = "0.0.0.0:9090"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(local_base_url(&path).unwrap(), "http://127.0.0.1:9090");
|
|
}
|
|
}
|