summaryrefslogtreecommitdiff
path: root/src/route53.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/route53.rs')
-rw-r--r--src/route53.rs143
1 files changed, 143 insertions, 0 deletions
diff --git a/src/route53.rs b/src/route53.rs
new file mode 100644
index 0000000..22e4126
--- /dev/null
+++ b/src/route53.rs
@@ -0,0 +1,143 @@
+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<C>(name: &Name, aws_context: &C) -> Result<HostedZone>
+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<C>(
+ dns_suffix: &Name,
+ zone_id: &str,
+ aws_context: &C,
+) -> Result<HashSet<Hashable<ResourceRecordSet>>>
+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<I, S>(
+ apex_hostname: &str,
+ dns_ttl: i64,
+ rr_type: RrType,
+ addresses: I,
+) -> ResourceRecordSet
+where
+ I: IntoIterator<Item = S>,
+ S: Into<String>,
+{
+ 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()
+}