From b26d4d7bbad7c77f466a00f0bd21e7e4ea09c3b1 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Sun, 11 Dec 2022 16:29:06 +0100 Subject: [PATCH] Change label format to be a single dns path --- run_local.sh | 2 +- src/dns_config.rs | 14 ++----- src/dns_updater.rs | 90 +++++++++++++++++++++++++++++++++---------- src/main.rs | 35 ++++++++++++----- src/provider/gandi.rs | 10 +++-- 5 files changed, 108 insertions(+), 43 deletions(-) diff --git a/run_local.sh b/run_local.sh index 0a110f1..150709a 100755 --- a/run_local.sh +++ b/run_local.sh @@ -3,6 +3,6 @@ RUST_LOG=d53=info cargo run \ -- \ --consul-addr http://localhost:8500 \ - --provider gandi \ + --providers deuxfleurs.org:gandi \ --gandi-api-key $GANDI_API_KEY \ --allowed-domains staging.deuxfleurs.org diff --git a/src/dns_config.rs b/src/dns_config.rs index 25cc5f0..37a49fa 100644 --- a/src/dns_config.rs +++ b/src/dns_config.rs @@ -26,8 +26,7 @@ pub struct DnsConfig { #[derive(Debug, Hash, PartialEq, Eq)] pub struct DnsEntryKey { - pub domain: String, - pub subdomain: String, + pub dns_path: String, pub record_type: DnsRecordType, } @@ -62,7 +61,7 @@ impl DnsConfig { fn parse_d53_tag(tag: &str, node: &ConsulNode) -> Option<(DnsEntryKey, DnsEntryValue)> { let splits = tag.split(' ').collect::>(); - if splits.len() != 3 { + if splits.len() != 2 { return None; } @@ -96,8 +95,7 @@ fn parse_d53_tag(tag: &str, node: &ConsulNode) -> Option<(DnsEntryKey, DnsEntryV Some(( DnsEntryKey { - domain: splits[1].to_string(), - subdomain: splits[2].to_string(), + dns_path: splits[1].to_string(), record_type, }, DnsEntryValue { targets }, @@ -259,11 +257,7 @@ impl std::fmt::Display for DnsRecordType { impl std::fmt::Display for DnsEntryKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}.{} IN {}", - self.subdomain, self.domain, self.record_type - ) + write!(f, "{} IN {}", self.dns_path, self.record_type) } } diff --git a/src/dns_updater.rs b/src/dns_updater.rs index d781671..e65689d 100644 --- a/src/dns_updater.rs +++ b/src/dns_updater.rs @@ -8,16 +8,28 @@ use tokio::sync::watch; use tracing::*; use crate::dns_config::*; -use crate::provider::DnsProvider; +use crate::DomainProvider; pub async fn dns_updater_task( mut rx_dns_config: watch::Receiver>, - provider: Box, + providers: Vec, allowed_domains: Vec, mut must_exit: watch::Receiver, ) { + for dom in allowed_domains.iter() { + info!(domain = dom, "allowing subdomains of domain"); + } + for prov in providers.iter() { + info!( + domain = prov.domain, + provider = prov.provider.provider(), + "got provider for domain" + ); + } + info!("DNS updater will start in 5 seconds"); tokio::time::sleep(Duration::from_secs(5)).await; + info!("DNS updater starting"); let mut config = Arc::new(DnsConfig::new()); @@ -32,21 +44,37 @@ pub async fn dns_updater_task( ); let new_config: Arc = rx_dns_config.borrow().clone(); - for (k, v) in new_config.entries.iter() { - if config.entries.get(k) != Some(v) { - let fulldomain = format!("{}.{}", k.subdomain, k.domain); - if !allowed_domains.iter().any(|d| fulldomain.ends_with(d)) { - error!( - "Got an entry for domain {} which is not in allowed list", - k.domain - ); - continue; - } + for (key, value) in new_config.entries.iter() { + // Skip entries that haven't changed + if config.entries.get(key) == Some(value) { + continue; + } - info!("Updating {} {}", k, v); - if let Err(e) = update_dns_entry(k, v, provider.as_ref()).await { - error!("Unable to update entry {} {}: {}", k, v, e); + // Skip entries for unallowed domains + if !allowed_domains.iter().any(|d| key.dns_path.ends_with(d)) { + error!( + domain = key.dns_path, + "domain/subdomain/hostname not in allowed list", + ); + continue; + } + + let provider = providers.iter().find(|p| key.dns_path.ends_with(&p.domain)); + + if let Some(provider) = provider { + if let Err(e) = update_dns_entry(key, value, provider).await { + error!( + record = key.to_string(), + target = value.to_string(), + error = e.to_string(), + "unable to update record" + ); } + } else { + error!( + domain = key.dns_path, + "no provider matches this domain/subdomain/hostname" + ); } } @@ -57,8 +85,22 @@ pub async fn dns_updater_task( async fn update_dns_entry( key: &DnsEntryKey, value: &DnsEntryValue, - provider: &dyn DnsProvider, + provider: &DomainProvider, ) -> Result<()> { + let subdomain = key + .dns_path + .strip_suffix(&provider.domain) + .unwrap() + .trim_end_matches('.'); + info!( + record = key.to_string(), + target = value.to_string(), + domain = provider.domain, + subdomain = &subdomain, + provider = provider.provider.provider(), + "updating record" + ); + if value.targets.is_empty() { bail!("zero targets (internal error)"); } @@ -73,7 +115,8 @@ async fn update_dns_entry( ); } provider - .update_a(&key.domain, &key.subdomain, &targets) + .provider + .update_a(&provider.domain, &subdomain, &targets) .await?; } DnsRecordType::AAAA => { @@ -85,17 +128,24 @@ async fn update_dns_entry( ); } provider - .update_aaaa(&key.domain, &key.subdomain, &targets) + .provider + .update_aaaa(&provider.domain, &subdomain, &targets) .await?; } DnsRecordType::CNAME => { let mut targets = value.targets.iter().cloned().collect::>(); if targets.len() > 1 { targets.sort(); - warn!("Several CNAME targets for {}: {:?}. Taking first one in alphabetical order. Consider switching to a single global target instead.", key, targets); + warn!( + record = key.to_string(), + all_targets = value.to_string(), + selected_target = targets[0], + "Several CNAME targets, taking first one in alphabetical order. Consider switching to a single global target instead." + ); } provider - .update_cname(&key.domain, &key.subdomain, &targets[0]) + .provider + .update_cname(&provider.domain, &subdomain, &targets[0]) .await?; } } diff --git a/src/main.rs b/src/main.rs index 667c058..77c8b16 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,8 +37,8 @@ pub struct Opt { pub consul_client_key: Option, /// DNS provider - #[structopt(long = "provider", env = "D53_PROVIDER")] - pub provider: String, + #[structopt(long = "providers", env = "D53_PROVIDERS")] + pub providers: String, /// Allowed domains #[structopt(long = "allowed-domains", env = "D53_ALLOWED_DOMAINS")] @@ -49,6 +49,11 @@ pub struct Opt { pub gandi_api_key: Option, } +pub struct DomainProvider { + pub domain: String, + pub provider: Box, +} + #[tokio::main] async fn main() { if std::env::var("RUST_LOG").is_err() { @@ -81,12 +86,24 @@ async fn main() { let consul = df_consul::Consul::new(consul_config, "").expect("Cannot build Consul"); - let provider: Box = match opt.provider.as_str() { - "gandi" => Box::new( - provider::gandi::GandiProvider::new(&opt).expect("Cannot initialize Gandi provier"), - ), - p => panic!("Unsupported DNS provider: {}", p), - }; + let mut domain_providers = vec![]; + for pstr in opt.providers.as_str().split(',') { + let (domain, provider) = pstr.split_once(':') + .expect("Invalid provider syntax, expected: :[,:[,...]]"); + let provider: Box = match provider { + "gandi" => Box::new( + provider::gandi::GandiProvider::new(&opt).expect("Cannot initialize Gandi provier"), + ), + p => panic!("Unsupported DNS provider: {}", p), + }; + domain_providers.push(DomainProvider { + domain: domain.to_string(), + provider, + }); + } + if domain_providers.is_empty() { + panic!("No domain providers were specified."); + } let allowed_domains = opt .allowed_domains @@ -98,7 +115,7 @@ async fn main() { let updater_task = tokio::spawn(dns_updater::dns_updater_task( rx_dns_config.clone(), - provider, + domain_providers, allowed_domains, exit_signal.clone(), )); diff --git a/src/provider/gandi.rs b/src/provider/gandi.rs index 85bf570..eeff641 100644 --- a/src/provider/gandi.rs +++ b/src/provider/gandi.rs @@ -4,7 +4,7 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use reqwest::header; use serde::Serialize; -use tracing::{info, warn}; +use tracing::*; use crate::provider::DnsProvider; use crate::Opt; @@ -34,11 +34,15 @@ impl GandiProvider { } async fn put_rrset(&self, url: &str, rrset: &GandiRrset) -> Result<()> { - info!("PUT {} with {:?}", url, rrset); + debug!(url = url, body = format!("{:?}", rrset), "PUT"); let http = self.client.put(url).json(rrset).send().await?; if !http.status().is_success() { - warn!("PUT {} returned {}", url, http.status()); + warn!( + url = url, + http_status = http.status().to_string(), + "PUT returned error" + ); } http.error_for_status()?;