syncable_cli/analyzer/helmlint/rules/
hl1xxx.rs

1//! HL1xxx - Chart Structure Rules
2//!
3//! Rules for validating Helm chart structure, Chart.yaml, and file organization.
4
5use crate::analyzer::helmlint::parser::chart::ApiVersion;
6use crate::analyzer::helmlint::rules::{LintContext, Rule};
7use crate::analyzer::helmlint::types::{CheckFailure, RuleCategory, Severity};
8
9/// Get all HL1xxx rules.
10pub 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
32/// HL1001: Missing Chart.yaml
33pub 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
68/// HL1002: Invalid apiVersion
69pub 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
109/// HL1003: Missing required field 'name'
110pub 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
146/// HL1004: Missing required field 'version'
147pub 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
183/// HL1005: Version not valid SemVer
184pub 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
223/// HL1006: Missing description
224pub 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
266/// HL1007: Missing maintainers
267pub 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
303/// HL1008: Chart is deprecated
304pub 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
340/// HL1009: Missing templates directory
341pub 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        // Skip for library charts
362        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
386/// HL1010: Invalid chart type
387pub 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        // This is handled during parsing - if type is invalid, serde will fail
408        // or produce Unknown variant which we handle elsewhere
409        vec![]
410    }
411}
412
413/// HL1011: Missing values.yaml
414pub 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
448/// HL1012: Chart name contains invalid characters
449pub 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
488/// HL1013: Icon URL not HTTPS
489pub 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
527/// HL1014: Home URL not HTTPS
528pub 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
566/// HL1015: Duplicate dependency names
567pub 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
604/// HL1016: Dependency missing version
605pub 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
646/// HL1017: Dependency missing repository
647pub 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                    // Skip if it's a file:// reference (local dependency)
678                    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
693/// Check if a version string is valid SemVer.
694fn 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    // Check major and minor are numeric
701    for (i, part) in parts.iter().enumerate() {
702        // Allow pre-release and build metadata on the last part
703        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
717/// Check if a chart name is valid.
718fn is_valid_chart_name(name: &str) -> bool {
719    if name.is_empty() {
720        return false;
721    }
722
723    // Must start with a letter
724    if !name
725        .chars()
726        .next()
727        .map(|c| c.is_ascii_lowercase())
728        .unwrap_or(false)
729    {
730        return false;
731    }
732
733    // Must contain only lowercase letters, numbers, and hyphens
734    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}