use async_trait::async_trait;
use chrono::Utc;
use k8s_openapi::api::core::v1::{
Container, ContainerPort, ContainerState, ContainerStateWaiting, ContainerStatus, Pod, PodSpec,
PodStatus,
};
use ratatui::{
layout::Rect,
style::Style,
widgets::{Cell, Row},
Frame,
};
use super::{
key_binding::DEFAULT_KEYBINDING,
models::{AppResource, KubeResource, Named},
utils::{self, UNKNOWN},
ActiveBlock, App,
};
use crate::{
network::Network,
ui::utils::{
action_hint, copy_and_escape_title_line, copy_scroll_and_escape_title_line,
describe_yaml_and_logs_hint, draw_describe_block, draw_resource_block, draw_yaml_block,
get_describe_active, get_resource_title, help_bold_line, help_part, layout_block_top_border,
loading, mixed_bold_line, responsive_columns, style_caution, style_failure, style_primary,
style_success, title_with_dual_style, wide_hint, ColumnDef, ResourceTableProps, ViewTier,
},
};
#[derive(Clone, Default, Debug, PartialEq)]
pub struct KubePod {
pub namespace: String,
pub name: String,
pub ready: (i32, i32),
pub status: String,
pub restarts: i32,
pub cpu: String,
pub mem: String,
pub node: String,
pub ip: String,
pub age: String,
pub containers: Vec<KubeContainer>,
k8s_obj: Pod,
}
#[derive(Clone, Default, Debug, PartialEq)]
pub struct KubeContainer {
pub name: String,
pub image: String,
pub ready: String,
pub status: String,
pub restarts: i32,
pub liveliness_probe: bool,
pub readiness_probe: bool,
pub ports: String,
pub age: String,
pub pod_name: String,
pub init: bool,
k8s_obj: Option<Container>,
}
impl Named for KubeContainer {
fn get_name(&self) -> &String {
&self.name
}
}
impl KubeResource<Option<Container>> for KubeContainer {
fn get_k8s_obj(&self) -> &Option<Container> {
&None
}
}
impl From<Pod> for KubePod {
fn from(pod: Pod) -> Self {
let age = utils::to_age(pod.metadata.creation_timestamp.as_ref(), Utc::now());
let pod_name = pod.metadata.name.clone().unwrap_or_default();
let main_containers = pod
.spec
.as_ref()
.map(|spec| spec.containers.clone())
.unwrap_or_default();
let ready_total = main_containers.len() as i32;
let (status, cr, restarts, ready_count, containers) = match &pod.status {
Some(status) => {
let (mut cr, mut rc) = (0, 0);
let has_container_statuses = status.container_statuses.is_some();
if let Some(c_stats) = status.container_statuses.as_ref() {
c_stats.iter().for_each(|cs| {
if cs.ready {
cr += 1;
}
rc += cs.restart_count;
});
}
let mut containers: Vec<KubeContainer> = main_containers
.iter()
.map(|c| {
KubeContainer::from_api(
c,
pod_name.to_owned(),
age.to_owned(),
&status.container_statuses,
false,
)
})
.collect();
let mut init_containers: Vec<KubeContainer> = pod
.spec
.as_ref()
.unwrap_or(&PodSpec::default())
.init_containers
.as_ref()
.unwrap_or(&vec![])
.iter()
.map(|c| {
KubeContainer::from_api(
c,
pod_name.to_owned(),
age.to_owned(),
&status.init_container_statuses,
true,
)
})
.collect();
containers.append(&mut init_containers);
let status_name = get_status(status, &pod);
let ready_count = if has_container_statuses || !is_pending_like_status(&status_name) {
status
.container_statuses
.as_ref()
.map(|c_stats| c_stats.len() as i32)
.unwrap_or_default()
} else {
ready_total
};
(status_name, cr, rc, ready_count, containers)
}
_ => (UNKNOWN.into(), 0, 0, 0, vec![]),
};
KubePod {
name: pod_name,
namespace: pod.metadata.namespace.clone().unwrap_or_default(),
ready: (cr, ready_count),
restarts,
cpu: String::default(),
mem: String::default(),
node: pod
.spec
.as_ref()
.and_then(|s| s.node_name.clone())
.unwrap_or_default(),
ip: pod
.status
.as_ref()
.and_then(|s| s.pod_ip.clone())
.unwrap_or_default(),
status,
age,
containers,
k8s_obj: utils::sanitize_obj(pod),
}
}
}
impl Named for KubePod {
fn get_name(&self) -> &String {
&self.name
}
}
impl KubeResource<Pod> for KubePod {
fn get_k8s_obj(&self) -> &Pod {
&self.k8s_obj
}
}
static PODS_TITLE: &str = "Pods";
pub struct PodResource {}
#[async_trait]
impl AppResource for PodResource {
fn render(block: ActiveBlock, f: &mut Frame<'_>, app: &mut App, area: Rect) {
match block {
ActiveBlock::Containers => draw_containers_block(f, app, area),
ActiveBlock::Describe => draw_describe_block(
f,
app,
area,
title_with_dual_style(
get_resource_title(
app,
PODS_TITLE,
get_describe_active(block),
app.data.pods.items.len(),
),
copy_and_escape_title_line(PODS_TITLE, app.light_theme),
app.light_theme,
),
),
ActiveBlock::Yaml => draw_yaml_block(
f,
app,
area,
title_with_dual_style(
get_resource_title(
app,
PODS_TITLE,
get_describe_active(block),
app.data.pods.items.len(),
),
copy_and_escape_title_line(PODS_TITLE, app.light_theme),
app.light_theme,
),
),
ActiveBlock::Logs => draw_logs_block(f, app, area),
ActiveBlock::Namespaces => Self::render(app.get_prev_route().active_block, f, app, area),
_ => draw_block(f, app, area),
}
}
async fn get_resource(nw: &Network<'_>) {
let items: Vec<KubePod> = nw.get_namespaced_resources(Pod::into).await;
let mut app = nw.app.lock().await;
if app.data.selected.pod.is_some() {
let containers = &items.iter().find_map(|pod| {
if pod.name == app.data.selected.pod.clone().unwrap() {
Some(&pod.containers)
} else {
None
}
});
if containers.is_some() {
app.data.containers.set_items(containers.unwrap().clone());
}
}
app.data.pods.set_items(items);
}
}
fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().to_string() + chars.as_str(),
}
}
pub(crate) fn draw_block_as_sub(f: &mut Frame<'_>, app: &mut App, area: Rect) {
let is_loading = app.is_loading();
let parent = app
.data
.selected
.pod_selector_resource
.as_deref()
.map(capitalize_first)
.unwrap_or_default();
let base = format!("{} -> Pods", parent);
let title = get_resource_title(app, &base, &String::new(), app.data.pods.items.len());
let tier = ViewTier::from_width(area.width, app.wide_columns);
let (headers, widths) = responsive_columns(&POD_COLUMNS, tier);
draw_resource_block(
f,
area,
ResourceTableProps {
title,
inline_help: help_bold_line(
format!(
"{} | {} | {} | back {} ",
action_hint("containers", DEFAULT_KEYBINDING.submit.key),
describe_yaml_and_logs_hint().trim_end(),
wide_hint(),
DEFAULT_KEYBINDING.esc.key
),
app.light_theme,
),
resource: &mut app.data.pods,
table_headers: headers,
column_widths: widths,
},
|c| {
let style = get_resource_row_style(c.status.as_str(), c.ready, app.light_theme);
let mut cells = vec![
Cell::from(c.namespace.to_owned()),
Cell::from(c.name.to_owned()),
Cell::from(format!("{}/{}", c.ready.0, c.ready.1)),
Cell::from(c.status.to_owned()),
Cell::from(c.restarts.to_string()),
];
if tier >= ViewTier::Standard {
cells.push(Cell::from(c.node.to_owned()));
cells.push(Cell::from(c.ip.to_owned()));
}
cells.push(Cell::from(c.age.to_owned()));
Row::new(cells).style(style)
},
app.light_theme,
is_loading,
);
}
const POD_COLUMNS: [ColumnDef; 8] = [
ColumnDef::all("Namespace", 25, 20, 20),
ColumnDef::all("Name", 35, 25, 25),
ColumnDef::all("Ready", 10, 8, 8),
ColumnDef::all("Status", 10, 10, 10),
ColumnDef::all("Restarts", 10, 7, 7),
ColumnDef::standard("Node", 12, 12),
ColumnDef::standard("IP", 10, 10),
ColumnDef::all("Age", 10, 8, 8),
];
fn draw_block(f: &mut Frame<'_>, app: &mut App, area: Rect) {
let is_loading = app.is_loading();
let title = get_resource_title(app, PODS_TITLE, "", app.data.pods.items.len());
let tier = ViewTier::from_width(area.width, app.wide_columns);
let (headers, widths) = responsive_columns(&POD_COLUMNS, tier);
draw_resource_block(
f,
area,
ResourceTableProps {
title,
inline_help: help_bold_line(
format!(
"{} | {} | {}",
action_hint("containers", DEFAULT_KEYBINDING.submit.key),
describe_yaml_and_logs_hint(),
wide_hint()
),
app.light_theme,
),
resource: &mut app.data.pods,
table_headers: headers,
column_widths: widths,
},
|c| {
let style = get_resource_row_style(c.status.as_str(), c.ready, app.light_theme);
let mut cells = vec![
Cell::from(c.namespace.to_owned()),
Cell::from(c.name.to_owned()),
Cell::from(format!("{}/{}", c.ready.0, c.ready.1)),
Cell::from(c.status.to_owned()),
Cell::from(c.restarts.to_string()),
];
if tier >= ViewTier::Standard {
cells.push(Cell::from(c.node.to_owned()));
cells.push(Cell::from(c.ip.to_owned()));
}
cells.push(Cell::from(c.age.to_owned()));
Row::new(cells).style(style)
},
app.light_theme,
is_loading,
);
}
const CONTAINER_COLUMNS: [ColumnDef; 9] = [
ColumnDef::all("Name", 20, 20, 20),
ColumnDef::all("Image", 25, 25, 25),
ColumnDef::all("Init", 5, 5, 5),
ColumnDef::all("Ready", 5, 5, 5),
ColumnDef::all("State", 10, 10, 10),
ColumnDef::all("Restarts", 5, 5, 5),
ColumnDef::all("Probes(L/R)", 10, 10, 10),
ColumnDef::all("Ports", 10, 10, 10),
ColumnDef::all("Age", 10, 10, 10),
];
pub(crate) fn draw_containers_block(f: &mut Frame<'_>, app: &mut App, area: Rect) {
let is_loading = app.is_loading();
let title = get_container_title(app, app.data.containers.items.len(), "");
let (headers, widths) = responsive_columns(&CONTAINER_COLUMNS, ViewTier::Compact);
draw_resource_block(
f,
area,
ResourceTableProps {
title,
inline_help: mixed_bold_line(
[
help_part(format!(
"{} | {} | ",
action_hint("logs", DEFAULT_KEYBINDING.submit.key),
action_hint("shell", DEFAULT_KEYBINDING.shell_exec.key),
)),
help_part(format!("back {} ", DEFAULT_KEYBINDING.esc.key)),
],
app.light_theme,
),
resource: &mut app.data.containers,
table_headers: headers,
column_widths: widths,
},
|c| {
let style = get_resource_row_style(c.status.as_str(), (0, 0), app.light_theme);
Row::new(vec![
Cell::from(c.name.to_owned()),
Cell::from(c.image.to_owned()),
Cell::from(c.init.to_string()),
Cell::from(c.ready.to_owned()),
Cell::from(c.status.to_owned()),
Cell::from(c.restarts.to_string()),
Cell::from(format!("{}/{}", c.liveliness_probe, c.readiness_probe,)),
Cell::from(c.ports.to_owned()),
Cell::from(c.age.to_owned()),
])
.style(style)
},
app.light_theme,
is_loading,
);
}
fn get_container_title<S: AsRef<str>>(app: &App, container_len: usize, suffix: S) -> String {
let base = match &app.data.selected.pod_selector_resource {
Some(resource) => format!("{} -> Pods", capitalize_first(resource)),
None => PODS_TITLE.to_string(),
};
let suffix = format!("-> Containers [{}] {}", container_len, suffix.as_ref());
get_resource_title(app, &base, &suffix, app.data.pods.items.len())
}
pub(crate) fn draw_logs_block(f: &mut Frame<'_>, app: &mut App, area: Rect) {
let is_aggregate = app.data.logs.id.starts_with("agg:");
let (title, hint) = if is_aggregate {
let resource = app
.data
.selected
.pod_selector_resource
.as_deref()
.map(capitalize_first)
.unwrap_or_default();
let agg_name = app.data.logs.id.strip_prefix("agg:").unwrap_or_default();
(
format!(" {} -> Logs ({}) ", resource, agg_name),
help_bold_line(
format!(
"{} | {} | back {} ",
action_hint("copy", DEFAULT_KEYBINDING.copy_to_clipboard.key),
action_hint(
if app.log_auto_scroll {
"pause scroll"
} else {
"resume scroll"
},
DEFAULT_KEYBINDING.log_auto_scroll.key
),
DEFAULT_KEYBINDING.esc.key
),
app.light_theme,
),
)
} else {
let selected_container = app.data.selected.container.clone();
let container_name = selected_container.unwrap_or_default();
(
get_container_title(
app,
app.data.containers.items.len(),
format!("-> Logs ({}) ", container_name),
),
copy_scroll_and_escape_title_line("Containers", app.log_auto_scroll, app.light_theme),
)
};
let title = title_with_dual_style(title, hint, app.light_theme);
let block = layout_block_top_border(title);
let selected_container = app.data.selected.container.clone();
let container_name = selected_container.unwrap_or_default();
if container_name == app.data.logs.id || is_aggregate {
app.data.logs.render_list(
f,
area,
block,
style_primary(app.light_theme),
app.log_auto_scroll,
);
} else {
loading(f, block, area, app.is_loading(), app.light_theme);
}
}
fn get_resource_row_style(status: &str, ready: (i32, i32), light: bool) -> Style {
if status == "Running" && ready.0 == ready.1 {
style_primary(light)
} else if status == "Completed" {
style_success(light)
} else if [
"ContainerCreating",
"PodInitializing",
"Pending",
"Initialized",
]
.contains(&status)
{
style_caution(light)
} else {
style_failure(light)
}
}
fn is_pending_like_status(status: &str) -> bool {
[
"ContainerCreating",
"PodInitializing",
"Pending",
"Initialized",
]
.contains(&status)
}
impl KubeContainer {
pub fn from_api(
container: &Container,
pod_name: String,
age: String,
c_stats_ref: &Option<Vec<ContainerStatus>>,
init: bool,
) -> Self {
let (mut ready, mut status, mut restarts) = ("false".to_string(), "<none>".to_string(), 0);
if let Some(c_stats) = c_stats_ref {
if let Some(c_stat) = c_stats.iter().find(|cs| cs.name == container.name) {
ready = c_stat.ready.to_string();
status = get_container_state(c_stat.state.clone());
restarts = c_stat.restart_count;
}
}
KubeContainer {
name: container.name.clone(),
pod_name,
image: container.image.clone().unwrap_or_default(),
ready,
status,
restarts,
liveliness_probe: container.liveness_probe.is_some(),
readiness_probe: container.readiness_probe.is_some(),
ports: get_container_ports(&container.ports).unwrap_or_default(),
age,
init,
k8s_obj: None,
}
}
}
fn get_container_state(os: Option<ContainerState>) -> String {
match os {
Some(s) => {
if let Some(sw) = s.waiting {
sw.reason.unwrap_or_else(|| "Waiting".into())
} else if let Some(st) = s.terminated {
st.reason.unwrap_or_else(|| "Terminating".into())
} else if s.running.is_some() {
"Running".into()
} else {
"<none>".into()
}
}
None => "<none>".into(),
}
}
fn get_status(stat: &PodStatus, pod: &Pod) -> String {
let status = match &stat.phase {
Some(phase) => phase.to_owned(),
_ => UNKNOWN.into(),
};
let status = match &stat.reason {
Some(reason) => {
if reason == "NodeLost" && pod.metadata.deletion_timestamp.is_some() {
UNKNOWN.into()
} else {
reason.to_owned()
}
}
None => status,
};
let status = match &stat.init_container_statuses {
Some(ics) => {
for (i, cs) in ics.iter().enumerate() {
let c_status = match &cs.state {
Some(s) => {
if let Some(st) = &s.terminated {
if st.exit_code == 0 {
"".into()
} else if st.reason.as_ref().unwrap_or(&String::default()).is_empty() {
format!("Init:{}", st.reason.as_ref().unwrap())
} else if st.signal.unwrap_or_default() != 0 {
format!("Init:Signal:{}", st.signal.unwrap())
} else {
format!("Init:ExitCode:{}", st.exit_code)
}
} else if is_pod_init(s.waiting.clone()) {
format!(
"Init:{}",
s.waiting
.as_ref()
.unwrap()
.reason
.as_ref()
.unwrap_or(&String::default())
)
} else {
format!(
"Init:{}/{}",
i,
pod
.spec
.as_ref()
.and_then(|ps| ps.init_containers.as_ref().map(|pic| pic.len()))
.unwrap_or(0)
)
}
}
None => "".into(),
};
if !c_status.is_empty() {
return c_status;
}
}
status
}
None => status,
};
let (mut status, running) = match &stat.container_statuses {
Some(css) => {
let mut running = false;
let status = css
.iter()
.rev()
.find_map(|cs| {
cs.state.as_ref().and_then(|s| {
if cs.ready && s.running.is_some() {
running = true;
}
if s
.waiting
.as_ref()
.and_then(|w| w.reason.as_ref().map(|v| !v.is_empty()))
.unwrap_or_default()
{
s.waiting.as_ref().and_then(|w| w.reason.clone())
} else if s
.terminated
.as_ref()
.and_then(|w| w.reason.as_ref().map(|v| !v.is_empty()))
.unwrap_or_default()
{
s.terminated.as_ref().and_then(|w| w.reason.clone())
} else if let Some(st) = &s.terminated {
if st.signal.unwrap_or_default() != 0 {
Some(format!("Signal:{}", st.signal.unwrap_or_default()))
} else {
Some(format!("ExitCode:{}", st.exit_code))
}
} else {
Some(status.clone())
}
})
})
.unwrap_or_default();
(status, running)
}
None => (status, false),
};
if running && status == "Completed" {
status = "Running".into();
}
if pod.metadata.deletion_timestamp.is_none() {
return status;
}
"Terminating".into()
}
fn is_pod_init(sw: Option<ContainerStateWaiting>) -> bool {
sw.map(|w| w.reason.unwrap_or_default() != "PodInitializing")
.unwrap_or_default()
}
fn get_container_ports(ports_ref: &Option<Vec<ContainerPort>>) -> Option<String> {
ports_ref.as_ref().map(|ports| {
ports
.iter()
.map(|c_port| {
let mut port = String::new();
if let Some(name) = c_port.name.clone() {
port = format!("{}:", name);
}
port = format!("{}{}", port, c_port.container_port);
if let Some(protocol) = c_port.protocol.clone() {
if protocol != "TCP" {
port = format!("{}/{}", port, c_port.protocol.clone().unwrap());
}
}
port
})
.collect::<Vec<_>>()
.join(", ")
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::test_utils::*;
use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
#[test]
fn test_get_container_title() {
let app = App::default();
assert_eq!(
get_container_title(&app, 3, "hello"),
" Pods (ns: all) [0] -> Containers [3] hello"
);
}
#[test]
fn test_get_container_title_with_resource_breadcrumb() {
let mut app = App::default();
app.data.selected.pod_selector_resource = Some("deployment".into());
assert_eq!(
get_container_title(&app, 2, ""),
" Deployment -> Pods (ns: all) [0] -> Containers [2] "
);
}
#[test]
fn test_get_container_title_with_resource_breadcrumb_and_logs() {
let mut app = App::default();
app.data.selected.pod_selector_resource = Some("statefulset".into());
assert_eq!(
get_container_title(&app, 1, "-> Logs (nginx) "),
" Statefulset -> Pods (ns: all) [0] -> Containers [1] -> Logs (nginx) "
);
}
#[test]
fn test_capitalize_first() {
assert_eq!(capitalize_first("deployment"), "Deployment");
assert_eq!(capitalize_first("statefulset"), "Statefulset");
assert_eq!(capitalize_first(""), "");
assert_eq!(capitalize_first("A"), "A");
}
#[test]
fn test_pod_from_api() {
let (pods, pods_list): (Vec<KubePod>, Vec<_>) = convert_resource_from_file("pods");
assert_eq!(pods.len(), 13);
assert_eq!(
pods[0],
KubePod {
namespace: "default".into(),
name: "adservice-f787c8dcd-tb6x2".into(),
ready: (0, 1),
status: "Pending".into(),
restarts: 0,
cpu: "".into(),
mem: "".into(),
node: "".into(),
ip: "".into(),
age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
containers: vec![KubeContainer {
name: "server".into(),
image: "gcr.io/google-samples/microservices-demo/adservice:v0.2.2".into(),
ready: "false".into(),
status: "<none>".into(),
restarts: 0,
liveliness_probe: true,
readiness_probe: true,
ports: "9555".into(),
age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
pod_name: "adservice-f787c8dcd-tb6x2".into(),
init: false,
k8s_obj: None,
}],
k8s_obj: pods_list[0].clone()
}
);
assert_eq!(
pods[1],
KubePod {
namespace: "default".into(),
name: "cartservice-67b89ffc69-s5qp8".into(),
ready: (0, 1),
status: "CrashLoopBackOff".into(),
restarts: 896,
cpu: "".into(),
mem: "".into(),
node: "gke-hello-hipster-default-pool-9e6f6ffb-q16l".into(),
ip: "10.24.1.9".into(),
age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
containers: vec![KubeContainer {
name: "server".into(),
image: "gcr.io/google-samples/microservices-demo/cartservice:v0.2.2".into(),
ready: "false".into(),
status: "CrashLoopBackOff".into(),
restarts: 896,
liveliness_probe: true,
readiness_probe: true,
ports: "7070".into(),
age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
pod_name: "cartservice-67b89ffc69-s5qp8".into(),
init: false,
k8s_obj: None,
}],
k8s_obj: pods_list[1].clone()
}
);
assert_eq!(
pods[3],
KubePod {
namespace: "default".into(),
name: "emailservice-5f8fc7dbb4-5lqdb".into(),
ready: (1, 1),
status: "Running".into(),
restarts: 3,
cpu: "".into(),
mem: "".into(),
node: "gke-hello-hipster-default-pool-9e6f6ffb-xzbc".into(),
ip: "10.24.0.3".into(),
age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
containers: vec![KubeContainer {
name: "server".into(),
image: "gcr.io/google-samples/microservices-demo/emailservice:v0.2.2".into(),
ready: "true".into(),
status: "Running".into(),
restarts: 3,
liveliness_probe: true,
readiness_probe: true,
ports: "8080".into(),
age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
pod_name: "emailservice-5f8fc7dbb4-5lqdb".into(),
init: false,
k8s_obj: None,
}],
k8s_obj: pods_list[3].clone()
}
);
let out_of_cpu_pod = &pods[4];
assert_eq!(out_of_cpu_pod.namespace, "default");
assert_eq!(out_of_cpu_pod.name, "frontend-5c4745dfdb-6k8wf");
assert_eq!(out_of_cpu_pod.ready, (0, 0));
assert_eq!(out_of_cpu_pod.status, "OutOfcpu");
assert_eq!(out_of_cpu_pod.restarts, 0);
assert_eq!(out_of_cpu_pod.cpu, "");
assert_eq!(out_of_cpu_pod.mem, "");
assert_eq!(
out_of_cpu_pod.age,
utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now())
);
assert_eq!(
out_of_cpu_pod.containers,
vec![KubeContainer {
name: "server".into(),
image: "gcr.io/google-samples/microservices-demo/frontend:v0.2.2".into(),
ready: "false".into(),
status: "<none>".into(),
restarts: 0,
liveliness_probe: true,
readiness_probe: true,
ports: "8080".into(),
age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
pod_name: "frontend-5c4745dfdb-6k8wf".into(),
k8s_obj: None,
init: false,
}]
);
assert_eq!(
out_of_cpu_pod.k8s_obj.metadata.name.as_deref(),
Some("frontend-5c4745dfdb-6k8wf")
);
assert_eq!(
pods[5],
KubePod {
namespace: "default".into(),
name: "frontend-5c4745dfdb-qz7fg".into(),
ready: (0, 0),
status: "Preempting".into(),
restarts: 0,
cpu: "".into(),
mem: "".into(),
node: "gke-hello-hipster-default-pool-9e6f6ffb-q16l".into(),
ip: "".into(),
age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
containers: vec![KubeContainer {
name: "server".into(),
image: "gcr.io/google-samples/microservices-demo/frontend:v0.2.2".into(),
ready: "false".into(),
status: "<none>".into(),
restarts: 0,
liveliness_probe: false,
readiness_probe: true,
ports: "8080/HTTP".into(),
age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
pod_name: "frontend-5c4745dfdb-qz7fg".into(),
k8s_obj: None,
init: false,
}],
k8s_obj: pods_list[5].clone()
}
);
assert_eq!(
pods[6],
KubePod {
namespace: "default".into(),
name: "frontend-5c4745dfdb-6k8wf".into(),
ready: (0, 0),
status: "Failed".into(),
restarts: 0,
cpu: "".into(),
mem: "".into(),
node: "gke-hello-hipster-default-pool-9e6f6ffb-q16l".into(),
ip: "".into(),
age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
containers: vec![KubeContainer {
name: "server".into(),
image: "gcr.io/google-samples/microservices-demo/frontend:v0.2.2".into(),
ready: "false".into(),
status: "<none>".into(),
restarts: 0,
liveliness_probe: true,
readiness_probe: true,
ports: "8080, 8081/UDP, Foo:8082/UDP, 8083".into(),
age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
pod_name: "frontend-5c4745dfdb-6k8wf".into(),
k8s_obj: None,
init: false,
}],
k8s_obj: pods_list[6].clone()
}
);
assert_eq!(
pods[11],
KubePod {
namespace: "default".into(),
name: "pod-init-container".into(),
ready: (0, 1),
status: "Init:1/2".into(),
restarts: 0,
cpu: "".into(),
mem: "".into(),
node: "k3d-my-kdash-cluster-server-0".into(),
ip: "10.42.0.20".into(),
age: utils::to_age(Some(&get_time("2021-06-18T08:57:56Z")), Utc::now()),
containers: vec![
KubeContainer {
name: "main-busybox".into(),
image: "busybox".into(),
ready: "false".into(),
status: "PodInitializing".into(),
restarts: 0,
liveliness_probe: false,
readiness_probe: false,
ports: "".into(),
age: utils::to_age(Some(&get_time("2021-06-18T08:57:56Z")), Utc::now()),
pod_name: "pod-init-container".into(),
k8s_obj: None,
init: false,
},
KubeContainer {
name: "init-busybox1".into(),
image: "busybox".into(),
ready: "true".into(),
status: "Completed".into(),
restarts: 0,
liveliness_probe: false,
readiness_probe: false,
ports: "".into(),
age: utils::to_age(Some(&get_time("2021-06-18T08:57:56Z")), Utc::now()),
pod_name: "pod-init-container".into(),
k8s_obj: None,
init: true,
},
KubeContainer {
name: "init-busybox2".into(),
image: "busybox".into(),
ready: "false".into(),
status: "Running".into(),
restarts: 0,
liveliness_probe: false,
readiness_probe: false,
ports: "".into(),
age: utils::to_age(Some(&get_time("2021-06-18T08:57:56Z")), Utc::now()),
pod_name: "pod-init-container".into(),
k8s_obj: None,
init: true,
}
],
k8s_obj: pods_list[11].clone()
}
);
assert_eq!(
pods[12],
KubePod {
namespace: "default".into(),
name: "pod-init-container-2".into(),
ready: (0, 1),
status: "Completed".into(),
restarts: 0,
cpu: "".into(),
mem: "".into(),
node: "k3d-my-kdash-cluster-server-0".into(),
ip: "10.42.0.21".into(),
age: utils::to_age(Some(&get_time("2021-06-18T09:26:11Z")), Utc::now()),
containers: vec![
KubeContainer {
name: "main-busybox".into(),
image: "busybox".into(),
ready: "false".into(),
status: "Completed".into(),
restarts: 0,
liveliness_probe: false,
readiness_probe: false,
ports: "".into(),
age: utils::to_age(Some(&get_time("2021-06-18T09:26:11Z")), Utc::now()),
pod_name: "pod-init-container-2".into(),
k8s_obj: None,
init: false,
},
KubeContainer {
name: "init-busybox1".into(),
image: "busybox".into(),
ready: "true".into(),
status: "Completed".into(),
restarts: 0,
liveliness_probe: false,
readiness_probe: false,
ports: "".into(),
age: utils::to_age(Some(&get_time("2021-06-18T09:26:11Z")), Utc::now()),
pod_name: "pod-init-container-2".into(),
k8s_obj: None,
init: true,
},
KubeContainer {
name: "init-busybox2".into(),
image: "busybox".into(),
ready: "true".into(),
status: "Completed".into(),
restarts: 0,
liveliness_probe: false,
readiness_probe: false,
ports: "".into(),
age: utils::to_age(Some(&get_time("2021-06-18T09:26:11Z")), Utc::now()),
pod_name: "pod-init-container-2".into(),
k8s_obj: None,
init: true,
}
],
k8s_obj: pods_list[12].clone()
}
);
}
#[test]
fn test_pending_pod_ready_count_uses_declared_containers() {
let pod = Pod {
metadata: ObjectMeta {
name: Some("pending-pod".into()),
namespace: Some("default".into()),
..Default::default()
},
spec: Some(PodSpec {
containers: vec![Container {
name: "server".into(),
image: Some("busybox".into()),
..Default::default()
}],
..Default::default()
}),
status: Some(PodStatus {
phase: Some("Pending".into()),
container_statuses: None,
..Default::default()
}),
};
let pod = KubePod::from(pod);
assert_eq!(pod.ready, (0, 1));
assert_eq!(pod.status, "Pending");
}
}