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 if !chart.has_valid_api_version() {
91 let version = match &chart.api_version {
92 ApiVersion::Unknown(v) => v.clone(),
93 _ => "unknown".to_string(),
94 };
95 return vec![CheckFailure::new(
96 "HL1002",
97 Severity::Error,
98 format!("Invalid apiVersion '{}'. Must be v1 or v2", version),
99 "Chart.yaml",
100 1,
101 RuleCategory::Structure,
102 )];
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 if chart.name.is_empty() {
132 return vec![CheckFailure::new(
133 "HL1003",
134 Severity::Error,
135 "Missing required field 'name' in Chart.yaml",
136 "Chart.yaml",
137 1,
138 RuleCategory::Structure,
139 )];
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 if chart.version.is_empty() {
169 return vec![CheckFailure::new(
170 "HL1004",
171 Severity::Error,
172 "Missing required field 'version' in Chart.yaml",
173 "Chart.yaml",
174 1,
175 RuleCategory::Structure,
176 )];
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 if !chart.version.is_empty() && !is_valid_semver(&chart.version) {
206 return vec![CheckFailure::new(
207 "HL1005",
208 Severity::Warning,
209 format!(
210 "Version '{}' is not valid SemVer (expected X.Y.Z format)",
211 chart.version
212 ),
213 "Chart.yaml",
214 1,
215 RuleCategory::Structure,
216 )];
217 }
218 }
219 vec![]
220 }
221}
222
223pub struct HL1006;
225
226impl Rule for HL1006 {
227 fn code(&self) -> &'static str {
228 "HL1006"
229 }
230
231 fn severity(&self) -> Severity {
232 Severity::Info
233 }
234
235 fn name(&self) -> &'static str {
236 "missing-description"
237 }
238
239 fn description(&self) -> &'static str {
240 "Chart should have a description"
241 }
242
243 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
244 if let Some(chart) = ctx.chart_metadata {
245 if chart.description.is_none()
246 || chart
247 .description
248 .as_ref()
249 .map(|d| d.is_empty())
250 .unwrap_or(true)
251 {
252 return vec![CheckFailure::new(
253 "HL1006",
254 Severity::Info,
255 "Chart.yaml is missing a description",
256 "Chart.yaml",
257 1,
258 RuleCategory::Structure,
259 )];
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 if chart.maintainers.is_empty() {
289 return vec![CheckFailure::new(
290 "HL1007",
291 Severity::Info,
292 "Chart.yaml has no maintainers listed",
293 "Chart.yaml",
294 1,
295 RuleCategory::Structure,
296 )];
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 if chart.is_deprecated() {
326 return vec![CheckFailure::new(
327 "HL1008",
328 Severity::Warning,
329 "Chart is marked as deprecated",
330 "Chart.yaml",
331 1,
332 RuleCategory::Structure,
333 )];
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 if chart.is_library() {
364 return vec![];
365 }
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 if !is_valid_chart_name(&chart.name) {
471 return vec![CheckFailure::new(
472 "HL1012",
473 Severity::Error,
474 format!(
475 "Chart name '{}' contains invalid characters. Use only lowercase letters, numbers, and hyphens",
476 chart.name
477 ),
478 "Chart.yaml",
479 1,
480 RuleCategory::Structure,
481 )];
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 if let Some(icon) = &chart.icon {
511 if icon.starts_with("http://") {
512 return vec![CheckFailure::new(
513 "HL1013",
514 Severity::Warning,
515 "Icon URL should use HTTPS instead of HTTP",
516 "Chart.yaml",
517 1,
518 RuleCategory::Structure,
519 )];
520 }
521 }
522 }
523 vec![]
524 }
525}
526
527pub struct HL1014;
529
530impl Rule for HL1014 {
531 fn code(&self) -> &'static str {
532 "HL1014"
533 }
534
535 fn severity(&self) -> Severity {
536 Severity::Warning
537 }
538
539 fn name(&self) -> &'static str {
540 "home-not-https"
541 }
542
543 fn description(&self) -> &'static str {
544 "Home URL should use HTTPS"
545 }
546
547 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
548 if let Some(chart) = ctx.chart_metadata {
549 if let Some(home) = &chart.home {
550 if home.starts_with("http://") {
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 }
561 }
562 vec![]
563 }
564}
565
566pub struct HL1015;
568
569impl Rule for HL1015 {
570 fn code(&self) -> &'static str {
571 "HL1015"
572 }
573
574 fn severity(&self) -> Severity {
575 Severity::Error
576 }
577
578 fn name(&self) -> &'static str {
579 "duplicate-dependencies"
580 }
581
582 fn description(&self) -> &'static str {
583 "Chart has duplicate dependency names"
584 }
585
586 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
587 if let Some(chart) = ctx.chart_metadata {
588 let duplicates = chart.has_duplicate_dependencies();
589 if !duplicates.is_empty() {
590 return vec![CheckFailure::new(
591 "HL1015",
592 Severity::Error,
593 format!("Duplicate dependency names: {}", duplicates.join(", ")),
594 "Chart.yaml",
595 1,
596 RuleCategory::Structure,
597 )];
598 }
599 }
600 vec![]
601 }
602}
603
604pub struct HL1016;
606
607impl Rule for HL1016 {
608 fn code(&self) -> &'static str {
609 "HL1016"
610 }
611
612 fn severity(&self) -> Severity {
613 Severity::Warning
614 }
615
616 fn name(&self) -> &'static str {
617 "dependency-missing-version"
618 }
619
620 fn description(&self) -> &'static str {
621 "Chart dependency is missing a version"
622 }
623
624 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
625 let mut failures = Vec::new();
626 if let Some(chart) = ctx.chart_metadata {
627 for dep in &chart.dependencies {
628 if dep.version.is_none()
629 || dep.version.as_ref().map(|v| v.is_empty()).unwrap_or(true)
630 {
631 failures.push(CheckFailure::new(
632 "HL1016",
633 Severity::Warning,
634 format!("Dependency '{}' is missing a version", dep.name),
635 "Chart.yaml",
636 1,
637 RuleCategory::Structure,
638 ));
639 }
640 }
641 }
642 failures
643 }
644}
645
646pub struct HL1017;
648
649impl Rule for HL1017 {
650 fn code(&self) -> &'static str {
651 "HL1017"
652 }
653
654 fn severity(&self) -> Severity {
655 Severity::Error
656 }
657
658 fn name(&self) -> &'static str {
659 "dependency-missing-repository"
660 }
661
662 fn description(&self) -> &'static str {
663 "Chart dependency is missing a repository"
664 }
665
666 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
667 let mut failures = Vec::new();
668 if let Some(chart) = ctx.chart_metadata {
669 for dep in &chart.dependencies {
670 if dep.repository.is_none()
671 || dep
672 .repository
673 .as_ref()
674 .map(|r| r.is_empty())
675 .unwrap_or(true)
676 {
677 failures.push(CheckFailure::new(
679 "HL1017",
680 Severity::Error,
681 format!("Dependency '{}' is missing a repository", dep.name),
682 "Chart.yaml",
683 1,
684 RuleCategory::Structure,
685 ));
686 }
687 }
688 }
689 failures
690 }
691}
692
693fn is_valid_semver(version: &str) -> bool {
695 let parts: Vec<&str> = version.split('.').collect();
696 if parts.len() < 2 || parts.len() > 3 {
697 return false;
698 }
699
700 for (i, part) in parts.iter().enumerate() {
702 let numeric_part = if i == parts.len() - 1 {
704 part.split(['-', '+']).next().unwrap_or(part)
705 } else {
706 part
707 };
708
709 if numeric_part.parse::<u64>().is_err() {
710 return false;
711 }
712 }
713
714 true
715}
716
717fn is_valid_chart_name(name: &str) -> bool {
719 if name.is_empty() {
720 return false;
721 }
722
723 if !name
725 .chars()
726 .next()
727 .map(|c| c.is_ascii_lowercase())
728 .unwrap_or(false)
729 {
730 return false;
731 }
732
733 name.chars()
735 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
736}
737
738#[cfg(test)]
739mod tests {
740 use super::*;
741
742 #[test]
743 fn test_valid_semver() {
744 assert!(is_valid_semver("1.0.0"));
745 assert!(is_valid_semver("0.1.0"));
746 assert!(is_valid_semver("10.20.30"));
747 assert!(is_valid_semver("1.0.0-alpha"));
748 assert!(is_valid_semver("1.0.0+build"));
749 assert!(is_valid_semver("1.0"));
750 assert!(!is_valid_semver("1"));
751 assert!(!is_valid_semver("v1.0.0"));
752 assert!(!is_valid_semver("1.0.0.0"));
753 assert!(!is_valid_semver(""));
754 }
755
756 #[test]
757 fn test_valid_chart_name() {
758 assert!(is_valid_chart_name("my-chart"));
759 assert!(is_valid_chart_name("mychart"));
760 assert!(is_valid_chart_name("my-chart-123"));
761 assert!(!is_valid_chart_name("My-Chart"));
762 assert!(!is_valid_chart_name("my_chart"));
763 assert!(!is_valid_chart_name("123-chart"));
764 assert!(!is_valid_chart_name(""));
765 }
766}