use std::cmp::{
max,
min,
Ord,
Ordering,
};
use tracing::*;
use super::*;
use crate::prelude::*;
impl PodLifecycleData {
fn new(start_ts: Option<i64>, end_ts: Option<i64>) -> PodLifecycleData {
match (start_ts, end_ts) {
(None, _) => PodLifecycleData::Empty,
(Some(ts), None) => PodLifecycleData::Running(ts),
(Some(start), Some(end)) => PodLifecycleData::Finished(start, end),
}
}
pub fn new_for(pod: &corev1::Pod) -> anyhow::Result<PodLifecycleData> {
let (mut earliest_start_ts, mut latest_end_ts) = (None, None);
let mut terminated_container_count = 0;
let pod_status = pod.status()?;
if let Some(cstats) = pod_status.init_container_statuses.as_ref() {
for (container, state) in cstats.iter().filter_map(|s| Some((&s.name, s.state.as_ref()?))) {
let (start_ts, end_ts) = get_start_end_ts(pod, container, state);
earliest_start_ts = min_some(start_ts, earliest_start_ts);
latest_end_ts = max(latest_end_ts, end_ts);
}
}
if let Some(cstats) = pod_status.container_statuses.as_ref() {
for (container, state) in cstats.iter().filter_map(|s| Some((&s.name, s.state.as_ref()?))) {
let (start_ts, end_ts) = get_start_end_ts(pod, container, state);
earliest_start_ts = min_some(start_ts, earliest_start_ts);
if end_ts.is_some() {
terminated_container_count += 1;
}
latest_end_ts = max(latest_end_ts, end_ts);
}
}
if terminated_container_count != pod.spec()?.containers.len() {
latest_end_ts = None;
}
Ok(PodLifecycleData::new(earliest_start_ts, latest_end_ts))
}
pub fn end_ts(&self) -> Option<i64> {
match self {
&PodLifecycleData::Finished(_, ts) => Some(ts),
_ => None,
}
}
pub fn start_ts(&self) -> Option<i64> {
match *self {
PodLifecycleData::Running(ts) => Some(ts),
PodLifecycleData::Finished(ts, _) => Some(ts),
_ => None,
}
}
pub fn overlaps(&self, start_ts: i64, end_ts: i64) -> bool {
match *self {
PodLifecycleData::Running(ts) => ts < end_ts,
PodLifecycleData::Finished(s, e) => (start_ts <= s && s < end_ts) || (start_ts <= e && e < end_ts),
_ => false,
}
}
pub fn guess_finished_lifecycle(
pod: &corev1::Pod,
current_lifecycle_data: &PodLifecycleData,
now: i64,
) -> anyhow::Result<PodLifecycleData> {
let new_lifecycle_data = PodLifecycleData::new_for(pod).unwrap_or(PodLifecycleData::Empty);
match new_lifecycle_data {
PodLifecycleData::Finished(..) => Ok(new_lifecycle_data),
PodLifecycleData::Running(start_ts) => Ok(PodLifecycleData::Finished(start_ts, now)),
PodLifecycleData::Empty => {
let start_ts = if let Some(ts) = current_lifecycle_data.start_ts() {
ts
} else if let Some(t) = pod.creation_timestamp() {
t.0.timestamp()
} else {
bail!("could not determine final pod lifecycle for {}", pod.namespaced_name());
};
Ok(PodLifecycleData::Finished(start_ts, now))
},
}
}
pub fn empty(&self) -> bool {
self == PodLifecycleData::Empty
}
pub fn running(&self) -> bool {
matches!(self, PodLifecycleData::Running(_))
}
pub fn finished(&self) -> bool {
matches!(self, PodLifecycleData::Finished(..))
}
}
impl PartialOrd for PodLifecycleData {
fn partial_cmp(&self, other: &PodLifecycleData) -> Option<Ordering> {
match self {
PodLifecycleData::Empty => {
if !other.empty() {
Some(Ordering::Less)
} else {
Some(Ordering::Equal)
}
},
PodLifecycleData::Running(ts) => match other {
PodLifecycleData::Empty => Some(Ordering::Greater),
PodLifecycleData::Running(other_ts) => {
if ts == other_ts {
Some(Ordering::Equal)
} else {
None
}
},
PodLifecycleData::Finished(..) => Some(Ordering::Less),
},
PodLifecycleData::Finished(sts, ets) => match other {
PodLifecycleData::Empty => Some(Ordering::Greater),
PodLifecycleData::Running(other_ts) => {
if sts == other_ts {
Some(Ordering::Greater)
} else {
None
}
},
PodLifecycleData::Finished(other_sts, other_ets) => {
if sts == other_sts && ets == other_ets {
Some(Ordering::Equal)
} else {
None
}
},
},
}
}
}
fn get_start_end_ts(pod: &corev1::Pod, container: &str, state: &corev1::ContainerState) -> (Option<i64>, Option<i64>) {
let start_ts = state.start_ts().unwrap_or_else(|err| {
warn!("could not find start_ts for container {container} in {}: {err:?}", pod.namespaced_name());
None
});
let end_ts = state.end_ts().unwrap_or_else(|err| {
warn!("could not find end_ts for container {container} in {}: {err:?}", pod.namespaced_name());
None
});
(start_ts, end_ts)
}
impl PartialEq<Option<&PodLifecycleData>> for PodLifecycleData {
fn eq(&self, other: &Option<&PodLifecycleData>) -> bool {
match self {
PodLifecycleData::Empty => other.is_none() || other.as_ref().is_some_and(|plt| plt.empty()),
_ => other.as_ref().is_some_and(|plt| plt == self),
}
}
}
impl PartialOrd<Option<&PodLifecycleData>> for PodLifecycleData {
fn partial_cmp(&self, other: &Option<&PodLifecycleData>) -> Option<Ordering> {
match self {
PodLifecycleData::Empty => other.as_ref().map_or(Some(Ordering::Equal), |o| self.partial_cmp(o)),
_ => other.as_ref().map_or(Some(Ordering::Greater), |o| self.partial_cmp(o)),
}
}
}
pub fn min_some<T: Ord>(o1: Option<T>, o2: Option<T>) -> Option<T> {
if o1.is_none() {
o2
} else if o2.is_none() {
o1
} else {
min(o1, o2)
}
}
#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod test {
use sk_testutils::*;
use super::*;
#[rstest]
#[case::both_none(None, None, None)]
#[case::left_some(Some(1), None, Some(1))]
#[case::right_some(None, Some(1), Some(1))]
#[case::both_some(Some(2), Some(1), Some(1))]
fn test_min_some(#[case] o1: Option<i32>, #[case] o2: Option<i32>, #[case] expected: Option<i32>) {
assert_eq!(min_some(o1, o2), expected);
}
}