Skip to main content

max / makenotwork

7.6 KB · 263 lines History Blame Raw
1 //! CLI command handlers for PoM subcommands.
2
3 mod incident;
4 mod serve;
5 mod status;
6 mod tasks;
7
8 pub(crate) use serve::cmd_serve;
9 pub(crate) use status::cmd_status;
10
11 use clap::Subcommand;
12
13 use pom::checks::{dns, http, ssh, whois};
14 use pom::config::Config;
15 use pom::db;
16 use pom::display;
17 use pom::error::{PomError, Result};
18
19 #[derive(Subcommand)]
20 pub(crate) enum HistoryKind {
21 /// Health check history
22 Health {
23 /// Filter by target
24 target: Option<String>,
25 /// Number of results
26 #[arg(short, default_value = "10")]
27 n: i64,
28 /// Output as JSON
29 #[arg(long)]
30 json: bool,
31 },
32 /// Test run history
33 Tests {
34 /// Filter by target
35 target: Option<String>,
36 /// Number of results
37 #[arg(short, default_value = "10")]
38 n: i64,
39 /// Output as JSON
40 #[arg(long)]
41 json: bool,
42 },
43 }
44
45 pub(crate) async fn cmd_health(
46 pool: &sqlx::SqlitePool,
47 config: &Config,
48 target: Option<&str>,
49 json: bool,
50 ) -> Result<()> {
51 let targets: Vec<String> = match target {
52 Some(t) => {
53 if config.get_target(t).is_none() {
54 eprintln!("Unknown target: {t}");
55 std::process::exit(1);
56 }
57 vec![t.to_string()]
58 }
59 None => config.target_names(),
60 };
61
62 let mut snapshots = Vec::new();
63
64 for name in &targets {
65 let target_config = config.get_target(name).unwrap();
66 if let Some(health_config) = &target_config.health {
67 let snapshot = http::check_health(name, health_config, health_config.expect.as_ref()).await;
68 db::insert_health_check(pool, &snapshot).await?;
69 snapshots.push(snapshot);
70 } else {
71 eprintln!("{name}: no health endpoint configured");
72 }
73 }
74
75 if json {
76 println!("{}", serde_json::to_string_pretty(&snapshots)?);
77 } else {
78 print!("{}", display::format_health_snapshots(&snapshots));
79 }
80
81 Ok(())
82 }
83
84 pub(crate) async fn cmd_test(
85 pool: &sqlx::SqlitePool,
86 config: &Config,
87 target_name: &str,
88 filter: Option<&str>,
89 json: bool,
90 ) -> Result<()> {
91 let target = config.get_target(target_name).ok_or_else(|| {
92 PomError::Config(format!("Unknown target: {target_name}"))
93 })?;
94 let tests_config = target.tests.as_ref().ok_or_else(|| {
95 PomError::Config(format!("Target '{target_name}' has no test configuration"))
96 })?;
97
98 eprintln!("Running tests on {target_name}...");
99 let run = ssh::run_tests(target_name, tests_config, filter).await;
100 let run_id = db::insert_test_run(pool, &run).await?;
101
102 // Store per-test details and detect regressions
103 if !run.summary.details.is_empty() {
104 db::insert_test_details(pool, run_id, &run.summary.details).await?;
105 }
106 let regressions = db::get_test_regressions(pool, target_name, run_id).await.unwrap_or_default();
107
108 if json {
109 let summary = serde_json::json!({
110 "target": run.target,
111 "passed": run.passed,
112 "exit_code": run.exit_code,
113 "duration_secs": run.duration_secs,
114 "started_at": run.started_at,
115 "finished_at": run.finished_at,
116 "filter": run.filter,
117 "summary": run.summary,
118 "regressions": regressions,
119 });
120 println!("{}", serde_json::to_string_pretty(&summary)?);
121 } else {
122 print!("{}", display::format_test_result(target_name, &run));
123 if !regressions.is_empty() {
124 print!("{}", display::format_regressions(&regressions));
125 }
126 }
127
128 Ok(())
129 }
130
131 pub(crate) async fn cmd_history(
132 pool: &sqlx::SqlitePool,
133 kind: HistoryKind,
134 ) -> Result<()> {
135 match kind {
136 HistoryKind::Health { target, n, json } => {
137 let history = db::get_health_history(pool, target.as_deref(), n).await?;
138 if json {
139 println!("{}", serde_json::to_string_pretty(&history)?);
140 } else {
141 print!("{}", display::format_health_history(&history));
142 }
143 }
144 HistoryKind::Tests { target, n, json } => {
145 let history = db::get_test_history(pool, target.as_deref(), n).await?;
146 if json {
147 let summaries: Vec<serde_json::Value> = history
148 .iter()
149 .map(|r| serde_json::json!({
150 "id": r.id,
151 "target": r.target,
152 "passed": r.passed,
153 "exit_code": r.exit_code,
154 "duration_secs": r.duration_secs,
155 "started_at": r.started_at,
156 "summary": r.summary,
157 }))
158 .collect();
159 println!("{}", serde_json::to_string_pretty(&summaries)?);
160 } else {
161 print!("{}", display::format_test_history(&history));
162 }
163 }
164 }
165
166 Ok(())
167 }
168
169 pub(crate) async fn cmd_prune(
170 pool: &sqlx::SqlitePool,
171 days: i64,
172 ) -> Result<()> {
173 let result = db::prune_old_records(pool, days).await?;
174 print!("{}", display::format_prune(&result, days));
175 Ok(())
176 }
177
178 pub(crate) async fn cmd_dns(
179 pool: &sqlx::SqlitePool,
180 config: &Config,
181 target: Option<&str>,
182 json: bool,
183 ) -> Result<()> {
184 let targets: Vec<String> = match target {
185 Some(t) => {
186 if config.get_target(t).is_none() {
187 eprintln!("Unknown target: {t}");
188 std::process::exit(1);
189 }
190 vec![t.to_string()]
191 }
192 None => config.target_names(),
193 };
194
195 let mut all_dns_results = Vec::new();
196 let mut all_whois_results = Vec::new();
197
198 for name in &targets {
199 let target_config = config.get_target(name).unwrap();
200
201 // DNS checks
202 if !target_config.dns.is_empty() {
203 let results = dns::check_dns(name, &target_config.dns).await;
204 for result in &results {
205 if let Err(e) = db::insert_dns_check(pool, result).await {
206 tracing::error!("{name}: failed to store DNS check: {e}");
207 }
208 }
209 all_dns_results.extend(results);
210 }
211
212 // WHOIS check
213 if let Some(ref whois_config) = target_config.whois {
214 let result = whois::check_whois(name, whois_config).await;
215 if let Err(e) = db::insert_whois_check(pool, &result).await {
216 tracing::error!("{name}: failed to store WHOIS check: {e}");
217 }
218 all_whois_results.push(result);
219 }
220 }
221
222 if json {
223 let output = serde_json::json!({
224 "dns": all_dns_results,
225 "whois": all_whois_results,
226 });
227 println!("{}", serde_json::to_string_pretty(&output)?);
228 } else if all_dns_results.is_empty() && all_whois_results.is_empty() {
229 println!("No DNS or WHOIS checks configured for the selected target(s).");
230 } else {
231 print!("{}", display::format_dns_results(&all_dns_results, &all_whois_results));
232 }
233
234 Ok(())
235 }
236
237 pub(crate) async fn cmd_mesh(
238 config: &Config,
239 json: bool,
240 ) -> Result<()> {
241 let listen = &config.serve.listen;
242 let url = format!("http://{listen}/api/mesh");
243
244 let client = reqwest::Client::builder()
245 .timeout(std::time::Duration::from_secs(5))
246 .build()?;
247
248 let response = client.get(&url).send().await.map_err(|e| {
249 PomError::Config(format!("Could not reach local PoM instance at {listen}: {e}"))
250 })?;
251
252 let data: serde_json::Value = response.json().await?;
253
254 if json {
255 println!("{}", serde_json::to_string_pretty(&data)?);
256 return Ok(());
257 }
258
259 print!("{}", display::format_mesh(&data));
260
261 Ok(())
262 }
263