syncable_cli/analyzer/kubelint/templates/
validation.rs1use crate::analyzer::kubelint::context::K8sObject;
4use crate::analyzer::kubelint::context::Object;
5use crate::analyzer::kubelint::extract;
6use crate::analyzer::kubelint::templates::{CheckFunc, ParameterDesc, Template, TemplateError};
7use crate::analyzer::kubelint::types::{Diagnostic, ObjectKindsDesc};
8
9pub struct UseNamespaceTemplate;
11
12impl Template for UseNamespaceTemplate {
13 fn key(&self) -> &str {
14 "use-namespace"
15 }
16
17 fn human_name(&self) -> &str {
18 "Use Namespace"
19 }
20
21 fn description(&self) -> &str {
22 "Checks that resources specify a namespace"
23 }
24
25 fn supported_object_kinds(&self) -> ObjectKindsDesc {
26 ObjectKindsDesc::new(&["DeploymentLike", "Service", "Ingress", "NetworkPolicy"])
27 }
28
29 fn parameters(&self) -> Vec<ParameterDesc> {
30 Vec::new()
31 }
32
33 fn instantiate(
34 &self,
35 _params: &serde_yaml::Value,
36 ) -> Result<Box<dyn CheckFunc>, TemplateError> {
37 Ok(Box::new(UseNamespaceCheck))
38 }
39}
40
41struct UseNamespaceCheck;
42
43impl CheckFunc for UseNamespaceCheck {
44 fn check(&self, object: &Object) -> Vec<Diagnostic> {
45 let mut diagnostics = Vec::new();
46
47 if object.namespace().is_none() || object.namespace() == Some("default") {
48 diagnostics.push(Diagnostic {
49 message: format!(
50 "Object '{}' does not specify a namespace or uses the default namespace",
51 object.name()
52 ),
53 remediation: Some(
54 "Specify an explicit namespace for your resources to improve isolation."
55 .to_string(),
56 ),
57 });
58 }
59
60 diagnostics
61 }
62}
63
64pub struct RestartPolicyTemplate;
66
67impl Template for RestartPolicyTemplate {
68 fn key(&self) -> &str {
69 "restart-policy"
70 }
71
72 fn human_name(&self) -> &str {
73 "Restart Policy"
74 }
75
76 fn description(&self) -> &str {
77 "Checks pod restart policy settings"
78 }
79
80 fn supported_object_kinds(&self) -> ObjectKindsDesc {
81 ObjectKindsDesc::new(&["DeploymentLike"])
82 }
83
84 fn parameters(&self) -> Vec<ParameterDesc> {
85 Vec::new()
86 }
87
88 fn instantiate(
89 &self,
90 _params: &serde_yaml::Value,
91 ) -> Result<Box<dyn CheckFunc>, TemplateError> {
92 Ok(Box::new(RestartPolicyCheck))
93 }
94}
95
96struct RestartPolicyCheck;
97
98impl CheckFunc for RestartPolicyCheck {
99 fn check(&self, object: &Object) -> Vec<Diagnostic> {
100 let mut diagnostics = Vec::new();
101
102 if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object)
103 && let Some(policy) = &pod_spec.restart_policy
104 {
105 match &object.k8s_object {
107 K8sObject::Deployment(_)
108 | K8sObject::StatefulSet(_)
109 | K8sObject::DaemonSet(_)
110 | K8sObject::ReplicaSet(_) => {
111 if policy != "Always" {
112 diagnostics.push(Diagnostic {
113 message: format!(
114 "Restart policy is '{}' but should be 'Always' for this workload type",
115 policy
116 ),
117 remediation: Some(
118 "Deployments, StatefulSets, DaemonSets, and ReplicaSets \
119 require restartPolicy: Always."
120 .to_string(),
121 ),
122 });
123 }
124 }
125 _ => {}
126 }
127 }
128
129 diagnostics
130 }
131}
132
133pub struct RequiredAnnotationTemplate;
135
136impl Template for RequiredAnnotationTemplate {
137 fn key(&self) -> &str {
138 "required-annotation"
139 }
140
141 fn human_name(&self) -> &str {
142 "Required Annotation"
143 }
144
145 fn description(&self) -> &str {
146 "Checks for required annotations on resources"
147 }
148
149 fn supported_object_kinds(&self) -> ObjectKindsDesc {
150 ObjectKindsDesc::new(&["Any"])
151 }
152
153 fn parameters(&self) -> Vec<ParameterDesc> {
154 vec![
155 ParameterDesc {
156 name: "key".to_string(),
157 description: "Required annotation key".to_string(),
158 param_type: "string".to_string(),
159 required: true,
160 default: None,
161 },
162 ParameterDesc {
163 name: "value".to_string(),
164 description: "Optional required value pattern (regex)".to_string(),
165 param_type: "string".to_string(),
166 required: false,
167 default: None,
168 },
169 ]
170 }
171
172 fn instantiate(&self, params: &serde_yaml::Value) -> Result<Box<dyn CheckFunc>, TemplateError> {
173 let key = params
174 .get("key")
175 .and_then(|v| v.as_str())
176 .ok_or_else(|| TemplateError::MissingParameter("key".to_string()))?
177 .to_string();
178 let value_pattern = params
179 .get("value")
180 .and_then(|v| v.as_str())
181 .map(|s| s.to_string());
182 Ok(Box::new(RequiredAnnotationCheck { key, value_pattern }))
183 }
184}
185
186struct RequiredAnnotationCheck {
187 key: String,
188 value_pattern: Option<String>,
189}
190
191impl CheckFunc for RequiredAnnotationCheck {
192 fn check(&self, object: &Object) -> Vec<Diagnostic> {
193 let mut diagnostics = Vec::new();
194
195 let has_annotation = object
196 .annotations()
197 .map(|annotations| {
198 if let Some(value) = annotations.get(&self.key) {
199 if let Some(pattern) = &self.value_pattern {
200 regex::Regex::new(pattern)
201 .map(|re| re.is_match(value))
202 .unwrap_or(false)
203 } else {
204 true
205 }
206 } else {
207 false
208 }
209 })
210 .unwrap_or(false);
211
212 if !has_annotation {
213 diagnostics.push(Diagnostic {
214 message: format!("Object is missing required annotation '{}'", self.key),
215 remediation: Some(format!(
216 "Add the annotation '{}' to your resource metadata.",
217 self.key
218 )),
219 });
220 }
221
222 diagnostics
223 }
224}
225
226pub struct RequiredLabelTemplate;
228
229impl Template for RequiredLabelTemplate {
230 fn key(&self) -> &str {
231 "required-label"
232 }
233
234 fn human_name(&self) -> &str {
235 "Required Label"
236 }
237
238 fn description(&self) -> &str {
239 "Checks for required labels on resources"
240 }
241
242 fn supported_object_kinds(&self) -> ObjectKindsDesc {
243 ObjectKindsDesc::new(&["Any"])
244 }
245
246 fn parameters(&self) -> Vec<ParameterDesc> {
247 vec![
248 ParameterDesc {
249 name: "key".to_string(),
250 description: "Required label key".to_string(),
251 param_type: "string".to_string(),
252 required: true,
253 default: None,
254 },
255 ParameterDesc {
256 name: "value".to_string(),
257 description: "Optional required value pattern (regex)".to_string(),
258 param_type: "string".to_string(),
259 required: false,
260 default: None,
261 },
262 ]
263 }
264
265 fn instantiate(&self, params: &serde_yaml::Value) -> Result<Box<dyn CheckFunc>, TemplateError> {
266 let key = params
267 .get("key")
268 .and_then(|v| v.as_str())
269 .ok_or_else(|| TemplateError::MissingParameter("key".to_string()))?
270 .to_string();
271 let value_pattern = params
272 .get("value")
273 .and_then(|v| v.as_str())
274 .map(|s| s.to_string());
275 Ok(Box::new(RequiredLabelCheck { key, value_pattern }))
276 }
277}
278
279struct RequiredLabelCheck {
280 key: String,
281 value_pattern: Option<String>,
282}
283
284impl CheckFunc for RequiredLabelCheck {
285 fn check(&self, object: &Object) -> Vec<Diagnostic> {
286 let mut diagnostics = Vec::new();
287
288 let labels = match &object.k8s_object {
289 K8sObject::Deployment(d) => d.labels.as_ref(),
290 K8sObject::StatefulSet(s) => s.labels.as_ref(),
291 K8sObject::DaemonSet(d) => d.labels.as_ref(),
292 K8sObject::Pod(p) => p.labels.as_ref(),
293 K8sObject::Service(s) => s.labels.as_ref(),
294 _ => None,
295 };
296
297 let has_label = labels
298 .map(|labels| {
299 if let Some(value) = labels.get(&self.key) {
300 if let Some(pattern) = &self.value_pattern {
301 regex::Regex::new(pattern)
302 .map(|re| re.is_match(value))
303 .unwrap_or(false)
304 } else {
305 true
306 }
307 } else {
308 false
309 }
310 })
311 .unwrap_or(false);
312
313 if !has_label {
314 diagnostics.push(Diagnostic {
315 message: format!("Object is missing required label '{}'", self.key),
316 remediation: Some(format!(
317 "Add the label '{}' to your resource metadata.",
318 self.key
319 )),
320 });
321 }
322
323 diagnostics
324 }
325}
326
327pub struct DisallowedGVKTemplate;
329
330impl Template for DisallowedGVKTemplate {
331 fn key(&self) -> &str {
332 "disallowed-gvk"
333 }
334
335 fn human_name(&self) -> &str {
336 "Disallowed API Version"
337 }
338
339 fn description(&self) -> &str {
340 "Checks for deprecated or disallowed API versions"
341 }
342
343 fn supported_object_kinds(&self) -> ObjectKindsDesc {
344 ObjectKindsDesc::new(&["Any"])
345 }
346
347 fn parameters(&self) -> Vec<ParameterDesc> {
348 Vec::new()
349 }
350
351 fn instantiate(
352 &self,
353 _params: &serde_yaml::Value,
354 ) -> Result<Box<dyn CheckFunc>, TemplateError> {
355 Ok(Box::new(DisallowedGVKCheck))
356 }
357}
358
359struct DisallowedGVKCheck;
360
361impl CheckFunc for DisallowedGVKCheck {
362 fn check(&self, object: &Object) -> Vec<Diagnostic> {
363 let mut diagnostics = Vec::new();
364
365 if let K8sObject::Unknown(unknown) = &object.k8s_object {
366 let api_version = &unknown.api_version;
367
368 if api_version == "extensions/v1beta1" {
370 diagnostics.push(Diagnostic {
371 message: "Resource uses deprecated API version 'extensions/v1beta1'"
372 .to_string(),
373 remediation: Some(
374 "Migrate to apps/v1 for Deployments, DaemonSets, ReplicaSets; \
375 networking.k8s.io/v1 for Ingress and NetworkPolicy."
376 .to_string(),
377 ),
378 });
379 }
380
381 if api_version == "apps/v1beta1" || api_version == "apps/v1beta2" {
383 diagnostics.push(Diagnostic {
384 message: format!("Resource uses deprecated API version '{}'", api_version),
385 remediation: Some("Migrate to apps/v1.".to_string()),
386 });
387 }
388 }
389
390 diagnostics
391 }
392}
393
394pub struct MismatchingSelectorTemplate;
396
397impl Template for MismatchingSelectorTemplate {
398 fn key(&self) -> &str {
399 "mismatching-selector"
400 }
401
402 fn human_name(&self) -> &str {
403 "Mismatching Selector"
404 }
405
406 fn description(&self) -> &str {
407 "Checks that deployment selector matches pod template labels"
408 }
409
410 fn supported_object_kinds(&self) -> ObjectKindsDesc {
411 ObjectKindsDesc::new(&["Deployment", "StatefulSet", "DaemonSet"])
412 }
413
414 fn parameters(&self) -> Vec<ParameterDesc> {
415 Vec::new()
416 }
417
418 fn instantiate(
419 &self,
420 _params: &serde_yaml::Value,
421 ) -> Result<Box<dyn CheckFunc>, TemplateError> {
422 Ok(Box::new(MismatchingSelectorCheck))
423 }
424}
425
426struct MismatchingSelectorCheck;
427
428impl CheckFunc for MismatchingSelectorCheck {
429 fn check(&self, object: &Object) -> Vec<Diagnostic> {
430 let mut diagnostics = Vec::new();
431
432 let (selector, pod_labels) = match &object.k8s_object {
433 K8sObject::Deployment(d) => {
434 let selector = d.selector.as_ref().and_then(|s| s.match_labels.as_ref());
435 let pod_labels = d.pod_spec.as_ref().and(d.labels.as_ref());
436 (selector, pod_labels)
437 }
438 K8sObject::StatefulSet(s) => {
439 let selector = s.selector.as_ref().and_then(|s| s.match_labels.as_ref());
440 let pod_labels = s.pod_spec.as_ref().and(s.labels.as_ref());
441 (selector, pod_labels)
442 }
443 K8sObject::DaemonSet(d) => {
444 let selector = d.selector.as_ref().and_then(|s| s.match_labels.as_ref());
445 let pod_labels = d.pod_spec.as_ref().and(d.labels.as_ref());
446 (selector, pod_labels)
447 }
448 _ => (None, None),
449 };
450
451 if let (Some(selector_labels), Some(pod_labels)) = (selector, pod_labels) {
452 for (key, value) in selector_labels {
453 if pod_labels.get(key) != Some(value) {
454 diagnostics.push(Diagnostic {
455 message: format!(
456 "Selector label '{}={}' does not match pod template labels",
457 key, value
458 ),
459 remediation: Some(
460 "Ensure the selector's matchLabels are present in the pod template's labels."
461 .to_string(),
462 ),
463 });
464 }
465 }
466 }
467
468 diagnostics
469 }
470}
471
472pub struct NodeAffinityTemplate;
474
475impl Template for NodeAffinityTemplate {
476 fn key(&self) -> &str {
477 "node-affinity"
478 }
479
480 fn human_name(&self) -> &str {
481 "Node Affinity"
482 }
483
484 fn description(&self) -> &str {
485 "Checks if node affinity is configured"
486 }
487
488 fn supported_object_kinds(&self) -> ObjectKindsDesc {
489 ObjectKindsDesc::default()
490 }
491
492 fn parameters(&self) -> Vec<ParameterDesc> {
493 Vec::new()
494 }
495
496 fn instantiate(
497 &self,
498 _params: &serde_yaml::Value,
499 ) -> Result<Box<dyn CheckFunc>, TemplateError> {
500 Ok(Box::new(NodeAffinityCheck))
501 }
502}
503
504struct NodeAffinityCheck;
505
506impl CheckFunc for NodeAffinityCheck {
507 fn check(&self, object: &Object) -> Vec<Diagnostic> {
508 let mut diagnostics = Vec::new();
509
510 if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) {
511 let has_node_affinity = pod_spec
512 .affinity
513 .as_ref()
514 .and_then(|a| a.node_affinity.as_ref())
515 .is_some();
516
517 if !has_node_affinity {
518 diagnostics.push(Diagnostic {
519 message: "Pod does not have node affinity configured".to_string(),
520 remediation: Some(
521 "Consider adding node affinity rules to control pod placement.".to_string(),
522 ),
523 });
524 }
525 }
526
527 diagnostics
528 }
529}
530
531pub struct JobTtlSecondsAfterFinishedTemplate;
533
534impl Template for JobTtlSecondsAfterFinishedTemplate {
535 fn key(&self) -> &str {
536 "job-ttl-seconds-after-finished"
537 }
538
539 fn human_name(&self) -> &str {
540 "Job TTL Seconds After Finished"
541 }
542
543 fn description(&self) -> &str {
544 "Checks if Job has ttlSecondsAfterFinished set"
545 }
546
547 fn supported_object_kinds(&self) -> ObjectKindsDesc {
548 ObjectKindsDesc::new(&["Job"])
549 }
550
551 fn parameters(&self) -> Vec<ParameterDesc> {
552 Vec::new()
553 }
554
555 fn instantiate(
556 &self,
557 _params: &serde_yaml::Value,
558 ) -> Result<Box<dyn CheckFunc>, TemplateError> {
559 Ok(Box::new(JobTtlSecondsAfterFinishedCheck))
560 }
561}
562
563struct JobTtlSecondsAfterFinishedCheck;
564
565impl CheckFunc for JobTtlSecondsAfterFinishedCheck {
566 fn check(&self, object: &Object) -> Vec<Diagnostic> {
567 let mut diagnostics = Vec::new();
568
569 if let K8sObject::Job(job) = &object.k8s_object
570 && job.ttl_seconds_after_finished.is_none()
571 {
572 diagnostics.push(Diagnostic {
573 message: "Job does not have ttlSecondsAfterFinished set".to_string(),
574 remediation: Some(
575 "Set ttlSecondsAfterFinished to automatically clean up finished Jobs."
576 .to_string(),
577 ),
578 });
579 }
580
581 diagnostics
582 }
583}
584
585pub struct PriorityClassNameTemplate;
587
588impl Template for PriorityClassNameTemplate {
589 fn key(&self) -> &str {
590 "priority-class-name"
591 }
592
593 fn human_name(&self) -> &str {
594 "Priority Class Name"
595 }
596
597 fn description(&self) -> &str {
598 "Checks if priorityClassName is set"
599 }
600
601 fn supported_object_kinds(&self) -> ObjectKindsDesc {
602 ObjectKindsDesc::default()
603 }
604
605 fn parameters(&self) -> Vec<ParameterDesc> {
606 Vec::new()
607 }
608
609 fn instantiate(
610 &self,
611 _params: &serde_yaml::Value,
612 ) -> Result<Box<dyn CheckFunc>, TemplateError> {
613 Ok(Box::new(PriorityClassNameCheck))
614 }
615}
616
617struct PriorityClassNameCheck;
618
619impl CheckFunc for PriorityClassNameCheck {
620 fn check(&self, object: &Object) -> Vec<Diagnostic> {
621 let mut diagnostics = Vec::new();
622
623 if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object)
624 && pod_spec.priority_class_name.is_none()
625 {
626 diagnostics.push(Diagnostic {
627 message: "Pod does not have priorityClassName set".to_string(),
628 remediation: Some(
629 "Set priorityClassName to control pod scheduling priority.".to_string(),
630 ),
631 });
632 }
633
634 diagnostics
635 }
636}
637
638pub struct ServiceTypeTemplate;
640
641impl Template for ServiceTypeTemplate {
642 fn key(&self) -> &str {
643 "service-type"
644 }
645
646 fn human_name(&self) -> &str {
647 "Service Type"
648 }
649
650 fn description(&self) -> &str {
651 "Checks Service type configuration"
652 }
653
654 fn supported_object_kinds(&self) -> ObjectKindsDesc {
655 ObjectKindsDesc::new(&["Service"])
656 }
657
658 fn parameters(&self) -> Vec<ParameterDesc> {
659 vec![ParameterDesc {
660 name: "disallowedTypes".to_string(),
661 description: "List of disallowed service types".to_string(),
662 param_type: "array".to_string(),
663 required: false,
664 default: Some(serde_yaml::Value::Sequence(vec![
665 serde_yaml::Value::String("NodePort".to_string()),
666 serde_yaml::Value::String("LoadBalancer".to_string()),
667 ])),
668 }]
669 }
670
671 fn instantiate(&self, params: &serde_yaml::Value) -> Result<Box<dyn CheckFunc>, TemplateError> {
672 let disallowed = params
673 .get("disallowedTypes")
674 .and_then(|v| v.as_sequence())
675 .map(|seq| {
676 seq.iter()
677 .filter_map(|v| v.as_str().map(|s| s.to_string()))
678 .collect()
679 })
680 .unwrap_or_else(|| vec!["NodePort".to_string(), "LoadBalancer".to_string()]);
681 Ok(Box::new(ServiceTypeCheck { disallowed }))
682 }
683}
684
685struct ServiceTypeCheck {
686 disallowed: Vec<String>,
687}
688
689impl CheckFunc for ServiceTypeCheck {
690 fn check(&self, object: &Object) -> Vec<Diagnostic> {
691 let mut diagnostics = Vec::new();
692
693 if let K8sObject::Service(svc) = &object.k8s_object
694 && let Some(svc_type) = &svc.type_
695 && self.disallowed.contains(svc_type)
696 {
697 diagnostics.push(Diagnostic {
698 message: format!("Service uses disallowed type '{}'", svc_type),
699 remediation: Some(format!("Consider using ClusterIP instead of {}.", svc_type)),
700 });
701 }
702
703 diagnostics
704 }
705}
706
707pub struct HpaMinReplicasTemplate;
709
710impl Template for HpaMinReplicasTemplate {
711 fn key(&self) -> &str {
712 "hpa-min-replicas"
713 }
714
715 fn human_name(&self) -> &str {
716 "HPA Minimum Replicas"
717 }
718
719 fn description(&self) -> &str {
720 "Checks HorizontalPodAutoscaler minReplicas setting"
721 }
722
723 fn supported_object_kinds(&self) -> ObjectKindsDesc {
724 ObjectKindsDesc::new(&["HorizontalPodAutoscaler"])
725 }
726
727 fn parameters(&self) -> Vec<ParameterDesc> {
728 vec![ParameterDesc {
729 name: "minReplicas".to_string(),
730 description: "Minimum recommended minReplicas value".to_string(),
731 param_type: "integer".to_string(),
732 required: false,
733 default: Some(serde_yaml::Value::Number(2.into())),
734 }]
735 }
736
737 fn instantiate(&self, params: &serde_yaml::Value) -> Result<Box<dyn CheckFunc>, TemplateError> {
738 let min_replicas = params
739 .get("minReplicas")
740 .and_then(|v| v.as_i64())
741 .unwrap_or(2) as i32;
742 Ok(Box::new(HpaMinReplicasCheck { min_replicas }))
743 }
744}
745
746struct HpaMinReplicasCheck {
747 min_replicas: i32,
748}
749
750impl CheckFunc for HpaMinReplicasCheck {
751 fn check(&self, object: &Object) -> Vec<Diagnostic> {
752 let mut diagnostics = Vec::new();
753
754 if let K8sObject::HorizontalPodAutoscaler(hpa) = &object.k8s_object
755 && let Some(min) = hpa.min_replicas
756 && min < self.min_replicas
757 {
758 diagnostics.push(Diagnostic {
759 message: format!(
760 "HPA minReplicas is {} but should be at least {}",
761 min, self.min_replicas
762 ),
763 remediation: Some(format!(
764 "Set minReplicas to at least {} for better availability.",
765 self.min_replicas
766 )),
767 });
768 }
769
770 diagnostics
771 }
772}
773
774#[cfg(test)]
775mod tests {
776 use super::*;
777 use crate::analyzer::kubelint::parser::yaml::parse_yaml;
778
779 #[test]
780 fn test_use_namespace_default() {
781 let yaml = r#"
782apiVersion: apps/v1
783kind: Deployment
784metadata:
785 name: test-deploy
786 namespace: default
787spec:
788 template:
789 spec:
790 containers:
791 - name: nginx
792 image: nginx:1.21.0
793"#;
794 let objects = parse_yaml(yaml).unwrap();
795 let check = UseNamespaceCheck;
796 let diagnostics = check.check(&objects[0]);
797 assert_eq!(diagnostics.len(), 1);
798 }
799
800 #[test]
801 fn test_use_namespace_ok() {
802 let yaml = r#"
803apiVersion: apps/v1
804kind: Deployment
805metadata:
806 name: test-deploy
807 namespace: production
808spec:
809 template:
810 spec:
811 containers:
812 - name: nginx
813 image: nginx:1.21.0
814"#;
815 let objects = parse_yaml(yaml).unwrap();
816 let check = UseNamespaceCheck;
817 let diagnostics = check.check(&objects[0]);
818 assert!(diagnostics.is_empty());
819 }
820
821 #[test]
822 fn test_hpa_min_replicas() {
823 let yaml = r#"
824apiVersion: autoscaling/v2
825kind: HorizontalPodAutoscaler
826metadata:
827 name: test-hpa
828spec:
829 scaleTargetRef:
830 apiVersion: apps/v1
831 kind: Deployment
832 name: test-deploy
833 minReplicas: 1
834 maxReplicas: 10
835"#;
836 let objects = parse_yaml(yaml).unwrap();
837 let check = HpaMinReplicasCheck { min_replicas: 2 };
838 let diagnostics = check.check(&objects[0]);
839 assert_eq!(diagnostics.len(), 1);
840 }
841}