flytrap/
app.rs

1use std::net::Ipv6Addr;
2
3use futures::future::join_all;
4use hickory_resolver::{
5    error::ResolveError, proto::rr::rdata::AAAA, IntoName, Name, TokioAsyncResolver,
6};
7
8use crate::{
9    error::Error,
10    resolver::{lookup_txt, Resolver},
11    Node, Peer, Region,
12};
13
14/// Query the Fly.io [internal DNS][] records for a particular app.
15///
16/// [internal DNS]: https://fly.io/docs/reference/private-networking/#fly-internal-addresses
17pub struct AppResolver<'r> {
18    domain: Name,
19    resolver: &'r TokioAsyncResolver,
20}
21
22impl<'r> AppResolver<'r> {
23    pub(crate) fn new(app: impl Into<String>, resolver: &'r Resolver) -> Self {
24        let app: String = app.into();
25        let name = Name::from_ascii(app).expect("invalid app name");
26        let domain = name.append_label("internal").unwrap();
27
28        Self {
29            domain,
30            resolver: &resolver.0,
31        }
32    }
33
34    /// Find the Fly.io regions where this app is deployed.
35    #[cfg(feature = "regions")]
36    #[cfg_attr(docsrs, doc(cfg(feature = "regions")))]
37    pub async fn regions(&self) -> Result<Vec<Region>, Error> {
38        let value = self.txt("regions").await?;
39
40        Ok(value
41            .split(',')
42            .filter_map(|code| code.parse::<Region>().ok())
43            .collect())
44    }
45
46    /// Find all running instances of this Fly.io app.
47    pub async fn nodes(&self) -> Result<Vec<Node>, Error> {
48        let value = self.txt("vms").await?;
49
50        value.split(',').map(|peer| peer.parse::<Node>()).collect()
51    }
52
53    /// Find all running [instances][AppResolver::nodes] of this Fly.io app, and
54    /// resolve all their instance ID’s to private IP addresses.
55    pub async fn peers(&self) -> Result<Vec<Peer>, Error> {
56        let nodes = self.nodes().await?;
57
58        let addrs = join_all(nodes.iter().map(|node| {
59            let name = Name::from_ascii(&node.id)
60                .expect("invalid node ID")
61                .append_label("vm")
62                .unwrap()
63                .append_domain(&self.domain)
64                .expect("invalid query");
65
66            self.resolver.ipv6_lookup(name)
67        }))
68        .await
69        .into_iter()
70        .collect::<Result<Vec<_>, ResolveError>>()
71        .map_err(Error::from)?;
72
73        Ok(nodes
74            .into_iter()
75            .zip(addrs.into_iter())
76            .filter_map(|(node, addrs)| {
77                if let Some(AAAA(addr)) = addrs.into_iter().next() {
78                    Some(node.into_peer(addr))
79                } else {
80                    None
81                }
82            })
83            .collect())
84    }
85
86    /// Find the geographically-nearest _n_ instances of this Fly.io app.
87    pub async fn nearest_peer_addresses(&self, n: usize) -> Result<Vec<Ipv6Addr>, Error> {
88        let top = Name::from_ascii(format!("top{n}"))
89            .expect("invalid top n")
90            .append_label("nearest")
91            .unwrap()
92            .append_label("of")
93            .unwrap()
94            .append_domain(&self.domain)
95            .expect("invalid query");
96
97        let results = self.resolver.ipv6_lookup(top).await.map_err(Error::from)?;
98
99        Ok(results.into_iter().map(|r| r.0).collect())
100    }
101
102    /// Perform an arbitrary `TXT` record query on the `<app>.internal` domain.
103    pub async fn txt(&self, name: impl IntoName) -> Result<String, Error> {
104        let query = name
105            .into_name()
106            .expect("invalid name")
107            .append_domain(&self.domain)
108            .expect("invalid app domain");
109
110        lookup_txt(self.resolver, query).await
111    }
112}