1use crate::analyzer::kubelint::context::Object;
4use crate::analyzer::kubelint::context::object::*;
5use std::collections::BTreeMap;
6use std::path::Path;
7
8pub fn parse_yaml(content: &str) -> Result<Vec<Object>, YamlParseError> {
10 parse_yaml_with_path(content, Path::new("<stdin>"))
11}
12
13pub 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 for doc in content.split("\n---") {
20 let doc = doc.trim();
21 if doc.is_empty() || doc.starts_with('#') {
22 line_number += doc.lines().count() as u32 + 1;
24 continue;
25 }
26
27 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 line_number += doc.lines().count() as u32 + 1;
44 }
45
46 Ok(objects)
47}
48
49pub 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
57pub 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 eprintln!("Warning: failed to parse {}: {}", entry_path.display(), e);
75 }
76 }
77 }
78 }
79 }
80
81 Ok(objects)
82}
83
84fn 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
117fn 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
513fn 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 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#[derive(Debug, Clone)]
943pub enum YamlParseError {
944 IoError(String),
946 SyntaxError(String),
948 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}