From 29cff3bf848c0a910d3042362323db24e9367424 Mon Sep 17 00:00:00 2001 From: lucalise Date: Thu, 8 Jan 2026 23:06:08 -0800 Subject: [PATCH] feat(homelab)!: parse from env or config.toml to allow containerization --- nix/homelab/Cargo.toml | 6 +- nix/homelab/src/commands/generate_routes.rs | 12 +-- nix/homelab/src/config.rs | 87 +++++++++++++++++++++ nix/homelab/src/error.rs | 6 ++ nix/homelab/src/main.rs | 33 ++------ 5 files changed, 112 insertions(+), 32 deletions(-) create mode 100644 nix/homelab/src/config.rs diff --git a/nix/homelab/Cargo.toml b/nix/homelab/Cargo.toml index eca9ff9..ad0d17c 100644 --- a/nix/homelab/Cargo.toml +++ b/nix/homelab/Cargo.toml @@ -12,5 +12,9 @@ serde = { version = "1.0.228", features = ["serde_derive"] } ssh2 = "0.9.5" thiserror = "2.0.17" tokio = { version = "1.49.0", features = ["macros", "rt"] } -toml = "0.9.10" +toml = { version = "0.9.10", optional = true } urlencoding = "2" + +[features] +default = ["file-config"] +file-config = ["dep:toml"] diff --git a/nix/homelab/src/commands/generate_routes.rs b/nix/homelab/src/commands/generate_routes.rs index edcdf4a..c26a03e 100644 --- a/nix/homelab/src/commands/generate_routes.rs +++ b/nix/homelab/src/commands/generate_routes.rs @@ -2,7 +2,7 @@ use std::collections::BTreeSet; use serde::{Deserialize, Serialize}; -use crate::{Config, HelperError}; +use crate::{HelperError, config::Config}; #[derive(Serialize, Deserialize, Default, Debug, Clone)] pub struct Route { @@ -30,8 +30,8 @@ enum RouteKind { TCP, } -pub fn generate_routes(config: &Config) -> Result<(), HelperError> { - let routes = config.routes.iter().enumerate().try_fold( +pub fn generate_routes(routes: &Vec) -> Result<(), HelperError> { + let routes_content = routes.iter().enumerate().try_fold( String::new(), |mut acc, (i, r)| -> Result<_, HelperError> { if i > 0 { @@ -41,10 +41,10 @@ pub fn generate_routes(config: &Config) -> Result<(), HelperError> { Ok(acc) }, )?; - let chains = generate_chains(&config.routes); - std::fs::write("kustomize/routes.yaml", &routes)?; + let chains = generate_chains(&routes); + std::fs::write("kustomize/routes.yaml", &routes_content)?; std::fs::write("kustomize/traefik/chains.yaml", &chains)?; - println!("Wrote: {}", routes); + println!("Wrote: {}", routes_content); Ok(()) } diff --git a/nix/homelab/src/config.rs b/nix/homelab/src/config.rs new file mode 100644 index 0000000..ed99e72 --- /dev/null +++ b/nix/homelab/src/config.rs @@ -0,0 +1,87 @@ +#[cfg(not(feature = "file-config"))] +use std::{collections::HashSet, env}; + +use serde::{Deserialize, Serialize}; + +use crate::{PiHoleConfig, RouterConfig, commands::generate_routes::Route, error::Result}; + +#[derive(Serialize, Deserialize)] +pub struct Config { + pub routes: Option>, + pub pihole: Option, + pub router: Option, +} + +#[cfg(not(feature = "file-config"))] +struct EnvCollector { + missing: Vec<&'static str>, +} + +#[cfg(not(feature = "file-config"))] +impl EnvCollector { + fn new() -> Self { + Self { + missing: Vec::new(), + } + } + + fn get(&mut self, key: &'static str) -> Option { + match env::var(key) { + Ok(val) => Some(val), + Err(_) => { + self.missing.push(key); + None + } + } + } + + fn finish(self) -> Result<()> { + if self.missing.is_empty() { + Ok(()) + } else { + Err(crate::error::Error::MissingEnvVars(self.missing.join(", "))) + } + } +} + +#[cfg(not(feature = "file-config"))] +pub fn parse_config() -> Result { + let mut env = EnvCollector::new(); + + let pihole_url = env.get("PIHOLE_URL"); + let pihole_password = env.get("PIHOLE_PASSWORD_FILE"); + let pihole_hosts = env.get("PIHOLE_EXTRA_HOSTS"); + let router_host = env.get("ROUTER_HOST"); + let router_user = env.get("ROUTER_USER"); + let router_key = env.get("ROUTER_KEY_PATH"); + let router_lease = env.get("ROUTER_LEASE_FILE"); + + env.finish()?; + + Ok(Config { + routes: None, + pihole: Some(PiHoleConfig { + url: pihole_url.unwrap(), + password_file: pihole_password.unwrap(), + extra_hosts: Some( + pihole_hosts + .unwrap() + .split('\n') + .map(String::from) + .collect::>(), + ), + }), + router: Some(RouterConfig { + host: router_host.unwrap(), + user: router_user.unwrap(), + key_path: router_key.unwrap().into(), + lease_file: router_lease.unwrap(), + }), + }) +} + +#[cfg(feature = "file-config")] +pub fn parse_config() -> Result { + let bytes = std::fs::read("./config.toml")?; + Ok(toml::from_slice::(&bytes)?) +} diff --git a/nix/homelab/src/error.rs b/nix/homelab/src/error.rs index 6d064b0..6508125 100644 --- a/nix/homelab/src/error.rs +++ b/nix/homelab/src/error.rs @@ -14,6 +14,12 @@ pub enum Error { Http(#[from] reqwest::Error), #[error("Pi-hole API error: {0}")] PiHole(String), + #[cfg(not(feature = "file-config"))] + #[error("missing environment variables: {0}")] + MissingEnvVars(String), + #[cfg(feature = "file-config")] + #[error("error parsing toml: {0}")] + TomlParse(#[from] toml::de::Error), } pub type Result = std::result::Result; diff --git a/nix/homelab/src/main.rs b/nix/homelab/src/main.rs index 7e08ed8..10183b3 100644 --- a/nix/homelab/src/main.rs +++ b/nix/homelab/src/main.rs @@ -1,4 +1,5 @@ mod commands; +mod config; mod dns; mod error; mod lease_parser; @@ -13,6 +14,7 @@ use clap::{CommandFactory, Parser}; use serde::{Deserialize, Serialize}; use thiserror::Error; +use crate::config::parse_config; use crate::{ commands::{ Commands, @@ -35,19 +37,10 @@ struct Cli { pub enum HelperError { #[error("error reading file")] ReadFile(#[from] std::io::Error), - #[error("error parsing config toml")] - TomlError(#[from] toml::de::Error), #[error("entrypoint required for tcproute: {0:?}")] TCPEntryPoint(String), } -#[derive(Serialize, Deserialize)] -pub struct Config { - routes: Vec, - pihole: Option, - router: Option, -} - #[derive(Serialize, Deserialize)] pub struct PiHoleConfig { url: String, @@ -63,30 +56,20 @@ pub struct RouterConfig { lease_file: String, } -pub fn parse_config>(path: T) -> anyhow::Result { - let bytes = std::fs::read(&path).context(format!( - "failed to read config file: {}", - path.as_ref().display() - ))?; - Ok(toml::from_slice::(&bytes)?) -} - -fn env_or>(key: &str, fallback: S) -> String { - env::var(key).unwrap_or_else(|_| fallback.into()) -} - #[tokio::main(flavor = "current_thread")] async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); match &cli.command { Some(Commands::GenerateRoutes {}) => { - let config = parse_config("./config.toml")?; - generate_routes(&config)?; + let config = parse_config()?; + let routes = config + .routes + .context("routes in config are required for generating route manifests")?; + generate_routes(&routes)?; } Some(Commands::SyncDNS {}) => { - let config_path = env_or("CONFIG_PATH", "./config.toml"); - let config = parse_config(config_path)?; + let config = parse_config()?; let pihole_config = config .pihole .context("pihole configuration is necessary for syncing dns")?;