Watch
0
0
Fork
You've already forked hyperhive
0

name the agent's entity before narrowing its role, and test hive provisioning against a fake store

This commit is contained in:
atlas 2026-10-27 10:11:02 +01:00
commit 09d89507a9

View file

@ -29,7 +29,9 @@ use std::{future::Future, time::Duration};
use anyhow::{Context, Result};
use backon::{BackoffBuilder as _, ConstantBuilder};
use swarm_secret_client::{Error, SecretStore, client::DEFAULT_CERT_MOUNT, policy};
use swarm_secret_client::{Error, client::DEFAULT_CERT_MOUNT, policy};
use crate::store::AccessStore;
/// File holding the authority hives are issued from, as
/// `swarm-controller.nix` names it.
@ -154,21 +156,38 @@ async fn provision(hives: &[String]) -> Result<(), Unreachable> {
}
Err(e) => return Err(Unreachable(e.to_string())),
};
if !write_shared_policy(&store).await {
return Ok(());
}
provision_with(&store, hives, read_hive_authority).await
}
/// The hive authority from the file `ENV_HIVE_CLIENT_CA` names, or `None` when
/// none is configured.
fn read_hive_authority() -> Result<Option<String>, Unreachable> {
let Some(path) = std::env::var_os(ENV_HIVE_CLIENT_CA) else {
tracing::info!("no hive certificate authority configured; hive roles are not managed here");
return Ok(());
return Ok(None);
};
let path = path.to_string_lossy().into_owned();
// Retryable rather than fatal: the unit that places this file and this
// daemon start together, so "not there yet" is a moment, not a verdict.
let ca = std::fs::read_to_string(&path)
.map_err(|e| Unreachable(format!("reading the hive authority {path}: {e}")))?;
std::fs::read_to_string(&path)
.map(Some)
.map_err(|e| Unreachable(format!("reading the hive authority {path}: {e}")))
}
/// [`provision`] against `store`, with the authority read by `read_authority`.
async fn provision_with(
store: &impl AccessStore,
hives: &[String],
read_authority: impl FnOnce() -> Result<Option<String>, Unreachable>,
) -> Result<(), Unreachable> {
if !write_shared_policy(store).await {
return Ok(());
}
let Some(ca) = read_authority()? else {
tracing::info!("no hive certificate authority configured; hive roles are not managed here");
return Ok(());
};
for hive in hives {
if let Err(e) = write_role(&store, hive, &ca).await {
if let Err(e) = write_role(store, hive, &ca).await {
tracing::warn!(hive, error = %format!("{e:#}"), "creating this hive's store role failed");
}
}
@ -180,7 +199,7 @@ async fn provision(hives: &[String]) -> Result<(), Unreachable> {
///
/// A store that refuses the accessor read (the controller's policy may not
/// carry it yet) or the write leaves the roles already in the store as they are.
async fn write_shared_policy(store: &SecretStore) -> bool {
async fn write_shared_policy(store: &impl AccessStore) -> bool {
let accessor = match store.auth_mount_accessor(DEFAULT_CERT_MOUNT).await {
Ok(accessor) => accessor,
Err(e) => {
@ -227,7 +246,7 @@ fn attached_policies() -> Vec<String> {
}
/// The role for one hive: its own certificate's subject and the shared policy.
async fn write_role(store: &SecretStore, hive: &str, ca: &str) -> Result<()> {
async fn write_role(store: &impl AccessStore, hive: &str, ca: &str) -> Result<()> {
let name = policy::hive_object_name(hive)?;
let attached = attached_policies();
store
@ -330,9 +349,11 @@ async fn rewrite_agent_policies() -> Result<(), Unreachable> {
mod tests {
use std::{cell::Cell, time::Duration};
use crate::store::fake::FakeAccess;
use super::{
RETRY_ATTEMPTS, RETRY_INTERVAL, RETRY_WINDOW, Unreachable, attached_policies, policy,
retry_until_ok,
provision_with, retry_until_ok,
};
#[test]
@ -385,4 +406,84 @@ mod tests {
// The control: the same lookup finds a policy that is attached.
assert!(attached.contains(&policy::SHARED_HIVE_POLICY.to_owned()));
}
fn hives() -> Vec<String> {
["pr1ma", "beta"].map(str::to_owned).to_vec()
}
#[tokio::test]
async fn every_hives_role_is_written_after_the_shared_policy_and_attaches_it_alone() {
let store = FakeAccess::default();
provision_with(&store, &hives(), || Ok(Some("HIVE-CA".to_owned())))
.await
.unwrap_or_else(|_| panic!("a store that accepts everything"));
assert_eq!(
store.roles_written(),
[
"write_cert_role hive-pr1ma hive-hives",
"write_cert_role hive-beta hive-hives"
]
);
let calls = store.calls();
let policy_at = calls
.iter()
.position(|c| c == "write_policy hive-hives")
.expect("the shared policy was written");
let first_role_at = calls
.iter()
.position(|c| c.starts_with("write_cert_role "))
.expect("a role was written");
assert!(policy_at < first_role_at, "{calls:?}");
}
#[tokio::test]
async fn an_unreadable_accessor_leaves_every_hive_role_as_it_was() {
let store = FakeAccess::without_accessor();
provision_with(&store, &hives(), || Ok(Some("HIVE-CA".to_owned())))
.await
.unwrap_or_else(|_| panic!("a policy that cannot be written is not a retry"));
assert!(store.roles_written().is_empty(), "{:?}", store.calls());
// The control: the accessor read was attempted, and the same fake
// writes roles when it works (the test above).
assert_eq!(store.calls(), ["auth_mount_accessor"]);
}
#[tokio::test]
async fn a_refused_shared_policy_leaves_every_hive_role_as_it_was() {
let store = FakeAccess::refusing_policies();
provision_with(&store, &hives(), || Ok(Some("HIVE-CA".to_owned())))
.await
.unwrap_or_else(|_| panic!("a refused write is not a retry"));
assert!(store.roles_written().is_empty(), "{:?}", store.calls());
assert!(
store
.calls()
.contains(&"write_policy hive-hives".to_owned())
);
}
#[tokio::test]
async fn without_an_authority_the_policy_is_written_and_no_role_is() {
let store = FakeAccess::default();
provision_with(&store, &hives(), || Ok(None))
.await
.unwrap_or_else(|_| panic!("no authority configured is not a retry"));
assert!(store.roles_written().is_empty(), "{:?}", store.calls());
assert!(
store
.calls()
.contains(&"write_policy hive-hives".to_owned())
);
}
#[tokio::test]
async fn an_authority_that_cannot_be_read_is_retried_and_writes_no_role() {
let store = FakeAccess::default();
let outcome = provision_with(&store, &hives(), || {
Err(Unreachable("not placed yet".to_owned()))
})
.await;
assert!(outcome.is_err());
assert!(store.roles_written().is_empty(), "{:?}", store.calls());
}
}