use std::collections::BTreeMap;
use engenho_controllers::status::resource_version_of;
use engenho_controllers::{Effect, Refusal, node_lease};
use engenho_store::command::{Reason, ResourceCommand, TxnCompare, TxnOp};
use engenho_store::{ResourceKey, ResourceValue, StoreMesh};
use engenho_types::generated_v1_34::core_v1::Node;
use engenho_types::generated_v1_34::types::{
DaemonEndpoint, NodeAddress, NodeDaemonEndpoints, NodeSpec, NodeStatus, Quantity,
};
use engenho_types::kind::KubeResource;
use engenho_types::meta::ObjectMeta;
use tracing::info;
use crate::error::RuntimeError;
const REGISTRATION_ATTEMPTS: u32 = 5;
#[derive(Debug, thiserror::Error)]
pub enum NodeRegistrationError {
#[error("could not serialize the registration of Node {node:?}: {source}")]
Serialize {
node: String,
#[source]
source: serde_json::Error,
},
#[error(
"Node {node:?} has no parseable metadata.resourceVersion; refusing to merge into it \
without a compare-and-swap"
)]
NoResourceVersion {
node: String,
},
#[error("the store refused the registration of Node {node:?}: {refusal}")]
Refused {
node: String,
refusal: Refusal,
},
#[error(
"Node {node:?} changed under every one of {attempts} registration attempts; \
something is rewriting it continuously"
)]
Contended {
node: String,
attempts: u32,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Registered {
Created,
Merged,
AlreadyCurrent,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct HostOwned {
labels: BTreeMap<String, String>,
resources: BTreeMap<String, Quantity>,
endpoint: Option<KubeletEndpointFields>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KubeletEndpointFields {
addresses: Vec<(NodeAddressType, String)>,
port: u16,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum NodeAddressType {
InternalIp,
Hostname,
}
impl NodeAddressType {
const fn as_str(self) -> &'static str {
match self {
Self::InternalIp => "InternalIP",
Self::Hostname => "Hostname",
}
}
}
impl KubeletEndpointFields {
#[must_use]
pub(crate) fn derive(
node_name: &str,
bound: std::net::SocketAddr,
advertise_address: &str,
) -> Self {
let internal = if bound.ip().is_unspecified() {
crate::runtime::advertised_ip(advertise_address)
} else {
Some(bound.ip())
};
let mut addresses = Vec::with_capacity(2);
if let Some(ip) = internal {
addresses.push((NodeAddressType::InternalIp, ip.to_string()));
}
if !node_name.is_empty() {
addresses.push((NodeAddressType::Hostname, node_name.to_owned()));
}
Self {
addresses,
port: bound.port(),
}
}
fn typed_addresses(&self) -> Vec<NodeAddress> {
self.addresses
.iter()
.map(|(ty, address)| NodeAddress {
address: address.clone(),
r#type: ty.as_str().to_owned(),
})
.collect()
}
fn typed_endpoints(&self) -> NodeDaemonEndpoints {
NodeDaemonEndpoints {
kubelet_endpoint: Some(DaemonEndpoint {
port: i32::from(self.port),
}),
}
}
fn is_current_on(&self, status: Option<&ResourceValue>) -> bool {
let want = serde_json::to_value(self.typed_addresses()).ok();
let addresses_current = status.and_then(|s| s.get("addresses")) == want.as_ref();
let port_current = status
.and_then(|s| s.pointer("/daemonEndpoints/kubeletEndpoint/Port"))
.and_then(serde_json::Value::as_u64)
== Some(u64::from(self.port));
addresses_current && port_current
}
}
impl HostOwned {
pub(crate) fn measured(node_name: &str) -> Self {
let (cpu, memory) = crate::runtime::host_capacity();
Self::of_host(node_name, cpu, memory)
}
pub(crate) fn advertising(
mut self,
caps: Option<&engenho_scheduler::NodeCapabilities>,
) -> Self {
if let Some(caps) = caps {
self.labels.extend(caps.runtime_labels());
}
self
}
pub(crate) fn reachable_at(mut self, endpoint: KubeletEndpointFields) -> Self {
self.endpoint = Some(endpoint);
self
}
pub(crate) fn of_host(node_name: &str, cpu: String, memory: String) -> Self {
Self {
labels: well_known_node_labels(node_name),
resources: BTreeMap::from([
("cpu".to_owned(), Quantity(cpu)),
("memory".to_owned(), Quantity(memory)),
]),
endpoint: None,
}
}
fn fields(&self) -> Node {
Node {
metadata: ObjectMeta {
labels: self.labels.clone(),
..ObjectMeta::default()
},
spec: None,
status: Some(NodeStatus {
capacity: self.resources.clone(),
allocatable: self.resources.clone(),
addresses: self
.endpoint
.as_ref()
.map(KubeletEndpointFields::typed_addresses)
.unwrap_or_default(),
daemon_endpoints: self
.endpoint
.as_ref()
.map(KubeletEndpointFields::typed_endpoints),
..NodeStatus::default()
}),
}
}
fn merge_patch(&self, node: &str) -> Result<ResourceValue, NodeRegistrationError> {
serde_json::to_value(self.fields()).map_err(|source| NodeRegistrationError::Serialize {
node: node.to_owned(),
source,
})
}
fn fresh_node(&self, node: &str, now: &str) -> Result<ResourceValue, NodeRegistrationError> {
let mut typed = self.fields();
node.clone_into(&mut typed.metadata.name);
typed.spec = Some(NodeSpec {
unschedulable: Some(false),
..NodeSpec::default()
});
let mut value =
serde_json::to_value(typed).map_err(|source| NodeRegistrationError::Serialize {
node: node.to_owned(),
source,
})?;
node_lease::project_ready_condition(&mut value, None, now);
if let Some(obj) = value.as_object_mut() {
obj.insert("apiVersion".to_owned(), Node::GVK.version.into());
obj.insert("kind".to_owned(), Node::GVK.kind.into());
}
crate::runtime::stamp_creation_timestamp_value(&mut value);
Ok(value)
}
fn is_current_on(&self, live: &ResourceValue) -> bool {
let labels = live.get("metadata").and_then(|m| m.get("labels"));
let labels_current = self.labels.iter().all(|(k, v)| {
labels.and_then(|l| l.get(k)).and_then(|x| x.as_str()) == Some(v.as_str())
});
let status = live.get("status");
let resources_current = ["capacity", "allocatable"].iter().all(|field| {
let map = status.and_then(|s| s.get(*field));
self.resources.iter().all(|(k, q)| {
map.and_then(|m| m.get(k)).and_then(|x| x.as_str()) == Some(q.0.as_str())
})
});
let endpoint_current = self
.endpoint
.as_ref()
.is_none_or(|e| e.is_current_on(status));
labels_current && resources_current && endpoint_current
}
}
pub(crate) fn node_key(node: &str) -> ResourceKey {
ResourceKey::cluster_scoped(Node::GVK.group, Node::GVK.version, Node::GVK.kind, node)
}
pub(crate) async fn register_node(
store: &StoreMesh,
node: &str,
host: &HostOwned,
) -> Result<Registered, RuntimeError> {
let key = node_key(node);
for _ in 0..REGISTRATION_ATTEMPTS {
let Some(live) = store.get(&key).await else {
let body = host.fresh_node(node, &engenho_types::time::now_rfc3339_utc())?;
let applied = store
.propose(ResourceCommand::Txn {
compares: vec![TxnCompare::NotExists { key: key.clone() }],
success: vec![TxnOp::Put {
key: key.clone(),
value: body,
}],
failure: Vec::new(),
reason: Reason::Operator,
})
.await?;
match Effect::of(applied.op) {
Effect::Written(_) => {
info!(
node,
"registered node (created, Ready=Unknown until its first heartbeat)"
);
return Ok(Registered::Created);
}
Effect::Unchanged => continue,
Effect::Rejected(refusal) => {
return Err(refused(node, refusal));
}
}
};
if host.is_current_on(&live) {
info!(node, "registered node (already current, nothing written)");
return Ok(Registered::AlreadyCurrent);
}
let Some(expected) = resource_version_of(&live) else {
return Err(NodeRegistrationError::NoResourceVersion {
node: node.to_owned(),
}
.into());
};
let applied = store
.propose(ResourceCommand::patch_cas(
key.clone(),
host.merge_patch(node)?,
Some(expected),
Reason::Operator,
))
.await?;
match Effect::of(applied.op) {
Effect::Written(_) => {
info!(node, "registered node (merged host-owned fields)");
return Ok(Registered::Merged);
}
Effect::Rejected(Refusal::Conflict) => {}
Effect::Rejected(refusal) => return Err(refused(node, refusal)),
Effect::Unchanged => return Ok(Registered::AlreadyCurrent),
}
}
Err(NodeRegistrationError::Contended {
node: node.to_owned(),
attempts: REGISTRATION_ATTEMPTS,
}
.into())
}
fn refused(node: &str, refusal: Refusal) -> RuntimeError {
NodeRegistrationError::Refused {
node: node.to_owned(),
refusal,
}
.into()
}
#[must_use]
fn kube_arch() -> &'static str {
match std::env::consts::ARCH {
"aarch64" => "arm64",
"x86_64" => "amd64",
"arm" => "arm",
"powerpc64" => "ppc64le",
"s390x" => "s390x",
other => other,
}
}
#[must_use]
fn kube_os() -> &'static str {
match std::env::consts::OS {
"macos" => "darwin",
other => other,
}
}
#[must_use]
fn well_known_node_labels(node_name: &str) -> BTreeMap<String, String> {
[
("kubernetes.io/hostname", node_name),
("kubernetes.io/os", kube_os()),
("kubernetes.io/arch", kube_arch()),
("beta.kubernetes.io/os", kube_os()),
("beta.kubernetes.io/arch", kube_arch()),
]
.into_iter()
.map(|(k, v)| (k.to_owned(), v.to_owned()))
.collect()
}
#[cfg(test)]
mod node_label_tests {
use super::{kube_arch, kube_os, well_known_node_labels};
#[test]
fn labels_use_go_vocabulary_not_rust() {
assert_ne!(
kube_arch(),
"aarch64",
"kubernetes.io/arch must never be the Rust spelling"
);
assert_ne!(
kube_os(),
"macos",
"kubernetes.io/os must never be the Rust spelling"
);
assert!(
matches!(kube_arch(), "arm64" | "amd64" | "arm" | "ppc64le" | "s390x"),
"unexpected GOARCH rendering: {}",
kube_arch()
);
assert!(
matches!(kube_os(), "linux" | "darwin" | "windows"),
"unexpected GOOS rendering: {}",
kube_os()
);
}
#[test]
fn every_well_known_label_is_present_and_non_empty() {
let labels = well_known_node_labels("cid");
for key in [
"kubernetes.io/hostname",
"kubernetes.io/os",
"kubernetes.io/arch",
"beta.kubernetes.io/os",
"beta.kubernetes.io/arch",
] {
let v = labels
.get(key)
.unwrap_or_else(|| panic!("missing well-known label {key}"));
assert!(!v.is_empty(), "{key} is empty, which matches nothing");
}
assert_eq!(labels["kubernetes.io/hostname"], "cid");
}
#[test]
fn beta_aliases_agree_with_their_replacements() {
let labels = well_known_node_labels("cid");
assert_eq!(labels["beta.kubernetes.io/os"], labels["kubernetes.io/os"]);
assert_eq!(
labels["beta.kubernetes.io/arch"],
labels["kubernetes.io/arch"]
);
}
}
#[cfg(test)]
mod registration_tests {
use std::sync::Arc;
use std::time::Duration;
use engenho_controllers::node_lease::find_ready_condition;
use engenho_store::command::{Reason, ResourceCommand};
use engenho_store::{InProcessRouter, ResourceValue, StoreMesh, default_config};
use serde_json::json;
use super::{HostOwned, Registered, node_key, register_node};
const NODE: &str = "ryn";
async fn boot() -> Arc<StoreMesh> {
let router = InProcessRouter::new();
let cfg = default_config("t1-3a").expect("config");
let store = Arc::new(
StoreMesh::start(1, "in-process://1".into(), router, cfg)
.await
.expect("store starts"),
);
store.initialize_singleton().await.expect("initialize");
assert!(store.wait_for_leadership(Duration::from_secs(3)).await);
store
}
fn host() -> HostOwned {
HostOwned::of_host(NODE, "8".to_owned(), "17179869184".to_owned())
}
async fn node(store: &StoreMesh) -> ResourceValue {
store.get(&node_key(NODE)).await.expect("the Node exists")
}
async fn operator_patch(store: &StoreMesh, patch: ResourceValue) {
let applied = store
.propose(ResourceCommand::patch(
node_key(NODE),
patch,
Reason::Operator,
))
.await
.expect("patch proposes");
assert!(
super::Effect::of(applied.op).landed(),
"the operator's own write must land: {:?}",
applied.op
);
}
async fn registered_then_restarted_after(patch: ResourceValue) -> ResourceValue {
let store = boot().await;
register_node(&store, NODE, &host())
.await
.expect("first boot");
operator_patch(&store, patch).await;
register_node(&store, NODE, &host()).await.expect("restart");
node(&store).await
}
#[tokio::test]
async fn a_fresh_node_registers_ready_unknown_never_updated() {
let store = boot().await;
let outcome = register_node(&store, NODE, &host())
.await
.expect("register");
assert_eq!(outcome, Registered::Created);
let n = node(&store).await;
let ready = find_ready_condition(&n).expect("a fresh Node carries a Ready condition");
assert_eq!(
ready["status"], "Unknown",
"a node that has never heartbeat is not Ready: {n}"
);
assert_eq!(ready["reason"], "NodeStatusNeverUpdated", "{n}");
let readies = n["status"]["conditions"]
.as_array()
.expect("conditions")
.iter()
.filter(|c| c["type"] == "Ready")
.count();
assert_eq!(readies, 1, "exactly one Ready condition: {n}");
assert_eq!(n["metadata"]["labels"]["kubernetes.io/hostname"], NODE);
assert_eq!(n["status"]["capacity"]["cpu"], "8");
assert_eq!(n["status"]["allocatable"]["memory"], "17179869184");
assert_eq!(
n["spec"]["unschedulable"], false,
"a fresh node is not cordoned"
);
assert_eq!(n["kind"], "Node");
assert_eq!(n["apiVersion"], "v1");
assert!(
n["metadata"]["creationTimestamp"].is_string(),
"a created Node carries a creationTimestamp: {n}"
);
}
#[tokio::test]
async fn a_cordon_survives_a_re_registration() {
let n = registered_then_restarted_after(json!({"spec": {"unschedulable": true}})).await;
assert_eq!(
n["spec"]["unschedulable"], true,
"a restart must not uncordon the node: {n}"
);
}
#[tokio::test]
async fn a_custom_taint_survives_a_re_registration() {
let taint = json!({"key": "gpu", "value": "true", "effect": "NoSchedule"});
let n = registered_then_restarted_after(json!({"spec": {"taints": [taint.clone()]}})).await;
assert_eq!(
n["spec"]["taints"],
json!([taint]),
"a restart must not drop the node's taints: {n}"
);
}
#[tokio::test]
async fn a_node_advertises_the_runtime_it_runs_and_a_restart_on_another_backend_overwrites_it()
{
use engenho_scheduler::{NodeCapabilities, Runtime};
let store = boot().await;
let native = NodeCapabilities::running([Runtime::Native]);
let oci = NodeCapabilities::running([Runtime::Oci]);
register_node(&store, NODE, &host().advertising(Some(&native)))
.await
.expect("first boot");
assert_eq!(NodeCapabilities::observe(&node(&store).await), native);
register_node(&store, NODE, &host().advertising(Some(&oci)))
.await
.expect("restart on an OCI backend");
assert_eq!(
NodeCapabilities::observe(&node(&store).await),
oci,
"a runtime the node no longer runs is still advertised"
);
}
#[tokio::test]
async fn a_custom_label_survives_a_re_registration() {
let n =
registered_then_restarted_after(json!({"metadata": {"labels": {"team": "storage"}}}))
.await;
assert_eq!(
n["metadata"]["labels"]["team"], "storage",
"a restart must not drop an operator's label: {n}"
);
assert_eq!(
n["metadata"]["labels"]["kubernetes.io/hostname"], NODE,
"the well-known labels are still there beside it: {n}"
);
}
#[tokio::test]
async fn a_published_ready_condition_survives_a_re_registration() {
let ready = json!({"type": "Ready", "status": "True", "reason": "KubeletReady",
"lastTransitionTime": "2026-09-01T00:00:00Z"});
let n = registered_then_restarted_after(json!({"status": {"conditions": [ready.clone()]}}))
.await;
assert_eq!(
find_ready_condition(&n),
Some(&ready),
"registration must not touch a condition: {n}"
);
}
#[tokio::test]
async fn a_re_registration_refreshes_the_host_owned_fields() {
let store = boot().await;
register_node(&store, NODE, &host())
.await
.expect("first boot");
operator_patch(
&store,
json!({
"metadata": {"labels": {"kubernetes.io/arch": "aarch64"}},
"status": {"capacity": {"cpu": "1", "pods": "110"},
"allocatable": {"cpu": "1"}}
}),
)
.await;
let outcome = register_node(&store, NODE, &host()).await.expect("restart");
assert_eq!(outcome, Registered::Merged);
let n = node(&store).await;
assert_eq!(
n["metadata"]["labels"]["kubernetes.io/arch"],
super::kube_arch(),
"{n}"
);
assert_eq!(n["status"]["capacity"]["cpu"], "8", "{n}");
assert_eq!(n["status"]["allocatable"]["cpu"], "8", "{n}");
assert_eq!(n["status"]["capacity"]["pods"], "110", "{n}");
}
#[tokio::test]
async fn re_registering_an_unchanged_host_writes_nothing() {
let store = boot().await;
register_node(&store, NODE, &host())
.await
.expect("first boot");
let before = node(&store).await["metadata"]["resourceVersion"].clone();
let outcome = register_node(&store, NODE, &host()).await.expect("restart");
assert_eq!(outcome, Registered::AlreadyCurrent);
assert_eq!(
node(&store).await["metadata"]["resourceVersion"],
before,
"an unchanged host must not consume a revision or wake a watcher"
);
}
#[tokio::test]
async fn the_bound_kubelet_endpoint_is_published_and_a_rebind_moves_it() {
use super::KubeletEndpointFields;
let store = boot().await;
register_node(&store, NODE, &host()).await.expect("boot");
let bound: std::net::SocketAddr = "10.0.0.7:10251".parse().expect("addr");
let reachable = host().reachable_at(KubeletEndpointFields::derive(NODE, bound, ""));
assert_eq!(
register_node(&store, NODE, &reachable)
.await
.expect("publish"),
Registered::Merged
);
let n = node(&store).await;
assert_eq!(
n["status"]["addresses"],
json!([{"address": "10.0.0.7", "type": "InternalIP"},
{"address": NODE, "type": "Hostname"}]),
"{n}"
);
assert_eq!(
n["status"]["daemonEndpoints"]["kubeletEndpoint"]["Port"],
10251
);
assert_eq!(
engenho_apiserver::kubelet_authority_of_node(&n).as_deref(),
Some("10.0.0.7:10251"),
"the apiserver's resolver reads what registration wrote"
);
assert_eq!(
register_node(&store, NODE, &reachable)
.await
.expect("again"),
Registered::AlreadyCurrent
);
let moved: std::net::SocketAddr = "10.0.0.8:10250".parse().expect("addr");
register_node(
&store,
NODE,
&host().reachable_at(KubeletEndpointFields::derive(NODE, moved, "")),
)
.await
.expect("rebind");
let n = node(&store).await;
assert_eq!(
engenho_apiserver::kubelet_authority_of_node(&n).as_deref(),
Some("10.0.0.8:10250")
);
assert_eq!(n["status"]["addresses"].as_array().map(Vec::len), Some(2));
}
#[test]
fn an_unspecified_bind_publishes_the_advertised_ip_or_no_internal_ip() {
use super::KubeletEndpointFields;
let any: std::net::SocketAddr = "0.0.0.0:10250".parse().expect("addr");
let body = |advertise: &str| {
host()
.reachable_at(KubeletEndpointFields::derive(NODE, any, advertise))
.merge_patch(NODE)
.expect("serializes")["status"]["addresses"]
.clone()
};
assert_eq!(
body("100.64.0.9:6443"),
json!([{"address": "100.64.0.9", "type": "InternalIP"},
{"address": NODE, "type": "Hostname"}])
);
assert_eq!(
body("rio.pleme.internal"),
json!([{"address": NODE, "type": "Hostname"}]),
"a name is not an InternalIP; 0.0.0.0 is not an address"
);
let v6: std::net::SocketAddr = "[fd00::7]:10250".parse().expect("addr");
let n = host()
.reachable_at(KubeletEndpointFields::derive(NODE, v6, ""))
.merge_patch(NODE)
.expect("serializes");
assert_eq!(
engenho_apiserver::kubelet_authority_of_node(&n).as_deref(),
Some("[fd00::7]:10250")
);
}
#[test]
fn the_merge_body_names_only_host_owned_fields() {
let patch = host().merge_patch(NODE).expect("serializes");
let top: Vec<&str> = patch
.as_object()
.expect("an object")
.keys()
.map(String::as_str)
.collect();
assert_eq!(
top,
["metadata", "status"],
"no spec, no type meta: {patch}"
);
let meta: Vec<&str> = patch["metadata"]
.as_object()
.expect("metadata")
.keys()
.map(String::as_str)
.collect();
assert_eq!(meta, ["labels"], "{patch}");
let status: Vec<&str> = patch["status"]
.as_object()
.expect("status")
.keys()
.map(String::as_str)
.collect();
assert_eq!(
status,
["allocatable", "capacity"],
"no conditions: {patch}"
);
}
}