copy the cache's pull facts to each hive and create them in the in-swarm cache
the in-swarm cache stores its address, name and public key beside its tokens, create-only. swarm-controller copies the four cache facts to swarm/hives/<hive>/attic/* every five minutes, with read on the sources and write on the copies. part 1 of 3: nothing reads the copies until the hive reader and probe land.
This commit is contained in:
parent
c79c41e004
commit
1e5070796d
2 changed files with 640 additions and 67 deletions
104
swarm-secret-client/src/attic.rs
Normal file
104
swarm-secret-client/src/attic.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
//! The binary cache agreement: the four facts a hive needs to pull from the
|
||||
//! swarm's cache, where the cache's operator keeps them, and where the
|
||||
//! controller copies them for each hive.
|
||||
//!
|
||||
//! The cache's own unit (or, for an external cache, the operator) writes them
|
||||
//! under `swarm/services/attic/`. No hive may read that prefix, so
|
||||
//! `swarm-controller` copies each leaf to `swarm/hives/<hive>/attic/<leaf>`,
|
||||
//! which the hive's own `swarm/hives/<hive>/*` stanza reads. The nix half of
|
||||
//! the source paths is `swarm-bao.nix`'s `atticFactLeaves`.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
Error,
|
||||
path::{Kind, principal_prefix},
|
||||
};
|
||||
|
||||
/// One of the facts a hive pulls with.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Leaf {
|
||||
/// The read-only token, as a basic-auth password.
|
||||
PullToken,
|
||||
/// The cache server's base URL, scheme included and no trailing slash.
|
||||
Url,
|
||||
/// The cache's name on that server.
|
||||
Cache,
|
||||
/// The cache's public signing key, `<name>:<base64>`.
|
||||
PublicKey,
|
||||
}
|
||||
|
||||
impl Leaf {
|
||||
/// Every leaf, in the order the controller copies them.
|
||||
pub const ALL: [Leaf; 4] = [Leaf::PullToken, Leaf::Url, Leaf::Cache, Leaf::PublicKey];
|
||||
|
||||
/// The last path segment, under both prefixes.
|
||||
#[must_use]
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Leaf::PullToken => "pull-token",
|
||||
Leaf::Url => "url",
|
||||
Leaf::Cache => "cache",
|
||||
Leaf::PublicKey => "public-key",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the cache's operator keeps `leaf`.
|
||||
///
|
||||
/// # Errors
|
||||
/// Never in practice: the name segments are constants. The `Result` is
|
||||
/// [`principal_prefix`]'s.
|
||||
pub fn service_path(leaf: Leaf) -> Result<String, Error> {
|
||||
let prefix = principal_prefix(Kind::Service, "attic")?;
|
||||
Ok(format!("{prefix}/{}", leaf.name()))
|
||||
}
|
||||
|
||||
/// Where `hive` reads its copy of `leaf`.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::PathSegment`] when `hive` contains anything but `[A-Za-z0-9_-]`,
|
||||
/// which keeps one hive's name from addressing another hive's secret.
|
||||
pub fn hive_path(hive: &str, leaf: Leaf) -> Result<String, Error> {
|
||||
let prefix = principal_prefix(Kind::Hive, hive)?;
|
||||
Ok(format!("{prefix}/attic/{}", leaf.name()))
|
||||
}
|
||||
|
||||
/// What every leaf holds: one string under the field `bao kv get
|
||||
/// -field=value` reads.
|
||||
///
|
||||
/// No `Debug`: for [`Leaf::PullToken`] `value` reads from the cache.
|
||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Fact {
|
||||
/// The fact itself.
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_paths_are_the_ones_the_store_grants() {
|
||||
// Spelled out: `swarm-bao.nix` grants each literal.
|
||||
let sources = Leaf::ALL.map(|l| service_path(l).expect("constant segments"));
|
||||
assert_eq!(
|
||||
sources,
|
||||
[
|
||||
"swarm/services/attic/pull-token",
|
||||
"swarm/services/attic/url",
|
||||
"swarm/services/attic/cache",
|
||||
"swarm/services/attic/public-key",
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
hive_path("alpha", Leaf::PullToken).expect("a plain name"),
|
||||
"swarm/hives/alpha/attic/pull-token"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hive_name_cannot_address_another_hives_copy() {
|
||||
assert!(hive_path("alpha/../beta", Leaf::Url).is_err());
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
//! deployment's environment into a logged-in client ([`client`]), and, per kind
|
||||
//! of secret, the path it lives at together with the fields it holds
|
||||
//! ([`matrix`], [`queue`], [`mtls`], [`forge`], [`github`], [`grafana`],
|
||||
//! [`acp`], [`ci`], [`review_classifier`], [`builder`], [`ci_metrics`],
|
||||
//! [`acp`], [`attic`], [`ci`], [`review_classifier`], [`builder`], [`ci_metrics`],
|
||||
//! [`claude_login`]).
|
||||
//! Each of those is a thing the controller and a hive must say identically, so
|
||||
//! it is said once here.
|
||||
|
|
@ -30,6 +30,7 @@
|
|||
//! needs only the provider and the store links nothing else.
|
||||
|
||||
pub mod acp;
|
||||
pub mod attic;
|
||||
pub mod builder;
|
||||
pub mod cert;
|
||||
#[cfg(feature = "otel")]
|
||||
|
|
|
|||
Loading…
Reference in a new issue