diff --git a/CHANGELOG.md b/CHANGELOG.md index 83a9e2d87..40ba60582 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,12 @@ All notable changes to this project will be documented in this file. ### Added +- status::condition module to compute the cluster resource status ([#571]). - Helper function to build RBAC resources ([#572]). - Add `ClusterResourceApplyStrategy` to `ClusterResource` ([#573]). - Add `ClusterOperation` common struct with `reconcilation_paused` and `stopped` flags ([#573]). +[#571]: https://github.com/stackabletech/operator-rs/pull/571 [#572]: https://github.com/stackabletech/operator-rs/pull/572 [#573]: https://github.com/stackabletech/operator-rs/pull/573 diff --git a/src/lib.rs b/src/lib.rs index d7ad9e6af..73b26be83 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,7 @@ pub mod pod_utils; pub mod product_config_utils; pub mod product_logging; pub mod role_utils; +pub mod status; pub mod utils; pub mod validation; pub mod yaml; diff --git a/src/status/condition/daemonset.rs b/src/status/condition/daemonset.rs new file mode 100644 index 000000000..6bdc39b1b --- /dev/null +++ b/src/status/condition/daemonset.rs @@ -0,0 +1,173 @@ +use crate::status::condition::{ + ClusterCondition, ClusterConditionSet, ClusterConditionStatus, ClusterConditionType, + ConditionBuilder, +}; + +use k8s_openapi::api::apps::v1::DaemonSet; +use kube::ResourceExt; +use std::cmp; + +/// Default implementation to build [`ClusterCondition`]s for +/// `DaemonSet` resources. +/// +/// Currently only the `ClusterConditionType::Available` is implemented. This will be extended +/// to support all `ClusterConditionType`s in the future. +#[derive(Default)] +pub struct DaemonSetConditionBuilder { + daemon_sets: Vec, +} + +impl ConditionBuilder for DaemonSetConditionBuilder { + fn build_conditions(&self) -> ClusterConditionSet { + vec![self.available()].into() + } +} + +impl DaemonSetConditionBuilder { + pub fn add(&mut self, ds: DaemonSet) { + self.daemon_sets.push(ds); + } + + fn available(&self) -> ClusterCondition { + let mut available = ClusterConditionStatus::True; + let mut unavailable_resources = vec![]; + + for ds in &self.daemon_sets { + let current_status = Self::daemon_set_available(ds); + + if current_status != ClusterConditionStatus::True { + unavailable_resources.push(ds.name_any()) + } + + available = cmp::max(available, current_status); + } + + // We need to sort here to make sure roles and role groups are not changing position + // due to the HashMap (random order) logic. + unavailable_resources.sort(); + + let message = match available { + ClusterConditionStatus::True => { + "All DaemonSet have the requested amount of ready replicas.".to_string() + } + ClusterConditionStatus::False => { + format!("DaemonSet {unavailable_resources:?} missing ready replicas.") + } + ClusterConditionStatus::Unknown => "DaemonSet status cannot be determined.".to_string(), + }; + + ClusterCondition { + reason: None, + message: Some(message), + status: available, + type_: ClusterConditionType::Available, + last_transition_time: None, + last_update_time: None, + } + } + + /// Returns a condition "Available: True" if the number of ready Pods is greater than zero. + /// This is an heuristic that doesn't take into consideration if *all* eligible nodes have + /// running Pods. + /// Other fields of the daemon set status have been considered and discarded for being even less + /// reliable/informative. + fn daemon_set_available(ds: &DaemonSet) -> ClusterConditionStatus { + let number_ready = ds + .status + .as_ref() + .map(|status| status.number_ready) + .unwrap_or_default(); + + if number_ready > 0 { + ClusterConditionStatus::True + } else { + ClusterConditionStatus::False + } + } +} + +#[cfg(test)] +mod test { + use crate::status::condition::daemonset::DaemonSetConditionBuilder; + use crate::status::condition::{ + ClusterCondition, ClusterConditionStatus, ClusterConditionType, ConditionBuilder, + }; + use k8s_openapi::api::apps::v1::{DaemonSet, DaemonSetStatus}; + + fn build_ds(number_ready: i32) -> DaemonSet { + DaemonSet { + status: Some(DaemonSetStatus { + number_ready, + ..DaemonSetStatus::default() + }), + ..DaemonSet::default() + } + } + + #[test] + fn test_daemon_set_available_true() { + let ds = build_ds(1); + + assert_eq!( + DaemonSetConditionBuilder::daemon_set_available(&ds), + ClusterConditionStatus::True + ); + } + + #[test] + fn test_daemon_set_available_false() { + let ds = build_ds(0); + assert_eq!( + DaemonSetConditionBuilder::daemon_set_available(&ds), + ClusterConditionStatus::False + ); + } + + #[test] + fn test_daemon_set_available_condition_true() { + let mut ds_condition_builder = DaemonSetConditionBuilder::default(); + ds_condition_builder.add(build_ds(1)); + + let conditions = ds_condition_builder.build_conditions(); + + let got = conditions + .conditions + .get::(ClusterConditionType::Available.into()) + .cloned() + .unwrap() + .unwrap(); + + let expected = ClusterCondition { + type_: ClusterConditionType::Available, + status: ClusterConditionStatus::True, + ..ClusterCondition::default() + }; + + assert_eq!(got.type_, expected.type_); + assert_eq!(got.status, expected.status); + } + + #[test] + fn test_daemon_set_available_condition_false() { + let mut ds_condition_builder = DaemonSetConditionBuilder::default(); + ds_condition_builder.add(build_ds(0)); + + let conditions = ds_condition_builder.build_conditions(); + + let got = conditions + .conditions + .get::(ClusterConditionType::Available.into()) + .cloned() + .unwrap() + .unwrap(); + + let expected = ClusterCondition { + type_: ClusterConditionType::Available, + status: ClusterConditionStatus::False, + ..ClusterCondition::default() + }; + + assert_eq!(got.type_, expected.type_); + assert_eq!(got.status, expected.status); + } +} diff --git a/src/status/condition/mod.rs b/src/status/condition/mod.rs new file mode 100644 index 000000000..4a7db91ac --- /dev/null +++ b/src/status/condition/mod.rs @@ -0,0 +1,502 @@ +pub mod daemonset; +pub mod operations; +pub mod statefulset; + +use chrono::Utc; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::Time; +use schemars::{self, JsonSchema}; +use serde::{Deserialize, Serialize}; +use strum::EnumCount; + +/// A **data structure** that contains a vector of `ClusterCondition`s. +/// Should usually be implemented on the status of a `CustomResource` or the `CustomResource` itself. +pub trait HasStatusCondition { + fn conditions(&self) -> Vec; +} + +/// A **data structure** that produces a `ClusterConditionSet` containing all required +/// `ClusterCondition`s. +pub trait ConditionBuilder { + fn build_conditions(&self) -> ClusterConditionSet; +} + +/// Computes the final conditions to be set in the operator status condition field. +/// +/// # Arguments +/// +/// * `resource` - A cluster resource or status implementing [`HasStatusCondition`] in order to +/// retrieve the "current" conditions set in the cluster. This is required to compute +/// condition change and set proper update / transition times. +/// * `condition_builders` - A slice of structs implementing [`ConditionBuilder`]. This can be a +/// one of the predefined ConditionBuilders like `DaemonSetConditionBuilder` or a custom +/// implementation for special resources or different behavior. +/// +/// # Examples +/// ``` +/// use stackable_operator::status::condition::daemonset::DaemonSetConditionBuilder; +/// use stackable_operator::status::condition::statefulset::StatefulSetConditionBuilder; +/// use k8s_openapi::api::apps::v1::{DaemonSet, StatefulSet}; +/// use stackable_operator::status::condition::{ClusterCondition, ConditionBuilder, HasStatusCondition, compute_conditions}; +/// +/// struct ClusterStatus { +/// conditions: Vec +/// } +/// +/// impl HasStatusCondition for ClusterStatus { +/// fn conditions(&self) -> Vec { +/// self.conditions.clone() +/// } +/// } +/// +/// let mut daemonset_condition_builder = DaemonSetConditionBuilder::default(); +/// daemonset_condition_builder.add(DaemonSet::default()); +/// +/// let mut statefulset_condition_builder = StatefulSetConditionBuilder::default(); +/// statefulset_condition_builder.add(StatefulSet::default()); +/// +/// let old_status = ClusterStatus { +/// conditions: vec![] +/// }; +/// +/// let new_status = ClusterStatus { +/// conditions: compute_conditions(&old_status, +/// &[ +/// &daemonset_condition_builder as &dyn ConditionBuilder, +/// &statefulset_condition_builder as &dyn ConditionBuilder +/// ] +/// ) +/// }; +/// +/// ``` +pub fn compute_conditions( + resource: &T, + condition_builders: &[&dyn ConditionBuilder], +) -> Vec { + let mut new_resource_conditions = ClusterConditionSet::new(); + // compute current conditions and merge their message if required + for cb in condition_builders { + let conditions: ClusterConditionSet = cb.build_conditions(); + new_resource_conditions = new_resource_conditions.merge(conditions, update_message); + } + + let old_resource_conditions: ClusterConditionSet = resource.conditions().into(); + // merge the computed conditions and update e.g. transition timestamps if required + old_resource_conditions + .merge(new_resource_conditions, update_timestamps) + .into() +} + +#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClusterCondition { + #[serde(skip_serializing_if = "Option::is_none")] + /// Last time the condition transitioned from one status to another. + pub last_transition_time: Option