syncable_cli/analyzer/helmlint/rules/
hl1xxx.rs1use crate::analyzer::helmlint::parser::chart::ApiVersion;
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(HL1001),
13 Box::new(HL1002),
14 Box::new(HL1003),
15 Box::new(HL1004),
16 Box::new(HL1005),
17 Box::new(HL1006),
18 Box::new(HL1007),
19 Box::new(HL1008),
20 Box::new(HL1009),
21 Box::new(HL1010),
22 Box::new(HL1011),
23 Box::new(HL1012),
24 Box::new(HL1013),
25 Box::new(HL1014),
26 Box::new(HL1015),
27 Box::new(HL1016),
28 Box::new(HL1017),
29 ]
30}
31
32pub struct HL1001;
34
35impl Rule for HL1001 {
36 fn code(&self) -> &'static str {
37 "HL1001"
38 }
39
40 fn severity(&self) -> Severity {
41 Severity::Error
42 }
43
44 fn name(&self) -> &'static str {
45 "missing-chart-yaml"
46 }
47
48 fn description(&self) -> &'static str {
49 "Chart.yaml is required for all Helm charts"
50 }
51
52 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
53 if ctx.chart_metadata.is_none() && !ctx.has_file("Chart.yaml") {
54 vec![CheckFailure::new(
55 "HL1001",
56 Severity::Error,
57 "Missing Chart.yaml file",
58 "Chart.yaml",
59 1,
60 RuleCategory::Structure,
61 )]
62 } else {
63 vec![]
64 }
65 }
66}
67
68pub struct HL1002;
70
71impl Rule for HL1002 {
72 fn code(&self) -> &'static str {
73 "HL1002"
74 }
75
76 fn severity(&self) -> Severity {
77 Severity::Error
78 }
79
80 fn name(&self) -> &'static str {
81 "invalid-api-version"
82 }
83
84 fn description(&self) -> &'static str {
85 "Chart apiVersion must be v1 or v2"
86 }
87
88 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
89 if let Some(chart) = ctx.chart_metadata
90 && !chart.has_valid_api_version()
91 {
92 let version = match &chart.api_version {
93 ApiVersion::Unknown(v) => v.clone(),
94 _ => "unknown".to_string(),
95 };
96 return vec![CheckFailure::new(
97 "HL1002",
98 Severity::Error,
99 format!("Invalid apiVersion '{}'. Must be v1 or v2", version),
100 "Chart.yaml",
101 1,
102 RuleCategory::Structure,
103 )];
104 }
105 vec![]
106 }
107}
108
109pub struct HL1003;
111
112impl Rule for HL1003 {
113 fn code(&self) -> &'static str {
114 "HL1003"
115 }
116
117 fn severity(&self) -> Severity {
118 Severity::Error
119 }
120
121 fn name(&self) -> &'static str {
122 "missing-name"
123 }
124
125 fn description(&self) -> &'static str {
126 "Chart.yaml must have a 'name' field"
127 }
128
129 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
130 if let Some(chart) = ctx.chart_metadata
131 && chart.name.is_empty()
132 {
133 return vec![CheckFailure::new(
134 "HL1003",
135 Severity::Error,
136 "Missing required field 'name' in Chart.yaml",
137 "Chart.yaml",
138 1,
139 RuleCategory::Structure,
140 )];
141 }
142 vec![]
143 }
144}
145
146pub struct HL1004;
148
149impl Rule for HL1004 {
150 fn code(&self) -> &'static str {
151 "HL1004"
152 }
153
154 fn severity(&self) -> Severity {
155 Severity::Error
156 }
157
158 fn name(&self) -> &'static str {
159 "missing-version"
160 }
161
162 fn description(&self) -> &'static str {
163 "Chart.yaml must have a 'version' field"
164 }
165
166 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
167 if let Some(chart) = ctx.chart_metadata
168 && chart.version.is_empty()
169 {
170 return vec![CheckFailure::new(
171 "HL1004",
172 Severity::Error,
173 "Missing required field 'version' in Chart.yaml",
174 "Chart.yaml",
175 1,
176 RuleCategory::Structure,
177 )];
178 }
179 vec![]
180 }
181}
182
183pub struct HL1005;
185
186impl Rule for HL1005 {
187 fn code(&self) -> &'static str {
188 "HL1005"
189 }
190
191 fn severity(&self) -> Severity {
192 Severity::Warning
193 }
194
195 fn name(&self) -> &'static str {
196 "invalid-semver"
197 }
198
199 fn description(&self) -> &'static str {
200 "Chart version should be valid SemVer"
201 }
202
203 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
204 if let Some(chart) = ctx.chart_metadata
205 && !chart.version.is_empty()
206 && !is_valid_semver(&chart.version)
207 {
208 return vec![CheckFailure::new(
209 "HL1005",
210 Severity::Warning,
211 format!(
212 "Version '{}' is not valid SemVer (expected X.Y.Z format)",
213 chart.version
214 ),
215 "Chart.yaml",
216 1,
217 RuleCategory::Structure,
218 )];
219 }
220 vec![]
221 }
222}
223
224pub struct HL1006;
226
227impl Rule for HL1006 {
228 fn code(&self) -> &'static str {
229 "HL1006"
230 }
231
232 fn severity(&self) -> Severity {
233 Severity::Info
234 }
235
236 fn name(&self) -> &'static str {
237 "missing-description"
238 }
239
240 fn description(&self) -> &'static str {
241 "Chart should have a description"
242 }
243
244 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
245 if let Some(chart) = ctx.chart_metadata
246 && (chart.description.is_none()
247 || chart
248 .description
249 .as_ref()
250 .map(|d| d.is_empty())
251 .unwrap_or(true))
252 {
253 return vec![CheckFailure::new(
254 "HL1006",
255 Severity::Info,
256 "Chart.yaml is missing a description",
257 "Chart.yaml",
258 1,
259 RuleCategory::Structure,
260 )];
261 }
262 vec![]
263 }
264}
265
266pub struct HL1007;
268
269impl Rule for HL1007 {
270 fn code(&self) -> &'static str {
271 "HL1007"
272 }
273
274 fn severity(&self) -> Severity {
275 Severity::Info
276 }
277
278 fn name(&self) -> &'static str {
279 "missing-maintainers"
280 }
281
282 fn description(&self) -> &'static str {
283 "Chart should have maintainers listed"
284 }
285
286 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
287 if let Some(chart) = ctx.chart_metadata
288 && chart.maintainers.is_empty()
289 {
290 return vec![CheckFailure::new(
291 "HL1007",
292 Severity::Info,
293 "Chart.yaml has no maintainers listed",
294 "Chart.yaml",
295 1,
296 RuleCategory::Structure,
297 )];
298 }
299 vec![]
300 }
301}
302
303pub struct HL1008;
305
306impl Rule for HL1008 {
307 fn code(&self) -> &'static str {
308 "HL1008"
309 }
310
311 fn severity(&self) -> Severity {
312 Severity::Warning
313 }
314
315 fn name(&self) -> &'static str {
316 "chart-deprecated"
317 }
318
319 fn description(&self) -> &'static str {
320 "Chart is marked as deprecated"
321 }
322
323 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
324 if let Some(chart) = ctx.chart_metadata
325 && chart.is_deprecated()
326 {
327 return vec![CheckFailure::new(
328 "HL1008",
329 Severity::Warning,
330 "Chart is marked as deprecated",
331 "Chart.yaml",
332 1,
333 RuleCategory::Structure,
334 )];
335 }
336 vec![]
337 }
338}
339
340pub struct HL1009;
342
343impl Rule for HL1009 {
344 fn code(&self) -> &'static str {
345 "HL1009"
346 }
347
348 fn severity(&self) -> Severity {
349 Severity::Warning
350 }
351
352 fn name(&self) -> &'static str {
353 "missing-templates"
354 }
355
356 fn description(&self) -> &'static str {
357 "Chart should have a templates directory"
358 }
359
360 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
361 if let Some(chart) = ctx.chart_metadata
363 && chart.is_library()
364 {
365 return vec![];
366 }
367
368 let has_templates = ctx
369 .files
370 .iter()
371 .any(|f| f.starts_with("templates/") || f.contains("/templates/"));
372 if !has_templates && ctx.templates.is_empty() {
373 return vec![CheckFailure::new(
374 "HL1009",
375 Severity::Warning,
376 "Chart has no templates directory",
377 ".",
378 1,
379 RuleCategory::Structure,
380 )];
381 }
382 vec![]
383 }
384}
385
386pub struct HL1010;
388
389impl Rule for HL1010 {
390 fn code(&self) -> &'static str {
391 "HL1010"
392 }
393
394 fn severity(&self) -> Severity {
395 Severity::Error
396 }
397
398 fn name(&self) -> &'static str {
399 "invalid-chart-type"
400 }
401
402 fn description(&self) -> &'static str {
403 "Chart type must be 'application' or 'library'"
404 }
405
406 fn check(&self, _ctx: &LintContext) -> Vec<CheckFailure> {
407 vec![]
410 }
411}
412
413pub struct HL1011;
415
416impl Rule for HL1011 {
417 fn code(&self) -> &'static str {
418 "HL1011"
419 }
420
421 fn severity(&self) -> Severity {
422 Severity::Warning
423 }
424
425 fn name(&self) -> &'static str {
426 "missing-values-yaml"
427 }
428
429 fn description(&self) -> &'static str {
430 "Chart should have a values.yaml file"
431 }
432
433 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
434 if ctx.values.is_none() && !ctx.has_file("values.yaml") {
435 return vec![CheckFailure::new(
436 "HL1011",
437 Severity::Warning,
438 "Missing values.yaml file",
439 "values.yaml",
440 1,
441 RuleCategory::Structure,
442 )];
443 }
444 vec![]
445 }
446}
447
448pub struct HL1012;
450
451impl Rule for HL1012 {
452 fn code(&self) -> &'static str {
453 "HL1012"
454 }
455
456 fn severity(&self) -> Severity {
457 Severity::Error
458 }
459
460 fn name(&self) -> &'static str {
461 "invalid-chart-name"
462 }
463
464 fn description(&self) -> &'static str {
465 "Chart name must contain only lowercase alphanumeric characters and hyphens"
466 }
467
468 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
469 if let Some(chart) = ctx.chart_metadata
470 && !is_valid_chart_name(&chart.name)
471 {
472 return vec![CheckFailure::new(
473 "HL1012",
474 Severity::Error,
475 format!(
476 "Chart name '{}' contains invalid characters. Use only lowercase letters, numbers, and hyphens",
477 chart.name
478 ),
479 "Chart.yaml",
480 1,
481 RuleCategory::Structure,
482 )];
483 }
484 vec![]
485 }
486}
487
488pub struct HL1013;
490
491impl Rule for HL1013 {
492 fn code(&self) -> &'static str {
493 "HL1013"
494 }
495
496 fn severity(&self) -> Severity {
497 Severity::Warning
498 }
499
500 fn name(&self) -> &'static str {
501 "icon-not-https"
502 }
503
504 fn description(&self) -> &'static str {
505 "Icon URL should use HTTPS"
506 }
507
508 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
509 if let Some(chart) = ctx.chart_metadata
510 && let Some(icon) = &chart.icon
511 && icon.starts_with("http://")
512 {
513 return vec![CheckFailure::new(
514 "HL1013",
515 Severity::Warning,
516 "Icon URL should use HTTPS instead of HTTP",
517 "Chart.yaml",
518 1,
519 RuleCategory::Structure,
520 )];
521 }
522 vec![]
523 }
524}
525
526pub struct HL1014;
528
529impl Rule for HL1014 {
530 fn code(&self) -> &'static str {
531 "HL1014"
532 }
533
534 fn severity(&self) -> Severity {
535 Severity::Warning
536 }
537
538 fn name(&self) -> &'static str {
539 "home-not-https"
540 }
541
542 fn description(&self) -> &'static str {
543 "Home URL should use HTTPS"
544 }
545
546 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
547 if let Some(chart) = ctx.chart_metadata
548 && let Some(home) = &chart.home
549 && home.starts_with("http://")
550 {
551 return vec![CheckFailure::new(
552 "HL1014",
553 Severity::Warning,
554 "Home URL should use HTTPS instead of HTTP",
555 "Chart.yaml",
556 1,
557 RuleCategory::Structure,
558 )];
559 }
560 vec![]
561 }
562}
563
564pub struct HL1015;
566
567impl Rule for HL1015 {
568 fn code(&self) -> &'static str {
569 "HL1015"
570 }
571
572 fn severity(&self) -> Severity {
573 Severity::Error
574 }
575
576 fn name(&self) -> &'static str {
577 "duplicate-dependencies"
578 }
579
580 fn description(&self) -> &'static str {
581 "Chart has duplicate dependency names"
582 }
583
584 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
585 if let Some(chart) = ctx.chart_metadata {
586 let duplicates = chart.has_duplicate_dependencies();
587 if !duplicates.is_empty() {
588 return vec![CheckFailure::new(
589 "HL1015",
590 Severity::Error,
591 format!("Duplicate dependency names: {}", duplicates.join(", ")),
592 "Chart.yaml",
593 1,
594 RuleCategory::Structure,
595 )];
596 }
597 }
598 vec![]
599 }
600}
601
602pub struct HL1016;
604
605impl Rule for HL1016 {
606 fn code(&self) -> &'static str {
607 "HL1016"
608 }
609
610 fn severity(&self) -> Severity {
611 Severity::Warning
612 }
613
614 fn name(&self) -> &'static str {
615 "dependency-missing-version"
616 }
617
618 fn description(&self) -> &'static str {
619 "Chart dependency is missing a version"
620 }
621
622 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
623 let mut failures = Vec::new();
624 if let Some(chart) = ctx.chart_metadata {
625 for dep in &chart.dependencies {
626 if dep.version.is_none()
627 || dep.version.as_ref().map(|v| v.is_empty()).unwrap_or(true)
628 {
629 failures.push(CheckFailure::new(
630 "HL1016",
631 Severity::Warning,
632 format!("Dependency '{}' is missing a version", dep.name),
633 "Chart.yaml",
634 1,
635 RuleCategory::Structure,
636 ));
637 }
638 }
639 }
640 failures
641 }
642}
643
644pub struct HL1017;
646
647impl Rule for HL1017 {
648 fn code(&self) -> &'static str {
649 "HL1017"
650 }
651
652 fn severity(&self) -> Severity {
653 Severity::Error
654 }
655
656 fn name(&self) -> &'static str {
657 "dependency-missing-repository"
658 }
659
660 fn description(&self) -> &'static str {
661 "Chart dependency is missing a repository"
662 }
663
664 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
665 let mut failures = Vec::new();
666 if let Some(chart) = ctx.chart_metadata {
667 for dep in &chart.dependencies {
668 if dep.repository.is_none()
669 || dep
670 .repository
671 .as_ref()
672 .map(|r| r.is_empty())
673 .unwrap_or(true)
674 {
675 failures.push(CheckFailure::new(
677 "HL1017",
678 Severity::Error,
679 format!("Dependency '{}' is missing a repository", dep.name),
680 "Chart.yaml",
681 1,
682 RuleCategory::Structure,
683 ));
684 }
685 }
686 }
687 failures
688 }
689}
690
691fn is_valid_semver(version: &str) -> bool {
693 let parts: Vec<&str> = version.split('.').collect();
694 if parts.len() < 2 || parts.len() > 3 {
695 return false;
696 }
697
698 for (i, part) in parts.iter().enumerate() {
700 let numeric_part = if i == parts.len() - 1 {
702 part.split(['-', '+']).next().unwrap_or(part)
703 } else {
704 part
705 };
706
707 if numeric_part.parse::<u64>().is_err() {
708 return false;
709 }
710 }
711
712 true
713}
714
715fn is_valid_chart_name(name: &str) -> bool {
717 if name.is_empty() {
718 return false;
719 }
720
721 if !name
723 .chars()
724 .next()
725 .map(|c| c.is_ascii_lowercase())
726 .unwrap_or(false)
727 {
728 return false;
729 }
730
731 name.chars()
733 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
734}
735
736#[cfg(test)]
737mod tests {
738 use super::*;
739
740 #[test]
741 fn test_valid_semver() {
742 assert!(is_valid_semver("1.0.0"));
743 assert!(is_valid_semver("0.1.0"));
744 assert!(is_valid_semver("10.20.30"));
745 assert!(is_valid_semver("1.0.0-alpha"));
746 assert!(is_valid_semver("1.0.0+build"));
747 assert!(is_valid_semver("1.0"));
748 assert!(!is_valid_semver("1"));
749 assert!(!is_valid_semver("v1.0.0"));
750 assert!(!is_valid_semver("1.0.0.0"));
751 assert!(!is_valid_semver(""));
752 }
753
754 #[test]
755 fn test_valid_chart_name() {
756 assert!(is_valid_chart_name("my-chart"));
757 assert!(is_valid_chart_name("mychart"));
758 assert!(is_valid_chart_name("my-chart-123"));
759 assert!(!is_valid_chart_name("My-Chart"));
760 assert!(!is_valid_chart_name("my_chart"));
761 assert!(!is_valid_chart_name("123-chart"));
762 assert!(!is_valid_chart_name(""));
763 }
764}