-
Notifications
You must be signed in to change notification settings - Fork 42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[internal-dns] Add utilities in the client library #1177
Changes from all commits
b78ff98
cca5795
fccc15c
2443215
a077bd4
f91cea1
d16eda2
8db30b7
3a0c6ba
33b3e02
3eb57dc
39aa9ff
dd04a67
63b6379
02f592d
ff2d7b9
a261155
6cc7864
2a035a5
1e0b8fe
da4a2b8
d7b10cf
4df23c2
71f3aac
5556d5f
226fd94
6126e41
e4f434f
62fccb2
1905985
fd8286a
b959c39
470da8b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,596 @@ | ||
// This Source Code Form is subject to the terms of the Mozilla Public | ||
// License, v. 2.0. If a copy of the MPL was not distributed with this | ||
// file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
|
||
use crate::types::{DnsKv, DnsRecord, DnsRecordKey, Srv}; | ||
use futures::stream::{self, StreamExt, TryStreamExt}; | ||
use omicron_common::address::{ | ||
Ipv6Subnet, ReservedRackSubnet, AZ_PREFIX, DNS_PORT, DNS_SERVER_PORT, | ||
}; | ||
use slog::{info, Logger}; | ||
use std::collections::HashMap; | ||
use std::net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV6}; | ||
use trust_dns_resolver::config::{ | ||
NameServerConfig, Protocol, ResolverConfig, ResolverOpts, | ||
}; | ||
use trust_dns_resolver::TokioAsyncResolver; | ||
|
||
pub type DnsError = crate::Error<crate::types::Error>; | ||
|
||
pub type AAAARecord = (crate::names::AAAA, SocketAddrV6); | ||
|
||
/// Describes how to find the DNS servers. | ||
/// | ||
/// In production code, this is nearly always [`Ipv6Subnet<AZ_PREFIX>`], | ||
/// but it allows a point of dependency-injection for tests to supply their | ||
/// own address lookups. | ||
pub trait DnsAddressLookup { | ||
fn dropshot_server_addrs(&self) -> Vec<SocketAddr>; | ||
|
||
fn dns_server_addrs(&self) -> Vec<SocketAddr>; | ||
} | ||
|
||
fn subnet_to_ips( | ||
subnet: Ipv6Subnet<AZ_PREFIX>, | ||
) -> impl Iterator<Item = IpAddr> { | ||
ReservedRackSubnet::new(subnet) | ||
.get_dns_subnets() | ||
.into_iter() | ||
.map(|dns_subnet| IpAddr::V6(dns_subnet.dns_address().ip())) | ||
} | ||
|
||
impl DnsAddressLookup for Ipv6Subnet<AZ_PREFIX> { | ||
fn dropshot_server_addrs(&self) -> Vec<SocketAddr> { | ||
subnet_to_ips(*self) | ||
.map(|address| SocketAddr::new(address, DNS_SERVER_PORT)) | ||
.collect() | ||
} | ||
|
||
fn dns_server_addrs(&self) -> Vec<SocketAddr> { | ||
subnet_to_ips(*self) | ||
.map(|address| SocketAddr::new(address, DNS_PORT)) | ||
.collect() | ||
} | ||
} | ||
|
||
/// A connection used to update multiple DNS servers. | ||
pub struct Updater { | ||
log: Logger, | ||
clients: Vec<crate::Client>, | ||
} | ||
|
||
impl Updater { | ||
pub fn new(address_getter: &impl DnsAddressLookup, log: Logger) -> Self { | ||
let addrs = address_getter.dropshot_server_addrs(); | ||
Self::new_from_addrs(addrs, log) | ||
} | ||
|
||
fn new_from_addrs(addrs: Vec<SocketAddr>, log: Logger) -> Self { | ||
let clients = addrs | ||
.into_iter() | ||
.map(|addr| { | ||
info!(log, "Adding DNS server: {}", addr); | ||
crate::Client::new(&format!("http://{}", addr), log.clone()) | ||
}) | ||
.collect::<Vec<_>>(); | ||
|
||
Self { log, clients } | ||
} | ||
|
||
/// Inserts all service records into the DNS server. | ||
/// | ||
/// Each SRV record should have one or more AAAA records. | ||
pub async fn insert_dns_records( | ||
&self, | ||
records: &HashMap<crate::names::SRV, Vec<AAAARecord>>, | ||
) -> Result<(), DnsError> { | ||
for (srv, aaaa) in records.iter() { | ||
info!(self.log, "Inserting DNS record: {:?}", srv); | ||
|
||
self.insert_dns_records_internal(aaaa, srv).await?; | ||
} | ||
Ok(()) | ||
} | ||
|
||
// Utility function to insert: | ||
// - A set of uniquely-named AAAA records, each corresponding to an address | ||
// - An SRV record, pointing to each of the AAAA records. | ||
async fn insert_dns_records_internal( | ||
&self, | ||
aaaa: &Vec<AAAARecord>, | ||
srv_key: &crate::names::SRV, | ||
) -> Result<(), DnsError> { | ||
let mut records = Vec::with_capacity(aaaa.len() + 1); | ||
|
||
// Add one DnsKv per AAAA, each with a single record. | ||
records.extend(aaaa.iter().map(|(name, addr)| DnsKv { | ||
key: DnsRecordKey { name: name.to_string() }, | ||
records: vec![DnsRecord::Aaaa(*addr.ip())], | ||
})); | ||
|
||
// Add the DnsKv for the SRV, with a record for each AAAA. | ||
records.push(DnsKv { | ||
key: DnsRecordKey { name: srv_key.to_string() }, | ||
records: aaaa | ||
.iter() | ||
.map(|(name, addr)| { | ||
DnsRecord::Srv(Srv { | ||
prio: 0, | ||
weight: 0, | ||
port: addr.port(), | ||
target: name.to_string(), | ||
}) | ||
}) | ||
.collect::<Vec<_>>(), | ||
}); | ||
self.dns_records_set(&records).await | ||
} | ||
|
||
/// Sets a records on all DNS servers. | ||
/// | ||
/// Returns an error if setting the record fails on any server. | ||
pub async fn dns_records_set<'a>( | ||
&'a self, | ||
body: &'a Vec<crate::types::DnsKv>, | ||
) -> Result<(), DnsError> { | ||
stream::iter(&self.clients) | ||
.map(Ok::<_, DnsError>) | ||
.try_for_each_concurrent(None, |client| async move { | ||
client.dns_records_set(body).await?; | ||
Ok(()) | ||
}) | ||
.await?; | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Deletes records in all DNS servers. | ||
/// | ||
/// Returns an error if deleting the record fails on any server. | ||
pub async fn dns_records_delete<'a>( | ||
&'a self, | ||
body: &'a Vec<crate::types::DnsRecordKey>, | ||
) -> Result<(), DnsError> { | ||
stream::iter(&self.clients) | ||
.map(Ok::<_, DnsError>) | ||
.try_for_each_concurrent(None, |client| async move { | ||
client.dns_records_delete(body).await?; | ||
Ok(()) | ||
}) | ||
.await?; | ||
|
||
Ok(()) | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone, thiserror::Error)] | ||
pub enum ResolveError { | ||
#[error(transparent)] | ||
Resolve(#[from] trust_dns_resolver::error::ResolveError), | ||
|
||
#[error("Record not found for SRV key: {0}")] | ||
NotFound(crate::names::SRV), | ||
} | ||
|
||
/// A wrapper around a DNS resolver, providing a way to conveniently | ||
/// look up IP addresses of services based on their SRV keys. | ||
pub struct Resolver { | ||
inner: Box<TokioAsyncResolver>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the motivation for boxing this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm using it in subsequent patches in an enum (you can provide an IP from a config, or use a resolver), and Clippy complained that it was too large (500+ bytes). Since it's internal to the struct, it shouldn't matter for users of this API. |
||
} | ||
|
||
impl Resolver { | ||
pub fn new( | ||
address_getter: &impl DnsAddressLookup, | ||
) -> Result<Self, ResolveError> { | ||
let dns_addrs = address_getter.dns_server_addrs(); | ||
Self::new_from_addrs(dns_addrs) | ||
} | ||
|
||
fn new_from_addrs( | ||
dns_addrs: Vec<SocketAddr>, | ||
) -> Result<Self, ResolveError> { | ||
let mut rc = ResolverConfig::new(); | ||
for socket_addr in dns_addrs.into_iter() { | ||
rc.add_name_server(NameServerConfig { | ||
socket_addr, | ||
protocol: Protocol::Udp, | ||
tls_dns_name: None, | ||
trust_nx_responses: false, | ||
bind_addr: None, | ||
}); | ||
} | ||
let inner = | ||
Box::new(TokioAsyncResolver::tokio(rc, ResolverOpts::default())?); | ||
|
||
Ok(Self { inner }) | ||
} | ||
|
||
/// Convenience wrapper for [`Resolver::new`] which determines the subnet | ||
/// based on a provided IP address. | ||
pub fn new_from_ip(address: Ipv6Addr) -> Result<Self, ResolveError> { | ||
let subnet = Ipv6Subnet::<AZ_PREFIX>::new(address); | ||
|
||
Resolver::new(&subnet) | ||
} | ||
|
||
/// Looks up a single [`Ipv6Addr`] based on the SRV name. | ||
/// Returns an error if the record does not exist. | ||
// TODO: There are lots of ways this API can expand: Caching, | ||
// actually respecting TTL, looking up ports, etc. | ||
// | ||
// For now, however, it serves as a very simple "get everyone using DNS" | ||
// API that can be improved upon later. | ||
pub async fn lookup_ipv6( | ||
&self, | ||
srv: crate::names::SRV, | ||
) -> Result<Ipv6Addr, ResolveError> { | ||
let response = self.inner.ipv6_lookup(&srv.to_string()).await?; | ||
let address = response | ||
.iter() | ||
.next() | ||
.ok_or_else(|| ResolveError::NotFound(srv))?; | ||
Ok(*address) | ||
} | ||
|
||
pub async fn lookup_ip( | ||
&self, | ||
srv: crate::names::SRV, | ||
) -> Result<IpAddr, ResolveError> { | ||
let response = self.inner.lookup_ip(&srv.to_string()).await?; | ||
let address = response | ||
.iter() | ||
.next() | ||
.ok_or_else(|| ResolveError::NotFound(srv))?; | ||
Ok(address) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::*; | ||
use crate::names::{BackendName, ServiceName, AAAA, SRV}; | ||
use omicron_test_utils::dev::test_setup_log; | ||
use std::str::FromStr; | ||
use std::sync::Arc; | ||
use tempfile::TempDir; | ||
use uuid::Uuid; | ||
|
||
struct DnsServer { | ||
_storage: TempDir, | ||
dns_server: internal_dns::dns_server::Server, | ||
dropshot_server: | ||
dropshot::HttpServer<Arc<internal_dns::dropshot_server::Context>>, | ||
} | ||
|
||
impl DnsServer { | ||
async fn create(log: &Logger) -> Self { | ||
let storage = | ||
TempDir::new().expect("Failed to create temporary directory"); | ||
|
||
let db = Arc::new(sled::open(&storage.path()).unwrap()); | ||
|
||
let dns_server = { | ||
let db = db.clone(); | ||
let log = log.clone(); | ||
let dns_config = internal_dns::dns_server::Config { | ||
bind_address: "[::1]:0".to_string(), | ||
zone: crate::names::DNS_ZONE.into(), | ||
}; | ||
|
||
internal_dns::dns_server::run(log, db, dns_config) | ||
.await | ||
.unwrap() | ||
}; | ||
|
||
let config = internal_dns::Config { | ||
log: dropshot::ConfigLogging::StderrTerminal { | ||
level: dropshot::ConfigLoggingLevel::Info, | ||
}, | ||
dropshot: dropshot::ConfigDropshot { | ||
bind_address: "[::1]:0".parse().unwrap(), | ||
request_body_max_bytes: 1024, | ||
..Default::default() | ||
}, | ||
data: internal_dns::dns_data::Config { | ||
nmax_messages: 16, | ||
storage_path: storage.path().to_string_lossy().into(), | ||
}, | ||
}; | ||
|
||
let dropshot_server = | ||
internal_dns::start_dropshot_server(config, log.clone(), db) | ||
.await | ||
.unwrap(); | ||
|
||
Self { _storage: storage, dns_server, dropshot_server } | ||
} | ||
|
||
fn dns_server_address(&self) -> SocketAddr { | ||
self.dns_server.address | ||
} | ||
|
||
fn dropshot_server_address(&self) -> SocketAddr { | ||
self.dropshot_server.local_addr() | ||
} | ||
} | ||
|
||
// A test-only way to infer DNS addresses. | ||
// | ||
// Rather than inferring DNS server addresses from the rack subnet, | ||
// they may be explicitly supplied. This results in easier-to-test code. | ||
#[derive(Default)] | ||
struct LocalAddressGetter { | ||
addrs: Vec<(SocketAddr, SocketAddr)>, | ||
} | ||
|
||
impl LocalAddressGetter { | ||
fn add_dns_server( | ||
&mut self, | ||
dns_address: SocketAddr, | ||
server_address: SocketAddr, | ||
) { | ||
self.addrs.push((dns_address, server_address)); | ||
} | ||
} | ||
|
||
impl DnsAddressLookup for LocalAddressGetter { | ||
fn dropshot_server_addrs(&self) -> Vec<SocketAddr> { | ||
self.addrs | ||
.iter() | ||
.map(|(_dns_address, dropshot_address)| *dropshot_address) | ||
.collect() | ||
} | ||
|
||
fn dns_server_addrs(&self) -> Vec<SocketAddr> { | ||
self.addrs | ||
.iter() | ||
.map(|(dns_address, _dropshot_address)| *dns_address) | ||
.collect() | ||
} | ||
} | ||
|
||
// The resolver cannot look up IPs before records have been inserted. | ||
#[tokio::test] | ||
async fn lookup_nonexistent_record_fails() { | ||
let logctx = test_setup_log("lookup_nonexistent_record_fails"); | ||
let dns_server = DnsServer::create(&logctx.log).await; | ||
|
||
let mut address_getter = LocalAddressGetter::default(); | ||
address_getter.add_dns_server( | ||
dns_server.dns_server_address(), | ||
dns_server.dropshot_server_address(), | ||
); | ||
|
||
let resolver = Resolver::new(&address_getter) | ||
.expect("Error creating localhost resolver"); | ||
|
||
let err = resolver | ||
.lookup_ip(SRV::Service(ServiceName::Cockroach)) | ||
.await | ||
.expect_err("Looking up non-existent service should fail"); | ||
|
||
let dns_error = match err { | ||
ResolveError::Resolve(err) => err, | ||
_ => panic!("Unexpected error: {err}"), | ||
}; | ||
assert!( | ||
matches!( | ||
dns_error.kind(), | ||
trust_dns_resolver::error::ResolveErrorKind::NoRecordsFound { .. }, | ||
), | ||
"Saw error: {dns_error}", | ||
); | ||
logctx.cleanup_successful(); | ||
} | ||
|
||
// Insert and retreive a single DNS record. | ||
#[tokio::test] | ||
async fn insert_and_lookup_one_record() { | ||
let logctx = test_setup_log("insert_and_lookup_one_record"); | ||
let dns_server = DnsServer::create(&logctx.log).await; | ||
|
||
let mut address_getter = LocalAddressGetter::default(); | ||
address_getter.add_dns_server( | ||
dns_server.dns_server_address(), | ||
dns_server.dropshot_server_address(), | ||
); | ||
|
||
let resolver = Resolver::new(&address_getter) | ||
.expect("Error creating localhost resolver"); | ||
let updater = Updater::new(&address_getter, logctx.log.clone()); | ||
|
||
let records = HashMap::from([( | ||
SRV::Service(ServiceName::Cockroach), | ||
vec![( | ||
AAAA::Zone(Uuid::new_v4()), | ||
SocketAddrV6::new( | ||
Ipv6Addr::from_str("ff::01").unwrap(), | ||
12345, | ||
0, | ||
0, | ||
), | ||
)], | ||
)]); | ||
updater.insert_dns_records(&records).await.unwrap(); | ||
|
||
let ip = resolver | ||
.lookup_ipv6(SRV::Service(ServiceName::Cockroach)) | ||
.await | ||
.expect("Should have been able to look up IP address"); | ||
assert_eq!( | ||
&ip, | ||
records[&SRV::Service(ServiceName::Cockroach)][0].1.ip() | ||
); | ||
|
||
logctx.cleanup_successful(); | ||
} | ||
|
||
// Insert multiple DNS records of different types. | ||
#[tokio::test] | ||
async fn insert_and_lookup_multiple_records() { | ||
let logctx = test_setup_log("insert_and_lookup_multiple_records"); | ||
let dns_server = DnsServer::create(&logctx.log).await; | ||
|
||
let mut address_getter = LocalAddressGetter::default(); | ||
address_getter.add_dns_server( | ||
dns_server.dns_server_address(), | ||
dns_server.dropshot_server_address(), | ||
); | ||
|
||
let resolver = Resolver::new(&address_getter) | ||
.expect("Error creating localhost resolver"); | ||
let updater = Updater::new(&address_getter, logctx.log.clone()); | ||
|
||
let cockroach_addrs = [ | ||
SocketAddrV6::new( | ||
Ipv6Addr::from_str("ff::01").unwrap(), | ||
1111, | ||
0, | ||
0, | ||
), | ||
SocketAddrV6::new( | ||
Ipv6Addr::from_str("ff::02").unwrap(), | ||
2222, | ||
0, | ||
0, | ||
), | ||
SocketAddrV6::new( | ||
Ipv6Addr::from_str("ff::03").unwrap(), | ||
3333, | ||
0, | ||
0, | ||
), | ||
]; | ||
let clickhouse_addr = SocketAddrV6::new( | ||
Ipv6Addr::from_str("fe::01").unwrap(), | ||
4444, | ||
0, | ||
0, | ||
); | ||
let crucible_addr = SocketAddrV6::new( | ||
Ipv6Addr::from_str("fd::02").unwrap(), | ||
5555, | ||
0, | ||
0, | ||
); | ||
|
||
let srv_crdb = SRV::Service(ServiceName::Cockroach); | ||
let srv_clickhouse = SRV::Service(ServiceName::Clickhouse); | ||
let srv_backend = SRV::Backend(BackendName::Crucible, Uuid::new_v4()); | ||
|
||
let records = HashMap::from([ | ||
// Three Cockroach services | ||
( | ||
srv_crdb.clone(), | ||
vec![ | ||
(AAAA::Zone(Uuid::new_v4()), cockroach_addrs[0]), | ||
(AAAA::Zone(Uuid::new_v4()), cockroach_addrs[1]), | ||
(AAAA::Zone(Uuid::new_v4()), cockroach_addrs[2]), | ||
], | ||
), | ||
// One Clickhouse service | ||
( | ||
srv_clickhouse.clone(), | ||
vec![(AAAA::Zone(Uuid::new_v4()), clickhouse_addr)], | ||
), | ||
// One Backend service | ||
( | ||
srv_backend.clone(), | ||
vec![(AAAA::Zone(Uuid::new_v4()), crucible_addr)], | ||
), | ||
]); | ||
updater.insert_dns_records(&records).await.unwrap(); | ||
|
||
// Look up Cockroach | ||
let ip = resolver | ||
.lookup_ipv6(SRV::Service(ServiceName::Cockroach)) | ||
.await | ||
.expect("Should have been able to look up IP address"); | ||
assert!(cockroach_addrs.iter().any(|addr| addr.ip() == &ip)); | ||
|
||
// Look up Clickhouse | ||
let ip = resolver | ||
.lookup_ipv6(SRV::Service(ServiceName::Clickhouse)) | ||
.await | ||
.expect("Should have been able to look up IP address"); | ||
assert_eq!(&ip, clickhouse_addr.ip()); | ||
|
||
// Look up Backend Service | ||
let ip = resolver | ||
.lookup_ipv6(srv_backend) | ||
.await | ||
.expect("Should have been able to look up IP address"); | ||
assert_eq!(&ip, crucible_addr.ip()); | ||
|
||
// If we remove the AAAA records for two of the CRDB services, | ||
// only one will remain. | ||
updater | ||
.dns_records_delete(&vec![ | ||
DnsRecordKey { name: records[&srv_crdb][0].0.to_string() }, | ||
DnsRecordKey { name: records[&srv_crdb][1].0.to_string() }, | ||
]) | ||
.await | ||
.expect("Should have been able to delete record"); | ||
let ip = resolver | ||
.lookup_ipv6(SRV::Service(ServiceName::Cockroach)) | ||
.await | ||
.expect("Should have been able to look up IP address"); | ||
assert_eq!(&ip, cockroach_addrs[2].ip()); | ||
|
||
logctx.cleanup_successful(); | ||
} | ||
|
||
#[tokio::test] | ||
async fn update_record() { | ||
let logctx = test_setup_log("update_record"); | ||
let dns_server = DnsServer::create(&logctx.log).await; | ||
|
||
let mut address_getter = LocalAddressGetter::default(); | ||
address_getter.add_dns_server( | ||
dns_server.dns_server_address(), | ||
dns_server.dropshot_server_address(), | ||
); | ||
|
||
let resolver = Resolver::new(&address_getter) | ||
.expect("Error creating localhost resolver"); | ||
let updater = Updater::new(&address_getter, logctx.log.clone()); | ||
|
||
// Insert a record, observe that it exists. | ||
let srv_crdb = SRV::Service(ServiceName::Cockroach); | ||
let mut records = HashMap::from([( | ||
srv_crdb.clone(), | ||
vec![( | ||
AAAA::Zone(Uuid::new_v4()), | ||
SocketAddrV6::new( | ||
Ipv6Addr::from_str("ff::01").unwrap(), | ||
12345, | ||
0, | ||
0, | ||
), | ||
)], | ||
)]); | ||
updater.insert_dns_records(&records).await.unwrap(); | ||
let ip = resolver | ||
.lookup_ipv6(SRV::Service(ServiceName::Cockroach)) | ||
.await | ||
.expect("Should have been able to look up IP address"); | ||
assert_eq!(&ip, records[&srv_crdb][0].1.ip()); | ||
|
||
// If we insert the same record with a new address, it should be | ||
// updated. | ||
records.get_mut(&srv_crdb).unwrap()[0].1 = SocketAddrV6::new( | ||
Ipv6Addr::from_str("ee::02").unwrap(), | ||
54321, | ||
0, | ||
0, | ||
); | ||
updater.insert_dns_records(&records).await.unwrap(); | ||
let ip = resolver | ||
.lookup_ipv6(SRV::Service(ServiceName::Cockroach)) | ||
.await | ||
.expect("Should have been able to look up IP address"); | ||
assert_eq!(&ip, records[&srv_crdb][0].1.ip()); | ||
|
||
logctx.cleanup_successful(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
// This Source Code Form is subject to the terms of the Mozilla Public | ||
// License, v. 2.0. If a copy of the MPL was not distributed with this | ||
// file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
|
||
//! Naming scheme for Internal DNS names (RFD 248). | ||
use std::fmt; | ||
use uuid::Uuid; | ||
|
||
pub(crate) const DNS_ZONE: &str = "control-plane.oxide.internal"; | ||
|
||
/// Names for services where backends are interchangeable. | ||
#[derive(Clone, Debug, Hash, Eq, PartialEq, PartialOrd)] | ||
pub enum ServiceName { | ||
Clickhouse, | ||
Cockroach, | ||
InternalDNS, | ||
Nexus, | ||
Oximeter, | ||
} | ||
|
||
impl fmt::Display for ServiceName { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
match &self { | ||
ServiceName::Clickhouse => write!(f, "clickhouse"), | ||
ServiceName::Cockroach => write!(f, "cockroach"), | ||
ServiceName::InternalDNS => write!(f, "internalDNS"), | ||
ServiceName::Nexus => write!(f, "nexus"), | ||
ServiceName::Oximeter => write!(f, "oximeter"), | ||
} | ||
} | ||
} | ||
|
||
/// Names for services where backends are not interchangeable. | ||
#[derive(Clone, Debug, Hash, Eq, PartialEq, PartialOrd)] | ||
pub enum BackendName { | ||
Crucible, | ||
SledAgent, | ||
} | ||
|
||
impl fmt::Display for BackendName { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
match &self { | ||
BackendName::Crucible => write!(f, "crucible"), | ||
BackendName::SledAgent => write!(f, "sledagent"), | ||
} | ||
} | ||
} | ||
|
||
#[derive(Clone, Debug, Hash, Eq, PartialEq, PartialOrd)] | ||
pub enum SRV { | ||
/// A service identified and accessed by name, such as "nexus", "CRDB", etc. | ||
/// | ||
/// This is used in cases where services are interchangeable. | ||
Service(ServiceName), | ||
|
||
/// A service identified by name and a unique identifier. | ||
/// | ||
/// This is used in cases where services are not interchangeable, such as | ||
/// for the Sled agent. | ||
Backend(BackendName, Uuid), | ||
} | ||
|
||
impl fmt::Display for SRV { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
match &self { | ||
SRV::Service(name) => { | ||
write!(f, "_{}._tcp.{}", name, DNS_ZONE) | ||
} | ||
SRV::Backend(name, id) => { | ||
write!(f, "_{}._tcp.{}.{}", name, id, DNS_ZONE) | ||
} | ||
} | ||
} | ||
} | ||
|
||
#[derive(Clone, Debug, PartialEq, PartialOrd)] | ||
pub enum AAAA { | ||
/// Identifies an AAAA record for a sled. | ||
Sled(Uuid), | ||
|
||
/// Identifies an AAAA record for a zone within a sled. | ||
Zone(Uuid), | ||
} | ||
|
||
impl fmt::Display for AAAA { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
match &self { | ||
AAAA::Sled(id) => { | ||
write!(f, "{}.sled.{}", id, DNS_ZONE) | ||
} | ||
AAAA::Zone(id) => { | ||
write!(f, "{}.host.{}", id, DNS_ZONE) | ||
} | ||
} | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::*; | ||
|
||
#[test] | ||
fn display_srv_service() { | ||
assert_eq!( | ||
SRV::Service(ServiceName::Clickhouse).to_string(), | ||
"_clickhouse._tcp.control-plane.oxide.internal", | ||
); | ||
assert_eq!( | ||
SRV::Service(ServiceName::Cockroach).to_string(), | ||
"_cockroach._tcp.control-plane.oxide.internal", | ||
); | ||
assert_eq!( | ||
SRV::Service(ServiceName::InternalDNS).to_string(), | ||
"_internalDNS._tcp.control-plane.oxide.internal", | ||
); | ||
assert_eq!( | ||
SRV::Service(ServiceName::Nexus).to_string(), | ||
"_nexus._tcp.control-plane.oxide.internal", | ||
); | ||
assert_eq!( | ||
SRV::Service(ServiceName::Oximeter).to_string(), | ||
"_oximeter._tcp.control-plane.oxide.internal", | ||
); | ||
} | ||
|
||
#[test] | ||
fn display_srv_backend() { | ||
let uuid = Uuid::nil(); | ||
assert_eq!( | ||
SRV::Backend(BackendName::Crucible, uuid).to_string(), | ||
"_crucible._tcp.00000000-0000-0000-0000-000000000000.control-plane.oxide.internal", | ||
); | ||
assert_eq!( | ||
SRV::Backend(BackendName::SledAgent, uuid).to_string(), | ||
"_sledagent._tcp.00000000-0000-0000-0000-000000000000.control-plane.oxide.internal", | ||
); | ||
} | ||
|
||
#[test] | ||
fn display_aaaa() { | ||
let uuid = Uuid::nil(); | ||
assert_eq!( | ||
AAAA::Sled(uuid).to_string(), | ||
"00000000-0000-0000-0000-000000000000.sled.control-plane.oxide.internal", | ||
); | ||
assert_eq!( | ||
AAAA::Zone(uuid).to_string(), | ||
"00000000-0000-0000-0000-000000000000.host.control-plane.oxide.internal", | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's pretty likely we'll want to change this behavior to cope with one or more of the DNS servers failing.
However, I really want our behavior to be "fail loud" until we have a good way of coping. If one of the DNS servers starts failing, ideally Nexus could:
Until then though, if an internal DNS server fails, I want high-visibility on the error. Hopefully this tradeoff makes sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also of note: This is exclusively on the "setting DNS records" side of things. The "getting DNS records" side should be able to cope with a server going offline already.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I generally agree about failing loud, but it does seem slightly at odds with the availability-first ethos of DNS. This is probably good for now though, without more information about how the client-side will work.