Skip to main content

knative_conditions/
lib.rs

1use schemars::JsonSchema;
2use serde::{Serialize, Deserialize};
3use std::fmt::Debug;
4
5/// Enums that implement [`ConditionType`] can be used to differentiate [`Condition`]
6/// and describe the state of the resource.
7pub trait ConditionType: Clone + Copy + Default + Debug + PartialEq
8where Self: 'static {
9    /// The top-level variant that determines overall readiness of the resource.
10    fn happy() -> Self;
11
12    /// Variants that must be true to consider the happy condition true.
13    fn dependents() -> &'static [Self];
14
15    /// Whether the [`ConditionType`] determines happiness.
16    fn is_terminal(&self) -> bool {
17        Self::dependents().contains(self) || *self == Self::happy()
18    }
19
20    /// A [`Condition`] severity defaults to whether it determines overall resource readiness or
21    /// not.
22    fn severity(&self) -> ConditionSeverity {
23        if self.is_terminal() {
24            ConditionSeverity::Error
25        } else {
26            ConditionSeverity::Info
27        }
28    }
29}
30
31/// Provides [`ConditionManager`] access to the [`Conditions`],
32/// and exposes control of the top-level [`Condition`].
33pub trait ConditionAccessor<C: ConditionType> {
34    /// Return the conditions of your CR status type.
35    fn conditions(&mut self) -> &mut Conditions<C>;
36
37    /// Returns a [`ConditionManager`] for more fine-grained control of [`Conditions`].
38    fn manager(&mut self) -> ConditionManager<C> {
39        ConditionManager::new(self.conditions())
40    }
41
42    /// Returns true if the resource is ready overall.
43    fn is_ready(&mut self) -> bool {
44        self.manager().is_happy()
45    }
46
47    /// Set the status of the top level condition type to false
48    fn mark_false(&mut self, reason: &str, message: Option<String>) {
49        let t = self.manager().get_top_level_condition().type_;
50        self.manager().mark_false(t, reason, message);
51    }
52
53    /// Set the status of the top level condition to unknown. Typically used when beginning the
54    /// reconciliation of a new generation.
55    fn mark_unknown(&mut self) {
56        let t = self.manager().get_top_level_condition().type_;
57        self.manager().mark_unknown(
58            t,
59            "NewObservedGenFailure",
60            Some("unsuccessfully observed a new generation".into())
61        );
62    }
63
64    fn mark_unknown_with_message(&mut self, reason: &str, message: Option<String>) {
65        let t = self.manager().get_top_level_condition().type_;
66        self.manager().mark_unknown(t, reason, message);
67    }
68}
69
70/// The state of a [`Condition`].
71#[derive(Deserialize, Serialize, Clone, Copy, Debug, JsonSchema, PartialEq)]
72pub enum ConditionStatus {
73    True,
74    False,
75    Unknown,
76}
77
78impl Default for ConditionStatus {
79    fn default() -> Self {
80        ConditionStatus::Unknown
81    }
82}
83
84#[derive(Deserialize, Serialize, Clone, Copy, Debug, JsonSchema, PartialEq)]
85#[non_exhaustive]
86/// The importance of a conditions status.
87pub enum ConditionSeverity {
88    Error,
89    Warning,
90    Info,
91}
92
93impl Default for ConditionSeverity {
94    fn default() -> Self {
95        ConditionSeverity::Error
96    }
97}
98
99impl ConditionSeverity {
100    pub fn is_err(&self) -> bool {
101        *self == ConditionSeverity::Error
102    }
103}
104
105/// A custom resource status condition.
106#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema, PartialEq)]
107pub struct Condition<C: ConditionType> {
108    #[serde(rename = "type")]
109    pub type_: C,
110    pub status: ConditionStatus,
111    /// ConditionSeverityError specifies that a failure of a condition type
112    /// should be viewed as an error.  As "Error" is the default for conditions
113    /// we use the empty string (coupled with omitempty) to avoid confusion in
114    /// the case where the condition is in state "True" (aka nothing is wrong).
115    // In rust lang we accomplish this with Error as a Default variant
116    #[serde(default)]
117    #[serde(skip_serializing_if = "ConditionSeverity::is_err")]
118    pub severity: ConditionSeverity,
119    // TODO: make this a "VolatileTime"
120    //#[serde(deserialize_with = "from_ts")]
121    pub last_transition_time: Option<chrono::DateTime<chrono::Utc>>,
122    pub reason: Option<String>,
123    pub message: Option<String>,
124}
125
126impl<C: ConditionType> Default for Condition<C> {
127    fn default() -> Condition<C> {
128        Condition {
129            type_: C::default(),
130            status: ConditionStatus::default(),
131            severity: ConditionSeverity::default(),
132            last_transition_time: Some(chrono::Utc::now()),
133            reason: None,
134            message: None
135        }
136    }
137}
138
139impl<C: ConditionType> PartialOrd for Condition<C> {
140    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
141        use ConditionStatus::*;
142        use std::cmp::Ordering;
143
144        let time_ord = match (self.last_transition_time, other.last_transition_time) {
145            (Some(left), Some(right)) => left.partial_cmp(&right),
146            _ => None
147        };
148
149        match (self.status, other.status) {
150            (False, False) | (Unknown, Unknown) | (True, True) => match time_ord {
151                Some(ord) => Some(ord),
152                None => Some(Ordering::Equal)
153            },
154            (False, _) | (Unknown, True) => Some(Ordering::Greater),
155            (Unknown, False) | (True, _) => Some(Ordering::Less),
156        }
157    }
158}
159
160impl<C: ConditionType> Condition<C> {
161    fn new(type_: C) -> Self {
162        Condition {
163            type_,
164            ..Default::default()
165        }
166    }
167
168    fn with_status(type_: C, status: ConditionStatus) -> Condition<C> {
169        Condition {
170            status,
171            ..Condition::new(type_)
172        }
173    }
174
175    fn is_true(&self) -> bool {
176        self.status == ConditionStatus::True
177    }
178
179    fn is_false(&self) -> bool {
180        self.status == ConditionStatus::False
181    }
182
183    #[allow(dead_code)]
184    fn is_unknown(&self) -> bool {
185        self.status == ConditionStatus::Unknown
186    }
187}
188
189/// A `Vec<Condition>` that maintains transition times.
190#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
191pub struct Conditions<C: ConditionType>(Vec<Condition<C>>);
192
193impl<C: ConditionType> Default for Conditions<C> {
194    fn default() -> Self {
195        let iter = [C::happy()]
196            .into_iter()
197            .chain(C::dependents().iter().cloned())
198            .map(Condition::new);
199        Conditions(Vec::from_iter(iter))
200    }
201}
202
203impl<C: ConditionType> Conditions<C> {
204    pub fn with_conditions(conditions: Vec<Condition<C>>) -> Conditions<C> {
205        assert!(
206            conditions.iter().any(|c| c.type_ == C::happy()),
207            "Conditions must be initialized with the happy ConditionType"
208        );
209        assert!(
210            conditions.iter().fold(std::collections::HashSet::new(), |mut acc, c| {
211                // insert the ConditionType as a string to avoid C: Hashable bound
212                acc.insert(format!("{:?}", c.type_));
213                acc
214            }).len() == conditions.len(),
215            "ConditionType must be unique to each Condition"
216        );
217        Conditions(conditions)
218    }
219
220    fn get_cond(&self, type_: &C) -> Option<&Condition<C>> {
221        self.0.iter().find(|c| c.type_ == *type_)
222    }
223
224    fn get_cond_mut(&mut self, type_: &C) -> Option<&mut Condition<C>> {
225        self.0.iter_mut().find(|c| c.type_ == *type_)
226    }
227
228    fn set_cond(&mut self, mut condition: Condition<C>) {
229        // The go version collects all the conditions of different type != arg
230        // into a new array, then checks if only the time has changed
231        // on the condition to set. If so it returns, otherwise
232        // it updates that single condition, re-sorts the array of conditions
233        // by Type (alphabetically?) and sets the new array as the conditions.
234        // This may be due to the "accessor" interface that we have skipped here.
235        match self.get_cond_mut(&condition.type_) {
236            Some(cond) => {
237                // Check if only the time has changed
238                let test_cond = Condition {
239                    last_transition_time: condition.last_transition_time,
240                    // OPTIMIZE: could check strings explicitly with no need for clone
241                    ..cond.clone()
242                };
243                if test_cond == condition {
244                    return
245                } else {
246                    *cond = Condition {
247                        last_transition_time: Some(chrono::Utc::now()),
248                        ..condition
249                    }
250                }
251            }
252            None => {
253                condition.last_transition_time = Some(chrono::Utc::now());
254                self.0.push(condition);
255                // TODO: sort the output...alphabetically by type name?
256            }
257        }
258    }
259
260    fn mark_true(&mut self, condition_type: C) {
261        self.set_cond(Condition::with_status(condition_type, ConditionStatus::True))
262    }
263
264    fn mark_true_with_reason(&mut self, condition_type: C, reason: String, message: Option<String>) {
265        self.set_cond(Condition {
266            reason: Some(reason),
267            message,
268            ..Condition::with_status(condition_type, ConditionStatus::True)
269        })
270    }
271
272    fn mark_false(&mut self, condition_type: C, reason: String, message: Option<String>) {
273        self.set_cond(Condition {
274            reason: Some(reason),
275            message,
276            ..Condition::with_status(condition_type, ConditionStatus::False)
277        });
278    }
279
280    fn mark_unknown(&mut self, condition_type: C, reason: String, message: Option<String>) {
281        self.set_cond(Condition {
282            reason: Some(reason),
283            message,
284            ..Condition::with_status(condition_type, ConditionStatus::Unknown)
285        });
286    }
287}
288
289/// Mutates [`Conditions`] in accordance with the condition dependency chain defined by a
290/// [`ConditionType`].
291pub struct ConditionManager<'a, C: ConditionType> {
292    conditions: &'a mut Conditions<C>,
293}
294
295impl<'a, C: ConditionType> ConditionManager<'a, C> {
296    pub fn new(conditions: &'a mut Conditions<C>) -> Self {
297        assert!(
298            !C::dependents().contains(&C::happy()),
299            "dependents may not contain happy condition"
300        );
301        ConditionManager { conditions }
302    }
303
304    pub fn get_condition(&self, condition_type: C) -> Option<&Condition<C>> {
305        self.conditions.get_cond(&condition_type)
306    }
307
308    /// Returns the happy [`Condition`].
309    ///
310    /// # Panic
311    /// Panics if the [`Conditions`] have not been properly initialized.
312    /// See [`Conditions::default()`].
313    pub fn get_top_level_condition(&self) -> &Condition<C> {
314        self.get_condition(C::happy())
315            .as_ref()
316            .expect("top level condition is initialized")
317    }
318
319    pub fn is_happy(&self) -> bool {
320        self.get_top_level_condition().is_true()
321    }
322
323    fn find_unhappy_dependent(&self) -> Option<&Condition<C>> {
324        self.conditions.0
325            .iter()
326            // Filter to non-true, terminal dependents
327            .filter(|cond| cond.type_ != C::happy() && cond.type_.is_terminal() && !cond.is_true())
328            // Return a condition, prioritizing most recent False over most recent Unknown
329            .reduce(|unhappy, cond| if cond > unhappy { cond } else { unhappy })
330    }
331
332    /// Mark the happy condition to true if all other dependents are also true.
333    fn recompute_happiness(&mut self, condition_type: &C) {
334        match self.find_unhappy_dependent() {
335            Some(dependent) => {
336                let cond = Condition {
337                    type_: C::happy(),
338                    status: dependent.status,
339                    reason: dependent.reason.clone(),
340                    message: dependent.message.clone(),
341                    severity: C::happy().severity(),
342                    ..Default::default()
343                };
344                self.conditions.set_cond(cond);
345            },
346            None => if *condition_type != C::happy() {
347                // set happy to true
348                self.conditions.set_cond(Condition {
349                    type_: C::happy(),
350                    status: ConditionStatus::True,
351                    severity: C::happy().severity(),
352                    ..Default::default()
353                })
354            }
355        }
356    }
357
358    pub fn mark_true(&mut self, condition_type: C) {
359        self.conditions.mark_true(condition_type);
360        self.recompute_happiness(&condition_type);
361    }
362
363    pub fn mark_true_with_reason(&mut self, condition_type: C, reason: &str, message: Option<String>) {
364        self.conditions.mark_true_with_reason(condition_type, reason.to_string(), message);
365        self.recompute_happiness(&condition_type);
366    }
367
368    /// Set the status of the condition type to false, as well as the happy condition if this
369    /// condition is a dependent.
370    pub fn mark_false(&mut self, condition_type: C, reason: &str, message: Option<String>) {
371        self.conditions.mark_false(condition_type, reason.to_string(), message.clone());
372
373        if C::dependents().contains(&condition_type) {
374            self.conditions.mark_false(C::happy(), reason.to_string(), message)
375        }
376    }
377
378    /// Set the status to unknown and also set the happy condition to unknown if no other dependent
379    /// condition is in an error state.
380    pub fn mark_unknown(&mut self, condition_type: C, reason: &str, message: Option<String>) {
381        self.conditions.mark_unknown(condition_type, reason.to_string(), message.clone());
382
383        // set happy condition to false if another dependent is false, otherwise set happy
384        // condition to unknown if this condition is a dependent
385        if let Some(dependent) = self.find_unhappy_dependent() {
386            if dependent.is_false() {
387                if !self.get_top_level_condition().is_false() {
388                    self.mark_false(C::happy(), reason, message);
389               }
390            }
391        } else if condition_type.is_terminal() {
392           self.conditions.mark_unknown(C::happy(), reason.to_string(), message);
393        }
394    }
395}
396
397#[cfg(test)]
398mod test {
399    use super::*;
400    use chrono::TimeZone;
401
402    #[derive(Deserialize, Copy, Clone, Debug, PartialEq)]
403    enum TestCondition {
404        Ready,
405        SinkProvided,
406        OtherCondition,
407        Unimportant
408    }
409
410    impl ConditionType for TestCondition {
411        fn happy() -> Self {
412            TestCondition::Ready
413        }
414
415        fn dependents() -> &'static [Self] {
416            &[TestCondition::SinkProvided, TestCondition::OtherCondition]
417        }
418    }
419
420    impl Default for TestCondition {
421        fn default() -> Self {
422            TestCondition::Ready
423        }
424    }
425
426    #[test]
427    fn find_unhappy_dependent_does_not_sort_vec() {
428        let dt = chrono::Utc.ymd(2022, 1, 1);
429        let mut conditions = Conditions::with_conditions(vec![
430            Condition {
431                type_: TestCondition::Ready,
432                status: ConditionStatus::False,
433                last_transition_time: Some(dt.and_hms(0, 0, 0)),
434                ..Default::default()
435            },
436            Condition {
437                type_: TestCondition::SinkProvided,
438                status: ConditionStatus::False,
439                last_transition_time: Some(dt.and_hms(3, 0, 0)),
440                ..Default::default()
441            },
442            Condition {
443                type_: TestCondition::OtherCondition,
444                status: ConditionStatus::False,
445                last_transition_time: Some(dt.and_hms(2, 0, 0)),
446                ..Default::default()
447            },
448            Condition {
449                type_: TestCondition::Unimportant,
450                status: ConditionStatus::False,
451                last_transition_time: Some(dt.and_hms(2, 0, 0)),
452                ..Default::default()
453            },
454        ]);
455
456        let manager = ConditionManager::new(&mut conditions);
457        let unhappy = manager.find_unhappy_dependent().unwrap();
458        // Returns most recent False dependent
459        assert_eq!(unhappy.type_, TestCondition::SinkProvided);
460        assert_eq!(unhappy.status, ConditionStatus::False);
461        assert_eq!(unhappy.last_transition_time.unwrap(), dt.and_hms(3, 0, 0));
462        // Maintains order
463        let mut iter = conditions.0.iter();
464        assert_eq!(iter.next().unwrap().type_, TestCondition::Ready);
465        assert_eq!(iter.next().unwrap().type_, TestCondition::SinkProvided);
466        assert_eq!(iter.next().unwrap().type_, TestCondition::OtherCondition);
467        assert_eq!(iter.next().unwrap().type_, TestCondition::Unimportant);
468    }
469
470    #[test]
471    fn condition_type_deserializes() {
472        let condition_type: TestCondition = serde_json::from_value(serde_json::json!(
473            "SinkProvided"
474        )).unwrap();
475        assert_eq!(condition_type, TestCondition::SinkProvided);
476        let condition_type: TestCondition = serde_json::from_value(serde_json::json!(
477            "Ready"
478        )).unwrap();
479        assert_eq!(condition_type, TestCondition::Ready);
480        let condition_type: Result<TestCondition, _> = serde_json::from_value(serde_json::json!(
481            "Succeeded"
482        ));
483        assert!(condition_type.is_err());
484    }
485}