use std::path::PathBuf;
use engenho_apiserver::load_or_generate_ca;
use engenho_config::{ConfigTier, EngenhoConfig, TieredConfig, render_provenance};
use engenho_kube_client::{emit_kubeconfig, emit_kubeconfig_with_admin};
use engenho_runtime::Runtime;
use tracing_subscriber::EnvFilter;
#[derive(Debug, PartialEq, Eq)]
enum Command {
Daemon,
Kubeconfig(Vec<String>),
ConfigShow(Option<String>),
ConfigDiff(String, String),
}
impl Command {
fn parse(mut args: impl Iterator<Item = String>) -> anyhow::Result<Self> {
match args.next().as_deref() {
None | Some("daemon") => Ok(Command::Daemon),
Some("kubeconfig") => Ok(Command::Kubeconfig(args.collect())),
Some("config-show") => Ok(Command::ConfigShow(args.next())),
Some("config-diff") => match (args.next(), args.next()) {
(Some(from), Some(to)) => Ok(Command::ConfigDiff(from, to)),
_ => Err(anyhow::anyhow!(
"config-diff requires two tier args: config-diff <from> <to> (bare|discovered|default|<yaml-path>)"
)),
},
Some(other) => Err(anyhow::anyhow!(
"unknown subcommand {other:?} (supported: daemon [or no args] to boot the daemon, kubeconfig, config-show [tier], config-diff <from> <to>)"
)),
}
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
match Command::parse(std::env::args().skip(1))? {
Command::Daemon => run_daemon().await,
Command::Kubeconfig(flags) => run_kubeconfig(flags.into_iter()),
Command::ConfigShow(tier) => run_config_show(tier),
Command::ConfigDiff(from, to) => run_config_diff(&from, &to),
}
}
async fn run_daemon() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| {
EnvFilter::new("engenho=info,engenho_runtime=info,engenho_store=info")
}))
.init();
tracing::info!(
version = env!("CARGO_PKG_VERSION"),
"engenho — typed, attested, Rust-native Kubernetes runtime"
);
let (config, provenance) = EngenhoConfig::resolve_progressively()?.into_parts();
tracing::info!(
cluster = %config.cluster.name,
node = %config.runtime.node_name,
listen = %config.runtime.listen_addr,
durable = config.runtime.durable,
tls = config.runtime.tls.enabled,
"loaded config"
);
let node_name_tier = provenance
.provenance_of(&["runtime", "node_name"])
.map_or("?", |p| p.tier().as_str());
tracing::info!(
leaves = provenance.len(),
tiers = %provenance
.contributing_tiers()
.iter()
.map(|t| t.as_str())
.collect::<Vec<_>>()
.join(", "),
node_name_from = node_name_tier,
"config provenance",
);
let runtime = Runtime::start(config).await?;
tracing::info!(addr = %runtime.local_addr(), "engenho up — apiserver bound");
tokio::signal::ctrl_c().await?;
tracing::info!("shutdown signal received");
runtime.shutdown().await?;
tracing::info!("engenho stopped cleanly");
Ok(())
}
fn run_kubeconfig(args: impl Iterator<Item = String>) -> anyhow::Result<()> {
let config = EngenhoConfig::discover()?;
let mut data_dir: Option<PathBuf> = None;
let mut server: Option<String> = None;
let mut args = args.peekable();
while let Some(flag) = args.next() {
match flag.as_str() {
"--data-dir" => {
data_dir = Some(PathBuf::from(args.next().ok_or_else(|| {
anyhow::anyhow!("--data-dir requires a path argument")
})?));
}
"--server" => {
server = Some(
args.next()
.ok_or_else(|| anyhow::anyhow!("--server requires a URL argument"))?,
);
}
other => {
return Err(anyhow::anyhow!(
"unknown flag {other:?} (supported: --data-dir, --server)"
));
}
}
}
let data_dir = data_dir.unwrap_or_else(|| config.runtime.data_dir.clone());
let ca = load_or_generate_ca(&data_dir).map_err(|e| anyhow::anyhow!("load cluster CA: {e}"))?;
let server_url = server.unwrap_or_else(|| default_server_url(&config));
let pki = data_dir.join("pki");
let admin_cert = std::fs::read(pki.join("admin.crt")).ok();
let admin_key = std::fs::read(pki.join("admin.key")).ok();
let yaml = match (admin_cert, admin_key) {
(Some(cert), Some(key)) => emit_kubeconfig_with_admin(
&config.cluster.name,
&server_url,
ca.cert_pem().as_bytes(),
&cert,
&key,
),
_ => emit_kubeconfig(&config.cluster.name, &server_url, ca.cert_pem().as_bytes()),
}
.map_err(|e| anyhow::anyhow!("emit kubeconfig: {e}"))?;
print!("{yaml}");
Ok(())
}
fn default_server_url(config: &EngenhoConfig) -> String {
let port = config
.runtime
.listen_addr
.rsplit(':')
.next()
.and_then(|p| p.parse::<u16>().ok())
.filter(|p| *p != 0)
.unwrap_or(6443);
let mut url = String::from("https://127.0.0.1:");
url.push_str(&port.to_string());
url
}
fn run_config_show(tier_arg: Option<String>) -> anyhow::Result<()> {
let tier = match tier_arg {
Some(s) => ConfigTier::from_str_or_default(&s),
None => ConfigTier::from_env("ENGENHO_TIER"),
};
match tier {
ConfigTier::Default => {
let resolution = EngenhoConfig::resolve_progressively()?;
print!("{}", resolution.value().to_yaml()?);
print!("{}", render_provenance(resolution.provenance()));
}
other => {
print!("{}", EngenhoConfig::resolve_tier(other).to_yaml()?);
}
}
Ok(())
}
fn run_config_diff(from: &str, to: &str) -> anyhow::Result<()> {
let from_cfg = EngenhoConfig::resolve_tier(ConfigTier::from_str_or_default(from));
let to_cfg = EngenhoConfig::resolve_tier(ConfigTier::from_str_or_default(to));
print!("{}", to_cfg.diff_against(&from_cfg).render_unified());
Ok(())
}
#[cfg(test)]
mod tests {
use super::Command;
fn parse(argv: &[&str]) -> anyhow::Result<Command> {
Command::parse(argv.iter().map(|s| (*s).to_string()))
}
#[test]
fn bare_no_args_is_daemon() {
assert_eq!(parse(&[]).unwrap(), Command::Daemon);
}
#[test]
fn daemon_subcommand_is_daemon() {
assert_eq!(parse(&["daemon"]).unwrap(), Command::Daemon);
}
#[test]
fn bare_and_daemon_subcommand_agree() {
assert_eq!(parse(&[]).unwrap(), parse(&["daemon"]).unwrap());
}
#[test]
fn kubeconfig_subcommand_carries_flags() {
assert_eq!(
parse(&["kubeconfig", "--data-dir", "/var/lib/engenho-rio"]).unwrap(),
Command::Kubeconfig(vec![
"--data-dir".to_string(),
"/var/lib/engenho-rio".to_string(),
]),
);
}
#[test]
fn unknown_subcommand_errors() {
let err = parse(&["frobnicate"]).unwrap_err().to_string();
assert!(
err.contains("frobnicate"),
"error should name the bad verb: {err}"
);
assert!(err.contains("daemon"), "error should list `daemon`: {err}");
assert!(
err.contains("kubeconfig"),
"error should list `kubeconfig`: {err}"
);
assert!(
err.contains("config-show"),
"error should list `config-show`: {err}"
);
assert!(
err.contains("config-diff"),
"error should list `config-diff`: {err}"
);
}
#[test]
fn config_show_without_tier() {
assert_eq!(parse(&["config-show"]).unwrap(), Command::ConfigShow(None));
}
#[test]
fn config_show_with_tier() {
assert_eq!(
parse(&["config-show", "bare"]).unwrap(),
Command::ConfigShow(Some("bare".to_string())),
);
}
#[test]
fn config_diff_carries_two_tiers() {
assert_eq!(
parse(&["config-diff", "bare", "default"]).unwrap(),
Command::ConfigDiff("bare".to_string(), "default".to_string()),
);
}
#[test]
fn config_diff_requires_two_args() {
assert!(parse(&["config-diff", "bare"]).is_err());
assert!(parse(&["config-diff"]).is_err());
}
}