use serde::{Deserialize, Serialize};
use crate::models::k8s::{K8sMetadata, K8sPodTemplate};
use crate::models::validation::{ConfigValidator, Diagnostic, Severity, YamlType};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HPAMetricTarget {
#[serde(default, rename = "type")]
pub target_type: Option<String>,
#[serde(default, rename = "averageUtilization")]
pub average_utilization: Option<u32>,
#[serde(default, rename = "averageValue")]
pub average_value: Option<String>,
#[serde(default)]
pub value: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HPAMetricResource {
pub name: String,
pub target: HPAMetricTarget,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HPAMetric {
#[serde(rename = "type")]
pub metric_type: String,
#[serde(default)]
pub resource: Option<HPAMetricResource>,
#[serde(default)]
pub pods: Option<serde_json::Value>,
#[serde(default)]
pub object: Option<serde_json::Value>,
#[serde(default)]
pub external: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HPAScaleTargetRef {
#[serde(rename = "apiVersion")]
pub api_version: Option<String>,
pub kind: String,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HPABehaviorPolicy {
#[serde(rename = "type")]
pub policy_type: String,
pub value: u32,
#[serde(rename = "periodSeconds")]
pub period_seconds: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HPABehaviorRules {
#[serde(default, rename = "stabilizationWindowSeconds")]
pub stabilization_window_seconds: Option<u32>,
#[serde(default)]
pub policies: Vec<HPABehaviorPolicy>,
#[serde(default, rename = "selectPolicy")]
pub select_policy: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HPABehavior {
#[serde(default, rename = "scaleUp")]
pub scale_up: Option<HPABehaviorRules>,
#[serde(default, rename = "scaleDown")]
pub scale_down: Option<HPABehaviorRules>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HPASpec {
#[serde(rename = "scaleTargetRef")]
pub scale_target_ref: HPAScaleTargetRef,
#[serde(rename = "minReplicas")]
pub min_replicas: Option<u32>,
#[serde(rename = "maxReplicas")]
pub max_replicas: u32,
#[serde(default)]
pub metrics: Vec<HPAMetric>,
#[serde(default)]
pub behavior: Option<HPABehavior>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sHPA {
#[serde(rename = "apiVersion")]
pub api_version: String,
pub kind: String,
pub metadata: K8sMetadata,
pub spec: HPASpec,
}
impl ConfigValidator for K8sHPA {
fn yaml_type(&self) -> YamlType { YamlType::K8sHPA }
fn validate_structure(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
if let Some(min) = self.spec.min_replicas
&& min > self.spec.max_replicas {
diags.push(Diagnostic {
severity: Severity::Error,
message: format!("minReplicas ({}) > maxReplicas ({})", min, self.spec.max_replicas),
path: Some("spec".into()),
});
}
if self.spec.max_replicas == 0 {
diags.push(Diagnostic {
severity: Severity::Error,
message: "maxReplicas is 0 — HPA will not create any pods".into(),
path: Some("spec > maxReplicas".into()),
});
}
diags
}
fn validate_semantics(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
if self.spec.metrics.is_empty() {
diags.push(Diagnostic {
severity: Severity::Warning,
message: "No metrics defined — HPA will default to CPU at 80%".into(),
path: Some("spec > metrics".into()),
});
}
if self.spec.min_replicas == Some(0) {
diags.push(Diagnostic {
severity: Severity::Warning,
message: "minReplicas=0 — scale-to-zero requires KEDA or custom setup".into(),
path: Some("spec > minReplicas".into()),
});
}
if self.spec.max_replicas > 100 {
diags.push(Diagnostic {
severity: Severity::Info,
message: format!("maxReplicas={} is unusually high", self.spec.max_replicas),
path: Some("spec > maxReplicas".into()),
});
}
diags
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CronJobJobTemplate {
#[serde(default)]
pub metadata: Option<K8sMetadata>,
pub spec: JobSpec,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CronJobSpec {
pub schedule: String,
#[serde(default, rename = "timeZone")]
pub time_zone: Option<String>,
#[serde(default, rename = "concurrencyPolicy")]
pub concurrency_policy: Option<String>,
#[serde(default, rename = "suspend")]
pub suspend: Option<bool>,
#[serde(default, rename = "successfulJobsHistoryLimit")]
pub successful_jobs_history_limit: Option<u32>,
#[serde(default, rename = "failedJobsHistoryLimit")]
pub failed_jobs_history_limit: Option<u32>,
#[serde(default, rename = "startingDeadlineSeconds")]
pub starting_deadline_seconds: Option<u64>,
#[serde(rename = "jobTemplate")]
pub job_template: CronJobJobTemplate,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sCronJob {
#[serde(rename = "apiVersion")]
pub api_version: String,
pub kind: String,
pub metadata: K8sMetadata,
pub spec: CronJobSpec,
}
fn is_valid_cron(expr: &str) -> bool {
let parts: Vec<&str> = expr.split_whitespace().collect();
parts.len() == 5
}
impl ConfigValidator for K8sCronJob {
fn yaml_type(&self) -> YamlType { YamlType::K8sCronJob }
fn validate_structure(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
if !is_valid_cron(&self.spec.schedule) {
diags.push(Diagnostic {
severity: Severity::Error,
message: format!("Invalid cron schedule '{}' — expected 5 fields (min hour dom month dow)", self.spec.schedule),
path: Some("spec > schedule".into()),
});
}
diags
}
fn validate_semantics(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
if let Some(policy) = &self.spec.concurrency_policy
&& !["Allow", "Forbid", "Replace"].contains(&policy.as_str()) {
diags.push(Diagnostic {
severity: Severity::Warning,
message: format!("Unknown concurrencyPolicy '{}' — expected Allow/Forbid/Replace", policy),
path: Some("spec > concurrencyPolicy".into()),
});
}
if self.spec.suspend == Some(true) {
diags.push(Diagnostic {
severity: Severity::Info,
message: "CronJob is suspended — no jobs will be created".into(),
path: Some("spec > suspend".into()),
});
}
if self.spec.schedule.starts_with("* ") || self.spec.schedule.starts_with("*/1 ") {
diags.push(Diagnostic {
severity: Severity::Warning,
message: "CronJob runs every minute — ensure this is intentional".into(),
path: Some("spec > schedule".into()),
});
}
diags
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct JobSpec {
pub template: K8sPodTemplate,
#[serde(default, rename = "backoffLimit")]
pub backoff_limit: Option<u32>,
#[serde(default)]
pub completions: Option<u32>,
#[serde(default)]
pub parallelism: Option<u32>,
#[serde(default, rename = "activeDeadlineSeconds")]
pub active_deadline_seconds: Option<u64>,
#[serde(default, rename = "ttlSecondsAfterFinished")]
pub ttl_seconds_after_finished: Option<u64>,
#[serde(default)]
pub selector: Option<serde_json::Value>,
#[serde(default, rename = "completionMode")]
pub completion_mode: Option<String>,
#[serde(default, rename = "manualSelector")]
pub manual_selector: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sJob {
#[serde(rename = "apiVersion")]
pub api_version: String,
pub kind: String,
pub metadata: K8sMetadata,
pub spec: JobSpec,
}
impl ConfigValidator for K8sJob {
fn yaml_type(&self) -> YamlType { YamlType::K8sJob }
fn validate_structure(&self) -> Vec<Diagnostic> {
vec![]
}
fn validate_semantics(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
if let Some(limit) = self.spec.backoff_limit
&& limit > 10 {
diags.push(Diagnostic {
severity: Severity::Warning,
message: format!("backoffLimit={} is high — job will retry many times before giving up", limit),
path: Some("spec > backoffLimit".into()),
});
}
if self.spec.active_deadline_seconds.is_none() {
diags.push(Diagnostic {
severity: Severity::Info,
message: "No activeDeadlineSeconds — job may run indefinitely".into(),
path: Some("spec > activeDeadlineSeconds".into()),
});
}
let restart = self.spec.template.spec.restart_policy.as_deref().unwrap_or("Always");
if restart == "Always" {
diags.push(Diagnostic {
severity: Severity::Warning,
message: "Job template has restartPolicy=Always — should be 'Never' or 'OnFailure'".into(),
path: Some("spec > template > spec > restartPolicy".into()),
});
}
diags
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct StatefulSetSpec {
#[serde(default)]
pub replicas: Option<u32>,
#[serde(rename = "serviceName")]
pub service_name: String,
pub selector: serde_json::Value,
pub template: K8sPodTemplate,
#[serde(default, rename = "volumeClaimTemplates")]
pub volume_claim_templates: Vec<serde_json::Value>,
#[serde(default, rename = "updateStrategy")]
pub update_strategy: Option<serde_json::Value>,
#[serde(default, rename = "podManagementPolicy")]
pub pod_management_policy: Option<String>,
#[serde(default, rename = "revisionHistoryLimit")]
pub revision_history_limit: Option<u32>,
#[serde(default, rename = "minReadySeconds")]
pub min_ready_seconds: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sStatefulSet {
#[serde(rename = "apiVersion")]
pub api_version: String,
pub kind: String,
pub metadata: K8sMetadata,
pub spec: StatefulSetSpec,
}
impl ConfigValidator for K8sStatefulSet {
fn yaml_type(&self) -> YamlType { YamlType::K8sStatefulSet }
fn validate_structure(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
if self.spec.service_name.is_empty() {
diags.push(Diagnostic {
severity: Severity::Error,
message: "serviceName is required for StatefulSet".into(),
path: Some("spec > serviceName".into()),
});
}
diags
}
fn validate_semantics(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
if self.spec.volume_claim_templates.is_empty() {
diags.push(Diagnostic {
severity: Severity::Info,
message: "No volumeClaimTemplates — StatefulSet pods will have no persistent storage".into(),
path: Some("spec > volumeClaimTemplates".into()),
});
}
if self.spec.replicas == Some(0) {
diags.push(Diagnostic {
severity: Severity::Warning,
message: "replicas=0 — no pods will be created".into(),
path: Some("spec > replicas".into()),
});
}
diags
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DaemonSetSpec {
pub selector: serde_json::Value,
pub template: K8sPodTemplate,
#[serde(default, rename = "updateStrategy")]
pub update_strategy: Option<serde_json::Value>,
#[serde(default, rename = "revisionHistoryLimit")]
pub revision_history_limit: Option<u32>,
#[serde(default, rename = "minReadySeconds")]
pub min_ready_seconds: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sDaemonSet {
#[serde(rename = "apiVersion")]
pub api_version: String,
pub kind: String,
pub metadata: K8sMetadata,
pub spec: DaemonSetSpec,
}
impl ConfigValidator for K8sDaemonSet {
fn yaml_type(&self) -> YamlType { YamlType::K8sDaemonSet }
fn validate_structure(&self) -> Vec<Diagnostic> {
vec![]
}
fn validate_semantics(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
let tolerations = self.spec.template.spec.tolerations.as_ref();
if tolerations.is_none() || tolerations.map(|t| t.is_null()).unwrap_or(true) {
diags.push(Diagnostic {
severity: Severity::Info,
message: "No tolerations — DaemonSet won't schedule on tainted nodes (including control-plane)".into(),
path: Some("spec > template > spec > tolerations".into()),
});
}
diags
}
}