syncable_cli/analyzer/kubelint/
config.rs1use crate::analyzer::kubelint::types::{ObjectKindsDesc, Severity};
10use serde::{Deserialize, Serialize};
11use std::collections::HashSet;
12use std::path::Path;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct KubelintConfig {
18 #[serde(default, rename = "addAllBuiltIn")]
20 pub add_all_builtin: bool,
21
22 #[serde(default)]
24 pub do_not_auto_add_defaults: bool,
25
26 #[serde(default)]
28 pub include: Vec<String>,
29
30 #[serde(default)]
32 pub exclude: Vec<String>,
33
34 #[serde(default)]
36 pub ignore_paths: Vec<String>,
37
38 #[serde(default)]
40 pub custom_checks: Vec<CheckSpec>,
41
42 #[serde(default)]
44 pub failure_threshold: Severity,
45
46 #[serde(default)]
48 pub no_fail: bool,
49}
50
51impl Default for KubelintConfig {
52 fn default() -> Self {
53 Self {
54 add_all_builtin: false,
55 do_not_auto_add_defaults: false,
56 include: Vec::new(),
57 exclude: Vec::new(),
58 ignore_paths: Vec::new(),
59 custom_checks: Vec::new(),
60 failure_threshold: Severity::Warning,
61 no_fail: false,
62 }
63 }
64}
65
66impl KubelintConfig {
67 pub fn new() -> Self {
69 Self::default()
70 }
71
72 pub fn include(mut self, check: impl Into<String>) -> Self {
74 self.include.push(check.into());
75 self
76 }
77
78 pub fn exclude(mut self, check: impl Into<String>) -> Self {
80 self.exclude.push(check.into());
81 self
82 }
83
84 pub fn ignore_path(mut self, pattern: impl Into<String>) -> Self {
86 self.ignore_paths.push(pattern.into());
87 self
88 }
89
90 pub fn with_threshold(mut self, threshold: Severity) -> Self {
92 self.failure_threshold = threshold;
93 self
94 }
95
96 pub fn with_all_builtin(mut self) -> Self {
98 self.add_all_builtin = true;
99 self
100 }
101
102 pub fn without_defaults(mut self) -> Self {
104 self.do_not_auto_add_defaults = true;
105 self
106 }
107
108 pub fn is_check_excluded(&self, check_name: &str) -> bool {
110 self.exclude.iter().any(|e| e == check_name)
111 }
112
113 pub fn is_check_included(&self, check_name: &str) -> bool {
115 self.include.iter().any(|e| e == check_name)
116 }
117
118 pub fn resolve_checks<'a>(&self, available: &'a [CheckSpec]) -> Vec<&'a CheckSpec> {
122 let default_checks: HashSet<&str> = DEFAULT_CHECKS.iter().copied().collect();
123
124 available
125 .iter()
126 .filter(|check| {
127 let name = check.name.as_str();
128
129 if self.is_check_excluded(name) {
131 return false;
132 }
133
134 if self.is_check_included(name) {
136 return true;
137 }
138
139 if self.add_all_builtin {
141 return true;
142 }
143
144 if !self.do_not_auto_add_defaults && default_checks.contains(name) {
146 return true;
147 }
148
149 false
150 })
151 .collect()
152 }
153
154 pub fn should_ignore_path(&self, path: &Path) -> bool {
156 let path_str = path.to_string_lossy();
157
158 for pattern in &self.ignore_paths {
159 if let Ok(glob) = glob::Pattern::new(pattern) {
160 if glob.matches(&path_str) {
161 return true;
162 }
163 }
164 if path_str.contains(pattern) {
166 return true;
167 }
168 }
169 false
170 }
171
172 pub fn load_from_file(path: &Path) -> Result<Self, ConfigError> {
174 let content =
175 std::fs::read_to_string(path).map_err(|e| ConfigError::IoError(e.to_string()))?;
176
177 Self::load_from_str(&content)
178 }
179
180 pub fn load_from_str(content: &str) -> Result<Self, ConfigError> {
182 serde_yaml::from_str(content).map_err(|e| ConfigError::ParseError(e.to_string()))
183 }
184
185 pub fn load_from_default() -> Option<Self> {
187 for filename in &[".kube-linter.yaml", ".kube-linter.yml"] {
188 let path = Path::new(filename);
189 if path.exists() {
190 if let Ok(config) = Self::load_from_file(path) {
191 return Some(config);
192 }
193 }
194 }
195 None
196 }
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct CheckSpec {
202 pub name: String,
204
205 pub description: String,
207
208 pub remediation: String,
210
211 pub template: String,
213
214 #[serde(default)]
216 pub params: serde_yaml::Value,
217
218 #[serde(default)]
220 pub scope: CheckScope,
221}
222
223impl CheckSpec {
224 pub fn new(
226 name: impl Into<String>,
227 description: impl Into<String>,
228 remediation: impl Into<String>,
229 template: impl Into<String>,
230 ) -> Self {
231 Self {
232 name: name.into(),
233 description: description.into(),
234 remediation: remediation.into(),
235 template: template.into(),
236 params: serde_yaml::Value::Null,
237 scope: CheckScope::default(),
238 }
239 }
240
241 pub fn with_params(mut self, params: serde_yaml::Value) -> Self {
243 self.params = params;
244 self
245 }
246
247 pub fn with_scope(mut self, scope: CheckScope) -> Self {
249 self.scope = scope;
250 self
251 }
252}
253
254#[derive(Debug, Clone, Default, Serialize, Deserialize)]
256pub struct CheckScope {
257 #[serde(default, rename = "objectKinds")]
259 pub object_kinds: ObjectKindsDesc,
260}
261
262impl CheckScope {
263 pub fn new(kinds: &[&str]) -> Self {
265 Self {
266 object_kinds: ObjectKindsDesc::new(kinds),
267 }
268 }
269}
270
271#[derive(Debug, Clone)]
273pub enum ConfigError {
274 IoError(String),
276 ParseError(String),
278}
279
280impl std::fmt::Display for ConfigError {
281 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282 match self {
283 ConfigError::IoError(msg) => write!(f, "I/O error: {}", msg),
284 ConfigError::ParseError(msg) => write!(f, "Parse error: {}", msg),
285 }
286 }
287}
288
289impl std::error::Error for ConfigError {}
290
291pub const DEFAULT_CHECKS: &[&str] = &[
293 "dangling-service",
294 "default-service-account",
295 "deprecated-service-account",
296 "drop-net-raw-capability",
297 "env-var-secret",
298 "host-mounts",
299 "mismatching-selector",
300 "no-anti-affinity",
301 "no-liveness-probe",
302 "no-readiness-probe",
303 "no-rolling-update-strategy",
304 "privilege-escalation",
305 "privileged-container",
306 "read-secret-from-env-var",
307 "run-as-non-root",
308 "ssh-port",
309 "unset-cpu-requirements",
310 "unset-memory-requirements",
311 "writable-host-mount",
312];
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn test_default_config() {
320 let config = KubelintConfig::default();
321 assert!(!config.add_all_builtin);
322 assert!(!config.do_not_auto_add_defaults);
323 assert!(config.include.is_empty());
324 assert!(config.exclude.is_empty());
325 assert_eq!(config.failure_threshold, Severity::Warning);
326 }
327
328 #[test]
329 fn test_config_builder() {
330 let config = KubelintConfig::new()
331 .include("custom-check")
332 .exclude("privileged-container")
333 .with_threshold(Severity::Error);
334
335 assert!(config.is_check_included("custom-check"));
336 assert!(config.is_check_excluded("privileged-container"));
337 assert_eq!(config.failure_threshold, Severity::Error);
338 }
339
340 #[test]
341 fn test_path_ignoring() {
342 let config = KubelintConfig::new()
343 .ignore_path("**/test/**")
344 .ignore_path("vendor/");
345
346 assert!(config.should_ignore_path(Path::new("vendor/k8s/deployment.yaml")));
347 }
349
350 #[test]
351 fn test_load_from_str() {
352 let yaml = r#"
353addAllBuiltIn: true
354exclude:
355 - latest-tag
356 - privileged-container
357include:
358 - custom-check
359failureThreshold: error
360"#;
361 let config = KubelintConfig::load_from_str(yaml).unwrap();
362 assert!(config.add_all_builtin);
363 assert!(config.is_check_excluded("latest-tag"));
364 assert!(config.is_check_excluded("privileged-container"));
365 assert!(config.is_check_included("custom-check"));
366 assert_eq!(config.failure_threshold, Severity::Error);
367 }
368
369 #[test]
370 fn test_check_spec() {
371 let check = CheckSpec::new(
372 "test-check",
373 "A test check",
374 "Fix the issue",
375 "test-template",
376 )
377 .with_scope(CheckScope::new(&["Deployment", "StatefulSet"]));
378
379 assert_eq!(check.name, "test-check");
380 assert_eq!(check.template, "test-template");
381 }
382}