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