Skip to main content

max / makenotwork

3.0 KB · 89 lines History Blame Raw
1 //! DNS record check storage, including pruning of records no longer configured.
2
3 use super::{DnsCheckResult, Result, SqlitePool};
4 use tracing::instrument;
5
6 #[derive(Debug, Clone, sqlx::FromRow, serde::Serialize)]
7 pub struct DnsCheckRow {
8 pub id: i64,
9 pub target: String,
10 pub name: String,
11 pub record_type: String,
12 pub expected: String,
13 pub actual: String,
14 pub matches: bool,
15 pub checked_at: String,
16 pub error: Option<String>,
17 }
18
19 #[instrument(skip_all)]
20 pub async fn insert_dns_check(pool: &SqlitePool, result: &DnsCheckResult) -> Result<i64> {
21 let expected = serde_json::to_string(&result.expected).unwrap_or_default();
22 let actual = serde_json::to_string(&result.actual).unwrap_or_default();
23 let record_type_str = result.record_type.to_string();
24
25 let row = sqlx::query(
26 "INSERT INTO dns_checks (target, name, record_type, expected, actual, matches, checked_at, error)
27 VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
28 )
29 .bind(&result.target)
30 .bind(&result.name)
31 .bind(&record_type_str)
32 .bind(&expected)
33 .bind(&actual)
34 .bind(result.matches)
35 .bind(&result.checked_at)
36 .bind(&result.error)
37 .execute(pool)
38 .await?;
39 Ok(row.last_insert_rowid())
40 }
41
42 /// Get the latest DNS check per (name, record_type) for a target.
43 #[instrument(skip_all)]
44 pub async fn get_latest_dns_checks(pool: &SqlitePool, target: &str) -> Result<Vec<DnsCheckRow>> {
45 Ok(sqlx::query_as::<_, DnsCheckRow>(
46 "SELECT d.id, d.target, d.name, d.record_type, d.expected, d.actual, d.matches, d.checked_at, d.error
47 FROM dns_checks d
48 INNER JOIN (SELECT name, record_type, MAX(id) as max_id FROM dns_checks WHERE target = ? GROUP BY name, record_type) latest
49 ON d.id = latest.max_id
50 ORDER BY d.name, d.record_type",
51 )
52 .bind(target)
53 .fetch_all(pool)
54 .await?)
55 }
56
57 /// Delete dns_checks for (name, record_type) pairs no longer in the config.
58 #[instrument(skip_all)]
59 pub async fn prune_stale_dns(
60 pool: &SqlitePool,
61 target: &str,
62 expected_dns: &[(String, String)],
63 ) -> Result<u64> {
64 if expected_dns.is_empty() {
65 // No configured DNS records, delete all DNS checks for this target
66 let result = sqlx::query("DELETE FROM dns_checks WHERE target = ?")
67 .bind(target)
68 .execute(pool)
69 .await?;
70 return Ok(result.rows_affected());
71 }
72
73 // Build a compound condition: keep rows matching any configured (name, record_type) pair
74 let conditions: Vec<String> = expected_dns
75 .iter()
76 .map(|_| "(name = ? AND record_type = ?)".to_string())
77 .collect();
78 let sql = format!(
79 "DELETE FROM dns_checks WHERE target = ? AND NOT ({})",
80 conditions.join(" OR ")
81 );
82 let mut query = sqlx::query(sqlx::AssertSqlSafe(sql)).bind(target);
83 for (name, record_type) in expected_dns {
84 query = query.bind(name).bind(record_type);
85 }
86 let result = query.execute(pool).await?;
87 Ok(result.rows_affected())
88 }
89