#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Fingerprint {
pub wire_version: u32,
pub eventlog_schema: u32,
pub crd_api_version: String,
pub tool_catalog_hash: String,
}
impl Fingerprint {
#[must_use]
pub fn new(
wire_version: u32,
eventlog_schema: u32,
crd_api_version: impl Into<String>,
tool_catalog_hash: impl Into<String>,
) -> Self {
Self {
wire_version,
eventlog_schema,
crd_api_version: crd_api_version.into(),
tool_catalog_hash: tool_catalog_hash.into(),
}
}
#[must_use]
pub fn runtime_target(&self) -> RuntimeTarget {
RuntimeTarget {
wire_version: self.wire_version,
eventlog_schema: self.eventlog_schema,
crd_api_version: self.crd_api_version.clone(),
}
}
#[must_use]
pub fn classify(&self, available: &Self) -> Compatibility {
if self.wire_version != available.wire_version
|| self.eventlog_schema != available.eventlog_schema
|| self.crd_api_version != available.crd_api_version
{
return Compatibility::Cold;
}
if self.tool_catalog_hash != available.tool_catalog_hash {
return Compatibility::Hot;
}
Compatibility::Warm
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeTarget {
pub wire_version: u32,
pub eventlog_schema: u32,
pub crd_api_version: String,
}
impl RuntimeTarget {
#[must_use]
pub fn new(
wire_version: u32,
eventlog_schema: u32,
crd_api_version: impl Into<String>,
) -> Self {
Self {
wire_version,
eventlog_schema,
crd_api_version: crd_api_version.into(),
}
}
#[must_use]
fn mismatch(&self, running: &Fingerprint) -> Option<Incompatibility> {
if self.wire_version != running.wire_version {
Some(Incompatibility::Wire)
} else if self.eventlog_schema != running.eventlog_schema {
Some(Incompatibility::EventLog)
} else if self.crd_api_version != running.crd_api_version {
Some(Incompatibility::Crd)
} else {
None
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StagedBundle {
pub target: RuntimeTarget,
pub catalog_hash: String,
}
impl StagedBundle {
#[must_use]
pub fn new(target: RuntimeTarget, catalog_hash: impl Into<String>) -> Self {
Self {
target,
catalog_hash: catalog_hash.into(),
}
}
#[must_use]
pub fn evaluate(&self, running: &Fingerprint) -> Compatibility {
self.target
.mismatch(running)
.map_or(Compatibility::Hot, Compatibility::Incompatible)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Compatibility {
Hot,
Warm,
Cold,
Incompatible(Incompatibility),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Incompatibility {
Wire,
EventLog,
Crd,
}
impl std::fmt::Display for Incompatibility {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let axis = match self {
Self::Wire => "the wire protocol version",
Self::EventLog => "the event-log schema version",
Self::Crd => "the reconciled CRD apiVersion",
};
write!(f, "{axis} differs from the target the bundle was built for")
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
fn fp(wire: u32, schema: u32, crd: &str, catalog: &str) -> Fingerprint {
Fingerprint::new(wire, schema, crd, catalog)
}
#[test]
fn classify_covers_hot_warm_cold() {
let crd = "polychrome.uno/v1";
let cases: &[(&str, Fingerprint, Fingerprint, Compatibility)] = &[
(
"identical fingerprint → warm (binary-internal change on restart)",
fp(3, 7, crd, "catalog-a"),
fp(3, 7, crd, "catalog-a"),
Compatibility::Warm,
),
(
"only the tool catalog moved → hot (config-as-data reload)",
fp(3, 7, crd, "catalog-a"),
fp(3, 7, crd, "catalog-b"),
Compatibility::Hot,
),
(
"wire version moved → cold",
fp(3, 7, crd, "catalog-a"),
fp(4, 7, crd, "catalog-a"),
Compatibility::Cold,
),
(
"event-log schema moved → cold",
fp(3, 7, crd, "catalog-a"),
fp(3, 8, crd, "catalog-a"),
Compatibility::Cold,
),
(
"CRD apiVersion moved → cold",
fp(3, 7, crd, "catalog-a"),
fp(3, 7, "polychrome.uno/v2", "catalog-a"),
Compatibility::Cold,
),
(
"format axis moved AND catalog moved → cold dominates hot",
fp(3, 7, crd, "catalog-a"),
fp(4, 7, crd, "catalog-b"),
Compatibility::Cold,
),
];
for (name, running, available, expected) in cases {
assert_eq!(
running.classify(available),
*expected,
"classify case: {name}"
);
}
}
#[test]
fn classify_is_direction_agnostic_on_format_axes() {
let old = fp(3, 7, "polychrome.uno/v1", "c");
let new = fp(4, 8, "polychrome.uno/v2", "c");
assert_eq!(old.classify(&new), Compatibility::Cold);
assert_eq!(new.classify(&old), Compatibility::Cold);
}
#[test]
fn bundle_applies_hot_when_runtime_matches_target() {
let running = fp(3, 7, "polychrome.uno/v1", "catalog-a");
let bundle = StagedBundle::new(running.runtime_target(), "catalog-b");
assert_eq!(bundle.evaluate(&running), Compatibility::Hot);
}
#[test]
fn bundle_applies_hot_even_when_payload_matches_current_catalog() {
let running = fp(3, 7, "polychrome.uno/v1", "catalog-a");
let bundle = StagedBundle::new(running.runtime_target(), "catalog-a");
assert_eq!(bundle.evaluate(&running), Compatibility::Hot);
}
#[test]
fn bundle_refused_when_running_cannot_satisfy_target() {
let running = fp(3, 7, "polychrome.uno/v1", "catalog-a");
let cases: &[(&str, RuntimeTarget, Incompatibility)] = &[
(
"bundle built for a newer wire version",
RuntimeTarget::new(4, 7, "polychrome.uno/v1"),
Incompatibility::Wire,
),
(
"bundle built for a newer event-log schema",
RuntimeTarget::new(3, 8, "polychrome.uno/v1"),
Incompatibility::EventLog,
),
(
"bundle built for a different CRD apiVersion",
RuntimeTarget::new(3, 7, "polychrome.uno/v2"),
Incompatibility::Crd,
),
];
for (name, target, reason) in cases {
let bundle = StagedBundle::new(target.clone(), "catalog-b");
assert_eq!(
bundle.evaluate(&running),
Compatibility::Incompatible(*reason),
"interlock case: {name}"
);
}
}
#[test]
fn interlock_reports_wire_before_other_axes() {
let running = fp(3, 7, "polychrome.uno/v1", "catalog-a");
let bundle = StagedBundle::new(RuntimeTarget::new(9, 9, "polychrome.uno/v9"), "catalog-b");
assert_eq!(
bundle.evaluate(&running),
Compatibility::Incompatible(Incompatibility::Wire),
);
}
#[test]
fn incompatibility_display_names_the_axis() {
assert_eq!(
Incompatibility::Wire.to_string(),
"the wire protocol version differs from the target the bundle was built for",
);
assert_eq!(
Incompatibility::EventLog.to_string(),
"the event-log schema version differs from the target the bundle was built for",
);
assert_eq!(
Incompatibility::Crd.to_string(),
"the reconciled CRD apiVersion differs from the target the bundle was built for",
);
}
}