podium 0.2.0

Your friendly pod buddy
Documentation
mod data;

use data::*;

use crate::k8s::ago;
use crate::{app::state::list::ListResource, client::Client, input::key::Key};
use k8s_openapi::api::core::v1::Pod;
use kube::{
    api::{DeleteParams, Preconditions},
    Api, Resource, ResourceExt,
};
use ratatui::{layout::*, style::*, widgets::*};
use std::{fmt::Debug, future::Future, hash::Hash, pin::Pin, sync::Arc};

impl ListResource for Pod {
    type Resource = Self;
    type Message = Msg;

    fn render_table<'a>(items: &mut [Arc<Self::Resource>]) -> Table<'a>
    where
        <<Self as ListResource>::Resource as Resource>::DynamicType: Hash + Eq,
    {
        items.sort_unstable_by_key(|a| a.name_any());

        let selected_style = Style::default().add_modifier(Modifier::REVERSED);
        let normal_style = Style::default();
        let header_cells = ["Name", "Ready", "State", "Restarts", "Age"]
            .iter()
            .map(|h| Cell::from(*h).style(Style::default().add_modifier(Modifier::BOLD)));
        let header = Row::new(header_cells).style(normal_style).height(1);

        let rows: Vec<Row> = items.iter().map(|pod| make_row(pod)).collect();

        Table::new(
            rows,
            [
                Constraint::Min(64),
                Constraint::Min(10),
                Constraint::Min(20),
                Constraint::Min(15),
                Constraint::Min(10),
            ],
        )
        .header(header)
        .block(Block::default().borders(Borders::ALL).title("Pods"))
        .highlight_style(selected_style)
        .highlight_symbol(">> ")
    }

    fn on_key(items: &[Arc<Self::Resource>], state: &TableState, key: Key) -> Option<Self::Message>
    where
        <<Self as ListResource>::Resource as kube::Resource>::DynamicType: Hash + Eq,
    {
        match key {
            Key::Char('k') => trigger_kill(items, state),
            _ => None,
        }
    }

    fn process(
        client: Arc<Client>,
        msg: Self::Message,
    ) -> Pin<Box<dyn Future<Output = ()> + Send>> {
        Box::pin(async {
            match msg {
                Msg::KillPod(pod) => execute_kill(client, &pod).await,
            }
        })
    }
}

fn trigger_kill(pods: &[Arc<Pod>], state: &TableState) -> Option<Msg> {
    let mut pods = pods.to_vec();
    pods.sort_unstable_by_key(|a| a.name_any());

    if let Some(pod) = state.selected().and_then(|i| pods.get(i)) {
        Some(Msg::KillPod(pod.clone()))
    } else {
        None
    }
}

fn make_row<'a>(pod: &Pod) -> Row<'a> {
    let mut style = Style::default();

    let name = pod.name_any();
    let ready = pod.status.as_ref().and_then(make_ready).unwrap_or_default();

    let state = if pod.meta().deletion_timestamp.is_some() {
        PodState::Terminating
    } else {
        pod.status.as_ref().map(make_state).unwrap_or_default()
    };
    let restarts = pod
        .status
        .as_ref()
        .and_then(make_restarts)
        .unwrap_or_else(|| String::from("0"));
    let age = pod
        .creation_timestamp()
        .as_ref()
        .and_then(ago)
        .unwrap_or_default();

    match &state {
        PodState::Pending => {
            style = style.bg(Color::Rgb(128, 0, 128));
        }
        PodState::Error => {
            style = style.bg(Color::Rgb(128, 0, 0)).add_modifier(Modifier::BOLD);
        }
        PodState::CrashLoopBackOff => {
            style = style.bg(Color::Rgb(128, 0, 0));
        }
        PodState::Terminating => {
            style = style.bg(Color::Rgb(128, 128, 0));
        }
        _ => {}
    }

    Row::new(vec![name, ready, state.to_string(), restarts, age]).style(style)
}

#[derive(Debug)]
pub enum Msg {
    KillPod(Arc<Pod>),
}

async fn execute_kill(client: Arc<Client>, pod: &Pod) {
    let result = client
        .run(|context| async move {
            if let Some(namespace) = pod.namespace() {
                let pods: Api<Pod> = Api::namespaced(context.client, &namespace);

                pods.delete(
                    &pod.name_any(),
                    &DeleteParams::default().preconditions(Preconditions {
                        uid: pod.uid(),
                        ..Default::default()
                    }),
                )
                .await?;
            }
            Ok::<_, anyhow::Error>(())
        })
        .await;

    match result {
        Ok(_) => {
            log::info!("Pod killed");
        }
        Err(err) => {
            log::warn!("Failed to kill pod: {err}");
        }
    }
}