syncable_cli/analyzer/kubelint/templates/
mod.rs

1//! Check templates for kube-linter.
2//!
3//! Templates are reusable check implementations that can be configured
4//! with parameters to create specific checks.
5
6pub mod antiaffinity;
7pub mod capabilities;
8pub mod dangling;
9pub mod envvar;
10pub mod hostmounts;
11pub mod hostnetwork;
12pub mod latesttag;
13pub mod livenessprobe;
14pub mod misc;
15pub mod pdb;
16pub mod ports;
17pub mod privileged;
18pub mod privilegeescalation;
19pub mod rbac;
20pub mod readinessprobe;
21pub mod readonlyrootfs;
22pub mod replicas;
23pub mod requirements;
24pub mod runasnonroot;
25pub mod serviceaccount;
26pub mod unsafeprocmount;
27pub mod updateconfig;
28pub mod validation;
29
30use crate::analyzer::kubelint::context::Object;
31use crate::analyzer::kubelint::types::{Diagnostic, ObjectKindsDesc};
32use std::collections::HashMap;
33use std::sync::OnceLock;
34
35/// A check function that analyzes a Kubernetes object.
36pub trait CheckFunc: Send + Sync {
37    /// Run the check on an object and return any diagnostics.
38    fn check(&self, object: &Object) -> Vec<Diagnostic>;
39}
40
41/// Parameter description for a template.
42#[derive(Debug, Clone)]
43pub struct ParameterDesc {
44    /// Parameter name.
45    pub name: String,
46    /// Human-readable description.
47    pub description: String,
48    /// Parameter type (string, bool, int, array, etc.).
49    pub param_type: String,
50    /// Whether the parameter is required.
51    pub required: bool,
52    /// Default value (if any).
53    pub default: Option<serde_yaml::Value>,
54}
55
56/// A template for creating checks.
57pub trait Template: Send + Sync {
58    /// Get the template key (unique identifier).
59    fn key(&self) -> &str;
60
61    /// Get the human-readable name.
62    fn human_name(&self) -> &str;
63
64    /// Get the template description.
65    fn description(&self) -> &str;
66
67    /// Get the supported object kinds.
68    fn supported_object_kinds(&self) -> ObjectKindsDesc;
69
70    /// Get parameter descriptions.
71    fn parameters(&self) -> Vec<ParameterDesc>;
72
73    /// Instantiate a check function with the given parameters.
74    fn instantiate(&self, params: &serde_yaml::Value) -> Result<Box<dyn CheckFunc>, TemplateError>;
75}
76
77/// Template instantiation errors.
78#[derive(Debug, Clone)]
79pub enum TemplateError {
80    /// Missing required parameter.
81    MissingParameter(String),
82    /// Invalid parameter value.
83    InvalidParameter(String),
84    /// Unknown template.
85    UnknownTemplate(String),
86}
87
88impl std::fmt::Display for TemplateError {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        match self {
91            Self::MissingParameter(name) => write!(f, "Missing required parameter: {}", name),
92            Self::InvalidParameter(msg) => write!(f, "Invalid parameter: {}", msg),
93            Self::UnknownTemplate(key) => write!(f, "Unknown template: {}", key),
94        }
95    }
96}
97
98impl std::error::Error for TemplateError {}
99
100/// Global template registry.
101static REGISTRY: OnceLock<HashMap<String, Box<dyn Template>>> = OnceLock::new();
102
103/// Get the template registry, initializing if needed.
104pub fn registry() -> &'static HashMap<String, Box<dyn Template>> {
105    REGISTRY.get_or_init(|| {
106        let mut map: HashMap<String, Box<dyn Template>> = HashMap::new();
107
108        // Register all built-in templates
109        map.insert(
110            "privileged".to_string(),
111            Box::new(privileged::PrivilegedTemplate),
112        );
113        map.insert(
114            "privilege-escalation".to_string(),
115            Box::new(privilegeescalation::PrivilegeEscalationTemplate),
116        );
117        map.insert(
118            "run-as-non-root".to_string(),
119            Box::new(runasnonroot::RunAsNonRootTemplate),
120        );
121        map.insert(
122            "read-only-root-fs".to_string(),
123            Box::new(readonlyrootfs::ReadOnlyRootFsTemplate),
124        );
125        map.insert(
126            "latest-tag".to_string(),
127            Box::new(latesttag::LatestTagTemplate),
128        );
129        map.insert(
130            "liveness-probe".to_string(),
131            Box::new(livenessprobe::LivenessProbeTemplate),
132        );
133        map.insert(
134            "readiness-probe".to_string(),
135            Box::new(readinessprobe::ReadinessProbeTemplate),
136        );
137        map.insert(
138            "cpu-requirements".to_string(),
139            Box::new(requirements::CpuRequirementsTemplate),
140        );
141        map.insert(
142            "memory-requirements".to_string(),
143            Box::new(requirements::MemoryRequirementsTemplate),
144        );
145        map.insert(
146            "anti-affinity".to_string(),
147            Box::new(antiaffinity::AntiAffinityTemplate),
148        );
149        map.insert(
150            "drop-net-raw-capability".to_string(),
151            Box::new(capabilities::DropNetRawCapabilityTemplate),
152        );
153        map.insert(
154            "host-mounts".to_string(),
155            Box::new(hostmounts::HostMountsTemplate),
156        );
157        map.insert(
158            "writable-host-mount".to_string(),
159            Box::new(hostmounts::WritableHostMountTemplate),
160        );
161        map.insert(
162            "service-account".to_string(),
163            Box::new(serviceaccount::ServiceAccountTemplate),
164        );
165        map.insert(
166            "deprecated-service-account-field".to_string(),
167            Box::new(serviceaccount::DeprecatedServiceAccountFieldTemplate),
168        );
169        map.insert(
170            "rolling-update-strategy".to_string(),
171            Box::new(updateconfig::RollingUpdateStrategyTemplate),
172        );
173
174        // Host namespace templates
175        map.insert(
176            "host-network".to_string(),
177            Box::new(hostnetwork::HostNetworkTemplate),
178        );
179        map.insert(
180            "host-pid".to_string(),
181            Box::new(hostnetwork::HostPIDTemplate),
182        );
183        map.insert(
184            "host-ipc".to_string(),
185            Box::new(hostnetwork::HostIPCTemplate),
186        );
187
188        // Replica and scaling templates
189        map.insert("replicas".to_string(), Box::new(replicas::ReplicasTemplate));
190
191        // Unsafe proc mount template
192        map.insert(
193            "unsafe-proc-mount".to_string(),
194            Box::new(unsafeprocmount::UnsafeProcMountTemplate),
195        );
196
197        // Environment variable templates
198        map.insert(
199            "env-var-secret".to_string(),
200            Box::new(envvar::EnvVarSecretTemplate),
201        );
202        map.insert(
203            "read-secret-from-env-var".to_string(),
204            Box::new(envvar::ReadSecretFromEnvVarTemplate),
205        );
206        map.insert(
207            "duplicate-env-var".to_string(),
208            Box::new(envvar::DuplicateEnvVarTemplate),
209        );
210
211        // Port templates
212        map.insert(
213            "privileged-ports".to_string(),
214            Box::new(ports::PrivilegedPortsTemplate),
215        );
216        map.insert("ssh-port".to_string(), Box::new(ports::SSHPortTemplate));
217        map.insert(
218            "liveness-port".to_string(),
219            Box::new(ports::LivenessPortTemplate),
220        );
221        map.insert(
222            "readiness-port".to_string(),
223            Box::new(ports::ReadinessPortTemplate),
224        );
225
226        // RBAC templates
227        map.insert(
228            "cluster-admin-role-binding".to_string(),
229            Box::new(rbac::ClusterAdminRoleBindingTemplate),
230        );
231        map.insert(
232            "wildcard-in-rules".to_string(),
233            Box::new(rbac::WildcardInRulesTemplate),
234        );
235        map.insert(
236            "access-to-secrets".to_string(),
237            Box::new(rbac::AccessToSecretsTemplate),
238        );
239        map.insert(
240            "access-to-create-pods".to_string(),
241            Box::new(rbac::AccessToCreatePodsTemplate),
242        );
243
244        // PDB templates
245        map.insert(
246            "pdb-max-unavailable".to_string(),
247            Box::new(pdb::PdbMaxUnavailableTemplate),
248        );
249        map.insert(
250            "pdb-min-available".to_string(),
251            Box::new(pdb::PdbMinAvailableTemplate),
252        );
253        map.insert(
254            "pdb-unhealthy-pod-eviction-policy".to_string(),
255            Box::new(pdb::PdbUnhealthyPodEvictionPolicyTemplate),
256        );
257
258        // Validation templates
259        map.insert(
260            "use-namespace".to_string(),
261            Box::new(validation::UseNamespaceTemplate),
262        );
263        map.insert(
264            "restart-policy".to_string(),
265            Box::new(validation::RestartPolicyTemplate),
266        );
267        map.insert(
268            "required-annotation".to_string(),
269            Box::new(validation::RequiredAnnotationTemplate),
270        );
271        map.insert(
272            "required-label".to_string(),
273            Box::new(validation::RequiredLabelTemplate),
274        );
275        map.insert(
276            "disallowed-gvk".to_string(),
277            Box::new(validation::DisallowedGVKTemplate),
278        );
279        map.insert(
280            "mismatching-selector".to_string(),
281            Box::new(validation::MismatchingSelectorTemplate),
282        );
283        map.insert(
284            "node-affinity".to_string(),
285            Box::new(validation::NodeAffinityTemplate),
286        );
287        map.insert(
288            "job-ttl-seconds-after-finished".to_string(),
289            Box::new(validation::JobTtlSecondsAfterFinishedTemplate),
290        );
291        map.insert(
292            "priority-class-name".to_string(),
293            Box::new(validation::PriorityClassNameTemplate),
294        );
295        map.insert(
296            "service-type".to_string(),
297            Box::new(validation::ServiceTypeTemplate),
298        );
299        map.insert(
300            "hpa-min-replicas".to_string(),
301            Box::new(validation::HpaMinReplicasTemplate),
302        );
303
304        // Misc templates
305        map.insert("sysctls".to_string(), Box::new(misc::SysctlsTemplate));
306        map.insert(
307            "dnsconfig-options".to_string(),
308            Box::new(misc::DnsConfigOptionsTemplate),
309        );
310        map.insert(
311            "startup-port".to_string(),
312            Box::new(misc::StartupPortTemplate),
313        );
314        map.insert(
315            "env-var-value-from".to_string(),
316            Box::new(misc::EnvVarValueFromTemplate),
317        );
318        map.insert(
319            "target-port".to_string(),
320            Box::new(misc::TargetPortTemplate),
321        );
322
323        // Dangling resource templates (cross-resource validation)
324        map.insert(
325            "dangling-service".to_string(),
326            Box::new(dangling::DanglingServiceTemplate),
327        );
328        map.insert(
329            "dangling-ingress".to_string(),
330            Box::new(dangling::DanglingIngressTemplate),
331        );
332        map.insert(
333            "dangling-hpa".to_string(),
334            Box::new(dangling::DanglingHpaTemplate),
335        );
336        map.insert(
337            "dangling-network-policy".to_string(),
338            Box::new(dangling::DanglingNetworkPolicyTemplate),
339        );
340        map.insert(
341            "dangling-network-policy-peer".to_string(),
342            Box::new(dangling::DanglingNetworkPolicyPeerTemplate),
343        );
344        map.insert(
345            "dangling-service-monitor".to_string(),
346            Box::new(dangling::DanglingServiceMonitorTemplate),
347        );
348        map.insert(
349            "non-existent-service-account".to_string(),
350            Box::new(dangling::NonExistentServiceAccountTemplate),
351        );
352        map.insert(
353            "non-isolated-pod".to_string(),
354            Box::new(dangling::NonIsolatedPodTemplate),
355        );
356        map.insert(
357            "scc-deny-privileged".to_string(),
358            Box::new(dangling::SccDenyPrivilegedTemplate),
359        );
360
361        map
362    })
363}
364
365/// Get a template by key.
366pub fn get_template(key: &str) -> Option<&'static dyn Template> {
367    registry().get(key).map(|t| t.as_ref())
368}
369
370/// List all registered templates.
371pub fn list_templates() -> Vec<&'static str> {
372    registry().keys().map(|s| s.as_str()).collect()
373}
374
375/// Initialize all built-in templates.
376/// This is called automatically on first access to the registry.
377pub fn init_builtin_templates() {
378    let _ = registry();
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn test_registry_initialization() {
387        let reg = registry();
388        assert!(!reg.is_empty());
389        assert!(reg.contains_key("privileged"));
390        assert!(reg.contains_key("latest-tag"));
391    }
392
393    #[test]
394    fn test_get_template() {
395        let template = get_template("privileged");
396        assert!(template.is_some());
397        assert_eq!(template.unwrap().key(), "privileged");
398    }
399
400    #[test]
401    fn test_list_templates() {
402        let templates = list_templates();
403        assert!(templates.contains(&"privileged"));
404        assert!(templates.contains(&"latest-tag"));
405    }
406}