A hive reads its own declaration today and that scopes cleanly: DIRECT.GET carries the key in the subject, so the grant can name it. A *watch* cannot be scoped that way — a consumer's filter travels in the request payload, so $JS.API.CONSUMER.CREATE.<stream> grants the whole stream. With every hive in one bucket, letting a hive watch its own declaration would let it read every other hive's. One bucket per hive (hive-wanted-<hive>) makes the stream a hive may hold exactly as wide as what it is allowed to see, which is what #4006's live-watch needs. That watch is a separate change; this only moves the boundary. mara's calls, both on #4006: one stream per hive rather than teaching the auth responder a hive roster, and a wildcard for the controller — "its okay if swarm controller can theoretically override hive". A bucket name is a single subject token with no prefix matching, so no wildcard narrower than * covers N per-hive buckets; the controller's grant is account-wide by consequence, and documented as chosen rather than left to look accidental. The reader arm of #4005's key-layout guard asserted the opposite of that ruling, so it is replaced rather than deleted: the hive arm survives as no_hive_may_write_another_role_s_agent_status (with a positive control), and the_readers_grant_is_deliberately_account_wide pins the decision and names the ruling, so the width reads as chosen to whoever finds it next. Two pre-existing negative assertions were silently defanged by the rename -- they matched hive-wanted.beta and $KV.hive-wanted.alpha, strings nothing produces any more, and kept passing. Both now match current names. swarm-controller resolves the store per hive per call instead of caching one in a OnceCell: there is no single handle that serves N buckets, and declarations change on operator action rather than per tick.
75 lines
3.4 KiB
Rust
75 lines
3.4 KiB
Rust
//! The hive-status KV bucket: its name, and the shape it is created with.
|
|
//!
|
|
//! Two processes touch this bucket from opposite ends — a hive writes its
|
|
//! own key, the swarm controller reads every key — and they live in
|
|
//! different crates. That is the whole reason this module exists rather
|
|
//! than a `const` on each side: **the two ends must agree, and a literal
|
|
//! repeated across crates is an agreement nothing checks.**
|
|
//!
|
|
//! The name is the obvious half. The sharper half is the *config*: both
|
|
//! ends open the bucket with `open_or_create`, because either may
|
|
//! arrive first on a fresh swarm and neither can assume the other has
|
|
//! run. If the two ends passed different `Config`s, whichever created it
|
|
//! would win and the other's `get_key_value` would succeed against a
|
|
//! bucket it did not ask for — no error, no log, just a retention policy
|
|
//! nobody chose. Sharing the constructor makes the race have one outcome
|
|
//! instead of two.
|
|
//!
|
|
//! The bucket *name* is unconditional; only `open_or_create` is behind the
|
|
//! `kv` feature. (Named in backticks rather than linked: with `kv` off the item
|
|
//! does not exist, and an intra-doc link to it fails the rustdoc gate in
|
|
//! exactly the configuration this split exists to support.) A third end names the bucket without ever opening it — the
|
|
//! auth-callout responder, which derives the subjects a hive may publish to
|
|
//! from it — and it speaks neither `jetstream` nor `kv`. Gating the name too
|
|
//! would have made that consumer choose between a JetStream stack it does not
|
|
//! use and a copied literal, and a copied literal is precisely the agreement
|
|
//! nothing checks.
|
|
|
|
#[cfg(feature = "kv")]
|
|
use crate::Error;
|
|
|
|
/// The KV bucket hives publish their status snapshots into, one key per
|
|
/// hive keyed by `hiveName`.
|
|
///
|
|
/// A constant and not an option: reader and writer must name the same
|
|
/// bucket, and an option is a way for two deployments to disagree about
|
|
/// which one that is. Nothing about a bucket name is site-specific.
|
|
pub const BUCKET: &str = "hive-status";
|
|
|
|
/// Open the status bucket, creating it if nothing has yet.
|
|
///
|
|
/// `history: 1` is the shape: every consumer reads *the last thing each
|
|
/// hive said*, and retaining more would be storage bought for a query
|
|
/// nobody makes.
|
|
///
|
|
/// Creating rather than requiring a provisioning step is deliberate — the
|
|
/// controller and the hives come up in no particular order, and a bucket
|
|
/// that must pre-exist turns "the swarm was deployed in the wrong order"
|
|
/// into a permanent, silent absence of data.
|
|
#[cfg(feature = "kv")]
|
|
pub async fn open_or_create(
|
|
client: &async_nats::Client,
|
|
) -> Result<async_nats::jetstream::kv::Store, Error> {
|
|
let js = async_nats::jetstream::new(client.clone());
|
|
match js.get_key_value(BUCKET).await {
|
|
Ok(store) => Ok(store),
|
|
Err(e) => {
|
|
tracing::info!(
|
|
bucket = BUCKET,
|
|
reason = %e,
|
|
"status bucket not available, creating it"
|
|
);
|
|
js.create_key_value(async_nats::jetstream::kv::Config {
|
|
bucket: BUCKET.to_owned(),
|
|
description: "Last status snapshot offered by each hive".to_owned(),
|
|
history: 1,
|
|
..Default::default()
|
|
})
|
|
.await
|
|
.map_err(|source| Error::CreateBucket {
|
|
bucket: BUCKET.to_owned(),
|
|
source,
|
|
})
|
|
}
|
|
}
|
|
}
|