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 from_str(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 = std::fs::read_to_string(path)
118 .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 = serde_yaml::from_str(yaml)
125 .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 if let Some(label_type) = LabelType::from_str(type_str) {
184 config.label_schema.insert(label.to_string(), label_type);
185 }
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 if let Some(severity) = Severity::from_str(threshold) {
204 config.failure_threshold = severity;
205 }
206 }
207
208 Ok(config)
209 }
210
211 pub fn find_and_load() -> Option<Self> {
219 let search_paths = [
220 ".hadolint.yaml",
221 ".hadolint.yml",
222 ];
223
224 for path in &search_paths {
225 let path = Path::new(path);
226 if path.exists() {
227 if let Ok(config) = Self::from_yaml_file(path) {
228 return Some(config);
229 }
230 }
231 }
232
233 if let Some(config_dir) = dirs::config_dir() {
235 let xdg_path = config_dir.join("hadolint.yaml");
236 if xdg_path.exists() {
237 if let Ok(config) = Self::from_yaml_file(&xdg_path) {
238 return Some(config);
239 }
240 }
241 }
242
243 if let Some(home_dir) = dirs::home_dir() {
245 let home_path = home_dir.join(".hadolint.yaml");
246 if home_path.exists() {
247 if let Ok(config) = Self::from_yaml_file(&home_path) {
248 return Some(config);
249 }
250 }
251 }
252
253 None
254 }
255
256 pub fn is_rule_ignored(&self, code: &RuleCode) -> bool {
258 self.ignore_rules.contains(code)
259 }
260
261 pub fn effective_severity(&self, code: &RuleCode, default: Severity) -> Severity {
263 if self.error_rules.contains(code) {
264 return Severity::Error;
265 }
266 if self.warning_rules.contains(code) {
267 return Severity::Warning;
268 }
269 if self.info_rules.contains(code) {
270 return Severity::Info;
271 }
272 if self.style_rules.contains(code) {
273 return Severity::Style;
274 }
275 default
276 }
277
278 pub fn ignore(mut self, code: impl Into<RuleCode>) -> Self {
280 self.ignore_rules.insert(code.into());
281 self
282 }
283
284 pub fn allow_registry(mut self, registry: impl Into<String>) -> Self {
286 self.allowed_registries.insert(registry.into());
287 self
288 }
289
290 pub fn with_threshold(mut self, threshold: Severity) -> Self {
292 self.failure_threshold = threshold;
293 self
294 }
295}
296
297#[derive(Debug, Clone)]
299pub enum ConfigError {
300 IoError(String),
302 ParseError(String),
304}
305
306impl std::fmt::Display for ConfigError {
307 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308 match self {
309 Self::IoError(msg) => write!(f, "I/O error: {}", msg),
310 Self::ParseError(msg) => write!(f, "Parse error: {}", msg),
311 }
312 }
313}
314
315impl std::error::Error for ConfigError {}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_default_config() {
323 let config = HadolintConfig::default();
324 assert!(config.ignore_rules.is_empty());
325 assert!(!config.strict_labels);
326 assert!(!config.disable_ignore_pragma);
327 assert_eq!(config.failure_threshold, Severity::Info);
328 }
329
330 #[test]
331 fn test_yaml_parsing() {
332 let yaml = r#"
333ignored:
334 - DL3008
335 - DL3009
336
337override:
338 error:
339 - DL3001
340 warning:
341 - DL3002
342
343trustedRegistries:
344 - docker.io
345 - gcr.io
346
347failure-threshold: warning
348strict-labels: true
349"#;
350
351 let config = HadolintConfig::from_yaml_str(yaml).unwrap();
352 assert!(config.ignore_rules.contains(&RuleCode::new("DL3008")));
353 assert!(config.ignore_rules.contains(&RuleCode::new("DL3009")));
354 assert!(config.error_rules.contains(&RuleCode::new("DL3001")));
355 assert!(config.warning_rules.contains(&RuleCode::new("DL3002")));
356 assert!(config.allowed_registries.contains("docker.io"));
357 assert!(config.allowed_registries.contains("gcr.io"));
358 assert_eq!(config.failure_threshold, Severity::Warning);
359 assert!(config.strict_labels);
360 }
361
362 #[test]
363 fn test_effective_severity() {
364 let config = HadolintConfig::default()
365 .ignore("DL3008".to_string());
366
367 assert!(config.is_rule_ignored(&RuleCode::new("DL3008")));
368 assert!(!config.is_rule_ignored(&RuleCode::new("DL3009")));
369 }
370
371 #[test]
372 fn test_builder_pattern() {
373 let config = HadolintConfig::new()
374 .ignore("DL3008")
375 .allow_registry("docker.io")
376 .with_threshold(Severity::Warning);
377
378 assert!(config.ignore_rules.contains(&RuleCode::new("DL3008")));
379 assert!(config.allowed_registries.contains("docker.io"));
380 assert_eq!(config.failure_threshold, Severity::Warning);
381 }
382}