WP-12: contract and failing tests for the Kubernetes overview
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / ui (push) Has been cancelled

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-02 22:29:36 +02:00
parent 684695e71a
commit 51ec216910
14 changed files with 922 additions and 0 deletions

View File

@ -0,0 +1,110 @@
//! Kubernetes cluster overview and workload actions.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum WorkloadKind {
Deployment,
StatefulSet,
DaemonSet,
}
impl WorkloadKind {
pub fn as_str(self) -> &'static str {
match self {
WorkloadKind::Deployment => "deployment",
WorkloadKind::StatefulSet => "statefulset",
WorkloadKind::DaemonSet => "daemonset",
}
}
pub fn parse(s: &str) -> Option<Self> {
[Self::Deployment, Self::StatefulSet, Self::DaemonSet]
.into_iter()
.find(|k| k.as_str() == s)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Container {
pub name: String,
pub image: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Workload {
pub namespace: String,
pub kind: WorkloadKind,
pub name: String,
pub ready: i32,
pub desired: i32,
pub containers: Vec<Container>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NodeInfo {
pub name: String,
pub version: String,
pub ready: bool,
pub os_image: String,
pub kernel: String,
pub container_runtime: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VolumeClaim {
pub namespace: String,
pub name: String,
pub capacity: String,
pub storage_class: String,
pub status: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClusterOverview {
pub nodes: Vec<NodeInfo>,
pub namespaces: Vec<String>,
pub workloads: Vec<Workload>,
pub volume_claims: Vec<VolumeClaim>,
pub fetched_at: DateTime<Utc>,
}
impl ClusterOverview {
/// Distinct images across all workloads (input for vulnerability scans).
pub fn images(&self) -> Vec<String> {
let mut v: Vec<String> = self
.workloads
.iter()
.flat_map(|w| w.containers.iter().map(|c| c.image.clone()))
.collect();
v.sort();
v.dedup();
v
}
}
/// Reference to a workload for actions.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WorkloadRef {
pub namespace: String,
pub kind: WorkloadKind,
pub name: String,
}
pub const MAX_REPLICAS: i32 = 20;
pub fn validate_k8s_name(name: &str) -> Result<(), crate::DomainError> {
let ok = !name.is_empty()
&& name.len() <= 253
&& name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '-' | '.'));
ok.then_some(())
.ok_or_else(|| crate::DomainError::Validation(format!("invalid kubernetes name: {name}")))
}
pub fn validate_image(image: &str) -> Result<(), crate::DomainError> {
let ok = !image.is_empty() && image.len() <= 512 && !image.chars().any(char::is_whitespace);
ok.then_some(())
.ok_or_else(|| crate::DomainError::Validation(format!("invalid image reference: {image}")))
}

View File

@ -1,6 +1,7 @@
//! Domain layer: entities, value objects, errors and the ports (traits) the application
//! layer depends on. No I/O here.
pub mod auth;
pub mod cluster;
pub mod error;
pub mod host;
pub mod jobs;

View File

@ -3,6 +3,7 @@ use async_trait::async_trait;
use uuid::Uuid;
use crate::auth::{AccessClaims, AuthEvent, RefreshToken};
use crate::cluster::{ClusterOverview, WorkloadRef};
use crate::host::{Inventory, OsInfo, Package};
use crate::jobs::{JobKind, JobRun, JobStatus};
use crate::settings::SmtpSettings;
@ -101,3 +102,18 @@ pub trait LineSink: Send + Sync {
pub trait HostUpdater: Send + Sync {
async fn upgrade(&self, packages: &[String], out: &dyn LineSink) -> Result<(), DomainError>;
}
#[async_trait]
pub trait ClusterGateway: Send + Sync {
async fn overview(&self) -> Result<ClusterOverview, DomainError>;
/// Rolling restart (like `kubectl rollout restart`).
async fn restart(&self, workload: &WorkloadRef) -> Result<(), DomainError>;
async fn scale(&self, workload: &WorkloadRef, replicas: i32) -> Result<(), DomainError>;
/// Set the image of one container (= application update).
async fn set_image(
&self,
workload: &WorkloadRef,
container: &str,
image: &str,
) -> Result<(), DomainError>;
}