suspicious_pods_lib/
lib.rs

1use std::fmt::Formatter;
2
3use k8s_openapi::api::core::v1::{ContainerStatus, Pod};
4use kube::{api::Api, Client};
5use serde::{Deserialize, Serialize};
6
7#[derive(Serialize, Deserialize)]
8pub enum SuspiciousContainerReason {
9    ContainerWaiting(Option<String>),
10    NotReady,
11    Restarted {
12        count: i32,
13        exit_code: Option<i32>,
14        reason: Option<String>,
15    },
16    TerminatedWithError(i32),
17}
18
19impl std::fmt::Display for SuspiciousContainerReason {
20    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
21        match self {
22            SuspiciousContainerReason::ContainerWaiting(reason) => {
23                write!(f, "Waiting")?;
24                if let Some(r) = reason {
25                    write!(f, ": {}", r)?;
26                }
27            }
28            SuspiciousContainerReason::NotReady => {
29                write!(f, "Not Ready")?;
30            }
31            SuspiciousContainerReason::Restarted {
32                count,
33                exit_code,
34                reason,
35            } => {
36                if *count == 1 {
37                    write!(f, "Restarted {} time", count)?;
38                } else {
39                    write!(f, "Restarted {} times", count)?;
40                }
41                if let Some(e) = exit_code {
42                    write!(f, ". Last exit code: {}", e)?;
43                }
44                if let Some(r) = reason {
45                    write!(f, ". ({})", r)?;
46                }
47            }
48            SuspiciousContainerReason::TerminatedWithError(exit_code) => {
49                write!(f, "Terminated with error. Exit code {}.", exit_code)?;
50            }
51        }
52        Ok(())
53    }
54}
55
56#[derive(Deserialize, Serialize)]
57pub struct SuspiciousContainer {
58    pub name: String,
59    pub reason: SuspiciousContainerReason,
60}
61
62#[derive(Deserialize, Serialize)]
63pub enum SuspiciousPodReason {
64    Pending,
65    StuckOnInitContainer(String),
66    SuspiciousContainers(Vec<SuspiciousContainer>),
67}
68
69#[derive(Deserialize, Serialize)]
70pub struct SuspiciousPod {
71    pub namespace: String,
72    pub name: String,
73    pub reason: SuspiciousPodReason,
74}
75
76pub type Result<T> = std::result::Result<T, kube::error::Error>;
77
78fn is_suspicious_container(pod_name: &str, status: ContainerStatus) -> Option<SuspiciousContainer> {
79    let container_name = status.name;
80    let state = status.state.unwrap_or_else(|| {
81        panic!(
82            "Cannot get state for container {} in pod {}",
83            container_name, pod_name
84        )
85    });
86    let reason = if status.restart_count > 0 {
87        let last_state = status
88            .last_state
89            .unwrap_or_else(|| {
90                panic!(
91                    "Cannot get last state for container {} in pod {}",
92                    container_name, pod_name
93                )
94            })
95            .terminated;
96        Some(SuspiciousContainerReason::Restarted {
97            count: status.restart_count,
98            exit_code: last_state.as_ref().map(|s| s.exit_code),
99            reason: last_state.and_then(|s| s.reason),
100        })
101    } else if let Some(waiting_state) = state.waiting {
102        let msg: Option<String> = waiting_state.reason.or(waiting_state.message);
103        Some(SuspiciousContainerReason::ContainerWaiting(msg))
104    } else if state.terminated.is_some() && state.terminated.as_ref().unwrap().exit_code != 0 {
105        Some(SuspiciousContainerReason::TerminatedWithError(
106            state.terminated.unwrap().exit_code,
107        ))
108    } else if state.running.is_some() && !status.ready {
109        Some(SuspiciousContainerReason::NotReady)
110    } else {
111        None
112    };
113    reason.map(|reason| SuspiciousContainer {
114        name: container_name,
115        reason,
116    })
117}
118
119pub fn is_suspicious_pod(p: Pod) -> Option<SuspiciousPod> {
120    let metadata = p.metadata;
121    let pod_namespace = metadata.namespace.unwrap_or_else(|| "default".to_string());
122    let pod_name = metadata.name.expect("Could not find pod name");
123    let status = p
124        .status
125        .unwrap_or_else(|| panic!("Cannot get status for pod {}", pod_name));
126    if let Some(init_containers) = status.init_container_statuses {
127        if let Some(stuck_init) = init_containers.into_iter().find(|c| !c.ready) {
128            return Some(SuspiciousPod {
129                namespace: pod_namespace,
130                name: pod_name,
131                reason: SuspiciousPodReason::StuckOnInitContainer(stuck_init.name),
132            });
133        }
134    }
135    if let Some(statuses) = status.container_statuses {
136        let suspicious_containers: Vec<_> = statuses
137            .into_iter()
138            .filter_map(|c| is_suspicious_container(&pod_name, c))
139            .collect();
140
141        if suspicious_containers.is_empty() {
142            None
143        } else {
144            Some(SuspiciousPod {
145                namespace: pod_namespace,
146                name: pod_name,
147                reason: SuspiciousPodReason::SuspiciousContainers(suspicious_containers),
148            })
149        }
150    } else {
151        Some(SuspiciousPod {
152            namespace: pod_namespace,
153            name: pod_name,
154            reason: SuspiciousPodReason::Pending,
155        })
156    }
157}
158
159pub async fn get_all_suspicious_pods() -> Result<impl Iterator<Item = SuspiciousPod>> {
160    let client = Client::try_default().await?;
161    let pods = Api::<Pod>::all(client).list(&Default::default()).await?;
162    Ok(pods.items.into_iter().filter_map(is_suspicious_pod))
163}
164
165pub async fn get_suspicious_pods(namespace: &str) -> Result<impl Iterator<Item = SuspiciousPod>> {
166    let client = Client::try_default().await?;
167    let pods = Api::<Pod>::namespaced(client, namespace)
168        .list(&Default::default())
169        .await?;
170    Ok(pods.items.into_iter().filter_map(is_suspicious_pod))
171}