char/dns-ez
cloudflare dns management via plaintext zonefiles
git clone https://git.t4t.associates/char/dns-ez
cb198f4
main
1use std:: collections:: HashMap ; 2 3use crate :: cloudflare:: LiveRecord ; 4use crate :: dns:: Record ; 5 6pub enum Action { 7Create ( Record ), 8// An update is always a TTL change; an rdata change is delete + create. 9Update { 10id : String , 11from_ttl : u32 , 12to : Record , 13}, 14Delete ( LiveRecord ), 15} 16 17impl Action { 18pub fn describe ( & self ) ->String { 19match self { 20Action :: Create ( r) =>format! ( "+ {r}" ), 21Action :: Update { from_ttl, to, ..} =>{ 22format! ( "~ {to} (ttl {from_ttl} -> {})" , to. ttl ) 23} 24Action :: Delete ( live) =>format! ( "- {}" , live. record ), 25} 26} 27} 28 29/// Diff desired (on disk) against live (at the provider). 30/// Pure set reconciliation on (name, type, rdata): a live record with no 31/// desired counterpart is a deletion, and vice versa; equal keys with 32/// different TTLs are updates. 33pub fn plan ( desired : & [ Record ], live : & [ LiveRecord ]) ->Vec < Action > { 34let mut unmatched: HashMap < _ , & LiveRecord > = live. iter (). map ( |l|( l. record . key (), l)). collect (); 35let mut actions =Vec :: new (); 36for din desired{ 37match unmatched. remove ( & d. key ()) { 38Some ( l) if l. record . ttl != d. ttl => actions. push ( Action :: Update { 39id : l. id . clone (), 40from_ttl : l. record . ttl , 41to : d. clone (), 42}), 43Some ( _) =>{} 44None => actions. push ( Action :: Create ( d. clone ())), 45} 46} 47 actions. extend ( unmatched. into_values (). cloned (). map ( Action :: Delete )); 48 actions. sort_by_key ( Action :: describe); 49 actions 50} 51 52# [ cfg ( test )] 53mod tests{ 54use super :: * ; 55 56fn live ( id : & str , name : & str , rtype : & str , ttl : u32 , content : & str ) ->LiveRecord { 57LiveRecord { 58id : id. to_string (), 59record : Record :: parse ( name, rtype, ttl, content). unwrap (), 60} 61} 62 63fn desired ( name : & str , rtype : & str , ttl : u32 , content : & str ) ->Record { 64Record :: parse ( name, rtype, ttl, content). unwrap () 65} 66 67# [ test ] 68fn in_sync_means_no_actions () { 69let desired =vec! [ desired ( "www.example.com" , "A" , 300 , "1.2.3.4" )]; 70let live =vec! [ live ( "1" , "www.example.com" , "A" , 300 , "1.2.3.4" )]; 71assert! ( plan ( & desired, & live). is_empty ()); 72} 73 74# [ test ] 75fn plans_create_update_delete () { 76let desired =vec! [ 77desired ( "www.example.com" , "A" , 300 , "1.2.3.4" ), // create 78desired ( "api.example.com" , "A" , 60 , "5.6.7.8" ), // ttl update 79]; 80let live =vec! [ 81live ( "1" , "api.example.com" , "A" , 300 , "5.6.7.8" ), 82live ( "2" , "old.example.com" , "TXT" , 300 , "stale" ), // delete 83]; 84let actions =plan ( & desired, & live); 85assert! ( matches! ( actions[ 0 ], Action :: Create ( _))); 86assert! ( matches! ( actions[ 1 ], Action :: Delete ( _))); 87assert! ( matches! ( actions[ 2 ], Action :: Update { ..})); 88assert_eq! ( actions. len (), 3 ); 89} 90 91# [ test ] 92fn txt_quoting_differences_are_not_changes () { 93let desired =vec! [ desired ( "example.com" , "TXT" , 300 , "v=spf1 -all" )]; 94let live =vec! [ live ( "1" , "example.com" , "TXT" , 300 , "\"v=spf1 -all\"" )]; 95assert! ( plan ( & desired, & live). is_empty ()); 96} 97}