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            && let Some(policy) = &pod_spec.restart_policy
104        {
105            // For Deployments, StatefulSets, DaemonSets - must be Always
106            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
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            && 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
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            && 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
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            && 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
707/// Template for checking HPA minimum replicas.
708pub 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}