The controller connects to the swarm queue as its own client and serves what each hive last said about itself at GET /api/hives/status. THE QUEUE IS THE STORE. A hive publishes into the `hive-status` JetStream KV bucket (history 1) and the controller reads it per request, keeping no copy. A cache here would be a second answer to the same question, free to disagree with the first, and the disagreement surfaces as a hive reading healthy on a dashboard while the bucket says otherwise. Whichever side arrives first creates the bucket; both want the same shape. Rows come from the roster rather than from the bucket, so an empty bucket renders as a swarm nobody has heard from instead of a healthy one, and `never_reported` stays distinct from `stale` - went quiet is a fault, never spoke is usually a deployment that has not happened. Freshness is derived at read time and never stored as a flag, because a stored `healthy` boolean goes stale silently the moment nothing arrives, which is the failure this endpoint is designed against. The timestamp is the NATS server's, applied when the value landed, so a publisher cannot make itself look fresher than it is. Authentication is per connection attempt, not per process. Authelia issues `client_credentials` tokens that expire in 3599s, and auth happens at CONNECT, so a long-lived connection is fine but a reconnect an hour later needs a token minted an hour later. `with_auth_callback` is re-run by async-nats for each attempt, which handles expiry by construction rather than by a timer - the alternative fails in the way this subsystem exists to prevent, with the controller still serving while its data quietly stops updating. Three failure shapes are deliberate: - A half-set environment is fatal; an absent one is not. Silently behaving like an unconfigured host is how every hive ends up reading `never_reported` with nothing to point at. - The endpoint answers 503 rather than an empty list when the store cannot be read. "I cannot reach the store" and "every hive is silent" are different answers, and rendering the second turns a local fault into an apparent swarm-wide outage. - `retry_on_initial_connect` makes the daemon and the queue bootable in either order, and the status handler refuses when the client is not Connected rather than issuing a request into it - a request made in that window does not fail, it waits, so every poll would hang and learn nothing. `Pending` is the state a never-connected client is in, which is why the test is `!= Connected` and not `== Disconnected`. The rendering rules are a pure function over a map, so the semantics are tested against a table rather than against a running server. The KV read, the credential rotation and the 503 paths are covered behaviourally instead: a real NATS server with a rotating token endpoint, asserting that the controller recovers only when the credential rotates, and mutation-tested by holding the credential wrong for the same window.
31 lines
887 B
TOML
31 lines
887 B
TOML
[package]
|
|
name = "swarm-controller"
|
|
version.workspace = true
|
|
readme = "README.md"
|
|
edition.workspace = true
|
|
|
|
[[bin]]
|
|
name = "swarm-controller"
|
|
path = "src/main.rs"
|
|
|
|
[dependencies]
|
|
anyhow.workspace = true
|
|
# `kv` (which pulls `jetstream`) on top of the workspace's feature set: the
|
|
# queue is this daemon's *store*, not just its transport - a hive's last
|
|
# status snapshot is read out of a JetStream KV bucket. Declared here rather
|
|
# than in the workspace entry so the auth-callout responder, which speaks
|
|
# neither, does not claim to need them.
|
|
async-nats = { workspace = true, features = ["kv"] }
|
|
axum.workspace = true
|
|
futures-util.workspace = true
|
|
reqwest.workspace = true
|
|
serde.workspace = true
|
|
serde_json.workspace = true
|
|
tokio.workspace = true
|
|
tracing.workspace = true
|
|
tracing-subscriber.workspace = true
|
|
utoipa.workspace = true
|
|
utoipa-axum.workspace = true
|
|
|
|
[lints]
|
|
workspace = true
|