podium/app/state/pods/
mod.rs

1mod 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}