#![deny(clippy::wildcard_enum_match_arm)]
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::convert::Infallible;
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use engenho_config::ControllerEnable;
pub use engenho_controllers::TickState;
use engenho_controllers::{ControllerType, Heartbeat, KindFilter, PanicMessage, Reads};
use tokio::task::{AbortHandle, Id, JoinSet};
use tracing::{error, info};
use crate::boot::Timestamp;
use crate::boot_config::BootConfig;
use crate::health::{Row, Tally};
engenho_controllers::closed_enum! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Driver {
Deployment,
ReplicaSet,
StatefulSet,
DaemonSet,
Job,
CronJob,
PodDisruptionBudget,
Endpoints,
ServiceRouting,
Gc,
Namespace,
PvBinder,
VolumeSnapshot,
PvcProtection,
Crd,
Scheduler,
ServedCapability,
NetworkPolicy,
CsiRegistrar,
CniStatus,
NodeManifests,
Kubelet,
}
}
engenho_controllers::closed_enum! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Listener {
KubeletHttp,
EtcdFacade,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Child {
Driver(Driver),
Listener(Listener),
NodeLease,
}
impl Driver {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Deployment => "deployment",
Self::ReplicaSet => "replicaset",
Self::StatefulSet => "statefulset",
Self::DaemonSet => "daemonset",
Self::Job => "job",
Self::CronJob => "cronjob",
Self::PodDisruptionBudget => "pdb",
Self::Endpoints => "endpoints",
Self::ServiceRouting => "service-routing",
Self::Gc => "gc",
Self::Namespace => "namespace",
Self::PvBinder => "pv-binder",
Self::VolumeSnapshot => "volume-snapshot",
Self::PvcProtection => "pvc-protection",
Self::Crd => "crd",
Self::Scheduler => "scheduler",
Self::ServedCapability => "served-capability",
Self::NetworkPolicy => "network-policy",
Self::CsiRegistrar => "csi-registrar",
Self::CniStatus => "cni-status",
Self::NodeManifests => "node-manifests",
Self::Kubelet => "kubelet",
}
}
#[must_use]
pub const fn tick_state(self) -> TickState {
match self {
Self::Deployment
| Self::ReplicaSet
| Self::StatefulSet
| Self::DaemonSet
| Self::Job
| Self::CronJob
| Self::PodDisruptionBudget
| Self::Endpoints
| Self::Gc
| Self::Namespace
| Self::PvBinder
| Self::VolumeSnapshot
| Self::PvcProtection
| Self::ServedCapability
| Self::CniStatus
| Self::NodeManifests => TickState::Stateless,
Self::Kubelet
| Self::Crd
| Self::ServiceRouting
| Self::NetworkPolicy
| Self::CsiRegistrar
| Self::Scheduler => TickState::Stateful,
}
}
#[must_use]
pub const fn switch(self) -> Option<EnableSwitch> {
use EnableSwitch as S;
match self {
Self::Deployment => Some(S::Deployment),
Self::ReplicaSet => Some(S::Replicaset),
Self::StatefulSet => Some(S::Statefulset),
Self::DaemonSet => Some(S::Daemonset),
Self::Job => Some(S::Job),
Self::CronJob => Some(S::Cronjob),
Self::PodDisruptionBudget => Some(S::Pdb),
Self::Endpoints => Some(S::Endpoints),
Self::ServiceRouting => Some(S::ServiceRouting),
Self::Gc => Some(S::Gc),
Self::Namespace => Some(S::Namespace),
Self::PvBinder | Self::VolumeSnapshot | Self::PvcProtection => Some(S::PvBinder),
Self::Crd => Some(S::Crd),
Self::Scheduler
| Self::ServedCapability
| Self::NetworkPolicy
| Self::CsiRegistrar
| Self::CniStatus
| Self::NodeManifests
| Self::Kubelet => None,
}
}
#[must_use]
pub const fn enabled(self, enable: &ControllerEnable) -> bool {
match self.switch() {
Some(switch) => switch.read(enable),
None => true,
}
}
}
engenho_controllers::closed_enum! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum EnableSwitch {
Replicaset,
Deployment,
Statefulset,
Daemonset,
Job,
Cronjob,
Endpoints,
ServiceRouting,
Gc,
Crd,
Namespace,
PvBinder,
Pdb,
}
}
impl EnableSwitch {
#[must_use]
pub fn of_leaf(path: &str) -> Option<Self> {
Self::ALL
.iter()
.copied()
.find(|switch| switch.leaf() == path)
}
pub fn drivers(self) -> impl Iterator<Item = Driver> {
Driver::ALL
.iter()
.copied()
.filter(move |driver| driver.switch() == Some(self))
}
#[must_use]
pub fn toggles_live(self) -> bool {
self.drivers()
.all(|driver| Child::Driver(driver).respawn() != Respawn::RuntimeRestartOnly)
}
#[must_use]
pub const fn read(self, enable: &ControllerEnable) -> bool {
let ControllerEnable {
replicaset,
deployment,
statefulset,
daemonset,
job,
cronjob,
endpoints,
service_routing,
gc,
crd,
namespace,
pv_binder,
pdb,
} = enable;
*match self {
Self::Replicaset => replicaset,
Self::Deployment => deployment,
Self::Statefulset => statefulset,
Self::Daemonset => daemonset,
Self::Job => job,
Self::Cronjob => cronjob,
Self::Endpoints => endpoints,
Self::ServiceRouting => service_routing,
Self::Gc => gc,
Self::Crd => crd,
Self::Namespace => namespace,
Self::PvBinder => pv_binder,
Self::Pdb => pdb,
}
}
#[must_use]
pub const fn leaf(self) -> &'static str {
match self {
Self::Replicaset => "controllers.enable.replicaset",
Self::Deployment => "controllers.enable.deployment",
Self::Statefulset => "controllers.enable.statefulset",
Self::Daemonset => "controllers.enable.daemonset",
Self::Job => "controllers.enable.job",
Self::Cronjob => "controllers.enable.cronjob",
Self::Endpoints => "controllers.enable.endpoints",
Self::ServiceRouting => "controllers.enable.service_routing",
Self::Gc => "controllers.enable.gc",
Self::Crd => "controllers.enable.crd",
Self::Namespace => "controllers.enable.namespace",
Self::PvBinder => "controllers.enable.pv_binder",
Self::Pdb => "controllers.enable.pdb",
}
}
}
impl Listener {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::KubeletHttp => "kubelet-http",
Self::EtcdFacade => "etcd-facade",
}
}
#[must_use]
pub const fn supervision(self, fault: Fault) -> Supervision {
match self {
Self::KubeletHttp | Self::EtcdFacade => match fault {
Fault::Panic | Fault::Error | Fault::BindFailure => Supervision::Rebinds,
Fault::Hang => Supervision::Unobserved,
},
}
}
#[must_use]
pub(crate) fn enabled(self, boot: &BootConfig) -> bool {
match self {
Self::KubeletHttp => true,
Self::EtcdFacade => !boot.etcd_listen_addr.is_empty(),
}
}
#[must_use]
pub(crate) fn listen_addr(self, boot: &BootConfig) -> &str {
match self {
Self::KubeletHttp => &boot.kubelet_listen_addr,
Self::EtcdFacade => &boot.etcd_listen_addr,
}
}
}
impl Child {
pub const NODE_LEASE_TICK_STATE: TickState = TickState::Stateless;
pub fn all() -> impl Iterator<Item = Self> {
const fn _every_shape_is_walked(child: Child) {
match child {
Child::Driver(_) | Child::Listener(_) | Child::NodeLease => {}
}
}
Driver::ALL
.iter()
.copied()
.map(Self::Driver)
.chain(Listener::ALL.iter().copied().map(Self::Listener))
.chain(std::iter::once(Self::NodeLease))
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Driver(d) => d.name(),
Self::Listener(l) => l.name(),
Self::NodeLease => "node-lease",
}
}
#[must_use]
pub const fn tick_state(self) -> Option<TickState> {
match self.tick_loop() {
Some(tick_loop) => Some(tick_loop.tick_state()),
None => None,
}
}
#[must_use]
pub(crate) const fn tick_loop(self) -> Option<TickLoop> {
match self {
Self::Driver(d) => Some(TickLoop::Driver(d)),
Self::NodeLease => Some(TickLoop::NodeLease),
Self::Listener(_) => None,
}
}
#[must_use]
pub const fn supervision(self, fault: Fault) -> Supervision {
match self {
Self::Driver(d) => TickLoop::Driver(d).supervision(fault),
Self::NodeLease => TickLoop::NodeLease.supervision(fault),
Self::Listener(l) => l.supervision(fault),
}
}
#[must_use]
pub(crate) fn enabled(self, boot: &BootConfig) -> bool {
match self {
Self::Driver(d) => d.enabled(&boot.enable),
Self::Listener(l) => l.enabled(boot),
Self::NodeLease => Driver::Kubelet.enabled(&boot.enable),
}
}
#[must_use]
pub(crate) fn rebuilt_between(self, before: &BootConfig, after: &BootConfig) -> bool {
match self {
Self::Listener(l) => l.listen_addr(before) != l.listen_addr(after),
Self::Driver(_) | Self::NodeLease => false,
}
}
#[must_use]
pub const fn respawn(self) -> Respawn {
match self {
Self::Driver(d) => match d {
Driver::Deployment
| Driver::ReplicaSet
| Driver::StatefulSet
| Driver::DaemonSet
| Driver::Job
| Driver::CronJob
| Driver::PodDisruptionBudget
| Driver::Endpoints
| Driver::Gc
| Driver::Namespace
| Driver::PvBinder
| Driver::VolumeSnapshot
| Driver::PvcProtection
| Driver::ServedCapability
| Driver::CniStatus
| Driver::NodeManifests
| Driver::Scheduler
| Driver::NetworkPolicy => Respawn::Rebuild,
Driver::Kubelet => {
Respawn::RebuildWith(&[Self::NodeLease, Self::Listener(Listener::KubeletHttp)])
}
Driver::Crd | Driver::ServiceRouting | Driver::CsiRegistrar => {
Respawn::RuntimeRestartOnly
}
},
Self::Listener(Listener::KubeletHttp | Listener::EtcdFacade) | Self::NodeLease => {
Respawn::Rebuild
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Respawn {
Rebuild,
RebuildWith(&'static [Child]),
RuntimeRestartOnly,
}
impl fmt::Display for Child {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TickLoop {
Driver(Driver),
NodeLease,
}
impl TickLoop {
#[must_use]
pub(crate) const fn child(self) -> Child {
match self {
Self::Driver(d) => Child::Driver(d),
Self::NodeLease => Child::NodeLease,
}
}
#[must_use]
pub(crate) const fn tick_state(self) -> TickState {
match self {
Self::Driver(d) => d.tick_state(),
Self::NodeLease => Child::NODE_LEASE_TICK_STATE,
}
}
const fn supervision(self, fault: Fault) -> Supervision {
match fault {
Fault::Panic => match self.tick_state() {
TickState::Stateless => Supervision::Contained,
TickState::Stateful => Supervision::Dead,
},
Fault::Hang => Supervision::Stalled,
Fault::Error => Supervision::Retried,
Fault::BindFailure => Supervision::Inapplicable,
}
}
}
engenho_controllers::closed_enum! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Fault {
Panic,
Hang,
Error,
BindFailure,
}
}
engenho_controllers::closed_enum! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Supervision {
Contained,
Dead,
Retried,
Stalled,
Rebinds,
Unobserved,
Inapplicable,
}
}
#[derive(Debug, Clone)]
pub struct Wiring {
controller: ControllerType,
reads: Reads,
wakes: KindFilter,
}
impl Wiring {
pub(crate) fn new(controller: ControllerType, reads: Reads, wakes: KindFilter) -> Self {
Self {
controller,
reads,
wakes,
}
}
#[must_use]
pub fn controller(&self) -> ControllerType {
self.controller
}
#[must_use]
pub fn reads(&self) -> &Reads {
&self.reads
}
#[must_use]
pub fn wakes(&self) -> &KindFilter {
&self.wakes
}
}
pub(crate) struct ChildTask {
beat: Arc<Heartbeat>,
wiring: Option<Wiring>,
tally: Option<Arc<Tally>>,
run: Pin<Box<dyn Future<Output = Infallible> + Send + 'static>>,
}
impl ChildTask {
pub(crate) fn new(
beat: Arc<Heartbeat>,
run: impl Future<Output = Infallible> + Send + 'static,
) -> Self {
Self {
beat,
wiring: None,
tally: None,
run: Box::pin(run),
}
}
pub(crate) fn driver(
beat: Arc<Heartbeat>,
wiring: Wiring,
tally: Arc<Tally>,
run: impl Future<Output = Infallible> + Send + 'static,
) -> Self {
Self {
beat,
wiring: Some(wiring),
tally: Some(tally),
run: Box::pin(run),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DeathCause {
Panicked,
Cancelled,
}
impl fmt::Display for DeathCause {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Panicked => "panicked",
Self::Cancelled => "cancelled",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ChildState {
Running,
Dead(DeathCause),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Death {
pub at: Timestamp,
pub cause: DeathCause,
}
const fn stopped_on_purpose(stopping: bool, cause: DeathCause) -> bool {
stopping && matches!(cause, DeathCause::Cancelled)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("{0} is running")]
pub struct StillRunning(pub Child);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Respawned {
pub child: Child,
pub generation: u64,
pub rebuilt: Vec<Child>,
pub died: Vec<DeadChild>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum RespawnError {
#[error("{0} is rebuilt only by restarting the runtime")]
RuntimeRestartOnly(Child),
#[error("{0} is not spawned: the configuration disables it")]
NotSpawned(Child),
#[error("{child} could not be built again: {reason}")]
Build {
child: Child,
reason: String,
},
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ChildrenFollowed {
pub changed: Vec<Child>,
pub died: Vec<DeadChild>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[must_use = "a dead child is already marked Dead and logged; this is the notice"]
pub struct DeadChild {
pub child: Child,
pub cause: DeathCause,
}
#[doc(alias = "DriverHandle")]
#[derive(Debug, Clone)]
pub struct ChildHandle {
beat: Arc<Heartbeat>,
wiring: Option<Wiring>,
task: AbortHandle,
tally: Option<Arc<Tally>>,
state: ChildState,
spawned_at: Timestamp,
ended_at: Option<Timestamp>,
generation: u64,
last_death: Option<Death>,
}
impl ChildHandle {
#[must_use]
pub const fn generation(&self) -> u64 {
self.generation
}
#[must_use]
pub const fn last_death(&self) -> Option<Death> {
self.last_death
}
#[must_use]
pub const fn spawned_at(&self) -> Timestamp {
self.spawned_at
}
#[must_use]
pub const fn ended_at(&self) -> Option<Timestamp> {
self.ended_at
}
#[must_use]
pub fn beat(&self) -> &Arc<Heartbeat> {
&self.beat
}
#[must_use]
pub fn wiring(&self) -> Option<&Wiring> {
self.wiring.as_ref()
}
#[must_use]
pub const fn state(&self) -> ChildState {
self.state
}
fn row(&self, child: Child) -> Row {
Row::new(
child,
self.beat.clone(),
self.task.clone(),
self.tally.clone(),
)
}
}
#[derive(Debug, Default)]
pub struct Children {
set: JoinSet<Infallible>,
by_task: HashMap<Id, Child>,
entries: BTreeMap<Child, ChildHandle>,
stopping: BTreeSet<Child>,
}
impl Children {
pub(crate) fn spawn_catalog(
boot: &BootConfig,
mut build: impl FnMut(Child, &Self) -> Option<ChildTask>,
) -> Self {
let mut children = Self::default();
for child in Child::all().filter(|c| c.enabled(boot)) {
if let Some(task) = build(child, &children) {
children.spawn(child, task);
}
}
children
}
fn spawn(&mut self, child: Child, task: ChildTask) {
self.install(child, task, 0, None);
}
pub(crate) fn spawn_one(&mut self, child: Child, task: ChildTask) -> Result<u64, StillRunning> {
let (generation, last_death) = match self.entries.get(&child) {
Some(entry) if entry.state == ChildState::Running => return Err(StillRunning(child)),
Some(entry) => (entry.generation.saturating_add(1), entry.last_death),
None => (0, None),
};
self.install(child, task, generation, last_death);
Ok(generation)
}
fn install(
&mut self,
child: Child,
task: ChildTask,
generation: u64,
last_death: Option<Death>,
) {
let abort = self.set.spawn(task.run);
self.by_task.insert(abort.id(), child);
self.entries.insert(
child,
ChildHandle {
beat: task.beat,
wiring: task.wiring,
task: abort,
tally: task.tally,
state: ChildState::Running,
spawned_at: Timestamp::now(),
ended_at: None,
generation,
last_death,
},
);
}
pub(crate) async fn stop_one(&mut self, child: Child) -> Vec<DeadChild> {
let Some(entry) = self.entries.get(&child) else {
return Vec::new();
};
if entry.state != ChildState::Running {
return Vec::new();
}
self.stopping.insert(child);
entry.task.abort();
let mut ended = Vec::new();
loop {
let dead = self.next_dead().await;
ended.push(dead);
if dead.child == child {
return ended;
}
}
}
pub(crate) fn forget(&mut self, child: Child) -> Result<(), StillRunning> {
match self.entries.get(&child) {
Some(entry) if entry.state == ChildState::Running => Err(StillRunning(child)),
Some(_) | None => {
self.entries.remove(&child);
Ok(())
}
}
}
pub(crate) fn rows(&self) -> Vec<Row> {
self.iter().map(|(child, entry)| entry.row(child)).collect()
}
pub(crate) fn row(&self, child: Child) -> Option<Row> {
self.get(child).map(|entry| entry.row(child))
}
pub async fn next_dead(&mut self) -> DeadChild {
loop {
let Some(joined) = self.set.join_next_with_id().await else {
return std::future::pending().await;
};
let ended = match joined {
Ok((_, never)) => match never {},
Err(ended) => ended,
};
let task = ended.id();
let cause = if ended.is_panic() {
DeathCause::Panicked
} else {
DeathCause::Cancelled
};
let panic = ended
.try_into_panic()
.ok()
.map(|p| PanicMessage::of(p.as_ref()));
let Some(child) = self.by_task.remove(&task) else {
error!(%task, %cause, "a runtime task with no catalog entry ended");
continue;
};
let at = Timestamp::now();
if let Some(entry) = self.entries.get_mut(&child) {
entry.state = ChildState::Dead(cause);
entry.ended_at = Some(at);
entry.last_death = Some(Death { at, cause });
if cause == DeathCause::Panicked {
entry.beat.record_panic();
}
}
if stopped_on_purpose(self.stopping.remove(&child), cause) {
info!(%child, "runtime child stopped");
} else {
error!(
%child,
%cause,
panic = panic.as_ref().map(tracing::field::display),
"runtime child ended; it is Dead until an operator restarts it"
);
}
return DeadChild { child, cause };
}
}
pub(crate) async fn stop(&mut self) {
self.set.shutdown().await;
self.by_task.clear();
self.stopping.clear();
let at = Timestamp::now();
for entry in self.entries.values_mut() {
if entry.state == ChildState::Running {
entry.state = ChildState::Dead(DeathCause::Cancelled);
entry.ended_at = Some(at);
entry.last_death = Some(Death {
at,
cause: DeathCause::Cancelled,
});
}
}
}
#[must_use]
pub fn get(&self, child: Child) -> Option<&ChildHandle> {
self.entries.get(&child)
}
pub fn iter(&self) -> impl Iterator<Item = (Child, &ChildHandle)> {
self.entries.iter().map(|(child, entry)| (*child, entry))
}
pub fn running(&self) -> impl Iterator<Item = Child> + '_ {
self.iter()
.filter(|(_, entry)| entry.state == ChildState::Running)
.map(|(child, _)| child)
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use std::time::Duration;
use super::*;
const DEADLINE: Duration = Duration::from_secs(5);
fn body(run: impl Future<Output = Infallible> + Send + 'static) -> (Arc<Heartbeat>, ChildTask) {
let beat = Arc::new(Heartbeat::new());
(beat.clone(), ChildTask::new(beat, run))
}
#[test]
fn all_walks_every_child_once() {
let walked: Vec<Child> = Child::all().collect();
let distinct: BTreeSet<Child> = walked.iter().copied().collect();
assert_eq!(
walked.len(),
distinct.len(),
"a child is walked twice: {walked:?}"
);
assert_eq!(
walked.len(),
Driver::ALL.len() + Listener::ALL.len() + 1,
"a top-level shape is declared and not walked: {walked:?}"
);
for d in Driver::ALL {
assert!(distinct.contains(&Child::Driver(*d)), "{d:?} is not walked");
}
for l in Listener::ALL {
assert!(
distinct.contains(&Child::Listener(*l)),
"{l:?} is not walked"
);
}
assert!(distinct.contains(&Child::NodeLease));
}
#[test]
fn the_node_lease_is_walked_after_the_kubelet_it_watches() {
let walked: Vec<Child> = Child::all().collect();
let at = |c: Child| walked.iter().position(|w| *w == c);
let (kubelet, lease) = (at(Child::Driver(Driver::Kubelet)), at(Child::NodeLease));
assert!(
matches!((kubelet, lease), (Some(k), Some(l)) if k < l),
"kubelet at {kubelet:?}, node lease at {lease:?}: {walked:?}"
);
}
#[tokio::test]
async fn a_child_is_built_seeing_the_children_spawned_before_it() {
let mut seen = None;
let children =
Children::spawn_catalog(&BootConfig::prescribed(), |child, before| match child {
Child::Driver(Driver::Kubelet) => Some(body(std::future::pending()).1),
Child::NodeLease => {
seen = Some(before.row(Child::Driver(Driver::Kubelet)).is_some());
Some(body(std::future::pending()).1)
}
Child::Driver(_) | Child::Listener(_) => None,
});
assert_eq!(
seen,
Some(true),
"the lease was built without the kubelet's row"
);
assert_eq!(
children.iter().map(|(c, _)| c).collect::<Vec<_>>(),
[Child::Driver(Driver::Kubelet), Child::NodeLease]
);
}
#[test]
fn the_node_lease_is_enabled_wherever_the_kubelet_runs() {
let boot = BootConfig::prescribed();
assert!(Child::Driver(Driver::Kubelet).enabled(&boot));
assert!(
Child::NodeLease.enabled(&boot),
"a node whose kubelet runs renews its lease"
);
}
#[test]
fn every_child_has_a_distinct_name() {
let names: BTreeSet<&str> = Child::all().map(Child::name).collect();
assert_eq!(
names.len(),
Child::all().count(),
"two children share a name"
);
}
#[test]
fn the_kubelet_is_stateful_and_listeners_do_not_tick() {
assert_eq!(
Child::Driver(Driver::Kubelet).tick_state(),
Some(TickState::Stateful)
);
for l in Listener::ALL {
assert_eq!(Child::Listener(*l).tick_state(), None);
}
}
#[test]
fn the_child_catalog_has_no_wildcard_arm() {
let files = [
("child.rs", include_str!("child.rs")),
("fault_matrix.rs", include_str!("fault_matrix.rs")),
];
let offenders: Vec<(&str, usize, &str)> = files
.iter()
.flat_map(|(file, src)| {
let code = src.split("#[cfg(test)]").next().unwrap_or(src);
code.lines()
.enumerate()
.filter(|(_, line)| !line.trim_start().starts_with("//"))
.filter(|(_, line)| line.contains("_ =>") || line.contains("| _"))
.map(|(i, line)| (*file, i + 1, line))
})
.collect();
assert!(
offenders.is_empty(),
"a wildcard arm hides a missing catalog row: {offenders:?}"
);
}
#[tokio::test]
async fn a_child_that_panics_is_reported_dead_and_its_panic_counted() {
let mut children = Children::default();
let child = Child::Driver(Driver::Gc);
let (beat, task) = body(async { panic!("tick blew up") });
children.spawn(child, task);
let dead = tokio::time::timeout(DEADLINE, children.next_dead())
.await
.expect("a panicked child was never reported");
assert_eq!(
dead,
DeadChild {
child,
cause: DeathCause::Panicked
}
);
assert_eq!(
children.get(child).map(ChildHandle::state),
Some(ChildState::Dead(DeathCause::Panicked))
);
assert_eq!(beat.snapshot().panics, 1);
assert_eq!(children.running().count(), 0);
}
#[tokio::test]
async fn an_aborted_child_is_reported_dead_as_cancelled() {
let mut children = Children::default();
let (_, task) = body(std::future::pending());
let child = Child::Listener(Listener::KubeletHttp);
children.spawn(child, task);
children.set.abort_all();
let dead = tokio::time::timeout(DEADLINE, children.next_dead())
.await
.expect("an aborted child was never reported");
assert_eq!(
dead,
DeadChild {
child,
cause: DeathCause::Cancelled
}
);
assert_eq!(
children.get(child).map(ChildHandle::state),
Some(ChildState::Dead(DeathCause::Cancelled))
);
}
#[tokio::test]
async fn one_death_marks_one_child() {
let mut children = Children::default();
let (_, alive) = body(std::future::pending());
let (_, doomed) = body(async { panic!("only me") });
children.spawn(Child::Driver(Driver::Kubelet), alive);
children.spawn(Child::Driver(Driver::Job), doomed);
let dead = tokio::time::timeout(DEADLINE, children.next_dead())
.await
.expect("reported");
assert_eq!(dead.child, Child::Driver(Driver::Job));
assert_eq!(
children.running().collect::<Vec<_>>(),
[Child::Driver(Driver::Kubelet)]
);
}
#[tokio::test]
async fn waiting_on_no_children_pends() {
let mut children = Children::default();
assert!(
tokio::time::timeout(Duration::from_millis(50), children.next_dead())
.await
.is_err()
);
}
#[tokio::test]
async fn stop_ends_every_child() {
let mut children = Children::default();
let (_, a) = body(std::future::pending());
let (_, b) = body(std::future::pending());
children.spawn(Child::Driver(Driver::Kubelet), a);
children.spawn(Child::Listener(Listener::EtcdFacade), b);
children.stop().await;
assert_eq!(children.running().count(), 0);
assert!(
children
.iter()
.all(|(_, e)| e.state() == ChildState::Dead(DeathCause::Cancelled))
);
}
#[test]
fn only_an_asked_for_abort_is_a_stop() {
assert!(stopped_on_purpose(true, DeathCause::Cancelled));
assert!(!stopped_on_purpose(true, DeathCause::Panicked));
assert!(!stopped_on_purpose(false, DeathCause::Cancelled));
assert!(!stopped_on_purpose(false, DeathCause::Panicked));
}
#[tokio::test]
async fn a_dead_child_respawns_one_generation_on_with_its_death_kept() {
let mut children = Children::default();
let child = Child::Driver(Driver::Gc);
children.spawn(child, body(async { panic!("first life") }).1);
let first = children.get(child).expect("spawned");
assert_eq!(first.generation(), 0);
assert_eq!(first.last_death(), None);
assert_eq!(
children.spawn_one(child, body(std::future::pending()).1),
Err(StillRunning(child)),
"a running child was given a second task"
);
let dead = tokio::time::timeout(DEADLINE, children.next_dead())
.await
.expect("reported");
assert_eq!(dead.cause, DeathCause::Panicked);
assert_eq!(
children.spawn_one(child, body(std::future::pending()).1),
Ok(1)
);
let handle = children.get(child).expect("respawned");
assert_eq!(handle.state(), ChildState::Running);
assert_eq!(handle.generation(), 1);
assert_eq!(
handle.last_death().map(|d| d.cause),
Some(DeathCause::Panicked),
"the respawn forgot how its previous task ended"
);
assert_eq!(handle.ended_at(), None);
}
#[tokio::test]
async fn stopping_one_child_leaves_its_siblings_running() {
let mut children = Children::default();
let (target, sibling) = (
Child::Listener(Listener::KubeletHttp),
Child::Driver(Driver::Kubelet),
);
children.spawn(target, body(std::future::pending()).1);
children.spawn(sibling, body(std::future::pending()).1);
let ended = tokio::time::timeout(DEADLINE, children.stop_one(target))
.await
.expect("the stop finished");
assert_eq!(
ended,
[DeadChild {
child: target,
cause: DeathCause::Cancelled
}]
);
assert_eq!(children.running().collect::<Vec<_>>(), [sibling]);
assert!(
children.stopping.is_empty(),
"a finished stop is still pending"
);
assert!(children.stop_one(target).await.is_empty());
assert!(
children
.stop_one(Child::Listener(Listener::EtcdFacade))
.await
.is_empty()
);
}
#[tokio::test]
async fn only_a_stopped_child_is_forgotten() {
let mut children = Children::default();
let child = Child::Driver(Driver::Job);
children.spawn(child, body(std::future::pending()).1);
assert_eq!(children.forget(child), Err(StillRunning(child)));
let _ = children.stop_one(child).await;
assert_eq!(children.forget(child), Ok(()));
assert!(children.get(child).is_none());
assert!(children.rows().is_empty(), "its health row outlived it");
assert_eq!(
children.spawn_one(child, body(std::future::pending()).1),
Ok(0),
"a forgotten child comes back as new"
);
}
#[test]
fn every_switch_is_a_leaf_that_gates_a_driver() {
for switch in EnableSwitch::ALL.iter().copied() {
assert_eq!(EnableSwitch::of_leaf(switch.leaf()), Some(switch));
assert!(
switch.drivers().next().is_some(),
"{switch:?} gates nothing"
);
}
let restart_only: Vec<EnableSwitch> = EnableSwitch::ALL
.iter()
.copied()
.filter(|s| !s.toggles_live())
.collect();
assert_eq!(
restart_only,
[EnableSwitch::ServiceRouting, EnableSwitch::Crd]
);
assert_eq!(
EnableSwitch::PvBinder.drivers().collect::<Vec<_>>(),
[
Driver::PvBinder,
Driver::VolumeSnapshot,
Driver::PvcProtection
]
);
assert_eq!(EnableSwitch::of_leaf("controllers.enable"), None);
}
#[test]
fn only_a_moved_listener_is_rebuilt_by_a_change_in_place() {
let before = BootConfig::prescribed();
let mut after = before.clone();
after.kubelet_listen_addr = "127.0.0.1:20250".into();
for child in Child::all() {
assert_eq!(
child.rebuilt_between(&before, &after),
child == Child::Listener(Listener::KubeletHttp),
"{child}"
);
}
assert!(Child::all().all(|c| !c.rebuilt_between(&before, &before)));
}
}