controller/cloudnativepg/placement/
cnpg_node_affinity.rs

1use crate::cloudnativepg::clusters::{
2    ClusterAffinityNodeAffinity,
3    ClusterAffinityNodeAffinityPreferredDuringSchedulingIgnoredDuringExecutionPreferenceMatchExpressions,
4    ClusterAffinityNodeAffinityPreferredDuringSchedulingIgnoredDuringExecutionPreferenceMatchFields,
5    ClusterAffinityNodeAffinityRequiredDuringSchedulingIgnoredDuringExecutionNodeSelectorTermsMatchExpressions,
6    ClusterAffinityNodeAffinityRequiredDuringSchedulingIgnoredDuringExecutionNodeSelectorTermsMatchFields,
7};
8use crate::cloudnativepg::poolers::{
9    PoolerTemplateSpecAffinityNodeAffinity,
10    PoolerTemplateSpecAffinityNodeAffinityPreferredDuringSchedulingIgnoredDuringExecution,
11    PoolerTemplateSpecAffinityNodeAffinityPreferredDuringSchedulingIgnoredDuringExecutionPreference,
12    PoolerTemplateSpecAffinityNodeAffinityPreferredDuringSchedulingIgnoredDuringExecutionPreferenceMatchExpressions,
13    PoolerTemplateSpecAffinityNodeAffinityPreferredDuringSchedulingIgnoredDuringExecutionPreferenceMatchFields,
14    PoolerTemplateSpecAffinityNodeAffinityRequiredDuringSchedulingIgnoredDuringExecution,
15    PoolerTemplateSpecAffinityNodeAffinityRequiredDuringSchedulingIgnoredDuringExecutionNodeSelectorTerms,
16    PoolerTemplateSpecAffinityNodeAffinityRequiredDuringSchedulingIgnoredDuringExecutionNodeSelectorTermsMatchExpressions,
17    PoolerTemplateSpecAffinityNodeAffinityRequiredDuringSchedulingIgnoredDuringExecutionNodeSelectorTermsMatchFields,
18};
19use k8s_openapi::api::core::v1::{
20    NodeAffinity, NodeSelector, NodeSelectorRequirement, NodeSelectorTerm, PreferredSchedulingTerm,
21};
22
23// Start of functions needed to convert a ClusterAffinityNodeAffinity to a k8s_openapi::api::core::v1::NodeAffinity
24//
25// convert_node_affinity converts a ClusterAffinityNodeAffinity to a k8s_openapi::api::core::v1::NodeAffinity
26// this is the meta function that calls the other conversion functions
27pub fn convert_node_affinity(ca: &ClusterAffinityNodeAffinity) -> NodeAffinity {
28    NodeAffinity {
29        preferred_during_scheduling_ignored_during_execution: Some(convert_preferred(ca)),
30        required_during_scheduling_ignored_during_execution: convert_required(ca),
31    }
32}
33
34// convert_preferred converts a ClusterAffinityNodeAffinity to a Vec<PreferredSchedulingTerm>
35fn convert_preferred(ca: &ClusterAffinityNodeAffinity) -> Vec<PreferredSchedulingTerm> {
36    match &ca.preferred_during_scheduling_ignored_during_execution {
37        Some(prefs) => prefs
38            .iter()
39            .map(|pref| PreferredSchedulingTerm {
40                weight: pref.weight,
41                preference: NodeSelectorTerm {
42                    match_expressions: Some(convert_match_expressions(
43                        &pref.preference.match_expressions,
44                    )),
45                    match_fields: Some(convert_match_fields(&pref.preference.match_fields)),
46                },
47            })
48            .collect(),
49        None => Vec::new(),
50    }
51}
52// convert_required converts a ClusterAffinityNodeAffinity to an Option<NodeSelector>
53fn convert_required(ca: &ClusterAffinityNodeAffinity) -> Option<NodeSelector> {
54    ca.required_during_scheduling_ignored_during_execution
55        .as_ref()
56        .map(|req| NodeSelector {
57            node_selector_terms: req
58                .node_selector_terms
59                .iter()
60                .map(|term| NodeSelectorTerm {
61                    match_expressions: convert_required_match_expressions(&term.match_expressions),
62                    match_fields: convert_required_match_fields(&term.match_fields),
63                })
64                .collect(),
65        })
66}
67
68// Convert match expressions safely without assuming defaults
69fn convert_required_match_expressions(
70    expressions: &Option<Vec<ClusterAffinityNodeAffinityRequiredDuringSchedulingIgnoredDuringExecutionNodeSelectorTermsMatchExpressions>>,
71) -> Option<Vec<NodeSelectorRequirement>> {
72    expressions.as_ref().map(|exprs| {
73        exprs
74            .iter()
75            .map(|expr| {
76                NodeSelectorRequirement {
77                    key: expr.key.clone(),
78                    operator: expr.operator.clone(),
79                    values: expr.values.as_ref().cloned(), // Use cloned to safely handle the Option
80                }
81            })
82            .collect()
83    })
84}
85
86// Convert match fields safely without assuming defaults
87fn convert_required_match_fields(
88    fields: &Option<Vec<ClusterAffinityNodeAffinityRequiredDuringSchedulingIgnoredDuringExecutionNodeSelectorTermsMatchFields>>,
89) -> Option<Vec<NodeSelectorRequirement>> {
90    fields.as_ref().map(|flds| {
91        flds.iter()
92            .map(|field| {
93                NodeSelectorRequirement {
94                    key: field.key.clone(),
95                    operator: field.operator.clone(),
96                    values: field.values.as_ref().cloned(), // Use cloned to safely handle the Option
97                }
98            })
99            .collect()
100    })
101}
102
103// convert_match_expressions converts a ClusterAffinityNodeAffinityPreferredDuringSchedulingIgnoredDuringExecutionPreferenceMatchExpressions to a Vec<NodeSelectorRequirement>
104fn convert_match_expressions(
105    expressions: &Option<Vec<ClusterAffinityNodeAffinityPreferredDuringSchedulingIgnoredDuringExecutionPreferenceMatchExpressions>>,
106) -> Vec<NodeSelectorRequirement> {
107    expressions.as_ref().map_or(Vec::new(), |exprs| {
108        exprs
109            .iter()
110            .map(|expr| NodeSelectorRequirement {
111                key: expr.key.clone(),
112                operator: expr.operator.clone(),
113                values: Some(
114                    expr.values
115                        .as_ref()
116                        .map_or_else(Vec::new, |vals| vals.clone()),
117                ),
118            })
119            .collect()
120    })
121}
122
123// convert_match_fields converts a ClusterAffinityNodeAffinityPreferredDuringSchedulingIgnoredDuringExecutionPreferenceMatchFields to a Vec<NodeSelectorRequirement>
124fn convert_match_fields(
125    fields: &Option<Vec<ClusterAffinityNodeAffinityPreferredDuringSchedulingIgnoredDuringExecutionPreferenceMatchFields>>,
126) -> Vec<NodeSelectorRequirement> {
127    fields.as_ref().map_or(Vec::new(), |flds| {
128        flds.iter()
129            .map(|field| NodeSelectorRequirement {
130                key: field.key.clone(),
131                operator: field.operator.clone(),
132                values: Some(
133                    field
134                        .values
135                        .as_ref()
136                        .map_or_else(Vec::new, |vals| vals.clone()),
137                ),
138            })
139            .collect()
140    })
141}
142
143// convert_node_affinity_to_pooler converts a NodeAffinity to a PoolerTemplateSpecAffinityNodeAffinity
144// to be used in the PoolerTemplateSpec struct when creating a new pooler.
145pub fn convert_node_affinity_to_pooler(
146    node_affinity: &NodeAffinity,
147) -> Option<PoolerTemplateSpecAffinityNodeAffinity> {
148    if node_affinity
149        .preferred_during_scheduling_ignored_during_execution
150        .is_none()
151        && node_affinity
152            .required_during_scheduling_ignored_during_execution
153            .is_none()
154    {
155        None
156    } else {
157        Some(PoolerTemplateSpecAffinityNodeAffinity {
158            required_during_scheduling_ignored_during_execution: node_affinity.required_during_scheduling_ignored_during_execution.as_ref().map(|req| {
159                PoolerTemplateSpecAffinityNodeAffinityRequiredDuringSchedulingIgnoredDuringExecution {
160                    node_selector_terms: convert_node_selector_terms_to_pooler(&req.node_selector_terms),
161                }
162            }),
163            preferred_during_scheduling_ignored_during_execution: node_affinity.preferred_during_scheduling_ignored_during_execution.as_ref().map(|prefs| {
164                convert_preferred_scheduling_terms_to_pooler(prefs)
165            }),
166        })
167    }
168}
169
170// convert_node_selector_terms_to_pooler converts a Vec<NodeSelectorTerm> to a Vec<PoolerTemplateSpecAffinityNodeAffinityRequiredDuringSchedulingIgnoredDuringExecutionNodeSelectorTerms>
171// to be used in the PoolerTemplateSpecAffinityNodeAffinity struct when creating a new pooler.
172fn convert_node_selector_terms_to_pooler(
173    terms: &[NodeSelectorTerm],
174) -> Vec<PoolerTemplateSpecAffinityNodeAffinityRequiredDuringSchedulingIgnoredDuringExecutionNodeSelectorTerms>{
175    terms.iter().map(|term| {
176        PoolerTemplateSpecAffinityNodeAffinityRequiredDuringSchedulingIgnoredDuringExecutionNodeSelectorTerms {
177            match_expressions: term.match_expressions.as_ref().map(|expressions| {
178                expressions.iter().map(|expr| {
179                    PoolerTemplateSpecAffinityNodeAffinityRequiredDuringSchedulingIgnoredDuringExecutionNodeSelectorTermsMatchExpressions {
180                        key: expr.key.clone(),
181                        operator: expr.operator.clone(),
182                        values: expr.values.clone(),
183                    }
184                }).collect()
185            }),
186            match_fields: term.match_fields.as_ref().map(|fields| {
187                fields.iter().map(|field| {
188                    PoolerTemplateSpecAffinityNodeAffinityRequiredDuringSchedulingIgnoredDuringExecutionNodeSelectorTermsMatchFields {
189                        key: field.key.clone(),
190                        operator: field.operator.clone(),
191                        values: field.values.clone(),
192                    }
193                }).collect()
194            }),
195        }
196    }).collect()
197}
198
199// convert_preferred_scheduling_terms_to_pooler converts a Vec<PreferredSchedulingTerm> to a Vec<PoolerTemplateSpecAffinityNodeAffinityPreferredDuringSchedulingIgnoredDuringExecution>
200// to be used in the PoolerTemplateSpecAffinityNodeAffinity struct when creating a new pooler.
201fn convert_preferred_scheduling_terms_to_pooler(
202    terms: &[PreferredSchedulingTerm],
203) -> Vec<PoolerTemplateSpecAffinityNodeAffinityPreferredDuringSchedulingIgnoredDuringExecution> {
204    terms.iter().map(|term| {
205        PoolerTemplateSpecAffinityNodeAffinityPreferredDuringSchedulingIgnoredDuringExecution {
206            preference: PoolerTemplateSpecAffinityNodeAffinityPreferredDuringSchedulingIgnoredDuringExecutionPreference {
207                match_expressions: term.preference.match_expressions.as_ref().map(|expressions| {
208                    expressions.iter().map(|expr| {
209                        PoolerTemplateSpecAffinityNodeAffinityPreferredDuringSchedulingIgnoredDuringExecutionPreferenceMatchExpressions {
210                            key: expr.key.clone(),
211                            operator: expr.operator.clone(),
212                            values: expr.values.clone(),
213                        }
214                    }).collect()
215                }),
216                match_fields: term.preference.match_fields.as_ref().map(|fields| {
217                    fields.iter().map(|field| {
218                        PoolerTemplateSpecAffinityNodeAffinityPreferredDuringSchedulingIgnoredDuringExecutionPreferenceMatchFields {
219                            key: field.key.clone(),
220                            operator: field.operator.clone(),
221                            values: field.values.clone(),
222                        }
223                    }).collect()
224                }),
225            },
226            weight: term.weight,
227        }
228    }).collect()
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    fn create_sample_node_affinity() -> NodeAffinity {
236        NodeAffinity {
237            required_during_scheduling_ignored_during_execution: Some(NodeSelector {
238                node_selector_terms: vec![NodeSelectorTerm {
239                    match_expressions: Some(vec![NodeSelectorRequirement {
240                        key: "region".to_string(),
241                        operator: "In".to_string(),
242                        values: Some(vec!["us-west-1".to_string()]),
243                    }]),
244                    ..Default::default()
245                }],
246            }),
247            preferred_during_scheduling_ignored_during_execution: Some(vec![
248                PreferredSchedulingTerm {
249                    weight: 100,
250                    preference: NodeSelectorTerm {
251                        match_expressions: Some(vec![NodeSelectorRequirement {
252                            key: "zone".to_string(),
253                            operator: "In".to_string(),
254                            values: Some(vec!["us-west-1a".to_string()]),
255                        }]),
256                        ..Default::default()
257                    },
258                },
259            ]),
260        }
261    }
262
263    #[test]
264    fn test_convert_node_affinity_empty() {
265        let ca = ClusterAffinityNodeAffinity {
266            preferred_during_scheduling_ignored_during_execution: None,
267            required_during_scheduling_ignored_during_execution: None,
268        };
269
270        let result = convert_node_affinity(&ca);
271        // assert!(result
272        //     .preferred_during_scheduling_ignored_during_execution
273        //     .expect("preferred_during_scheduling_ignored_during_execution should be Some"));
274        assert!(result
275            .required_during_scheduling_ignored_during_execution
276            .is_none());
277    }
278
279    #[test]
280    fn test_convert_required_match_expressions_empty() {
281        let expressions = None;
282        let result = convert_required_match_expressions(&expressions);
283        assert_eq!(result, None);
284    }
285
286    #[test]
287    fn test_convert_required_match_fields_empty() {
288        let fields = None;
289        let result = convert_required_match_fields(&fields);
290        assert_eq!(result, None);
291    }
292
293    #[test]
294    fn test_convert_required_match_expressions() {
295        let expressions = Some(vec![
296            ClusterAffinityNodeAffinityRequiredDuringSchedulingIgnoredDuringExecutionNodeSelectorTermsMatchExpressions {
297                key: "key1".to_string(),
298                operator: "In".to_string(),
299                values: Some(vec!["value1".to_string(), "value2".to_string()]),
300            },
301        ]);
302
303        let result = convert_required_match_expressions(&expressions).unwrap();
304        assert_eq!(result.len(), 1);
305        assert_eq!(result[0].key, "key1");
306        assert_eq!(result[0].operator, "In");
307        assert_eq!(
308            result[0].values,
309            Some(vec!["value1".to_string(), "value2".to_string()])
310        );
311    }
312
313    #[test]
314    fn test_convert_required_match_fields() {
315        let fields = Some(vec![
316            ClusterAffinityNodeAffinityRequiredDuringSchedulingIgnoredDuringExecutionNodeSelectorTermsMatchFields {
317                key: "field1".to_string(),
318                operator: "Exists".to_string(),
319                values: None,
320            },
321        ]);
322
323        let result = convert_required_match_fields(&fields).unwrap();
324        assert_eq!(result.len(), 1);
325        assert_eq!(result[0].key, "field1");
326        assert_eq!(result[0].operator, "Exists");
327        assert!(result[0].values.is_none());
328    }
329    #[test]
330    fn test_convert_node_affinity_to_pooler_non_empty() {
331        let node_affinity = create_sample_node_affinity();
332        let result = convert_node_affinity_to_pooler(&node_affinity);
333
334        assert!(result.is_some());
335        let pooler_node_affinity = result.unwrap();
336        assert!(pooler_node_affinity
337            .required_during_scheduling_ignored_during_execution
338            .is_some());
339        assert!(pooler_node_affinity
340            .preferred_during_scheduling_ignored_during_execution
341            .is_some());
342
343        let required = &pooler_node_affinity
344            .required_during_scheduling_ignored_during_execution
345            .unwrap();
346        assert_eq!(required.node_selector_terms.len(), 1);
347        assert_eq!(
348            required.node_selector_terms[0]
349                .match_expressions
350                .as_ref()
351                .unwrap()[0]
352                .key,
353            "region"
354        );
355
356        let preferred = &pooler_node_affinity
357            .preferred_during_scheduling_ignored_during_execution
358            .unwrap()[0];
359        assert_eq!(preferred.weight, 100);
360        assert_eq!(
361            preferred.preference.match_expressions.as_ref().unwrap()[0].key,
362            "zone"
363        );
364    }
365
366    #[test]
367    fn test_convert_node_affinity_to_pooler_empty() {
368        let node_affinity = NodeAffinity {
369            required_during_scheduling_ignored_during_execution: None,
370            preferred_during_scheduling_ignored_during_execution: None,
371        };
372        let result = convert_node_affinity_to_pooler(&node_affinity);
373
374        assert!(result.is_none());
375    }
376}