//! Runtime configuration read from environment variables (see `.env.example`). use std::net::SocketAddr; #[derive(Clone, Debug)] pub struct Config { pub database_url: String, pub jwt_secret: String, /// 64 hex chars; encrypts secrets at rest. pub master_key: String, /// Use fake host adapters (dev machines without apt/kubectl). pub fake_host: bool, /// Path to a kubeconfig; None infers. Defaults to the microk8s client config if present. pub kubeconfig: Option, /// kubectl invocation for backups, e.g. ["/snap/bin/microk8s", "kubectl"]. pub kubectl: Vec, /// containerd socket for local image scans (default: microk8s socket if present). pub containerd_socket: Option, /// Scratch directory for backup archives. pub work_dir: std::path::PathBuf, pub bind: SocketAddr, pub bootstrap_admin: Option<(String, String)>, pub cookie_secure: bool, /// Login attempts per IP and minute. pub login_rate_limit: u32, pub frontend_dir: String, } impl Config { pub fn from_env() -> anyhow::Result { let env = |k: &str| std::env::var(k).ok().filter(|v| !v.is_empty()); let jwt_secret = env("JWT_SECRET").ok_or_else(|| anyhow::anyhow!("JWT_SECRET is required"))?; anyhow::ensure!( jwt_secret.len() >= 32, "JWT_SECRET must be at least 32 characters" ); let master_key = env("MASTER_KEY") .ok_or_else(|| anyhow::anyhow!("MASTER_KEY is required (64 hex characters)"))?; Ok(Self { database_url: env("DATABASE_URL") .unwrap_or_else(|| "sqlite://data/monitoring.db?mode=rwc".into()), jwt_secret, master_key, fake_host: env("FAKE_HOST").is_some_and(|v| v == "true" || v == "1"), kubectl: env("KUBECTL") .map(|v| v.split_whitespace().map(String::from).collect()) .unwrap_or_else(|| { if std::path::Path::new("/snap/bin/microk8s").exists() { vec!["/snap/bin/microk8s".into(), "kubectl".into()] } else { vec!["kubectl".into()] } }), containerd_socket: env("CONTAINERD_ADDRESS").or_else(|| { let microk8s = "/var/snap/microk8s/common/run/containerd.sock"; std::path::Path::new(microk8s) .exists() .then(|| microk8s.to_string()) }), work_dir: env("WORK_DIR") .map(Into::into) .unwrap_or_else(|| "data/work".into()), kubeconfig: env("KUBECONFIG").or_else(|| { let microk8s = "/var/snap/microk8s/current/credentials/client.config"; std::path::Path::new(microk8s) .exists() .then(|| microk8s.to_string()) }), bind: env("BIND") .unwrap_or_else(|| "127.0.0.1:8080".into()) .parse()?, bootstrap_admin: env("BOOTSTRAP_ADMIN_EMAIL").zip(env("BOOTSTRAP_ADMIN_PASSWORD")), cookie_secure: env("COOKIE_SECURE").is_some_and(|v| v == "true" || v == "1"), login_rate_limit: env("LOGIN_RATE_LIMIT") .and_then(|v| v.parse().ok()) .unwrap_or(10), frontend_dir: env("FRONTEND_DIR").unwrap_or_else(|| "../frontend/dist".into()), }) } }