use serde::{Deserialize, Serialize};
pub fn dnsmanaged_key(project: &str, site: &str, host: &str) -> String {
format!(
"project/{project}/dnsmanaged/{site}/{}",
crate::domain_verify::normalize_host(host)
)
}
pub fn dnsmanaged_site_prefix(project: &str, site: &str) -> String {
format!("project/{project}/dnsmanaged/{site}/")
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ManagedRecord {
pub kind: String,
pub name: String,
pub value: String,
pub ttl: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ManagedDns {
pub version: u32,
pub host: String,
pub provider: String,
pub records: Vec<ManagedRecord>,
pub updated_at_unix: u64,
}
impl Default for ManagedDns {
fn default() -> Self {
Self {
version: crate::SCHEMA_VERSION,
host: String::new(),
provider: String::new(),
records: Vec::new(),
updated_at_unix: 0,
}
}
}
impl ManagedDns {
pub fn new(host: &str, provider: &str, records: Vec<ManagedRecord>, now_unix: u64) -> Self {
Self {
version: crate::SCHEMA_VERSION,
host: crate::domain_verify::normalize_host(host),
provider: provider.to_string(),
records,
updated_at_unix: now_unix,
}
}
pub fn from_json(bytes: &[u8]) -> Result<Self, crate::error::ConfigError> {
serde_json::from_slice(bytes)
.map_err(|err| crate::error::ConfigError::parse(err.to_string()))
}
pub fn to_json(&self) -> Result<Vec<u8>, crate::error::ConfigError> {
serde_json::to_vec(self).map_err(|err| crate::error::ConfigError::parse(err.to_string()))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ReconcilePlan {
pub to_point: Vec<String>,
pub to_retract: Vec<String>,
}
pub fn plan_reconcile(verified: &[String], managed: &[String]) -> ReconcilePlan {
use std::collections::BTreeSet;
let verified: BTreeSet<String> = verified
.iter()
.map(|h| crate::domain_verify::normalize_host(h))
.collect();
let managed: BTreeSet<String> = managed
.iter()
.map(|h| crate::domain_verify::normalize_host(h))
.collect();
ReconcilePlan {
to_point: verified.iter().cloned().collect(),
to_retract: managed.difference(&verified).cloned().collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_normalizes_the_host() {
assert_eq!(
dnsmanaged_key("default", "blog", "*.WWW.Example.com."),
"project/default/dnsmanaged/blog/www.example.com"
);
assert_eq!(
dnsmanaged_site_prefix("default", "blog"),
"project/default/dnsmanaged/blog/"
);
}
#[test]
fn json_roundtrip() {
let ledger = ManagedDns::new(
"www.example.com",
"cloudflare",
vec![ManagedRecord {
kind: "A".into(),
name: "www.example.com".into(),
value: "203.0.113.7".into(),
ttl: 300,
}],
42,
);
let bytes = ledger.to_json().unwrap();
assert_eq!(ManagedDns::from_json(&bytes).unwrap(), ledger);
}
#[test]
fn new_normalizes_host_and_pins_version() {
let ledger = ManagedDns::new("*.Example.com", "route53", Vec::new(), 1);
assert_eq!(ledger.host, "example.com");
assert_eq!(ledger.version, crate::SCHEMA_VERSION);
assert_eq!(ledger.provider, "route53");
}
#[test]
fn plan_points_verified_and_retracts_orphans() {
let plan = plan_reconcile(
&["a.example.com".into(), "b.example.com".into()],
&["b.example.com".into(), "old.example.com".into()],
);
assert_eq!(plan.to_point, vec!["a.example.com", "b.example.com"]);
assert_eq!(plan.to_retract, vec!["old.example.com"]);
}
#[test]
fn plan_normalizes_before_comparing() {
let plan = plan_reconcile(&["*.Example.com".into()], &["example.com".into()]);
assert_eq!(plan.to_point, vec!["example.com"]);
assert!(plan.to_retract.is_empty());
}
}