Modularize documentation and add architecture diagram

This commit is contained in:
jv 2026-09-16 22:00:00 -05:00
parent 6dfae38adf
commit be02fa45d2
9 changed files with 28056 additions and 2965 deletions

1483
README.md

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,221 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Homelab Infrastructure Architecture</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'JetBrains Mono', monospace; background: #020617; min-height: 100vh; padding: 2rem; color: white; }
.container { max-width: 1200px; margin: 0 auto; }
.header { margin-bottom: 2rem; }
.header-row { display: flex; align-items: center; gap: 1rem; margin-bottom: 0.5rem; }
.pulse-dot { width: 12px; height: 12px; background: #22d3ee; border-radius: 50%; animation: pulse 2s infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
h1 { font-size: 1.5rem; font-weight: 700; letter-spacing: -0.025em; }
.subtitle { color: #94a3b8; font-size: 0.875rem; margin-left: 1.75rem; }
.diagram-container { background: rgba(15, 23, 42, 0.5); border-radius: 1rem; border: 1px solid #1e293b; padding: 1.5rem; overflow-x: auto; }
svg { width: 100%; min-width: 1000px; display: block; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; margin-top: 2rem; }
.card { background: rgba(15, 23, 42, 0.5); border-radius: 0.75rem; border: 1px solid #1e293b; padding: 1.25rem; }
.card-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; }
.card-dot { width: 8px; height: 8px; border-radius: 50%; }
.card-dot.cyan { background: #22d3ee; }
.card-dot.emerald { background: #34d399; }
.card-dot.violet { background: #a78bfa; }
.card-dot.amber { background: #fbbf24; }
.card-dot.rose { background: #fb7185; }
.card h3 { font-size: 0.875rem; font-weight: 600; }
.card ul { list-style: none; color: #94a3b8; font-size: 0.75rem; }
.card li { margin-bottom: 0.375rem; }
.footer { text-align: center; margin-top: 1.5rem; color: #475569; font-size: 0.75rem; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="header-row">
<div class="pulse-dot"></div>
<h1>Homelab Infrastructure Architecture</h1>
</div>
<p class="subtitle">Hybrid K8s + Docker Compose + OCI Edge Bridge</p>
</div>
<div class="diagram-container">
<svg viewBox="0 0 1000 680">
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
</marker>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#1e293b" stroke-width="0.5"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
<!-- =================================================================
EXTERNAL & EDGE
================================================================= -->
<!-- Users -->
<rect x="30" y="280" width="100" height="50" rx="6" fill="rgba(30, 41, 59, 0.5)" stroke="#94a3b8" stroke-width="1.5"/>
<text x="80" y="300" fill="white" font-size="11" font-weight="600" text-anchor="middle">Users</text>
<text x="80" y="316" fill="#94a3b8" font-size="9" text-anchor="middle">Public Internet</text>
<!-- OCI Boundary -->
<rect x="160" y="180" width="160" height="320" rx="12" fill="rgba(251, 191, 36, 0.05)" stroke="#fbbf24" stroke-width="1" stroke-dasharray="8,4"/>
<text x="172" y="198" fill="#fbbf24" font-size="10" font-weight="600">OCI Edge (Jump Box)</text>
<rect x="200" y="220" width="110" height="50" rx="6" fill="rgba(120, 53, 15, 0.3)" stroke="#fbbf24" stroke-width="1.5"/>
<text x="255" y="240" fill="white" font-size="11" font-weight="600" text-anchor="middle">Nginx</text>
<text x="255" y="256" fill="#94a3b8" font-size="9" text-anchor="middle">TLS / Gzip / H2</text>
<rect x="200" y="300" width="110" height="50" rx="6" fill="rgba(120, 53, 15, 0.3)" stroke="#fbbf24" stroke-width="1.5"/>
<text x="255" y="320" fill="white" font-size="11" font-weight="600" text-anchor="middle">HAProxy</text>
<text x="255" y="336" fill="#94a3b8" font-size="9" text-anchor="middle">L7 Routing</text>
<rect x="200" y="380" width="110" height="50" rx="6" fill="rgba(120, 53, 15, 0.3)" stroke="#fbbf24" stroke-width="1.5"/>
<text x="255" y="400" fill="white" font-size="11" font-weight="600" text-anchor="middle">Varnish/Squid</text>
<text x="255" y="416" fill="#94a3b8" font-size="9" text-anchor="middle">Edge Cache</text>
<!-- =================================================================
TRANSPORT
================================================================= -->
<line x1="130" y1="305" x2="198" y2="305" stroke="#22d3ee" stroke-width="1.5" marker-end="url(#arrowhead)"/>
<text x="164" y="299" fill="#94a3b8" font-size="9" text-anchor="middle">HTTPS</text>
<line x1="255" y1="270" x2="255" y2="298" stroke="#fbbf24" stroke-width="1.5" marker-end="url(#arrowhead)"/>
<line x1="255" y1="350" x2="255" y2="378" stroke="#fbbf24" stroke-width="1.5" marker-end="url(#arrowhead)"/>
<!-- Tailscale Tunnel -->
<path d="M 310 365 L 400 365" stroke="#94a3b8" stroke-width="2" stroke-dasharray="5,5" marker-end="url(#arrowhead)"/>
<text x="355" y="358" fill="#94a3b8" font-size="9" text-anchor="middle">Tailscale Mesh</text>
<!-- =================================================================
HOMELAB LAN
================================================================= -->
<rect x="400" y="50" width="550" height="600" rx="12" fill="rgba(15, 23, 42, 0.5)" stroke="#34d399" stroke-width="1" stroke-dasharray="8,4"/>
<text x="412" y="68" fill="#34d399" font-size="10" font-weight="600">Homelab LAN (192.168.100.0/24)</text>
<!-- Debian Host (Host Services) -->
<rect x="450" y="100" width="200" height="220" rx="8" fill="transparent" stroke="#94a3b8" stroke-width="1" stroke-dasharray="4,4"/>
<text x="458" y="118" fill="#94a3b8" font-size="9">debian:control-plane</text>
<rect x="480" y="130" width="140" height="45" rx="6" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1.5"/>
<text x="550" y="150" fill="white" font-size="11" font-weight="600" text-anchor="middle">Gitea</text>
<text x="550" y="165" fill="#94a3b8" font-size="9" text-anchor="middle">GitOps Source</text>
<rect x="480" y="185" width="140" height="45" rx="6" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1.5"/>
<text x="550" y="205" fill="white" font-size="11" font-weight="600" text-anchor="middle">Heimdall</text>
<text x="550" y="220" fill="#94a3b8" font-size="9" text-anchor="middle">App Dashboard</text>
<rect x="480" y="240" width="140" height="45" rx="6" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1.5"/>
<text x="550" y="260" fill="white" font-size="11" font-weight="600" text-anchor="middle">Ollama</text>
<text x="550" y="275" fill="#94a3b8" font-size="9" text-anchor="middle">Local LLM API</text>
<!-- K8s Cluster -->
<rect x="450" y="350" width="450" height="260" rx="8" fill="rgba(6, 78, 59, 0.05)" stroke="#34d399" stroke-width="1" stroke-dasharray="4,4"/>
<text x="458" y="368" fill="#34d399" font-size="9">Kubernetes Cluster</text>
<rect x="480" y="380" width="110" height="50" rx="6" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1.5"/>
<text x="535" y="400" fill="white" font-size="11" font-weight="600" text-anchor="middle">Traefik</text>
<text x="535" y="416" fill="#94a3b8" font-size="9" text-anchor="middle">Ingress (MetalLB)</text>
<!-- App Layer -->
<rect x="620" y="380" width="130" height="80" rx="6" fill="rgba(8, 51, 68, 0.4)" stroke="#22d3ee" stroke-width="1.5"/>
<text x="685" y="400" fill="white" font-size="11" font-weight="600" text-anchor="middle">Web Apps</text>
<text x="685" y="420" fill="#94a3b8" font-size="9" text-anchor="middle">• Website (PHP)</text>
<text x="685" y="435" fill="#94a3b8" font-size="9" text-anchor="middle">• Demos Static</text>
<text x="685" y="450" fill="#94a3b8" font-size="9" text-anchor="middle">• n8n</text>
<!-- Platform Layer -->
<rect x="620" y="480" width="130" height="70" rx="6" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1.5"/>
<text x="685" y="500" fill="white" font-size="11" font-weight="600" text-anchor="middle">Platform</text>
<text x="685" y="520" fill="#94a3b8" font-size="9" text-anchor="middle">• Argo CD</text>
<text x="685" y="535" fill="#94a3b8" font-size="9" text-anchor="middle">• Prometheus/Grafana</text>
<!-- Worker Nodes -->
<rect x="780" y="410" width="110" height="80" rx="6" fill="rgba(30, 41, 59, 0.5)" stroke="#94a3b8" stroke-width="1.5"/>
<text x="835" y="430" fill="white" font-size="11" font-weight="600" text-anchor="middle">Workers</text>
<text x="835" y="450" fill="#94a3b8" font-size="9" text-anchor="middle">• Pimox VMs</text>
<text x="835" y="465" fill="#94a3b8" font-size="9" text-anchor="middle">• Raspberry Pi</text>
<!-- Storage -->
<rect x="780" y="520" width="110" height="60" rx="6" fill="rgba(76, 29, 149, 0.4)" stroke="#a78bfa" stroke-width="1.5"/>
<text x="835" y="540" fill="white" font-size="11" font-weight="600" text-anchor="middle">Storage</text>
<text x="835" y="560" fill="#94a3b8" font-size="9" text-anchor="middle">OpenEBS Hostpath</text>
<!-- K8s Internal Flows -->
<line x1="590" y1="405" x2="618" y2="405" stroke="#22d3ee" stroke-width="1.5" marker-end="url(#arrowhead)"/>
<line x1="590" y1="415" x2="618" y2="515" stroke="#34d399" stroke-width="1.5" marker-end="url(#arrowhead)"/>
<line x1="750" y1="420" x2="778" y2="440" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#arrowhead)"/>
<line x1="750" y1="440" x2="778" y2="530" stroke="#a78bfa" stroke-width="1.5" marker-end="url(#arrowhead)"/>
<!-- Edge to Homelab Flows -->
<line x1="320" y1="405" x2="478" y2="405" stroke="#22d3ee" stroke-width="1.5" marker-end="url(#arrowhead)"/>
<text x="390" y="398" fill="#94a3b8" font-size="9" text-anchor="middle">App Path</text>
<line x1="320" y1="330" x2="478" y2="150" stroke="#34d399" stroke-width="1.5" stroke-dasharray="5,5" marker-end="url(#arrowhead)"/>
<text x="390" y="320" fill="#94a3b8" font-size="9" text-anchor="middle">GitOps/SSH</text>
<!-- App to AI Flow -->
<path d="M 685 380 L 685 320 L 620 320 L 620 260 L 622 260" fill="none" stroke="#fb7185" stroke-width="1.5" stroke-dasharray="4,4" marker-end="url(#arrowhead)"/>
<text x="640" y="310" fill="#fb7185" font-size="8">Ollama API</text>
<!-- Legend -->
<text x="720" y="70" fill="white" font-size="10" font-weight="600">Legend</text>
<rect x="720" y="82" width="16" height="10" rx="2" fill="rgba(8, 51, 68, 0.4)" stroke="#22d3ee" stroke-width="1"/>
<text x="742" y="90" fill="#94a3b8" font-size="8">Frontend/App</text>
<rect x="720" y="98" width="16" height="10" rx="2" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1"/>
<text x="742" y="106" fill="#94a3b8" font-size="8">Backend/Platform</text>
<rect x="720" y="114" width="16" height="10" rx="2" fill="rgba(120, 53, 15, 0.3)" stroke="#fbbf24" stroke-width="1"/>
<text x="742" y="122" fill="#94a3b8" font-size="8">OCI Cloud</text>
<rect x="720" y="130" width="16" height="10" rx="2" fill="rgba(76, 29, 149, 0.4)" stroke="#a78bfa" stroke-width="1"/>
<text x="742" y="138" fill="#94a3b8" font-size="8">Storage/DB</text>
<rect x="720" y="146" width="16" height="10" rx="2" fill="rgba(136, 19, 55, 0.4)" stroke="#fb7185" stroke-width="1"/>
<text x="742" y="154" fill="#94a3b8" font-size="8">AI/Security</text>
</svg>
</div>
<div class="cards">
<div class="card">
<div class="card-header">
<div class="card-dot amber"></div>
<h3>Edge Gateway</h3>
</div>
<ul>
<li>• TLS termination at OCI Edge</li>
<li>• Varnish cache for static HTML</li>
<li>• Tailscale tunnel to Homelab LAN</li>
</ul>
</div>
<div class="card">
<div class="card-header">
<div class="card-dot emerald"></div>
<h3>K8s Core</h3>
</div>
<ul>
<li>• Traefik L7 Ingress via MetalLB</li>
<li>• Argo CD GitOps reconciliation</li>
<li>• Kyverno supply-chain enforcement</li>
</ul>
</div>
<div class="card">
<div class="card-header">
<div class="card-dot rose"></div>
<h3>AI & Data</h3>
</div>
<ul>
<li>• Ollama local LLM on Debian host</li>
<li>• Redis translation cache (Website)</li>
<li>• OpenEBS local PV retention</li>
</ul>
</div>
</div>
<p class="footer">
Homelab Infrastructure • v1.0.0 • Generated by Hermes Agent
</p>
</div>
</body>
</html>

32
docs/architecture.md Normal file
View File

@ -0,0 +1,32 @@
# Architecture Overview
![Infrastructure Diagram](./architecture-diagram.html)
*(Note: Since this is an HTML file, click the link below to open it in your browser)*
[👉 Open Interactive Architecture Diagram](./architecture-diagram.html)
## System Design
This homelab utilizes a hybrid architecture combining a high-performance K8s cluster for scalable workloads and a stable Docker-Compose/Host environment for core infrastructure services.
### 1. The Edge Path (OCI $\rightarrow$ LAN)
Traffic enters via the OCI Edge bridge to minimize exposure of the home IP and provide low-latency caching.
- **Ingress:** Nginx handles TLS termination and HTTP/2.
- **Routing:** HAProxy manages L7 routing to the internal mesh.
- **Caching:** Varnish/Squid provides an edge cache for static content.
- **Transport:** A Tailscale mesh tunnel connects the OCI Edge to the internal homelab LAN.
### 2. The Core Infrastructure (LAN)
The internal network is centered around a Debian control-plane and a mix of ARM64 worker nodes.
- **Host Services:** Core GitOps (Gitea), Dashboard (Heimdall), and Local AI (Ollama) run directly on the Debian host for maximum reliability.
- **K8s Cluster:** Managed via `jeannie` (OpenTofu + ArgoCD).
- **Ingress:** Traefik provides L7 routing via MetalLB.
- **Workloads:** Segmented into `Web Apps` (PHP, Static) and `Platform Services` (ArgoCD, Prometheus).
- **Workers:** A mix of Pimox-virtualized VMs and Raspberry Pi nodes.
- **Storage:** Local persistence is managed via OpenEBS Hostpath.
### 3. AI Integration Flow
AI capabilities are decoupled from the K8s lifecycle to utilize host-level GPU/CPU resources efficiently.
- **API Layer:** Ollama runs as a host service.
- **Consumption:** K8s-based applications communicate with the Ollama API over the internal LAN.
- **Memory:** Redis (transitioning to Valkey) provides a translation and session cache for AI-driven web features.

15322
docs/templates/ai-ops.md.tmpl vendored Normal file

File diff suppressed because it is too large Load Diff

2624
docs/templates/app-ops.md.tmpl vendored Normal file

File diff suppressed because it is too large Load Diff

6172
docs/templates/architecture.md.tmpl vendored Normal file

File diff suppressed because it is too large Load Diff

3605
docs/templates/deployment.md.tmpl vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,8 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
INVENTORY_FILE="${REPO_ROOT}/homelab.yml"
README_TEMPLATE="${REPO_ROOT}/README.md.tmpl"
README_OUTPUT="${REPO_ROOT}/README.md"
TEMPLATES_DIR="${REPO_ROOT}/docs/templates"
DOCS_DIR="${REPO_ROOT}/docs"
usage() {
cat <<'USAGE'
@ -57,22 +59,22 @@ print(value)
PY
}
render_readme() {
local main_script="$1"
render_template() {
local template_path="$1"
local output_path="$2"
local main_script="$3"
if grep -Eo '{{[[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*}}' "${README_TEMPLATE}" |
grep -Evq '{{[[:space:]]*main_script[[:space:]]*}}'; then
printf 'Unsupported template variable in %s\n' "${README_TEMPLATE}" >&2
if [[ ! -f "$template_path" ]]; then
return 1
fi
sed "s/{{[[:space:]]*main_script[[:space:]]*}}/${main_script}/g" "${README_TEMPLATE}"
sed "s/{{[[:space:]]*main_script[[:space:]]*}}/${main_script}/g" "$template_path" > "$output_path"
}
main() {
local mode="render"
local main_script
local rendered
local rendered_tmp
case "${1:-}" in
"")
@ -94,12 +96,6 @@ main() {
printf 'Missing homelab inventory: %s\n' "${INVENTORY_FILE}" >&2
return 1
fi
if [[ ! -s "${README_TEMPLATE}" ]]; then
printf 'Missing README template: %s\n' "${README_TEMPLATE}" >&2
return 1
fi
"${REPO_ROOT}/scripts/validate-homelab-inventory" "${INVENTORY_FILE}"
main_script="$(read_inventory_value metadata.main_script)"
if [[ -z "${main_script}" ]]; then
@ -107,25 +103,55 @@ main() {
return 1
fi
rendered="$(mktemp)"
render_readme "${main_script}" >"${rendered}"
# 1. Render README
rendered_tmp=$(mktemp)
render_template "${README_TEMPLATE}" "$rendered_tmp" "${main_script}"
if [[ "${mode}" == "check" ]]; then
"${REPO_ROOT}/scripts/render-service-catalog" --check
if ! cmp -s "${rendered}" "${README_OUTPUT}"; then
printf 'Generated README.md is stale. Run scripts/render-docs.\n' >&2
diff -u "${README_OUTPUT}" "${rendered}" || true
rm -f "${rendered}"
if ! cmp -s "$rendered_tmp" "${README_OUTPUT}"; then
printf 'Generated README.md is stale.\n' >&2
diff -u "${README_OUTPUT}" "$rendered_tmp" || true
rm -f "$rendered_tmp"
return 1
fi
printf 'docs are current\n'
rm -f "${rendered}"
return 0
else
mv "$rendered_tmp" "${README_OUTPUT}"
fi
rm -f "$rendered_tmp"
# 2. Render Modular Docs
for tmpl in "${TEMPLATES_DIR}"/*.tmpl; do
[[ -e "$tmpl" ]] || continue
local filename=$(basename "$tmpl" .tmpl)
local output_file="${DOCS_DIR}/${filename}.md"
if [[ "${mode}" == "check" ]]; then
rendered_tmp=$(mktemp)
render_template "$tmpl" "$rendered_tmp" "${main_script}"
if ! cmp -s "$rendered_tmp" "$output_file"; then
printf 'Generated %s is stale.\n' "$output_file" >&2
diff -u "$output_file" "$rendered_tmp" || true
rm -f "$rendered_tmp"
return 1
fi
rm -f "$rendered_tmp"
else
render_template "$tmpl" "$output_file" "${main_script}"
fi
done
# 3. Render Service Catalog
if [[ "${mode}" == "check" ]]; then
"${REPO_ROOT}/scripts/render-service-catalog" --check
else
"${REPO_ROOT}/scripts/render-service-catalog"
fi
mv "${rendered}" "${README_OUTPUT}"
"${REPO_ROOT}/scripts/render-service-catalog"
printf 'Rendered %s from %s\n' "${README_OUTPUT}" "${README_TEMPLATE}"
if [[ "${mode}" == "render" ]]; then
printf 'Successfully rendered all documentation from templates.\n'
else
printf 'All docs are current.\n'
fi
}
main "$@"