use std::collections::BTreeMap;
use super::inventory::FederationInventory;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObservedNode {
pub id: String,
pub present: bool,
pub can_sign: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ObservedState {
pub nodes: Vec<ObservedNode>,
pub strict_enforced: bool,
}
impl ObservedState {
#[must_use]
pub fn node(&self, id: &str) -> Option<&ObservedNode> {
self.nodes.iter().find(|n| n.id == id)
}
fn is_sign_capable(&self, id: &str) -> bool {
self.node(id).is_some_and(|n| n.present && n.can_sign)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReconcileAction {
EnrollNode { id: String },
IssueCredential { id: String },
DecommissionNode { id: String },
EnableStrictEnforcement,
DisableStrictEnforcement,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ReconcilePlan {
pub actions: Vec<ReconcileAction>,
}
impl ReconcilePlan {
#[must_use]
pub fn is_noop(&self) -> bool {
self.actions.is_empty()
}
}
#[must_use]
pub fn reconcile(desired: &FederationInventory, observed: &ObservedState) -> ReconcilePlan {
let mut actions = Vec::new();
let desired_ids: BTreeMap<&str, ()> = desired.nodes().map(|n| (n.id.as_str(), ())).collect();
for node in desired.nodes() {
match observed.node(&node.id) {
None => actions.push(ReconcileAction::EnrollNode {
id: node.id.clone(),
}),
Some(obs) if !obs.present => actions.push(ReconcileAction::EnrollNode {
id: node.id.clone(),
}),
Some(obs) if !obs.can_sign => actions.push(ReconcileAction::IssueCredential {
id: node.id.clone(),
}),
Some(_) => {}
}
}
for obs in &observed.nodes {
if !desired_ids.contains_key(obs.id.as_str()) {
actions.push(ReconcileAction::DecommissionNode { id: obs.id.clone() });
}
}
let want_strict = desired.enforcement.require_sig;
if want_strict && !observed.strict_enforced {
let all_sign_capable = desired.nodes().all(|n| observed.is_sign_capable(&n.id));
if all_sign_capable {
actions.push(ReconcileAction::EnableStrictEnforcement);
}
} else if !want_strict && observed.strict_enforced {
actions.push(ReconcileAction::DisableStrictEnforcement);
}
ReconcilePlan { actions }
}
#[cfg(test)]
mod tests {
use super::*;
fn inv(yaml: &str) -> FederationInventory {
FederationInventory::from_yaml_str(yaml).expect("valid inventory")
}
const TWO_NODE_STRICT: &str = "\
trust_domain: fleet
regions:
- name: r
nodes:
- id: node-1
attestor: mtls-cert
cred_ttl: 1h
renew_before: 5m
- id: node-2
attestor: mtls-cert
cred_ttl: 1h
renew_before: 5m
quorum:
width: 2
enforcement:
require_sig: true
";
fn signing(id: &str) -> ObservedNode {
ObservedNode {
id: id.to_string(),
present: true,
can_sign: true,
}
}
#[test]
fn converged_fleet_is_a_noop() {
let desired = inv(TWO_NODE_STRICT);
let observed = ObservedState {
nodes: vec![signing("node-1"), signing("node-2")],
strict_enforced: true,
};
let plan = reconcile(&desired, &observed);
assert!(plan.is_noop(), "converged state must produce no actions");
}
#[test]
fn absent_desired_node_is_enrolled() {
let desired = inv(TWO_NODE_STRICT);
let observed = ObservedState {
nodes: vec![signing("node-1")],
strict_enforced: false,
};
let plan = reconcile(&desired, &observed);
assert!(plan.actions.contains(&ReconcileAction::EnrollNode {
id: "node-2".to_string()
}));
}
#[test]
fn not_present_node_is_enrolled_not_issued() {
let desired = inv(TWO_NODE_STRICT);
let observed = ObservedState {
nodes: vec![
signing("node-1"),
ObservedNode {
id: "node-2".to_string(),
present: false,
can_sign: false,
},
],
strict_enforced: false,
};
let plan = reconcile(&desired, &observed);
assert!(plan.actions.contains(&ReconcileAction::EnrollNode {
id: "node-2".to_string()
}));
assert!(!plan.actions.contains(&ReconcileAction::IssueCredential {
id: "node-2".to_string()
}));
}
#[test]
fn present_node_without_credential_gets_issue() {
let desired = inv(TWO_NODE_STRICT);
let observed = ObservedState {
nodes: vec![
signing("node-1"),
ObservedNode {
id: "node-2".to_string(),
present: true,
can_sign: false,
},
],
strict_enforced: false,
};
let plan = reconcile(&desired, &observed);
assert!(plan.actions.contains(&ReconcileAction::IssueCredential {
id: "node-2".to_string()
}));
}
#[test]
fn observed_node_not_in_inventory_is_decommissioned() {
let desired = inv(TWO_NODE_STRICT);
let observed = ObservedState {
nodes: vec![signing("node-1"), signing("node-2"), signing("ghost")],
strict_enforced: true,
};
let plan = reconcile(&desired, &observed);
assert_eq!(
plan.actions,
vec![ReconcileAction::DecommissionNode {
id: "ghost".to_string()
}]
);
}
#[test]
fn strict_enforcement_is_deferred_until_all_nodes_sign_capable() {
let desired = inv(TWO_NODE_STRICT);
let observed = ObservedState {
nodes: vec![
signing("node-1"),
ObservedNode {
id: "node-2".to_string(),
present: true,
can_sign: false,
},
],
strict_enforced: false,
};
let plan = reconcile(&desired, &observed);
assert!(
!plan
.actions
.contains(&ReconcileAction::EnableStrictEnforcement),
"must NOT strict-enforce while a desired node cannot sign"
);
assert!(plan.actions.contains(&ReconcileAction::IssueCredential {
id: "node-2".to_string()
}));
}
#[test]
fn strict_enforcement_enabled_and_ordered_last_when_all_sign_capable() {
let desired = inv(TWO_NODE_STRICT);
let observed = ObservedState {
nodes: vec![signing("node-1"), signing("node-2")],
strict_enforced: false,
};
let plan = reconcile(&desired, &observed);
assert_eq!(
plan.actions.last(),
Some(&ReconcileAction::EnableStrictEnforcement),
"enforcement tightening must be the final action"
);
}
#[test]
fn enable_strict_is_last_even_with_enroll_and_decommission_in_same_pass() {
let desired = inv(TWO_NODE_STRICT);
let observed = ObservedState {
nodes: vec![signing("node-1"), signing("node-2"), signing("ghost")],
strict_enforced: false,
};
let plan = reconcile(&desired, &observed);
assert_eq!(
plan.actions.last(),
Some(&ReconcileAction::EnableStrictEnforcement)
);
let decomm_idx = plan
.actions
.iter()
.position(|a| matches!(a, ReconcileAction::DecommissionNode { .. }))
.expect("decommission present");
let enforce_idx = plan
.actions
.iter()
.position(|a| matches!(a, ReconcileAction::EnableStrictEnforcement))
.expect("enforce present");
assert!(decomm_idx < enforce_idx);
}
#[test]
fn relaxing_enforcement_is_immediate() {
let permissive = inv("\
trust_domain: fleet
regions:
- name: r
nodes:
- id: node-1
attestor: mtls-cert
cred_ttl: 1h
renew_before: 5m
quorum:
width: 1
enforcement:
require_sig: false
");
let observed = ObservedState {
nodes: vec![signing("node-1")],
strict_enforced: true,
};
let plan = reconcile(&permissive, &observed);
assert_eq!(
plan.actions,
vec![ReconcileAction::DisableStrictEnforcement]
);
}
#[test]
fn already_permissive_desired_permissive_is_noop() {
let permissive = inv("\
trust_domain: fleet
quorum:
width: 1
");
let observed = ObservedState::default();
let plan = reconcile(&permissive, &observed);
assert!(plan.is_noop());
}
}