Skip to main content

dynomite/seeds/
dns.rs

1//! DNS-backed seeds provider.
2//!
3//! The reference engine issues a `T_TXT` query (or `T_A` when
4//! `DYNOMITE_DNS_TYPE=A`) against `_dynomite.<host>` and returns
5//! each TXT record's contents (or one synthesised seed per A
6//! record). The Rust port abstracts the resolver behind the
7//! [`Resolver`] trait so the unit test can drive a deterministic
8//! in-memory resolver. The caller is expected to wire
9//! `tokio::net::lookup_host` (or a similar resolver) when
10//! building the production provider.
11//!
12//! # Examples
13//!
14//! ```
15//! use dynomite::seeds::dns::{DnsSeedsProvider, Resolver, ResolvedSeeds};
16//! use dynomite::seeds::SeedsProvider;
17//!
18//! struct StaticResolver;
19//! impl Resolver for StaticResolver {
20//!     fn resolve(&self, _name: &str)
21//!         -> Result<ResolvedSeeds, dynomite::seeds::SeedsError>
22//!     {
23//!         Ok(ResolvedSeeds::Txt(vec![
24//!             "h1:8101:rA:dc1:1".into(),
25//!             "h2:8101:rA:dc1:2".into(),
26//!         ]))
27//!     }
28//! }
29//! let p = DnsSeedsProvider::new("_dynomite.example".into(), Box::new(StaticResolver));
30//! assert_eq!(p.get_seeds().unwrap().len(), 2);
31//! ```
32
33use std::sync::Arc;
34
35use crate::conf::ConfDynSeed;
36use crate::seeds::{SeedsError, SeedsProvider};
37
38/// Resolver result.
39#[derive(Debug, Clone)]
40pub enum ResolvedSeeds {
41    /// One TXT record per element. Each TXT body must be a
42    /// `host:port:rack:dc:tokens` seed (mirrors the reference
43    /// engine's `dns_get_seeds` TXT branch).
44    Txt(Vec<String>),
45    /// One A record per element, returned as `host:port` strings.
46    /// The provider attaches the supplied default rack/dc/tokens
47    /// when building the seed (mirrors the reference's `T_A`
48    /// branch where every result shares the same rack / dc).
49    A {
50        /// Resolved IP literals.
51        ips: Vec<String>,
52        /// Default port to attach.
53        port: u16,
54        /// Default rack name.
55        rack: String,
56        /// Default dc name.
57        dc: String,
58        /// Default token list.
59        tokens: String,
60    },
61}
62
63/// Trait used by [`DnsSeedsProvider`] to look up a name. Tests
64/// inject a deterministic implementation; the production binary
65/// wires `tokio::net::lookup_host` plus a TXT lookup helper.
66pub trait Resolver: Send + Sync {
67    /// Resolve `name` and return the [`ResolvedSeeds`].
68    fn resolve(&self, name: &str) -> Result<ResolvedSeeds, SeedsError>;
69}
70
71impl<T: Resolver + ?Sized> Resolver for Arc<T> {
72    fn resolve(&self, name: &str) -> Result<ResolvedSeeds, SeedsError> {
73        (**self).resolve(name)
74    }
75}
76
77/// DNS-backed provider.
78pub struct DnsSeedsProvider {
79    name: String,
80    resolver: Box<dyn Resolver>,
81}
82
83impl DnsSeedsProvider {
84    /// Build a provider that queries `name` via `resolver`.
85    ///
86    /// # Examples
87    ///
88    /// ```
89    /// use dynomite::seeds::dns::{DnsSeedsProvider, Resolver, ResolvedSeeds};
90    /// struct R;
91    /// impl Resolver for R {
92    ///     fn resolve(&self, _: &str)
93    ///         -> Result<ResolvedSeeds, dynomite::seeds::SeedsError>
94    ///     {
95    ///         Ok(ResolvedSeeds::Txt(Vec::new()))
96    ///     }
97    /// }
98    /// let p = DnsSeedsProvider::new("n".into(), Box::new(R));
99    /// assert_eq!(p.name(), "n");
100    /// ```
101    #[must_use]
102    pub fn new(name: String, resolver: Box<dyn Resolver>) -> Self {
103        Self { name, resolver }
104    }
105
106    /// DNS query name.
107    #[must_use]
108    pub fn name(&self) -> &str {
109        &self.name
110    }
111}
112
113impl std::fmt::Debug for DnsSeedsProvider {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        f.debug_struct("DnsSeedsProvider")
116            .field("name", &self.name)
117            .finish_non_exhaustive()
118    }
119}
120
121impl SeedsProvider for DnsSeedsProvider {
122    fn get_seeds(&self) -> Result<Vec<ConfDynSeed>, SeedsError> {
123        let resolved = self.resolver.resolve(&self.name)?;
124        match resolved {
125            ResolvedSeeds::Txt(entries) => {
126                let mut out = Vec::with_capacity(entries.len());
127                for raw in entries {
128                    let seed =
129                        ConfDynSeed::parse(&raw).map_err(|e| SeedsError::Parse(e.to_string()))?;
130                    out.push(seed);
131                }
132                Ok(out)
133            }
134            ResolvedSeeds::A {
135                ips,
136                port,
137                rack,
138                dc,
139                tokens,
140            } => {
141                let mut out = Vec::with_capacity(ips.len());
142                for ip in ips {
143                    let raw = format!("{ip}:{port}:{rack}:{dc}:{tokens}");
144                    let seed =
145                        ConfDynSeed::parse(&raw).map_err(|e| SeedsError::Parse(e.to_string()))?;
146                    out.push(seed);
147                }
148                Ok(out)
149            }
150        }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    struct StaticResolver(ResolvedSeeds);
159    impl Resolver for StaticResolver {
160        fn resolve(&self, _: &str) -> Result<ResolvedSeeds, SeedsError> {
161            Ok(self.0.clone())
162        }
163    }
164
165    #[test]
166    fn txt_branch() {
167        let r = StaticResolver(ResolvedSeeds::Txt(vec![
168            "127.0.0.1:8101:rA:dc1:1".into(),
169            "127.0.0.2:8101:rA:dc1:2".into(),
170        ]));
171        let p = DnsSeedsProvider::new("n".into(), Box::new(r));
172        let v = p.get_seeds().unwrap();
173        assert_eq!(v.len(), 2);
174        assert_eq!(v[0].host(), "127.0.0.1");
175    }
176
177    #[test]
178    fn a_branch_synthesises_seed_format() {
179        let r = StaticResolver(ResolvedSeeds::A {
180            ips: vec!["10.0.0.1".into(), "10.0.0.2".into()],
181            port: 8101,
182            rack: "rA".into(),
183            dc: "dc1".into(),
184            tokens: "1".into(),
185        });
186        let p = DnsSeedsProvider::new("n".into(), Box::new(r));
187        let v = p.get_seeds().unwrap();
188        assert_eq!(v.len(), 2);
189        assert_eq!(v[0].port(), 8101);
190        assert_eq!(v[0].dc(), "dc1");
191    }
192
193    #[test]
194    fn parse_error_propagates() {
195        let r = StaticResolver(ResolvedSeeds::Txt(vec!["invalid-seed".into()]));
196        let p = DnsSeedsProvider::new("n".into(), Box::new(r));
197        assert!(matches!(p.get_seeds(), Err(SeedsError::Parse(_))));
198    }
199}