podium 0.2.0

Your friendly pod buddy
Documentation
use crate::app::state::list::ListResource;
use crate::client::Client;
use crate::input::key::Key;
use crate::k8s::{ago, Scale};
use k8s_openapi::api::apps::v1::Deployment;
use kube::{Api, Resource, ResourceExt};
use ratatui::{layout::*, style::*, widgets::*};
use std::future::Future;
use std::hash::Hash;
use std::pin::Pin;
use std::sync::Arc;

pub enum Msg {
    Restart(Arc<Deployment>),
    ScaleUp(Arc<Deployment>),
    ScaleDown(Arc<Deployment>),
}

pub struct Deployments;

impl ListResource for Deployments {
    type Resource = Deployment;
    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", "Updated", "Available", "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(|deployment| Self::make_row(deployment))
            .collect();

        Table::new(
            rows,
            [
                Constraint::Min(64),
                Constraint::Min(15),
                Constraint::Min(10),
                Constraint::Min(10),
            ],
        )
        .header(header)
        .block(Block::default().borders(Borders::ALL).title("Deployments"))
        .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 Resource>::DynamicType: Hash + Eq,
    {
        match key {
            Key::Char('r') => Self::with_selection(items, state, Msg::Restart),
            Key::Char('+') => Self::with_selection(items, state, Msg::ScaleUp),
            Key::Char('-') => Self::with_selection(items, state, Msg::ScaleDown),
            _ => None,
        }
    }

    fn process(
        client: Arc<Client>,
        msg: Self::Message,
    ) -> Pin<Box<dyn Future<Output = ()> + Send>> {
        Box::pin(async {
            match msg {
                Msg::Restart(deployment) => {
                    Self::restart(client, &deployment).await;
                }
                Msg::ScaleUp(deployment) => {
                    Self::scale(client, &deployment, 1).await;
                }
                Msg::ScaleDown(deployment) => {
                    Self::scale(client, &deployment, -1).await;
                }
            }
        })
    }
}

impl Deployments {
    fn with_selection<F, I>(
        deployments: &[Arc<Deployment>],
        state: &TableState,
        f: F,
    ) -> Option<Msg>
    where
        F: FnOnce(Arc<Deployment>) -> I,
        I: Into<Option<Msg>>,
    {
        let mut deployments = deployments.to_vec();
        deployments.sort_unstable_by_key(|a| a.name_any());

        if let Some(deployment) = state.selected().and_then(|i| deployments.get(i)) {
            f(deployment.clone()).into()
        } else {
            None
        }
    }

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

        let name = deployment.name_any();

        let (ready, updated, available) = deployment
            .status
            .as_ref()
            .map(|s| {
                (
                    (
                        s.ready_replicas.unwrap_or_default(),
                        s.replicas.unwrap_or_default(),
                    ),
                    s.updated_replicas.unwrap_or_default(),
                    s.available_replicas.unwrap_or_default(),
                )
            })
            .unwrap_or_default();

        let age = deployment
            .creation_timestamp()
            .as_ref()
            .and_then(ago)
            .unwrap_or_default();

        if ready.0 == 0 {
            style.fg = Some(Color::Red);
        } else if ready.0 < ready.1 {
            style.fg = Some(Color::Yellow);
        }

        Row::new(vec![
            name,
            format!("{}/{}", ready.0, ready.1),
            updated.to_string(),
            available.to_string(),
            age,
        ])
        .style(style)
    }

    async fn restart(client: Arc<Client>, deployment: &Deployment) {
        let _ = client
            .run(|ctx| {
                let api: Api<Deployment> = ctx.api_namespaced();
                async move { api.restart(&deployment.name_any()).await }
            })
            .await;
    }

    async fn scale(client: Arc<Client>, deployment: &Deployment, amount: i32) {
        let _ = client
            .run(|ctx| {
                let api: Api<Deployment> = ctx.api_namespaced();
                async move {
                    let current: i32 = deployment
                        .spec
                        .as_ref()
                        .and_then(|s| s.replicas)
                        .unwrap_or_default();

                    let replicas = current.saturating_add(amount);
                    if replicas != current {
                        api.replicas(&deployment.name_any(), replicas)
                            .await
                            .map(|_| ())
                    } else {
                        Ok(())
                    }
                }
            })
            .await;
    }
}