//! /api/cluster: Kubernetes overview and workload actions. use axum::extract::{Path, State}; use axum::http::StatusCode; use axum::routing::{get, post}; use axum::{Json, Router}; use domain::cluster::{ClusterOverview, WorkloadKind, WorkloadRef}; use domain::jobs::JobKind; use domain::DomainError; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use crate::error::ApiError; use crate::extract::{AdminUser, AuthUser}; use crate::AppState; pub fn router() -> Router { Router::new() .route("/overview", get(overview)) .route("/workloads/{ns}/{kind}/{name}/restart", post(restart)) .route("/workloads/{ns}/{kind}/{name}/scale", post(scale)) .route("/workloads/{ns}/{kind}/{name}/image", post(set_image)) .route("/images/check", post(check_images)) } #[derive(Serialize, ToSchema)] pub struct OverviewResponse { #[serde(flatten)] #[schema(value_type = Object)] pub overview: ClusterOverview, pub images: Vec, } #[utoipa::path(get, path = "/api/cluster/overview", tag = "cluster", security(("bearer" = [])), responses((status = 200, body = OverviewResponse), (status = 502)))] async fn overview( State(state): State, _: AuthUser, ) -> Result, ApiError> { let overview = state.cluster.overview().await?; Ok(Json(OverviewResponse { images: overview.images(), overview, })) } fn workload_ref((ns, kind, name): (String, String, String)) -> Result { let kind = WorkloadKind::parse(&kind).ok_or(DomainError::NotFound)?; Ok(WorkloadRef { namespace: ns, kind, name, }) } #[utoipa::path(post, path = "/api/cluster/workloads/{ns}/{kind}/{name}/restart", tag = "cluster", security(("bearer" = [])), responses((status = 204), (status = 404)))] async fn restart( State(state): State, _: AdminUser, Path(p): Path<(String, String, String)>, ) -> Result { state.cluster.restart(workload_ref(p)?).await?; Ok(StatusCode::NO_CONTENT) } #[derive(Deserialize, ToSchema)] pub struct ScaleRequest { pub replicas: i32, } #[utoipa::path(post, path = "/api/cluster/workloads/{ns}/{kind}/{name}/scale", tag = "cluster", security(("bearer" = [])), request_body = ScaleRequest, responses((status = 204), (status = 422)))] async fn scale( State(state): State, _: AdminUser, Path(p): Path<(String, String, String)>, Json(req): Json, ) -> Result { state.cluster.scale(workload_ref(p)?, req.replicas).await?; Ok(StatusCode::NO_CONTENT) } #[derive(Deserialize, ToSchema)] pub struct CheckImagesRequest { /// One image, or all images running on the cluster when omitted. pub image: Option, } #[utoipa::path(post, path = "/api/cluster/images/check", tag = "cluster", security(("bearer" = [])), request_body = CheckImagesRequest, responses((status = 202, body = crate::jobs::JobRunDto), (status = 409)))] async fn check_images( State(state): State, AdminUser(admin): AdminUser, Json(req): Json, ) -> Result<(StatusCode, Json), ApiError> { let run = state .jobs .start(JobKind::ImageUpdateCheck, req.image, &admin.email) .await?; Ok((StatusCode::ACCEPTED, Json(run.into()))) } #[derive(Deserialize, ToSchema)] pub struct ImageRequest { pub image: String, pub container: Option, } #[utoipa::path(post, path = "/api/cluster/workloads/{ns}/{kind}/{name}/image", tag = "cluster", security(("bearer" = [])), request_body = ImageRequest, responses((status = 204), (status = 404), (status = 422)))] async fn set_image( State(state): State, _: AdminUser, Path(p): Path<(String, String, String)>, Json(req): Json, ) -> Result { state .cluster .set_image(workload_ref(p)?, req.container, &req.image) .await?; Ok(StatusCode::NO_CONTENT) }