WP-12: Kubernetes overview and workload actions
kube-rs gateway (nodes, namespaces, deployments/statefulsets/daemonsets with images, PVCs; rollout restart, scale, set image via patches) using the microk8s kubeconfig by default, fake gateway for dev, /api/cluster routes, Kubernetes page with node cards, workload table with admin actions and PVC list. Also implements the streaming command runner that WP-11 relied on. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
96
backend/crates/api/src/cluster.rs
Normal file
96
backend/crates/api/src/cluster.rs
Normal file
@ -0,0 +1,96 @@
|
||||
//! /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::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))
|
||||
}
|
||||
|
||||
#[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 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)
|
||||
}
|
||||
Reference in New Issue
Block a user