Change label format to be a single dns path
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Alex 2022-12-11 16:29:06 +01:00
parent 0d37299b24
commit b26d4d7bba
Signed by: lx
GPG key ID: 0E496D15096376BE
5 changed files with 108 additions and 43 deletions

View file

@ -3,6 +3,6 @@
RUST_LOG=d53=info cargo run \ RUST_LOG=d53=info cargo run \
-- \ -- \
--consul-addr http://localhost:8500 \ --consul-addr http://localhost:8500 \
--provider gandi \ --providers deuxfleurs.org:gandi \
--gandi-api-key $GANDI_API_KEY \ --gandi-api-key $GANDI_API_KEY \
--allowed-domains staging.deuxfleurs.org --allowed-domains staging.deuxfleurs.org

View file

@ -26,8 +26,7 @@ pub struct DnsConfig {
#[derive(Debug, Hash, PartialEq, Eq)] #[derive(Debug, Hash, PartialEq, Eq)]
pub struct DnsEntryKey { pub struct DnsEntryKey {
pub domain: String, pub dns_path: String,
pub subdomain: String,
pub record_type: DnsRecordType, pub record_type: DnsRecordType,
} }
@ -62,7 +61,7 @@ impl DnsConfig {
fn parse_d53_tag(tag: &str, node: &ConsulNode) -> Option<(DnsEntryKey, DnsEntryValue)> { fn parse_d53_tag(tag: &str, node: &ConsulNode) -> Option<(DnsEntryKey, DnsEntryValue)> {
let splits = tag.split(' ').collect::<Vec<_>>(); let splits = tag.split(' ').collect::<Vec<_>>();
if splits.len() != 3 { if splits.len() != 2 {
return None; return None;
} }
@ -96,8 +95,7 @@ fn parse_d53_tag(tag: &str, node: &ConsulNode) -> Option<(DnsEntryKey, DnsEntryV
Some(( Some((
DnsEntryKey { DnsEntryKey {
domain: splits[1].to_string(), dns_path: splits[1].to_string(),
subdomain: splits[2].to_string(),
record_type, record_type,
}, },
DnsEntryValue { targets }, DnsEntryValue { targets },
@ -259,11 +257,7 @@ impl std::fmt::Display for DnsRecordType {
impl std::fmt::Display for DnsEntryKey { impl std::fmt::Display for DnsEntryKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!( write!(f, "{} IN {}", self.dns_path, self.record_type)
f,
"{}.{} IN {}",
self.subdomain, self.domain, self.record_type
)
} }
} }

View file

@ -8,16 +8,28 @@ use tokio::sync::watch;
use tracing::*; use tracing::*;
use crate::dns_config::*; use crate::dns_config::*;
use crate::provider::DnsProvider; use crate::DomainProvider;
pub async fn dns_updater_task( pub async fn dns_updater_task(
mut rx_dns_config: watch::Receiver<Arc<DnsConfig>>, mut rx_dns_config: watch::Receiver<Arc<DnsConfig>>,
provider: Box<dyn DnsProvider>, providers: Vec<DomainProvider>,
allowed_domains: Vec<String>, allowed_domains: Vec<String>,
mut must_exit: watch::Receiver<bool>, mut must_exit: watch::Receiver<bool>,
) { ) {
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"); info!("DNS updater will start in 5 seconds");
tokio::time::sleep(Duration::from_secs(5)).await; tokio::time::sleep(Duration::from_secs(5)).await;
info!("DNS updater starting"); info!("DNS updater starting");
let mut config = Arc::new(DnsConfig::new()); let mut config = Arc::new(DnsConfig::new());
@ -32,21 +44,37 @@ pub async fn dns_updater_task(
); );
let new_config: Arc<DnsConfig> = rx_dns_config.borrow().clone(); let new_config: Arc<DnsConfig> = rx_dns_config.borrow().clone();
for (k, v) in new_config.entries.iter() { for (key, value) in new_config.entries.iter() {
if config.entries.get(k) != Some(v) { // Skip entries that haven't changed
let fulldomain = format!("{}.{}", k.subdomain, k.domain); if config.entries.get(key) == Some(value) {
if !allowed_domains.iter().any(|d| fulldomain.ends_with(d)) { continue;
error!( }
"Got an entry for domain {} which is not in allowed list",
k.domain
);
continue;
}
info!("Updating {} {}", k, v); // Skip entries for unallowed domains
if let Err(e) = update_dns_entry(k, v, provider.as_ref()).await { if !allowed_domains.iter().any(|d| key.dns_path.ends_with(d)) {
error!("Unable to update entry {} {}: {}", k, v, e); 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( async fn update_dns_entry(
key: &DnsEntryKey, key: &DnsEntryKey,
value: &DnsEntryValue, value: &DnsEntryValue,
provider: &dyn DnsProvider, provider: &DomainProvider,
) -> Result<()> { ) -> 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() { if value.targets.is_empty() {
bail!("zero targets (internal error)"); bail!("zero targets (internal error)");
} }
@ -73,7 +115,8 @@ async fn update_dns_entry(
); );
} }
provider provider
.update_a(&key.domain, &key.subdomain, &targets) .provider
.update_a(&provider.domain, &subdomain, &targets)
.await?; .await?;
} }
DnsRecordType::AAAA => { DnsRecordType::AAAA => {
@ -85,17 +128,24 @@ async fn update_dns_entry(
); );
} }
provider provider
.update_aaaa(&key.domain, &key.subdomain, &targets) .provider
.update_aaaa(&provider.domain, &subdomain, &targets)
.await?; .await?;
} }
DnsRecordType::CNAME => { DnsRecordType::CNAME => {
let mut targets = value.targets.iter().cloned().collect::<Vec<_>>(); let mut targets = value.targets.iter().cloned().collect::<Vec<_>>();
if targets.len() > 1 { if targets.len() > 1 {
targets.sort(); 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 provider
.update_cname(&key.domain, &key.subdomain, &targets[0]) .provider
.update_cname(&provider.domain, &subdomain, &targets[0])
.await?; .await?;
} }
} }

View file

@ -37,8 +37,8 @@ pub struct Opt {
pub consul_client_key: Option<String>, pub consul_client_key: Option<String>,
/// DNS provider /// DNS provider
#[structopt(long = "provider", env = "D53_PROVIDER")] #[structopt(long = "providers", env = "D53_PROVIDERS")]
pub provider: String, pub providers: String,
/// Allowed domains /// Allowed domains
#[structopt(long = "allowed-domains", env = "D53_ALLOWED_DOMAINS")] #[structopt(long = "allowed-domains", env = "D53_ALLOWED_DOMAINS")]
@ -49,6 +49,11 @@ pub struct Opt {
pub gandi_api_key: Option<String>, pub gandi_api_key: Option<String>,
} }
pub struct DomainProvider {
pub domain: String,
pub provider: Box<dyn provider::DnsProvider>,
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
if std::env::var("RUST_LOG").is_err() { 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 consul = df_consul::Consul::new(consul_config, "").expect("Cannot build Consul");
let provider: Box<dyn provider::DnsProvider> = match opt.provider.as_str() { let mut domain_providers = vec![];
"gandi" => Box::new( for pstr in opt.providers.as_str().split(',') {
provider::gandi::GandiProvider::new(&opt).expect("Cannot initialize Gandi provier"), let (domain, provider) = pstr.split_once(':')
), .expect("Invalid provider syntax, expected: <domain_name>:<provider>[,<domain_name>:<provider>[,...]]");
p => panic!("Unsupported DNS provider: {}", p), let provider: Box<dyn provider::DnsProvider> = 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 let allowed_domains = opt
.allowed_domains .allowed_domains
@ -98,7 +115,7 @@ async fn main() {
let updater_task = tokio::spawn(dns_updater::dns_updater_task( let updater_task = tokio::spawn(dns_updater::dns_updater_task(
rx_dns_config.clone(), rx_dns_config.clone(),
provider, domain_providers,
allowed_domains, allowed_domains,
exit_signal.clone(), exit_signal.clone(),
)); ));

View file

@ -4,7 +4,7 @@ use anyhow::{anyhow, Result};
use async_trait::async_trait; use async_trait::async_trait;
use reqwest::header; use reqwest::header;
use serde::Serialize; use serde::Serialize;
use tracing::{info, warn}; use tracing::*;
use crate::provider::DnsProvider; use crate::provider::DnsProvider;
use crate::Opt; use crate::Opt;
@ -34,11 +34,15 @@ impl GandiProvider {
} }
async fn put_rrset(&self, url: &str, rrset: &GandiRrset) -> Result<()> { 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?; let http = self.client.put(url).json(rrset).send().await?;
if !http.status().is_success() { 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()?; http.error_for_status()?;