use crate::analyzer::kubelint::types::{ObjectKindsDesc, Severity};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KubelintConfig {
#[serde(default, rename = "addAllBuiltIn")]
pub add_all_builtin: bool,
#[serde(default)]
pub do_not_auto_add_defaults: bool,
#[serde(default)]
pub include: Vec<String>,
#[serde(default)]
pub exclude: Vec<String>,
#[serde(default)]
pub ignore_paths: Vec<String>,
#[serde(default)]
pub custom_checks: Vec<CheckSpec>,
#[serde(default)]
pub failure_threshold: Severity,
#[serde(default)]
pub no_fail: bool,
}
impl Default for KubelintConfig {
fn default() -> Self {
Self {
add_all_builtin: false,
do_not_auto_add_defaults: false,
include: Vec::new(),
exclude: Vec::new(),
ignore_paths: Vec::new(),
custom_checks: Vec::new(),
failure_threshold: Severity::Warning,
no_fail: false,
}
}
}
impl KubelintConfig {
pub fn new() -> Self {
Self::default()
}
pub fn include(mut self, check: impl Into<String>) -> Self {
self.include.push(check.into());
self
}
pub fn exclude(mut self, check: impl Into<String>) -> Self {
self.exclude.push(check.into());
self
}
pub fn ignore_path(mut self, pattern: impl Into<String>) -> Self {
self.ignore_paths.push(pattern.into());
self
}
pub fn with_threshold(mut self, threshold: Severity) -> Self {
self.failure_threshold = threshold;
self
}
pub fn with_all_builtin(mut self) -> Self {
self.add_all_builtin = true;
self
}
pub fn without_defaults(mut self) -> Self {
self.do_not_auto_add_defaults = true;
self
}
pub fn is_check_excluded(&self, check_name: &str) -> bool {
self.exclude.iter().any(|e| e == check_name)
}
pub fn is_check_included(&self, check_name: &str) -> bool {
self.include.iter().any(|e| e == check_name)
}
pub fn resolve_checks<'a>(&self, available: &'a [CheckSpec]) -> Vec<&'a CheckSpec> {
let default_checks: HashSet<&str> = DEFAULT_CHECKS.iter().copied().collect();
available
.iter()
.filter(|check| {
let name = check.name.as_str();
if self.is_check_excluded(name) {
return false;
}
if self.is_check_included(name) {
return true;
}
if self.add_all_builtin {
return true;
}
if !self.do_not_auto_add_defaults && default_checks.contains(name) {
return true;
}
false
})
.collect()
}
pub fn should_ignore_path(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
for pattern in &self.ignore_paths {
if let Ok(glob) = glob::Pattern::new(pattern)
&& glob.matches(&path_str)
{
return true;
}
if path_str.contains(pattern) {
return true;
}
}
false
}
pub fn load_from_file(path: &Path) -> Result<Self, ConfigError> {
let content =
std::fs::read_to_string(path).map_err(|e| ConfigError::IoError(e.to_string()))?;
Self::load_from_str(&content)
}
pub fn load_from_str(content: &str) -> Result<Self, ConfigError> {
serde_yaml::from_str(content).map_err(|e| ConfigError::ParseError(e.to_string()))
}
pub fn load_from_default() -> Option<Self> {
for filename in &[".kube-linter.yaml", ".kube-linter.yml"] {
let path = Path::new(filename);
if path.exists()
&& let Ok(config) = Self::load_from_file(path)
{
return Some(config);
}
}
None
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckSpec {
pub name: String,
pub description: String,
pub remediation: String,
pub template: String,
#[serde(default)]
pub params: serde_yaml::Value,
#[serde(default)]
pub scope: CheckScope,
}
impl CheckSpec {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
remediation: impl Into<String>,
template: impl Into<String>,
) -> Self {
Self {
name: name.into(),
description: description.into(),
remediation: remediation.into(),
template: template.into(),
params: serde_yaml::Value::Null,
scope: CheckScope::default(),
}
}
pub fn with_params(mut self, params: serde_yaml::Value) -> Self {
self.params = params;
self
}
pub fn with_scope(mut self, scope: CheckScope) -> Self {
self.scope = scope;
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CheckScope {
#[serde(default, rename = "objectKinds")]
pub object_kinds: ObjectKindsDesc,
}
impl CheckScope {
pub fn new(kinds: &[&str]) -> Self {
Self {
object_kinds: ObjectKindsDesc::new(kinds),
}
}
}
#[derive(Debug, Clone)]
pub enum ConfigError {
IoError(String),
ParseError(String),
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigError::IoError(msg) => write!(f, "I/O error: {}", msg),
ConfigError::ParseError(msg) => write!(f, "Parse error: {}", msg),
}
}
}
impl std::error::Error for ConfigError {}
pub const DEFAULT_CHECKS: &[&str] = &[
"dangling-service",
"default-service-account",
"deprecated-service-account",
"drop-net-raw-capability",
"env-var-secret",
"host-mounts",
"mismatching-selector",
"no-anti-affinity",
"no-liveness-probe",
"no-readiness-probe",
"no-rolling-update-strategy",
"privilege-escalation",
"privileged-container",
"read-secret-from-env-var",
"run-as-non-root",
"ssh-port",
"unset-cpu-requirements",
"unset-memory-requirements",
"writable-host-mount",
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = KubelintConfig::default();
assert!(!config.add_all_builtin);
assert!(!config.do_not_auto_add_defaults);
assert!(config.include.is_empty());
assert!(config.exclude.is_empty());
assert_eq!(config.failure_threshold, Severity::Warning);
}
#[test]
fn test_config_builder() {
let config = KubelintConfig::new()
.include("custom-check")
.exclude("privileged-container")
.with_threshold(Severity::Error);
assert!(config.is_check_included("custom-check"));
assert!(config.is_check_excluded("privileged-container"));
assert_eq!(config.failure_threshold, Severity::Error);
}
#[test]
fn test_path_ignoring() {
let config = KubelintConfig::new()
.ignore_path("**/test/**")
.ignore_path("vendor/");
assert!(config.should_ignore_path(Path::new("vendor/k8s/deployment.yaml")));
}
#[test]
fn test_load_from_str() {
let yaml = r#"
addAllBuiltIn: true
exclude:
- latest-tag
- privileged-container
include:
- custom-check
failureThreshold: error
"#;
let config = KubelintConfig::load_from_str(yaml).unwrap();
assert!(config.add_all_builtin);
assert!(config.is_check_excluded("latest-tag"));
assert!(config.is_check_excluded("privileged-container"));
assert!(config.is_check_included("custom-check"));
assert_eq!(config.failure_threshold, Severity::Error);
}
#[test]
fn test_check_spec() {
let check = CheckSpec::new(
"test-check",
"A test check",
"Fix the issue",
"test-template",
)
.with_scope(CheckScope::new(&["Deployment", "StatefulSet"]));
assert_eq!(check.name, "test-check");
assert_eq!(check.template, "test-template");
}
}