use std::collections::HashSet; use anyhow::Result; use aws_sdk_ec2 as ec2; use aws_sdk_ec2::types::Filter; use aws_sdk_route53::types::{ResourceRecord, ResourceRecordSet, RrType}; // Needed until try_collect is stable, see use itertools::Itertools; use trust_dns_proto::rr::Name; use crate::hashable::Hashable; pub trait Ec2 { fn ec2(&self) -> &ec2::Client; } pub async fn instance_recordsets( aws_context: &C, asg_name: &str, dns_name: &Name, dns_ttl: i64, live_instance_ids: &[String], ) -> Result>> where C: Ec2, { assert!(dns_name.is_fqdn()); // If there's nothing running, then (a) we don't need to ask AWS about // running instances, and (b) we can't anyways as the API call requires at // least one instance ID. Abort here. if live_instance_ids.is_empty() { return Ok(HashSet::new()); } let asg_filter = Filter::builder() .name("tag:aws:autoscaling:groupName") .values(asg_name) .build(); let mut ip4 = HashSet::new(); let mut ip6 = HashSet::new(); let mut instances_paginator = aws_context .ec2() .describe_instances() .set_instance_ids(Some(live_instance_ids.to_owned())) .filters(asg_filter) .into_paginator() .items() .send(); while let Some(reservation) = instances_paginator.try_next().await? { let instances = reservation.instances(); for instance in instances { // Mild abuse of the fact that optional values are also iterable ip4.extend(instance.public_ip_address().map(String::from)); let instance_interfaces = instance.network_interfaces(); let instance_ip6: Vec<_> = instance_interfaces .iter() .flat_map(|interface| interface.ipv6_addresses()) // Flatmap here to drop the None values, unwrap the Some values .flat_map(|ipv6| ipv6.ipv6_address()) .to_owned() .collect(); ip6.extend(instance_ip6.iter().map(ToOwned::to_owned).map(String::from)); } } let dns_name = dns_name.to_ascii(); let ip4_recordset = host_recordset(&dns_name, dns_ttl, RrType::A, ip4)?; let ip6_recordset = host_recordset(&dns_name, dns_ttl, RrType::Aaaa, ip6)?; Ok(ip4_recordset .into_iter() .chain(ip6_recordset.into_iter()) .map(Hashable::from) .collect()) } fn host_recordset( dns_name: &str, dns_ttl: i64, 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 = ResourceRecordSet::builder() .name(dns_name) .r#type(rr_type) .ttl(dns_ttl) .set_resource_records(Some(records)) .build()?; Ok(Some(record_set)) } }