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"
|
ssh2 = "0.9.5"
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0.17"
|
||||||
tokio = { version = "1.49.0", features = ["macros", "rt"] }
|
tokio = { version = "1.49.0", features = ["macros", "rt"] }
|
||||||
toml = "0.9.10"
|
toml = { version = "0.9.10", optional = true }
|
||||||
urlencoding = "2"
|
urlencoding = "2"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["file-config"]
|
||||||
|
file-config = ["dep:toml"]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::collections::BTreeSet;
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{Config, HelperError};
|
use crate::{HelperError, config::Config};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||||
pub struct Route {
|
pub struct Route {
|
||||||
@@ -30,8 +30,8 @@ enum RouteKind {
|
|||||||
TCP,
|
TCP,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_routes(config: &Config) -> Result<(), HelperError> {
|
pub fn generate_routes(routes: &Vec<Route>) -> Result<(), HelperError> {
|
||||||
let routes = config.routes.iter().enumerate().try_fold(
|
let routes_content = routes.iter().enumerate().try_fold(
|
||||||
String::new(),
|
String::new(),
|
||||||
|mut acc, (i, r)| -> Result<_, HelperError> {
|
|mut acc, (i, r)| -> Result<_, HelperError> {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
@@ -41,10 +41,10 @@ pub fn generate_routes(config: &Config) -> Result<(), HelperError> {
|
|||||||
Ok(acc)
|
Ok(acc)
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
let chains = generate_chains(&config.routes);
|
let chains = generate_chains(&routes);
|
||||||
std::fs::write("kustomize/routes.yaml", &routes)?;
|
std::fs::write("kustomize/routes.yaml", &routes_content)?;
|
||||||
std::fs::write("kustomize/traefik/chains.yaml", &chains)?;
|
std::fs::write("kustomize/traefik/chains.yaml", &chains)?;
|
||||||
println!("Wrote: {}", routes);
|
println!("Wrote: {}", routes_content);
|
||||||
|
|
||||||
Ok(())
|
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),
|
Http(#[from] reqwest::Error),
|
||||||
#[error("Pi-hole API error: {0}")]
|
#[error("Pi-hole API error: {0}")]
|
||||||
PiHole(String),
|
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>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
mod commands;
|
mod commands;
|
||||||
|
mod config;
|
||||||
mod dns;
|
mod dns;
|
||||||
mod error;
|
mod error;
|
||||||
mod lease_parser;
|
mod lease_parser;
|
||||||
@@ -13,6 +14,7 @@ use clap::{CommandFactory, Parser};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::config::parse_config;
|
||||||
use crate::{
|
use crate::{
|
||||||
commands::{
|
commands::{
|
||||||
Commands,
|
Commands,
|
||||||
@@ -35,19 +37,10 @@ struct Cli {
|
|||||||
pub enum HelperError {
|
pub enum HelperError {
|
||||||
#[error("error reading file")]
|
#[error("error reading file")]
|
||||||
ReadFile(#[from] std::io::Error),
|
ReadFile(#[from] std::io::Error),
|
||||||
#[error("error parsing config toml")]
|
|
||||||
TomlError(#[from] toml::de::Error),
|
|
||||||
#[error("entrypoint required for tcproute: {0:?}")]
|
#[error("entrypoint required for tcproute: {0:?}")]
|
||||||
TCPEntryPoint(String),
|
TCPEntryPoint(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct Config {
|
|
||||||
routes: Vec<Route>,
|
|
||||||
pihole: Option<PiHoleConfig>,
|
|
||||||
router: Option<RouterConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct PiHoleConfig {
|
pub struct PiHoleConfig {
|
||||||
url: String,
|
url: String,
|
||||||
@@ -63,30 +56,20 @@ pub struct RouterConfig {
|
|||||||
lease_file: String,
|
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")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
match &cli.command {
|
match &cli.command {
|
||||||
Some(Commands::GenerateRoutes {}) => {
|
Some(Commands::GenerateRoutes {}) => {
|
||||||
let config = parse_config("./config.toml")?;
|
let config = parse_config()?;
|
||||||
generate_routes(&config)?;
|
let routes = config
|
||||||
|
.routes
|
||||||
|
.context("routes in config are required for generating route manifests")?;
|
||||||
|
generate_routes(&routes)?;
|
||||||
}
|
}
|
||||||
Some(Commands::SyncDNS {}) => {
|
Some(Commands::SyncDNS {}) => {
|
||||||
let config_path = env_or("CONFIG_PATH", "./config.toml");
|
let config = parse_config()?;
|
||||||
let config = parse_config(config_path)?;
|
|
||||||
let pihole_config = config
|
let pihole_config = config
|
||||||
.pihole
|
.pihole
|
||||||
.context("pihole configuration is necessary for syncing dns")?;
|
.context("pihole configuration is necessary for syncing dns")?;
|
||||||
|
|||||||
Reference in New Issue
Block a user