use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use arc_swap::ArcSwap;
use engenho_apiserver::{
ApiServer, ChainAuthenticator, ClientMaterial, PkiFile, RbacAuthorizer, RouterHandlerSink,
RouterState, SanEntry, ServerSanInputs, StoreRbacEnv, TlsMaterial, client_verifier,
handlers_from_catalog_with_admission, issue_admin_client_material, issue_server_material,
load_or_generate_ca, metrics::log_would_reject,
};
use engenho_config::leaf::{flatten, nest};
use engenho_config::mutability::{self, Mutability, RespawnSet};
use engenho_config::{
EngenhoConfig, KubeconfigVisibility, KubeletBackendKind as CfgBackendKind, ResolvedDatapath,
};
use engenho_controllers::{
Controller, ControllerError, ControllerType, CrdController, CronJobController,
DaemonSetController, DeclaresReads, DeploymentController, DynamicHandlerSink,
EndpointsController, FakeRouter, GcController, Heartbeat, IptablesRouter, IpvsRouter,
JobController, NamespaceController, PodDisruptionBudgetController, PvBinderController,
PvcProtectionController, Reads, ReconcileOutcome, ReplicaSetController, ServiceRouter,
ServiceRoutingController, StatefulSetController, WallClock, WatchDriver, WatchDriverConfig,
admission::{AdmissionChain, AdmissionMode, AdmissionWebhook},
cluster_ip::{ClusterIpDefaultingWebhook, StoreServiceIpSource},
event_recorder::EventSink,
gvk,
};
use engenho_kube_client::{emit_kubeconfig, emit_kubeconfig_with_admin};
use engenho_kubelet::config_bridge::KubeletBackendKind;
use engenho_kubelet::{ContainerRuntime, Kubelet, LogOptions, make_container_runtime_on_node};
use engenho_scheduler::{
ConfiguredScheduler, NodeCapabilities, Runtime as SchedRuntime, Scheduler,
};
use engenho_store::{
InProcessRouter, ResourceKey, StoreMesh,
command::{Reason, ResourceCommand},
default_config,
};
use engenho_substrate::WouldRejectLedger;
use engenho_types::generated_v1_34::core_v1::Namespace;
use engenho_types::generated_v1_34::rbac_v1::{
ClusterRole, ClusterRoleBinding, PolicyRule, RoleRef, Subject,
};
use engenho_types::generated_v1_34::types::{NamespaceSpec, NamespaceStatus};
use engenho_types::kind::GroupVersionKind;
use tracing::{error, info, warn};
use crate::boot::{BootKind, BootPhase, BootRecorder};
use crate::boot_config::{ApiserverTls, BootConfig};
use crate::child::{
Child, ChildHandle, ChildTask, Children, ChildrenFollowed, DeadChild, Driver, EnableSwitch,
Listener, Respawn, RespawnError, Respawned, TickLoop, Wiring,
};
use crate::error::{RuntimeError, ShutdownStage, StrongCounts};
use crate::health::{Health, Row, Tallied, Tally, Windows};
use crate::layout::Area;
use crate::node_lease::NodeLease;
use crate::node_registration::{HostOwned, register_node};
use crate::panics::PanicCounter;
use crate::publish::{KubeconfigTarget, PublishRecord, SkipReason};
use crate::rebind::serve_rebinding;
use crate::release::{BootFailed, BootUnwind, StoreReleased};
use crate::runtime_health::RuntimeHealthSource;
pub struct Runtime {
config: EngenhoConfig,
store: Arc<StoreMesh>,
apiserver: ApiServer,
children: Children,
parts: Parts,
panics: PanicCounter,
health: Arc<Health>,
would_reject: Arc<WouldRejectLedger>,
boot_kind: BootKind,
publish: Vec<PublishRecord>,
publisher: Publisher,
server_sans: Option<Vec<String>>,
}
impl Runtime {
pub async fn start(config: EngenhoConfig) -> Result<Self, RuntimeError> {
Self::boot(config)
.await
.map_err(BootFailed::into_logged_error)
}
pub async fn start_with_backend(
config: EngenhoConfig,
backend: Arc<dyn ContainerRuntime>,
) -> Result<Self, RuntimeError> {
Self::boot_with_backend(config, backend)
.await
.map_err(BootFailed::into_logged_error)
}
pub async fn boot(config: EngenhoConfig) -> Result<Self, BootFailed> {
Self::boot_recorded(config, &mut BootRecorder::silent()).await
}
pub async fn boot_recorded(
config: EngenhoConfig,
rec: &mut BootRecorder,
) -> Result<Self, BootFailed> {
rec.enter(BootPhase::ResolveConfig)
.map_err(|e| rec.failed(e))?;
Self::boot_resolved(config, None, rec).await
}
pub async fn boot_with_backend(
config: EngenhoConfig,
backend: Arc<dyn ContainerRuntime>,
) -> Result<Self, BootFailed> {
let mut rec = BootRecorder::silent();
rec.enter(BootPhase::ResolveConfig)
.map_err(|e| rec.failed(e))?;
Self::boot_resolved(config, Some(backend), &mut rec).await
}
pub(crate) async fn boot_resolved(
config: EngenhoConfig,
backend: Option<Arc<dyn ContainerRuntime>>,
rec: &mut BootRecorder,
) -> Result<Self, BootFailed> {
rec.enter(BootPhase::ReadBootConfig)
.map_err(|e| rec.failed(e))?;
let boot = BootConfig::read(&config).map_err(|e| rec.failed(e.into()))?;
rec.enter(BootPhase::PreflightBackend)
.map_err(|e| rec.failed(e))?;
let advertised = backend
.is_none()
.then(|| advertised_capabilities(boot.kubelet_backend));
let backend = if let Some(backend) = backend {
backend
} else {
preflight_backend(&boot).map_err(|e| rec.failed(e))?;
build_backend(&boot).map_err(|e| rec.failed(e))?
};
Self::start_inner(config, boot, backend, advertised, rec).await
}
async fn start_inner(
config: EngenhoConfig,
boot: BootConfig,
backend: Arc<dyn ContainerRuntime>,
advertised: Option<NodeCapabilities>,
rec: &mut BootRecorder,
) -> Result<Self, BootFailed> {
let panics = PanicCounter::install();
let would_reject = Arc::new(WouldRejectLedger::new(log_would_reject));
rec.enter(BootPhase::ValidateConfig)
.map_err(|e| rec.failed(e))?;
config.validate().map_err(|e| rec.failed(e.into()))?;
for not_run in &boot.not_run {
info!(field = not_run.field(), why = %not_run, "config read, not run");
}
rec.enter(BootPhase::OpenStore).map_err(|e| rec.failed(e))?;
let (store, boot_kind) = boot_store(&boot).await.map_err(|e| rec.failed(e))?;
rec.observe(boot_kind);
match Self::assemble(
&boot,
&backend,
advertised.as_ref(),
&store,
panics,
&would_reject,
rec,
)
.await
{
Ok(Assembled {
apiserver,
children,
parts,
health,
publish,
publisher,
server_sans,
}) => Ok(Self {
config,
store,
apiserver,
children,
parts,
panics,
health,
would_reject,
boot_kind,
publish,
publisher,
server_sans,
}),
Err(error) => {
let phase = rec.current();
let unwind = unwind_failed_boot(store).await;
Err(BootFailed {
error,
phase,
unwind,
})
}
}
}
async fn assemble(
boot: &BootConfig,
backend: &Arc<dyn ContainerRuntime>,
advertised: Option<&NodeCapabilities>,
store: &Arc<StoreMesh>,
panics: PanicCounter,
would_reject: &Arc<WouldRejectLedger>,
rec: &mut BootRecorder,
) -> Result<Assembled, RuntimeError> {
rec.enter(BootPhase::AwaitLeadership)?;
await_leadership(store, boot, rec).await?;
rec.enter(BootPhase::BuildScheduler)?;
let scheduler = Scheduler::from_config(store.clone(), &boot.scheduler)?;
let windows = boot.windows(scheduler.fallback_interval());
let health = Arc::new(Health::new(store, windows, panics));
rec.enter(BootPhase::RegisterNode)?;
let node_name = &boot.node_name;
let host = HostOwned::measured(node_name).advertising(advertised);
register_node(store, node_name, &host).await?;
rec.enter(BootPhase::SeedCluster)?;
seed_system_namespaces(store).await?;
seed_kubernetes_service(store, boot).await?;
seed_bootstrap_rbac(store).await?;
seed_default_storage_class(store).await?;
seed_snapshot_crds(store).await?;
rec.enter(BootPhase::IssuePki)?;
let listen_addr: SocketAddr =
boot.listen_addr
.parse()
.map_err(|source| RuntimeError::ListenAddr {
addr: boot.listen_addr.clone(),
source,
})?;
let IssuedPki {
tls,
ca_cert_pem,
admin: admin_material,
} = issue_pki(boot, listen_addr)?;
let server_sans = tls.as_ref().map(|t| t.sans.clone());
rec.enter(BootPhase::LoadAdminToken)?;
let admin_token: Option<String> = Some(load_or_generate_admin_token(&boot.data_dir)?);
if admin_token.is_some() {
info!("bootstrap admin bearer token available at data_dir/pki/admin.token");
}
rec.enter(BootPhase::BuildAuth)?;
let cluster_ip_hook: Arc<dyn AdmissionWebhook> = Arc::new(ClusterIpDefaultingWebhook::new(
boot.service_cidr.clone(),
Arc::new(StoreServiceIpSource::new(store.clone())),
));
let admission = Arc::new(AdmissionChain::new(
vec![cluster_ip_hook],
AdmissionMode::FailClosed,
));
let authenticator: Arc<ChainAuthenticator> =
Arc::new(build_authenticator(&boot.data_dir, admin_token));
let authorizer: Arc<dyn engenho_apiserver::Authorizer> =
Arc::new(RbacAuthorizer::new(StoreRbacEnv::new(store.clone())));
let router_state = RouterState::new(handlers_from_catalog_with_admission(
store.clone(),
admission.clone(),
))
.with_authenticator(authenticator)
.with_authorizer(authorizer)
.with_would_reject_ledger(Arc::clone(would_reject))
.with_liveness_source(health.clone())
.with_metrics_source(health.clone());
let router_state = match build_token_issuer(&boot.data_dir) {
Some(issuer) => router_state.with_token_issuer(issuer),
None => router_state,
};
let handler_sink: Arc<dyn DynamicHandlerSink> =
RouterHandlerSink::new(store.clone(), admission.clone(), router_state.clone())
.into_dyn();
let router_state_for_logs = router_state.clone();
rec.enter(BootPhase::BindApiserver)?;
let apiserver = ApiServer::start_with_state(listen_addr, router_state, tls).await?;
let bound_addr = apiserver.local_addr();
info!(addr = %bound_addr, tls = boot.tls.is_enabled(), "apiserver bound");
rec.enter_committed(BootPhase::PublishKubeconfigs);
let publisher = Publisher::new(bound_addr, ca_cert_pem, admin_material);
let publish = match publisher.publish(boot) {
Ok(records) => records,
Err(err) => {
let _ = apiserver.shutdown().await;
return Err(err);
}
};
rec.enter_committed(BootPhase::SpawnChildren);
let parts = Parts::assemble(
boot,
store,
backend,
scheduler,
&handler_sink,
&router_state_for_logs,
windows,
);
let children = spawn_children(&parts);
info!(count = children.len(), "children spawned");
rec.enter_committed(BootPhase::AdoptHealth);
health.publish(children.rows());
if let Some(pod_handler) = build_pod_log_handler(store, &admission, &parts) {
router_state_for_logs.register(pod_handler);
info!("registered Pod /log handler (in-process kubelet log reader)");
}
for kind in [
engenho_apiserver::resource_metrics::MetricsKind::Pod,
engenho_apiserver::resource_metrics::MetricsKind::Node,
] {
router_state_for_logs.register(Arc::new(
engenho_apiserver::resource_metrics::MetricsHandler::new(
kind,
parts.metrics_reader(),
store.clone(),
),
));
}
Ok(Assembled {
apiserver,
children,
parts,
health,
publish,
publisher,
server_sans,
})
}
#[must_use]
pub fn health(&self) -> &Arc<Health> {
&self.health
}
#[must_use]
pub fn panics(&self) -> PanicCounter {
self.panics
}
#[must_use]
pub fn would_reject(&self) -> &Arc<WouldRejectLedger> {
&self.would_reject
}
#[must_use]
pub fn children(&self) -> &Children {
&self.children
}
pub async fn next_dead_child(&mut self) -> DeadChild {
self.children.next_dead().await
}
#[must_use]
pub fn local_addr(&self) -> SocketAddr {
self.apiserver.local_addr()
}
#[must_use]
pub fn store(&self) -> Arc<StoreMesh> {
self.store.clone()
}
#[must_use]
pub fn config(&self) -> &EngenhoConfig {
&self.config
}
#[must_use]
pub const fn boot_kind(&self) -> BootKind {
self.boot_kind
}
#[must_use]
pub fn publish_records(&self) -> &[PublishRecord] {
&self.publish
}
#[must_use]
pub fn server_sans(&self) -> Option<&[String]> {
self.server_sans.as_deref()
}
pub async fn respawn(&mut self, child: Child) -> Result<Respawned, RespawnError> {
let dependents: &[Child] = match child.respawn() {
Respawn::RuntimeRestartOnly => return Err(RespawnError::RuntimeRestartOnly(child)),
Respawn::Rebuild => &[],
Respawn::RebuildWith(dependents) => dependents,
};
if self.children.get(child).is_none() {
return Err(RespawnError::NotSpawned(child));
}
let set: Vec<Child> = std::iter::once(child)
.chain(
dependents
.iter()
.copied()
.filter(|d| self.children.get(*d).is_some()),
)
.collect();
let renewal = self.parts.renew(child).map_err(|e| RespawnError::Build {
child,
reason: e.to_string(),
})?;
let mut died = self.stop_all(set.iter().rev().copied()).await;
died.retain(|dead| !set.contains(&dead.child));
self.parts.install(renewal);
let rebuilt = self.spawn_all(&set);
let generation = self.children.get(child).map_or(0, ChildHandle::generation);
self.health.publish(self.children.rows());
info!(%child, generation, ?rebuilt, "child respawned");
Ok(Respawned {
child,
generation,
rebuilt,
died,
})
}
async fn stop_all(&mut self, children: impl Iterator<Item = Child>) -> Vec<DeadChild> {
let mut ended = Vec::new();
for child in children {
ended.extend(self.children.stop_one(child).await);
}
ended
}
fn spawn_all(&mut self, children: &[Child]) -> Vec<Child> {
let mut spawned = Vec::new();
for &child in children {
let Some(task) = self.parts.task(child, &self.children) else {
continue;
};
match self.children.spawn_one(child, task) {
Ok(_) => spawned.push(child),
Err(running) => error!(%running, "not spawned again: it was not stopped"),
}
}
spawned
}
async fn follow(&mut self, next: BootConfig) -> ChildrenFollowed {
let before = &self.parts.boot;
let mut stop = Vec::new();
let mut start = Vec::new();
for child in Child::all() {
let (was, now) = (child.enabled(before), child.enabled(&next));
let rebuilt = was && now && child.rebuilt_between(before, &next);
if was && (!now || rebuilt) {
stop.push(child);
}
if now && (!was || rebuilt) {
start.push(child);
}
}
let mut died = self.stop_all(stop.iter().rev().copied()).await;
let touched: Vec<Child> = stop.iter().chain(&start).copied().collect();
died.retain(|dead| !touched.contains(&dead.child));
for &child in stop.iter().filter(|c| !start.contains(c)) {
if let Err(running) = self.children.forget(child) {
error!(%running, "a disabled child was not stopped");
}
}
self.parts.boot = next;
let spawned = self.spawn_all(&start);
if !touched.is_empty() {
self.health.publish(self.children.rows());
info!(stopped = ?stop, spawned = ?spawned, "children follow the configuration");
}
let rebuilt_or_new: Vec<Child> =
spawned.into_iter().filter(|c| !stop.contains(c)).collect();
let mut changed = stop;
changed.extend(rebuilt_or_new);
ChildrenFollowed { changed, died }
}
fn takes_in_place(spec: &mutability::LeafSpec) -> bool {
match spec.mutability {
Mutability::Inert { .. }
| Mutability::Live { .. }
| Mutability::Respawn {
set: RespawnSet::NodeLocalListeners,
} => true,
Mutability::Respawn {
set: RespawnSet::EnabledDrivers,
} => EnableSwitch::of_leaf(spec.path.as_str()).is_some_and(EnableSwitch::toggles_live),
Mutability::NextBoot
| Mutability::RestartRuntime
| Mutability::NotOverridable { .. } => false,
}
}
pub(crate) async fn adopt(
&mut self,
effective: &EngenhoConfig,
) -> Result<Adopted, RuntimeError> {
let json = |c: &EngenhoConfig| serde_json::to_value(c).unwrap_or_default();
let mut current = flatten(&json(&self.config));
let target = flatten(&json(effective));
let (mut moved, mut republish) = (false, false);
for spec in mutability::leaves()
.iter()
.filter(|spec| Self::takes_in_place(spec))
{
let want = target.get(&spec.path);
if current.get(&spec.path) == want {
continue;
}
match want {
Some(value) => current.insert(spec.path.clone(), value.clone()),
None => current.remove(&spec.path),
};
moved = true;
republish |= matches!(spec.mutability, Mutability::Live { .. });
}
if !moved {
return Ok(Adopted::default());
}
let adopted: EngenhoConfig = serde_json::from_value(nest(¤t)).map_err(|e| {
engenho_config::ConfigError::Parse(format!("adopting in-place leaves: {e}"))
})?;
let boot = BootConfig::read(&adopted)?;
let published = if republish {
Some(self.publisher.publish(&boot)?)
} else {
None
};
self.config = adopted;
if let Some(records) = &published {
self.publish.clone_from(records);
}
let children = self.follow(boot).await;
Ok(Adopted {
published,
children,
})
}
pub(crate) fn republish(&mut self) -> Result<Vec<PublishRecord>, RuntimeError> {
let boot = BootConfig::read(&self.config)?;
self.publish = self.publisher.publish(&boot)?;
Ok(self.publish.clone())
}
pub async fn store_position(&self) -> (u64, bool) {
let revision = self.store.current_revision().await;
(revision.get(), self.store.is_leader().await)
}
pub async fn shutdown(self) -> Result<StoreReleased, RuntimeError> {
let Self {
mut children,
parts,
apiserver,
store,
health,
..
} = self;
health.begin_drain();
children.stop().await;
drop(parts);
let drivers_awaited = Arc::strong_count(&store);
apiserver.shutdown().await?;
let apiserver_stopped = Arc::strong_count(&store);
let quiesced = store.quiesce().await;
if quiesced.any_panicked() {
error!(
rpc_pump = ?quiesced.rpc_pump,
bookmark_ticker = ?quiesced.bookmark_ticker,
"a store background task had panicked before shutdown stopped it"
);
}
let store_quiesced = Arc::strong_count(&store);
let flushed = store.flush().await?;
info!(?flushed, "store flushed at shutdown");
let counts = StrongCounts {
drivers_awaited,
apiserver_stopped,
store_quiesced,
store_flushed: Arc::strong_count(&store),
};
let store = Arc::try_unwrap(store).map_err(|shared| {
let strong_count = Arc::strong_count(&shared);
let after = counts.blame();
error!(
%after,
strong_count,
after_drivers_awaited = counts.after(ShutdownStage::DriversAwaited),
after_apiserver_stopped = counts.after(ShutdownStage::ApiserverStopped),
after_store_quiesced = counts.after(ShutdownStage::StoreQuiesced),
after_store_flushed = counts.after(ShutdownStage::StoreFlushed),
"store still shared at shutdown (already flushed); strong count after each stage"
);
RuntimeError::StoreStillShared {
strong_count,
after,
}
})?;
store.terminate().await?;
Ok(StoreReleased::minted())
}
}
#[derive(Debug, Default)]
pub(crate) struct Adopted {
pub(crate) published: Option<Vec<PublishRecord>>,
pub(crate) children: ChildrenFollowed,
}
struct Assembled {
apiserver: ApiServer,
children: Children,
parts: Parts,
health: Arc<Health>,
publish: Vec<PublishRecord>,
publisher: Publisher,
server_sans: Option<Vec<String>>,
}
struct Publisher {
bound_addr: SocketAddr,
ca_pem: Option<String>,
admin: Option<ClientMaterial>,
}
impl Publisher {
const fn new(
bound_addr: SocketAddr,
ca_pem: Option<String>,
admin: Option<ClientMaterial>,
) -> Self {
Self {
bound_addr,
ca_pem,
admin,
}
}
fn publish(&self, boot: &BootConfig) -> Result<Vec<PublishRecord>, RuntimeError> {
self.ca_pem.as_deref().map_or_else(
|| Ok(PublishRecord::all_skipped(SkipReason::TlsDisabled)),
|ca_pem| write_boot_kubeconfig(boot, self.bound_addr, ca_pem, self.admin.as_ref()),
)
}
}
async fn await_leadership(
store: &StoreMesh,
boot: &BootConfig,
rec: &BootRecorder,
) -> Result<(), RuntimeError> {
let timeout_s = boot.leadership_timeout_seconds;
let led = tokio::select! {
led = store.wait_for_leadership(Duration::from_secs(u64::from(timeout_s))) => led,
() = rec.cancelled() => {
return Err(RuntimeError::BootCancelled {
phase: BootPhase::AwaitLeadership,
});
}
};
if !led {
return Err(RuntimeError::LeadershipTimeout { seconds: timeout_s });
}
info!(node = %boot.node_name, "store reached leadership");
Ok(())
}
#[derive(Default)]
struct IssuedPki {
tls: Option<TlsMaterial>,
ca_cert_pem: Option<String>,
admin: Option<ClientMaterial>,
}
fn issue_pki(boot: &BootConfig, listen_addr: SocketAddr) -> Result<IssuedPki, RuntimeError> {
let ApiserverTls::SelfIssued { extra_sans } = &boot.tls else {
return Ok(IssuedPki::default());
};
let ca = load_or_generate_ca(&boot.data_dir).map_err(|e| RuntimeError::Server(e.into()))?;
if ca.is_publicly_derivable() && !is_loopback_only(listen_addr) {
return Err(RuntimeError::PublicCaOnReachableAddress {
listen_addr: boot.listen_addr.clone(),
pki_dir: PkiFile::dir(&boot.data_dir).display().to_string(),
});
}
let listen_ip = san_listen_ip(listen_addr);
let extra_sans = server_sans(extra_sans, &boot.advertise_address)?;
let material = issue_server_material(
&ca,
&ServerSanInputs {
node_name: &boot.node_name,
listen_ip,
extra_sans: &extra_sans,
},
)
.map_err(|e| RuntimeError::Server(e.into()))?;
let verifier = client_verifier(&ca).map_err(|e| RuntimeError::Server(e.into()))?;
let admin = issue_admin_client_material(&ca).map_err(|e| RuntimeError::Server(e.into()))?;
persist_admin_material(&boot.data_dir, &admin)?;
Ok(IssuedPki {
tls: Some(material.with_client_verifier(verifier)),
ca_cert_pem: Some(ca.cert_pem().to_string()),
admin: Some(admin),
})
}
async fn unwind_failed_boot(store: Arc<StoreMesh>) -> BootUnwind {
let quiesced = store.quiesce().await;
if quiesced.any_panicked() {
error!(
rpc_pump = ?quiesced.rpc_pump,
bookmark_ticker = ?quiesced.bookmark_ticker,
"a store background task had panicked before the failed boot unwound"
);
}
match Arc::try_unwrap(store) {
Ok(mesh) => match mesh.terminate().await {
Ok(()) => {
info!("failed boot unwound: store terminated and released");
BootUnwind::Released(StoreReleased::minted())
}
Err(err) => {
error!(error = %err, "failed boot unwound, but terminating the store failed");
BootUnwind::TerminateFailed(Box::new(err.into()))
}
},
Err(shared) => {
let strong_count = Arc::strong_count(&shared);
error!(
strong_count,
"failed boot could not release its store: something still holds it"
);
BootUnwind::StillShared { strong_count }
}
}
}
impl BootFailed {
fn into_logged_error(self) -> RuntimeError {
match &self.unwind {
BootUnwind::NeverOpened | BootUnwind::Released(_) => {}
BootUnwind::StillShared { strong_count } => error!(
strong_count,
error = %self.error,
"boot failed and its store is still held"
),
BootUnwind::TerminateFailed(err) => error!(
terminate_error = %err,
error = %self.error,
"boot failed and its store could not be terminated"
),
}
self.error
}
}
struct MeshEventStore {
store: Arc<engenho_store::StoreMesh>,
}
#[async_trait::async_trait]
impl engenho_controllers::event_recorder::EventStore for MeshEventStore {
async fn put_event(
&self,
key: engenho_store::ResourceKey,
value: serde_json::Value,
) -> Result<(), String> {
self.store
.propose(engenho_store::command::ResourceCommand::Put {
key,
value,
expected: None,
reason: engenho_store::command::Reason::Controller,
})
.await
.map(|_| ())
.map_err(|e| e.to_string())
}
}
struct KubeletLogReader {
kubelet: KubeletSlot,
}
struct WeakKubeletApi {
kubelet: std::sync::Weak<Kubelet>,
}
impl WeakKubeletApi {
fn get(&self) -> Result<Arc<Kubelet>, String> {
self.kubelet
.upgrade()
.ok_or_else(|| "kubelet is shutting down on this node".to_string())
}
}
#[async_trait::async_trait]
impl engenho_kubelet::server::KubeletApi for WeakKubeletApi {
async fn container_logs(
&self,
namespace: &str,
pod: &str,
container: &str,
opts: &LogOptions,
) -> Result<String, String> {
engenho_kubelet::server::KubeletApi::container_logs(
self.get()?.as_ref(),
namespace,
pod,
container,
opts,
)
.await
}
async fn pods(&self) -> serde_json::Value {
match self.get() {
Ok(k) => engenho_kubelet::server::KubeletApi::pods(k.as_ref()).await,
Err(_) => serde_json::json!({ "kind": "PodList", "apiVersion": "v1", "items": [] }),
}
}
async fn running_pods(&self) -> serde_json::Value {
match self.get() {
Ok(k) => engenho_kubelet::server::KubeletApi::running_pods(k.as_ref()).await,
Err(_) => serde_json::json!({ "kind": "PodList", "apiVersion": "v1", "items": [] }),
}
}
async fn exec(
&self,
namespace: &str,
pod: &str,
container: &str,
argv: &[String],
) -> Result<engenho_kubelet::backend::ExecOutcome, String> {
engenho_kubelet::server::KubeletApi::exec(
self.get()?.as_ref(),
namespace,
pod,
container,
argv,
)
.await
}
async fn exec_stream(
&self,
namespace: &str,
pod: &str,
container: &str,
req: &engenho_kubelet::stream::StreamRequest,
) -> Result<engenho_kubelet::stream::StreamSession, String> {
engenho_kubelet::server::KubeletApi::exec_stream(
self.get()?.as_ref(),
namespace,
pod,
container,
req,
)
.await
}
async fn attach(
&self,
namespace: &str,
pod: &str,
container: &str,
req: &engenho_kubelet::stream::StreamRequest,
) -> Result<engenho_kubelet::stream::StreamSession, String> {
engenho_kubelet::server::KubeletApi::attach(
self.get()?.as_ref(),
namespace,
pod,
container,
req,
)
.await
}
async fn pod_address(&self, namespace: &str, pod: &str) -> Result<std::net::IpAddr, String> {
engenho_kubelet::server::KubeletApi::pod_address(self.get()?.as_ref(), namespace, pod).await
}
}
#[async_trait::async_trait]
impl engenho_apiserver::PodLogReader for KubeletLogReader {
async fn read_pod_logs(
&self,
namespace: &str,
name: &str,
query: &engenho_apiserver::LogQuery,
) -> Result<String, engenho_apiserver::ApiError> {
Ok(self.read_pod_log_chunk(namespace, name, query).await?.text)
}
async fn read_pod_log_chunk(
&self,
namespace: &str,
name: &str,
query: &engenho_apiserver::LogQuery,
) -> Result<engenho_apiserver::LogChunk, engenho_apiserver::ApiError> {
let opts = LogOptions {
tail: query.tail_lines,
timestamps: query.timestamps,
from_byte: query.from_byte,
since: query.since,
};
self.kubelet
.load_full()
.container_log_read(namespace, name, query.container.as_deref(), &opts)
.await
.map(|read| engenho_apiserver::LogChunk {
text: read.text,
cursor: read.cursor,
})
.map_err(|e| match e.kind() {
"invalid_pod" => engenho_apiserver::ApiError::NotFound(format!(
"could not get logs for pod {namespace}/{name}: {e}"
)),
_ => engenho_apiserver::ApiError::Internal(format!(
"log read failed for pod {namespace}/{name}: {e}"
)),
})
}
}
struct KubeletMetricsReader {
kubelet: KubeletSlot,
}
fn metrics_window(
u: &engenho_kubelet::usage::Usage,
) -> engenho_apiserver::resource_metrics::Window {
engenho_apiserver::resource_metrics::Window {
timestamp: u.timestamp,
length: u.window,
}
}
#[async_trait::async_trait]
impl engenho_apiserver::resource_metrics::ResourceMetricsReader for KubeletMetricsReader {
async fn pods(&self) -> Vec<engenho_apiserver::resource_metrics::PodSample> {
use engenho_apiserver::resource_metrics::{ContainerSample, PodSample};
let kubelet = self.kubelet.load_full();
kubelet
.pod_usage()
.await
.into_iter()
.map(|p| {
let mut window = None;
let mut containers = Vec::new();
let mut unmeasurable = None;
for c in p.containers {
match c.usage {
Ok(u) => {
window.get_or_insert_with(|| metrics_window(&u));
containers.push(ContainerSample {
name: c.name,
cpu_nanocores: u.cpu_nanocores,
memory_bytes: u.memory_working_set_bytes,
});
}
Err(why) => {
unmeasurable
.get_or_insert_with(|| [c.name.as_str(), ": ", &why].concat());
}
}
}
let usage = match (unmeasurable, window) {
(Some(why), _) => Err(why),
(None, Some(w)) => Ok((w, containers)),
(None, None) => Err("the pod has no running container".to_owned()),
};
PodSample {
namespace: p.namespace,
name: p.name,
usage,
}
})
.collect()
}
async fn nodes(&self) -> Vec<engenho_apiserver::resource_metrics::NodeSample> {
let kubelet = self.kubelet.load_full();
let usage = kubelet.node_usage().await.map(|u| {
(
metrics_window(&u),
u.cpu_nanocores,
u.memory_working_set_bytes,
)
});
vec![engenho_apiserver::resource_metrics::NodeSample {
name: kubelet.node_name().to_owned(),
usage,
}]
}
}
fn build_pod_log_handler(
store: &Arc<StoreMesh>,
admission: &Arc<AdmissionChain>,
parts: &Parts,
) -> Option<Arc<dyn engenho_apiserver::ResourceHandler>> {
let pod_descriptor = engenho_types::generated_v1_34::RESOURCE_CATALOG
.iter()
.find(|d| d.kind == "Pod" && d.group.is_empty())?;
let handler =
engenho_apiserver::StoreBackedHandler::from_descriptor(store.clone(), pod_descriptor)
.with_admission(admission.clone())
.with_log_reader(parts.log_reader())
.with_stream_proxy(parts.stream_proxy());
Some(Arc::new(handler))
}
pub(crate) type KubeletEndpoint = Arc<arc_swap::ArcSwapOption<SocketAddr>>;
struct KubeletStreamProxy {
store: Arc<StoreMesh>,
local_node: String,
local_kubelet: KubeletEndpoint,
}
impl KubeletStreamProxy {
async fn authority(&self, node: &str) -> Result<String, engenho_apiserver::ApiError> {
if node == self.local_node {
let Some(bound) = self.local_kubelet.load_full() else {
return Err(engenho_apiserver::ApiError::ServiceUnavailable(format!(
"the kubelet listener on node {node} is not bound"
)));
};
let ip = match bound.ip() {
std::net::IpAddr::V4(v4) if v4.is_unspecified() => {
std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
}
std::net::IpAddr::V6(v6) if v6.is_unspecified() => {
std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST)
}
ip => ip,
};
return Ok(SocketAddr::new(ip, bound.port()).to_string());
}
let Some(live) = self
.store
.get(&crate::node_registration::node_key(node))
.await
else {
return Err(engenho_apiserver::ApiError::NotFound(format!(
"node {node} not found"
)));
};
engenho_apiserver::kubelet_authority_of_node(&live).ok_or_else(|| {
engenho_apiserver::ApiError::ServiceUnavailable(format!(
"node {node} publishes no kubelet address (status.addresses)"
))
})
}
}
#[async_trait::async_trait]
impl engenho_apiserver::PodStreamProxy for KubeletStreamProxy {
async fn open(
&self,
target: &engenho_apiserver::PodStreamTarget,
) -> Result<engenho_apiserver::PodStreamUpstream, engenho_apiserver::ApiError> {
let Some(node) = target.node_name.as_deref() else {
return Err(engenho_apiserver::ApiError::BadRequest(format!(
"pod {}/{} does not have a host assigned",
target.namespace, target.pod
)));
};
let authority = self.authority(node).await?;
engenho_apiserver::dial_kubelet(&authority, target).await
}
}
fn itoa_i32(n: i32) -> String {
let mut out = String::new();
let mut v = i64::from(n);
if v < 0 {
out.push('-');
v = -v;
}
let mut digits = Vec::new();
if v == 0 {
digits.push(b'0');
}
while v > 0 {
digits.push(b'0' + u8::try_from(v % 10).unwrap_or(0));
v /= 10;
}
digits.reverse();
out.push_str(&String::from_utf8(digits).unwrap_or_default());
out
}
const STDERR_TAIL_MAX: usize = 400;
fn stderr_tail(stderr: &[u8]) -> String {
let text = String::from_utf8_lossy(stderr);
let Some(line) = text
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.next_back()
else {
return String::new();
};
let mut out = String::from(": ");
if line.len() > STDERR_TAIL_MAX {
let mut end = STDERR_TAIL_MAX;
while end > 0 && !line.is_char_boundary(end) {
end -= 1;
}
out.push_str(&line[..end]);
out.push('…');
} else {
out.push_str(line);
}
out
}
fn preflight_backend(boot: &BootConfig) -> Result<(), RuntimeError> {
if let Some(refused) = engenho_kubelet::config_bridge::construction_refusal(
kubelet_backend_kind(boot.kubelet_backend),
) {
return Err(RuntimeError::BackendRefused(refused));
}
match boot.kubelet_backend {
CfgBackendKind::Fake => return Ok(()),
CfgBackendKind::Native => return Ok(()),
CfgBackendKind::Cri => return Ok(()),
CfgBackendKind::PodmanApi | CfgBackendKind::Podman => {}
}
let binary = boot
.podman_binary
.clone()
.unwrap_or_else(|| "podman".to_string());
match std::process::Command::new(&binary).arg("info").output() {
Ok(out) if out.status.success() => {
info!(backend = "podman", %binary, "container runtime resolved");
Ok(())
}
Ok(out) => Err(RuntimeError::ContainerRuntimeUnavailable {
backend: "podman".to_string(),
binary,
source: std::io::Error::other(
match out.status.code() {
Some(c) => ["`podman info` exited ", itoa_i32(c).as_str()].concat(),
None => "`podman info` was terminated by a signal".to_string(),
} + stderr_tail(&out.stderr).as_str(),
),
}),
Err(source) => Err(RuntimeError::ContainerRuntimeUnavailable {
backend: "podman".to_string(),
binary,
source,
}),
}
}
const DEFAULT_KUBERNETES_SERVICE_IP: &str = "10.96.0.1";
const SA_ISSUER: &str = "https://kubernetes.default.svc";
struct RuntimeSaProjector {
signing: ed25519_dalek::SigningKey,
issuer: String,
audience: String,
ca_cert_pem: String,
lifetime_secs: i64,
}
#[async_trait::async_trait]
impl engenho_kubelet::ServiceAccountProjector for RuntimeSaProjector {
async fn project(
&self,
namespace: &str,
service_account: &str,
pod_name: &str,
pod_uid: &str,
) -> Result<Option<std::collections::BTreeMap<String, Vec<u8>>>, String> {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| e.to_string())?
.as_secs();
let now = i64::try_from(now).map_err(|e| e.to_string())?;
let token = engenho_apiserver::sa_token::issue(
&self.signing,
&self.issuer,
namespace,
service_account,
pod_uid,
&[self.audience.clone()],
Some(engenho_apiserver::sa_token::NamedUid {
name: pod_name.to_string(),
uid: pod_uid.to_string(),
}),
now,
self.lifetime_secs,
)
.map_err(|e| format!("mint ServiceAccount token: {e}"))?;
let mut files = std::collections::BTreeMap::new();
files.insert("token".to_string(), token.into_bytes());
files.insert("ca.crt".to_string(), self.ca_cert_pem.clone().into_bytes());
files.insert("namespace".to_string(), namespace.as_bytes().to_vec());
Ok(Some(files))
}
fn token_lifetime(&self) -> Option<std::time::Duration> {
u64::try_from(self.lifetime_secs)
.ok()
.map(std::time::Duration::from_secs)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ApiserverReachability {
ServiceVip {
ip: String,
port: u16,
},
HostGateway {
host: String,
port: u16,
},
Unknown,
}
impl ApiserverReachability {
fn injectable(&self) -> Option<(String, u16)> {
match self {
Self::ServiceVip { ip, port } => Some((ip.clone(), *port)),
Self::HostGateway { host, port } => Some((host.clone(), *port)),
Self::Unknown => None,
}
}
}
const PODMAN_HOST_GATEWAY: &str = "host.containers.internal";
fn apiserver_reachability(boot: &BootConfig) -> ApiserverReachability {
let Some(port) = boot
.listen_addr
.rsplit(':')
.next()
.and_then(|p| p.parse::<u16>().ok())
else {
return ApiserverReachability::Unknown;
};
match boot.kubelet_backend {
CfgBackendKind::PodmanApi | CfgBackendKind::Podman => ApiserverReachability::HostGateway {
host: PODMAN_HOST_GATEWAY.to_string(),
port,
},
CfgBackendKind::Native => ApiserverReachability::HostGateway {
host: "127.0.0.1".to_string(),
port,
},
CfgBackendKind::Cri => ApiserverReachability::Unknown,
CfgBackendKind::Fake => ApiserverReachability::ServiceVip {
ip: DEFAULT_KUBERNETES_SERVICE_IP.to_string(),
port: 443,
},
}
}
fn advertised_capabilities(kind: CfgBackendKind) -> NodeCapabilities {
match kind {
CfgBackendKind::Native => NodeCapabilities::running([SchedRuntime::Native]),
CfgBackendKind::Cri | CfgBackendKind::PodmanApi | CfgBackendKind::Podman => {
NodeCapabilities::running([SchedRuntime::Oci])
}
CfgBackendKind::Fake => NodeCapabilities::running(SchedRuntime::ALL),
}
}
const fn kubelet_backend_kind(kind: CfgBackendKind) -> KubeletBackendKind {
match kind {
CfgBackendKind::Cri => KubeletBackendKind::Cri,
CfgBackendKind::PodmanApi => KubeletBackendKind::PodmanApi,
CfgBackendKind::Podman => KubeletBackendKind::Podman,
CfgBackendKind::Fake => KubeletBackendKind::Fake,
CfgBackendKind::Native => KubeletBackendKind::Native,
}
}
fn build_backend(boot: &BootConfig) -> Result<Arc<dyn ContainerRuntime>, RuntimeError> {
let kind = kubelet_backend_kind(boot.kubelet_backend);
let reachability = apiserver_reachability(boot);
match &reachability {
ApiserverReachability::ServiceVip { ip, port } => {
info!(%ip, %port, "apiserver advertised to pods via the service VIP");
}
ApiserverReachability::HostGateway { host, port } => {
info!(
%host, %port,
"apiserver advertised to pods via the container host gateway \
(no service datapath installs the cluster VIP)"
);
}
ApiserverReachability::Unknown => {
warn!(
listen_addr = %boot.listen_addr,
"cannot determine an apiserver address pods can reach; \
injecting no KUBERNETES_SERVICE_* env so in-cluster config \
fails construction and clients fall back to a kubeconfig"
);
}
}
Ok(make_container_runtime_on_node(
kind,
boot.podman_binary.as_deref(),
reachability.injectable(),
boot.native_cgroups,
)?)
}
pub(crate) const STORE_DIR: &str = crate::layout::Area::Store.dir();
async fn boot_store(boot: &BootConfig) -> Result<(Arc<StoreMesh>, BootKind), RuntimeError> {
let cfg = default_config(&boot.cluster_name)?;
let router = InProcessRouter::new();
let listen = "in-process://1".to_string();
if boot.durable {
let store_path = boot.data_dir.join(STORE_DIR);
let (mesh, fresh) = StoreMesh::start_or_resume(1, listen, router, cfg, store_path).await?;
let kind = if fresh {
BootKind::FirstBoot
} else {
BootKind::Resume
};
info!(?kind, "durable store opened");
Ok((Arc::new(mesh), kind))
} else {
let mesh = StoreMesh::start(1, listen, router, cfg).await?;
mesh.initialize_singleton().await?;
info!("ephemeral store initialized");
Ok((Arc::new(mesh), BootKind::Ephemeral))
}
}
pub(crate) fn stamp_creation_timestamp_value(body: &mut serde_json::Value) {
if let Some(obj) = body.as_object_mut() {
let metadata = obj
.entry("metadata".to_string())
.or_insert_with(|| serde_json::json!({}));
if let Some(meta_obj) = metadata.as_object_mut() {
let absent = !meta_obj.contains_key("creationTimestamp")
|| meta_obj.get("creationTimestamp") == Some(&serde_json::Value::Null);
if absent {
meta_obj.insert(
"creationTimestamp".to_string(),
serde_json::Value::String(engenho_types::time::now_rfc3339_utc()),
);
}
}
}
}
const RBAC_GROUP: &str = "rbac.authorization.k8s.io";
const RBAC_VERSION: &str = "v1";
async fn seed_system_namespaces(store: &StoreMesh) -> Result<(), RuntimeError> {
for name in SYSTEM_NAMESPACES {
let ns = system_namespace(name);
put_namespace(store, &ns).await?;
}
info!(
count = SYSTEM_NAMESPACES.len(),
"seeded system namespaces (default, kube-system, kube-public, kube-node-lease)"
);
Ok(())
}
const DEFAULT_STORAGE_CLASS: &str = "engenho-local-path";
async fn seed_default_storage_class(store: &StoreMesh) -> Result<(), RuntimeError> {
use engenho_types::generated_v1_34::storage_v1::StorageClass;
let mut sc = StorageClass {
provisioner: engenho_controllers::pv_binder::ENGENHO_LOCAL_PATH_PROVISIONER.to_string(),
..Default::default()
};
sc.metadata.name = DEFAULT_STORAGE_CLASS.to_string();
sc.metadata.annotations.insert(
"storageclass.kubernetes.io/is-default-class".to_string(),
"true".to_string(),
);
sc.reclaim_policy = Some("Delete".to_string());
sc.volume_binding_mode = Some("Immediate".to_string());
let mut value = serde_json::to_value(&sc)
.map_err(|e| RuntimeError::Server(seed_serialize_err("StorageClass", &e)))?;
stamp_creation_timestamp_value(&mut value);
store
.propose(ResourceCommand::Put {
key: ResourceKey::cluster_scoped(
"storage.k8s.io",
"v1",
"StorageClass",
DEFAULT_STORAGE_CLASS.to_string(),
),
value,
expected: None,
reason: Reason::Operator,
})
.await?;
info!(
class = DEFAULT_STORAGE_CLASS,
provisioner = engenho_controllers::pv_binder::ENGENHO_LOCAL_PATH_PROVISIONER,
"seeded the default StorageClass (PVCs with no class are now provisionable)"
);
Ok(())
}
async fn seed_snapshot_crds(store: &StoreMesh) -> Result<(), RuntimeError> {
let kinds = [
("VolumeSnapshot", "volumesnapshots", "volumesnapshot", true),
(
"VolumeSnapshotContent",
"volumesnapshotcontents",
"volumesnapshotcontent",
false,
),
(
"VolumeSnapshotClass",
"volumesnapshotclasses",
"volumesnapshotclass",
false,
),
];
for (kind, plural, singular, namespaced) in kinds {
let name = [
plural,
".",
engenho_controllers::volume_snapshot::SNAPSHOT_GROUP,
]
.concat();
let list_kind = [kind, "List"].concat();
let crd = serde_json::json!({
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": { "name": name },
"spec": {
"group": engenho_controllers::volume_snapshot::SNAPSHOT_GROUP,
"scope": if namespaced { "Namespaced" } else { "Cluster" },
"names": {
"kind": kind,
"listKind": list_kind,
"plural": plural,
"singular": singular,
},
"versions": [{
"name": engenho_controllers::volume_snapshot::SNAPSHOT_VERSION,
"served": true,
"storage": true,
"subresources": { "status": {} },
"schema": {
"openAPIV3Schema": {
"type": "object",
"x-kubernetes-preserve-unknown-fields": true
}
}
}]
}
});
let mut value = crd;
stamp_creation_timestamp_value(&mut value);
store
.propose(ResourceCommand::Put {
key: ResourceKey::cluster_scoped(
"apiextensions.k8s.io",
"v1",
"CustomResourceDefinition",
name,
),
value,
expected: None,
reason: Reason::Operator,
})
.await?;
}
info!(
group = engenho_controllers::volume_snapshot::SNAPSHOT_GROUP,
count = kinds.len(),
"seeded the VolumeSnapshot CRDs"
);
Ok(())
}
async fn seed_kubernetes_service(store: &StoreMesh, boot: &BootConfig) -> Result<(), RuntimeError> {
if boot.service_cidr.is_empty() {
return Ok(());
}
let mut allocator =
match engenho_controllers::cluster_ip::ClusterIpAllocator::new(&boot.service_cidr) {
Ok(a) => a,
Err(e) => {
warn!(
cidr = %boot.service_cidr,
error = %e,
"service_cidr unparseable; skipping the kubernetes Service seed"
);
return Ok(());
}
};
let Ok(vip) = allocator.allocate() else {
warn!("service CIDR has no assignable address; skipping the kubernetes Service seed");
return Ok(());
};
let port = boot
.listen_addr
.rsplit(':')
.next()
.and_then(|p| p.parse::<i64>().ok())
.unwrap_or(6443);
let value = serde_json::json!({
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "kubernetes",
"namespace": "default",
"labels": { "component": "apiserver", "provider": "kubernetes" },
},
"spec": {
"type": "ClusterIP",
"clusterIP": vip,
"clusterIPs": [vip],
"ports": [{ "name": "https", "port": 443, "protocol": "TCP", "targetPort": port }],
"sessionAffinity": "None",
},
"status": { "loadBalancer": {} }
});
let mut value = value;
stamp_creation_timestamp_value(&mut value);
store
.propose(ResourceCommand::Put {
key: ResourceKey::namespaced("", "v1", "Service", "default", "kubernetes"),
value,
expected: None,
reason: Reason::Operator,
})
.await?;
info!(%vip, port, "seeded the kubernetes Service (reserves the first service VIP)");
Ok(())
}
const SYSTEM_NAMESPACES: &[&str] = &["default", "kube-system", "kube-public", "kube-node-lease"];
fn system_namespace(name: &str) -> Namespace {
let mut metadata = engenho_types::meta::ObjectMeta {
name: name.to_string(),
..Default::default()
};
metadata
.labels
.insert("kubernetes.io/metadata.name".to_string(), name.to_string());
Namespace {
metadata,
spec: Some(NamespaceSpec {
finalizers: vec!["kubernetes".to_string()],
}),
status: Some(NamespaceStatus {
phase: Some("Active".to_string()),
..Default::default()
}),
}
}
async fn put_namespace(store: &StoreMesh, ns: &Namespace) -> Result<(), RuntimeError> {
let mut value = serde_json::to_value(ns)
.map_err(|e| RuntimeError::Server(seed_serialize_err("Namespace", &e)))?;
stamp_creation_timestamp_value(&mut value);
let name = ns.metadata.name.clone();
store
.propose(ResourceCommand::Put {
key: ResourceKey::cluster_scoped("", "v1", "Namespace", name),
value,
expected: None,
reason: Reason::Operator,
})
.await?;
Ok(())
}
async fn seed_bootstrap_rbac(store: &StoreMesh) -> Result<(), RuntimeError> {
let cluster_admin = ClusterRole {
metadata: rbac_meta("cluster-admin"),
rules: vec![
PolicyRule {
verbs: vec!["*".into()],
api_groups: vec!["*".into()],
resources: vec!["*".into()],
..Default::default()
},
PolicyRule {
verbs: vec!["*".into()],
non_resource_urls: vec!["*".into()],
..Default::default()
},
],
..Default::default()
};
let cluster_admin_binding = ClusterRoleBinding {
metadata: rbac_meta("cluster-admin"),
role_ref: cluster_role_ref("cluster-admin"),
subjects: vec![group_subject("system:masters")],
};
let discovery = ClusterRole {
metadata: rbac_meta("system:discovery"),
rules: vec![PolicyRule {
verbs: vec!["get".into()],
non_resource_urls: vec![
"/api".into(),
"/api/*".into(),
"/apis".into(),
"/apis/*".into(),
"/openapi".into(),
"/openapi/*".into(),
"/version".into(),
"/version/*".into(),
"/healthz".into(),
"/livez".into(),
"/readyz".into(),
],
..Default::default()
}],
..Default::default()
};
let discovery_binding = ClusterRoleBinding {
metadata: rbac_meta("system:discovery"),
role_ref: cluster_role_ref("system:discovery"),
subjects: vec![group_subject("system:authenticated")],
};
let basic_user = ClusterRole {
metadata: rbac_meta("system:basic-user"),
rules: vec![
PolicyRule {
verbs: vec!["create".into()],
api_groups: vec!["authorization.k8s.io".into()],
resources: vec![
"selfsubjectaccessreviews".into(),
"selfsubjectrulesreviews".into(),
],
..Default::default()
},
PolicyRule {
verbs: vec!["create".into()],
api_groups: vec!["authentication.k8s.io".into()],
resources: vec!["selfsubjectreviews".into()],
..Default::default()
},
],
..Default::default()
};
let basic_user_binding = ClusterRoleBinding {
metadata: rbac_meta("system:basic-user"),
role_ref: cluster_role_ref("system:basic-user"),
subjects: vec![group_subject("system:authenticated")],
};
let public_info = ClusterRole {
metadata: rbac_meta("system:public-info-viewer"),
rules: vec![PolicyRule {
verbs: vec!["get".into()],
non_resource_urls: vec![
"/healthz".into(),
"/livez".into(),
"/readyz".into(),
"/version".into(),
"/version/*".into(),
"/api".into(),
"/api/*".into(),
"/apis".into(),
"/apis/*".into(),
"/openapi".into(),
"/openapi/*".into(),
],
..Default::default()
}],
..Default::default()
};
let public_info_binding = ClusterRoleBinding {
metadata: rbac_meta("system:public-info-viewer"),
role_ref: cluster_role_ref("system:public-info-viewer"),
subjects: vec![
group_subject("system:authenticated"),
group_subject("system:unauthenticated"),
],
};
put_cluster_role(store, &cluster_admin).await?;
put_cluster_role_binding(store, &cluster_admin_binding).await?;
put_cluster_role(store, &discovery).await?;
put_cluster_role_binding(store, &discovery_binding).await?;
put_cluster_role(store, &basic_user).await?;
put_cluster_role_binding(store, &basic_user_binding).await?;
put_cluster_role(store, &public_info).await?;
put_cluster_role_binding(store, &public_info_binding).await?;
info!(
"seeded bootstrap RBAC policy (cluster-admin + system:discovery + system:basic-user + system:public-info-viewer)"
);
Ok(())
}
fn rbac_meta(name: &str) -> engenho_types::meta::ObjectMeta {
engenho_types::meta::ObjectMeta {
name: name.to_string(),
..Default::default()
}
}
fn cluster_role_ref(name: &str) -> RoleRef {
RoleRef {
api_group: RBAC_GROUP.to_string(),
kind: "ClusterRole".to_string(),
name: name.to_string(),
}
}
fn group_subject(name: &str) -> Subject {
Subject {
kind: "Group".to_string(),
api_group: Some(RBAC_GROUP.to_string()),
name: name.to_string(),
namespace: None,
}
}
async fn put_cluster_role(store: &StoreMesh, cr: &ClusterRole) -> Result<(), RuntimeError> {
let mut value = serde_json::to_value(cr)
.map_err(|e| RuntimeError::Server(seed_serialize_err("ClusterRole", &e)))?;
stamp_creation_timestamp_value(&mut value);
let name = cr.metadata.name.clone();
store
.propose(ResourceCommand::Put {
key: ResourceKey::cluster_scoped(RBAC_GROUP, RBAC_VERSION, "ClusterRole", name),
value,
expected: None,
reason: Reason::Operator,
})
.await?;
Ok(())
}
async fn put_cluster_role_binding(
store: &StoreMesh,
crb: &ClusterRoleBinding,
) -> Result<(), RuntimeError> {
let mut value = serde_json::to_value(crb)
.map_err(|e| RuntimeError::Server(seed_serialize_err("ClusterRoleBinding", &e)))?;
stamp_creation_timestamp_value(&mut value);
let name = crb.metadata.name.clone();
store
.propose(ResourceCommand::Put {
key: ResourceKey::cluster_scoped(RBAC_GROUP, RBAC_VERSION, "ClusterRoleBinding", name),
value,
expected: None,
reason: Reason::Operator,
})
.await?;
Ok(())
}
fn seed_serialize_err(kind: &str, e: &serde_json::Error) -> engenho_apiserver::ServerError {
engenho_apiserver::ServerError::Serve(std::io::Error::other(format!(
"failed to serialize bootstrap {kind}: {e}"
)))
}
#[must_use]
fn parse_mem_total_bytes(meminfo: &str) -> Option<u64> {
let line = meminfo.lines().find(|l| l.starts_with("MemTotal:"))?;
let mut it = line.split_whitespace().skip(1);
let value: u64 = it.next()?.parse().ok()?;
match it.next() {
Some("kB" | "KB") => Some(value * 1024),
Some("mB" | "MB") => Some(value * 1024 * 1024),
None => Some(value),
Some(_) => None,
}
}
fn host_memory_bytes() -> Option<u64> {
#[cfg(target_os = "linux")]
{
std::fs::read_to_string("/proc/meminfo")
.ok()
.as_deref()
.and_then(parse_mem_total_bytes)
}
#[cfg(not(target_os = "linux"))]
{
None
}
}
pub(crate) fn host_capacity() -> (String, String) {
let cpus = std::thread::available_parallelism()
.map(std::num::NonZeroUsize::get)
.unwrap_or(1);
let memory = match host_memory_bytes() {
Some(bytes) => {
bytes.to_string()
}
None => {
warn!(
"cannot read total host memory on this target; advertising the \
8Gi fallback — the scheduler will pack this node against a \
value that is not measured"
);
"8Gi".to_string()
}
};
(cpus.to_string(), memory)
}
fn san_listen_ip(addr: SocketAddr) -> Option<std::net::IpAddr> {
let ip = addr.ip();
if ip.is_unspecified() { None } else { Some(ip) }
}
fn build_authenticator(
data_dir: &std::path::Path,
admin_token: Option<String>,
) -> ChainAuthenticator {
match engenho_apiserver::sa_token::load_or_generate_sa_key(data_dir) {
Ok(kp) => {
info!("ServiceAccount token authentication enabled");
ChainAuthenticator::bootstrap_with_sa(
admin_token,
kp.verifying,
SA_ISSUER.to_string(),
SA_ISSUER.to_string(),
)
}
Err(e) => {
warn!(
error = %e,
"no ServiceAccount signing key; in-cluster clients will take a typed 401 and \
must fall back to a kubeconfig"
);
ChainAuthenticator::bootstrap(admin_token)
}
}
}
fn build_token_issuer(
data_dir: &std::path::Path,
) -> Option<Arc<engenho_apiserver::sa_token::SaIssuer>> {
match engenho_apiserver::sa_token::load_or_generate_sa_key(data_dir) {
Ok(kp) => {
info!("ServiceAccount token issuance enabled (POST serviceaccounts/<name>/token)");
Some(Arc::new(engenho_apiserver::sa_token::SaIssuer {
signing: kp.signing,
issuer: SA_ISSUER.to_string(),
default_audience: SA_ISSUER.to_string(),
}))
}
Err(e) => {
warn!(
error = %e,
"no ServiceAccount signing key; `kubectl create token` and every in-cluster \
identity will take a typed error"
);
None
}
}
}
fn server_sans(
extra_sans: &[String],
advertise_address: &str,
) -> Result<Vec<SanEntry>, RuntimeError> {
let mut sans = parse_extra_sans(extra_sans)?;
if let Some(host) = advertised_host(advertise_address) {
let entry = host
.parse::<SanEntry>()
.map_err(|source| RuntimeError::ExtraSan { source })?;
if !sans.contains(&entry) {
sans.push(entry);
}
}
Ok(sans)
}
fn advertised_host(advertise: &str) -> Option<&str> {
let value = advertise.trim();
if value.is_empty() {
return None;
}
if let Some(rest) = value.strip_prefix('[') {
return rest.split(']').next().filter(|h| !h.is_empty());
}
if value.matches(':').count() == 1 {
return value.split(':').next().filter(|h| !h.is_empty());
}
Some(value)
}
pub(crate) fn advertised_ip(advertise: &str) -> Option<std::net::IpAddr> {
advertised_host(advertise).and_then(|h| h.parse().ok())
}
fn advertised_server_url(advertise_address: &str, bound: SocketAddr) -> Option<String> {
let value = advertise_address.trim();
if value.is_empty() {
return None;
}
let has_port = if value.starts_with('[') {
value.rsplit(']').next().is_some_and(|t| t.starts_with(':'))
} else {
value.matches(':').count() == 1
};
if has_port {
Some(format!("https://{value}"))
} else {
let host = if value.parse::<std::net::Ipv6Addr>().is_ok() {
format!("[{value}]")
} else {
value.to_string()
};
Some(format!("https://{host}:{}", bound.port()))
}
}
fn is_loopback_only(addr: SocketAddr) -> bool {
addr.ip().is_loopback()
}
fn parse_extra_sans(raw: &[String]) -> Result<Vec<SanEntry>, RuntimeError> {
raw.iter()
.map(|s| s.parse::<SanEntry>())
.collect::<Result<Vec<_>, _>>()
.map_err(|source| RuntimeError::ExtraSan { source })
}
fn write_boot_kubeconfig(
boot: &BootConfig,
bound_addr: SocketAddr,
ca_pem: &str,
admin: Option<&ClientMaterial>,
) -> Result<Vec<PublishRecord>, RuntimeError> {
let emit = |server: &str| {
match admin {
Some(admin) => emit_kubeconfig_with_admin(
&boot.cluster_name,
server,
ca_pem.as_bytes(),
admin.cert_pem.as_bytes(),
admin.key_pem.as_bytes(),
),
None => emit_kubeconfig(&boot.cluster_name, server, ca_pem.as_bytes()),
}
.map_err(|e| RuntimeError::Kubeconfig(e.to_string()))
};
let publish = |target: KubeconfigTarget, path: &std::path::Path, yaml: &str, what: &str| {
let visibility = boot.kubeconfig_publish_visibility;
match write_kubeconfig_file(path, yaml, visibility) {
Ok(()) => {
info!(path = %path.display(), "{what} published");
PublishRecord::written(target, path, visibility.mode())
}
Err(e) => {
tracing::warn!(
path = %path.display(), error = %e,
"{what} publish failed — the daemon is serving"
);
PublishRecord::failed(target, path, &e)
}
}
};
let server_url = loopback_server_url(bound_addr);
let yaml = emit(&server_url)?;
let path = data_dir_kubeconfig(boot);
write_kubeconfig_file(&path, &yaml, KubeconfigVisibility::Private)?;
info!(path = %path.display(), server = %server_url, admin = admin.is_some(), "kubeconfig written");
let mut records = vec![PublishRecord::written(
KubeconfigTarget::DataDir,
&path,
KubeconfigVisibility::Private.mode(),
)];
records.push(match kubeconfig_path(boot, KubeconfigTarget::Pod) {
Err(reason) => PublishRecord::skipped(KubeconfigTarget::Pod, reason),
Ok(pod_publish) => {
if let Some((host, port)) = apiserver_reachability(boot).injectable() {
let pod_yaml = emit(&format!("https://{host}:{port}"))?;
publish(
KubeconfigTarget::Pod,
&pod_publish,
&pod_yaml,
"pod-facing kubeconfig",
)
} else {
tracing::warn!(
"pod-facing kubeconfig requested but no pod-reachable apiserver \
address is known; writing nothing rather than an unusable file"
);
PublishRecord::skipped(KubeconfigTarget::Pod, SkipReason::NoPodAddress)
}
}
});
records.push(match kubeconfig_path(boot, KubeconfigTarget::Remote) {
Err(reason) => PublishRecord::skipped(KubeconfigTarget::Remote, reason),
Ok(remote_publish) => {
if let Some(remote_server) = advertised_server_url(&boot.advertise_address, bound_addr)
{
let remote_yaml = emit(&remote_server)?;
publish(
KubeconfigTarget::Remote,
&remote_publish,
&remote_yaml,
"remote kubeconfig",
)
} else {
tracing::warn!(
"remote_kubeconfig_publish_path is set but advertise_address is empty; \
writing nothing rather than a file with a loopback server url"
);
PublishRecord::skipped(KubeconfigTarget::Remote, SkipReason::NoAdvertiseAddress)
}
}
});
records.push(match kubeconfig_path(boot, KubeconfigTarget::Operator) {
Err(reason) => PublishRecord::skipped(KubeconfigTarget::Operator, reason),
Ok(path) => publish(
KubeconfigTarget::Operator,
&path,
&yaml,
"kubeconfig for kubectl/k9s/flux",
),
});
Ok(records)
}
pub(crate) fn kubeconfig_path(
boot: &BootConfig,
target: KubeconfigTarget,
) -> Result<std::path::PathBuf, SkipReason> {
match target {
KubeconfigTarget::DataDir => Ok(data_dir_kubeconfig(boot)),
KubeconfigTarget::Operator => resolve_publish_path(&boot.kubeconfig_publish_path),
KubeconfigTarget::Pod => resolve_publish_path(&boot.pod_kubeconfig_publish_path),
KubeconfigTarget::Remote => resolve_publish_path(&boot.remote_kubeconfig_publish_path),
}
}
fn data_dir_kubeconfig(boot: &BootConfig) -> std::path::PathBuf {
boot.data_dir.join("kubeconfig")
}
fn resolve_publish_path(raw: &str) -> Result<std::path::PathBuf, SkipReason> {
let raw = raw.trim();
if raw.is_empty() {
return Err(SkipReason::NotConfigured);
}
if let Some(rest) = raw.strip_prefix("~/") {
let home = std::env::var("HOME")
.ok()
.filter(|h| !h.is_empty())
.ok_or(SkipReason::NoHome)?;
return Ok(std::path::PathBuf::from(home).join(rest));
}
Ok(std::path::PathBuf::from(raw))
}
fn persist_admin_material(
data_dir: &std::path::Path,
admin: &ClientMaterial,
) -> Result<(), RuntimeError> {
create_pki_dir(&PkiFile::dir(data_dir))?;
write_at_mode(&PkiFile::AdminCert.path(data_dir), &admin.cert_pem, 0o644)?;
write_at_mode(&PkiFile::AdminKey.path(data_dir), &admin.key_pem, 0o600)?;
Ok(())
}
pub(crate) fn load_or_generate_admin_token(
data_dir: &std::path::Path,
) -> Result<String, RuntimeError> {
let token_path = PkiFile::AdminToken.path(data_dir);
if let Ok(existing) = std::fs::read_to_string(&token_path) {
let trimmed = existing.trim().to_string();
if !trimmed.is_empty() {
return Ok(trimmed);
}
}
create_pki_dir(&PkiFile::dir(data_dir))?;
let token = random_admin_token();
write_at_mode(&token_path, &token, 0o600)?;
Ok(token)
}
fn random_admin_token() -> String {
let bytes = crate::entropy::random::<32>().unwrap_or_else(|_| {
let pid = std::process::id().to_le_bytes();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos())
.to_le_bytes();
let mut bytes = [0u8; 32];
for ((i, b), n) in bytes.iter_mut().enumerate().zip(0u8..) {
*b = pid[i % pid.len()] ^ nanos[i % nanos.len()] ^ n;
}
bytes
});
crate::entropy::lower_hex(&bytes)
}
#[cfg(all(unix, feature = "with-cofre"))]
fn create_pki_dir(pki: &std::path::Path) -> Result<(), RuntimeError> {
cofre_fs::create_secret_dir(pki, 0o700).map_err(|source| RuntimeError::KubeconfigIo {
path: pki.to_path_buf(),
source,
})
}
#[cfg(all(unix, not(feature = "with-cofre")))]
fn create_pki_dir(pki: &std::path::Path) -> Result<(), RuntimeError> {
std::fs::create_dir_all(pki).map_err(|source| RuntimeError::KubeconfigIo {
path: pki.to_path_buf(),
source,
})?;
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(pki, std::fs::Permissions::from_mode(0o700)).map_err(|source| {
RuntimeError::KubeconfigIo {
path: pki.to_path_buf(),
source,
}
})
}
#[cfg(not(unix))]
fn create_pki_dir(pki: &std::path::Path) -> Result<(), RuntimeError> {
std::fs::create_dir_all(pki).map_err(|source| RuntimeError::KubeconfigIo {
path: pki.to_path_buf(),
source,
})
}
#[cfg(all(unix, feature = "with-cofre"))]
fn write_at_mode(path: &std::path::Path, contents: &str, mode: u32) -> Result<(), RuntimeError> {
cofre_fs::write_secret(path, contents.as_bytes(), mode).map_err(|source| {
RuntimeError::KubeconfigIo {
path: path.to_path_buf(),
source,
}
})
}
#[cfg(all(unix, not(feature = "with-cofre")))]
fn write_at_mode(path: &std::path::Path, contents: &str, mode: u32) -> Result<(), RuntimeError> {
std::fs::write(path, contents.as_bytes()).map_err(|source| RuntimeError::KubeconfigIo {
path: path.to_path_buf(),
source,
})?;
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode)).map_err(|source| {
RuntimeError::KubeconfigIo {
path: path.to_path_buf(),
source,
}
})
}
#[cfg(not(unix))]
fn write_at_mode(path: &std::path::Path, contents: &str, _mode: u32) -> Result<(), RuntimeError> {
std::fs::write(path, contents.as_bytes()).map_err(|source| RuntimeError::KubeconfigIo {
path: path.to_path_buf(),
source,
})
}
fn loopback_server_url(bound_addr: SocketAddr) -> String {
let mut url = String::from("https://127.0.0.1:");
url.push_str(&bound_addr.port().to_string());
url
}
fn write_kubeconfig_file(
path: &std::path::Path,
contents: &str,
visibility: KubeconfigVisibility,
) -> Result<(), RuntimeError> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|source| RuntimeError::KubeconfigIo {
path: parent.to_path_buf(),
source,
})?;
}
write_at_mode(path, contents, visibility.mode())
}
const CNI_CONFIG_DIR: &str = "/etc/cni/net.d";
const CNI_INSTALL: engenho_cni::exec::CniInstall = engenho_cni::exec::CniInstall::Planned;
fn spawn_children(parts: &Parts) -> Children {
Children::spawn_catalog(&parts.boot, |child, before| parts.task(child, before))
}
type KubeletSlot = Arc<ArcSwap<Kubelet>>;
struct ConfiguredSchedulerLoop(ConfiguredScheduler);
#[async_trait::async_trait]
impl Controller for ConfiguredSchedulerLoop {
fn name(&self) -> &'static str {
Controller::name(self.0.scheduler())
}
async fn tick(&self) -> Result<ReconcileOutcome, ControllerError> {
Controller::tick(self.0.scheduler()).await
}
fn controller_type(&self) -> ControllerType {
Controller::controller_type(self.0.scheduler())
}
}
struct DeclaredHere<C> {
controller: C,
reads: Reads,
}
impl<C> DeclaredHere<C> {
fn new(controller: C, reads: Reads) -> Self {
Self { controller, reads }
}
}
#[async_trait::async_trait]
impl<C: Controller> Controller for DeclaredHere<C> {
fn name(&self) -> &'static str {
self.controller.name()
}
async fn tick(&self) -> Result<ReconcileOutcome, ControllerError> {
self.controller.tick().await
}
fn controller_type(&self) -> ControllerType {
self.controller.controller_type()
}
}
impl<C> DeclaresReads for DeclaredHere<C> {
fn reads(&self) -> Reads {
self.reads.clone()
}
}
const KUBELET_READS: &[GroupVersionKind] = &[
gvk("", "v1", "Pod"),
gvk("", "v1", "Node"),
gvk("coordination.k8s.io", "v1", "Lease"),
gvk("", "v1", "Service"),
gvk("", "v1", "ConfigMap"),
gvk("", "v1", "Secret"),
gvk("", "v1", "ServiceAccount"),
gvk("", "v1", "PersistentVolumeClaim"),
gvk("", "v1", "PersistentVolume"),
];
const SCHEDULER_READS: &[GroupVersionKind] = &[gvk("", "v1", "Pod"), gvk("", "v1", "Node")];
struct Parts {
boot: BootConfig,
store: Arc<StoreMesh>,
backend: Arc<dyn ContainerRuntime>,
handler_sink: Arc<dyn DynamicHandlerSink>,
ns: Option<String>,
windows: Windows,
csi_drivers: engenho_kubelet::DriverTable,
events: Arc<dyn EventSink>,
scheduler: Arc<ConfiguredSchedulerLoop>,
kubelet: KubeletSlot,
kubelet_endpoint: KubeletEndpoint,
router: RouterState,
}
enum Renewal {
Nothing,
Scheduler(Arc<ConfiguredSchedulerLoop>),
Kubelet(Arc<Kubelet>),
}
impl Parts {
fn assemble(
boot: &BootConfig,
store: &Arc<StoreMesh>,
backend: &Arc<dyn ContainerRuntime>,
scheduler: ConfiguredScheduler,
handler_sink: &Arc<dyn DynamicHandlerSink>,
router: &RouterState,
windows: Windows,
) -> Self {
let ns = boot.controllers_namespace.clone();
let csi_drivers = engenho_kubelet::DriverTable::new();
let events: Arc<dyn EventSink> = Arc::new(
engenho_controllers::event_recorder::StoreEventSink::new(Arc::new(MeshEventStore {
store: store.clone(),
})),
);
let scheduler = Arc::new(ConfiguredSchedulerLoop(scheduler));
let kubelet = build_kubelet(boot, store, backend, events.clone(), &csi_drivers);
Self {
boot: boot.clone(),
store: store.clone(),
backend: backend.clone(),
handler_sink: handler_sink.clone(),
ns,
windows,
csi_drivers,
events,
scheduler,
kubelet: Arc::new(ArcSwap::new(kubelet)),
kubelet_endpoint: KubeletEndpoint::default(),
router: router.clone(),
}
}
fn renew(&self, child: Child) -> Result<Renewal, RuntimeError> {
Ok(match child {
Child::Driver(Driver::Scheduler) => {
Renewal::Scheduler(Arc::new(ConfiguredSchedulerLoop(Scheduler::from_config(
self.store.clone(),
&self.boot.scheduler,
)?)))
}
Child::Driver(Driver::Kubelet) => Renewal::Kubelet(build_kubelet(
&self.boot,
&self.store,
&self.backend,
self.events.clone(),
&self.csi_drivers,
)),
Child::Driver(_) | Child::Listener(_) | Child::NodeLease => Renewal::Nothing,
})
}
fn metrics_reader(&self) -> Arc<KubeletMetricsReader> {
Arc::new(KubeletMetricsReader {
kubelet: self.kubelet.clone(),
})
}
fn log_reader(&self) -> Arc<KubeletLogReader> {
Arc::new(KubeletLogReader {
kubelet: self.kubelet.clone(),
})
}
fn stream_proxy(&self) -> Arc<KubeletStreamProxy> {
Arc::new(KubeletStreamProxy {
store: self.store.clone(),
local_node: self.boot.node_name.clone(),
local_kubelet: self.kubelet_endpoint.clone(),
})
}
fn install(&mut self, renewal: Renewal) {
match renewal {
Renewal::Nothing => {}
Renewal::Scheduler(scheduler) => self.scheduler = scheduler,
Renewal::Kubelet(kubelet) => self.kubelet.store(kubelet),
}
}
fn task(&self, child: Child, before: &Children) -> Option<ChildTask> {
match child {
Child::Driver(driver) => Some(self.driver(driver)),
Child::Listener(listener) => Some(listener_task(
listener,
&self.boot,
Arc::downgrade(&self.kubelet.load()),
&self.store,
&self.kubelet_endpoint,
)),
Child::NodeLease => {
let Some(kubelet) = before.row(Child::Driver(Driver::Kubelet)) else {
error!(
"the node lease has no kubelet to renew by and is not spawned; this \
node will read NotReady"
);
return None;
};
Some(drive_node_lease(
&self.store,
&self.boot.node_name,
kubelet,
self.windows,
RuntimeHealthSource::Unobserved,
))
}
}
}
fn watch<C: Controller + DeclaresReads + 'static>(
&self,
driver: Driver,
controller: C,
) -> ChildTask {
drive(
TickLoop::Driver(driver),
controller,
&self.store,
self.windows.of_child(Child::Driver(driver)),
)
}
#[allow(
clippy::too_many_lines,
reason = "one arm per catalog driver; splitting the match would split the catalog"
)]
fn driver(&self, driver: Driver) -> ChildTask {
let store = &self.store;
let ns = || self.ns.clone();
let events = || self.events.clone();
match driver {
Driver::Deployment => self.watch(
driver,
DeploymentController::new(store.clone(), ns()).with_event_sink(events()),
),
Driver::ReplicaSet => self.watch(
driver,
ReplicaSetController::new(store.clone(), ns()).with_event_sink(events()),
),
Driver::StatefulSet => self.watch(
driver,
StatefulSetController::new(store.clone(), ns()).with_event_sink(events()),
),
Driver::DaemonSet => self.watch(
driver,
DaemonSetController::new(store.clone(), ns()).with_event_sink(events()),
),
Driver::Job => self.watch(
driver,
JobController::new(store.clone(), ns()).with_event_sink(events()),
),
Driver::CronJob => self.watch(
driver,
CronJobController::new(store.clone(), Arc::new(WallClock), ns())
.with_event_sink(events()),
),
Driver::PodDisruptionBudget => self.watch(
driver,
PodDisruptionBudgetController::new(store.clone(), ns()),
),
Driver::Endpoints => self.watch(
driver,
EndpointsController::new(store.clone(), ns()).with_event_sink(events()),
),
Driver::ServiceRouting => {
let resolved = self.boot.datapath_mode.resolve(cfg!(target_os = "linux"));
let backend = make_service_router(resolved);
info!(
datapath = backend.name(),
mode = ?self.boot.datapath_mode,
"service routing backend selected"
);
self.watch(
driver,
ServiceRoutingController::new(store.clone(), backend, ns()),
)
}
Driver::Gc => self.watch(driver, GcController::new(store.clone(), ns())),
Driver::Namespace => self.watch(driver, NamespaceController::new(store.clone(), ns())),
Driver::PvcProtection => {
self.watch(driver, PvcProtectionController::new(store.clone(), ns()))
}
Driver::PvBinder => {
let local_path_root = Area::LocalPath
.path(&self.boot.data_dir)
.to_string_lossy()
.into_owned();
self.watch(
driver,
PvBinderController::new(store.clone(), ns(), local_path_root)
.with_csi(Arc::new(engenho_kubelet::DriverCsiProvisioner::new(
self.csi_drivers.clone(),
)))
.with_event_sink(events()),
)
}
Driver::VolumeSnapshot => {
let snapshot_root = Area::Snapshots
.path(&self.boot.data_dir)
.to_string_lossy()
.into_owned();
self.watch(
driver,
engenho_controllers::volume_snapshot::VolumeSnapshotController::new(
store.clone(),
snapshot_root,
Arc::new(engenho_controllers::volume_snapshot::HostSnapshotEnv),
),
)
}
Driver::Crd => self.watch(
driver,
CrdController::new(store.clone(), self.handler_sink.clone()),
),
Driver::Scheduler => self.watch(
driver,
DeclaredHere::new(self.scheduler.clone(), Reads::of(SCHEDULER_READS)),
),
Driver::ServedCapability => self.watch(
driver,
engenho_controllers::served_capability::ServedCapabilityController::new(
store.clone(),
),
),
Driver::NetworkPolicy => {
let enforcer = Arc::new(
engenho_controllers::network_policy::ComputedNetworkPolicyEnforcer::new(),
);
self.watch(
driver,
engenho_controllers::network_policy_controller::NetworkPolicyController::new(
store.clone(),
enforcer,
)
.with_event_sink(events()),
)
}
Driver::CsiRegistrar => self.watch(
driver,
DeclaredHere::new(
engenho_kubelet::CsiRegistrarController::new(
&self.boot.data_dir,
self.csi_drivers.clone(),
),
Reads::nothing(),
),
),
Driver::CniStatus => self.watch(
driver,
engenho_controllers::cni_status::CniStatusController::new(
store.clone(),
self.boot.node_name.clone(),
std::path::PathBuf::from(CNI_CONFIG_DIR),
CNI_INSTALL,
),
),
Driver::NodeManifests => self.watch(
driver,
crate::node_manifests::NodeManifestsController::new(
self.boot.node_manifests_dir.clone(),
&self.boot.data_dir,
self.router.clone(),
),
),
Driver::Kubelet => self.watch(
driver,
DeclaredHere::new(self.kubelet.load_full(), Reads::of(KUBELET_READS)),
),
}
}
}
pub(crate) fn listener_task(
listener: Listener,
boot: &BootConfig,
kubelet: std::sync::Weak<Kubelet>,
store: &Arc<StoreMesh>,
kubelet_endpoint: &KubeletEndpoint,
) -> ChildTask {
let beat = Arc::new(Heartbeat::new());
let addr = listener.listen_addr(boot).to_owned();
match listener {
Listener::KubeletHttp => {
let api: Arc<dyn engenho_kubelet::server::KubeletApi> =
Arc::new(WeakKubeletApi { kubelet });
let endpoint = kubelet_endpoint.clone();
let publish = KubeletPublish {
store: Arc::downgrade(store),
node_name: boot.node_name.clone(),
advertise_address: boot.advertise_address.clone(),
};
ChildTask::new(
beat.clone(),
serve_rebinding(listener, beat, move || {
serve_kubelet_http(addr.clone(), api.clone(), endpoint.clone(), publish.clone())
}),
)
}
Listener::EtcdFacade => {
let etcd_store = crate::etcd_facade::MeshEtcdStore::new(store);
ChildTask::new(
beat.clone(),
serve_rebinding(listener, beat, move || {
serve_etcd_facade(addr.clone(), etcd_store.clone())
}),
)
}
}
}
pub(crate) fn drive<C: Controller + DeclaresReads + 'static>(
tick_loop: TickLoop,
controller: C,
store: &Arc<StoreMesh>,
windows: Windows,
) -> ChildTask {
let reads = controller.reads();
let config = WatchDriverConfig {
filter: reads.filter(),
debounce: windows.debounce(),
fallback_interval: windows.fallback(),
stuck_tick_after: windows.stuck_tick_after(),
tick_state: tick_loop.tick_state(),
};
let controller_type = controller.controller_type();
let tally = Arc::new(Tally::new(tick_loop.child().name()));
let watch = WatchDriver::new(
Tallied::new(controller, tally.clone()),
store.clone(),
config,
);
let wiring = Wiring::new(controller_type, reads, watch.wakes().clone());
ChildTask::driver(watch.heartbeat(), wiring, tally, watch.run())
}
pub(crate) fn drive_node_lease(
store: &Arc<StoreMesh>,
node: &str,
kubelet: Row,
windows: Windows,
runtime: RuntimeHealthSource,
) -> ChildTask {
drive(
TickLoop::NodeLease,
NodeLease::new(store.clone(), node, kubelet, windows, runtime),
store,
windows.of_child(Child::NodeLease),
)
}
fn build_kubelet(
boot: &BootConfig,
store: &Arc<StoreMesh>,
backend: &Arc<dyn ContainerRuntime>,
events: Arc<dyn EventSink>,
csi_drivers: &engenho_kubelet::DriverTable,
) -> Arc<Kubelet> {
let csi_materializer: Arc<dyn engenho_kubelet::VolumeMaterializer> =
Arc::new(engenho_kubelet::CsiVolumeMaterializer::new(
Arc::new(
engenho_kubelet::PodmanVolumeMaterializer::new()
.with_data_root(Area::Volumes.path(&boot.data_dir)),
),
csi_drivers.clone(),
boot.data_dir.clone(),
));
let sa_key = engenho_apiserver::sa_token::load_or_generate_sa_key(&boot.data_dir)
.map_err(|e| warn!(error = %e, "no SA signing key; pods get no ServiceAccount projection"))
.ok();
let ca_pem_for_sa = engenho_apiserver::load_or_generate_ca(&boot.data_dir)
.map(|ca| ca.cert_pem().to_string())
.map_err(|e| warn!(error = %e, "no cluster CA; pods get no ServiceAccount projection"))
.ok();
let sa_projector: Arc<dyn engenho_kubelet::ServiceAccountProjector> =
match (sa_key.as_ref(), ca_pem_for_sa.as_ref()) {
(Some(kp), Some(ca)) => Arc::new(RuntimeSaProjector {
signing: kp.signing.clone(),
issuer: SA_ISSUER.to_string(),
audience: SA_ISSUER.to_string(),
ca_cert_pem: ca.clone(),
lifetime_secs: 3600,
}),
_ => Arc::new(engenho_kubelet::NoServiceAccountProjection),
};
let closures: Arc<dyn engenho_kubelet::ClosureStore> = match boot.kubelet_backend {
CfgBackendKind::Native => {
match engenho_kubelet::NixClosureStore::discover(
Area::Pods.path(&boot.data_dir).join("gcroots"),
) {
Some(store) => {
info!(roots = %store.root_dir().display(), "kubelet roots pod closures in the nix store");
Arc::new(store)
}
None => {
warn!(
"no nix-store found: pod closures are neither fetched nor rooted, and garbage collection can remove a running pod's image"
);
Arc::new(engenho_kubelet::UnmanagedClosures)
}
}
}
_ => Arc::new(engenho_kubelet::UnmanagedClosures),
};
Arc::new(
Kubelet::new(store.clone(), backend.clone(), boot.node_name.clone())
.with_event_sink(events)
.with_closure_store(closures)
.with_sa_projector(sa_projector)
.with_volume_materializer(csi_materializer)
.with_host_path_policy(engenho_kubelet::pod_volume::HostPathPolicy::allowing(
boot.host_path_allowlist.clone(),
)),
)
}
#[derive(Clone)]
pub(crate) struct KubeletPublish {
store: std::sync::Weak<StoreMesh>,
node_name: String,
advertise_address: String,
}
impl KubeletPublish {
async fn publish(&self, bound: SocketAddr) {
let Some(store) = self.store.upgrade() else {
return;
};
let endpoint = crate::node_registration::KubeletEndpointFields::derive(
&self.node_name,
bound,
&self.advertise_address,
);
let host = HostOwned::measured(&self.node_name).reachable_at(endpoint);
if let Err(e) = register_node(&store, &self.node_name, &host).await {
warn!(
node = %self.node_name,
error = %e,
"could not publish the kubelet endpoint on the Node; pods here are \
unreachable for exec, attach, port-forward and logs from other nodes"
);
}
}
}
async fn serve_kubelet_http(
addr: String,
api: Arc<dyn engenho_kubelet::server::KubeletApi>,
endpoint: KubeletEndpoint,
publish: KubeletPublish,
) {
match tokio::net::TcpListener::bind(&addr).await {
Ok(listener) => {
let local = listener.local_addr().ok();
endpoint.store(local.map(Arc::new));
if let Some(bound) = local {
publish.publish(bound).await;
}
let bound = local.map_or_else(|| addr.clone(), |a| a.to_string());
info!(addr = %bound, "kubelet HTTP surface bound");
let app = engenho_kubelet::server::KubeletServer::new(api).routes();
let served = axum::serve(listener, app).await;
endpoint.store(None);
match served {
Ok(()) => warn!(addr = %bound, "kubelet HTTP surface stopped"),
Err(e) => warn!(addr = %bound, error = %e, "kubelet HTTP surface stopped"),
}
}
Err(e) => warn!(
addr = %addr,
error = %e,
"kubelet HTTP surface could not bind; container logs and exec \
are unreachable from off-process (the apiserver is unaffected)"
),
}
}
async fn serve_etcd_facade(addr: String, etcd_store: crate::etcd_facade::MeshEtcdStore) {
match tokio::net::TcpListener::bind(&addr).await {
Ok(listener) => {
let bound = listener
.local_addr()
.map_or_else(|_| addr.clone(), |a| a.to_string());
info!(addr = %bound, "etcd v3 facade bound (read-only)");
let identity = engenho_etcd::server::ServerIdentity::default();
let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener);
let kv = engenho_etcd::server::ReadOnlyKv {
store: etcd_store.clone(),
identity,
};
let maintenance = engenho_etcd::server::MaintenanceSvc {
store: etcd_store.clone(),
identity,
};
let watch = engenho_etcd::server::WatchSvc::new(Arc::new(etcd_store), identity);
let served = tonic::transport::Server::builder()
.add_service(engenho_etcd::pb::etcdserverpb::kv_server::KvServer::new(kv))
.add_service(engenho_etcd::pb::etcdserverpb::watch_server::WatchServer::new(watch))
.add_service(
engenho_etcd::pb::etcdserverpb::maintenance_server::MaintenanceServer::new(
maintenance,
),
)
.serve_with_incoming(incoming)
.await;
match served {
Ok(()) => warn!(addr = %bound, "etcd v3 facade stopped"),
Err(e) => warn!(addr = %bound, error = %e, "etcd v3 facade stopped"),
}
}
Err(e) => warn!(
addr = %addr,
error = %e,
"etcd v3 facade could not bind; etcdctl, snapshot tooling and \
any --etcd-servers consumer are unreachable (the apiserver is \
unaffected)"
),
}
}
fn make_service_router(resolved: ResolvedDatapath) -> Arc<dyn ServiceRouter> {
match resolved {
ResolvedDatapath::Iptables => Arc::new(IptablesRouter::new()),
ResolvedDatapath::Ipvs => Arc::new(IpvsRouter::new()),
ResolvedDatapath::ComputeOnly => Arc::new(FakeRouter::new()),
}
}
#[cfg(test)]
mod tests {
fn read(config: &super::EngenhoConfig) -> super::BootConfig {
super::BootConfig::read(config).expect("the runtime runs this config")
}
#[test]
fn the_native_backend_is_not_podman_probed() {
let mut config = super::EngenhoConfig::prescribed_default();
config.runtime.kubelet_backend = super::CfgBackendKind::Native;
config.runtime.podman_binary = Some("/nonexistent/definitely-not-podman".to_string());
super::preflight_backend(&read(&config))
.expect("native must skip the podman probe entirely");
}
#[test]
fn a_podman_backend_with_an_unusable_binary_still_fails_preflight() {
let mut config = super::EngenhoConfig::prescribed_default();
config.runtime.kubelet_backend = super::CfgBackendKind::Podman;
config.runtime.podman_binary = Some("/nonexistent/definitely-not-podman".to_string());
let verdict = super::preflight_backend(&read(&config));
assert!(
matches!(
verdict,
Err(super::RuntimeError::ContainerRuntimeUnavailable { .. })
),
"the probe must still catch a broken podman, or skipping it for \
native proves nothing; and the refusal check before it must not \
swallow an admitted backend. Got {verdict:?}"
);
}
#[test]
fn a_cri_node_is_refused_before_any_podman_probe() {
let mut config = super::EngenhoConfig::prescribed_default();
config.runtime.kubelet_backend = super::CfgBackendKind::Cri;
config.runtime.podman_binary = Some("/nonexistent/definitely-not-podman".to_string());
let expected =
engenho_kubelet::config_bridge::construction_refusal(super::KubeletBackendKind::Cri)
.expect(
"CRI is refused while cri_backend::UNSUPPORTED is non-empty. Once it is \
admitted this premise is gone: give preflight_backend's Cri arm the \
probe its pending-cri note names, then rewrite this test",
);
match super::preflight_backend(&read(&config)) {
Err(super::RuntimeError::BackendRefused(refused)) => assert_eq!(
refused, expected,
"the node must carry the kubelet's refusal verbatim"
),
other => panic!("a cri node must fail with the typed refusal, got {other:?}"),
}
}
#[test]
fn podman_pods_are_told_the_host_gateway_not_the_unrouted_service_vip() {
let mut cfg = EngenhoConfig::default();
cfg.runtime.kubelet_backend = CfgBackendKind::Podman;
cfg.runtime.listen_addr = "127.0.0.1:6443".to_string();
let r = super::apiserver_reachability(&read(&cfg));
assert_eq!(
r,
super::ApiserverReachability::HostGateway {
host: super::PODMAN_HOST_GATEWAY.to_string(),
port: 6443,
},
"the ClusterIP has no datapath under podman; advertising it makes \
in-cluster construction SUCCEED and removes the kubeconfig \
fallback, so every request fails instead"
);
let (host, port) = r.injectable().expect("podman must advertise something");
assert_ne!(
host,
super::DEFAULT_KUBERNETES_SERVICE_IP,
"injecting the unrouted VIP is the defect"
);
assert_eq!(
port, 6443,
"the REAL listen port, not 443 — nothing rewrites it"
);
}
#[test]
fn an_unknown_address_injects_nothing_so_the_kubeconfig_fallback_survives() {
let mut cfg = EngenhoConfig::default();
cfg.runtime.kubelet_backend = CfgBackendKind::Podman;
cfg.runtime.listen_addr = "not-a-socket-address".to_string();
let r = super::apiserver_reachability(&read(&cfg));
assert_eq!(r, super::ApiserverReachability::Unknown);
assert!(
r.injectable().is_none(),
"a guessed address is worse than none: it removes the fallback"
);
}
#[test]
fn the_advertised_port_follows_the_configured_listen_port() {
let mut cfg = EngenhoConfig::default();
cfg.runtime.kubelet_backend = CfgBackendKind::Podman;
cfg.runtime.listen_addr = "127.0.0.1:16443".to_string();
let (_, port) = super::apiserver_reachability(&read(&cfg))
.injectable()
.unwrap();
assert_eq!(port, 16443);
}
#[test]
fn empty_publish_path_disables_publishing() {
assert_eq!(
super::resolve_publish_path(""),
Err(crate::publish::SkipReason::NotConfigured)
);
assert_eq!(
super::resolve_publish_path(" "),
Err(crate::publish::SkipReason::NotConfigured)
);
}
#[test]
fn absolute_publish_path_is_verbatim() {
assert_eq!(
super::resolve_publish_path("/etc/engenho/kubeconfig"),
Ok(std::path::PathBuf::from("/etc/engenho/kubeconfig"))
);
}
#[test]
fn tilde_expands_against_home_at_write_time() {
let Ok(home) = std::env::var("HOME") else {
return; };
assert_eq!(
super::resolve_publish_path("~/.kube/configs/engenho"),
Ok(std::path::PathBuf::from(home).join(".kube/configs/engenho"))
);
}
use super::*;
use crate::Dormant;
use crate::TickState;
use crate::child::DeathCause;
use crate::child::{ChildHandle, ChildState};
use crate::impl_census::{Implementors, workspace_sources};
use crate::read_census::{Census, Section};
use engenho_config::KubeletBackendKind as CfgKind;
use engenho_controllers::{Beat, KindFilter, TickClass};
use std::collections::{BTreeMap, BTreeSet};
#[test]
fn mem_total_is_parsed_in_kb_not_bytes() {
let meminfo = "MemTotal: 32793532 kB\nMemFree: 1234 kB\n";
assert_eq!(
super::parse_mem_total_bytes(meminfo),
Some(32_793_532 * 1024)
);
let gib = super::parse_mem_total_bytes(meminfo).unwrap() / (1024 * 1024 * 1024);
assert_eq!(gib, 31);
}
#[test]
fn an_unrecognised_unit_is_refused_not_guessed() {
assert_eq!(
super::parse_mem_total_bytes("MemTotal: 100 furlongs\n"),
None
);
assert_eq!(super::parse_mem_total_bytes("MemTotal: 4096\n"), Some(4096));
}
#[test]
fn a_meminfo_without_memtotal_yields_none() {
assert_eq!(super::parse_mem_total_bytes("MemFree: 1 kB\n"), None);
assert_eq!(super::parse_mem_total_bytes(""), None);
assert_eq!(super::parse_mem_total_bytes("SwapTotal: 8 kB\n"), None);
}
#[test]
fn host_capacity_no_longer_reports_the_8gi_literal_on_linux() {
let (cpu, mem) = super::host_capacity();
assert!(cpu.parse::<u64>().is_ok(), "cpu must be an integer: {cpu}");
if cfg!(target_os = "linux") {
assert_ne!(
mem, "8Gi",
"memory must be measured on linux, not defaulted"
);
assert!(
mem.parse::<u64>().is_ok(),
"measured memory is plain bytes: {mem}"
);
} else {
assert_eq!(mem, "8Gi");
}
}
use shikumi::TieredConfig;
fn ephemeral_test_config() -> EngenhoConfig {
let mut cfg = EngenhoConfig::prescribed_default();
cfg.runtime.listen_addr = "127.0.0.1:0".into();
cfg.runtime.durable = false;
cfg.runtime.node_name = "node-A".into();
cfg.runtime.kubelet_backend = CfgKind::Fake;
cfg.runtime.leadership_timeout_seconds = 5;
cfg.runtime.tls.enabled = false;
static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let unique = format!(
"engenho-test-{}-{}",
std::process::id(),
SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
);
cfg.runtime.data_dir = std::env::temp_dir().join(unique);
cfg.runtime.node_manifests_dir = cfg.runtime.data_dir.join("manifests.d");
cfg.controllers.fallback_interval_seconds = 1;
cfg.controllers.debounce_milliseconds = 20;
cfg.runtime.kubelet_listen_addr = "127.0.0.1:0".into();
cfg.runtime.etcd_listen_addr = "127.0.0.1:0".into();
cfg
}
fn running_drivers(rt: &Runtime) -> usize {
rt.children()
.running()
.filter(|c| matches!(c, Child::Driver(_)))
.count()
}
#[tokio::test]
async fn runtime_boots_all_subsystems_and_registers_node() {
let rt = Runtime::start(ephemeral_test_config()).await.unwrap();
assert_ne!(rt.local_addr().port(), 0);
let key = ResourceKey::cluster_scoped("", "v1", "Node", "node-A");
let node = rt.store().get(&key).await.expect("Node registered");
assert_eq!(node.get("kind").unwrap(), "Node");
assert_eq!(
node.get("spec").unwrap().get("unschedulable").unwrap(),
false
);
assert_eq!(running_drivers(&rt), 22);
assert_eq!(Driver::ALL.len(), 22);
rt.shutdown().await.unwrap();
}
const TICKS: u64 = 3;
const TICK_DEADLINE: Duration = Duration::from_secs(30);
async fn every_child(rt: &Runtime, of: &[Child], ready: impl Fn(&Beat) -> bool) {
let deadline = std::time::Instant::now() + TICK_DEADLINE;
loop {
let lagging: Vec<(Child, Option<Beat>)> = of
.iter()
.map(|c| (*c, rt.children().get(*c).map(|h| h.beat().snapshot())))
.filter(|(_, beat)| !beat.as_ref().is_some_and(&ready))
.collect();
if lagging.is_empty() {
return;
}
assert!(
std::time::Instant::now() < deadline,
"within {TICK_DEADLINE:?} these children never got there: {lagging:#?}"
);
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn every_enabled_child_spawns_and_ticks_under_a_deadline() {
let rt = Runtime::start(ephemeral_test_config()).await.unwrap();
let spawned: BTreeSet<Child> = rt.children().iter().map(|(c, _)| c).collect();
let catalog: BTreeSet<Child> = Child::all().collect();
assert_eq!(
catalog.difference(&spawned).copied().collect::<Vec<_>>(),
[],
"with every switch on, every child in the catalog is spawned"
);
let drivers: Vec<Child> = Driver::ALL.iter().map(|d| Child::Driver(*d)).collect();
every_child(&rt, &drivers, |b| b.ticks_finished >= TICKS).await;
every_child(&rt, &[Child::NodeLease], |b| b.ticks_finished >= 1).await;
let listeners: Vec<Child> = Listener::ALL.iter().map(|l| Child::Listener(*l)).collect();
every_child(&rt, &listeners, |b| {
b.in_flight() && b.last_class != Some(TickClass::Halted)
})
.await;
assert_eq!(
rt.children().running().count(),
spawned.len(),
"a child died during boot"
);
rt.shutdown().await.unwrap();
}
const PANIC_FALLBACK: Duration = Duration::from_millis(200);
struct PanicsEveryTick;
#[async_trait::async_trait]
impl Controller for PanicsEveryTick {
fn name(&self) -> &'static str {
"panics-every-tick"
}
async fn tick(&self) -> Result<ReconcileOutcome, ControllerError> {
panic!("tripped over a bad object");
}
}
async fn a_panicking(driver: Driver) -> (Children, Arc<StoreMesh>) {
let boot = read(&ephemeral_test_config());
let (store, kind) = boot_store(&boot).await.unwrap();
assert_eq!(kind, BootKind::Ephemeral);
assert!(store.wait_for_leadership(Duration::from_secs(5)).await);
let children = Children::spawn_catalog(&boot, |child, _| {
(child == Child::Driver(driver)).then(|| {
drive(
TickLoop::Driver(driver),
DeclaredHere::new(PanicsEveryTick, Reads::nothing()),
&store,
boot.windows(PANIC_FALLBACK).with_fallback(PANIC_FALLBACK),
)
})
});
assert_eq!(children.len(), 1, "precondition: {driver:?} is spawned");
(children, store)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_stateless_driver_whose_tick_panics_keeps_ticking() {
let driver = Driver::CniStatus;
assert_eq!(driver.tick_state(), TickState::Stateless, "precondition");
let (mut children, _store) = a_panicking(driver).await;
let child = Child::Driver(driver);
let deadline = std::time::Instant::now() + TICK_DEADLINE;
let beat = loop {
let beat = children.get(child).map(|h| h.beat().snapshot());
if let Some(beat) = beat.filter(|b| b.panics >= 3) {
break beat;
}
assert!(
std::time::Instant::now() < deadline,
"three panicking ticks never happened: {beat:?}"
);
tokio::time::sleep(Duration::from_millis(50)).await;
};
let died = tokio::time::timeout(PANIC_FALLBACK * 3, children.next_dead()).await;
children.stop().await;
assert!(died.is_err(), "a contained panic ended the child: {died:?}");
assert_eq!(beat.last_class, Some(TickClass::Panicked), "{beat:?}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_stateful_driver_whose_tick_panics_is_dead() {
let driver = Driver::Kubelet;
assert_eq!(driver.tick_state(), TickState::Stateful, "precondition");
let (mut children, _store) = a_panicking(driver).await;
let child = Child::Driver(driver);
let dead = tokio::time::timeout(TICK_DEADLINE, children.next_dead())
.await
.expect("a stateful driver's panic never ended its child");
tokio::time::sleep(PANIC_FALLBACK * 3).await;
let beat = children.get(child).map(|h| h.beat().snapshot());
assert_eq!(
dead,
DeadChild {
child,
cause: DeathCause::Panicked
}
);
assert_eq!(
children.get(child).map(ChildHandle::state),
Some(ChildState::Dead(DeathCause::Panicked))
);
assert_eq!(beat.map(|b| b.ticks_started), Some(1), "{beat:?}");
assert_eq!(beat.map(|b| b.panics), Some(1), "{beat:?}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_remote_apiserver_resolves_the_kubelet_this_node_published() {
let mut cfg = ephemeral_test_config();
cfg.runtime.kubelet_listen_addr = "127.0.0.1:0".into();
let rt = Runtime::start(cfg).await.unwrap();
let store = rt.store();
let key = crate::node_registration::node_key("node-A");
let deadline = std::time::Instant::now() + TICK_DEADLINE;
let published = loop {
let node = store.get(&key).await;
if let Some(a) = node
.as_ref()
.and_then(engenho_apiserver::kubelet_authority_of_node)
{
break (a, node);
}
assert!(
std::time::Instant::now() < deadline,
"the Node never carried a kubelet endpoint: {node:?}"
);
tokio::time::sleep(Duration::from_millis(50)).await;
};
let bound: SocketAddr = published.0.parse().expect("an ip:port authority");
assert!(
bound.ip().is_loopback() && bound.port() != 0,
"{:?}",
published.1
);
tokio::net::TcpStream::connect(bound)
.await
.expect("the published endpoint is the one the kubelet listens on");
let remote = KubeletStreamProxy {
store: store.clone(),
local_node: "node-B".into(),
local_kubelet: KubeletEndpoint::default(),
};
assert_eq!(remote.authority("node-A").await.unwrap(), bound.to_string());
assert!(matches!(
remote.authority("node-C").await,
Err(engenho_apiserver::ApiError::NotFound(_))
));
drop((remote, store));
rt.shutdown().await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_listener_whose_port_is_taken_binds_it_once_it_is_free() {
let blocker = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let addr = blocker.local_addr().unwrap();
let mut cfg = ephemeral_test_config();
cfg.runtime.kubelet_listen_addr = addr.to_string();
let rt = Runtime::start(cfg).await.unwrap();
let kubelet_http = Child::Listener(Listener::KubeletHttp);
every_child(&rt, &[kubelet_http], |b| {
b.last_class == Some(TickClass::Halted)
})
.await;
drop(blocker);
let deadline = std::time::Instant::now() + TICK_DEADLINE;
while tokio::net::TcpStream::connect(addr).await.is_err() {
assert!(
std::time::Instant::now() < deadline,
"within {TICK_DEADLINE:?} the listener never bound {addr} after it was freed"
);
tokio::time::sleep(Duration::from_millis(100)).await;
}
assert_eq!(
rt.children().get(kubelet_http).map(ChildHandle::state),
Some(ChildState::Running),
"a listener that could not bind is not dead"
);
rt.shutdown().await.unwrap();
}
#[tokio::test]
async fn runtime_rejects_invalid_config() {
let mut cfg = ephemeral_test_config();
cfg.runtime.node_name = String::new();
match Runtime::start(cfg).await {
Err(RuntimeError::Config(_)) => {}
Err(other) => panic!("expected Config error, got {other:?}"),
Ok(_) => panic!("expected validation failure, got a booted Runtime"),
}
}
#[tokio::test]
async fn runtime_rejects_unimplemented_scheduling_strategy() {
let mut cfg = ephemeral_test_config();
cfg.scheduler.strategy = engenho_config::SchedulerStrategyKind::BinPack;
match Runtime::start(cfg).await {
Err(RuntimeError::Config(engenho_config::ConfigError::InvalidField {
field, ..
})) => {
assert_eq!(field, "scheduler.strategy");
}
Err(other) => panic!("expected Config/InvalidField, got {other:?}"),
Ok(_) => panic!("expected a typed strategy error, got a booted Runtime"),
}
}
#[tokio::test]
async fn registered_node_advertises_allocatable() {
let rt = Runtime::start(ephemeral_test_config()).await.unwrap();
let key = ResourceKey::cluster_scoped("", "v1", "Node", "node-A");
let node = rt.store().get(&key).await.expect("Node registered");
let alloc = node
.get("status")
.and_then(|s| s.get("allocatable"))
.expect("status.allocatable present");
assert!(
alloc.get("cpu").and_then(|c| c.as_str()).is_some(),
"allocatable.cpu must be set; node={node:#}"
);
assert!(
alloc.get("memory").and_then(|m| m.as_str()).is_some(),
"allocatable.memory must be set; node={node:#}"
);
rt.shutdown().await.unwrap();
}
#[test]
fn make_service_router_dispatches_by_resolved_datapath() {
assert_eq!(
make_service_router(ResolvedDatapath::Iptables).name(),
"iptables"
);
assert_eq!(make_service_router(ResolvedDatapath::Ipvs).name(), "ipvs");
assert_eq!(
make_service_router(ResolvedDatapath::ComputeOnly).name(),
"fake"
);
}
#[test]
fn datapath_auto_selects_compute_only_off_linux() {
let mode = engenho_config::DatapathMode::Auto;
assert_eq!(
make_service_router(mode.resolve(false)).name(),
"fake",
"Auto off-Linux must be the compute-only FakeRouter"
);
assert_eq!(
make_service_router(mode.resolve(true)).name(),
"iptables",
"Auto on Linux installs the iptables kernel datapath"
);
}
#[tokio::test]
async fn disabling_service_routing_drops_one_driver() {
let mut cfg = ephemeral_test_config();
cfg.controllers.enable.service_routing = false;
let rt = Runtime::start(cfg).await.unwrap();
assert_eq!(running_drivers(&rt), 21);
assert!(
rt.children()
.get(Child::Driver(Driver::ServiceRouting))
.is_none()
);
rt.shutdown().await.unwrap();
}
fn controller_sources(driver: Driver) -> Vec<Section> {
macro_rules! file {
($path:literal) => {
Section::file($path, include_str!(concat!("../../", $path)))
};
}
macro_rules! section {
($path:literal, $from:literal, $until:expr) => {
Section::between($path, include_str!(concat!("../../", $path)), $from, $until)
};
}
match driver {
Driver::Deployment => vec![file!("engenho-controllers/src/deployment.rs")],
Driver::ReplicaSet => vec![file!("engenho-controllers/src/replicaset.rs")],
Driver::StatefulSet => vec![file!("engenho-controllers/src/statefulset.rs")],
Driver::DaemonSet => vec![file!("engenho-controllers/src/daemonset.rs")],
Driver::Job => vec![section!(
"engenho-controllers/src/job.rs",
"pub struct JobController",
Some("pub struct CronJobController")
)],
Driver::CronJob => vec![section!(
"engenho-controllers/src/job.rs",
"pub struct CronJobController",
None
)],
Driver::PodDisruptionBudget => vec![file!("engenho-controllers/src/pdb.rs")],
Driver::Endpoints => vec![file!("engenho-controllers/src/endpoints.rs")],
Driver::ServiceRouting => vec![file!("engenho-controllers/src/service_router.rs")],
Driver::Gc => vec![file!("engenho-controllers/src/gc.rs")],
Driver::Namespace => vec![file!("engenho-controllers/src/namespace.rs")],
Driver::PvcProtection => vec![file!("engenho-controllers/src/pvc_protection.rs")],
Driver::PvBinder => vec![
file!("engenho-controllers/src/pv_binder.rs"),
file!("engenho-controllers/src/pv_binder/identity.rs"),
],
Driver::VolumeSnapshot => vec![file!("engenho-controllers/src/volume_snapshot.rs")],
Driver::Crd => vec![file!("engenho-controllers/src/crd.rs")],
Driver::Scheduler => vec![file!("engenho-scheduler/src/scheduler.rs")],
Driver::ServedCapability => {
vec![file!("engenho-controllers/src/served_capability.rs")]
}
Driver::NetworkPolicy => {
vec![file!(
"engenho-controllers/src/network_policy_controller.rs"
)]
}
Driver::CsiRegistrar => vec![section!(
"engenho-kubelet/src/csi_materializer.rs",
"pub struct CsiRegistrarController",
None
)],
Driver::CniStatus => vec![file!("engenho-controllers/src/cni_status.rs")],
Driver::NodeManifests => vec![file!("engenho-runtime/src/node_manifests.rs")],
Driver::Kubelet => vec![file!("engenho-kubelet/src/kubelet.rs")],
}
}
#[tokio::test]
async fn every_driver_wakes_on_every_kind_its_controller_reads() {
let rt = Runtime::start(ephemeral_test_config()).await.unwrap();
let mut undeclared: BTreeSet<(Driver, String)> = BTreeSet::new();
let mut sleeps_through: BTreeSet<(Driver, String)> = BTreeSet::new();
for &driver in Driver::ALL {
let wiring = rt
.children()
.get(Child::Driver(driver))
.and_then(ChildHandle::wiring)
.unwrap_or_else(|| panic!("{driver:?} was not spawned with its wiring"));
let (reads, wakes) = (wiring.reads(), wiring.wakes());
let census = Census::of(&controller_sources(driver));
if census.every && reads.kinds().is_some() {
undeclared.insert((driver, "every kind (the whole catalog)".to_owned()));
}
for kind in &census.kinds {
if !reads.includes(kind) {
undeclared.insert((driver, kind.clone()));
}
if !wakes.wakes_on(kind) {
sleeps_through.insert((driver, kind.clone()));
}
}
match reads.kinds() {
Some(declared) => {
for kind in declared {
if !wakes.wakes_on(kind.kind) {
sleeps_through.insert((driver, kind.kind.to_owned()));
}
}
}
None => {
if !matches!(wakes, KindFilter::All) {
sleeps_through.insert((driver, "every kind".to_owned()));
}
}
}
}
rt.shutdown().await.unwrap();
assert!(
undeclared.is_empty(),
"a controller reads a kind it does not declare: {undeclared:?}"
);
assert!(
sleeps_through.is_empty(),
"a driver does not wake on a kind its controller reads: {sleeps_through:?}"
);
}
#[test]
fn the_census_sees_the_reads_the_hand_lists_missed() {
let seen = |driver| Census::of(&controller_sources(driver)).kinds;
assert!(seen(Driver::StatefulSet).contains("PersistentVolumeClaim"));
assert!(seen(Driver::DaemonSet).contains("Node"));
let binder = seen(Driver::PvBinder);
assert!(binder.contains("VolumeSnapshot") && binder.contains("VolumeSnapshotContent"));
let kubelet = Census::of(&controller_sources(Driver::Kubelet));
assert!(
kubelet.computed >= 1,
"the helper's Node read is still seen as a read"
);
assert!(
KUBELET_READS.iter().any(|g| g.kind == "Node"),
"the kubelet declares the Node it reads through node_readiness"
);
let kubelet = kubelet.kinds;
for kind in [
"Pod",
"Service",
"ConfigMap",
"Secret",
"ServiceAccount",
"PersistentVolumeClaim",
"PersistentVolume",
] {
assert!(kubelet.contains(kind), "the kubelet census missed {kind}");
}
assert_eq!(
seen(Driver::Scheduler).into_iter().collect::<Vec<_>>(),
["Node", "Pod"]
);
let registrar = Census::of(&controller_sources(Driver::CsiRegistrar));
assert!(registrar.kinds.is_empty() && registrar.computed == 0);
}
#[tokio::test]
async fn a_driver_whose_controller_reads_nothing_wakes_on_nothing() {
let rt = Runtime::start(ephemeral_test_config()).await.unwrap();
let wiring = rt
.children()
.get(Child::Driver(Driver::CsiRegistrar))
.and_then(ChildHandle::wiring)
.cloned()
.expect("the CSI registrar is spawned with its wiring");
rt.shutdown().await.unwrap();
assert_eq!(wiring.reads().kinds(), Some(&[][..]));
assert!(!wiring.wakes().wakes_on("CSINode"));
assert!(!wiring.wakes().wakes_on("Pod"));
}
#[tokio::test]
async fn the_node_manifests_driver_applies_the_directory_at_boot() {
let cfg = ephemeral_test_config();
let dir = cfg.runtime.node_manifests_dir.clone();
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("declared.yaml"),
"apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: node-declared\ndata:\n k: v\n",
)
.unwrap();
let rt = Runtime::start(cfg).await.unwrap();
let wiring = rt
.children()
.get(Child::Driver(Driver::NodeManifests))
.and_then(ChildHandle::wiring)
.cloned()
.expect("the node-manifests driver is spawned with its wiring");
assert_eq!(wiring.reads().kinds(), Some(&[][..]));
assert!(!wiring.wakes().wakes_on("ConfigMap"));
let key = engenho_store::ResourceKey::namespaced(
"",
"v1",
"ConfigMap",
"default",
"node-declared",
);
let mut found = None;
for _ in 0..100 {
if let Some(v) = rt.store().get(&key).await {
found = Some(v);
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
rt.shutdown().await.unwrap();
let object = found.expect("the declared ConfigMap was applied");
assert_eq!(object["data"]["k"], "v");
assert_eq!(
object["metadata"]["labels"][crate::node_manifests::DECLARED_BY_LABEL],
crate::node_manifests::DECLARED_BY_VALUE
);
}
fn crate_and_name(t: ControllerType) -> (String, String) {
(t.krate().to_owned(), t.ident().to_owned())
}
const FORWARDING: &[(&str, &str)] = &[
("engenho_controllers", "Arc"),
("engenho_runtime", "DeclaredHere"),
("engenho_runtime", "Tallied"),
];
const FORWARDING_NEWTYPES: &[(&str, &str)] = &[("engenho_runtime", "ConfiguredSchedulerLoop")];
#[tokio::test]
async fn every_controller_type_is_spawned_or_dormant() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("the runtime crate sits inside the workspace");
let census = Implementors::of(&workspace_sources(root), "Controller");
let mut implemented: BTreeMap<(String, String), String> = census
.concrete
.iter()
.map(|i| ((i.krate.clone(), i.ident.clone()), i.path.clone()))
.collect();
for (krate, ident) in FORWARDING_NEWTYPES {
assert!(
implemented
.remove(&((*krate).to_owned(), (*ident).to_owned()))
.is_some(),
"FORWARDING_NEWTYPES names {krate}::{ident}, which the census does not see"
);
}
let rt = Runtime::start(ephemeral_test_config()).await.unwrap();
let wirings: Vec<Wiring> = rt
.children()
.iter()
.filter_map(|(_, handle)| handle.wiring().cloned())
.collect();
rt.shutdown().await.unwrap();
let spawned: BTreeSet<(String, String)> = wirings
.iter()
.map(|w| crate_and_name(w.controller()))
.collect();
let dormant: BTreeSet<(String, String)> = Dormant::ALL
.iter()
.map(|d| crate_and_name(d.controller_type()))
.collect();
assert_eq!(
wirings.len(),
Child::all().filter(|c| c.tick_state().is_some()).count(),
"every tick loop (each driver, the node lease) records the controller it runs"
);
let unseen: Vec<&(String, String)> = spawned
.iter()
.chain(&dormant)
.filter(|t| !implemented.contains_key(*t))
.collect();
assert!(
unseen.is_empty(),
"the census does not see these controller types: {unseen:?}"
);
let forwarding: BTreeSet<(String, String)> = census
.forwarding
.iter()
.map(|i| (i.krate.clone(), i.ident.clone()))
.collect();
let named: BTreeSet<(String, String)> = FORWARDING
.iter()
.map(|(k, i)| ((*k).to_owned(), (*i).to_owned()))
.collect();
assert_eq!(
forwarding, named,
"the adapters forwarding Controller are not the ones FORWARDING names"
);
let neither: Vec<(&(String, String), &String)> = implemented
.iter()
.filter(|(t, _)| !spawned.contains(*t) && !dormant.contains(*t))
.collect();
assert!(
neither.is_empty(),
"a Controller type is neither run by a Driver nor declared Dormant: {neither:?}"
);
let both: Vec<&(String, String)> = spawned.intersection(&dormant).collect();
assert!(
both.is_empty(),
"a Dormant controller is run by a Driver; delete its row: {both:?}"
);
}
}
#[cfg(test)]
mod authenticator_wiring {
use super::{SA_ISSUER, build_authenticator};
use engenho_apiserver::RequestCreds;
fn now_secs() -> i64 {
i64::try_from(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock is after the epoch")
.as_secs(),
)
.expect("seconds fit in i64")
}
#[test]
fn a_pod_token_authenticates_as_its_service_account() {
let dir = tempfile::tempdir().expect("tempdir");
let kp = engenho_apiserver::sa_token::load_or_generate_sa_key(dir.path())
.expect("key generates on first read");
let now = now_secs();
let token = engenho_apiserver::sa_token::issue(
&kp.signing,
SA_ISSUER,
"pangea-system",
"pangea-operator",
"sa-uid-1",
&[SA_ISSUER.to_string()],
None,
now,
3600,
)
.expect("mint");
let chain = build_authenticator(dir.path(), None);
let user = chain
.authenticate(&RequestCreds {
client_cert: None,
bearer: Some(token),
})
.expect("a validly-minted token must not be a typed error");
assert_eq!(
user.username, "system:serviceaccount:pangea-system:pangea-operator",
"the workload must authenticate AS ITSELF — this being wrong is what \
forced every pod to mount an admin kubeconfig instead"
);
assert!(
user.groups.iter().any(|g| g == "system:serviceaccounts"),
"SA group membership carries the RBAC bindings; got {:?}",
user.groups
);
}
#[test]
fn the_runtime_chain_differs_observably_from_the_keyless_one() {
let dir = tempfile::tempdir().expect("tempdir");
let kp = engenho_apiserver::sa_token::load_or_generate_sa_key(dir.path()).expect("key");
let token = engenho_apiserver::sa_token::issue(
&kp.signing,
SA_ISSUER,
"default",
"probe",
"uid",
&[SA_ISSUER.to_string()],
None,
now_secs(),
3600,
)
.expect("mint");
let creds = RequestCreds {
client_cert: None,
bearer: Some(token),
};
let keyless = engenho_apiserver::ChainAuthenticator::bootstrap(None);
assert!(
keyless.authenticate(&creds).is_err(),
"the keyless chain must still REJECT — the fallback path has to stay \
a refusal, never a silent anonymous"
);
assert!(
build_authenticator(dir.path(), None)
.authenticate(&creds)
.is_ok(),
"the chain the runtime builds must ACCEPT the same token the keyless \
one refuses; if these ever agree, the key stopped being wired"
);
}
#[test]
fn an_unreadable_key_falls_back_to_rejecting_not_to_admitting() {
let dir = tempfile::tempdir().expect("tempdir");
let not_a_dir = dir.path().join("occupied");
std::fs::write(¬_a_dir, b"not a directory").expect("write");
let real = tempfile::tempdir().expect("tempdir");
let kp = engenho_apiserver::sa_token::load_or_generate_sa_key(real.path()).expect("key");
let token = engenho_apiserver::sa_token::issue(
&kp.signing,
SA_ISSUER,
"default",
"probe",
"uid",
&[SA_ISSUER.to_string()],
None,
now_secs(),
3600,
)
.expect("mint");
let chain = build_authenticator(¬_a_dir, None);
assert!(
chain
.authenticate(&RequestCreds {
client_cert: None,
bearer: Some(token)
})
.is_err(),
"with no usable key an SA bearer must be a typed 401 — losing the key \
must never widen access"
);
}
}
#[cfg(test)]
mod advertised_address {
use super::{
ApiserverTls, BootConfig, RuntimeError, SanEntry, advertised_host, advertised_server_url,
server_sans,
};
use engenho_config::EngenhoConfig;
use shikumi::TieredConfig;
fn config_with(advertise: &str, extra: &[&str]) -> BootConfig {
let mut c = EngenhoConfig::prescribed_default();
c.runtime.advertise_address = advertise.to_string();
c.runtime.tls.extra_sans = extra.iter().map(|s| (*s).to_string()).collect();
BootConfig::read(&c).expect("the runtime runs this config")
}
fn sans(boot: &BootConfig) -> Result<Vec<SanEntry>, RuntimeError> {
let ApiserverTls::SelfIssued { extra_sans } = &boot.tls else {
panic!("the prescribed default serves TLS");
};
server_sans(extra_sans, &boot.advertise_address)
}
fn bound() -> std::net::SocketAddr {
"0.0.0.0:6443".parse().expect("addr")
}
#[test]
fn advertising_a_name_puts_it_in_the_certificate() {
let sans = sans(&config_with("plo.natal.quero.cloud", &[])).expect("sans");
assert!(
sans.contains(&SanEntry::Dns("plo.natal.quero.cloud".to_string())),
"the advertised host MUST be a SAN — a kubeconfig naming an address \
the cert does not is a file that cannot work, and it fails at the \
client, not here; got {sans:?}"
);
}
#[test]
fn advertising_an_address_puts_it_in_the_certificate_as_an_ip() {
let sans = sans(&config_with("100.64.0.7:6443", &[])).expect("sans");
assert!(
sans.contains(&SanEntry::Ip("100.64.0.7".parse().unwrap())),
"got {sans:?}"
);
}
#[test]
fn the_port_reaches_the_url_and_never_the_certificate() {
let cfg = config_with("plo.quero.cloud:16443", &[]);
let sans = sans(&cfg).expect("sans");
assert!(
sans.contains(&SanEntry::Dns("plo.quero.cloud".to_string())),
"the SAN must be the bare host; got {sans:?}"
);
assert!(
!sans.iter().any(|s| s.to_string().contains(":16443")),
"no SAN may carry a port; got {sans:?}"
);
assert_eq!(
advertised_server_url(&cfg.advertise_address, bound()).as_deref(),
Some("https://plo.quero.cloud:16443"),
"the URL must keep the advertised port — a proxy or forward makes it \
legitimately different from the bound one"
);
}
#[test]
fn an_advertised_host_without_a_port_inherits_the_bound_one() {
assert_eq!(
advertised_server_url(&config_with("plo", &[]).advertise_address, bound()).as_deref(),
Some("https://plo:6443")
);
}
#[test]
fn advertising_nothing_yields_no_url_and_no_extra_san() {
let cfg = config_with("", &[]);
assert_eq!(advertised_server_url(&cfg.advertise_address, bound()), None);
assert!(
sans(&cfg).expect("sans").is_empty(),
"a node-local apiserver must gain no SANs from this path"
);
}
#[test]
fn declaring_the_advertised_name_explicitly_does_not_duplicate_it() {
let sans = sans(&config_with("plo.quero.cloud", &["plo.quero.cloud"])).expect("sans");
assert_eq!(
sans.iter()
.filter(|s| **s == SanEntry::Dns("plo.quero.cloud".to_string()))
.count(),
1,
"got {sans:?}"
);
}
#[test]
fn a_malformed_advertised_address_fails_at_start_rather_than_at_a_client() {
assert!(
sans(&config_with("https://plo:6443", &[])).is_err(),
"a URL is not an address and must be refused, not encoded"
);
}
#[test]
fn ipv6_is_bracketed_in_the_url_and_bare_in_the_certificate() {
let cfg = config_with("fd00::1", &[]);
assert_eq!(advertised_host("fd00::1"), Some("fd00::1"));
assert_eq!(
advertised_server_url(&cfg.advertise_address, bound()).as_deref(),
Some("https://[fd00::1]:6443"),
"an unbracketed IPv6 host makes a URL that will not parse"
);
assert!(
sans(&cfg)
.expect("sans")
.contains(&SanEntry::Ip("fd00::1".parse().unwrap())),
"the SAN is the bare address, unbracketed"
);
}
#[test]
fn a_bracketed_ipv6_with_a_port_splits_correctly() {
assert_eq!(advertised_host("[fd00::1]:6443"), Some("fd00::1"));
assert_eq!(
advertised_server_url(
&config_with("[fd00::1]:6443", &[]).advertise_address,
bound()
)
.as_deref(),
Some("https://[fd00::1]:6443")
);
}
}
#[cfg(test)]
mod public_ca_guard {
use super::is_loopback_only;
#[test]
fn loopback_is_the_only_safe_address() {
for addr in ["127.0.0.1:6443", "127.0.0.53:6443", "[::1]:6443"] {
assert!(
is_loopback_only(addr.parse().unwrap()),
"{addr} is loopback and a pre-seed cluster stays usable on it"
);
}
}
#[test]
fn unspecified_addresses_are_the_most_reachable_not_the_least() {
for addr in ["0.0.0.0:6443", "[::]:6443"] {
assert!(
!is_loopback_only(addr.parse().unwrap()),
"{addr} binds every interface and must NOT count as loopback"
);
}
}
#[test]
fn routable_addresses_are_not_loopback() {
for addr in ["100.91.10.110:6443", "192.168.50.3:6443", "[fd00::1]:6443"] {
assert!(!is_loopback_only(addr.parse().unwrap()), "{addr}");
}
}
}
#[cfg(test)]
mod preflight_stderr_tail {
use super::{STDERR_TAIL_MAX, stderr_tail};
#[test]
fn selects_the_cause_not_the_preamble() {
let stderr = b"OS: darwin/arm64\nprovider: applehv\nversion: 5.7.0\n\n\
Cannot connect to Podman. Please verify your connection\n\
Error: unable to connect to Podman socket: knownhosts: /Users/x/.ssh/known_hosts:9: knownhosts: missing key type pattern\n";
let tail = stderr_tail(stderr);
assert!(
tail.contains("known_hosts:9"),
"the file AND line are the whole diagnosis; got {tail:?}"
);
assert!(
!tail.contains("provider: applehv"),
"preamble must not be selected; got {tail:?}"
);
assert!(tail.starts_with(": "), "must append cleanly; got {tail:?}");
}
#[test]
fn silent_stderr_yields_nothing_to_append() {
assert_eq!(stderr_tail(b""), "");
assert_eq!(stderr_tail(b"\n \n\t\n"), "", "whitespace is not a cause");
}
#[test]
fn a_chatty_runtime_cannot_flood_the_error() {
let noisy = "x".repeat(5_000);
let tail = stderr_tail(noisy.as_bytes());
assert!(
tail.len() <= STDERR_TAIL_MAX + 8,
"unbounded tail: {} bytes",
tail.len()
);
assert!(
tail.ends_with('…'),
"truncation must be visible to the reader"
);
}
#[test]
fn truncating_multibyte_text_does_not_panic() {
let wide = "é".repeat(STDERR_TAIL_MAX);
let tail = stderr_tail(wide.as_bytes());
assert!(tail.ends_with('…'));
}
#[test]
fn invalid_utf8_still_reports_something() {
assert!(!stderr_tail(&[b'E', b'r', b'r', 0xFF, 0xFE]).is_empty());
}
}