headless_lms_server/controllers/main_frontend/
status.rs

1//! Controllers for requests starting with `/api/v0/main-frontend/status`.
2
3use 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;
13
14pub use system_health::{
15    CronJobInfo, DeploymentInfo, EventInfo, IngressInfo, JobInfo, PodDisruptionBudgetInfo, PodInfo,
16    ServiceInfo, ServicePortInfo,
17};
18
19/**
20GET `/api/v0/main-frontend/status/pods`
21
22Returns the status of all Pods in the current namespace.
23*/
24pub async fn pods(
25    pool: web::Data<PgPool>,
26    user: AuthUser,
27) -> ControllerResult<web::Json<Vec<PodInfo>>> {
28    let mut conn = pool.acquire().await?;
29    let token = authorize(
30        &mut conn,
31        Action::Administrate,
32        Some(user.id),
33        Resource::GlobalPermissions,
34    )
35    .await?;
36    let ns = get_namespace();
37    let pods = get_pods(&ns).await.map_err(|e| {
38        ControllerError::new(
39            ControllerErrorType::InternalServerError,
40            e.to_string(),
41            None,
42        )
43    })?;
44    token.authorized_ok(web::Json(pods))
45}
46
47/**
48GET `/api/v0/main-frontend/status/deployments`
49
50Returns the status of all Deployments in the current namespace.
51*/
52pub async fn deployments(
53    pool: web::Data<PgPool>,
54    user: AuthUser,
55) -> ControllerResult<web::Json<Vec<DeploymentInfo>>> {
56    let mut conn = pool.acquire().await?;
57    let token = authorize(
58        &mut conn,
59        Action::Administrate,
60        Some(user.id),
61        Resource::GlobalPermissions,
62    )
63    .await?;
64    let ns = get_namespace();
65    let deployments = get_deployments(&ns).await.map_err(|e| {
66        ControllerError::new(
67            ControllerErrorType::InternalServerError,
68            e.to_string(),
69            None,
70        )
71    })?;
72    token.authorized_ok(web::Json(deployments))
73}
74
75/**
76GET `/api/v0/main-frontend/status/cronjobs`
77
78Returns the status of all CronJobs in the current namespace.
79*/
80pub async fn cronjobs(
81    pool: web::Data<PgPool>,
82    user: AuthUser,
83) -> ControllerResult<web::Json<Vec<CronJobInfo>>> {
84    let mut conn = pool.acquire().await?;
85    let token = authorize(
86        &mut conn,
87        Action::Administrate,
88        Some(user.id),
89        Resource::GlobalPermissions,
90    )
91    .await?;
92    let ns = get_namespace();
93    let cronjobs = get_cronjobs(&ns).await.map_err(|e| {
94        ControllerError::new(
95            ControllerErrorType::InternalServerError,
96            e.to_string(),
97            None,
98        )
99    })?;
100    token.authorized_ok(web::Json(cronjobs))
101}
102
103/**
104GET `/api/v0/main-frontend/status/jobs`
105
106Returns the status of all Jobs in the current namespace.
107*/
108pub async fn jobs(
109    pool: web::Data<PgPool>,
110    user: AuthUser,
111) -> ControllerResult<web::Json<Vec<JobInfo>>> {
112    let mut conn = pool.acquire().await?;
113    let token = authorize(
114        &mut conn,
115        Action::Administrate,
116        Some(user.id),
117        Resource::GlobalPermissions,
118    )
119    .await?;
120    let ns = get_namespace();
121    let jobs = get_jobs(&ns).await.map_err(|e| {
122        ControllerError::new(
123            ControllerErrorType::InternalServerError,
124            e.to_string(),
125            None,
126        )
127    })?;
128    token.authorized_ok(web::Json(jobs))
129}
130
131/**
132GET `/api/v0/main-frontend/status/services`
133
134Returns the status of all Services in the current namespace.
135*/
136pub async fn services(
137    pool: web::Data<PgPool>,
138    user: AuthUser,
139) -> ControllerResult<web::Json<Vec<ServiceInfo>>> {
140    let mut conn = pool.acquire().await?;
141    let token = authorize(
142        &mut conn,
143        Action::Administrate,
144        Some(user.id),
145        Resource::GlobalPermissions,
146    )
147    .await?;
148    let ns = get_namespace();
149    let services = get_services(&ns).await.map_err(|e| {
150        ControllerError::new(
151            ControllerErrorType::InternalServerError,
152            e.to_string(),
153            None,
154        )
155    })?;
156    token.authorized_ok(web::Json(services))
157}
158
159/**
160GET `/api/v0/main-frontend/status/events`
161
162Returns the status of all Events in the current namespace.
163*/
164pub async fn events(
165    pool: web::Data<PgPool>,
166    user: AuthUser,
167) -> ControllerResult<web::Json<Vec<EventInfo>>> {
168    let mut conn = pool.acquire().await?;
169    let token = authorize(
170        &mut conn,
171        Action::Administrate,
172        Some(user.id),
173        Resource::GlobalPermissions,
174    )
175    .await?;
176    let ns = get_namespace();
177    let events = get_events(&ns).await.map_err(|e| {
178        ControllerError::new(
179            ControllerErrorType::InternalServerError,
180            e.to_string(),
181            None,
182        )
183    })?;
184    token.authorized_ok(web::Json(events))
185}
186
187/**
188GET `/api/v0/main-frontend/status/ingresses`
189
190Returns the status of all Ingresses in the current namespace.
191*/
192pub async fn ingresses(
193    pool: web::Data<PgPool>,
194    user: AuthUser,
195) -> ControllerResult<web::Json<Vec<IngressInfo>>> {
196    let mut conn = pool.acquire().await?;
197    let token = authorize(
198        &mut conn,
199        Action::Administrate,
200        Some(user.id),
201        Resource::GlobalPermissions,
202    )
203    .await?;
204    let ns = get_namespace();
205    let ingresses = get_ingresses(&ns).await.map_err(|e| {
206        ControllerError::new(
207            ControllerErrorType::InternalServerError,
208            e.to_string(),
209            None,
210        )
211    })?;
212    token.authorized_ok(web::Json(ingresses))
213}
214
215/**
216GET `/api/v0/main-frontend/status/pod-disruption-budgets`
217
218Returns the status of all PodDisruptionBudgets in the current namespace.
219*/
220pub async fn pod_disruption_budgets(
221    pool: web::Data<PgPool>,
222    user: AuthUser,
223) -> ControllerResult<web::Json<Vec<PodDisruptionBudgetInfo>>> {
224    let mut conn = pool.acquire().await?;
225    let token = authorize(
226        &mut conn,
227        Action::Administrate,
228        Some(user.id),
229        Resource::GlobalPermissions,
230    )
231    .await?;
232    let ns = get_namespace();
233    let pdbs = get_pod_disruption_budgets(&ns).await.map_err(|e| {
234        ControllerError::new(
235            ControllerErrorType::InternalServerError,
236            e.to_string(),
237            None,
238        )
239    })?;
240    token.authorized_ok(web::Json(pdbs))
241}
242
243fn parse_and_validate_tail(tail_str: Option<&String>) -> i64 {
244    const DEFAULT_TAIL_LINES: i64 = 1000;
245    const MAX_TAIL_LINES: i64 = 10_000;
246
247    match tail_str {
248        Some(s) => match s.parse::<u64>() {
249            Ok(val) => {
250                let clamped = val.min(MAX_TAIL_LINES as u64);
251                clamped as i64
252            }
253            Err(_) => DEFAULT_TAIL_LINES,
254        },
255        None => DEFAULT_TAIL_LINES,
256    }
257}
258
259/**
260GET `/api/v0/main-frontend/status/pods/{pod_name}/logs`
261
262Returns logs from a specific pod.
263
264Query parameters:
265- container: Optional<String> - Container name (if pod has multiple containers)
266- tail: Optional<u64> - Number of lines to tail from the end (default: 1000, max: 10000)
267*/
268pub async fn pod_logs(
269    path: web::Path<String>,
270    query: web::Query<HashMap<String, String>>,
271    pool: web::Data<PgPool>,
272    user: AuthUser,
273) -> ControllerResult<HttpResponse> {
274    let mut conn = pool.acquire().await?;
275    let token = authorize(
276        &mut conn,
277        Action::Administrate,
278        Some(user.id),
279        Resource::GlobalPermissions,
280    )
281    .await?;
282    let pod_name = path.into_inner();
283    let ns = get_namespace();
284
285    let container = query.get("container").map(|s| s.as_str());
286    let tail = parse_and_validate_tail(query.get("tail"));
287
288    let logs = get_pod_logs(&ns, &pod_name, container, tail)
289        .await
290        .map_err(|e| {
291            ControllerError::new(
292                ControllerErrorType::InternalServerError,
293                e.to_string(),
294                None,
295            )
296        })?;
297
298    token.authorized_ok(
299        HttpResponse::Ok()
300            .content_type("text/plain; charset=utf-8")
301            .body(logs),
302    )
303}
304
305/**
306GET `/api/v0/main-frontend/status/health`
307
308Returns detailed system health status with issues list (admin only).
309*/
310pub async fn health(
311    pool: web::Data<PgPool>,
312    user: AuthUser,
313) -> ControllerResult<web::Json<SystemHealthStatus>> {
314    let mut conn = pool.acquire().await?;
315    let token = authorize(
316        &mut conn,
317        Action::Administrate,
318        Some(user.id),
319        Resource::GlobalPermissions,
320    )
321    .await?;
322    let ns = get_namespace();
323    let health_status = check_system_health_detailed(&ns, Some(&pool))
324        .await
325        .map_err(|e| {
326            ControllerError::new(
327                ControllerErrorType::InternalServerError,
328                e.to_string(),
329                None,
330            )
331        })?;
332    token.authorized_ok(web::Json(health_status))
333}
334
335/**
336GET `/api/v0/main-frontend/status/system-health` Returns a boolean indicating whether the system is healthy.
337
338Uses the same underlying checking logic as the detailed health endpoint.
339Unauthenticated users get a boolean. Authenticated admins get error details on failure.
340*/
341pub async fn system_health(
342    pool: web::Data<PgPool>,
343    user: Option<AuthUser>,
344) -> ControllerResult<web::Json<bool>> {
345    let ns = get_namespace();
346    let kubernetes_health = check_system_health_detailed(&ns, Some(&pool)).await;
347
348    let is_healthy = matches!(
349        kubernetes_health,
350        Ok(SystemHealthStatus {
351            status: HealthStatus::Healthy,
352            ..
353        })
354    );
355
356    if is_healthy {
357        let token = skip_authorize();
358        return token.authorized_ok(web::Json(true));
359    }
360
361    if let Some(user) = user {
362        let mut conn = pool.acquire().await?;
363        authorize(
364            &mut conn,
365            Action::Administrate,
366            Some(user.id),
367            Resource::GlobalPermissions,
368        )
369        .await?;
370
371        let error_msg = match kubernetes_health {
372            Err(e) => format!("System health check failed: {}", e),
373            Ok(health_status) => {
374                if health_status.issues.is_empty() {
375                    "System is unhealthy".to_string()
376                } else {
377                    format!("System is unhealthy: {}", health_status.issues.join(", "))
378                }
379            }
380        };
381
382        Err(ControllerError::new(
383            ControllerErrorType::InternalServerError,
384            error_msg,
385            None,
386        ))
387    } else {
388        let token = skip_authorize();
389        token.authorized_ok(web::Json(false))
390    }
391}
392
393pub fn _add_routes(cfg: &mut ServiceConfig) {
394    cfg.route("/pods", web::get().to(pods))
395        .route("/deployments", web::get().to(deployments))
396        .route("/cronjobs", web::get().to(cronjobs))
397        .route("/jobs", web::get().to(jobs))
398        .route("/services", web::get().to(services))
399        .route("/events", web::get().to(events))
400        .route("/ingresses", web::get().to(ingresses))
401        .route(
402            "/pod-disruption-budgets",
403            web::get().to(pod_disruption_budgets),
404        )
405        .route("/pods/{pod_name}/logs", web::get().to(pod_logs))
406        .route("/health", web::get().to(health))
407        .route("/system-health", web::get().to(system_health));
408}