Files
Infrastruktur-Monitoring-Sy…/backend/crates/api/src/cluster.rs
Dennis Nemec b984cc8c9f Link image findings to their workloads and suggest a fixing image update
Container findings now name the workloads that run the image and link
into the Kubernetes view, which highlights them. An update check asks the
registry for newer tags of the same variant, scans the newest one and
records which of the open findings are gone in it. The image row then
shows the candidate tag, how many findings it fixes and how many remain,
marks those CVEs in the expanded list, and offers to roll every workload
over to it. The check runs as a job, nightly for all running images or on
demand for one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 20:19:12 +02:00

119 lines
4.0 KiB
Rust

//! /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<AppState> {
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<String>,
}
#[utoipa::path(get, path = "/api/cluster/overview", tag = "cluster", security(("bearer" = [])), responses((status = 200, body = OverviewResponse), (status = 502)))]
async fn overview(
State(state): State<AppState>,
_: AuthUser,
) -> Result<Json<OverviewResponse>, ApiError> {
let overview = state.cluster.overview().await?;
Ok(Json(OverviewResponse {
images: overview.images(),
overview,
}))
}
fn workload_ref((ns, kind, name): (String, String, String)) -> Result<WorkloadRef, DomainError> {
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<AppState>,
_: AdminUser,
Path(p): Path<(String, String, String)>,
) -> Result<StatusCode, ApiError> {
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<AppState>,
_: AdminUser,
Path(p): Path<(String, String, String)>,
Json(req): Json<ScaleRequest>,
) -> Result<StatusCode, ApiError> {
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<String>,
}
#[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<AppState>,
AdminUser(admin): AdminUser,
Json(req): Json<CheckImagesRequest>,
) -> Result<(StatusCode, Json<crate::jobs::JobRunDto>), 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<String>,
}
#[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<AppState>,
_: AdminUser,
Path(p): Path<(String, String, String)>,
Json(req): Json<ImageRequest>,
) -> Result<StatusCode, ApiError> {
state
.cluster
.set_image(workload_ref(p)?, req.container, &req.image)
.await?;
Ok(StatusCode::NO_CONTENT)
}