Change label format to be a single dns path
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
0d37299b24
commit
b26d4d7bba
5 changed files with 108 additions and 43 deletions
|
@ -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
|
||||
|
|
|
@ -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::<Vec<_>>();
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Arc<DnsConfig>>,
|
||||
provider: Box<dyn DnsProvider>,
|
||||
providers: Vec<DomainProvider>,
|
||||
allowed_domains: Vec<String>,
|
||||
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");
|
||||
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<DnsConfig> = 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::<Vec<_>>();
|
||||
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?;
|
||||
}
|
||||
}
|
||||
|
|
35
src/main.rs
35
src/main.rs
|
@ -37,8 +37,8 @@ pub struct Opt {
|
|||
pub consul_client_key: Option<String>,
|
||||
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
pub struct DomainProvider {
|
||||
pub domain: String,
|
||||
pub provider: Box<dyn provider::DnsProvider>,
|
||||
}
|
||||
|
||||
#[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<dyn provider::DnsProvider> = 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: <domain_name>:<provider>[,<domain_name>:<provider>[,...]]");
|
||||
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
|
||||
.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(),
|
||||
));
|
||||
|
|
|
@ -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()?;
|
||||
|
|
Loading…
Reference in a new issue