From dd46358638ab3ed20cffffdacef0f5d0b46eef21 Mon Sep 17 00:00:00 2001 From: GatewayJ <18332154+GatewayJ@users.noreply.github.com> Date: Sun, 28 Jun 2026 01:22:53 +0800 Subject: [PATCH] fix(console): handle stale progressing tenant status --- src/console/models/tenant.rs | 85 +++++++++++++++++++++++++++++++++--- src/status.rs | 39 +++++++++++++++++ src/types/v1alpha1/status.rs | 6 ++- 3 files changed, 123 insertions(+), 7 deletions(-) diff --git a/src/console/models/tenant.rs b/src/console/models/tenant.rs index 7f794b2..3cf7a76 100755 --- a/src/console/models/tenant.rs +++ b/src/console/models/tenant.rs @@ -25,6 +25,8 @@ use kube::ResourceExt; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +const LEGACY_PROGRESSING_CONDITION: &str = "Progressing"; + /// Single tenant row in a list view #[derive(Debug, Serialize, ToSchema)] pub struct TenantListItem { @@ -267,22 +269,43 @@ pub fn tenant_status_summary(tenant: &Tenant) -> TenantStatusSummary { } else { legacy_ready_for_state(¤t_state) }; - let mut reconciling = if has_conditions { - condition_is_true(status, ConditionType::Reconciling.as_str()) - || condition_is_true(status, "Progressing") - } else { - legacy_reconciling_for_state(¤t_state) - }; let degraded = if has_conditions { condition_is_true(status, ConditionType::Degraded.as_str()) } else { legacy_degraded_for_state(¤t_state) }; + let legacy_progressing_condition = if has_conditions && !ready && !degraded { + status.and_then(|status| { + status + .condition_by_type(LEGACY_PROGRESSING_CONDITION) + .filter(|condition| condition.status == ConditionStatus::True.as_str()) + }) + } else { + None + }; + let legacy_progressing = legacy_progressing_condition.is_some(); + + if legacy_progressing && matches!(current_state.as_str(), "Unknown" | "NotReady") { + current_state = CurrentState::Reconciling.as_str().to_string(); + } + + let mut reconciling = if has_conditions { + condition_is_true(status, ConditionType::Reconciling.as_str()) || legacy_progressing + } else { + legacy_reconciling_for_state(¤t_state) + }; let primary = status.and_then(primary_condition); let mut primary_reason = primary.map(|condition| condition.reason.clone()); let mut primary_message = primary.map(|condition| condition.message.clone()); + if primary_reason.is_none() + && let Some(condition) = legacy_progressing_condition + { + primary_reason = Some(condition.reason.clone()); + primary_message = Some(condition.message.clone()); + } + if stale { current_state = CurrentState::Reconciling.as_str().to_string(); ready = false; @@ -533,6 +556,10 @@ mod tests { assert!(summary.ready); assert!(!summary.reconciling); assert!(!summary.degraded); + assert!(summary.primary_reason.is_none()); + assert!(summary.next_actions.is_empty()); + assert!(summary.primary_reason.is_none()); + assert!(summary.next_actions.is_empty()); } #[test] @@ -551,6 +578,52 @@ mod tests { assert_eq!(summary.current_state, "Reconciling"); assert!(!summary.ready); assert!(summary.reconciling); + assert_eq!(summary.primary_reason.as_deref(), Some("RolloutInProgress")); + assert_eq!(summary.next_actions, vec!["waitForRollout"]); + } + + #[test] + fn tenant_summary_ignores_legacy_progressing_after_ready_success() { + let mut tenant = crate::tests::create_test_tenant(None, None); + tenant.metadata.generation = Some(3); + tenant.status = Some(Status { + current_state: "Ready".to_string(), + observed_generation: Some(3), + conditions: vec![ + condition("Ready", "True", "ReconcileSucceeded"), + condition("Reconciling", "False", "ReconcileSucceeded"), + condition("Degraded", "False", "ReconcileSucceeded"), + condition("Progressing", "True", "RolloutInProgress"), + ], + ..Default::default() + }); + + let summary = tenant_status_summary(&tenant); + + assert_eq!(summary.current_state, "Ready"); + assert!(summary.ready); + assert!(!summary.reconciling); + assert!(!summary.degraded); + } + + #[test] + fn tenant_summary_uses_legacy_progressing_without_ready_success() { + let mut tenant = crate::tests::create_test_tenant(None, None); + tenant.metadata.generation = Some(3); + tenant.status = Some(Status { + current_state: "Ready".to_string(), + observed_generation: Some(3), + conditions: vec![condition("Progressing", "True", "RolloutInProgress")], + ..Default::default() + }); + + let summary = tenant_status_summary(&tenant); + + assert_eq!(summary.current_state, "Reconciling"); + assert!(!summary.ready); + assert!(summary.reconciling); + assert_eq!(summary.primary_reason.as_deref(), Some("RolloutInProgress")); + assert_eq!(summary.next_actions, vec!["waitForRollout"]); } #[test] diff --git a/src/status.rs b/src/status.rs index 3df8a0a..c444b8c 100644 --- a/src/status.rs +++ b/src/status.rs @@ -21,6 +21,8 @@ use crate::types::v1alpha1::status::{ use crate::types::v1alpha1::tenant::Tenant; use kube::runtime::events::EventType; +const LEGACY_PROGRESSING_CONDITION: &str = "Progressing"; + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum StatusImpact { UserBlocked, @@ -510,6 +512,8 @@ impl StatusBuilder { } pub fn build(mut self) -> Status { + self.next + .remove_condition_by_type(LEGACY_PROGRESSING_CONDITION); self.next.observed_generation = self.generation; self.next.current_state = summarize_current_state(&self.next); self.next.sort_conditions(); @@ -913,6 +917,41 @@ mod tests { ); } + #[test] + fn status_builder_prunes_legacy_progressing_condition() { + let mut tenant = crate::tests::create_test_tenant(None, None); + tenant.metadata.generation = Some(1); + tenant.status = Some(Status { + current_state: "Ready".to_string(), + observed_generation: Some(1), + conditions: vec![ + condition("Ready", "True", "ReconcileSucceeded"), + condition("Reconciling", "False", "ReconcileSucceeded"), + condition("Degraded", "False", "ReconcileSucceeded"), + condition("Progressing", "True", "RolloutInProgress"), + ], + ..Default::default() + }); + + let mut builder = StatusBuilder::from_tenant(&tenant); + builder.finish_success(); + let status = builder.build(); + + assert_eq!(status.current_state, "Ready"); + assert!( + status + .conditions + .iter() + .all(|condition| condition.type_ != "Progressing") + ); + assert_eq!( + status + .condition(ConditionType::Reconciling) + .map(|condition| condition.status.as_str()), + Some("False") + ); + } + #[test] fn transient_error_does_not_keep_previous_blocked_condition_current() { let mut tenant = crate::tests::create_test_tenant(None, None); diff --git a/src/types/v1alpha1/status.rs b/src/types/v1alpha1/status.rs index 56b18f3..1abb72d 100755 --- a/src/types/v1alpha1/status.rs +++ b/src/types/v1alpha1/status.rs @@ -245,7 +245,7 @@ pub struct ConditionInput { #[derive(Deserialize, Serialize, Clone, Debug, JsonSchema, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Condition { - /// Type of condition (Ready, Progressing, Degraded) + /// Type of condition (Ready, Reconciling, Degraded) #[serde(rename = "type")] pub type_: String, @@ -346,6 +346,10 @@ impl Status { }); } + pub fn remove_condition_by_type(&mut self, type_: &str) { + self.conditions.retain(|condition| condition.type_ != type_); + } + pub fn condition_is_true(&self, type_: ConditionType) -> bool { self.condition(type_) .is_some_and(|condition| condition.status == ConditionStatus::True.as_str())