use chrono::{DateTime, SecondsFormat, Utc};
use engenho_serve::StopSignal;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use crate::error::RuntimeError;
use crate::release::{BootFailed, BootUnwind};
engenho_substrate::closed_enum! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BootPhase {
ResolveConfig,
ReadBootConfig,
PreflightBackend,
ValidateConfig,
OpenStore,
AwaitLeadership,
BuildScheduler,
RegisterNode,
SeedCluster,
IssuePki,
LoadAdminToken,
BuildAuth,
BindApiserver,
PublishKubeconfigs,
SpawnChildren,
AdoptHealth,
}
}
impl BootPhase {
pub const FIRST: Self = Self::ResolveConfig;
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::ResolveConfig => "resolve_config",
Self::ReadBootConfig => "read_boot_config",
Self::PreflightBackend => "preflight_backend",
Self::ValidateConfig => "validate_config",
Self::OpenStore => "open_store",
Self::AwaitLeadership => "await_leadership",
Self::BuildScheduler => "build_scheduler",
Self::RegisterNode => "register_node",
Self::SeedCluster => "seed_cluster",
Self::IssuePki => "issue_pki",
Self::LoadAdminToken => "load_admin_token",
Self::BuildAuth => "build_auth",
Self::BindApiserver => "bind_apiserver",
Self::PublishKubeconfigs => "publish_kubeconfigs",
Self::SpawnChildren => "spawn_children",
Self::AdoptHealth => "adopt_health",
}
}
}
impl std::fmt::Display for BootPhase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BootKind {
FirstBoot,
Resume,
Ephemeral,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Timestamp(DateTime<Utc>);
impl Timestamp {
#[must_use]
pub fn now() -> Self {
Self(Utc::now())
}
pub fn parse(text: &str) -> Result<Self, chrono::ParseError> {
DateTime::parse_from_rfc3339(text).map(|t| Self(t.with_timezone(&Utc)))
}
#[must_use]
pub fn after(self, delay: std::time::Duration) -> Self {
chrono::Duration::from_std(delay)
.ok()
.and_then(|d| self.0.checked_add_signed(d))
.map_or(Self(DateTime::<Utc>::MAX_UTC), Self)
}
#[must_use]
pub const fn utc(self) -> DateTime<Utc> {
self.0
}
#[must_use]
pub fn to_rfc3339(self) -> String {
self.0.to_rfc3339_opts(SecondsFormat::Millis, true)
}
}
impl std::fmt::Display for Timestamp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.to_rfc3339())
}
}
impl Serialize for Timestamp {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_rfc3339())
}
}
impl<'de> Deserialize<'de> for Timestamp {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = String::deserialize(deserializer)?;
Self::parse(&text).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BootProgress {
Entered {
phase: BootPhase,
at: Timestamp,
},
Kind(BootKind),
}
#[derive(Debug)]
pub struct BootRecorder {
progress: Option<mpsc::UnboundedSender<BootProgress>>,
cancel: Option<StopSignal>,
current: BootPhase,
}
impl BootRecorder {
#[must_use]
pub fn silent() -> Self {
Self {
progress: None,
cancel: None,
current: BootPhase::FIRST,
}
}
#[must_use]
pub fn new(progress: mpsc::UnboundedSender<BootProgress>, cancel: StopSignal) -> Self {
Self {
progress: Some(progress),
cancel: Some(cancel),
current: BootPhase::FIRST,
}
}
#[must_use]
pub const fn current(&self) -> BootPhase {
self.current
}
pub(crate) fn enter(&mut self, phase: BootPhase) -> Result<(), RuntimeError> {
if self.is_cancelled() {
return Err(RuntimeError::BootCancelled {
phase: self.current,
});
}
self.current = phase;
self.report(BootProgress::Entered {
phase,
at: Timestamp::now(),
});
Ok(())
}
pub(crate) fn enter_committed(&mut self, phase: BootPhase) {
self.current = phase;
self.report(BootProgress::Entered {
phase,
at: Timestamp::now(),
});
}
pub(crate) fn failed(&self, error: RuntimeError) -> BootFailed {
BootFailed {
error,
phase: self.current,
unwind: BootUnwind::NeverOpened,
}
}
pub(crate) fn observe(&self, kind: BootKind) {
self.report(BootProgress::Kind(kind));
}
fn report(&self, progress: BootProgress) {
if let Some(sink) = &self.progress {
let _ = sink.send(progress);
}
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.cancel.as_ref().is_some_and(StopSignal::is_stopped)
}
pub(crate) async fn cancelled(&self) {
match &self.cancel {
Some(signal) => signal.clone().stopped().await,
None => std::future::pending().await,
}
}
}