syncable_cli/analyzer/kubelint/templates/
pdb.rs

1//! PodDisruptionBudget check templates.
2
3use crate::analyzer::kubelint::context::K8sObject;
4use crate::analyzer::kubelint::context::Object;
5use crate::analyzer::kubelint::templates::{CheckFunc, ParameterDesc, Template, TemplateError};
6use crate::analyzer::kubelint::types::{Diagnostic, ObjectKindsDesc};
7
8/// Template for checking PDB maxUnavailable settings.
9pub struct PdbMaxUnavailableTemplate;
10
11impl Template for PdbMaxUnavailableTemplate {
12    fn key(&self) -> &str {
13        "pdb-max-unavailable"
14    }
15
16    fn human_name(&self) -> &str {
17        "PDB Max Unavailable"
18    }
19
20    fn description(&self) -> &str {
21        "Checks PodDisruptionBudget maxUnavailable settings"
22    }
23
24    fn supported_object_kinds(&self) -> ObjectKindsDesc {
25        ObjectKindsDesc::new(&["PodDisruptionBudget"])
26    }
27
28    fn parameters(&self) -> Vec<ParameterDesc> {
29        Vec::new()
30    }
31
32    fn instantiate(
33        &self,
34        _params: &serde_yaml::Value,
35    ) -> Result<Box<dyn CheckFunc>, TemplateError> {
36        Ok(Box::new(PdbMaxUnavailableCheck))
37    }
38}
39
40struct PdbMaxUnavailableCheck;
41
42impl CheckFunc for PdbMaxUnavailableCheck {
43    fn check(&self, object: &Object) -> Vec<Diagnostic> {
44        let mut diagnostics = Vec::new();
45
46        if let K8sObject::PodDisruptionBudget(pdb) = &object.k8s_object {
47            if let Some(max_unavailable) = &pdb.max_unavailable {
48                // Check if it's set to 0 or 0%
49                if max_unavailable == "0" || max_unavailable == "0%" {
50                    diagnostics.push(Diagnostic {
51                        message:
52                            "PDB maxUnavailable is set to 0, which blocks all voluntary disruptions"
53                                .to_string(),
54                        remediation: Some(
55                            "Set maxUnavailable to at least 1 or a non-zero percentage to allow \
56                             voluntary disruptions during cluster maintenance."
57                                .to_string(),
58                        ),
59                    });
60                }
61            }
62        }
63
64        diagnostics
65    }
66}
67
68/// Template for checking PDB minAvailable settings.
69pub struct PdbMinAvailableTemplate;
70
71impl Template for PdbMinAvailableTemplate {
72    fn key(&self) -> &str {
73        "pdb-min-available"
74    }
75
76    fn human_name(&self) -> &str {
77        "PDB Min Available"
78    }
79
80    fn description(&self) -> &str {
81        "Checks PodDisruptionBudget minAvailable settings"
82    }
83
84    fn supported_object_kinds(&self) -> ObjectKindsDesc {
85        ObjectKindsDesc::new(&["PodDisruptionBudget"])
86    }
87
88    fn parameters(&self) -> Vec<ParameterDesc> {
89        Vec::new()
90    }
91
92    fn instantiate(
93        &self,
94        _params: &serde_yaml::Value,
95    ) -> Result<Box<dyn CheckFunc>, TemplateError> {
96        Ok(Box::new(PdbMinAvailableCheck))
97    }
98}
99
100struct PdbMinAvailableCheck;
101
102impl CheckFunc for PdbMinAvailableCheck {
103    fn check(&self, object: &Object) -> Vec<Diagnostic> {
104        let mut diagnostics = Vec::new();
105
106        if let K8sObject::PodDisruptionBudget(pdb) = &object.k8s_object {
107            if let Some(min_available) = &pdb.min_available {
108                // Check if it's set to 100%
109                if min_available == "100%" {
110                    diagnostics.push(Diagnostic {
111                        message: "PDB minAvailable is set to 100%, which blocks all voluntary disruptions".to_string(),
112                        remediation: Some(
113                            "Set minAvailable to less than 100% to allow voluntary disruptions \
114                             during cluster maintenance."
115                                .to_string(),
116                        ),
117                    });
118                }
119            }
120        }
121
122        diagnostics
123    }
124}
125
126/// Template for checking PDB unhealthyPodEvictionPolicy.
127pub struct PdbUnhealthyPodEvictionPolicyTemplate;
128
129impl Template for PdbUnhealthyPodEvictionPolicyTemplate {
130    fn key(&self) -> &str {
131        "pdb-unhealthy-pod-eviction-policy"
132    }
133
134    fn human_name(&self) -> &str {
135        "PDB Unhealthy Pod Eviction Policy"
136    }
137
138    fn description(&self) -> &str {
139        "Checks PodDisruptionBudget unhealthyPodEvictionPolicy settings"
140    }
141
142    fn supported_object_kinds(&self) -> ObjectKindsDesc {
143        ObjectKindsDesc::new(&["PodDisruptionBudget"])
144    }
145
146    fn parameters(&self) -> Vec<ParameterDesc> {
147        Vec::new()
148    }
149
150    fn instantiate(
151        &self,
152        _params: &serde_yaml::Value,
153    ) -> Result<Box<dyn CheckFunc>, TemplateError> {
154        Ok(Box::new(PdbUnhealthyPodEvictionPolicyCheck))
155    }
156}
157
158struct PdbUnhealthyPodEvictionPolicyCheck;
159
160impl CheckFunc for PdbUnhealthyPodEvictionPolicyCheck {
161    fn check(&self, object: &Object) -> Vec<Diagnostic> {
162        let mut diagnostics = Vec::new();
163
164        if let K8sObject::PodDisruptionBudget(pdb) = &object.k8s_object {
165            // Check if unhealthyPodEvictionPolicy is not set (defaults to IfHealthyBudget)
166            if pdb.unhealthy_pod_eviction_policy.is_none() {
167                diagnostics.push(Diagnostic {
168                    message: "PDB does not specify unhealthyPodEvictionPolicy".to_string(),
169                    remediation: Some(
170                        "Consider setting unhealthyPodEvictionPolicy to 'AlwaysAllow' to allow \
171                         eviction of unhealthy pods even when budget is violated."
172                            .to_string(),
173                    ),
174                });
175            }
176        }
177
178        diagnostics
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use crate::analyzer::kubelint::parser::yaml::parse_yaml;
186
187    #[test]
188    fn test_pdb_max_unavailable_zero() {
189        let yaml = r#"
190apiVersion: policy/v1
191kind: PodDisruptionBudget
192metadata:
193  name: strict-pdb
194spec:
195  maxUnavailable: 0
196  selector:
197    matchLabels:
198      app: test
199"#;
200        let objects = parse_yaml(yaml).unwrap();
201        let check = PdbMaxUnavailableCheck;
202        let diagnostics = check.check(&objects[0]);
203        assert_eq!(diagnostics.len(), 1);
204        assert!(diagnostics[0].message.contains("maxUnavailable"));
205    }
206
207    #[test]
208    fn test_pdb_min_available_100_percent() {
209        let yaml = r#"
210apiVersion: policy/v1
211kind: PodDisruptionBudget
212metadata:
213  name: strict-pdb
214spec:
215  minAvailable: "100%"
216  selector:
217    matchLabels:
218      app: test
219"#;
220        let objects = parse_yaml(yaml).unwrap();
221        let check = PdbMinAvailableCheck;
222        let diagnostics = check.check(&objects[0]);
223        assert_eq!(diagnostics.len(), 1);
224        assert!(diagnostics[0].message.contains("minAvailable"));
225    }
226}