use std::collections::HashSet; use std::str::FromStr; use anyhow::anyhow; use aws_sdk_route53 as route53; use aws_sdk_route53::types::{HostedZone, ResourceRecord, ResourceRecordSet, RrType}; use trust_dns_proto::rr::Name; use crate::dns::suffixes; use crate::hashable::Hashable; use crate::result::Result; pub trait Route53 { fn route53(&self) -> &route53::Client; } pub async fn zone_for_domain(name: &Name, aws_context: &C) -> Result where C: Route53, { let names = suffixes(name.clone()); // Outer pagination loop needs to be pulled out to a trait - this is some hot nonsense. let mut zone = None; let mut depth = None; let mut zones_marker = None; loop { let zones_resp = aws_context .route53() .list_hosted_zones() .set_marker(zones_marker) .send() .await?; let zones = zones_resp.hosted_zones().unwrap_or(&[]); for candidate_zone in zones.iter() { let zone_name = match candidate_zone.name() { None => continue, Some(name) => name, }; let zone_name = Name::from_str(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); } } (_, _) => {} } } if zones_resp.is_truncated() { zones_marker = zones_resp.next_marker().map(String::from); } else { break; } } zone.ok_or(anyhow!("No Route53 zone found for DNS suffix: {}", name)) } pub async fn zone_suffix_recordsets( dns_suffix: &Name, zone_id: &str, aws_context: &C, ) -> Result>> where C: Route53, { let mut suffix_records = HashSet::new(); let mut next_record_name = Some(dns_suffix.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().unwrap_or(&[]); for recordset in recordsets { let recordset_name = recordset.name().ok_or(anyhow!( "Record set with no name found in zone: {}", zone_id ))?; let recordset_name = Name::from_str(recordset_name)?; let recordset_names = suffixes(recordset_name); if !recordset_names.iter().any(|name| name == dns_suffix) { break; } 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().map(Clone::clone); next_record_identifier = records_resp.next_record_identifier().map(String::from); } else { break; } } Ok(suffix_records) } pub fn recordset( apex_hostname: &str, dns_ttl: i64, rr_type: RrType, addresses: I, ) -> ResourceRecordSet where I: IntoIterator, S: Into, { let apex_ip4_records = addresses .into_iter() .map(|address| address.into()) .map(|address| ResourceRecord::builder().value(address).build()) .collect(); ResourceRecordSet::builder() .name(apex_hostname) .r#type(rr_type) .ttl(dns_ttl) .set_resource_records(Some(apex_ip4_records)) .build() }