feat(homelab)!: parse from env or config.toml to allow containerization

This commit is contained in:
2026-01-08 23:06:08 -08:00
parent dd904c151d
commit 29cff3bf84
5 changed files with 112 additions and 32 deletions

View File

@@ -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<Route>) -> 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(())
}

87
nix/homelab/src/config.rs Normal file
View File

@@ -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<Vec<Route>>,
pub pihole: Option<PiHoleConfig>,
pub router: Option<RouterConfig>,
}
#[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<String> {
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<Config> {
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::<HashSet<String>>(),
),
}),
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<Config> {
let bytes = std::fs::read("./config.toml")?;
Ok(toml::from_slice::<Config>(&bytes)?)
}

View File

@@ -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<T> = std::result::Result<T, Error>;

View File

@@ -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<Route>,
pihole: Option<PiHoleConfig>,
router: Option<RouterConfig>,
}
#[derive(Serialize, Deserialize)]
pub struct PiHoleConfig {
url: String,
@@ -63,30 +56,20 @@ pub struct RouterConfig {
lease_file: String,
}
pub fn parse_config<T: AsRef<Path>>(path: T) -> anyhow::Result<Config> {
let bytes = std::fs::read(&path).context(format!(
"failed to read config file: {}",
path.as_ref().display()
))?;
Ok(toml::from_slice::<Config>(&bytes)?)
}
fn env_or<S: Into<String>>(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")?;