use crate::api::list_objects;
use crate::events::with_events;
use crate::json::{integer, items, text};
use crate::metadata::{annotation_section, identity, label_section};
use crate::time::value_timestamp;
use crate::{pod, policy, quantity};
use k8s_openapi::api::core::v1::Event;
use k8s_openapi::jiff::Timestamp;
use k8s_openapi::jiff::tz::TimeZone;
use kube::Client;
use kube::api::{ApiResource, DynamicObject, ListParams};
use serde_json::Value;
use std::fmt::Write;
fn scalar(v: &Value) -> String {
v.as_str().map(str::to_owned).unwrap_or_else(|| {
if v.is_null() {
"<nil>".into()
} else {
v.to_string()
}
})
}
fn selector(object: &DynamicObject, kind: &str) -> String {
if kind == "ReplicationController" {
labels(&object.data["spec"]["selector"])
} else {
policy::selector(&object.data["spec"]["selector"])
}
}
fn labels(value: &Value) -> String {
let mut pairs: Vec<_> = value
.as_object()
.into_iter()
.flatten()
.map(|(k, v)| format!("{k}={}", text(v)))
.collect();
pairs.sort();
if pairs.is_empty() {
"<none>".into()
} else {
pairs.join(",")
}
}
pub(crate) async fn related(
client: Client,
object: &DynamicObject,
kind: &str,
) -> Result<Vec<DynamicObject>, String> {
let query = selector(object, kind);
if query == "<error>" {
return Err("invalid controller label selector".into());
}
let (group, resource_kind) = if kind == "Deployment" {
("apps", "ReplicaSet")
} else {
("", "Pod")
};
let ar = ApiResource::from_gvk(&kube::core::GroupVersionKind::gvk(
group,
"v1",
resource_kind,
));
let params = ListParams::default().labels(if query == "<none>" { "" } else { &query });
list_objects(
client,
&ar,
object.metadata.namespace.as_deref().unwrap_or_default(),
params,
)
.await
.map_err(|e| e.to_string())
}
pub(crate) fn render(
object: &DynamicObject,
kind: &str,
related: Result<&[DynamicObject], &str>,
events: Option<&[Event]>,
now: Timestamp,
zone: &TimeZone,
) -> Result<String, String> {
if !matches!(kind, "Deployment" | "ReplicaSet")
&& let Err(error) = related
{
return Err(error.into());
}
let spec = &object.data["spec"];
let status = &object.data["status"];
let selected = selector(object, kind);
if selected == "<error>" {
return Err("invalid controller label selector".into());
}
let raw_selector = if selected == "<none>" { "" } else { &selected };
let mut out = identity(&object.metadata, true);
let created = object
.metadata
.creation_timestamp
.as_ref()
.map(|t| Value::String(t.0.to_string()))
.unwrap_or(Value::Null);
let mut prefix = String::new();
if matches!(kind, "Deployment" | "StatefulSet") {
writeln!(
prefix,
"CreationTimestamp:\t{}",
value_timestamp(&created, zone)
)
.unwrap();
}
if kind != "Deployment" {
writeln!(
prefix,
"Selector:\t{}",
if matches!(kind, "ReplicationController" | "ReplicaSet") {
&selected
} else {
raw_selector
}
)
.unwrap();
}
if kind == "DaemonSet" {
writeln!(
prefix,
"Node-Selector:\t{}",
labels(&spec["template"]["spec"]["nodeSelector"])
)
.unwrap();
}
out.push_str(&prefix);
out.push_str(&label_section(&object.metadata, ""));
out.push_str(&annotation_section(&object.metadata, ""));
if kind == "ReplicaSet"
&& let Some(owner) = object
.metadata
.owner_references
.iter()
.flatten()
.find(|o| o.controller == Some(true))
{
writeln!(out, "Controlled By:\t{}/{}", owner.kind, owner.name).unwrap();
}
match kind {
"ReplicationController" | "ReplicaSet" => writeln!(
out,
"Replicas:\t{} current / {} desired",
integer(&status["replicas"]),
integer(&spec["replicas"])
)
.unwrap(),
"DaemonSet" => {
for (field, label) in [
(
"desiredNumberScheduled",
"Desired Number of Nodes Scheduled",
),
(
"currentNumberScheduled",
"Current Number of Nodes Scheduled",
),
(
"updatedNumberScheduled",
"Number of Nodes Scheduled with Up-to-date Pods",
),
(
"numberAvailable",
"Number of Nodes Scheduled with Available Pods",
),
("numberMisscheduled", "Number of Nodes Misscheduled"),
] {
writeln!(out, "{label}: {}", integer(&status[field])).unwrap();
}
}
"StatefulSet" => {
for (field, label) in [
("serviceName", "Service Name"),
("podManagementPolicy", "Pod Management Policy"),
] {
if !text(&spec[field]).is_empty() {
writeln!(out, "{label}:\t{}", text(&spec[field])).unwrap();
}
}
writeln!(
out,
"Replicas:\t{} desired | {} total\nUpdate Strategy:\t{}",
integer(&spec["replicas"]),
integer(&status["replicas"]),
text(&spec["updateStrategy"]["type"])
)
.unwrap();
let rolling = &spec["updateStrategy"]["rollingUpdate"];
if !rolling["partition"].is_null() {
writeln!(out, " Partition:\t{}", integer(&rolling["partition"])).unwrap();
if !rolling["maxUnavailable"].is_null() {
writeln!(
out,
" MaxUnavailable:\t{}",
scalar(&rolling["maxUnavailable"])
)
.unwrap();
}
}
let retention = &spec["persistentVolumeClaimRetentionPolicy"];
if !retention.is_null() {
writeln!(out, "Persistent Volume Claim Retention Policy:\n WhenDeleted:\t{}\n WhenScaled:\t{}", text(&retention["whenDeleted"]), text(&retention["whenScaled"])).unwrap();
}
}
"Deployment" => {
writeln!(out,"Selector:\t{raw_selector}\nReplicas:\t{} desired | {} updated | {} total | {} available | {} unavailable\nStrategyType:\t{}\nMinReadySeconds:\t{}",integer(&spec["replicas"]),integer(&status["updatedReplicas"]),integer(&status["replicas"]),integer(&status["availableReplicas"]),integer(&status["unavailableReplicas"]),text(&spec["strategy"]["type"]),integer(&spec["minReadySeconds"])).unwrap();
let rolling = &spec["strategy"]["rollingUpdate"];
if !rolling.is_null() {
writeln!(
out,
"RollingUpdateStrategy:\t{} max unavailable, {} max surge",
scalar(&rolling["maxUnavailable"]),
scalar(&rolling["maxSurge"])
)
.unwrap();
}
}
_ => unreachable!(),
}
if kind != "Deployment" {
out.push_str("Pods Status:\t");
match related {
Err(error) => writeln!(out, "error in fetching pods: {error}").unwrap(),
Ok(pods) => {
let mut counts = [0; 4];
for pod in pods.iter().filter(|p| owned_by(p, object)) {
if let Some(index) = ["Running", "Pending", "Succeeded", "Failed"]
.iter()
.position(|phase| *phase == text(&pod.data["status"]["phase"]))
{
counts[index] += 1;
}
}
writeln!(
out,
"{} Running / {} Waiting / {} Succeeded / {} Failed",
counts[0], counts[1], counts[2], counts[3]
)
.unwrap();
}
}
}
pod::template(&mut out, &spec["template"], zone);
if matches!(kind, "Deployment" | "ReplicaSet" | "ReplicationController")
&& !items(&status["conditions"]).is_empty()
{
out.push_str("Conditions:\n Type\tStatus\tReason\n ----\t------\t------\n");
for c in items(&status["conditions"]) {
writeln!(
out,
" {} \t{}\t{}",
text(&c["type"]),
text(&c["status"]),
text(&c["reason"])
)
.unwrap();
}
}
if kind == "StatefulSet" {
claims(&mut out, items(&spec["volumeClaimTemplates"]));
}
if kind == "Deployment"
&& let Ok(sets) = related
{
let mut owned: Vec<_> = sets.iter().filter(|rs| owned_by(rs, object)).collect();
owned.sort_by(|a, b| {
a.metadata
.creation_timestamp
.cmp(&b.metadata.creation_timestamp)
.then_with(|| a.metadata.name.cmp(&b.metadata.name))
});
let template = without_hash(&spec["template"]);
let new = owned
.iter()
.find(|rs| without_hash(&rs.data["spec"]["template"]) == template)
.copied();
let old: Vec<_> = owned
.iter()
.copied()
.filter(|rs| new.is_none_or(|new| new.metadata.uid != rs.metadata.uid))
.collect();
if !old.is_empty() || new.is_some() {
writeln!(
out,
"OldReplicaSets:\t{}\nNewReplicaSet:\t{}",
replica_sets(&old),
replica_sets(&new.into_iter().collect::<Vec<_>>())
)
.unwrap();
}
}
Ok(with_events(out, events, now))
}
fn owned_by(child: &DynamicObject, parent: &DynamicObject) -> bool {
child
.metadata
.owner_references
.iter()
.flatten()
.find(|o| o.controller == Some(true))
.is_some_and(|o| Some(&o.uid) == parent.metadata.uid.as_ref())
}
fn without_hash(template: &Value) -> Value {
let mut template = template.clone();
if let Some(labels) = template
.pointer_mut("/metadata/labels")
.and_then(Value::as_object_mut)
{
labels.remove("pod-template-hash");
}
template
}
fn replica_sets(sets: &[&DynamicObject]) -> String {
if sets.is_empty() {
return "<none>".into();
}
sets.iter()
.map(|rs| {
format!(
"{} ({}/{} replicas created)",
rs.metadata.name.as_deref().unwrap_or_default(),
integer(&rs.data["status"]["replicas"]),
integer(&rs.data["spec"]["replicas"])
)
})
.collect::<Vec<_>>()
.join(", ")
}
fn claims(out: &mut String, claims: &[Value]) {
if claims.is_empty() {
out.push_str("Volume Claims:\t<none>\n");
return;
}
out.push_str("Volume Claims:\n");
for claim in claims {
let spec = &claim["spec"];
let meta = &claim["metadata"];
let class = meta["annotations"]["volume.beta.kubernetes.io/storage-class"]
.as_str()
.unwrap_or_else(|| text(&spec["storageClassName"]));
writeln!(
out,
" Name:\t{}\n StorageClass:\t{class}",
text(&meta["name"])
)
.unwrap();
for (field, label) in [("labels", "Labels"), ("annotations", "Annotations")] {
write!(out, " {label}:\t").unwrap();
let mut pairs: Vec<_> = meta[field].as_object().into_iter().flatten().collect();
pairs.sort_by_key(|(k, _)| *k);
if pairs.is_empty() {
out.push_str("<none>\n");
}
for (i, (k, v)) in pairs.iter().enumerate() {
if i > 0 {
out.push_str(" \t");
}
writeln!(out, "{k}={}", text(v)).unwrap();
}
}
writeln!(
out,
" Capacity:\t{}\n Access Modes:\t[{}]",
spec["resources"]["requests"]["storage"]
.as_str()
.map(quantity::canonical)
.unwrap_or("<default>".into()),
items(&spec["accessModes"])
.iter()
.map(text)
.collect::<Vec<_>>()
.join(" ")
)
.unwrap();
}
}