use std::collections::HashSet; use std::str::FromStr; use anyhow::{anyhow, Result}; use aws_sdk_route53 as route53; use aws_sdk_route53::types::{HostedZone, ResourceRecord, RrType}; // Needed until try_collect is stable, see use itertools::Itertools; use trust_dns_proto::rr::Name; use crate::dns; pub trait Route53 { fn route53(&self) -> &route53::Client; } pub async fn zone_for_name(aws_context: &C, name: &Name) -> Result where C: Route53, { let names = dns::suffixes(name.clone()); let mut zone = None; let mut depth = None; let mut zones = aws_context .route53() .list_hosted_zones() .into_paginator() .items() .send(); while let Some(candidate_zone) = zones.try_next().await? { let zone_name = Name::from_str(candidate_zone.name())?; let match_position = names.iter().position(|name| *name == zone_name); match (depth, match_position) { (None, Some(matched_depth)) => { zone = Some(candidate_zone.clone()); depth = Some(matched_depth); } (Some(found_depth), Some(matched_depth)) => { if matched_depth < found_depth { zone = Some(candidate_zone.clone()); depth = Some(matched_depth); } } (_, _) => {} } } zone.ok_or(anyhow!("No Route53 zone found for DNS suffix: {}", name)) } pub async fn zone_actual_recordsets( aws_context: &C, zone_id: &str, dns_name: &Name, ) -> Result> where C: Route53, { let mut suffix_records = HashSet::new(); let mut next_record_name = Some(dns_name.to_ascii()); let mut next_record_type = None; let mut next_record_identifier = None; loop { let records_resp = aws_context .route53() .list_resource_record_sets() .hosted_zone_id(zone_id) .set_start_record_name(next_record_name) .set_start_record_type(next_record_type) .set_start_record_identifier(next_record_identifier) .send() .await?; let recordsets = records_resp.resource_record_sets(); for recordset in recordsets { let recordset_name = recordset.name(); let recordset_name = Name::from_str(recordset_name)?; if &recordset_name != dns_name { break; } if [RrType::A, RrType::Aaaa].contains(recordset.r#type()) { suffix_records.insert(recordset.clone().into()); } } if records_resp.is_truncated() { next_record_name = records_resp.next_record_name().map(String::from); next_record_type = records_resp.next_record_type().cloned(); next_record_identifier = records_resp.next_record_identifier().map(String::from); } else { break; } } Ok(suffix_records) } pub struct Target { name: Name, ttl: i64, } impl Target { pub fn new(name: &Name, ttl: i64) -> Result { let name = dns::absolute(name.to_owned())?; Ok(Self { name, ttl }) } pub fn name(&self) -> &Name { &self.name } pub fn host_proposal( &self, rr_type: RrType, addresses: HashSet>, ) -> Result> { if addresses.is_empty() { Ok(None) } else { let records = addresses .into_iter() .map(|address| address.into()) .map(|address| ResourceRecord::builder().value(address).build()) .try_collect()?; let record_set = route53::types::ResourceRecordSet::builder() .name(self.name().to_ascii()) .r#type(rr_type) .ttl(self.ttl) .set_resource_records(Some(records)) .build()? .into(); Ok(Some(record_set)) } } } // ResourceRecordSet isn't hashable (reasonably enough), but we're going to do set difference on them // later to identify which records to remove, and which to keep. This wrapper type adds a trivial - but // probably correct - implementation of Hash and Eq so that ResourceRecordSet instances can be stored // in hash sets. #[derive(Clone, Debug, PartialEq)] pub struct ResourceRecordSet(route53::types::ResourceRecordSet); // Equality is based on the observation that ResourceRecordSet already implements partialeq by derive, // and that none of its fields have any actual partially-equal results. As a result, any two // ResourceRecordSet instances that are fieldwise-equal are themselves equal under PartialEq. Promote // this result to Eq. impl Eq for ResourceRecordSet {} // Hash ResourceRecordSet instances by name and address(es). impl std::hash::Hash for ResourceRecordSet { fn hash(&self, state: &mut H) { self.0.name().hash(state); self.0 .resource_records() .iter() .map(ResourceRecord::value) .for_each(|val| val.hash(state)); } } impl From for ResourceRecordSet { fn from(recordset: route53::types::ResourceRecordSet) -> Self { Self(recordset) } } impl From for route53::types::ResourceRecordSet { fn from(recordset: ResourceRecordSet) -> Self { recordset.0 } }