use async_trait::async_trait;
use chrono::Utc;
use k8s_openapi::api::networking::v1::{Ingress, IngressBackend, IngressRule, IngressStatus};
use ratatui::{
layout::Rect,
widgets::{Cell, Row},
Frame,
};
use super::{
models::{AppResource, KubeResource, Named},
utils::{self, UNKNOWN},
ActiveBlock, App,
};
use crate::{
draw_resource_tab,
network::Network,
ui::utils::{
describe_yaml_and_esc_hint, draw_describe_block, draw_resource_block, draw_yaml_block,
get_describe_active, get_resource_title, help_bold_line, responsive_columns, style_caution,
style_primary, title_with_dual_style, wide_hint, ColumnDef, ResourceTableProps, ViewTier,
},
};
#[derive(Clone, Debug, PartialEq)]
pub struct KubeIngress {
pub namespace: String,
pub name: String,
pub ingress_class: String,
pub address: String,
pub paths: String,
pub default_backend: String,
pub tls: String,
pub age: String,
k8s_obj: Ingress,
}
impl From<Ingress> for KubeIngress {
fn from(ingress: Ingress) -> Self {
let (ingress_class, rules, default_backend) = match &ingress.spec {
Some(spec) => {
let class_name = match &spec.ingress_class_name {
Some(c) => c.clone(),
None => UNKNOWN.into(),
};
(
class_name,
get_rules(&spec.rules),
format_backend(&spec.default_backend),
)
}
None => (String::default(), None, String::default()),
};
let name = match &ingress.metadata.name {
Some(n) => n.clone(),
None => UNKNOWN.into(),
};
let namespace = match &ingress.metadata.namespace {
Some(n) => n.clone(),
None => UNKNOWN.into(),
};
let paths = rules.unwrap_or_default();
let tls = match &ingress.spec {
Some(spec) => match &spec.tls {
Some(tls_list) if !tls_list.is_empty() => {
let hosts: Vec<String> = tls_list
.iter()
.flat_map(|t| t.hosts.clone().unwrap_or_default())
.collect();
if hosts.is_empty() {
"Yes".into()
} else {
hosts.join(",")
}
}
_ => "No".into(),
},
None => "No".into(),
};
Self {
name,
namespace,
ingress_class,
address: get_addresses(&ingress.status),
paths,
default_backend,
tls,
age: utils::to_age(ingress.metadata.creation_timestamp.as_ref(), Utc::now()),
k8s_obj: utils::sanitize_obj(ingress),
}
}
}
impl Named for KubeIngress {
fn get_name(&self) -> &String {
&self.name
}
}
impl KubeResource<Ingress> for KubeIngress {
fn get_k8s_obj(&self) -> &Ingress {
&self.k8s_obj
}
}
fn format_backend(backend: &Option<IngressBackend>) -> String {
match backend {
Some(backend) => {
if let Some(resource) = &backend.resource {
return resource.name.to_string();
}
if let Some(service) = &backend.service {
match &service.port {
Some(port) => {
if let Some(name) = &port.name {
format!("{}:{}", service.name, name)
} else if let Some(number) = &port.number {
format!("{}:{}", service.name, number)
} else {
String::default()
}
}
None => String::default(),
}
} else {
String::default()
}
}
None => String::default(),
}
}
fn get_rules(i_rules: &Option<Vec<IngressRule>>) -> Option<String> {
i_rules.as_ref().map(|rules| {
rules
.iter()
.map(|i_rule| {
let mut rule = i_rule.host.clone().unwrap_or("*".to_string());
if let Some(http) = &i_rule.http {
http.paths.iter().for_each(|path| {
rule = format!(
"{}{}►{}",
rule,
&path.path.clone().unwrap_or("/*".to_string()),
format_backend(&Some(path.backend.clone()))
);
});
}
rule
})
.collect::<Vec<_>>()
.join(" ")
})
}
fn get_addresses(i_status: &Option<IngressStatus>) -> String {
match i_status {
Some(status) => match &status.load_balancer {
Some(lb) => match &lb.ingress {
Some(ingress) => ingress
.iter()
.map(|i| {
if let Some(h) = &i.hostname {
h.to_string()
} else if let Some(ip) = &i.ip {
ip.to_string()
} else {
"<pending>".to_string()
}
})
.collect::<Vec<_>>()
.join(" "),
None => String::default(),
},
None => String::default(),
},
None => String::default(),
}
}
static INGRESS_TITLE: &str = "Ingresses";
pub struct IngressResource {}
#[async_trait]
impl AppResource for IngressResource {
fn render(block: ActiveBlock, f: &mut Frame<'_>, app: &mut App, area: Rect) {
draw_resource_tab!(
INGRESS_TITLE,
block,
f,
app,
area,
Self::render,
draw_block,
app.data.ingress
);
}
async fn get_resource(nw: &Network<'_>) {
let items: Vec<KubeIngress> = nw.get_namespaced_resources(Ingress::into).await;
let mut app = nw.app.lock().await;
app.data.ingress.set_items(items);
}
}
const INGRESS_COLUMNS: [ColumnDef; 8] = [
ColumnDef::all("Namespace", 10, 8, 8),
ColumnDef::all("Name", 20, 18, 18),
ColumnDef::all("Ingress class", 10, 10, 10),
ColumnDef::all("Paths", 25, 22, 22),
ColumnDef::all("Default backend", 10, 10, 10),
ColumnDef::all("Addresses", 10, 10, 10),
ColumnDef::standard("TLS", 12, 12),
ColumnDef::all("Age", 10, 10, 10),
];
fn draw_block(f: &mut Frame<'_>, app: &mut App, area: Rect) {
let is_loading = app.is_loading();
let title = get_resource_title(app, INGRESS_TITLE, "", app.data.ingress.items.len());
let tier = ViewTier::from_width(area.width, app.wide_columns);
let (headers, widths) = responsive_columns(&INGRESS_COLUMNS, tier);
draw_resource_block(
f,
area,
ResourceTableProps {
title,
inline_help: help_bold_line(
format!("{} | {}", describe_yaml_and_esc_hint(), wide_hint()),
app.light_theme,
),
resource: &mut app.data.ingress,
table_headers: headers,
column_widths: widths,
},
|c| {
let style = if c.address == "<pending>" {
style_caution(app.light_theme)
} else {
style_primary(app.light_theme)
};
let mut cells = vec![
Cell::from(c.namespace.to_owned()),
Cell::from(c.name.to_owned()),
Cell::from(c.ingress_class.to_owned()),
Cell::from(c.paths.to_owned()),
Cell::from(c.default_backend.to_owned()),
Cell::from(c.address.to_owned()),
];
if tier >= ViewTier::Standard {
cells.push(Cell::from(c.tls.to_owned()));
}
cells.push(Cell::from(c.age.to_owned()));
Row::new(cells).style(style)
},
app.light_theme,
is_loading,
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::test_utils::*;
#[test]
fn test_ingresses_from_api() {
let (ingresses, ingress_list): (Vec<KubeIngress>, Vec<_>) =
convert_resource_from_file("ingress");
assert_eq!(ingresses.len(), 3);
assert_eq!(
ingresses[0],
KubeIngress {
name: "ingdefault".into(),
namespace: "default".into(),
age: utils::to_age(Some(&get_time("2023-05-24T16:14:32Z")), Utc::now()),
k8s_obj: ingress_list[0].clone(),
ingress_class: "default".into(),
address: "".into(),
paths: "foo.com/►svc:8080".into(),
default_backend: "defaultsvc:http".into(),
tls: "foo.com".into(),
}
);
assert_eq!(
ingresses[1],
KubeIngress {
name: "test".into(),
namespace: "default".into(),
age: utils::to_age(Some(&get_time("2023-05-24T16:20:48Z")), Utc::now()),
k8s_obj: ingress_list[1].clone(),
ingress_class: "nginx".into(),
address: "192.168.49.2".into(),
paths: "".into(),
default_backend: "test:5701".into(),
tls: "No".into(),
}
);
assert_eq!(
ingresses[2],
KubeIngress {
name: "test-ingress".into(),
namespace: "dev".into(),
age: utils::to_age(Some(&get_time("2023-05-24T16:22:23Z")), Utc::now()),
k8s_obj: ingress_list[2].clone(),
ingress_class: "nginx".into(),
address: "192.168.49.2".into(),
paths: "demo.apps.mlopshub.com/►hello-service:80".into(),
default_backend: "".into(),
tls: "No".into(),
}
);
}
}