feat(homelab)!: parse from env or config.toml to allow containerization
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -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
87
nix/homelab/src/config.rs
Normal 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)?)
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
Reference in New Issue
Block a user