max / pom
7 files changed,
+118 insertions,
-12 deletions
| @@ -17,17 +17,17 @@ expected_routes = ["/", "/discover", "/login", "/docs"] | |||
| 17 | 17 | [[targets.mnw.dns]] | |
| 18 | 18 | name = "makenot.work" | |
| 19 | 19 | record_type = "A" | |
| 20 | - | expected = ["5.78.144.244"] | |
| 20 | + | expected = [] | |
| 21 | 21 | ||
| 22 | 22 | [[targets.mnw.dns]] | |
| 23 | 23 | name = "forums.makenot.work" | |
| 24 | 24 | record_type = "A" | |
| 25 | - | expected = ["5.78.144.244"] | |
| 25 | + | expected = [] | |
| 26 | 26 | ||
| 27 | 27 | [[targets.mnw.dns]] | |
| 28 | 28 | name = "git.makenot.work" | |
| 29 | 29 | record_type = "A" | |
| 30 | - | expected = ["5.78.144.244"] | |
| 30 | + | expected = [] | |
| 31 | 31 | ||
| 32 | 32 | [targets.mnw.whois] | |
| 33 | 33 | domain = "makenot.work" | |
| @@ -81,7 +81,7 @@ label = "htpy.app" | |||
| 81 | 81 | [[targets.htpy.dns]] | |
| 82 | 82 | name = "htpy.app" | |
| 83 | 83 | record_type = "A" | |
| 84 | - | expected = ["5.78.135.189"] | |
| 84 | + | expected = [] | |
| 85 | 85 | ||
| 86 | 86 | [targets.htpy.whois] | |
| 87 | 87 | domain = "htpy.app" |
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | [serve] | |
| 2 | 2 | interval_secs = 300 | |
| 3 | 3 | prune_days = 30 | |
| 4 | - | listen = "100.120.174.96:9100" | |
| 4 | + | listen = "0.0.0.0:9100" | |
| 5 | 5 | peer_heartbeat_secs = 60 | |
| 6 | 6 | route_check_interval_secs = 300 | |
| 7 | 7 | dashboard = false | |
| @@ -17,17 +17,17 @@ expected_routes = ["/", "/discover", "/login", "/docs"] | |||
| 17 | 17 | [[targets.mnw.dns]] | |
| 18 | 18 | name = "makenot.work" | |
| 19 | 19 | record_type = "A" | |
| 20 | - | expected = ["5.78.144.244"] | |
| 20 | + | expected = [] | |
| 21 | 21 | ||
| 22 | 22 | [[targets.mnw.dns]] | |
| 23 | 23 | name = "forums.makenot.work" | |
| 24 | 24 | record_type = "A" | |
| 25 | - | expected = ["5.78.144.244"] | |
| 25 | + | expected = [] | |
| 26 | 26 | ||
| 27 | 27 | [[targets.mnw.dns]] | |
| 28 | 28 | name = "git.makenot.work" | |
| 29 | 29 | record_type = "A" | |
| 30 | - | expected = ["5.78.144.244"] | |
| 30 | + | expected = [] | |
| 31 | 31 | ||
| 32 | 32 | [targets.mnw.whois] | |
| 33 | 33 | domain = "makenot.work" | |
| @@ -81,7 +81,7 @@ label = "htpy.app" | |||
| 81 | 81 | [[targets.htpy.dns]] | |
| 82 | 82 | name = "htpy.app" | |
| 83 | 83 | record_type = "A" | |
| 84 | - | expected = ["5.78.135.189"] | |
| 84 | + | expected = [] | |
| 85 | 85 | ||
| 86 | 86 | [targets.htpy.whois] | |
| 87 | 87 | domain = "htpy.app" |
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | //! HTTP API for serve mode — exposes health check data to consumers like MNW. | |
| 2 | 2 | ||
| 3 | - | use std::collections::HashMap; | |
| 3 | + | use std::collections::{HashMap, HashSet}; | |
| 4 | 4 | use std::sync::Arc; | |
| 5 | 5 | use std::sync::atomic::{AtomicU64, Ordering}; | |
| 6 | 6 | ||
| @@ -336,8 +336,12 @@ async fn build_target_status( | |||
| 336 | 336 | let route_checks = db::get_latest_route_checks(pool, name) | |
| 337 | 337 | .await | |
| 338 | 338 | .unwrap_or_default(); | |
| 339 | + | let expected_routes: HashSet<&str> = config.get_target(name) | |
| 340 | + | .map(|t| t.expected_routes.iter().map(|s| s.as_str()).collect()) | |
| 341 | + | .unwrap_or_default(); | |
| 339 | 342 | let route_status: Vec<RouteStatusJson> = route_checks | |
| 340 | 343 | .into_iter() | |
| 344 | + | .filter(|r| expected_routes.contains(r.path.as_str())) | |
| 341 | 345 | .map(|r| RouteStatusJson { | |
| 342 | 346 | path: r.path, | |
| 343 | 347 | status_code: r.status_code, | |
| @@ -350,8 +354,12 @@ async fn build_target_status( | |||
| 350 | 354 | let dns_checks = db::get_latest_dns_checks(pool, name) | |
| 351 | 355 | .await | |
| 352 | 356 | .unwrap_or_default(); | |
| 357 | + | let expected_dns: HashSet<(String, String)> = config.get_target(name) | |
| 358 | + | .map(|t| t.dns.iter().map(|d| (d.name.clone(), d.record_type.to_string())).collect()) | |
| 359 | + | .unwrap_or_default(); | |
| 353 | 360 | let dns_status: Vec<DnsStatusJson> = dns_checks | |
| 354 | 361 | .into_iter() | |
| 362 | + | .filter(|r| expected_dns.contains(&(r.name.clone(), r.record_type.clone()))) | |
| 355 | 363 | .map(|r| DnsStatusJson { | |
| 356 | 364 | name: r.name, | |
| 357 | 365 | record_type: r.record_type, |
| @@ -30,6 +30,17 @@ pub(crate) fn spawn_dns_tasks( | |||
| 30 | 30 | info!("{name}: DNS check every {dns_interval_secs}s ({n} records)"); | |
| 31 | 31 | ||
| 32 | 32 | handles.push(tokio::spawn(async move { | |
| 33 | + | // Prune stale DNS data from DB (records removed from config) | |
| 34 | + | let expected_dns_keys: Vec<(String, String)> = dns_records | |
| 35 | + | .iter() | |
| 36 | + | .map(|d| (d.name.clone(), d.record_type.to_string())) | |
| 37 | + | .collect(); | |
| 38 | + | match db::prune_stale_dns(&pool, &name, &expected_dns_keys).await { | |
| 39 | + | Ok(0) => {} | |
| 40 | + | Ok(n) => info!("{name}: pruned {n} stale DNS check rows"), | |
| 41 | + | Err(e) => tracing::error!("{name}: failed to prune stale DNS: {e}"), | |
| 42 | + | } | |
| 43 | + | ||
| 33 | 44 | let mut interval = tokio::time::interval( | |
| 34 | 45 | std::time::Duration::from_secs(dns_interval_secs), | |
| 35 | 46 | ); |
| @@ -33,6 +33,13 @@ pub(crate) fn spawn_route_tasks( | |||
| 33 | 33 | info!("{name}: route checks every {route_interval}s ({n} routes)"); | |
| 34 | 34 | ||
| 35 | 35 | handles.push(tokio::spawn(async move { | |
| 36 | + | // Prune stale route data from DB (paths removed from config) | |
| 37 | + | match db::prune_stale_routes(&pool, &name, &route_paths).await { | |
| 38 | + | Ok(0) => {} | |
| 39 | + | Ok(n) => info!("{name}: pruned {n} stale route check rows"), | |
| 40 | + | Err(e) => tracing::error!("{name}: failed to prune stale routes: {e}"), | |
| 41 | + | } | |
| 42 | + | ||
| 36 | 43 | let mut interval = tokio::time::interval( | |
| 37 | 44 | std::time::Duration::from_secs(route_interval), | |
| 38 | 45 | ); |
| @@ -966,6 +966,71 @@ pub async fn get_latest_whois_check( | |||
| 966 | 966 | .await?) | |
| 967 | 967 | } | |
| 968 | 968 | ||
| 969 | + | // --- Stale data cleanup --- | |
| 970 | + | ||
| 971 | + | /// Delete route_checks for paths no longer in the config. | |
| 972 | + | #[instrument(skip_all)] | |
| 973 | + | pub async fn prune_stale_routes( | |
| 974 | + | pool: &SqlitePool, | |
| 975 | + | target: &str, | |
| 976 | + | expected_routes: &[String], | |
| 977 | + | ) -> Result<u64> { | |
| 978 | + | if expected_routes.is_empty() { | |
| 979 | + | // No configured routes — delete all route checks for this target | |
| 980 | + | let result = sqlx::query("DELETE FROM route_checks WHERE target = ?") | |
| 981 | + | .bind(target) | |
| 982 | + | .execute(pool) | |
| 983 | + | .await?; | |
| 984 | + | return Ok(result.rows_affected()); | |
| 985 | + | } | |
| 986 | + | ||
| 987 | + | // Build placeholders for the IN clause | |
| 988 | + | let placeholders: Vec<&str> = expected_routes.iter().map(|_| "?").collect(); | |
| 989 | + | let sql = format!( | |
| 990 | + | "DELETE FROM route_checks WHERE target = ? AND path NOT IN ({})", | |
| 991 | + | placeholders.join(", ") | |
| 992 | + | ); | |
| 993 | + | let mut query = sqlx::query(&sql).bind(target); | |
| 994 | + | for route in expected_routes { | |
| 995 | + | query = query.bind(route); | |
| 996 | + | } | |
| 997 | + | let result = query.execute(pool).await?; | |
| 998 | + | Ok(result.rows_affected()) | |
| 999 | + | } | |
| 1000 | + | ||
| 1001 | + | /// Delete dns_checks for (name, record_type) pairs no longer in the config. | |
| 1002 | + | #[instrument(skip_all)] | |
| 1003 | + | pub async fn prune_stale_dns( | |
| 1004 | + | pool: &SqlitePool, | |
| 1005 | + | target: &str, | |
| 1006 | + | expected_dns: &[(String, String)], | |
| 1007 | + | ) -> Result<u64> { | |
| 1008 | + | if expected_dns.is_empty() { | |
| 1009 | + | // No configured DNS records — delete all DNS checks for this target | |
| 1010 | + | let result = sqlx::query("DELETE FROM dns_checks WHERE target = ?") | |
| 1011 | + | .bind(target) | |
| 1012 | + | .execute(pool) | |
| 1013 | + | .await?; | |
| 1014 | + | return Ok(result.rows_affected()); | |
| 1015 | + | } | |
| 1016 | + | ||
| 1017 | + | // Build a compound condition: keep rows matching any configured (name, record_type) pair | |
| 1018 | + | let conditions: Vec<String> = expected_dns | |
| 1019 | + | .iter() | |
| 1020 | + | .map(|_| "(name = ? AND record_type = ?)".to_string()) | |
| 1021 | + | .collect(); | |
| 1022 | + | let sql = format!( | |
| 1023 | + | "DELETE FROM dns_checks WHERE target = ? AND NOT ({})", | |
| 1024 | + | conditions.join(" OR ") | |
| 1025 | + | ); | |
| 1026 | + | let mut query = sqlx::query(&sql).bind(target); | |
| 1027 | + | for (name, record_type) in expected_dns { | |
| 1028 | + | query = query.bind(name).bind(record_type); | |
| 1029 | + | } | |
| 1030 | + | let result = query.execute(pool).await?; | |
| 1031 | + | Ok(result.rows_affected()) | |
| 1032 | + | } | |
| 1033 | + | ||
| 969 | 1034 | // --- Maintenance --- | |
| 970 | 1035 | ||
| 971 | 1036 | /// Prune result with counts for each table. |
| @@ -1293,7 +1293,13 @@ async fn route_check_prune() { | |||
| 1293 | 1293 | #[tokio::test] | |
| 1294 | 1294 | async fn api_status_includes_route_status() { | |
| 1295 | 1295 | let pool = db::connect_in_memory().await.unwrap(); | |
| 1296 | - | let config = test_config(); | |
| 1296 | + | let config: pom::config::Config = toml::from_str(r#" | |
| 1297 | + | [targets.mnw] | |
| 1298 | + | label = "MakeNotWork" | |
| 1299 | + | expected_routes = ["/"] | |
| 1300 | + | [targets.mnw.health] | |
| 1301 | + | url = "https://makenot.work/health" | |
| 1302 | + | "#).unwrap(); | |
| 1297 | 1303 | let app = pom::api::router(pool.clone(), config, None); | |
| 1298 | 1304 | ||
| 1299 | 1305 | // Insert route checks | |
| @@ -2512,7 +2518,16 @@ async fn prune_removes_old_whois_checks() { | |||
| 2512 | 2518 | #[tokio::test] | |
| 2513 | 2519 | async fn api_status_includes_dns_status() { | |
| 2514 | 2520 | let pool = db::connect_in_memory().await.unwrap(); | |
| 2515 | - | let config = test_config(); | |
| 2521 | + | let config: pom::config::Config = toml::from_str(r#" | |
| 2522 | + | [targets.mnw] | |
| 2523 | + | label = "MakeNotWork" | |
| 2524 | + | [targets.mnw.health] | |
| 2525 | + | url = "https://makenot.work/health" | |
| 2526 | + | [[targets.mnw.dns]] | |
| 2527 | + | name = "makenot.work" | |
| 2528 | + | record_type = "A" | |
| 2529 | + | expected = ["5.78.144.244"] | |
| 2530 | + | "#).unwrap(); | |
| 2516 | 2531 | let app = pom::api::router(pool.clone(), config, None); | |
| 2517 | 2532 | ||
| 2518 | 2533 | // Insert DNS check data |