feat(swarm-controller): serve the agent roster
GET /api/agents, beside the POST that creates one. The identity store is the roster rather than a source to assemble one from, so this is a read with nothing to merge or reconcile. It says nothing about health, deliberately. A roster is the set other views are complete against — it is what makes "this agent has never reported" expressible, and that only survives while the declared set and the reported set stay apart. The two verbs treat a missing bridge differently and the asymmetry is the design: POST queues a job that fails loud when claimed, GET has nowhere to defer to and returns 503. Answering [] there would render a store nobody could read as a swarm with no agents.
This commit is contained in:
parent
3700167279
commit
6a1a349a71
3 changed files with 141 additions and 23 deletions
|
|
@ -1,6 +1,6 @@
|
|||
//! Client for `swarm-authelia-bridge` — the only writer of the swarm's
|
||||
//! authelia users database (see that crate's README for why this daemon
|
||||
//! cannot write it directly).
|
||||
//! Client for `swarm-authelia-bridge` — this daemon's only access to the
|
||||
//! swarm's authelia users database, in either direction (see that crate's
|
||||
//! README for why the file cannot be touched from here).
|
||||
//!
|
||||
//! Authenticated with THIS daemon's own queue OIDC identity
|
||||
//! (`SWARM_CONTROLLER_OIDC_*`, the same one `swarm-queue-client` mints for
|
||||
|
|
@ -13,9 +13,12 @@
|
|||
//!
|
||||
//! `None` when this deployment did not wire a bridge up — the bridge only
|
||||
//! exists on hosts that also run `swarm-authelia`, so a controller split
|
||||
//! from it simply has no identity-creation capability yet
|
||||
//! (`SwarmNodeKind::CreateIdentity` fails such a job explicitly rather
|
||||
//! than this module papering over the gap).
|
||||
//! from it can neither create an identity nor read the roster.
|
||||
//!
|
||||
//! Each caller answers that absence in its own terms rather than this
|
||||
//! module inventing a shared one: `SwarmNodeKind::CreateIdentity` fails
|
||||
//! the job explicitly, and the roster endpoint refuses. Neither substitutes
|
||||
//! an empty answer, which is the failure mode a default here would create.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use swarm_authelia_bridge_sock::{BridgeRequest, BridgeResponse};
|
||||
|
|
@ -59,6 +62,37 @@ impl AuthBridge {
|
|||
|
||||
/// Idempotently ensure `name` exists as an authelia subject.
|
||||
pub async fn ensure_agent_identity(&self, name: &str) -> Result<BridgeResponse> {
|
||||
self.request(&BridgeRequest::EnsureAgentIdentity {
|
||||
name: name.to_owned(),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// The swarm's agent roster, straight from the identity store.
|
||||
///
|
||||
/// Returns the names rather than the whole [`BridgeResponse`]: every
|
||||
/// other variant is a protocol error for this request, and a caller that
|
||||
/// had to match them would be free to treat one as an empty roster. An
|
||||
/// empty roster and a bridge that answered something else are different
|
||||
/// facts, and only one of them is a valid render.
|
||||
pub async fn list_agent_identities(&self) -> Result<Vec<String>> {
|
||||
match self.request(&BridgeRequest::ListAgentIdentities).await? {
|
||||
BridgeResponse::Agents { agents } => {
|
||||
Ok(agents.into_iter().map(|entry| entry.name).collect())
|
||||
}
|
||||
other => {
|
||||
anyhow::bail!("swarm-authelia-bridge answered a roster request with {other:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One authenticated round-trip to the bridge.
|
||||
///
|
||||
/// A fresh token per call, deliberately — see the module doc. Shared by
|
||||
/// every operation so the auth and the error handling cannot drift
|
||||
/// between them; a second copy is how one path ends up treating a 500 as
|
||||
/// an answer.
|
||||
async fn request(&self, req: &BridgeRequest) -> Result<BridgeResponse> {
|
||||
// `mint_token_for` builds its own HTTP client (trusting the queue's
|
||||
// configured CA, if any) — deliberately not `self.http`, which is
|
||||
// the bridge's own client and has nothing to do with authelia's
|
||||
|
|
@ -71,9 +105,7 @@ impl AuthBridge {
|
|||
.http
|
||||
.post(format!("{}/requests", self.base_url))
|
||||
.bearer_auth(token)
|
||||
.json(&BridgeRequest::EnsureAgentIdentity {
|
||||
name: name.to_owned(),
|
||||
})
|
||||
.json(req)
|
||||
.send()
|
||||
.await
|
||||
.context("calling swarm-authelia-bridge")?;
|
||||
|
|
|
|||
Loading…
Reference in a new issue