pub mod registry;
#[cfg(test)]
mod tests;
use std::time::Duration;
use serde::Serialize;
use crate::backends::FailureCategory;
use crate::convergence::{RevisionReport, SnapshotSource};
use crate::shutdown::Phase;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Component {
ControlPlane,
Catalogue,
SecretStore,
BudgetStore,
RateLimitStore,
RevocationStore,
UsageSink,
ProviderCredentials,
}
pub const COMPONENTS: &[&str] = &[
"control_plane",
"catalogue",
"secret_store",
"budget_store",
"rate_limit_store",
"revocation_store",
"usage_sink",
"provider_credentials",
];
impl Component {
pub const ALL: &'static [Self] = &[
Self::ControlPlane,
Self::Catalogue,
Self::SecretStore,
Self::BudgetStore,
Self::RateLimitStore,
Self::RevocationStore,
Self::UsageSink,
Self::ProviderCredentials,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::ControlPlane => "control_plane",
Self::Catalogue => "catalogue",
Self::SecretStore => "secret_store",
Self::BudgetStore => "budget_store",
Self::RateLimitStore => "rate_limit_store",
Self::RevocationStore => "revocation_store",
Self::UsageSink => "usage_sink",
Self::ProviderCredentials => "provider_credentials",
}
}
pub const fn is_tenant_visible(self) -> bool {
matches!(
self,
Self::Catalogue
| Self::BudgetStore
| Self::RateLimitStore
| Self::RevocationStore
| Self::ProviderCredentials
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComponentState {
Ok,
Degraded,
Unavailable,
Disabled,
}
impl ComponentState {
pub const ALL: &'static [Self] = &[Self::Ok, Self::Degraded, Self::Unavailable, Self::Disabled];
pub const fn as_str(self) -> &'static str {
match self {
Self::Ok => "ok",
Self::Degraded => "degraded",
Self::Unavailable => "unavailable",
Self::Disabled => "disabled",
}
}
pub const fn gauge_value(self) -> u64 {
match self {
Self::Disabled => 0,
Self::Ok => 1,
Self::Degraded => 2,
Self::Unavailable => 3,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusReason {
Unavailable,
Unreachable,
Timeout,
AuthenticationRejected,
PermissionDenied,
SchemaIncompatible,
PayloadCorrupt,
ValidationRejected,
ProjectionRejected,
SnapshotRejected,
SecretUnresolved,
Stale,
NotConfigured,
Draining,
CapacityExhausted,
Unknown,
}
impl StatusReason {
pub const ALL: &'static [Self] = &[
Self::Unavailable,
Self::Unreachable,
Self::Timeout,
Self::AuthenticationRejected,
Self::PermissionDenied,
Self::SchemaIncompatible,
Self::PayloadCorrupt,
Self::ValidationRejected,
Self::ProjectionRejected,
Self::SnapshotRejected,
Self::SecretUnresolved,
Self::Stale,
Self::NotConfigured,
Self::Draining,
Self::CapacityExhausted,
Self::Unknown,
];
pub const fn code(self) -> &'static str {
match self {
Self::Unavailable => "unavailable",
Self::Unreachable => "unreachable",
Self::Timeout => "timeout",
Self::AuthenticationRejected => "authentication_rejected",
Self::PermissionDenied => "permission_denied",
Self::SchemaIncompatible => "schema_incompatible",
Self::PayloadCorrupt => "payload_corrupt",
Self::ValidationRejected => "validation_rejected",
Self::ProjectionRejected => "projection_rejected",
Self::SnapshotRejected => "snapshot_rejected",
Self::SecretUnresolved => "secret_unresolved",
Self::Stale => "stale",
Self::NotConfigured => "not_configured",
Self::Draining => "draining",
Self::CapacityExhausted => "capacity_exhausted",
Self::Unknown => "unknown",
}
}
pub const fn is_tenant_safe(self) -> bool {
matches!(
self,
Self::Unavailable
| Self::Stale
| Self::NotConfigured
| Self::Draining
| Self::CapacityExhausted
| Self::Unknown
)
}
pub const fn for_scope(self, scope: StatusScope) -> Self {
match scope {
StatusScope::Deployment => self,
StatusScope::Namespace if self.is_tenant_safe() => self,
StatusScope::Namespace => Self::Unavailable,
}
}
pub const fn from_failure(category: FailureCategory) -> Self {
match category {
FailureCategory::Unavailable => Self::Unreachable,
FailureCategory::Conflict => Self::Unknown,
FailureCategory::NotFound => Self::NotConfigured,
FailureCategory::Invalid => Self::ValidationRejected,
FailureCategory::Denied => Self::PermissionDenied,
FailureCategory::Corrupt => Self::PayloadCorrupt,
}
}
pub fn from_revision_reason(reason: &str) -> Self {
match reason {
"unavailable" => Self::Unreachable,
"corrupt" => Self::PayloadCorrupt,
"incompatible" => Self::SchemaIncompatible,
"projection" => Self::ProjectionRejected,
"validation" | "invalid" => Self::ValidationRejected,
"secret" => Self::SecretUnresolved,
"snapshot" => Self::SnapshotRejected,
"not_found" => Self::NotConfigured,
"denied" => Self::PermissionDenied,
_ => Self::Unknown,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusScope {
Namespace,
Deployment,
}
impl StatusScope {
pub const fn as_str(self) -> &'static str {
match self {
Self::Namespace => "namespace",
Self::Deployment => "deployment",
}
}
pub const fn for_operator_authority(direct_operator_authority: bool) -> Self {
if direct_operator_authority {
Self::Deployment
} else {
Self::Namespace
}
}
}
#[derive(Debug, Clone)]
pub struct ComponentObservation {
pub component: Component,
pub state: ComponentState,
pub reason: Option<StatusReason>,
pub detail: Option<String>,
}
impl ComponentObservation {
pub const fn ok(component: Component) -> Self {
Self {
component,
state: ComponentState::Ok,
reason: None,
detail: None,
}
}
pub fn unavailable(component: Component, reason: StatusReason, detail: String) -> Self {
Self {
component,
state: ComponentState::Unavailable,
reason: Some(reason),
detail: Some(detail),
}
}
pub fn degraded(component: Component, reason: StatusReason, detail: String) -> Self {
Self {
component,
state: ComponentState::Degraded,
reason: Some(reason),
detail: Some(detail),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Observed {
pub component: Component,
pub state: ComponentState,
pub reason: Option<StatusReason>,
pub age: Duration,
pub stale: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatusView {
pub components: Vec<Observed>,
}
impl StatusView {
pub fn stale(&self) -> bool {
self.components.iter().any(|observed| observed.stale)
}
pub fn project(
&self,
scope: StatusScope,
phase: Phase,
revision: Option<&RevisionReport>,
) -> StatusResponse {
let visible: Vec<&Observed> = self
.components
.iter()
.filter(|observed| match scope {
StatusScope::Deployment => true,
StatusScope::Namespace => observed.component.is_tenant_visible(),
})
.collect();
StatusResponse {
object: "status",
observed: "replica",
scope: scope.as_str(),
phase: phase.as_str(),
stale: visible.iter().any(|observed| observed.stale),
components: visible
.iter()
.map(|observed| ComponentStatus {
component: observed.component.as_str(),
state: observed.state.as_str(),
reason: observed.reason.map(|reason| reason.for_scope(scope).code()),
observed_age_ms: coarsen_age(observed.age, scope),
})
.collect(),
revision: match scope {
StatusScope::Deployment => revision.map(RevisionSummary::from_report),
StatusScope::Namespace => None,
},
}
}
}
fn coarsen_age(age: Duration, scope: StatusScope) -> u64 {
let millis = u64::try_from(age.as_millis()).unwrap_or(u64::MAX);
match scope {
StatusScope::Deployment => millis,
StatusScope::Namespace => (millis / 1_000) * 1_000,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct StatusResponse {
pub object: &'static str,
pub observed: &'static str,
pub scope: &'static str,
pub phase: &'static str,
pub stale: bool,
pub components: Vec<ComponentStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub revision: Option<RevisionSummary>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ComponentStatus {
pub component: &'static str,
pub state: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<&'static str>,
pub observed_age_ms: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct RevisionSummary {
pub converged: bool,
pub lag_ms: u64,
pub generation: u64,
pub consecutive_failures: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<&'static str>,
}
impl RevisionSummary {
pub fn from_report(report: &RevisionReport) -> Self {
Self {
converged: report.converged(),
lag_ms: u64::try_from(report.lag.as_millis()).unwrap_or(u64::MAX),
generation: report.generation,
consecutive_failures: report.consecutive_failures,
source: report.source.map(SnapshotSource::as_str),
reason: report
.last_rejection
.as_ref()
.map(|rejection| StatusReason::from_revision_reason(rejection.reason).code()),
}
}
}