syncable_cli/analyzer/kubelint/templates/
validation.rs

1//! General validation check templates.
2
3use 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
9/// Template for checking use of namespace.
10pub 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
64/// Template for checking restart policy.
65pub 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            if let Some(policy) = &pod_spec.restart_policy {
104                // For Deployments, StatefulSets, DaemonSets - must be Always
105                match &object.k8s_object {
106                    K8sObject::Deployment(_)
107                    | K8sObject::StatefulSet(_)
108                    | K8sObject::DaemonSet(_)
109                    | K8sObject::ReplicaSet(_) => {
110                        if policy != "Always" {
111                            diagnostics.push(Diagnostic {
112                                message: format!(
113                                    "Restart policy is '{}' but should be 'Always' for this workload type",
114                                    policy
115                                ),
116                                remediation: Some(
117                                    "Deployments, StatefulSets, DaemonSets, and ReplicaSets \
118                                     require restartPolicy: Always."
119                                        .to_string(),
120                                ),
121                            });
122                        }
123                    }
124                    _ => {}
125                }
126            }
127        }
128
129        diagnostics
130    }
131}
132
133/// Template for checking required annotations.
134pub 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
226/// Template for checking required labels.
227pub 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
327/// Template for checking deprecated API versions.
328pub 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            // Check for deprecated extensions/v1beta1 API
369            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            // Check for deprecated apps/v1beta1 and apps/v1beta2
382            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
394/// Template for checking mismatching selectors.
395pub 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
472/// Template for checking node affinity.
473pub 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
531/// Template for checking Job TTL after finished.
532pub 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            if job.ttl_seconds_after_finished.is_none() {
571                diagnostics.push(Diagnostic {
572                    message: "Job does not have ttlSecondsAfterFinished set".to_string(),
573                    remediation: Some(
574                        "Set ttlSecondsAfterFinished to automatically clean up finished Jobs."
575                            .to_string(),
576                    ),
577                });
578            }
579        }
580
581        diagnostics
582    }
583}
584
585/// Template for checking priority class name.
586pub 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            if pod_spec.priority_class_name.is_none() {
625                diagnostics.push(Diagnostic {
626                    message: "Pod does not have priorityClassName set".to_string(),
627                    remediation: Some(
628                        "Set priorityClassName to control pod scheduling priority.".to_string(),
629                    ),
630                });
631            }
632        }
633
634        diagnostics
635    }
636}
637
638/// Template for checking Service type.
639pub 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            if let Some(svc_type) = &svc.type_ {
695                if self.disallowed.contains(svc_type) {
696                    diagnostics.push(Diagnostic {
697                        message: format!("Service uses disallowed type '{}'", svc_type),
698                        remediation: Some(format!(
699                            "Consider using ClusterIP instead of {}.",
700                            svc_type
701                        )),
702                    });
703                }
704            }
705        }
706
707        diagnostics
708    }
709}
710
711/// Template for checking HPA minimum replicas.
712pub struct HpaMinReplicasTemplate;
713
714impl Template for HpaMinReplicasTemplate {
715    fn key(&self) -> &str {
716        "hpa-min-replicas"
717    }
718
719    fn human_name(&self) -> &str {
720        "HPA Minimum Replicas"
721    }
722
723    fn description(&self) -> &str {
724        "Checks HorizontalPodAutoscaler minReplicas setting"
725    }
726
727    fn supported_object_kinds(&self) -> ObjectKindsDesc {
728        ObjectKindsDesc::new(&["HorizontalPodAutoscaler"])
729    }
730
731    fn parameters(&self) -> Vec<ParameterDesc> {
732        vec![ParameterDesc {
733            name: "minReplicas".to_string(),
734            description: "Minimum recommended minReplicas value".to_string(),
735            param_type: "integer".to_string(),
736            required: false,
737            default: Some(serde_yaml::Value::Number(2.into())),
738        }]
739    }
740
741    fn instantiate(&self, params: &serde_yaml::Value) -> Result<Box<dyn CheckFunc>, TemplateError> {
742        let min_replicas = params
743            .get("minReplicas")
744            .and_then(|v| v.as_i64())
745            .unwrap_or(2) as i32;
746        Ok(Box::new(HpaMinReplicasCheck { min_replicas }))
747    }
748}
749
750struct HpaMinReplicasCheck {
751    min_replicas: i32,
752}
753
754impl CheckFunc for HpaMinReplicasCheck {
755    fn check(&self, object: &Object) -> Vec<Diagnostic> {
756        let mut diagnostics = Vec::new();
757
758        if let K8sObject::HorizontalPodAutoscaler(hpa) = &object.k8s_object {
759            if let Some(min) = hpa.min_replicas {
760                if min < self.min_replicas {
761                    diagnostics.push(Diagnostic {
762                        message: format!(
763                            "HPA minReplicas is {} but should be at least {}",
764                            min, self.min_replicas
765                        ),
766                        remediation: Some(format!(
767                            "Set minReplicas to at least {} for better availability.",
768                            self.min_replicas
769                        )),
770                    });
771                }
772            }
773        }
774
775        diagnostics
776    }
777}
778
779#[cfg(test)]
780mod tests {
781    use super::*;
782    use crate::analyzer::kubelint::parser::yaml::parse_yaml;
783
784    #[test]
785    fn test_use_namespace_default() {
786        let yaml = r#"
787apiVersion: apps/v1
788kind: Deployment
789metadata:
790  name: test-deploy
791  namespace: default
792spec:
793  template:
794    spec:
795      containers:
796      - name: nginx
797        image: nginx:1.21.0
798"#;
799        let objects = parse_yaml(yaml).unwrap();
800        let check = UseNamespaceCheck;
801        let diagnostics = check.check(&objects[0]);
802        assert_eq!(diagnostics.len(), 1);
803    }
804
805    #[test]
806    fn test_use_namespace_ok() {
807        let yaml = r#"
808apiVersion: apps/v1
809kind: Deployment
810metadata:
811  name: test-deploy
812  namespace: production
813spec:
814  template:
815    spec:
816      containers:
817      - name: nginx
818        image: nginx:1.21.0
819"#;
820        let objects = parse_yaml(yaml).unwrap();
821        let check = UseNamespaceCheck;
822        let diagnostics = check.check(&objects[0]);
823        assert!(diagnostics.is_empty());
824    }
825
826    #[test]
827    fn test_hpa_min_replicas() {
828        let yaml = r#"
829apiVersion: autoscaling/v2
830kind: HorizontalPodAutoscaler
831metadata:
832  name: test-hpa
833spec:
834  scaleTargetRef:
835    apiVersion: apps/v1
836    kind: Deployment
837    name: test-deploy
838  minReplicas: 1
839  maxReplicas: 10
840"#;
841        let objects = parse_yaml(yaml).unwrap();
842        let check = HpaMinReplicasCheck { min_replicas: 2 };
843        let diagnostics = check.check(&objects[0]);
844        assert_eq!(diagnostics.len(), 1);
845    }
846}