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            && let Some(max_unavailable) = &pdb.max_unavailable
48        {
49            // Check if it's set to 0 or 0%
50            if max_unavailable == "0" || max_unavailable == "0%" {
51                diagnostics.push(Diagnostic {
52                    message:
53                        "PDB maxUnavailable is set to 0, which blocks all voluntary disruptions"
54                            .to_string(),
55                    remediation: Some(
56                        "Set maxUnavailable to at least 1 or a non-zero percentage to allow \
57                             voluntary disruptions during cluster maintenance."
58                            .to_string(),
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            && let Some(min_available) = &pdb.min_available
108        {
109            // Check if it's set to 100%
110            if min_available == "100%" {
111                diagnostics.push(Diagnostic {
112                    message:
113                        "PDB minAvailable is set to 100%, which blocks all voluntary disruptions"
114                            .to_string(),
115                    remediation: Some(
116                        "Set minAvailable to less than 100% to allow voluntary disruptions \
117                             during cluster maintenance."
118                            .to_string(),
119                    ),
120                });
121            }
122        }
123
124        diagnostics
125    }
126}
127
128/// Template for checking PDB unhealthyPodEvictionPolicy.
129pub struct PdbUnhealthyPodEvictionPolicyTemplate;
130
131impl Template for PdbUnhealthyPodEvictionPolicyTemplate {
132    fn key(&self) -> &str {
133        "pdb-unhealthy-pod-eviction-policy"
134    }
135
136    fn human_name(&self) -> &str {
137        "PDB Unhealthy Pod Eviction Policy"
138    }
139
140    fn description(&self) -> &str {
141        "Checks PodDisruptionBudget unhealthyPodEvictionPolicy settings"
142    }
143
144    fn supported_object_kinds(&self) -> ObjectKindsDesc {
145        ObjectKindsDesc::new(&["PodDisruptionBudget"])
146    }
147
148    fn parameters(&self) -> Vec<ParameterDesc> {
149        Vec::new()
150    }
151
152    fn instantiate(
153        &self,
154        _params: &serde_yaml::Value,
155    ) -> Result<Box<dyn CheckFunc>, TemplateError> {
156        Ok(Box::new(PdbUnhealthyPodEvictionPolicyCheck))
157    }
158}
159
160struct PdbUnhealthyPodEvictionPolicyCheck;
161
162impl CheckFunc for PdbUnhealthyPodEvictionPolicyCheck {
163    fn check(&self, object: &Object) -> Vec<Diagnostic> {
164        let mut diagnostics = Vec::new();
165
166        if let K8sObject::PodDisruptionBudget(pdb) = &object.k8s_object {
167            // Check if unhealthyPodEvictionPolicy is not set (defaults to IfHealthyBudget)
168            if pdb.unhealthy_pod_eviction_policy.is_none() {
169                diagnostics.push(Diagnostic {
170                    message: "PDB does not specify unhealthyPodEvictionPolicy".to_string(),
171                    remediation: Some(
172                        "Consider setting unhealthyPodEvictionPolicy to 'AlwaysAllow' to allow \
173                         eviction of unhealthy pods even when budget is violated."
174                            .to_string(),
175                    ),
176                });
177            }
178        }
179
180        diagnostics
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use crate::analyzer::kubelint::parser::yaml::parse_yaml;
188
189    #[test]
190    fn test_pdb_max_unavailable_zero() {
191        let yaml = r#"
192apiVersion: policy/v1
193kind: PodDisruptionBudget
194metadata:
195  name: strict-pdb
196spec:
197  maxUnavailable: 0
198  selector:
199    matchLabels:
200      app: test
201"#;
202        let objects = parse_yaml(yaml).unwrap();
203        let check = PdbMaxUnavailableCheck;
204        let diagnostics = check.check(&objects[0]);
205        assert_eq!(diagnostics.len(), 1);
206        assert!(diagnostics[0].message.contains("maxUnavailable"));
207    }
208
209    #[test]
210    fn test_pdb_min_available_100_percent() {
211        let yaml = r#"
212apiVersion: policy/v1
213kind: PodDisruptionBudget
214metadata:
215  name: strict-pdb
216spec:
217  minAvailable: "100%"
218  selector:
219    matchLabels:
220      app: test
221"#;
222        let objects = parse_yaml(yaml).unwrap();
223        let check = PdbMinAvailableCheck;
224        let diagnostics = check.check(&objects[0]);
225        assert_eq!(diagnostics.len(), 1);
226        assert!(diagnostics[0].message.contains("minAvailable"));
227    }
228}