syncable_cli/analyzer/hadolint/
config.rs1use crate::analyzer::hadolint::types::{RuleCode, Severity};
13use std::collections::{HashMap, HashSet};
14use std::path::Path;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum LabelType {
19 Email,
21 GitHash,
23 RawText,
25 Rfc3339,
27 SemVer,
29 Spdx,
31 Url,
33}
34
35impl LabelType {
36 pub fn parse(s: &str) -> Option<Self> {
38 match s.to_lowercase().as_str() {
39 "email" => Some(Self::Email),
40 "hash" => Some(Self::GitHash),
41 "text" | "" => Some(Self::RawText),
42 "rfc3339" => Some(Self::Rfc3339),
43 "semver" => Some(Self::SemVer),
44 "spdx" => Some(Self::Spdx),
45 "url" => Some(Self::Url),
46 _ => None,
47 }
48 }
49
50 pub fn as_str(&self) -> &'static str {
52 match self {
53 Self::Email => "email",
54 Self::GitHash => "hash",
55 Self::RawText => "text",
56 Self::Rfc3339 => "rfc3339",
57 Self::SemVer => "semver",
58 Self::Spdx => "spdx",
59 Self::Url => "url",
60 }
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct HadolintConfig {
67 pub ignore_rules: HashSet<RuleCode>,
69 pub error_rules: HashSet<RuleCode>,
71 pub warning_rules: HashSet<RuleCode>,
73 pub info_rules: HashSet<RuleCode>,
75 pub style_rules: HashSet<RuleCode>,
77 pub allowed_registries: HashSet<String>,
79 pub label_schema: HashMap<String, LabelType>,
81 pub strict_labels: bool,
83 pub disable_ignore_pragma: bool,
85 pub failure_threshold: Severity,
87 pub no_fail: bool,
89}
90
91impl Default for HadolintConfig {
92 fn default() -> Self {
93 Self {
94 ignore_rules: HashSet::new(),
95 error_rules: HashSet::new(),
96 warning_rules: HashSet::new(),
97 info_rules: HashSet::new(),
98 style_rules: HashSet::new(),
99 allowed_registries: HashSet::new(),
100 label_schema: HashMap::new(),
101 strict_labels: false,
102 disable_ignore_pragma: false,
103 failure_threshold: Severity::Info,
104 no_fail: false,
105 }
106 }
107}
108
109impl HadolintConfig {
110 pub fn new() -> Self {
112 Self::default()
113 }
114
115 pub fn from_yaml_file(path: &Path) -> Result<Self, ConfigError> {
117 let content =
118 std::fs::read_to_string(path).map_err(|e| ConfigError::IoError(e.to_string()))?;
119 Self::from_yaml_str(&content)
120 }
121
122 pub fn from_yaml_str(yaml: &str) -> Result<Self, ConfigError> {
124 let value: serde_yaml::Value =
125 serde_yaml::from_str(yaml).map_err(|e| ConfigError::ParseError(e.to_string()))?;
126
127 let mut config = Self::default();
128
129 if let Some(ignored) = value.get("ignored").and_then(|v| v.as_sequence()) {
131 for item in ignored {
132 if let Some(code) = item.as_str() {
133 config.ignore_rules.insert(RuleCode::new(code));
134 }
135 }
136 }
137
138 if let Some(overrides) = value.get("override").and_then(|v| v.as_mapping()) {
140 if let Some(errors) = overrides.get("error").and_then(|v| v.as_sequence()) {
141 for item in errors {
142 if let Some(code) = item.as_str() {
143 config.error_rules.insert(RuleCode::new(code));
144 }
145 }
146 }
147 if let Some(warnings) = overrides.get("warning").and_then(|v| v.as_sequence()) {
148 for item in warnings {
149 if let Some(code) = item.as_str() {
150 config.warning_rules.insert(RuleCode::new(code));
151 }
152 }
153 }
154 if let Some(infos) = overrides.get("info").and_then(|v| v.as_sequence()) {
155 for item in infos {
156 if let Some(code) = item.as_str() {
157 config.info_rules.insert(RuleCode::new(code));
158 }
159 }
160 }
161 if let Some(styles) = overrides.get("style").and_then(|v| v.as_sequence()) {
162 for item in styles {
163 if let Some(code) = item.as_str() {
164 config.style_rules.insert(RuleCode::new(code));
165 }
166 }
167 }
168 }
169
170 if let Some(registries) = value.get("trustedRegistries").and_then(|v| v.as_sequence()) {
172 for item in registries {
173 if let Some(registry) = item.as_str() {
174 config.allowed_registries.insert(registry.to_string());
175 }
176 }
177 }
178
179 if let Some(schema) = value.get("label-schema").and_then(|v| v.as_mapping()) {
181 for (key, val) in schema {
182 if let (Some(label), Some(type_str)) = (key.as_str(), val.as_str())
183 && let Some(label_type) = LabelType::parse(type_str)
184 {
185 config.label_schema.insert(label.to_string(), label_type);
186 }
187 }
188 }
189
190 if let Some(strict) = value.get("strict-labels").and_then(|v| v.as_bool()) {
192 config.strict_labels = strict;
193 }
194 if let Some(disable) = value.get("disable-ignore-pragma").and_then(|v| v.as_bool()) {
195 config.disable_ignore_pragma = disable;
196 }
197 if let Some(no_fail) = value.get("no-fail").and_then(|v| v.as_bool()) {
198 config.no_fail = no_fail;
199 }
200
201 if let Some(threshold) = value.get("failure-threshold").and_then(|v| v.as_str())
203 && let Some(severity) = Severity::parse(threshold)
204 {
205 config.failure_threshold = severity;
206 }
207
208 Ok(config)
209 }
210
211 pub fn find_and_load() -> Option<Self> {
219 let search_paths = [".hadolint.yaml", ".hadolint.yml"];
220
221 for path in &search_paths {
222 let path = Path::new(path);
223 if path.exists()
224 && let Ok(config) = Self::from_yaml_file(path)
225 {
226 return Some(config);
227 }
228 }
229
230 if let Some(config_dir) = dirs::config_dir() {
232 let xdg_path = config_dir.join("hadolint.yaml");
233 if xdg_path.exists()
234 && let Ok(config) = Self::from_yaml_file(&xdg_path)
235 {
236 return Some(config);
237 }
238 }
239
240 if let Some(home_dir) = dirs::home_dir() {
242 let home_path = home_dir.join(".hadolint.yaml");
243 if home_path.exists()
244 && let Ok(config) = Self::from_yaml_file(&home_path)
245 {
246 return Some(config);
247 }
248 }
249
250 None
251 }
252
253 pub fn is_rule_ignored(&self, code: &RuleCode) -> bool {
255 self.ignore_rules.contains(code)
256 }
257
258 pub fn effective_severity(&self, code: &RuleCode, default: Severity) -> Severity {
260 if self.error_rules.contains(code) {
261 return Severity::Error;
262 }
263 if self.warning_rules.contains(code) {
264 return Severity::Warning;
265 }
266 if self.info_rules.contains(code) {
267 return Severity::Info;
268 }
269 if self.style_rules.contains(code) {
270 return Severity::Style;
271 }
272 default
273 }
274
275 pub fn ignore(mut self, code: impl Into<RuleCode>) -> Self {
277 self.ignore_rules.insert(code.into());
278 self
279 }
280
281 pub fn allow_registry(mut self, registry: impl Into<String>) -> Self {
283 self.allowed_registries.insert(registry.into());
284 self
285 }
286
287 pub fn with_threshold(mut self, threshold: Severity) -> Self {
289 self.failure_threshold = threshold;
290 self
291 }
292}
293
294#[derive(Debug, Clone)]
296pub enum ConfigError {
297 IoError(String),
299 ParseError(String),
301}
302
303impl std::fmt::Display for ConfigError {
304 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305 match self {
306 Self::IoError(msg) => write!(f, "I/O error: {}", msg),
307 Self::ParseError(msg) => write!(f, "Parse error: {}", msg),
308 }
309 }
310}
311
312impl std::error::Error for ConfigError {}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn test_default_config() {
320 let config = HadolintConfig::default();
321 assert!(config.ignore_rules.is_empty());
322 assert!(!config.strict_labels);
323 assert!(!config.disable_ignore_pragma);
324 assert_eq!(config.failure_threshold, Severity::Info);
325 }
326
327 #[test]
328 fn test_yaml_parsing() {
329 let yaml = r#"
330ignored:
331 - DL3008
332 - DL3009
333
334override:
335 error:
336 - DL3001
337 warning:
338 - DL3002
339
340trustedRegistries:
341 - docker.io
342 - gcr.io
343
344failure-threshold: warning
345strict-labels: true
346"#;
347
348 let config = HadolintConfig::from_yaml_str(yaml).unwrap();
349 assert!(config.ignore_rules.contains(&RuleCode::new("DL3008")));
350 assert!(config.ignore_rules.contains(&RuleCode::new("DL3009")));
351 assert!(config.error_rules.contains(&RuleCode::new("DL3001")));
352 assert!(config.warning_rules.contains(&RuleCode::new("DL3002")));
353 assert!(config.allowed_registries.contains("docker.io"));
354 assert!(config.allowed_registries.contains("gcr.io"));
355 assert_eq!(config.failure_threshold, Severity::Warning);
356 assert!(config.strict_labels);
357 }
358
359 #[test]
360 fn test_effective_severity() {
361 let config = HadolintConfig::default().ignore("DL3008".to_string());
362
363 assert!(config.is_rule_ignored(&RuleCode::new("DL3008")));
364 assert!(!config.is_rule_ignored(&RuleCode::new("DL3009")));
365 }
366
367 #[test]
368 fn test_builder_pattern() {
369 let config = HadolintConfig::new()
370 .ignore("DL3008")
371 .allow_registry("docker.io")
372 .with_threshold(Severity::Warning);
373
374 assert!(config.ignore_rules.contains(&RuleCode::new("DL3008")));
375 assert!(config.allowed_registries.contains("docker.io"));
376 assert_eq!(config.failure_threshold, Severity::Warning);
377 }
378}