syncable_cli/analyzer/kubelint/templates/
replicas.rs

1//! Replica count check templates.
2
3use crate::analyzer::kubelint::context::{K8sObject, Object};
4use crate::analyzer::kubelint::templates::{CheckFunc, ParameterDesc, Template, TemplateError};
5use crate::analyzer::kubelint::types::{Diagnostic, ObjectKindsDesc};
6
7/// Template for checking minimum replica count.
8pub struct ReplicasTemplate;
9
10impl Template for ReplicasTemplate {
11    fn key(&self) -> &str {
12        "replicas"
13    }
14
15    fn human_name(&self) -> &str {
16        "Minimum Replicas"
17    }
18
19    fn description(&self) -> &str {
20        "Checks that deployments have at least a minimum number of replicas"
21    }
22
23    fn supported_object_kinds(&self) -> ObjectKindsDesc {
24        ObjectKindsDesc::new(&["Deployment", "StatefulSet"])
25    }
26
27    fn parameters(&self) -> Vec<ParameterDesc> {
28        vec![ParameterDesc {
29            name: "minReplicas".to_string(),
30            description: "Minimum required replicas".to_string(),
31            param_type: "integer".to_string(),
32            required: false,
33            default: Some(serde_yaml::Value::Number(2.into())),
34        }]
35    }
36
37    fn instantiate(&self, params: &serde_yaml::Value) -> Result<Box<dyn CheckFunc>, TemplateError> {
38        let min_replicas = params
39            .get("minReplicas")
40            .and_then(|v| v.as_i64())
41            .unwrap_or(2) as i32;
42        Ok(Box::new(ReplicasCheck { min_replicas }))
43    }
44}
45
46struct ReplicasCheck {
47    min_replicas: i32,
48}
49
50impl CheckFunc for ReplicasCheck {
51    fn check(&self, object: &Object) -> Vec<Diagnostic> {
52        let mut diagnostics = Vec::new();
53
54        let replicas = match &object.k8s_object {
55            K8sObject::Deployment(d) => d.replicas,
56            K8sObject::StatefulSet(s) => s.replicas,
57            _ => None,
58        };
59
60        if let Some(replica_count) = replicas {
61            if replica_count < self.min_replicas {
62                diagnostics.push(Diagnostic {
63                    message: format!(
64                        "Object has only {} replicas, but minimum recommended is {}",
65                        replica_count, self.min_replicas
66                    ),
67                    remediation: Some(format!(
68                        "Increase replicas to at least {} for better availability.",
69                        self.min_replicas
70                    )),
71                });
72            }
73        } else {
74            // No replicas specified - defaults to 1
75            if self.min_replicas > 1 {
76                diagnostics.push(Diagnostic {
77                    message: format!(
78                        "No replica count specified (defaults to 1), but minimum recommended is {}",
79                        self.min_replicas
80                    ),
81                    remediation: Some(format!(
82                        "Explicitly set replicas to at least {}.",
83                        self.min_replicas
84                    )),
85                });
86            }
87        }
88
89        diagnostics
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::analyzer::kubelint::parser::yaml::parse_yaml;
97
98    #[test]
99    fn test_low_replicas_detected() {
100        let yaml = r#"
101apiVersion: apps/v1
102kind: Deployment
103metadata:
104  name: single-replica
105spec:
106  replicas: 1
107  template:
108    spec:
109      containers:
110      - name: nginx
111        image: nginx:1.21.0
112"#;
113        let objects = parse_yaml(yaml).unwrap();
114        let check = ReplicasCheck { min_replicas: 2 };
115        let diagnostics = check.check(&objects[0]);
116        assert_eq!(diagnostics.len(), 1);
117        assert!(diagnostics[0].message.contains("only 1 replicas"));
118    }
119
120    #[test]
121    fn test_adequate_replicas_ok() {
122        let yaml = r#"
123apiVersion: apps/v1
124kind: Deployment
125metadata:
126  name: multi-replica
127spec:
128  replicas: 3
129  template:
130    spec:
131      containers:
132      - name: nginx
133        image: nginx:1.21.0
134"#;
135        let objects = parse_yaml(yaml).unwrap();
136        let check = ReplicasCheck { min_replicas: 2 };
137        let diagnostics = check.check(&objects[0]);
138        assert!(diagnostics.is_empty());
139    }
140
141    #[test]
142    fn test_no_replicas_detected() {
143        let yaml = r#"
144apiVersion: apps/v1
145kind: Deployment
146metadata:
147  name: no-replica-spec
148spec:
149  template:
150    spec:
151      containers:
152      - name: nginx
153        image: nginx:1.21.0
154"#;
155        let objects = parse_yaml(yaml).unwrap();
156        let check = ReplicasCheck { min_replicas: 2 };
157        let diagnostics = check.check(&objects[0]);
158        assert_eq!(diagnostics.len(), 1);
159        assert!(diagnostics[0].message.contains("defaults to 1"));
160    }
161}