poddy/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 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}