syncable_cli/analyzer/helmlint/
config.rs

1//! Configuration for the helmlint linter.
2//!
3//! Provides configuration options for:
4//! - Enabling/disabling rules
5//! - Severity overrides
6//! - Kubernetes version targeting
7//! - Values schema validation
8
9use std::collections::{HashMap, HashSet};
10use std::path::PathBuf;
11
12use crate::analyzer::helmlint::types::Severity;
13
14/// Configuration for the helmlint linter.
15#[derive(Debug, Clone)]
16pub struct HelmlintConfig {
17    /// Rules to ignore (by code, e.g., "HL1001").
18    pub ignored_rules: HashSet<String>,
19
20    /// Severity overrides for specific rules.
21    pub severity_overrides: HashMap<String, Severity>,
22
23    /// Minimum severity threshold for reporting.
24    pub failure_threshold: Severity,
25
26    /// If true, ignore inline pragma comments.
27    pub disable_ignore_pragma: bool,
28
29    /// If true, don't fail even if errors are found.
30    pub no_fail: bool,
31
32    /// Target Kubernetes version for API deprecation checks.
33    pub k8s_version: Option<String>,
34
35    /// Path to a JSON schema for values.yaml validation.
36    pub values_schema_path: Option<PathBuf>,
37
38    /// Strict mode - treat warnings as errors.
39    pub strict: bool,
40
41    /// Only report fixable issues.
42    pub fixable_only: bool,
43
44    /// Files or patterns to exclude.
45    pub exclude_patterns: Vec<String>,
46}
47
48impl Default for HelmlintConfig {
49    fn default() -> Self {
50        Self {
51            ignored_rules: HashSet::new(),
52            severity_overrides: HashMap::new(),
53            failure_threshold: Severity::Warning,
54            disable_ignore_pragma: false,
55            no_fail: false,
56            k8s_version: None,
57            values_schema_path: None,
58            strict: false,
59            fixable_only: false,
60            exclude_patterns: Vec::new(),
61        }
62    }
63}
64
65impl HelmlintConfig {
66    /// Create a new default configuration.
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    /// Add a rule to ignore.
72    pub fn ignore(mut self, rule: impl Into<String>) -> Self {
73        self.ignored_rules.insert(rule.into());
74        self
75    }
76
77    /// Add multiple rules to ignore.
78    pub fn ignore_all(mut self, rules: impl IntoIterator<Item = impl Into<String>>) -> Self {
79        for rule in rules {
80            self.ignored_rules.insert(rule.into());
81        }
82        self
83    }
84
85    /// Override severity for a specific rule.
86    pub fn with_severity(mut self, rule: impl Into<String>, severity: Severity) -> Self {
87        self.severity_overrides.insert(rule.into(), severity);
88        self
89    }
90
91    /// Set the failure threshold.
92    pub fn with_threshold(mut self, threshold: Severity) -> Self {
93        self.failure_threshold = threshold;
94        self
95    }
96
97    /// Set the target Kubernetes version.
98    pub fn with_k8s_version(mut self, version: impl Into<String>) -> Self {
99        self.k8s_version = Some(version.into());
100        self
101    }
102
103    /// Set the values schema path.
104    pub fn with_values_schema(mut self, path: impl Into<PathBuf>) -> Self {
105        self.values_schema_path = Some(path.into());
106        self
107    }
108
109    /// Enable strict mode.
110    pub fn with_strict(mut self, strict: bool) -> Self {
111        self.strict = strict;
112        self
113    }
114
115    /// Check if a rule is ignored.
116    pub fn is_rule_ignored(&self, code: &str) -> bool {
117        self.ignored_rules.contains(code)
118    }
119
120    /// Get the effective severity for a rule.
121    pub fn effective_severity(&self, code: &str, default: Severity) -> Severity {
122        if let Some(&override_severity) = self.severity_overrides.get(code) {
123            override_severity
124        } else if self.strict && default == Severity::Warning {
125            Severity::Error
126        } else {
127            default
128        }
129    }
130
131    /// Check if a severity should be reported based on threshold.
132    pub fn should_report(&self, severity: Severity) -> bool {
133        severity >= self.failure_threshold
134    }
135
136    /// Check if a file is excluded.
137    pub fn is_excluded(&self, path: &str) -> bool {
138        for pattern in &self.exclude_patterns {
139            if path.contains(pattern) {
140                return true;
141            }
142            // Simple glob matching
143            if pattern.contains('*') {
144                let parts: Vec<&str> = pattern.split('*').collect();
145                let mut remaining = path;
146                let mut matched = true;
147                for (i, part) in parts.iter().enumerate() {
148                    if part.is_empty() {
149                        continue;
150                    }
151                    if i == 0 {
152                        if !remaining.starts_with(part) {
153                            matched = false;
154                            break;
155                        }
156                        remaining = &remaining[part.len()..];
157                    } else if i == parts.len() - 1 {
158                        if !remaining.ends_with(part) {
159                            matched = false;
160                            break;
161                        }
162                    } else if let Some(pos) = remaining.find(part) {
163                        remaining = &remaining[pos + part.len()..];
164                    } else {
165                        matched = false;
166                        break;
167                    }
168                }
169                if matched {
170                    return true;
171                }
172            }
173        }
174        false
175    }
176
177    /// Parse Kubernetes version string to (major, minor).
178    pub fn parse_k8s_version(&self) -> Option<(u32, u32)> {
179        self.k8s_version.as_ref().and_then(|v| {
180            let v = v.trim_start_matches('v');
181            let parts: Vec<&str> = v.split('.').collect();
182            if parts.len() >= 2 {
183                let major = parts[0].parse().ok()?;
184                let minor = parts[1].parse().ok()?;
185                Some((major, minor))
186            } else {
187                None
188            }
189        })
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_default_config() {
199        let config = HelmlintConfig::default();
200        assert!(config.ignored_rules.is_empty());
201        assert!(config.severity_overrides.is_empty());
202        assert_eq!(config.failure_threshold, Severity::Warning);
203        assert!(!config.strict);
204    }
205
206    #[test]
207    fn test_ignore_rule() {
208        let config = HelmlintConfig::default().ignore("HL1001");
209        assert!(config.is_rule_ignored("HL1001"));
210        assert!(!config.is_rule_ignored("HL1002"));
211    }
212
213    #[test]
214    fn test_severity_override() {
215        let config = HelmlintConfig::default().with_severity("HL1001", Severity::Error);
216        assert_eq!(
217            config.effective_severity("HL1001", Severity::Warning),
218            Severity::Error
219        );
220        assert_eq!(
221            config.effective_severity("HL1002", Severity::Warning),
222            Severity::Warning
223        );
224    }
225
226    #[test]
227    fn test_strict_mode() {
228        let config = HelmlintConfig::default().with_strict(true);
229        assert_eq!(
230            config.effective_severity("HL1001", Severity::Warning),
231            Severity::Error
232        );
233        assert_eq!(
234            config.effective_severity("HL1001", Severity::Info),
235            Severity::Info
236        );
237    }
238
239    #[test]
240    fn test_k8s_version_parsing() {
241        let config = HelmlintConfig::default().with_k8s_version("v1.28");
242        assert_eq!(config.parse_k8s_version(), Some((1, 28)));
243
244        let config = HelmlintConfig::default().with_k8s_version("1.25.0");
245        assert_eq!(config.parse_k8s_version(), Some((1, 25)));
246    }
247
248    #[test]
249    fn test_exclusion() {
250        let mut config = HelmlintConfig::default();
251        config.exclude_patterns = vec!["test".to_string(), "*.bak".to_string()];
252
253        assert!(config.is_excluded("templates/test.yaml"));
254        assert!(config.is_excluded("backup.bak"));
255        assert!(!config.is_excluded("templates/deployment.yaml"));
256    }
257}