syncable_cli/analyzer/helmlint/rules/
hl5xxx.rs

1//! HL5xxx - Best Practice Rules
2//!
3//! Rules for validating Kubernetes best practices in Helm charts.
4
5use crate::analyzer::helmlint::parser::template::TemplateToken;
6use crate::analyzer::helmlint::rules::{LintContext, Rule};
7use crate::analyzer::helmlint::types::{CheckFailure, RuleCategory, Severity};
8
9/// Get all HL5xxx rules.
10pub fn rules() -> Vec<Box<dyn Rule>> {
11    vec![
12        Box::new(HL5001),
13        Box::new(HL5002),
14        Box::new(HL5003),
15        Box::new(HL5004),
16        Box::new(HL5005),
17        Box::new(HL5006),
18    ]
19}
20
21/// HL5001: Missing resource limits
22pub struct HL5001;
23
24impl Rule for HL5001 {
25    fn code(&self) -> &'static str {
26        "HL5001"
27    }
28
29    fn severity(&self) -> Severity {
30        Severity::Warning
31    }
32
33    fn name(&self) -> &'static str {
34        "missing-resource-limits"
35    }
36
37    fn description(&self) -> &'static str {
38        "Container should have resource limits defined"
39    }
40
41    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
42        let mut failures = Vec::new();
43
44        // Check values.yaml for resource limits
45        if let Some(values) = ctx.values {
46            let has_limits = values
47                .defined_paths
48                .iter()
49                .any(|p| p.contains("resources.limits") || p.ends_with(".limits"));
50
51            if !has_limits {
52                failures.push(CheckFailure::new(
53                    "HL5001",
54                    Severity::Warning,
55                    "No resource limits found in values.yaml. Define resources.limits for predictable resource usage",
56                    "values.yaml",
57                    1,
58                    RuleCategory::BestPractice,
59                ));
60            }
61        }
62
63        failures
64    }
65}
66
67/// HL5002: Missing resource requests
68pub struct HL5002;
69
70impl Rule for HL5002 {
71    fn code(&self) -> &'static str {
72        "HL5002"
73    }
74
75    fn severity(&self) -> Severity {
76        Severity::Warning
77    }
78
79    fn name(&self) -> &'static str {
80        "missing-resource-requests"
81    }
82
83    fn description(&self) -> &'static str {
84        "Container should have resource requests defined"
85    }
86
87    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
88        let mut failures = Vec::new();
89
90        if let Some(values) = ctx.values {
91            let has_requests = values
92                .defined_paths
93                .iter()
94                .any(|p| p.contains("resources.requests") || p.ends_with(".requests"));
95
96            if !has_requests {
97                failures.push(CheckFailure::new(
98                    "HL5002",
99                    Severity::Warning,
100                    "No resource requests found in values.yaml. Define resources.requests for proper scheduling",
101                    "values.yaml",
102                    1,
103                    RuleCategory::BestPractice,
104                ));
105            }
106        }
107
108        failures
109    }
110}
111
112/// HL5003: Missing liveness probe
113pub struct HL5003;
114
115impl Rule for HL5003 {
116    fn code(&self) -> &'static str {
117        "HL5003"
118    }
119
120    fn severity(&self) -> Severity {
121        Severity::Info
122    }
123
124    fn name(&self) -> &'static str {
125        "missing-liveness-probe"
126    }
127
128    fn description(&self) -> &'static str {
129        "Container should have a liveness probe for health checking"
130    }
131
132    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
133        let mut failures = Vec::new();
134
135        // Check if any template has livenessProbe
136        let has_liveness_in_template = ctx.templates.iter().any(|t| {
137            t.tokens.iter().any(|token| match token {
138                TemplateToken::Text { content, .. } => content.contains("livenessProbe"),
139                TemplateToken::Action { content, .. } => content.contains("livenessProbe"),
140                _ => false,
141            })
142        });
143
144        // Check values.yaml
145        let has_liveness_in_values = ctx
146            .values
147            .map(|v| {
148                v.defined_paths
149                    .iter()
150                    .any(|p| p.to_lowercase().contains("livenessprobe"))
151            })
152            .unwrap_or(false);
153
154        if !has_liveness_in_template && !has_liveness_in_values {
155            failures.push(CheckFailure::new(
156                "HL5003",
157                Severity::Info,
158                "No livenessProbe found. Consider adding a liveness probe for container health monitoring",
159                "templates/",
160                1,
161                RuleCategory::BestPractice,
162            ));
163        }
164
165        failures
166    }
167}
168
169/// HL5004: Missing readiness probe
170pub struct HL5004;
171
172impl Rule for HL5004 {
173    fn code(&self) -> &'static str {
174        "HL5004"
175    }
176
177    fn severity(&self) -> Severity {
178        Severity::Info
179    }
180
181    fn name(&self) -> &'static str {
182        "missing-readiness-probe"
183    }
184
185    fn description(&self) -> &'static str {
186        "Container should have a readiness probe for traffic management"
187    }
188
189    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
190        let mut failures = Vec::new();
191
192        let has_readiness_in_template = ctx.templates.iter().any(|t| {
193            t.tokens.iter().any(|token| match token {
194                TemplateToken::Text { content, .. } => content.contains("readinessProbe"),
195                TemplateToken::Action { content, .. } => content.contains("readinessProbe"),
196                _ => false,
197            })
198        });
199
200        let has_readiness_in_values = ctx
201            .values
202            .map(|v| {
203                v.defined_paths
204                    .iter()
205                    .any(|p| p.to_lowercase().contains("readinessprobe"))
206            })
207            .unwrap_or(false);
208
209        if !has_readiness_in_template && !has_readiness_in_values {
210            failures.push(CheckFailure::new(
211                "HL5004",
212                Severity::Info,
213                "No readinessProbe found. Consider adding a readiness probe for proper load balancing",
214                "templates/",
215                1,
216                RuleCategory::BestPractice,
217            ));
218        }
219
220        failures
221    }
222}
223
224/// HL5005: Using deprecated Kubernetes API
225pub struct HL5005;
226
227impl Rule for HL5005 {
228    fn code(&self) -> &'static str {
229        "HL5005"
230    }
231
232    fn severity(&self) -> Severity {
233        Severity::Error
234    }
235
236    fn name(&self) -> &'static str {
237        "deprecated-api"
238    }
239
240    fn description(&self) -> &'static str {
241        "Template uses deprecated Kubernetes API version"
242    }
243
244    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
245        let mut failures = Vec::new();
246
247        // Deprecated APIs and their replacements
248        let deprecated_apis = [
249            (
250                "extensions/v1beta1",
251                "apps/v1",
252                "Deployment, DaemonSet, ReplicaSet",
253            ),
254            ("apps/v1beta1", "apps/v1", "Deployment, StatefulSet"),
255            (
256                "apps/v1beta2",
257                "apps/v1",
258                "Deployment, StatefulSet, DaemonSet, ReplicaSet",
259            ),
260            (
261                "networking.k8s.io/v1beta1",
262                "networking.k8s.io/v1",
263                "Ingress",
264            ),
265            (
266                "rbac.authorization.k8s.io/v1beta1",
267                "rbac.authorization.k8s.io/v1",
268                "Role, ClusterRole, RoleBinding",
269            ),
270            (
271                "admissionregistration.k8s.io/v1beta1",
272                "admissionregistration.k8s.io/v1",
273                "MutatingWebhookConfiguration, ValidatingWebhookConfiguration",
274            ),
275            (
276                "apiextensions.k8s.io/v1beta1",
277                "apiextensions.k8s.io/v1",
278                "CustomResourceDefinition",
279            ),
280            ("policy/v1beta1", "policy/v1", "PodDisruptionBudget"),
281            ("batch/v1beta1", "batch/v1", "CronJob"),
282        ];
283
284        for template in ctx.templates {
285            for token in &template.tokens {
286                if let TemplateToken::Text { content, line } = token {
287                    for (deprecated, replacement, resources) in &deprecated_apis {
288                        if content.contains(&format!("apiVersion: {}", deprecated)) {
289                            failures.push(CheckFailure::new(
290                                "HL5005",
291                                Severity::Error,
292                                format!(
293                                    "Deprecated API '{}' for {}. Use '{}' instead",
294                                    deprecated, resources, replacement
295                                ),
296                                &template.path,
297                                *line,
298                                RuleCategory::BestPractice,
299                            ));
300                        }
301                    }
302                }
303            }
304        }
305
306        failures
307    }
308}
309
310/// HL5006: Labels missing recommended keys
311pub struct HL5006;
312
313impl Rule for HL5006 {
314    fn code(&self) -> &'static str {
315        "HL5006"
316    }
317
318    fn severity(&self) -> Severity {
319        Severity::Info
320    }
321
322    fn name(&self) -> &'static str {
323        "missing-recommended-labels"
324    }
325
326    fn description(&self) -> &'static str {
327        "Resources should have recommended Kubernetes labels"
328    }
329
330    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
331        let mut failures = Vec::new();
332
333        // Recommended labels per Kubernetes best practices
334        let recommended_labels = [
335            "app.kubernetes.io/name",
336            "app.kubernetes.io/instance",
337            "app.kubernetes.io/version",
338            "app.kubernetes.io/component",
339            "app.kubernetes.io/part-of",
340            "app.kubernetes.io/managed-by",
341        ];
342
343        // Check if helpers define standard labels
344        let has_labels_helper = ctx
345            .helpers
346            .map(|h| {
347                h.helpers.iter().any(|helper| {
348                    helper.name.contains("labels") || helper.name.contains("selectorLabels")
349                })
350            })
351            .unwrap_or(false);
352
353        if !has_labels_helper {
354            // Check templates for any recommended labels
355            let has_recommended_labels = ctx.templates.iter().any(|t| {
356                t.tokens.iter().any(|token| match token {
357                    TemplateToken::Text { content, .. } => {
358                        recommended_labels.iter().any(|l| content.contains(l))
359                    }
360                    _ => false,
361                })
362            });
363
364            if !has_recommended_labels {
365                failures.push(CheckFailure::new(
366                    "HL5006",
367                    Severity::Info,
368                    "No recommended Kubernetes labels found. Consider adding app.kubernetes.io/* labels",
369                    "templates/_helpers.tpl",
370                    1,
371                    RuleCategory::BestPractice,
372                ));
373            }
374        }
375
376        failures
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    #[test]
385    fn test_rules_exist() {
386        let all_rules = rules();
387        assert!(!all_rules.is_empty());
388    }
389
390    #[test]
391    fn test_deprecated_api_list() {
392        // Verify our deprecated API list is reasonable
393        let deprecated_apis = [
394            "extensions/v1beta1",
395            "apps/v1beta1",
396            "apps/v1beta2",
397            "networking.k8s.io/v1beta1",
398        ];
399
400        for api in &deprecated_apis {
401            assert!(api.contains("beta") || api.contains("v1beta"));
402        }
403    }
404}