feat(homelab)!: create interface for homelab management, use templating for route generation & support more options and route types

This commit is contained in:
2026-01-11 14:57:19 -08:00
parent 29cff3bf84
commit 73f8cb91c4
45 changed files with 3907 additions and 109 deletions

View File

@@ -1,8 +1,9 @@
use std::collections::BTreeSet;
use askama::Template;
use serde::{Deserialize, Serialize};
use crate::{HelperError, config::Config};
use crate::HelperError;
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub struct Route {
@@ -21,15 +22,47 @@ impl Route {
fn hostname(&self) -> &str {
self.hostname.as_ref().unwrap_or(&self.name)
}
fn service(&self) -> &str {
self.service.as_ref().unwrap_or(&self.name)
}
}
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum RouteKind {
#[default]
HTTP,
TCP,
}
#[derive(Template)]
#[template(path = "httproute.yaml", escape = "none")]
struct HttpRoute<'a> {
name: &'a str,
namespace: &'a str,
hostname: &'a str,
service: &'a str,
port: i16,
private: bool,
}
#[derive(Template)]
#[template(path = "ingressroutetcp.yaml", escape = "none")]
struct TcpRoute<'a> {
name: &'a str,
namespace: &'a str,
entrypoint: &'a str,
service: &'a str,
port: i16,
private: bool,
}
#[derive(Template)]
#[template(path = "http_middleware_chain.yaml", escape = "none")]
struct MiddlewareChain<'a> {
namespace: &'a str,
}
pub fn generate_routes(routes: &Vec<Route>) -> Result<(), HelperError> {
let routes_content = routes.iter().enumerate().try_fold(
String::new(),
@@ -41,7 +74,7 @@ pub fn generate_routes(routes: &Vec<Route>) -> Result<(), HelperError> {
Ok(acc)
},
)?;
let chains = generate_chains(&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_content);
@@ -50,122 +83,62 @@ pub fn generate_routes(routes: &Vec<Route>) -> Result<(), HelperError> {
}
fn generate_route(route: &Route) -> Result<String, HelperError> {
Ok(match route.kind {
RouteKind::HTTP => generate_http_route(route),
RouteKind::TCP => generate_tcp_route(route)?,
})
match route.kind {
RouteKind::HTTP => {
let template = HttpRoute {
name: &route.name,
namespace: &route.namespace,
hostname: route.hostname(),
service: route.service(),
port: route.port,
private: route.private,
};
Ok(template.render()?)
}
RouteKind::TCP => {
let entrypoint = route
.entrypoint
.as_ref()
.ok_or(HelperError::TCPEntryPoint(route.name.clone()))?;
let template = TcpRoute {
name: &route.name,
namespace: &route.namespace,
entrypoint,
service: route.service(),
port: route.port,
private: route.private,
};
Ok(template.render()?)
}
}
}
fn generate_chains(routes: &[Route]) -> String {
fn generate_chains(routes: &[Route]) -> Result<String, HelperError> {
let namespaces = routes
.iter()
.filter_map(|r| {
if !r.private {
return None;
}
Some(r.namespace.as_str())
})
.filter_map(|r| r.private.then_some((r.kind.clone(), &r.namespace)))
.collect::<BTreeSet<_>>();
namespaces
.iter()
.enumerate()
.fold(String::new(), |mut acc, (i, n)| {
.try_fold(String::new(), |mut acc, (i, (kind, namespace))| {
match kind {
RouteKind::HTTP => {}
_ => {
return Ok(acc);
}
}
if i > 0 {
acc.push_str("\n---\n");
}
acc.push_str(&format!(
r#"apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: private-networks
namespace: {}
spec:
chain:
middlewares:
- name: private-networks
namespace: kube-system"#,
n
));
acc
let rendered = match kind {
RouteKind::HTTP => Some(MiddlewareChain { namespace }.render()?),
_ => None,
};
if let Some(rendered) = rendered {
acc.push_str(&rendered);
}
Ok(acc)
})
}
fn generate_http_route(route: &Route) -> String {
let mut filters_section = String::new();
if route.private {
filters_section = format!(
r#"
filters:
- type: ExtensionRef
extensionRef:
group: traefik.io
kind: Middleware
name: private-networks"#
);
};
format!(
r#"apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: {}
namespace: {}
spec:
parentRefs:
- name: traefik-gateway
namespace: kube-system
hostnames:
- {}.lucalise.ca
rules:
- backendRefs:
- name: {}
port: {}{}"#,
route.name,
route.namespace,
route.hostname(),
route.service.as_ref().unwrap_or_else(|| &route.name),
route.port,
filters_section
)
.trim_end()
.to_string()
}
fn generate_tcp_route(route: &Route) -> Result<String, HelperError> {
let mut middlewares_section = String::new();
if route.private {
middlewares_section = format!(
r#"
middlewares:
- name: private-networks"#
);
}
Ok(format!(
r#"apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: {}
namespace: {}
spec:
entryPoints:
- {}
routes:
- match: HostSNI(`*`){}
services:
- name: {}
port: {}"#,
route.name,
route.namespace,
route
.entrypoint
.as_ref()
.ok_or(HelperError::TCPEntryPoint(route.name.clone()))?,
middlewares_section,
route.service.as_ref().unwrap_or_else(|| &route.name),
route.port
)
.trim_end()
.to_string())
}

View File

@@ -39,6 +39,8 @@ pub enum HelperError {
ReadFile(#[from] std::io::Error),
#[error("entrypoint required for tcproute: {0:?}")]
TCPEntryPoint(String),
#[error("template rendering error: {0}")]
Template(#[from] askama::Error),
}
#[derive(Serialize, Deserialize)]

View File

@@ -0,0 +1,10 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: private-networks
namespace: {{ namespace }}
spec:
chain:
middlewares:
- name: private-networks
namespace: kube-system

View File

@@ -0,0 +1,23 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: {{ name }}
namespace: {{ namespace }}
spec:
parentRefs:
- name: traefik-gateway
namespace: kube-system
hostnames:
- {{ hostname }}.lucalise.ca
rules:
- backendRefs:
- name: {{ service }}
port: {{ port }}
{%- if private %}
filters:
- type: ExtensionRef
extensionRef:
group: traefik.io
kind: Middleware
name: private-networks
{%- endif %}

View File

@@ -0,0 +1,18 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: {{ name }}
namespace: {{ namespace }}
spec:
entryPoints:
- {{ entrypoint }}
routes:
- match: HostSNI(`*`)
{%- if private %}
middlewares:
- name: private-networks-tcp
namespace: kube-system
{%- endif %}
services:
- name: {{ service }}
port: {{ port }}