suspicious_pods_lib/
lib.rs1use 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}