podium/app/state/pods/
mod.rs1mod data;
2
3use data::*;
4
5use crate::k8s::ago;
6use crate::{app::state::list::ListResource, client::Client, input::key::Key};
7use k8s_openapi::api::core::v1::Pod;
8use kube::{
9 api::{DeleteParams, Preconditions},
10 Api, Resource, ResourceExt,
11};
12use ratatui::{layout::*, style::*, widgets::*};
13use std::{fmt::Debug, future::Future, hash::Hash, pin::Pin, sync::Arc};
14
15impl ListResource for Pod {
16 type Resource = Self;
17 type Message = Msg;
18
19 fn render_table<'a>(items: &mut [Arc<Self::Resource>]) -> Table<'a>
20 where
21 <<Self as ListResource>::Resource as Resource>::DynamicType: Hash + Eq,
22 {
23 items.sort_unstable_by_key(|a| a.name_any());
24
25 let selected_style = Style::default().add_modifier(Modifier::REVERSED);
26 let normal_style = Style::default();
27 let header_cells = ["Name", "Ready", "State", "Restarts", "Age"]
28 .iter()
29 .map(|h| Cell::from(*h).style(Style::default().add_modifier(Modifier::BOLD)));
30 let header = Row::new(header_cells).style(normal_style).height(1);
31
32 let rows: Vec<Row> = items.iter().map(|pod| make_row(pod)).collect();
33
34 Table::new(
35 rows,
36 [
37 Constraint::Min(64),
38 Constraint::Min(10),
39 Constraint::Min(20),
40 Constraint::Min(15),
41 Constraint::Min(10),
42 ],
43 )
44 .header(header)
45 .block(Block::default().borders(Borders::ALL).title("Pods"))
46 .highlight_style(selected_style)
47 .highlight_symbol(">> ")
48 }
49
50 fn on_key(items: &[Arc<Self::Resource>], state: &TableState, key: Key) -> Option<Self::Message>
51 where
52 <<Self as ListResource>::Resource as kube::Resource>::DynamicType: Hash + Eq,
53 {
54 match key {
55 Key::Char('k') => trigger_kill(items, state),
56 _ => None,
57 }
58 }
59
60 fn process(
61 client: Arc<Client>,
62 msg: Self::Message,
63 ) -> Pin<Box<dyn Future<Output = ()> + Send>> {
64 Box::pin(async {
65 match msg {
66 Msg::KillPod(pod) => execute_kill(client, &pod).await,
67 }
68 })
69 }
70}
71
72fn trigger_kill(pods: &[Arc<Pod>], state: &TableState) -> Option<Msg> {
73 let mut pods = pods.to_vec();
74 pods.sort_unstable_by_key(|a| a.name_any());
75
76 if let Some(pod) = state.selected().and_then(|i| pods.get(i)) {
77 Some(Msg::KillPod(pod.clone()))
78 } else {
79 None
80 }
81}
82
83fn make_row<'a>(pod: &Pod) -> Row<'a> {
84 let mut style = Style::default();
85
86 let name = pod.name_any();
87 let ready = pod.status.as_ref().and_then(make_ready).unwrap_or_default();
88
89 let state = if pod.meta().deletion_timestamp.is_some() {
90 PodState::Terminating
91 } else {
92 pod.status.as_ref().map(make_state).unwrap_or_default()
93 };
94 let restarts = pod
95 .status
96 .as_ref()
97 .and_then(make_restarts)
98 .unwrap_or_else(|| String::from("0"));
99 let age = pod
100 .creation_timestamp()
101 .as_ref()
102 .and_then(ago)
103 .unwrap_or_default();
104
105 match &state {
106 PodState::Pending => {
107 style = style.bg(Color::Rgb(128, 0, 128));
108 }
109 PodState::Error => {
110 style = style.bg(Color::Rgb(128, 0, 0)).add_modifier(Modifier::BOLD);
111 }
112 PodState::CrashLoopBackOff => {
113 style = style.bg(Color::Rgb(128, 0, 0));
114 }
115 PodState::Terminating => {
116 style = style.bg(Color::Rgb(128, 128, 0));
117 }
118 _ => {}
119 }
120
121 Row::new(vec![name, ready, state.to_string(), restarts, age]).style(style)
122}
123
124#[derive(Debug)]
125pub enum Msg {
126 KillPod(Arc<Pod>),
127}
128
129async fn execute_kill(client: Arc<Client>, pod: &Pod) {
130 let result = client
131 .run(|context| async move {
132 if let Some(namespace) = pod.namespace() {
133 let pods: Api<Pod> = Api::namespaced(context.client, &namespace);
134
135 pods.delete(
136 &pod.name_any(),
137 &DeleteParams::default().preconditions(Preconditions {
138 uid: pod.uid(),
139 ..Default::default()
140 }),
141 )
142 .await?;
143 }
144 Ok::<_, anyhow::Error>(())
145 })
146 .await;
147
148 match result {
149 Ok(_) => {
150 log::info!("Pod killed");
151 }
152 Err(err) => {
153 log::warn!("Failed to kill pod: {err}");
154 }
155 }
156}