WP-12: contract and failing tests for the Kubernetes overview
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
110
backend/crates/domain/src/cluster.rs
Normal file
110
backend/crates/domain/src/cluster.rs
Normal 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}")))
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user