kdash 1.1.1

A fast and simple dashboard for Kubernetes
//! ReplicaSet-specific troubleshooting checks.
//!
//! This module inspects cached ReplicaSet state and produces findings for
//! RSs that are in an unhealthy or noteworthy phase.
//!
//! References:
//! - <https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#replicaset-v1-apps>

use crate::app::{models::KubeResource, replicasets::KubeReplicaSet};

use super::{DisplayFinding, ResourceKind, Severity};

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn finding(
  rs: &KubeReplicaSet,
  severity: Severity,
  reason: String,
  message: String,
) -> DisplayFinding {
  DisplayFinding {
    severity,
    reason,
    resource_kind: ResourceKind::ReplicaSet,
    namespace: Some(rs.namespace.clone()),
    resource_name: rs.name.clone(),
    message,
    age: rs.age.clone(),
  }
}

/// Named replica counts from `.status` (missing fields default to 0).
struct ReplicaCounts {
  available: i32,
  fully_labeled: i32,
  ready: i32,
  replicas: i32,
}

impl ReplicaCounts {
  fn from_rs(rs: &KubeReplicaSet) -> Self {
    let status = rs.get_k8s_obj().status.as_ref();
    Self {
      available: status
        .and_then(|s| s.available_replicas)
        .unwrap_or_default(),
      fully_labeled: status
        .and_then(|s| s.fully_labeled_replicas)
        .unwrap_or_default(),
      ready: status.and_then(|s| s.ready_replicas).unwrap_or_default(),
      replicas: status.map_or(0, |s| s.replicas),
    }
  }

  fn all_equal(&self) -> bool {
    self.available == self.fully_labeled
      && self.fully_labeled == self.ready
      && self.ready == self.replicas
  }
}

// ---------------------------------------------------------------------------
// Individual RS checks
// ---------------------------------------------------------------------------

/// Flag mismatched status replica counts.
fn check_status(rs: &KubeReplicaSet) -> Option<DisplayFinding> {
  let counts = ReplicaCounts::from_rs(rs);

  if counts.all_equal() {
    return None;
  }

  Some(finding(
    rs,
    Severity::Warn,
    "Replica counts differ".into(),
    format!(
      "ReplicaSet status mismatch: available={}, fully_labeled={}, ready={}, replicas={}",
      counts.available, counts.fully_labeled, counts.ready, counts.replicas
    ),
  ))
}

// ---------------------------------------------------------------------------
// Evaluation entry point
// ---------------------------------------------------------------------------

/// Run all RS checks and collect findings.
pub fn evaluate(items: &[KubeReplicaSet]) -> Vec<DisplayFinding> {
  items.iter().filter_map(check_status).collect()
}

#[cfg(test)]
mod tests {
  use super::*;
  use k8s_openapi::api::apps::v1::{ReplicaSet, ReplicaSetSpec, ReplicaSetStatus};
  use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;

  fn build_rs(status: Option<ReplicaSetStatus>) -> KubeReplicaSet {
    let rs = ReplicaSet {
      metadata: ObjectMeta {
        name: Some("rs-1".into()),
        namespace: Some("ns-1".into()),
        ..Default::default()
      },
      spec: Some(ReplicaSetSpec {
        replicas: Some(2),
        ..Default::default()
      }),
      status,
    };

    KubeReplicaSet::from(rs)
  }

  #[test]
  fn test_rs_replica_counts_defaults() {
    let rs = build_rs(None);
    let counts = ReplicaCounts::from_rs(&rs);
    assert_eq!(
      (
        counts.available,
        counts.fully_labeled,
        counts.ready,
        counts.replicas
      ),
      (0, 0, 0, 0)
    );
  }

  #[test]
  fn test_check_rs_status_no_finding_when_equal() {
    let status = ReplicaSetStatus {
      replicas: 2,
      available_replicas: Some(2),
      fully_labeled_replicas: Some(2),
      ready_replicas: Some(2),
      ..Default::default()
    };
    let rs = build_rs(Some(status));
    assert!(check_status(&rs).is_none());
  }

  #[test]
  fn test_check_rs_status_finding_on_mismatch() {
    let status = ReplicaSetStatus {
      replicas: 2,
      available_replicas: Some(1),
      fully_labeled_replicas: Some(2),
      ready_replicas: Some(2),
      ..Default::default()
    };
    let rs = build_rs(Some(status));
    assert!(check_status(&rs).is_some());
  }
}