remove vestigial agent-ports.json tcp web-port map
This commit is contained in:
parent
53f49615fa
commit
ad6b39b425
7 changed files with 33 additions and 209 deletions
|
|
@ -120,36 +120,19 @@ idempotent — skips the rename when content is unchanged. Failed reloads
|
|||
are retried automatically on subsequent poll ticks via
|
||||
`gateway_nginx::reload_if_pending`.
|
||||
|
||||
## Agent port map (`agent-ports.json`)
|
||||
## TCP loopback fallback
|
||||
|
||||
`/var/lib/hyperhive/run/agent-ports.json` is a flat JSON object keyed by
|
||||
logical agent name → TCP web port:
|
||||
While an agent's unix-socket marker is absent, the gateway routes its
|
||||
`/agent/<name>/` traffic to a TCP loopback upstream in `agents.conf`. The
|
||||
port is derived on the fly from `lifecycle::agent_web_port(name)` — a pure
|
||||
FNV-1a hash of the name, reproducible from the name alone, no name
|
||||
special-cased (so no on-disk port map is needed). Once the agent binds its
|
||||
unix socket — every agent does, via `HIVE_WEB_SOCKET` — the gateway
|
||||
switches to the socket upstream from `agent-sockets.json`. The root agent's
|
||||
UI is routed at `/agent/root/`.
|
||||
|
||||
```json
|
||||
{
|
||||
"iris": 8178,
|
||||
"atlas": 8304,
|
||||
"argus": 8267,
|
||||
"damocles": 8549
|
||||
}
|
||||
```
|
||||
|
||||
Written alongside `agents.conf` on every topology change. Ports come from
|
||||
`lifecycle::agent_web_port(name)` — a pure FNV-1a hash of the name,
|
||||
reproducible from the name alone. Every agent gets an entry — no name is
|
||||
special-cased. Note this TCP map is now a fallback the gateway no longer
|
||||
reaches: all agents bind a unix-socket web UI (`HIVE_WEB_SOCKET`) and the
|
||||
gateway routes via `agent-sockets.json`, picking the socket upstream
|
||||
whenever one exists. The root agent's UI is routed at `/agent/root/`.
|
||||
|
||||
The file doubles as a human-readable audit artifact — `cat agent-ports.json`
|
||||
shows every registered sub-agent and its deterministic port assignment. TCP
|
||||
loopback upstreams in `agents.conf` reference these ports for agents that
|
||||
haven't opted into unix-socket mode yet.
|
||||
|
||||
Both `agent-ports.json` and `agents.conf` use atomic `<path>.tmp` +
|
||||
`rename()` writes so a crashing c0re process never leaves a partial or
|
||||
unparseable file behind.
|
||||
`agents.conf` uses atomic `<path>.tmp` + `rename()` writes so a crashing
|
||||
c0re process never leaves a partial or unparseable file behind.
|
||||
|
||||
## Dashboard link shape (gateway vs direct)
|
||||
|
||||
|
|
|
|||
|
|
@ -846,8 +846,8 @@ window.marked = marked;
|
|||
// When hive-gateway is in front of the dashboard, build same-origin
|
||||
// `/agent/<name>/` URLs instead of the direct `http://<host>:<port>/`
|
||||
// TCP fallback — the gateway proxies the prefix to the per-agent
|
||||
// harness (TCP via `agent-ports.json` or unix-domain via
|
||||
// `agent-sockets.json`). See
|
||||
// harness (unix-domain via `agent-sockets.json`, or a computed TCP
|
||||
// loopback port while the socket marker is absent). See
|
||||
// `docs/web-ui.md::Container row` + `docs/gateway.md::Vhost map`.
|
||||
const gatewayLinks = !!(s && s.gateway_enabled);
|
||||
// Forge public URL: prefer state.forge_public_url (set by the NixOS
|
||||
|
|
|
|||
|
|
@ -1,131 +0,0 @@
|
|||
//! `/var/lib/hyperhive/run/agent-ports.json` writer — flat map of
|
||||
//! agent name → TCP web port. Written alongside `agents.conf` on
|
||||
//! every topology change. JSON shape, port derivation (FNV-1a hash),
|
||||
//! and atomicity: `docs/gateway.md::Agent port map`.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::lifecycle;
|
||||
|
||||
#[must_use]
|
||||
pub fn host_ports_path() -> PathBuf {
|
||||
crate::paths::agent_ports_file()
|
||||
}
|
||||
|
||||
/// Compute the agent-port map for the given logical agent names. Every
|
||||
/// agent gets an entry — no name is special-cased. (All agents now bind
|
||||
/// a unix-socket web UI via `HIVE_WEB_SOCKET`, so this TCP map is a
|
||||
/// fallback the gateway no longer reaches; tracked for removal.)
|
||||
///
|
||||
/// `BTreeMap` keeps the JSON output sorted by key so a re-emit
|
||||
/// without churn produces byte-identical output (helpful when the
|
||||
/// operator inspects the file by hand or diffs deploys).
|
||||
#[must_use]
|
||||
pub fn build_map(names: &[String]) -> BTreeMap<String, u16> {
|
||||
names
|
||||
.iter()
|
||||
.map(|n| (n.clone(), lifecycle::agent_web_port(n)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Render the map as pretty-printed JSON. Pretty so a human peek at
|
||||
/// `cat /var/lib/hyperhive/run/agent-ports.json` shows one row per agent
|
||||
/// — keeps the file readable without a separate jq step.
|
||||
fn render(map: &BTreeMap<String, u16>) -> String {
|
||||
// BTreeMap → serde_json::to_string_pretty preserves key order,
|
||||
// so the output is deterministic across calls with the same
|
||||
// agent set.
|
||||
serde_json::to_string_pretty(map).expect("BTreeMap<String, u16> is always serialisable")
|
||||
}
|
||||
|
||||
/// Atomically write the JSON for `names` to
|
||||
/// `/var/lib/hyperhive/run/agent-ports.json`. Writes via a sibling
|
||||
/// `<path>.tmp` + rename so a crashing process never leaves a
|
||||
/// partial file behind that the gateway worker would fail to parse.
|
||||
///
|
||||
/// Idempotent — if the rendered content matches what's already on
|
||||
/// disk, the write + rename are skipped so the file's mtime stays
|
||||
/// stable and inotify watchers in nginx (or any future watchers)
|
||||
/// don't fire spurious reload events.
|
||||
pub fn write(names: &[String]) -> Result<()> {
|
||||
let map = build_map(names);
|
||||
let body = render(&map);
|
||||
let path = host_ports_path();
|
||||
if std::fs::read_to_string(&path).ok().as_deref() == Some(&body) {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
|
||||
}
|
||||
let tmp = path.with_extension("json.tmp");
|
||||
std::fs::write(&tmp, &body).with_context(|| format!("write {}", tmp.display()))?;
|
||||
std::fs::rename(&tmp, &path).with_context(|| {
|
||||
format!(
|
||||
"rename {} -> {} (atomic publish)",
|
||||
tmp.display(),
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn build_map_includes_every_agent() {
|
||||
// No name is special-cased: the bootstrap/root container gets a
|
||||
// port entry like any other agent. Use the bootstrap name in the
|
||||
// input so this guards against re-introducing an exclusion.
|
||||
let names: Vec<String> = ["iris", lifecycle::MANAGER_NAME, "argus"]
|
||||
.iter()
|
||||
.map(|s| (*s).to_owned())
|
||||
.collect();
|
||||
let map = build_map(&names);
|
||||
assert!(map.contains_key(lifecycle::MANAGER_NAME));
|
||||
assert!(map.contains_key("iris"));
|
||||
assert!(map.contains_key("argus"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_map_uses_agent_web_port() {
|
||||
let names = vec!["iris".to_owned()];
|
||||
let map = build_map(&names);
|
||||
assert_eq!(map.get("iris"), Some(&lifecycle::agent_web_port("iris")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_map_handles_empty_input() {
|
||||
let map = build_map(&[]);
|
||||
assert!(map.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_map_dedupes_via_btreemap_key_collision() {
|
||||
// Duplicate inputs collapse via the map; the last value wins.
|
||||
// No callsite passes dups today, but guarding the invariant
|
||||
// here means a future bug doesn't surface as a corrupt JSON
|
||||
// doc (two `"iris":` keys).
|
||||
let names = vec!["iris".to_owned(), "iris".to_owned()];
|
||||
let map = build_map(&names);
|
||||
assert_eq!(map.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_is_pretty_and_sorted() {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert("zeta".to_owned(), 9000u16);
|
||||
map.insert("alpha".to_owned(), 8000u16);
|
||||
let body = render(&map);
|
||||
// Pretty-print = newlines between keys + indentation.
|
||||
assert!(body.contains('\n'));
|
||||
// BTreeMap sorts → alpha before zeta in output.
|
||||
let alpha_pos = body.find("alpha").expect("alpha in output");
|
||||
let zeta_pos = body.find("zeta").expect("zeta in output");
|
||||
assert!(alpha_pos < zeta_pos, "sorted order broken:\n{body}");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
//! `/var/lib/hyperhive/run/agent-sockets.json` writer. Sibling to
|
||||
//! `agent_ports.rs`; same atomic `<path>.tmp` + `rename()` shape so
|
||||
//! the gateway's nginx worker never reads a partial file. Includes
|
||||
//! manager and sub-agents so the gateway can route
|
||||
//! `/agent/<name>/` for all containers with a bound unix socket.
|
||||
//! `/var/lib/hyperhive/run/agent-sockets.json` writer. Atomic
|
||||
//! `<path>.tmp` + `rename()` write so the gateway's nginx worker never
|
||||
//! reads a partial file. Includes manager and sub-agents so the gateway
|
||||
//! can route `/agent/<name>/` for all containers with a bound unix
|
||||
//! socket.
|
||||
//!
|
||||
//! Full mechanism — per-agent subdir bind-mount, `hyperhive-socket-bound`
|
||||
//! marker gate, gateway UDS upstream, transition vs `agent-ports.json`,
|
||||
//! 10s poll loop: `docs/gateway.md::Per-agent unix-socket upstream`.
|
||||
//! marker gate, gateway UDS upstream, 10s poll loop:
|
||||
//! `docs/gateway.md::Per-agent unix-socket upstream`.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
@ -74,8 +74,7 @@ pub fn socket_path_for(name: &str) -> PathBuf {
|
|||
/// new marker name).
|
||||
///
|
||||
/// `BTreeMap` keeps the JSON output sorted by key so a re-emit
|
||||
/// without churn produces byte-identical output — same idempotency
|
||||
/// shape `agent_ports::write` relies on.
|
||||
/// without churn produces byte-identical output for idempotent writes.
|
||||
#[must_use]
|
||||
pub fn build_map(names: &[String]) -> BTreeMap<String, PathBuf> {
|
||||
build_map_with(names, |name| {
|
||||
|
|
@ -110,8 +109,7 @@ pub fn ready_marker_for(name: &str) -> PathBuf {
|
|||
|
||||
/// Render the map as pretty-printed JSON. Pretty so a human peek at
|
||||
/// `cat /var/lib/hyperhive/run/agent-sockets.json` shows one row per agent
|
||||
/// — keeps the file readable without a separate jq step (mirrors
|
||||
/// `agent_ports::render`).
|
||||
/// — keeps the file readable without a separate jq step.
|
||||
fn render(map: &BTreeMap<String, PathBuf>) -> String {
|
||||
// Serialize as strings (PathBuf → JSON string via the Display
|
||||
// impl). BTreeMap → serde_json::to_string_pretty preserves key
|
||||
|
|
@ -133,8 +131,7 @@ fn render(map: &BTreeMap<String, PathBuf>) -> String {
|
|||
/// Idempotent — if the rendered content matches what's already on
|
||||
/// disk, the write + rename are skipped so the file's mtime stays
|
||||
/// stable and inotify watchers in the gateway (or any future
|
||||
/// watchers) don't fire spurious reload events. Mirrors the
|
||||
/// `agent_ports::write` shape — keep them in lockstep.
|
||||
/// watchers) don't fire spurious reload events.
|
||||
pub fn write(names: &[String]) -> Result<()> {
|
||||
let map = build_map(names);
|
||||
let body = render(&map);
|
||||
|
|
@ -268,7 +265,7 @@ mod tests {
|
|||
// Duplicate inputs collapse via the map; no callsite passes
|
||||
// dups today, but guarding the invariant here means a future
|
||||
// bug doesn't surface as a corrupt JSON doc (two `"iris":`
|
||||
// keys). Mirrors agent_ports test.
|
||||
// keys).
|
||||
let names = vec!["iris".to_owned(), "iris".to_owned()];
|
||||
let map = build_map_with(&names, |_| true);
|
||||
assert_eq!(map.len(), 1);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
//! surface beyond "this is where the modules live".
|
||||
|
||||
pub mod actions;
|
||||
pub mod agent_ports;
|
||||
pub mod agent_server;
|
||||
pub mod agent_sockets;
|
||||
pub mod approvals;
|
||||
|
|
|
|||
|
|
@ -119,27 +119,12 @@ pub async fn sync_agents(hive: &HiveEnv, agents: &[AgentSpec]) -> Result<()> {
|
|||
crate::topology::reconcile(&agent_names, &pending)
|
||||
.with_context(|| format!("reconcile {}", crate::topology::topology_path().display()))?;
|
||||
|
||||
// Refresh /var/lib/hyperhive/run/agent-ports.json so the hive-gateway
|
||||
// nginx sees the new agent set. The file is the single source of
|
||||
// truth for which agents the gateway proxies to, since the
|
||||
// gateway container lives in system config and can't be rebuilt
|
||||
// from meta-flake events. Atomic write (tmp + rename) so a
|
||||
// partial write never trips the gateway's read.
|
||||
if let Err(e) = crate::agent_ports::write(&agent_names) {
|
||||
// Best-effort: a failed write doesn't block the meta-flake
|
||||
// regen + container ops that follow. The gateway falls back
|
||||
// to whatever map is currently on disk (or an empty map on
|
||||
// first boot, meaning no per-agent routing yet).
|
||||
tracing::warn!(error = ?e, "agent_ports::write failed (non-fatal)");
|
||||
}
|
||||
|
||||
// Refresh /var/lib/hyperhive/run/agent-sockets.json — sibling to the
|
||||
// ports map, drives the gateway's unix-socket upstreams once
|
||||
// agents opt in to `HIVE_WEB_SOCKET`. Coexists with the TCP-port
|
||||
// map during the transition: the gateway picks the socket
|
||||
// upstream when one exists, falls back to the TCP port otherwise.
|
||||
// Same best-effort + non-fatal shape. See
|
||||
// `docs/gateway.md::Per-agent unix-socket upstream`.
|
||||
// Refresh /var/lib/hyperhive/run/agent-sockets.json — drives the
|
||||
// gateway's unix-socket upstreams. Every agent binds a unix socket
|
||||
// (`HIVE_WEB_SOCKET`); the gateway routes via this map, falling back
|
||||
// to a computed TCP loopback port (`lifecycle::agent_web_port`) only
|
||||
// while an agent's socket marker is absent. Best-effort + non-fatal.
|
||||
// See `docs/gateway.md::Per-agent unix-socket upstream`.
|
||||
if let Err(e) = crate::agent_sockets::write(&agent_names) {
|
||||
tracing::warn!(error = ?e, "agent_sockets::write failed (non-fatal)");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Central host-side state paths under `/var/lib/hyperhive`.
|
||||
//!
|
||||
//! Historically these were flat string literals scattered across many
|
||||
//! modules (`broker.sqlite`, `matrix-admin-token`, `agent-ports.json`,
|
||||
//! modules (`broker.sqlite`, `matrix-admin-token`, `agent-sockets.json`,
|
||||
//! …) directly under the state root. This module groups the **strictly
|
||||
//! host-side** ones (read/written by hive-c0re alone, no nix-module or
|
||||
//! container coupling) into subdirs: `db/`, `forge/`, `matrix/`, `run/`.
|
||||
|
|
@ -111,12 +111,6 @@ pub fn run_dir() -> PathBuf {
|
|||
state_root().join("run")
|
||||
}
|
||||
|
||||
/// `run/agent-ports.json` — name→port map the gateway routing reads.
|
||||
#[must_use]
|
||||
pub fn agent_ports_file() -> PathBuf {
|
||||
run_dir().join("agent-ports.json")
|
||||
}
|
||||
|
||||
/// `run/agent-sockets.json` — name→socket-path map for UDS upstreams.
|
||||
#[must_use]
|
||||
pub fn agent_sockets_file() -> PathBuf {
|
||||
|
|
@ -150,9 +144,6 @@ pub fn relocate_legacy_state() {
|
|||
for (old_rel, new) in &moves {
|
||||
move_if_legacy(&root.join(old_rel), new);
|
||||
}
|
||||
// agent-ports.json handled here too (kept out of the array only to
|
||||
// keep the fixed-size literal tidy).
|
||||
move_if_legacy(&root.join("agent-ports.json"), &agent_ports_file());
|
||||
// `forge-email-aligned-<name>` markers: glob the flat root.
|
||||
if let Ok(rd) = std::fs::read_dir(&root) {
|
||||
for ent in rd.flatten() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue