1use crate::{
4 domain::authorization::{Action, Resource, authorize},
5 domain::system_health::{
6 self, HealthStatus, SystemHealthStatus, check_system_health_detailed, get_cronjobs,
7 get_deployments, get_events, get_ingresses, get_jobs, get_namespace,
8 get_pod_disruption_budgets, get_pod_logs, get_pods, get_services,
9 },
10 prelude::*,
11};
12use std::collections::HashMap;
13use utoipa::OpenApi;
14
15pub use system_health::{
16 CronJobInfo, DeploymentInfo, EventInfo, IngressInfo, JobInfo, PodDisruptionBudgetInfo, PodInfo,
17 ServiceInfo, ServicePortInfo,
18};
19
20#[derive(OpenApi)]
21#[openapi(paths(
22 pods,
23 deployments,
24 cronjobs,
25 jobs,
26 services,
27 events,
28 ingresses,
29 pod_disruption_budgets,
30 pod_logs,
31 health,
32 system_health
33))]
34pub(crate) struct MainFrontendStatusApiDoc;
35
36#[utoipa::path(
42 get,
43 path = "/pods",
44 operation_id = "getStatusPods",
45 tag = "status",
46 responses(
47 (status = 200, description = "Pods", body = [PodInfo])
48 )
49)]
50pub async fn pods(
51 pool: web::Data<PgPool>,
52 user: AuthUser,
53) -> ControllerResult<web::Json<Vec<PodInfo>>> {
54 let mut conn = pool.acquire().await?;
55 let token = authorize(
56 &mut conn,
57 Action::Administrate,
58 Some(user.id),
59 Resource::GlobalPermissions,
60 )
61 .await?;
62 let ns = get_namespace();
63 let pods = get_pods(&ns).await.map_err(|e| {
64 ControllerError::new(
65 ControllerErrorType::InternalServerError,
66 e.to_string(),
67 None,
68 )
69 })?;
70 token.authorized_ok(web::Json(pods))
71}
72
73#[utoipa::path(
79 get,
80 path = "/deployments",
81 operation_id = "getStatusDeployments",
82 tag = "status",
83 responses(
84 (status = 200, description = "Deployments", body = [DeploymentInfo])
85 )
86)]
87pub async fn deployments(
88 pool: web::Data<PgPool>,
89 user: AuthUser,
90) -> ControllerResult<web::Json<Vec<DeploymentInfo>>> {
91 let mut conn = pool.acquire().await?;
92 let token = authorize(
93 &mut conn,
94 Action::Administrate,
95 Some(user.id),
96 Resource::GlobalPermissions,
97 )
98 .await?;
99 let ns = get_namespace();
100 let deployments = get_deployments(&ns).await.map_err(|e| {
101 ControllerError::new(
102 ControllerErrorType::InternalServerError,
103 e.to_string(),
104 None,
105 )
106 })?;
107 token.authorized_ok(web::Json(deployments))
108}
109
110#[utoipa::path(
116 get,
117 path = "/cronjobs",
118 operation_id = "getStatusCronjobs",
119 tag = "status",
120 responses(
121 (status = 200, description = "Cronjobs", body = [CronJobInfo])
122 )
123)]
124pub async fn cronjobs(
125 pool: web::Data<PgPool>,
126 user: AuthUser,
127) -> ControllerResult<web::Json<Vec<CronJobInfo>>> {
128 let mut conn = pool.acquire().await?;
129 let token = authorize(
130 &mut conn,
131 Action::Administrate,
132 Some(user.id),
133 Resource::GlobalPermissions,
134 )
135 .await?;
136 let ns = get_namespace();
137 let cronjobs = get_cronjobs(&ns).await.map_err(|e| {
138 ControllerError::new(
139 ControllerErrorType::InternalServerError,
140 e.to_string(),
141 None,
142 )
143 })?;
144 token.authorized_ok(web::Json(cronjobs))
145}
146
147#[utoipa::path(
153 get,
154 path = "/jobs",
155 operation_id = "getStatusJobs",
156 tag = "status",
157 responses(
158 (status = 200, description = "Jobs", body = [JobInfo])
159 )
160)]
161pub async fn jobs(
162 pool: web::Data<PgPool>,
163 user: AuthUser,
164) -> ControllerResult<web::Json<Vec<JobInfo>>> {
165 let mut conn = pool.acquire().await?;
166 let token = authorize(
167 &mut conn,
168 Action::Administrate,
169 Some(user.id),
170 Resource::GlobalPermissions,
171 )
172 .await?;
173 let ns = get_namespace();
174 let jobs = get_jobs(&ns).await.map_err(|e| {
175 ControllerError::new(
176 ControllerErrorType::InternalServerError,
177 e.to_string(),
178 None,
179 )
180 })?;
181 token.authorized_ok(web::Json(jobs))
182}
183
184#[utoipa::path(
190 get,
191 path = "/services",
192 operation_id = "getStatusServices",
193 tag = "status",
194 responses(
195 (status = 200, description = "Services", body = [ServiceInfo])
196 )
197)]
198pub async fn services(
199 pool: web::Data<PgPool>,
200 user: AuthUser,
201) -> ControllerResult<web::Json<Vec<ServiceInfo>>> {
202 let mut conn = pool.acquire().await?;
203 let token = authorize(
204 &mut conn,
205 Action::Administrate,
206 Some(user.id),
207 Resource::GlobalPermissions,
208 )
209 .await?;
210 let ns = get_namespace();
211 let services = get_services(&ns).await.map_err(|e| {
212 ControllerError::new(
213 ControllerErrorType::InternalServerError,
214 e.to_string(),
215 None,
216 )
217 })?;
218 token.authorized_ok(web::Json(services))
219}
220
221#[utoipa::path(
227 get,
228 path = "/events",
229 operation_id = "getStatusEvents",
230 tag = "status",
231 responses(
232 (status = 200, description = "Events", body = [EventInfo])
233 )
234)]
235pub async fn events(
236 pool: web::Data<PgPool>,
237 user: AuthUser,
238) -> ControllerResult<web::Json<Vec<EventInfo>>> {
239 let mut conn = pool.acquire().await?;
240 let token = authorize(
241 &mut conn,
242 Action::Administrate,
243 Some(user.id),
244 Resource::GlobalPermissions,
245 )
246 .await?;
247 let ns = get_namespace();
248 let events = get_events(&ns).await.map_err(|e| {
249 ControllerError::new(
250 ControllerErrorType::InternalServerError,
251 e.to_string(),
252 None,
253 )
254 })?;
255 token.authorized_ok(web::Json(events))
256}
257
258#[utoipa::path(
264 get,
265 path = "/ingresses",
266 operation_id = "getStatusIngresses",
267 tag = "status",
268 responses(
269 (status = 200, description = "Ingresses", body = [IngressInfo])
270 )
271)]
272pub async fn ingresses(
273 pool: web::Data<PgPool>,
274 user: AuthUser,
275) -> ControllerResult<web::Json<Vec<IngressInfo>>> {
276 let mut conn = pool.acquire().await?;
277 let token = authorize(
278 &mut conn,
279 Action::Administrate,
280 Some(user.id),
281 Resource::GlobalPermissions,
282 )
283 .await?;
284 let ns = get_namespace();
285 let ingresses = get_ingresses(&ns).await.map_err(|e| {
286 ControllerError::new(
287 ControllerErrorType::InternalServerError,
288 e.to_string(),
289 None,
290 )
291 })?;
292 token.authorized_ok(web::Json(ingresses))
293}
294
295#[utoipa::path(
301 get,
302 path = "/pod-disruption-budgets",
303 operation_id = "getStatusPodDisruptionBudgets",
304 tag = "status",
305 responses(
306 (status = 200, description = "Pod disruption budgets", body = [PodDisruptionBudgetInfo])
307 )
308)]
309pub async fn pod_disruption_budgets(
310 pool: web::Data<PgPool>,
311 user: AuthUser,
312) -> ControllerResult<web::Json<Vec<PodDisruptionBudgetInfo>>> {
313 let mut conn = pool.acquire().await?;
314 let token = authorize(
315 &mut conn,
316 Action::Administrate,
317 Some(user.id),
318 Resource::GlobalPermissions,
319 )
320 .await?;
321 let ns = get_namespace();
322 let pdbs = get_pod_disruption_budgets(&ns).await.map_err(|e| {
323 ControllerError::new(
324 ControllerErrorType::InternalServerError,
325 e.to_string(),
326 None,
327 )
328 })?;
329 token.authorized_ok(web::Json(pdbs))
330}
331
332fn parse_and_validate_tail(tail_str: Option<&String>) -> i64 {
333 const DEFAULT_TAIL_LINES: i64 = 1000;
334 const MAX_TAIL_LINES: i64 = 10_000;
335
336 match tail_str {
337 Some(s) => match s.parse::<u64>() {
338 Ok(val) => {
339 let clamped = val.min(MAX_TAIL_LINES as u64);
340 clamped as i64
341 }
342 Err(_) => DEFAULT_TAIL_LINES,
343 },
344 None => DEFAULT_TAIL_LINES,
345 }
346}
347
348#[utoipa::path(
358 get,
359 path = "/pods/{pod_name}/logs",
360 operation_id = "getStatusPodLogs",
361 tag = "status",
362 params(
363 ("pod_name" = String, Path, description = "Pod name"),
364 ("container" = Option<String>, Query, description = "Container name"),
365 ("tail" = Option<u64>, Query, description = "Number of log lines")
366 ),
367 responses(
368 (status = 200, description = "Pod logs", body = String)
369 )
370)]
371pub async fn pod_logs(
372 path: web::Path<String>,
373 query: web::Query<HashMap<String, String>>,
374 pool: web::Data<PgPool>,
375 user: AuthUser,
376) -> ControllerResult<HttpResponse> {
377 let mut conn = pool.acquire().await?;
378 let token = authorize(
379 &mut conn,
380 Action::Administrate,
381 Some(user.id),
382 Resource::GlobalPermissions,
383 )
384 .await?;
385 let pod_name = path.into_inner();
386 let ns = get_namespace();
387
388 let container = query.get("container").map(|s| s.as_str());
389 let tail = parse_and_validate_tail(query.get("tail"));
390
391 let logs = get_pod_logs(&ns, &pod_name, container, tail)
392 .await
393 .map_err(|e| {
394 ControllerError::new(
395 ControllerErrorType::InternalServerError,
396 e.to_string(),
397 None,
398 )
399 })?;
400
401 token.authorized_ok(
402 HttpResponse::Ok()
403 .content_type("text/plain; charset=utf-8")
404 .body(logs),
405 )
406}
407
408#[utoipa::path(
414 get,
415 path = "/health",
416 operation_id = "getStatusHealth",
417 tag = "status",
418 responses(
419 (status = 200, description = "Detailed system health", body = SystemHealthStatus)
420 )
421)]
422pub async fn health(
423 pool: web::Data<PgPool>,
424 user: AuthUser,
425) -> ControllerResult<web::Json<SystemHealthStatus>> {
426 let mut conn = pool.acquire().await?;
427 let token = authorize(
428 &mut conn,
429 Action::Administrate,
430 Some(user.id),
431 Resource::GlobalPermissions,
432 )
433 .await?;
434 let ns = get_namespace();
435 let health_status = check_system_health_detailed(&ns, Some(&pool))
436 .await
437 .map_err(|e| {
438 ControllerError::new(
439 ControllerErrorType::InternalServerError,
440 e.to_string(),
441 None,
442 )
443 })?;
444 token.authorized_ok(web::Json(health_status))
445}
446
447#[utoipa::path(
454 get,
455 path = "/system-health",
456 operation_id = "getStatusSystemHealth",
457 tag = "status",
458 responses(
459 (status = 200, description = "System health", body = bool)
460 )
461)]
462pub async fn system_health(
463 pool: web::Data<PgPool>,
464 user: Option<AuthUser>,
465) -> ControllerResult<web::Json<bool>> {
466 let ns = get_namespace();
467 let kubernetes_health = check_system_health_detailed(&ns, Some(&pool)).await;
468
469 let is_healthy = matches!(
470 kubernetes_health,
471 Ok(SystemHealthStatus {
472 status: HealthStatus::Healthy,
473 ..
474 })
475 );
476
477 if is_healthy {
478 let token = skip_authorize();
479 return token.authorized_ok(web::Json(true));
480 }
481
482 if let Some(user) = user {
483 let mut conn = pool.acquire().await?;
484 authorize(
485 &mut conn,
486 Action::Administrate,
487 Some(user.id),
488 Resource::GlobalPermissions,
489 )
490 .await?;
491
492 let error_msg = match kubernetes_health {
493 Err(e) => format!("System health check failed: {}", e),
494 Ok(health_status) => {
495 if health_status.issues.is_empty() {
496 "System is unhealthy".to_string()
497 } else {
498 format!("System is unhealthy: {}", health_status.issues.join(", "))
499 }
500 }
501 };
502
503 Err(ControllerError::new(
504 ControllerErrorType::InternalServerError,
505 error_msg,
506 None,
507 ))
508 } else {
509 let token = skip_authorize();
510 token.authorized_ok(web::Json(false))
511 }
512}
513
514pub fn _add_routes(cfg: &mut ServiceConfig) {
515 cfg.route("/pods", web::get().to(pods))
516 .route("/deployments", web::get().to(deployments))
517 .route("/cronjobs", web::get().to(cronjobs))
518 .route("/jobs", web::get().to(jobs))
519 .route("/services", web::get().to(services))
520 .route("/events", web::get().to(events))
521 .route("/ingresses", web::get().to(ingresses))
522 .route(
523 "/pod-disruption-budgets",
524 web::get().to(pod_disruption_budgets),
525 )
526 .route("/pods/{pod_name}/logs", web::get().to(pod_logs))
527 .route("/health", web::get().to(health))
528 .route("/system-health", web::get().to(system_health));
529}