use std::{ collections::{BTreeSet, HashSet}, net::IpAddr, }; #[derive(Debug, Default, PartialEq)] pub struct Plan { pub add: Vec, /// (record id, ip) pub remove: Vec<(String, IpAddr)>, } /// Diff the healthy IPs against the group's current A records. /// /// Returns `None` when nothing is healthy: deleting every record would turn the name into /// NXDOMAIN, so the caller should leave DNS as it is. pub fn plan(desired: &BTreeSet, current: &[(String, IpAddr)]) -> Option { if desired.is_empty() { return None; } let mut kept = HashSet::new(); let mut remove = Vec::new(); for (id, ip) in current { // `insert` is false for a duplicate record of an IP we already keep. if desired.contains(ip) && kept.insert(*ip) { continue; } remove.push((id.clone(), *ip)); } let add = desired.iter().filter(|ip| !kept.contains(*ip)).copied().collect(); Some(Plan { add, remove }) } #[cfg(test)] mod tests { use super::*; use std::net::Ipv4Addr; fn ip(last: u8) -> IpAddr { IpAddr::V4(Ipv4Addr::new(192, 0, 2, last)) } fn rec(id: &str, last: u8) -> (String, IpAddr) { (id.to_string(), ip(last)) } #[test] fn replaces_placeholder_with_healthy_ips() { let desired = BTreeSet::from([ip(1), ip(2)]); let current = [("p".to_string(), IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)))]; let p = plan(&desired, ¤t).unwrap(); assert_eq!(p.add, vec![ip(1), ip(2)]); assert_eq!(p.remove, vec![("p".to_string(), IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)))]); } #[test] fn in_sync_is_a_noop() { let desired = BTreeSet::from([ip(1), ip(2)]); let p = plan(&desired, &[rec("a", 1), rec("b", 2)]).unwrap(); assert_eq!(p, Plan::default()); } #[test] fn removes_dead_and_adds_recovered() { let desired = BTreeSet::from([ip(1), ip(3)]); let p = plan(&desired, &[rec("a", 1), rec("b", 2)]).unwrap(); assert_eq!(p.add, vec![ip(3)]); assert_eq!(p.remove, vec![rec("b", 2)]); } #[test] fn drops_duplicate_records() { let desired = BTreeSet::from([ip(1)]); let p = plan(&desired, &[rec("a", 1), rec("b", 1)]).unwrap(); assert!(p.add.is_empty()); assert_eq!(p.remove, vec![rec("b", 1)]); } #[test] fn nothing_healthy_leaves_dns_alone() { assert_eq!(plan(&BTreeSet::new(), &[rec("a", 1)]), None); } }