drogue_client/core/v1/
mod.rs

1use crate::{Dialect, Section};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use std::ops::{Deref, DerefMut};
5
6pub const CONDITION_READY: &str = "Ready";
7
8#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
9#[serde(rename_all = "camelCase")]
10pub struct Condition {
11    pub last_transition_time: DateTime<Utc>,
12    #[serde(default, skip_serializing_if = "is_empty")]
13    pub message: Option<String>,
14    #[serde(default, skip_serializing_if = "is_empty")]
15    pub reason: Option<String>,
16    #[serde(default = "default_condition_status")]
17    pub status: String,
18    pub r#type: String,
19}
20
21fn is_empty(value: &Option<String>) -> bool {
22    match value {
23        None => true,
24        Some(str) if str.is_empty() => true,
25        _ => false,
26    }
27}
28
29#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
30pub struct Conditions(pub Vec<Condition>);
31
32fn default_condition_status() -> String {
33    "Unknown".into()
34}
35
36impl Dialect for Conditions {
37    fn key() -> &'static str {
38        "conditions"
39    }
40
41    fn section() -> Section {
42        Section::Status
43    }
44}
45
46#[derive(Clone, Debug, Default)]
47pub struct ConditionStatus {
48    pub status: Option<bool>,
49    pub reason: Option<String>,
50    pub message: Option<String>,
51}
52
53impl From<Option<bool>> for ConditionStatus {
54    fn from(value: Option<bool>) -> Self {
55        Self {
56            status: value,
57            ..Default::default()
58        }
59    }
60}
61
62impl From<bool> for ConditionStatus {
63    fn from(value: bool) -> Self {
64        Self {
65            status: Some(value),
66            ..Default::default()
67        }
68    }
69}
70
71impl Conditions {
72    fn make_status(status: Option<bool>) -> String {
73        match status {
74            Some(true) => "True",
75            Some(false) => "False",
76            None => "Unknown",
77        }
78        .into()
79    }
80
81    pub fn update<T, S>(&mut self, r#type: T, status: S)
82    where
83        T: AsRef<str>,
84        S: Into<ConditionStatus>,
85    {
86        let status = status.into();
87        let str_status = Self::make_status(status.status);
88
89        for mut condition in &mut self.0 {
90            if condition.r#type == r#type.as_ref() {
91                if condition.status != str_status {
92                    condition.last_transition_time = Utc::now();
93                    condition.status = str_status;
94                }
95                condition.reason = status.reason;
96                condition.message = status.message;
97
98                return;
99            }
100        }
101
102        // did not find entry so far
103
104        self.0.push(Condition {
105            last_transition_time: Utc::now(),
106            message: status.message,
107            reason: status.reason,
108            status: str_status,
109            r#type: r#type.as_ref().into(),
110        });
111    }
112
113    /// Aggregate the "Ready" condition.
114    pub fn aggregate_ready(mut self) -> Self {
115        let mut ready = true;
116        for condition in &self.0 {
117            if condition.r#type == CONDITION_READY {
118                continue;
119            }
120
121            if condition.status != "True" {
122                ready = false;
123                break;
124            }
125        }
126
127        self.update(
128            CONDITION_READY,
129            match ready {
130                true => ConditionStatus {
131                    status: Some(true),
132                    reason: None,
133                    message: None,
134                },
135                false => ConditionStatus {
136                    status: Some(false),
137                    reason: Some("NonReadyConditions".into()),
138                    message: None,
139                },
140            },
141        );
142        self
143    }
144
145    /// Clear the provided condition and re-aggregate the ready state.
146    pub fn clear_ready<T>(mut self, r#type: T) -> Self
147    where
148        T: AsRef<str>,
149    {
150        let r#type = r#type.as_ref();
151        self.0.retain(|c| c.r#type != r#type);
152        self.aggregate_ready()
153    }
154}
155
156impl Deref for Conditions {
157    type Target = Vec<Condition>;
158
159    fn deref(&self) -> &Self::Target {
160        &self.0
161    }
162}
163
164impl DerefMut for Conditions {
165    fn deref_mut(&mut self) -> &mut Self::Target {
166        &mut self.0
167    }
168}
169
170#[cfg(test)]
171mod test {
172
173    use super::*;
174
175    #[test]
176    fn insert_cond_1() {
177        let mut conditions = Conditions::default();
178
179        conditions.update(
180            "KafkaReady",
181            ConditionStatus {
182                status: Some(true),
183                ..Default::default()
184            },
185        );
186
187        let now = Utc::now();
188        conditions.0[0].last_transition_time = now;
189
190        assert_eq!(
191            conditions.0,
192            vec![Condition {
193                last_transition_time: now,
194                message: None,
195                reason: None,
196                status: "True".to_string(),
197                r#type: "KafkaReady".to_string()
198            }]
199        );
200    }
201
202    #[test]
203    fn update_cond_1() {
204        let mut conditions = Conditions::default();
205
206        // create two conditions
207
208        conditions.update(
209            "KafkaReady",
210            ConditionStatus {
211                status: Some(true),
212                ..Default::default()
213            },
214        );
215        conditions.update(
216            "FooBarReady",
217            ConditionStatus {
218                status: None,
219                ..Default::default()
220            },
221        );
222
223        // reset timestamps to known values
224        let now = Utc::now();
225        conditions.0[0].last_transition_time = now;
226        conditions.0[1].last_transition_time = now;
227
228        assert_eq!(
229            conditions.0,
230            vec![
231                Condition {
232                    last_transition_time: now,
233                    message: None,
234                    reason: None,
235                    status: "True".to_string(),
236                    r#type: "KafkaReady".to_string()
237                },
238                Condition {
239                    last_transition_time: now,
240                    message: None,
241                    reason: None,
242                    status: "Unknown".to_string(),
243                    r#type: "FooBarReady".to_string()
244                }
245            ]
246        );
247
248        conditions.update(
249            "FooBarReady",
250            ConditionStatus {
251                status: Some(true),
252                message: Some("All systems are ready to go".into()),
253                reason: Some("AllSystemsGo".into()),
254                ..Default::default()
255            },
256        );
257
258        // the second timestamp should be different now
259        assert_eq!(conditions.0[0].last_transition_time, now);
260        assert_ne!(conditions.0[1].last_transition_time, now);
261
262        // reset timestamps to known values
263        let now = Utc::now();
264        conditions.0[0].last_transition_time = now;
265        conditions.0[1].last_transition_time = now;
266
267        assert_eq!(
268            conditions.0,
269            vec![
270                Condition {
271                    last_transition_time: now,
272                    message: None,
273                    reason: None,
274                    status: "True".to_string(),
275                    r#type: "KafkaReady".to_string()
276                },
277                Condition {
278                    last_transition_time: now,
279                    message: Some("All systems are ready to go".into()),
280                    reason: Some("AllSystemsGo".into()),
281                    status: "True".to_string(),
282                    r#type: "FooBarReady".to_string()
283                }
284            ]
285        );
286    }
287
288    #[test]
289    fn serialize() {
290        let json = serde_json::to_string(&Conditions(vec![Condition {
291            last_transition_time: DateTime::parse_from_rfc3339("2001-02-03T12:34:56Z")
292                .expect("Valid timestmap")
293                .with_timezone(&Utc),
294            message: None,
295            reason: None,
296            status: "True".to_string(),
297            r#type: "Ready".to_string(),
298        }]))
299        .expect("Serialize to JSON");
300        assert_eq!(
301            json,
302            r#"[{"lastTransitionTime":"2001-02-03T12:34:56Z","status":"True","type":"Ready"}]"#
303        );
304    }
305
306    #[test]
307    fn conversions() {
308        let mut conditions = Conditions::default();
309        conditions.update(
310            "Foo",
311            ConditionStatus {
312                status: None,
313                reason: None,
314                message: None,
315            },
316        );
317        conditions.update("Foo", true);
318        conditions.update("Bar", Some(true));
319    }
320
321    #[test]
322    fn clear_ready() {
323        let mut conditions = Conditions::default();
324        conditions.update(
325            "SomeReady",
326            ConditionStatus {
327                status: None,
328                reason: None,
329                message: None,
330            },
331        );
332        conditions = conditions.aggregate_ready();
333        assert_eq!(conditions.len(), 2);
334        conditions = conditions.clear_ready("SomeReady");
335        assert_eq!(conditions.len(), 1);
336        let c = conditions.get(0).unwrap();
337        assert_eq!(c.r#type, "Ready");
338        assert_eq!(c.status, "True");
339    }
340}