name the agent's entity before narrowing its role, and test hive provisioning against a fake store
This commit is contained in:
parent
139007e656
commit
09d89507a9
1 changed files with 535 additions and 67 deletions
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue