syncable_cli/analyzer/kubelint/parser/
yaml.rs

1//! YAML parsing for Kubernetes manifests.
2
3use crate::analyzer::kubelint::context::Object;
4use crate::analyzer::kubelint::context::object::*;
5use std::collections::BTreeMap;
6use std::path::Path;
7
8/// Parse a YAML string containing one or more Kubernetes objects.
9pub fn parse_yaml(content: &str) -> Result<Vec<Object>, YamlParseError> {
10    parse_yaml_with_path(content, Path::new("<stdin>"))
11}
12
13/// Parse YAML content with a source file path.
14pub fn parse_yaml_with_path(content: &str, path: &Path) -> Result<Vec<Object>, YamlParseError> {
15    let mut objects = Vec::new();
16    let mut line_number = 1u32;
17
18    // Split on document separator and track line numbers
19    for doc in content.split("\n---") {
20        let doc = doc.trim();
21        if doc.is_empty() || doc.starts_with('#') {
22            // Count lines for empty or comment-only documents
23            line_number += doc.lines().count() as u32 + 1;
24            continue;
25        }
26
27        // Parse the YAML document
28        match serde_yaml::from_str::<serde_yaml::Value>(doc) {
29            Ok(value) => {
30                if let Some(obj) = parse_k8s_object(&value, path, line_number) {
31                    objects.push(obj);
32                }
33            }
34            Err(e) => {
35                return Err(YamlParseError::SyntaxError(format!(
36                    "at line {}: {}",
37                    line_number, e
38                )));
39            }
40        }
41
42        // Update line number for next document
43        line_number += doc.lines().count() as u32 + 1;
44    }
45
46    Ok(objects)
47}
48
49/// Parse a YAML file.
50pub fn parse_yaml_file(path: &Path) -> Result<Vec<Object>, YamlParseError> {
51    let content =
52        std::fs::read_to_string(path).map_err(|e| YamlParseError::IoError(e.to_string()))?;
53
54    parse_yaml_with_path(&content, path)
55}
56
57/// Parse all YAML files in a directory (recursively).
58pub fn parse_yaml_dir(path: &Path) -> Result<Vec<Object>, YamlParseError> {
59    let mut objects = Vec::new();
60
61    for entry in walkdir::WalkDir::new(path)
62        .follow_links(true)
63        .into_iter()
64        .filter_map(|e| e.ok())
65    {
66        let entry_path = entry.path();
67        if entry_path.is_file() {
68            let ext = entry_path.extension().and_then(|e| e.to_str());
69            if matches!(ext, Some("yaml") | Some("yml")) {
70                match parse_yaml_file(entry_path) {
71                    Ok(mut objs) => objects.append(&mut objs),
72                    Err(e) => {
73                        // Log warning but continue parsing other files
74                        eprintln!("Warning: failed to parse {}: {}", entry_path.display(), e);
75                    }
76                }
77            }
78        }
79    }
80
81    Ok(objects)
82}
83
84/// Parse a single K8s object from a YAML value.
85fn parse_k8s_object(value: &serde_yaml::Value, path: &Path, line: u32) -> Option<Object> {
86    let api_version = value.get("apiVersion")?.as_str()?;
87    let kind = value.get("kind")?.as_str()?;
88
89    let metadata = ObjectMetadata::from_file(path).with_line(line);
90    let k8s_obj = match kind {
91        "Deployment" => K8sObject::Deployment(Box::new(parse_deployment(value))),
92        "StatefulSet" => K8sObject::StatefulSet(Box::new(parse_statefulset(value))),
93        "DaemonSet" => K8sObject::DaemonSet(Box::new(parse_daemonset(value))),
94        "ReplicaSet" => K8sObject::ReplicaSet(Box::new(parse_replicaset(value))),
95        "Pod" => K8sObject::Pod(Box::new(parse_pod(value))),
96        "Job" => K8sObject::Job(Box::new(parse_job(value))),
97        "CronJob" => K8sObject::CronJob(Box::new(parse_cronjob(value))),
98        "Service" => K8sObject::Service(Box::new(parse_service(value))),
99        "Ingress" => K8sObject::Ingress(Box::new(parse_ingress(value))),
100        "NetworkPolicy" => K8sObject::NetworkPolicy(Box::new(parse_network_policy(value))),
101        "Role" => K8sObject::Role(Box::new(parse_role(value))),
102        "ClusterRole" => K8sObject::ClusterRole(Box::new(parse_cluster_role(value))),
103        "RoleBinding" => K8sObject::RoleBinding(Box::new(parse_role_binding(value))),
104        "ClusterRoleBinding" => {
105            K8sObject::ClusterRoleBinding(Box::new(parse_cluster_role_binding(value)))
106        }
107        "ServiceAccount" => K8sObject::ServiceAccount(Box::new(parse_service_account(value))),
108        "HorizontalPodAutoscaler" => K8sObject::HorizontalPodAutoscaler(Box::new(parse_hpa(value))),
109        "PodDisruptionBudget" => K8sObject::PodDisruptionBudget(Box::new(parse_pdb(value))),
110        "PersistentVolumeClaim" => K8sObject::PersistentVolumeClaim(Box::new(parse_pvc(value))),
111        _ => K8sObject::Unknown(Box::new(parse_unknown(value, api_version, kind))),
112    };
113
114    Some(Object::new(metadata, k8s_obj))
115}
116
117// ============================================================================
118// Parse helper functions
119// ============================================================================
120
121fn get_string(value: &serde_yaml::Value, key: &str) -> Option<String> {
122    value.get(key)?.as_str().map(|s| s.to_string())
123}
124
125fn get_i32(value: &serde_yaml::Value, key: &str) -> Option<i32> {
126    value.get(key)?.as_i64().map(|n| n as i32)
127}
128
129fn get_i64(value: &serde_yaml::Value, key: &str) -> Option<i64> {
130    value.get(key)?.as_i64()
131}
132
133fn get_bool(value: &serde_yaml::Value, key: &str) -> Option<bool> {
134    value.get(key)?.as_bool()
135}
136
137fn get_string_map(value: &serde_yaml::Value, key: &str) -> Option<BTreeMap<String, String>> {
138    let mapping = value.get(key)?.as_mapping()?;
139    let mut map = BTreeMap::new();
140    for (k, v) in mapping {
141        if let (Some(key), Some(val)) = (k.as_str(), v.as_str()) {
142            map.insert(key.to_string(), val.to_string());
143        }
144    }
145    if map.is_empty() { None } else { Some(map) }
146}
147
148fn parse_metadata(
149    value: &serde_yaml::Value,
150) -> (
151    String,
152    Option<String>,
153    Option<BTreeMap<String, String>>,
154    Option<BTreeMap<String, String>>,
155) {
156    let metadata = value.get("metadata");
157    let name = metadata
158        .and_then(|m| get_string(m, "name"))
159        .unwrap_or_default();
160    let namespace = metadata.and_then(|m| get_string(m, "namespace"));
161    let labels = metadata.and_then(|m| get_string_map(m, "labels"));
162    let annotations = metadata.and_then(|m| get_string_map(m, "annotations"));
163    (name, namespace, labels, annotations)
164}
165
166fn parse_label_selector(value: &serde_yaml::Value) -> Option<LabelSelector> {
167    let selector = value.get("selector")?;
168    Some(LabelSelector {
169        match_labels: get_string_map(selector, "matchLabels"),
170    })
171}
172
173fn parse_pod_spec(value: &serde_yaml::Value) -> Option<PodSpec> {
174    let spec = value.get("spec")?.get("template")?.get("spec")?;
175    Some(parse_pod_spec_inner(spec))
176}
177
178fn parse_pod_spec_direct(value: &serde_yaml::Value) -> Option<PodSpec> {
179    let spec = value.get("spec")?;
180    Some(parse_pod_spec_inner(spec))
181}
182
183fn parse_pod_spec_inner(spec: &serde_yaml::Value) -> PodSpec {
184    PodSpec {
185        containers: parse_containers(spec.get("containers")),
186        init_containers: parse_containers(spec.get("initContainers")),
187        volumes: parse_volumes(spec.get("volumes")),
188        service_account_name: get_string(spec, "serviceAccountName")
189            .or_else(|| get_string(spec, "serviceAccount")),
190        host_network: get_bool(spec, "hostNetwork"),
191        host_pid: get_bool(spec, "hostPID"),
192        host_ipc: get_bool(spec, "hostIPC"),
193        security_context: parse_pod_security_context(spec.get("securityContext")),
194        affinity: parse_affinity(spec.get("affinity")),
195        dns_config: parse_dns_config(spec.get("dnsConfig")),
196        restart_policy: get_string(spec, "restartPolicy"),
197        priority_class_name: get_string(spec, "priorityClassName"),
198    }
199}
200
201fn parse_containers(containers: Option<&serde_yaml::Value>) -> Vec<ContainerSpec> {
202    let Some(containers) = containers else {
203        return Vec::new();
204    };
205    let Some(arr) = containers.as_sequence() else {
206        return Vec::new();
207    };
208
209    arr.iter().map(parse_container).collect()
210}
211
212fn parse_container(c: &serde_yaml::Value) -> ContainerSpec {
213    ContainerSpec {
214        name: get_string(c, "name").unwrap_or_default(),
215        image: get_string(c, "image"),
216        security_context: parse_security_context(c.get("securityContext")),
217        resources: parse_resources(c.get("resources")),
218        liveness_probe: parse_probe(c.get("livenessProbe")),
219        readiness_probe: parse_probe(c.get("readinessProbe")),
220        startup_probe: parse_probe(c.get("startupProbe")),
221        env: parse_env_vars(c.get("env")),
222        volume_mounts: parse_volume_mounts(c.get("volumeMounts")),
223        ports: parse_container_ports(c.get("ports")),
224    }
225}
226
227fn parse_security_context(sc: Option<&serde_yaml::Value>) -> Option<SecurityContext> {
228    let sc = sc?;
229    Some(SecurityContext {
230        privileged: get_bool(sc, "privileged"),
231        allow_privilege_escalation: get_bool(sc, "allowPrivilegeEscalation"),
232        run_as_non_root: get_bool(sc, "runAsNonRoot"),
233        run_as_user: get_i64(sc, "runAsUser"),
234        read_only_root_filesystem: get_bool(sc, "readOnlyRootFilesystem"),
235        capabilities: parse_capabilities(sc.get("capabilities")),
236        proc_mount: get_string(sc, "procMount"),
237    })
238}
239
240fn parse_capabilities(caps: Option<&serde_yaml::Value>) -> Option<Capabilities> {
241    let caps = caps?;
242    Some(Capabilities {
243        add: parse_string_array(caps.get("add")),
244        drop: parse_string_array(caps.get("drop")),
245    })
246}
247
248fn parse_string_array(value: Option<&serde_yaml::Value>) -> Vec<String> {
249    value
250        .and_then(|v| v.as_sequence())
251        .map(|arr| {
252            arr.iter()
253                .filter_map(|v| v.as_str().map(|s| s.to_string()))
254                .collect()
255        })
256        .unwrap_or_default()
257}
258
259fn parse_resources(res: Option<&serde_yaml::Value>) -> Option<ResourceRequirements> {
260    let res = res?;
261    Some(ResourceRequirements {
262        limits: get_string_map(res, "limits"),
263        requests: get_string_map(res, "requests"),
264    })
265}
266
267fn parse_probe(probe: Option<&serde_yaml::Value>) -> Option<Probe> {
268    let probe = probe?;
269    Some(Probe {
270        http_get: probe.get("httpGet").map(|h| HttpGetAction {
271            port: h.get("port").and_then(|p| p.as_i64()).unwrap_or(0) as i32,
272            path: get_string(h, "path"),
273        }),
274        tcp_socket: probe.get("tcpSocket").map(|t| TcpSocketAction {
275            port: t.get("port").and_then(|p| p.as_i64()).unwrap_or(0) as i32,
276        }),
277        exec: probe.get("exec").map(|e| ExecAction {
278            command: parse_string_array(e.get("command")),
279        }),
280    })
281}
282
283fn parse_env_vars(env: Option<&serde_yaml::Value>) -> Vec<EnvVar> {
284    let Some(env) = env else {
285        return Vec::new();
286    };
287    let Some(arr) = env.as_sequence() else {
288        return Vec::new();
289    };
290
291    arr.iter()
292        .map(|e| EnvVar {
293            name: get_string(e, "name").unwrap_or_default(),
294            value: get_string(e, "value"),
295            value_from: parse_env_var_source(e.get("valueFrom")),
296        })
297        .collect()
298}
299
300fn parse_env_var_source(vf: Option<&serde_yaml::Value>) -> Option<EnvVarSource> {
301    let vf = vf?;
302    if let Some(secret) = vf.get("secretKeyRef") {
303        return Some(EnvVarSource::SecretKeyRef {
304            name: get_string(secret, "name").unwrap_or_default(),
305            key: get_string(secret, "key").unwrap_or_default(),
306        });
307    }
308    if let Some(cm) = vf.get("configMapKeyRef") {
309        return Some(EnvVarSource::ConfigMapKeyRef {
310            name: get_string(cm, "name").unwrap_or_default(),
311            key: get_string(cm, "key").unwrap_or_default(),
312        });
313    }
314    if let Some(field) = vf.get("fieldRef") {
315        return Some(EnvVarSource::FieldRef {
316            field_path: get_string(field, "fieldPath").unwrap_or_default(),
317        });
318    }
319    None
320}
321
322fn parse_volume_mounts(mounts: Option<&serde_yaml::Value>) -> Vec<VolumeMount> {
323    let Some(mounts) = mounts else {
324        return Vec::new();
325    };
326    let Some(arr) = mounts.as_sequence() else {
327        return Vec::new();
328    };
329
330    arr.iter()
331        .map(|m| VolumeMount {
332            name: get_string(m, "name").unwrap_or_default(),
333            mount_path: get_string(m, "mountPath").unwrap_or_default(),
334            read_only: get_bool(m, "readOnly"),
335        })
336        .collect()
337}
338
339fn parse_container_ports(ports: Option<&serde_yaml::Value>) -> Vec<ContainerPort> {
340    let Some(ports) = ports else {
341        return Vec::new();
342    };
343    let Some(arr) = ports.as_sequence() else {
344        return Vec::new();
345    };
346
347    arr.iter()
348        .map(|p| ContainerPort {
349            container_port: get_i32(p, "containerPort").unwrap_or(0),
350            protocol: get_string(p, "protocol"),
351            host_port: get_i32(p, "hostPort"),
352        })
353        .collect()
354}
355
356fn parse_volumes(volumes: Option<&serde_yaml::Value>) -> Vec<Volume> {
357    let Some(volumes) = volumes else {
358        return Vec::new();
359    };
360    let Some(arr) = volumes.as_sequence() else {
361        return Vec::new();
362    };
363
364    arr.iter()
365        .map(|v| Volume {
366            name: get_string(v, "name").unwrap_or_default(),
367            host_path: v.get("hostPath").map(|h| HostPathVolumeSource {
368                path: get_string(h, "path").unwrap_or_default(),
369                type_: get_string(h, "type"),
370            }),
371            secret: v.get("secret").map(|s| SecretVolumeSource {
372                secret_name: get_string(s, "secretName"),
373            }),
374        })
375        .collect()
376}
377
378fn parse_pod_security_context(psc: Option<&serde_yaml::Value>) -> Option<PodSecurityContext> {
379    let psc = psc?;
380    Some(PodSecurityContext {
381        run_as_non_root: get_bool(psc, "runAsNonRoot"),
382        run_as_user: get_i64(psc, "runAsUser"),
383        sysctls: parse_sysctls(psc.get("sysctls")),
384    })
385}
386
387fn parse_sysctls(sysctls: Option<&serde_yaml::Value>) -> Vec<Sysctl> {
388    let Some(sysctls) = sysctls else {
389        return Vec::new();
390    };
391    let Some(arr) = sysctls.as_sequence() else {
392        return Vec::new();
393    };
394
395    arr.iter()
396        .map(|s| Sysctl {
397            name: get_string(s, "name").unwrap_or_default(),
398            value: get_string(s, "value").unwrap_or_default(),
399        })
400        .collect()
401}
402
403fn parse_affinity(affinity: Option<&serde_yaml::Value>) -> Option<Affinity> {
404    let affinity = affinity?;
405    Some(Affinity {
406        pod_anti_affinity: parse_pod_anti_affinity(affinity.get("podAntiAffinity")),
407        node_affinity: parse_node_affinity(affinity.get("nodeAffinity")),
408    })
409}
410
411fn parse_pod_anti_affinity(paa: Option<&serde_yaml::Value>) -> Option<PodAntiAffinity> {
412    let paa = paa?;
413    Some(PodAntiAffinity {
414        required_during_scheduling_ignored_during_execution: parse_pod_affinity_terms(
415            paa.get("requiredDuringSchedulingIgnoredDuringExecution"),
416        ),
417        preferred_during_scheduling_ignored_during_execution: parse_weighted_pod_affinity_terms(
418            paa.get("preferredDuringSchedulingIgnoredDuringExecution"),
419        ),
420    })
421}
422
423fn parse_pod_affinity_terms(terms: Option<&serde_yaml::Value>) -> Vec<PodAffinityTerm> {
424    let Some(terms) = terms else {
425        return Vec::new();
426    };
427    let Some(arr) = terms.as_sequence() else {
428        return Vec::new();
429    };
430
431    arr.iter()
432        .map(|t| PodAffinityTerm {
433            topology_key: get_string(t, "topologyKey").unwrap_or_default(),
434        })
435        .collect()
436}
437
438fn parse_weighted_pod_affinity_terms(
439    terms: Option<&serde_yaml::Value>,
440) -> Vec<WeightedPodAffinityTerm> {
441    let Some(terms) = terms else {
442        return Vec::new();
443    };
444    let Some(arr) = terms.as_sequence() else {
445        return Vec::new();
446    };
447
448    arr.iter()
449        .map(|t| WeightedPodAffinityTerm {
450            weight: get_i32(t, "weight").unwrap_or(0),
451            pod_affinity_term: t
452                .get("podAffinityTerm")
453                .map(|pat| PodAffinityTerm {
454                    topology_key: get_string(pat, "topologyKey").unwrap_or_default(),
455                })
456                .unwrap_or_default(),
457        })
458        .collect()
459}
460
461fn parse_node_affinity(na: Option<&serde_yaml::Value>) -> Option<NodeAffinity> {
462    let na = na?;
463    Some(NodeAffinity {
464        required_during_scheduling_ignored_during_execution: na
465            .get("requiredDuringSchedulingIgnoredDuringExecution")
466            .map(|r| NodeSelector {
467                node_selector_terms: r
468                    .get("nodeSelectorTerms")
469                    .and_then(|t| t.as_sequence())
470                    .map(|arr| {
471                        arr.iter()
472                            .map(|term| NodeSelectorTerm {
473                                match_expressions: term
474                                    .get("matchExpressions")
475                                    .and_then(|e| e.as_sequence())
476                                    .map(|arr| {
477                                        arr.iter()
478                                            .map(|expr| NodeSelectorRequirement {
479                                                key: get_string(expr, "key").unwrap_or_default(),
480                                                operator: get_string(expr, "operator")
481                                                    .unwrap_or_default(),
482                                                values: parse_string_array(expr.get("values")),
483                                            })
484                                            .collect()
485                                    })
486                                    .unwrap_or_default(),
487                            })
488                            .collect()
489                    })
490                    .unwrap_or_default(),
491            }),
492    })
493}
494
495fn parse_dns_config(dns: Option<&serde_yaml::Value>) -> Option<DnsConfig> {
496    let dns = dns?;
497    Some(DnsConfig {
498        options: dns
499            .get("options")
500            .and_then(|o| o.as_sequence())
501            .map(|arr| {
502                arr.iter()
503                    .map(|opt| PodDnsConfigOption {
504                        name: get_string(opt, "name"),
505                        value: get_string(opt, "value"),
506                    })
507                    .collect()
508            })
509            .unwrap_or_default(),
510    })
511}
512
513// ============================================================================
514// Object type parsers
515// ============================================================================
516
517fn parse_deployment(value: &serde_yaml::Value) -> DeploymentData {
518    let (name, namespace, labels, annotations) = parse_metadata(value);
519    let spec = value.get("spec");
520
521    DeploymentData {
522        name,
523        namespace,
524        labels,
525        annotations,
526        replicas: spec.and_then(|s| get_i32(s, "replicas")),
527        selector: parse_label_selector(value.get("spec").unwrap_or(value)),
528        pod_spec: parse_pod_spec(value),
529        strategy: spec
530            .and_then(|s| s.get("strategy"))
531            .map(|strat| DeploymentStrategy {
532                type_: get_string(strat, "type"),
533                rolling_update: strat
534                    .get("rollingUpdate")
535                    .map(|ru| RollingUpdateDeployment {
536                        max_unavailable: get_string(ru, "maxUnavailable")
537                            .or_else(|| get_i32(ru, "maxUnavailable").map(|n| n.to_string())),
538                        max_surge: get_string(ru, "maxSurge")
539                            .or_else(|| get_i32(ru, "maxSurge").map(|n| n.to_string())),
540                    }),
541            }),
542    }
543}
544
545fn parse_statefulset(value: &serde_yaml::Value) -> StatefulSetData {
546    let (name, namespace, labels, annotations) = parse_metadata(value);
547    let spec = value.get("spec");
548
549    StatefulSetData {
550        name,
551        namespace,
552        labels,
553        annotations,
554        replicas: spec.and_then(|s| get_i32(s, "replicas")),
555        selector: parse_label_selector(value.get("spec").unwrap_or(value)),
556        pod_spec: parse_pod_spec(value),
557    }
558}
559
560fn parse_daemonset(value: &serde_yaml::Value) -> DaemonSetData {
561    let (name, namespace, labels, annotations) = parse_metadata(value);
562    let spec = value.get("spec");
563
564    DaemonSetData {
565        name,
566        namespace,
567        labels,
568        annotations,
569        selector: parse_label_selector(value.get("spec").unwrap_or(value)),
570        pod_spec: parse_pod_spec(value),
571        update_strategy: spec.and_then(|s| s.get("updateStrategy")).map(|us| {
572            DaemonSetUpdateStrategy {
573                type_: get_string(us, "type"),
574            }
575        }),
576    }
577}
578
579fn parse_replicaset(value: &serde_yaml::Value) -> ReplicaSetData {
580    let (name, namespace, labels, annotations) = parse_metadata(value);
581    let spec = value.get("spec");
582
583    ReplicaSetData {
584        name,
585        namespace,
586        labels,
587        annotations,
588        replicas: spec.and_then(|s| get_i32(s, "replicas")),
589        selector: parse_label_selector(value.get("spec").unwrap_or(value)),
590        pod_spec: parse_pod_spec(value),
591    }
592}
593
594fn parse_pod(value: &serde_yaml::Value) -> PodData {
595    let (name, namespace, labels, annotations) = parse_metadata(value);
596
597    PodData {
598        name,
599        namespace,
600        labels,
601        annotations,
602        spec: parse_pod_spec_direct(value),
603    }
604}
605
606fn parse_job(value: &serde_yaml::Value) -> JobData {
607    let (name, namespace, labels, annotations) = parse_metadata(value);
608    let spec = value.get("spec");
609
610    JobData {
611        name,
612        namespace,
613        labels,
614        annotations,
615        pod_spec: parse_pod_spec(value),
616        ttl_seconds_after_finished: spec.and_then(|s| get_i32(s, "ttlSecondsAfterFinished")),
617    }
618}
619
620fn parse_cronjob(value: &serde_yaml::Value) -> CronJobData {
621    let (name, namespace, labels, annotations) = parse_metadata(value);
622
623    // CronJob has jobTemplate.spec.template.spec
624    let job_template = value.get("spec").and_then(|s| s.get("jobTemplate"));
625
626    let job_spec = job_template.map(|jt| {
627        let (_, _, job_labels, job_annotations) = jt
628            .get("metadata")
629            .map(|m| {
630                (
631                    get_string(m, "name").unwrap_or_default(),
632                    get_string(m, "namespace"),
633                    get_string_map(m, "labels"),
634                    get_string_map(m, "annotations"),
635                )
636            })
637            .unwrap_or_default();
638
639        let job_spec = jt.get("spec");
640        JobData {
641            name: name.clone(),
642            namespace: namespace.clone(),
643            labels: job_labels,
644            annotations: job_annotations,
645            pod_spec: job_spec.and_then(|js| {
646                js.get("template")
647                    .and_then(|t| t.get("spec"))
648                    .map(parse_pod_spec_inner)
649            }),
650            ttl_seconds_after_finished: job_spec
651                .and_then(|s| get_i32(s, "ttlSecondsAfterFinished")),
652        }
653    });
654
655    CronJobData {
656        name,
657        namespace,
658        labels,
659        annotations,
660        job_spec,
661    }
662}
663
664fn parse_service(value: &serde_yaml::Value) -> ServiceData {
665    let (name, namespace, labels, annotations) = parse_metadata(value);
666    let spec = value.get("spec");
667
668    ServiceData {
669        name,
670        namespace,
671        labels,
672        annotations,
673        selector: spec.and_then(|s| get_string_map(s, "selector")),
674        ports: spec
675            .and_then(|s| s.get("ports"))
676            .and_then(|p| p.as_sequence())
677            .map(|arr| {
678                arr.iter()
679                    .map(|p| ServicePort {
680                        port: get_i32(p, "port").unwrap_or(0),
681                        target_port: get_string(p, "targetPort")
682                            .or_else(|| get_i32(p, "targetPort").map(|n| n.to_string())),
683                        protocol: get_string(p, "protocol"),
684                        name: get_string(p, "name"),
685                    })
686                    .collect()
687            })
688            .unwrap_or_default(),
689        type_: spec.and_then(|s| get_string(s, "type")),
690    }
691}
692
693fn parse_ingress(value: &serde_yaml::Value) -> IngressData {
694    let (name, namespace, labels, annotations) = parse_metadata(value);
695    let spec = value.get("spec");
696
697    IngressData {
698        name,
699        namespace,
700        labels,
701        annotations,
702        rules: spec
703            .and_then(|s| s.get("rules"))
704            .and_then(|r| r.as_sequence())
705            .map(|arr| {
706                arr.iter()
707                    .map(|rule| IngressRule {
708                        host: get_string(rule, "host"),
709                        http: rule.get("http").map(|http| HttpIngressRuleValue {
710                            paths: http
711                                .get("paths")
712                                .and_then(|p| p.as_sequence())
713                                .map(|arr| {
714                                    arr.iter()
715                                        .map(|path| HttpIngressPath {
716                                            path: get_string(path, "path"),
717                                            backend: path
718                                                .get("backend")
719                                                .map(|b| IngressBackend {
720                                                    service: b.get("service").map(|svc| {
721                                                        IngressServiceBackend {
722                                                            name: get_string(svc, "name")
723                                                                .unwrap_or_default(),
724                                                            port: svc.get("port").map(|p| {
725                                                                ServiceBackendPort {
726                                                                    number: get_i32(p, "number"),
727                                                                    name: get_string(p, "name"),
728                                                                }
729                                                            }),
730                                                        }
731                                                    }),
732                                                })
733                                                .unwrap_or_default(),
734                                        })
735                                        .collect()
736                                })
737                                .unwrap_or_default(),
738                        }),
739                    })
740                    .collect()
741            })
742            .unwrap_or_default(),
743    }
744}
745
746fn parse_network_policy(value: &serde_yaml::Value) -> NetworkPolicyData {
747    let (name, namespace, labels, annotations) = parse_metadata(value);
748    let spec = value.get("spec");
749
750    NetworkPolicyData {
751        name,
752        namespace,
753        labels,
754        annotations,
755        pod_selector: spec
756            .and_then(|s| s.get("podSelector"))
757            .map(|ps| LabelSelector {
758                match_labels: get_string_map(ps, "matchLabels"),
759            }),
760    }
761}
762
763fn parse_role(value: &serde_yaml::Value) -> RoleData {
764    let (name, namespace, labels, annotations) = parse_metadata(value);
765
766    RoleData {
767        name,
768        namespace,
769        labels,
770        annotations,
771        rules: parse_policy_rules(value.get("rules")),
772    }
773}
774
775fn parse_cluster_role(value: &serde_yaml::Value) -> ClusterRoleData {
776    let (name, _, labels, annotations) = parse_metadata(value);
777
778    ClusterRoleData {
779        name,
780        labels,
781        annotations,
782        rules: parse_policy_rules(value.get("rules")),
783    }
784}
785
786fn parse_policy_rules(rules: Option<&serde_yaml::Value>) -> Vec<PolicyRule> {
787    let Some(rules) = rules else {
788        return Vec::new();
789    };
790    let Some(arr) = rules.as_sequence() else {
791        return Vec::new();
792    };
793
794    arr.iter()
795        .map(|r| PolicyRule {
796            api_groups: parse_string_array(r.get("apiGroups")),
797            resources: parse_string_array(r.get("resources")),
798            verbs: parse_string_array(r.get("verbs")),
799        })
800        .collect()
801}
802
803fn parse_role_binding(value: &serde_yaml::Value) -> RoleBindingData {
804    let (name, namespace, labels, annotations) = parse_metadata(value);
805
806    RoleBindingData {
807        name,
808        namespace,
809        labels,
810        annotations,
811        role_ref: parse_role_ref(value.get("roleRef")),
812        subjects: parse_subjects(value.get("subjects")),
813    }
814}
815
816fn parse_cluster_role_binding(value: &serde_yaml::Value) -> ClusterRoleBindingData {
817    let (name, _, labels, annotations) = parse_metadata(value);
818
819    ClusterRoleBindingData {
820        name,
821        labels,
822        annotations,
823        role_ref: parse_role_ref(value.get("roleRef")),
824        subjects: parse_subjects(value.get("subjects")),
825    }
826}
827
828fn parse_role_ref(role_ref: Option<&serde_yaml::Value>) -> RoleRef {
829    let Some(rr) = role_ref else {
830        return RoleRef::default();
831    };
832    RoleRef {
833        api_group: get_string(rr, "apiGroup").unwrap_or_default(),
834        kind: get_string(rr, "kind").unwrap_or_default(),
835        name: get_string(rr, "name").unwrap_or_default(),
836    }
837}
838
839fn parse_subjects(subjects: Option<&serde_yaml::Value>) -> Vec<Subject> {
840    let Some(subjects) = subjects else {
841        return Vec::new();
842    };
843    let Some(arr) = subjects.as_sequence() else {
844        return Vec::new();
845    };
846
847    arr.iter()
848        .map(|s| Subject {
849            kind: get_string(s, "kind").unwrap_or_default(),
850            name: get_string(s, "name").unwrap_or_default(),
851            namespace: get_string(s, "namespace"),
852        })
853        .collect()
854}
855
856fn parse_service_account(value: &serde_yaml::Value) -> ServiceAccountData {
857    let (name, namespace, labels, annotations) = parse_metadata(value);
858
859    ServiceAccountData {
860        name,
861        namespace,
862        labels,
863        annotations,
864    }
865}
866
867fn parse_hpa(value: &serde_yaml::Value) -> HpaData {
868    let (name, namespace, labels, annotations) = parse_metadata(value);
869    let spec = value.get("spec");
870
871    HpaData {
872        name,
873        namespace,
874        labels,
875        annotations,
876        min_replicas: spec.and_then(|s| get_i32(s, "minReplicas")),
877        max_replicas: spec.and_then(|s| get_i32(s, "maxReplicas")).unwrap_or(0),
878        scale_target_ref: spec
879            .and_then(|s| s.get("scaleTargetRef"))
880            .map(|str| CrossVersionObjectReference {
881                api_version: get_string(str, "apiVersion"),
882                kind: get_string(str, "kind").unwrap_or_default(),
883                name: get_string(str, "name").unwrap_or_default(),
884            })
885            .unwrap_or_default(),
886    }
887}
888
889fn parse_pdb(value: &serde_yaml::Value) -> PdbData {
890    let (name, namespace, labels, annotations) = parse_metadata(value);
891    let spec = value.get("spec");
892
893    PdbData {
894        name,
895        namespace,
896        labels,
897        annotations,
898        min_available: spec.and_then(|s| {
899            get_string(s, "minAvailable")
900                .or_else(|| get_i32(s, "minAvailable").map(|n| n.to_string()))
901        }),
902        max_unavailable: spec.and_then(|s| {
903            get_string(s, "maxUnavailable")
904                .or_else(|| get_i32(s, "maxUnavailable").map(|n| n.to_string()))
905        }),
906        selector: spec
907            .and_then(|s| s.get("selector"))
908            .map(|sel| LabelSelector {
909                match_labels: get_string_map(sel, "matchLabels"),
910            }),
911        unhealthy_pod_eviction_policy: spec
912            .and_then(|s| get_string(s, "unhealthyPodEvictionPolicy")),
913    }
914}
915
916fn parse_pvc(value: &serde_yaml::Value) -> PvcData {
917    let (name, namespace, labels, annotations) = parse_metadata(value);
918
919    PvcData {
920        name,
921        namespace,
922        labels,
923        annotations,
924    }
925}
926
927fn parse_unknown(value: &serde_yaml::Value, api_version: &str, kind: &str) -> UnknownObject {
928    let (name, namespace, labels, annotations) = parse_metadata(value);
929
930    UnknownObject {
931        api_version: api_version.to_string(),
932        kind: kind.to_string(),
933        name,
934        namespace,
935        labels,
936        annotations,
937        raw: value.clone(),
938    }
939}
940
941/// YAML parsing errors.
942#[derive(Debug, Clone)]
943pub enum YamlParseError {
944    /// I/O error reading file.
945    IoError(String),
946    /// YAML syntax error.
947    SyntaxError(String),
948    /// Invalid Kubernetes object.
949    InvalidObject(String),
950}
951
952impl std::fmt::Display for YamlParseError {
953    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
954        match self {
955            Self::IoError(msg) => write!(f, "I/O error: {}", msg),
956            Self::SyntaxError(msg) => write!(f, "YAML syntax error: {}", msg),
957            Self::InvalidObject(msg) => write!(f, "Invalid K8s object: {}", msg),
958        }
959    }
960}
961
962impl std::error::Error for YamlParseError {}
963
964#[cfg(test)]
965mod tests {
966    use super::*;
967
968    #[test]
969    fn test_parse_deployment() {
970        let yaml = r#"
971apiVersion: apps/v1
972kind: Deployment
973metadata:
974  name: nginx-deployment
975  namespace: default
976  labels:
977    app: nginx
978spec:
979  replicas: 3
980  selector:
981    matchLabels:
982      app: nginx
983  template:
984    metadata:
985      labels:
986        app: nginx
987    spec:
988      containers:
989      - name: nginx
990        image: nginx:1.14.2
991        ports:
992        - containerPort: 80
993"#;
994        let objects = parse_yaml(yaml).unwrap();
995        assert_eq!(objects.len(), 1);
996        assert_eq!(objects[0].name(), "nginx-deployment");
997        assert_eq!(objects[0].namespace(), Some("default"));
998
999        if let K8sObject::Deployment(dep) = &objects[0].k8s_object {
1000            assert_eq!(dep.replicas, Some(3));
1001            assert!(dep.pod_spec.is_some());
1002            let pod_spec = dep.pod_spec.as_ref().unwrap();
1003            assert_eq!(pod_spec.containers.len(), 1);
1004            assert_eq!(pod_spec.containers[0].name, "nginx");
1005            assert_eq!(
1006                pod_spec.containers[0].image,
1007                Some("nginx:1.14.2".to_string())
1008            );
1009        } else {
1010            panic!("Expected Deployment");
1011        }
1012    }
1013
1014    #[test]
1015    fn test_parse_multi_document() {
1016        let yaml = r#"
1017apiVersion: v1
1018kind: Service
1019metadata:
1020  name: my-service
1021spec:
1022  selector:
1023    app: nginx
1024  ports:
1025  - port: 80
1026---
1027apiVersion: apps/v1
1028kind: Deployment
1029metadata:
1030  name: my-deployment
1031spec:
1032  replicas: 1
1033  selector:
1034    matchLabels:
1035      app: nginx
1036  template:
1037    spec:
1038      containers:
1039      - name: nginx
1040        image: nginx:latest
1041"#;
1042        let objects = parse_yaml(yaml).unwrap();
1043        assert_eq!(objects.len(), 2);
1044        assert_eq!(objects[0].name(), "my-service");
1045        assert_eq!(objects[1].name(), "my-deployment");
1046    }
1047
1048    #[test]
1049    fn test_parse_security_context() {
1050        let yaml = r#"
1051apiVersion: v1
1052kind: Pod
1053metadata:
1054  name: security-pod
1055spec:
1056  securityContext:
1057    runAsNonRoot: true
1058    runAsUser: 1000
1059  containers:
1060  - name: app
1061    image: myapp:1.0
1062    securityContext:
1063      privileged: false
1064      allowPrivilegeEscalation: false
1065      readOnlyRootFilesystem: true
1066      capabilities:
1067        drop:
1068        - ALL
1069        add:
1070        - NET_BIND_SERVICE
1071"#;
1072        let objects = parse_yaml(yaml).unwrap();
1073        assert_eq!(objects.len(), 1);
1074
1075        if let K8sObject::Pod(pod) = &objects[0].k8s_object {
1076            let spec = pod.spec.as_ref().unwrap();
1077            let psc = spec.security_context.as_ref().unwrap();
1078            assert_eq!(psc.run_as_non_root, Some(true));
1079            assert_eq!(psc.run_as_user, Some(1000));
1080
1081            let csc = spec.containers[0].security_context.as_ref().unwrap();
1082            assert_eq!(csc.privileged, Some(false));
1083            assert_eq!(csc.allow_privilege_escalation, Some(false));
1084            assert_eq!(csc.read_only_root_filesystem, Some(true));
1085
1086            let caps = csc.capabilities.as_ref().unwrap();
1087            assert_eq!(caps.drop, vec!["ALL"]);
1088            assert_eq!(caps.add, vec!["NET_BIND_SERVICE"]);
1089        } else {
1090            panic!("Expected Pod");
1091        }
1092    }
1093
1094    #[test]
1095    fn test_parse_unknown_crd() {
1096        let yaml = r#"
1097apiVersion: custom.io/v1
1098kind: MyCustomResource
1099metadata:
1100  name: my-custom
1101  namespace: custom-ns
1102spec:
1103  customField: value
1104"#;
1105        let objects = parse_yaml(yaml).unwrap();
1106        assert_eq!(objects.len(), 1);
1107
1108        if let K8sObject::Unknown(obj) = &objects[0].k8s_object {
1109            assert_eq!(obj.api_version, "custom.io/v1");
1110            assert_eq!(obj.kind, "MyCustomResource");
1111            assert_eq!(obj.name, "my-custom");
1112            assert_eq!(obj.namespace, Some("custom-ns".to_string()));
1113        } else {
1114            panic!("Expected Unknown");
1115        }
1116    }
1117
1118    #[test]
1119    fn test_parse_empty_yaml() {
1120        let yaml = "";
1121        let objects = parse_yaml(yaml).unwrap();
1122        assert!(objects.is_empty());
1123    }
1124
1125    #[test]
1126    fn test_parse_comment_only() {
1127        let yaml = "# This is a comment\n# Another comment";
1128        let objects = parse_yaml(yaml).unwrap();
1129        assert!(objects.is_empty());
1130    }
1131}