syncable_cli/analyzer/helmlint/rules/
hl5xxx.rs1use crate::analyzer::helmlint::parser::template::TemplateToken;
6use crate::analyzer::helmlint::rules::{LintContext, Rule};
7use crate::analyzer::helmlint::types::{CheckFailure, RuleCategory, Severity};
8
9pub 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
21pub 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 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
67pub 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
112pub 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 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 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
169pub 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
224pub 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 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
310pub 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 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 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 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 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}