use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use engenho_apiserver::{
ApiServer, ChainAuthenticator, ClientMaterial, RbacAuthorizer, RouterHandlerSink, RouterState,
ServerSanInputs, StoreRbacEnv, TlsMaterial, client_verifier,
handlers_from_catalog_with_admission, issue_admin_client_material, issue_server_material,
load_or_generate_ca,
};
use engenho_config::{
ConfigError, EngenhoConfig, KubeletBackendKind as CfgBackendKind, ResolvedDatapath,
};
use engenho_controllers::{
CrdController, CronJobController, DaemonSetController, DeploymentController,
DynamicHandlerSink, EndpointsController, FakeRouter, GcController, IptablesRouter, IpvsRouter,
JobController, KindFilter, NamespaceController, PodDisruptionBudgetController,
PvBinderController, ReplicaSetController, ServiceRouter, ServiceRoutingController,
StatefulSetController, WallClock, WatchDriver, WatchDriverConfig,
admission::{AdmissionChain, AdmissionMode, AdmissionWebhook},
cluster_ip::{ClusterIpDefaultingWebhook, StoreServiceIpSource},
};
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};
use engenho_scheduler::{Scheduler, make_scheduling_strategy};
use engenho_store::{
InProcessRouter, ResourceKey, StoreMesh,
command::{Reason, ResourceCommand},
default_config,
};
use engenho_types::generated_v1_34::rbac_v1::{
ClusterRole, ClusterRoleBinding, PolicyRule, RoleRef, Subject,
};
use tokio::task::JoinHandle;
use tracing::info;
use crate::error::RuntimeError;
pub struct Runtime {
config: EngenhoConfig,
store: Arc<StoreMesh>,
apiserver: ApiServer,
drivers: Vec<JoinHandle<()>>,
#[allow(dead_code)]
backend: Arc<dyn ContainerRuntime>,
}
impl Runtime {
pub async fn start(config: EngenhoConfig) -> Result<Self, RuntimeError> {
let backend = build_backend(&config);
Self::start_inner(config, backend).await
}
pub async fn start_with_backend(
config: EngenhoConfig,
backend: Arc<dyn ContainerRuntime>,
) -> Result<Self, RuntimeError> {
Self::start_inner(config, backend).await
}
async fn start_inner(
config: EngenhoConfig,
backend: Arc<dyn ContainerRuntime>,
) -> Result<Self, RuntimeError> {
config.validate()?;
let store = boot_store(&config).await?;
let timeout_s = config.runtime.leadership_timeout_seconds;
if !store
.wait_for_leadership(Duration::from_secs(u64::from(timeout_s)))
.await
{
return Err(RuntimeError::LeadershipTimeout { seconds: timeout_s });
}
info!(node = %config.runtime.node_name, "store reached leadership");
register_node(&store, &config.runtime.node_name).await?;
seed_bootstrap_rbac(&store).await?;
let listen_addr: SocketAddr =
config
.runtime
.listen_addr
.parse()
.map_err(|source| RuntimeError::ListenAddr {
addr: config.runtime.listen_addr.clone(),
source,
})?;
let mut ca_cert_pem: Option<String> = None;
let mut admin_material: Option<ClientMaterial> = None;
let tls: Option<TlsMaterial> = if config.runtime.tls.enabled {
let ca = load_or_generate_ca(&config.runtime.data_dir)
.map_err(|e| RuntimeError::Server(e.into()))?;
ca_cert_pem = Some(ca.cert_pem().to_string());
let listen_ip = san_listen_ip(listen_addr);
let material = issue_server_material(
&ca,
&ServerSanInputs {
node_name: &config.runtime.node_name,
listen_ip,
},
)
.map_err(|e| RuntimeError::Server(e.into()))?;
let verifier = client_verifier(&ca).map_err(|e| RuntimeError::Server(e.into()))?;
let material = material.with_client_verifier(verifier);
let admin =
issue_admin_client_material(&ca).map_err(|e| RuntimeError::Server(e.into()))?;
persist_admin_material(&config.runtime.data_dir, &admin)?;
admin_material = Some(admin);
Some(material)
} else {
None
};
let admin_token: Option<String> =
Some(load_or_generate_admin_token(&config.runtime.data_dir)?);
if admin_token.is_some() {
info!("bootstrap admin bearer token available at data_dir/pki/admin.token");
}
let cluster_ip_hook: Arc<dyn AdmissionWebhook> = Arc::new(ClusterIpDefaultingWebhook::new(
config.networking.service_cidr.clone(),
Arc::new(StoreServiceIpSource::new(store.clone())),
));
let admission = Arc::new(AdmissionChain::new(
vec![cluster_ip_hook],
AdmissionMode::FailClosed,
));
let authenticator = Arc::new(ChainAuthenticator::bootstrap(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);
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();
let apiserver = ApiServer::start_with_state(listen_addr, router_state, tls).await?;
let bound_addr = apiserver.local_addr();
info!(addr = %bound_addr, tls = config.runtime.tls.enabled, "apiserver bound");
if let Some(ca_pem) = ca_cert_pem.as_deref() {
write_boot_kubeconfig(&config, bound_addr, ca_pem, admin_material.as_ref())?;
}
let strategy = make_scheduling_strategy(&config.scheduler).map_err(|e| match e {
engenho_scheduler::SchedulerError::UnsupportedStrategy { requested } => {
RuntimeError::Config(ConfigError::InvalidField {
field: "scheduler.strategy".into(),
reason: format!("unsupported scheduling strategy: {requested:?}"),
})
}
other => RuntimeError::Config(ConfigError::Incoherent(other.to_string())),
})?;
let (drivers, kubelet) = spawn_drivers(&config, &store, &backend, strategy, &handler_sink);
info!(count = drivers.len(), "drivers spawned");
let log_reader: Arc<dyn engenho_apiserver::PodLogReader> =
Arc::new(KubeletLogReader { kubelet });
if let Some(pod_handler) = build_pod_log_handler(&store, &admission, log_reader) {
router_state_for_logs.register(pod_handler);
info!("registered Pod /log handler (in-process kubelet log reader)");
}
Ok(Self {
config,
store,
apiserver,
drivers,
backend,
})
}
#[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
}
pub async fn shutdown(self) -> Result<(), RuntimeError> {
for handle in &self.drivers {
handle.abort();
}
for handle in self.drivers {
let _ = handle.await;
}
self.apiserver.shutdown().await?;
let store = Arc::try_unwrap(self.store).map_err(|arc| RuntimeError::StoreStillShared {
strong_count: Arc::strong_count(&arc),
})?;
store.terminate().await?;
Ok(())
}
}
struct KubeletLogReader {
kubelet: Arc<Kubelet>,
}
#[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> {
let opts = LogOptions {
tail: query.tail_lines,
timestamps: query.timestamps,
};
self.kubelet
.container_logs(namespace, name, query.container.as_deref(), &opts)
.await
.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}"
)),
})
}
}
fn build_pod_log_handler(
store: &Arc<StoreMesh>,
admission: &Arc<AdmissionChain>,
log_reader: Arc<dyn engenho_apiserver::PodLogReader>,
) -> 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(log_reader);
Some(Arc::new(handler))
}
fn build_backend(config: &EngenhoConfig) -> Arc<dyn ContainerRuntime> {
let kind = match config.runtime.kubelet_backend {
CfgBackendKind::Podman => KubeletBackendKind::Podman,
CfgBackendKind::Fake => KubeletBackendKind::Fake,
};
make_container_runtime(kind, config.runtime.podman_binary.as_deref())
}
async fn boot_store(config: &EngenhoConfig) -> Result<Arc<StoreMesh>, RuntimeError> {
let cfg = default_config(&config.cluster.name)?;
let router = InProcessRouter::new();
let listen = "in-process://1".to_string();
if config.runtime.durable {
let store_path = config.runtime.data_dir.join("store");
let (mesh, fresh) = StoreMesh::start_or_resume(1, listen, router, cfg, store_path).await?;
info!(fresh, "durable store opened");
Ok(Arc::new(mesh))
} else {
let mesh = StoreMesh::start(1, listen, router, cfg).await?;
mesh.initialize_singleton().await?;
info!("ephemeral store initialized");
Ok(Arc::new(mesh))
}
}
async fn register_node(store: &StoreMesh, node_name: &str) -> Result<(), RuntimeError> {
let (cpu, memory) = host_capacity();
let mut value = serde_json::json!({
"kind": "Node",
"apiVersion": "v1",
"metadata": { "name": node_name },
"spec": { "unschedulable": false },
"status": {
"capacity": { "cpu": cpu, "memory": memory },
"allocatable": { "cpu": cpu, "memory": memory },
"conditions": [{ "type": "Ready", "status": "True" }]
}
});
stamp_creation_timestamp_value(&mut value);
store
.propose(ResourceCommand::Put {
key: ResourceKey::cluster_scoped("", "v1", "Node", node_name),
value,
expected: None,
reason: Reason::Operator,
})
.await?;
info!(node = %node_name, "registered schedulable node");
Ok(())
}
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_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}"
)))
}
fn host_capacity() -> (String, String) {
let cpus = std::thread::available_parallelism()
.map(std::num::NonZeroUsize::get)
.unwrap_or(1);
(cpus.to_string(), "8Gi".to_string())
}
fn san_listen_ip(addr: SocketAddr) -> Option<std::net::IpAddr> {
let ip = addr.ip();
if ip.is_unspecified() { None } else { Some(ip) }
}
fn write_boot_kubeconfig(
config: &EngenhoConfig,
bound_addr: SocketAddr,
ca_pem: &str,
admin: Option<&ClientMaterial>,
) -> Result<(), RuntimeError> {
let server_url = loopback_server_url(bound_addr);
let yaml = match admin {
Some(admin) => emit_kubeconfig_with_admin(
&config.cluster.name,
&server_url,
ca_pem.as_bytes(),
admin.cert_pem.as_bytes(),
admin.key_pem.as_bytes(),
),
None => emit_kubeconfig(&config.cluster.name, &server_url, ca_pem.as_bytes()),
}
.map_err(|e| RuntimeError::Kubeconfig(e.to_string()))?;
let path = config.runtime.data_dir.join("kubeconfig");
write_kubeconfig_file(&path, &yaml)?;
info!(path = %path.display(), server = %server_url, admin = admin.is_some(), "kubeconfig written");
Ok(())
}
fn persist_admin_material(
data_dir: &std::path::Path,
admin: &ClientMaterial,
) -> Result<(), RuntimeError> {
let pki = data_dir.join("pki");
create_pki_dir(&pki)?;
write_at_mode(&pki.join("admin.crt"), &admin.cert_pem, 0o644)?;
write_at_mode(&pki.join("admin.key"), &admin.key_pem, 0o600)?;
Ok(())
}
fn load_or_generate_admin_token(data_dir: &std::path::Path) -> Result<String, RuntimeError> {
let pki = data_dir.join("pki");
let token_path = pki.join("admin.token");
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(&pki)?;
let token = random_admin_token();
write_at_mode(&token_path, &token, 0o600)?;
Ok(token)
}
fn random_admin_token() -> String {
let mut bytes = [0u8; 32];
if getrandom::fill(&mut bytes).is_err() {
let pid = std::process::id().to_le_bytes();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
.to_le_bytes();
for (i, b) in bytes.iter_mut().enumerate() {
*b = pid[i % pid.len()] ^ nanos[i % nanos.len()] ^ (i as u8);
}
}
let mut out = String::with_capacity(64);
for b in bytes {
out.push(char::from_digit(u32::from(b >> 4), 16).unwrap());
out.push(char::from_digit(u32::from(b & 0xf), 16).unwrap());
}
out
}
#[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) -> 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, 0o600)
}
fn spawn_drivers(
config: &EngenhoConfig,
store: &Arc<StoreMesh>,
backend: &Arc<dyn ContainerRuntime>,
strategy: Box<dyn engenho_scheduler::SchedulingStrategy>,
handler_sink: &Arc<dyn DynamicHandlerSink>,
) -> (Vec<JoinHandle<()>>, Arc<Kubelet>) {
let mut handles = Vec::new();
let ns: Option<String> = {
let n = &config.controllers.namespace;
if n.is_empty() { None } else { Some(n.clone()) }
};
let debounce = Duration::from_millis(u64::from(config.controllers.debounce_milliseconds));
let fallback = Duration::from_secs(u64::from(config.controllers.fallback_interval_seconds));
let driver_config = |kinds: &[&str]| WatchDriverConfig {
filter: KindFilter::Kinds(kinds.iter().map(|k| (*k).to_string()).collect()),
debounce,
fallback_interval: fallback,
};
let enable = &config.controllers.enable;
if enable.deployment {
let c = DeploymentController::new(store.clone(), ns.clone());
handles.push(
WatchDriver::new(
c,
store.clone(),
driver_config(&["Deployment", "ReplicaSet"]),
)
.spawn(),
);
}
if enable.replicaset {
let c = ReplicaSetController::new(store.clone(), ns.clone());
handles.push(
WatchDriver::new(c, store.clone(), driver_config(&["ReplicaSet", "Pod"])).spawn(),
);
}
if enable.statefulset {
let c = StatefulSetController::new(store.clone(), ns.clone());
handles.push(
WatchDriver::new(c, store.clone(), driver_config(&["StatefulSet", "Pod"])).spawn(),
);
}
if enable.daemonset {
let c = DaemonSetController::new(store.clone(), ns.clone());
handles.push(
WatchDriver::new(
c,
store.clone(),
driver_config(&["DaemonSet", "Pod", "Node"]),
)
.spawn(),
);
}
if enable.job {
let c = JobController::new(store.clone(), ns.clone());
handles.push(WatchDriver::new(c, store.clone(), driver_config(&["Job", "Pod"])).spawn());
}
if enable.cronjob {
let c = CronJobController::new(store.clone(), Arc::new(WallClock), ns.clone());
handles
.push(WatchDriver::new(c, store.clone(), driver_config(&["CronJob", "Job"])).spawn());
}
if enable.pdb {
let c = PodDisruptionBudgetController::new(store.clone(), ns.clone());
handles.push(
WatchDriver::new(
c,
store.clone(),
driver_config(&["PodDisruptionBudget", "Pod"]),
)
.spawn(),
);
}
if enable.endpoints {
let c = EndpointsController::new(store.clone(), ns.clone());
handles.push(
WatchDriver::new(
c,
store.clone(),
driver_config(&["Service", "Pod", "Endpoints", "EndpointSlice"]),
)
.spawn(),
);
}
if enable.service_routing {
let resolved = config
.networking
.datapath_mode
.resolve(cfg!(target_os = "linux"));
let backend = make_service_router(resolved);
info!(
datapath = backend.name(),
mode = ?config.networking.datapath_mode,
"service routing backend selected"
);
let c = ServiceRoutingController::new(store.clone(), backend, ns.clone());
handles.push(
WatchDriver::new(
c,
store.clone(),
driver_config(&["Service", "Endpoints", "EndpointSlice"]),
)
.spawn(),
);
}
if enable.gc {
let c = GcController::new(store.clone(), ns.clone());
handles.push(
WatchDriver::new(
c,
store.clone(),
WatchDriverConfig {
filter: KindFilter::All,
debounce,
fallback_interval: fallback,
},
)
.spawn(),
);
}
if enable.namespace {
let c = NamespaceController::new(store.clone(), ns.clone());
handles.push(WatchDriver::new(c, store.clone(), driver_config(&["Namespace"])).spawn());
}
if enable.pv_binder {
let local_path_root = config
.runtime
.data_dir
.join("local-path")
.to_string_lossy()
.into_owned();
let c = PvBinderController::new(store.clone(), ns.clone(), local_path_root);
handles.push(
WatchDriver::new(
c,
store.clone(),
driver_config(&["PersistentVolumeClaim", "PersistentVolume", "StorageClass"]),
)
.spawn(),
);
}
if enable.crd {
let c = CrdController::new(store.clone(), handler_sink.clone());
handles.push(
WatchDriver::new(
c,
store.clone(),
driver_config(&["CustomResourceDefinition"]),
)
.spawn(),
);
}
{
let c = Scheduler::new(store.clone(), strategy, ns.clone());
handles.push(WatchDriver::new(c, store.clone(), driver_config(&["Pod", "Node"])).spawn());
}
let kubelet = Arc::new(Kubelet::new(
store.clone(),
backend.clone(),
config.runtime.node_name.clone(),
));
handles.push(WatchDriver::new(kubelet.clone(), store.clone(), driver_config(&["Pod"])).spawn());
(handles, kubelet)
}
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 {
use super::*;
use engenho_config::KubeletBackendKind as CfgKind;
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.controllers.fallback_interval_seconds = 1;
cfg.controllers.debounce_milliseconds = 20;
cfg
}
#[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!(rt.drivers.len(), 15);
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(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!(rt.drivers.len(), 14);
rt.shutdown().await.unwrap();
}
}