Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ddabdc410 | ||
|
|
32e4c687b8 | ||
|
|
0456206e52 |
2 changed files with 24 additions and 41 deletions
|
|
@ -103,10 +103,10 @@ now set unconditionally for every agent. The mechanism:
|
||||||
(Legacy name `.bound` also accepted during the transition window.)
|
(Legacy name `.bound` also accepted during the transition window.)
|
||||||
4. **Gateway side**. `gateway_nginx::write` generates
|
4. **Gateway side**. `gateway_nginx::write` generates
|
||||||
`/var/lib/hyperhive/gateway/agents.conf` — a plain nginx include
|
`/var/lib/hyperhive/gateway/agents.conf` — a plain nginx include
|
||||||
file with one `location /agent/<name>/` block per agent. UDS
|
file with one `location /agent/<name>/` block per agent. Always
|
||||||
upstream (`http://unix:/run/hive-agent/<name>/web.sock:/`) when
|
a UDS upstream (`http://unix:/run/hive-agent/<name>/web.sock:/`);
|
||||||
`hyperhive-socket-bound` marker present; TCP loopback for agents
|
if the socket is not yet bound, nginx returns 502 caught by the
|
||||||
that haven't yet been rebuilt under the new config.
|
`error_page 502 503 504 = /__hive_agent_unreachable` directive.
|
||||||
The gateway container bind-mounts `/var/lib/hyperhive/gateway/` at
|
The gateway container bind-mounts `/var/lib/hyperhive/gateway/` at
|
||||||
`/run/hive-state/`; nginx includes `/run/hive-state/agents.conf`.
|
`/run/hive-state/`; nginx includes `/run/hive-state/agents.conf`.
|
||||||
After each write, c0re triggers the appropriate nginx action inside
|
After each write, c0re triggers the appropriate nginx action inside
|
||||||
|
|
@ -127,17 +127,6 @@ idempotent — skips the rename when content is unchanged. Failed reloads
|
||||||
are retried automatically on subsequent poll ticks via
|
are retried automatically on subsequent poll ticks via
|
||||||
`gateway_nginx::reload_if_pending`.
|
`gateway_nginx::reload_if_pending`.
|
||||||
|
|
||||||
## TCP loopback fallback
|
|
||||||
|
|
||||||
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/`.
|
|
||||||
|
|
||||||
`agents.conf` uses atomic `<path>.tmp` + `rename()` writes so a crashing
|
`agents.conf` uses atomic `<path>.tmp` + `rename()` writes so a crashing
|
||||||
c0re process never leaves a partial or unparseable file behind.
|
c0re process never leaves a partial or unparseable file behind.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Runtime nginx include-file generator for the gateway's per-agent
|
//! Runtime nginx include-file generator for the gateway's per-agent
|
||||||
//! `/agent/<name>/` location blocks. Writes
|
//! `/agent/<name>/` location blocks. Writes
|
||||||
//! `/var/lib/hyperhive/gateway/agents.conf` on every topology change.
|
//! `/var/lib/hyperhive/gateway/agents.conf` on every topology change.
|
||||||
//! UDS vs TCP upstream selection, reload trigger (`systemd-run
|
//! UDS upstream selection, reload trigger (`systemd-run
|
||||||
//! --machine=hive-gateway`), and idempotency:
|
//! --machine=hive-gateway`), and idempotency:
|
||||||
//! `docs/gateway.md::Per-agent unix-socket upstream`.
|
//! `docs/gateway.md::Per-agent unix-socket upstream`.
|
||||||
|
|
||||||
|
|
@ -14,7 +14,6 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use crate::priv_client;
|
use crate::priv_client;
|
||||||
|
|
||||||
use crate::agent_sockets;
|
use crate::agent_sockets;
|
||||||
use crate::lifecycle;
|
|
||||||
|
|
||||||
/// Set when `write` publishes a new agents.conf; cleared when
|
/// Set when `write` publishes a new agents.conf; cleared when
|
||||||
/// `reload_gateway_nginx` submits the reload command successfully.
|
/// `reload_gateway_nginx` submits the reload command successfully.
|
||||||
|
|
@ -74,12 +73,11 @@ const PROXY_HEADER_BLOCK: &str = " proxy_http_version 1.1;
|
||||||
/// When `frontend_dir` is `None` (legacy) each agent gets a single
|
/// When `frontend_dir` is `None` (legacy) each agent gets a single
|
||||||
/// `location /agent/<name>/` proxy block.
|
/// `location /agent/<name>/` proxy block.
|
||||||
///
|
///
|
||||||
/// Output is deterministic for the same (sorted) set of names +
|
/// Output is deterministic for the same (sorted) set of names:
|
||||||
/// `.bound` state: no timestamps, no UUIDs. `BTreeMap` would give
|
/// no timestamps, no UUIDs. `BTreeMap` would give alphabetical order;
|
||||||
/// alphabetical order; we rely on the caller to pass sorted names if
|
/// we rely on the caller to pass sorted names if they care about diff
|
||||||
/// they care about diff stability, but the gateway treats the blocks
|
/// stability, but the gateway treats the blocks as unordered by nginx's
|
||||||
/// as unordered by nginx's longest-prefix-match rules so ordering only
|
/// longest-prefix-match rules so ordering only affects human readability.
|
||||||
/// affects human readability.
|
|
||||||
fn render(names: &[String], frontend_dir: Option<&str>) -> String {
|
fn render(names: &[String], frontend_dir: Option<&str>) -> String {
|
||||||
let mut out = String::from(
|
let mut out = String::from(
|
||||||
"# Generated by hive-c0re \u{2014} do not edit.\
|
"# Generated by hive-c0re \u{2014} do not edit.\
|
||||||
|
|
@ -87,24 +85,16 @@ fn render(names: &[String], frontend_dir: Option<&str>) -> String {
|
||||||
\n# Reload triggered by hive-c0re via systemd-run --machine=hive-gateway.\n",
|
\n# Reload triggered by hive-c0re via systemd-run --machine=hive-gateway.\n",
|
||||||
);
|
);
|
||||||
for name in names {
|
for name in names {
|
||||||
let port = lifecycle::agent_web_port(name);
|
|
||||||
// Two upstream forms because named locations (split mode's
|
// Two upstream forms because named locations (split mode's
|
||||||
// `@<name>_dynamic`) forbid a URI part on `proxy_pass`. The
|
// `@<name>_dynamic`) forbid a URI part on `proxy_pass`. The
|
||||||
// legacy prefix-location path keeps the trailing `/` so nginx
|
// legacy prefix-location path keeps the trailing `/` so nginx
|
||||||
// strips `/agent/<name>/` automatically; the named-location
|
// strips `/agent/<name>/` automatically; the named-location
|
||||||
// path strips the prefix via `rewrite` and uses a bare upstream.
|
// path strips the prefix via `rewrite` and uses a bare upstream.
|
||||||
let (upstream_prefix, upstream_bare) = if agent_sockets::ready_marker_for(name).exists() {
|
// When the socket is not yet bound, nginx returns 502, caught by
|
||||||
|
// the error_page directive below.
|
||||||
let sock = agent_sockets::socket_path_for(name).display().to_string();
|
let sock = agent_sockets::socket_path_for(name).display().to_string();
|
||||||
(
|
let upstream_prefix = format!("http://unix:{sock}:/");
|
||||||
format!("http://unix:{sock}:/"),
|
let upstream_bare = format!("http://unix:{sock}:");
|
||||||
format!("http://unix:{sock}:"),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
format!("http://127.0.0.1:{port}/"),
|
|
||||||
format!("http://127.0.0.1:{port}"),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(frontend) = frontend_dir {
|
if let Some(frontend) = frontend_dir {
|
||||||
// Split mode: try to serve files from the nix-store dist first;
|
// Split mode: try to serve files from the nix-store dist first;
|
||||||
|
|
@ -303,14 +293,18 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_tcp_upstream_when_no_bound_marker() {
|
fn render_uds_upstream_unconditional() {
|
||||||
// No .bound file on disk → falls back to TCP loopback.
|
|
||||||
let names = vec!["iris".to_owned()];
|
let names = vec!["iris".to_owned()];
|
||||||
let body = render(&names, None);
|
let body = render(&names, None);
|
||||||
let port = lifecycle::agent_web_port("iris");
|
// UDS form present
|
||||||
assert!(
|
assert!(
|
||||||
body.contains(&format!("proxy_pass http://127.0.0.1:{port}/")),
|
body.contains("proxy_pass http://unix:"),
|
||||||
"expected TCP upstream for iris, got:\n{body}"
|
"expected UDS upstream for iris, got:\n{body}"
|
||||||
|
);
|
||||||
|
// No TCP loopback fallback
|
||||||
|
assert!(
|
||||||
|
!body.contains("127.0.0.1"),
|
||||||
|
"unexpected TCP loopback in output:\n{body}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue