Compare commits

...

6 Commits

Author SHA1 Message Date
juvdiaz 0544dba9f5 Require inventory values for edge wiring 2026-06-29 21:09:08 -06:00
juvdiaz ae84c3d259 Limit resource recommendations to Kubernetes apps 2026-06-29 21:07:01 -06:00
juvdiaz bf35a78658 Allow explain for new read-only helpers 2026-06-29 21:06:20 -06:00
juvdiaz 3c24b7fbc4 Add AI knowledge exclude list 2026-06-29 21:05:30 -06:00
juvdiaz af75f2c608 Move inactive MLOps app to labs 2026-06-29 21:04:37 -06:00
juvdiaz a57c459c8a Archive legacy ARR Kubernetes manifests 2026-06-29 21:02:11 -06:00
22 changed files with 103 additions and 59 deletions

View File

@ -833,10 +833,10 @@ The ARR/media stack is intentionally not deployed by the Kubernetes app
pipeline. It is better suited to a Docker Compose stack on the Debian host with pipeline. It is better suited to a Docker Compose stack on the Debian host with
persistent volumes under `/data`, where large media paths, qBittorrent writes, persistent volumes under `/data`, where large media paths, qBittorrent writes,
and service config directories can survive cluster rebuilds without OpenEBS and service config directories can survive cluster rebuilds without OpenEBS
node-affinity or PVC migration concerns. The legacy `apps/arr-stack` manifests node-affinity or PVC migration concerns. The active host-level Compose
are retained only as migration reference. The active host-level Compose
definition lives under `infra/arr-stack` and stores persistent data under definition lives under `infra/arr-stack` and stores persistent data under
`/data/arr`. `/data/arr`. Historical Kubernetes manifests are archived under
`archive/apps/arr-stack` for migration reference only.
## Storage ## Storage
@ -1113,7 +1113,8 @@ Build the local homelab knowledge index separately from the main deployment:
``` ```
The index is built from the non-secret source list in The index is built from the non-secret source list in
`infra/ai/knowledge-sources.txt` and defaults to `/data/homelab-ai/index`. `infra/ai/knowledge-sources.txt`, skips paths in
`infra/ai/knowledge-excludes.txt`, and defaults to `/data/homelab-ai/index`.
Doctor commands use it as extra context when it exists, but infrastructure Doctor commands use it as extra context when it exists, but infrastructure
deployment does not depend on it. deployment does not depend on it.
Use `ask` as the runbook and command finder. It returns the closest indexed Use `ask` as the runbook and command finder. It returns the closest indexed

View File

@ -833,10 +833,10 @@ The ARR/media stack is intentionally not deployed by the Kubernetes app
pipeline. It is better suited to a Docker Compose stack on the Debian host with pipeline. It is better suited to a Docker Compose stack on the Debian host with
persistent volumes under `/data`, where large media paths, qBittorrent writes, persistent volumes under `/data`, where large media paths, qBittorrent writes,
and service config directories can survive cluster rebuilds without OpenEBS and service config directories can survive cluster rebuilds without OpenEBS
node-affinity or PVC migration concerns. The legacy `apps/arr-stack` manifests node-affinity or PVC migration concerns. The active host-level Compose
are retained only as migration reference. The active host-level Compose
definition lives under `infra/arr-stack` and stores persistent data under definition lives under `infra/arr-stack` and stores persistent data under
`/data/arr`. `/data/arr`. Historical Kubernetes manifests are archived under
`archive/apps/arr-stack` for migration reference only.
## Storage ## Storage
@ -1113,7 +1113,8 @@ Build the local homelab knowledge index separately from the main deployment:
``` ```
The index is built from the non-secret source list in The index is built from the non-secret source list in
`infra/ai/knowledge-sources.txt` and defaults to `/data/homelab-ai/index`. `infra/ai/knowledge-sources.txt`, skips paths in
`infra/ai/knowledge-excludes.txt`, and defaults to `/data/homelab-ai/index`.
Doctor commands use it as extra context when it exists, but infrastructure Doctor commands use it as extra context when it exists, but infrastructure
deployment does not depend on it. deployment does not depend on it.
Use `ask` as the runbook and command finder. It returns the closest indexed Use `ask` as the runbook and command finder. It returns the closest indexed

View File

@ -1,6 +1,11 @@
variable "edge_host" { variable "edge_host" {
type = string type = string
default = "132.145.170.74" default = ""
validation {
condition = var.edge_host != ""
error_message = "edge_host must be provided from homelab.yml through jeannie or an explicit tfvars/env value."
}
} }
variable "edge_user" { variable "edge_user" {
@ -25,19 +30,17 @@ variable "edge_install_docker" {
variable "server_name" { variable "server_name" {
type = string type = string
default = "lab2025.duckdns.org" default = ""
validation {
condition = var.server_name != ""
error_message = "server_name must be provided from homelab.yml through jeannie or an explicit tfvars/env value."
}
} }
variable "additional_server_names" { variable "additional_server_names" {
type = list(string) type = list(string)
default = [ default = []
"demos.lab2025.duckdns.org",
"heimdall.lab2025.duckdns.org",
"grafana.lab2025.duckdns.org",
"prometheus.lab2025.duckdns.org",
"alertmanager.lab2025.duckdns.org",
"argocd.lab2025.duckdns.org",
]
} }
variable "enable_letsencrypt" { variable "enable_letsencrypt" {
@ -57,7 +60,12 @@ variable "letsencrypt_staging" {
variable "backend_host" { variable "backend_host" {
type = string type = string
default = "192.168.100.240" default = ""
validation {
condition = var.backend_host != ""
error_message = "backend_host must be provided from homelab.yml through jeannie or an explicit tfvars/env value."
}
} }
variable "backend_port" { variable "backend_port" {
@ -77,7 +85,12 @@ variable "gitea_backend_port" {
variable "gitea_backend_host" { variable "gitea_backend_host" {
type = string type = string
default = "100.85.138.30" default = ""
validation {
condition = var.gitea_backend_host != ""
error_message = "gitea_backend_host must be provided from homelab.yml through jeannie or an explicit tfvars/env value."
}
} }
variable "haproxy_stats_user" { variable "haproxy_stats_user" {

View File

@ -147,7 +147,9 @@ plane pods off the Debian host.
including the meaning, diagnostic commands, whether a Jeannie fix command including the meaning, diagnostic commands, whether a Jeannie fix command
already exists, risk, and auto-fix safety. This command is read-only and refuses already exists, risk, and auto-fix safety. This command is read-only and refuses
mutating commands. Use `explain scorecard [--all]` for scorecard-specific mutating commands. Use `explain scorecard [--all]` for scorecard-specific
category explanations, or pipe captured output into `explain output`. category explanations, use it directly with helpers such as `capacity-advisor`,
`capacity-limits`, `impact`, and `review-last-change`, or pipe captured output
into `explain output`.
`artifact-cache {status|up|down|instructions}` `artifact-cache {status|up|down|instructions}`
: Manage the optional Debian-host artifact cache stack for apt packages and : Manage the optional Debian-host artifact cache stack for apt packages and

View File

@ -0,0 +1,6 @@
# Files and globs excluded from the local homelab AI helper.
# Keep inactive reference material out of answers about the active lab.
archive/**
labs/**
apps/arr-stack/**
apps/mlops-platform/**

View File

@ -4,6 +4,9 @@ defaults:
limit_cpu_multiplier: 4 limit_cpu_multiplier: 4
limit_memory_multiplier: 2 limit_memory_multiplier: 2
# Suggestion-only profiles for Kubernetes namespaces managed by the app pipeline.
# Host-level Docker Compose stacks such as infra/arr-stack are intentionally not
# listed here.
namespaces: namespaces:
website-production: website-production:
request_cpu_m: 75 request_cpu_m: 75
@ -20,11 +23,6 @@ namespaces:
request_memory_mib: 128 request_memory_mib: 128
limit_cpu_multiplier: 4 limit_cpu_multiplier: 4
limit_memory_multiplier: 2 limit_memory_multiplier: 2
arr-stack:
request_cpu_m: 150
request_memory_mib: 512
limit_cpu_multiplier: 3
limit_memory_multiplier: 2
argocd: argocd:
request_cpu_m: 100 request_cpu_m: 100
request_memory_mib: 256 request_memory_mib: 256

60
jeannie
View File

@ -29,6 +29,7 @@ load_homelab_inventory_defaults() {
rendered_defaults="$(python3 - "${inventory_file}" <<'PY' rendered_defaults="$(python3 - "${inventory_file}" <<'PY'
import re import re
import json
import shlex import shlex
import sys import sys
@ -140,6 +141,14 @@ if debian_ip and gitea_http:
gitea_ts = values.get("hosts.debian.tailscale_ip") gitea_ts = values.get("hosts.debian.tailscale_ip")
if gitea_ts: if gitea_ts:
print(f": ${{LAB_GITEA_TAILSCALE_IP:={shlex.quote(gitea_ts)}}}") print(f": ${{LAB_GITEA_TAILSCALE_IP:={shlex.quote(gitea_ts)}}}")
subdomains = values.get("domain.subdomains")
if subdomains:
if isinstance(subdomains, str):
subdomain_values = [item.strip() for item in subdomains.split(",") if item.strip()]
else:
subdomain_values = list(subdomains)
if subdomain_values:
print(f": ${{LAB_ADDITIONAL_SERVER_NAMES_JSON:={shlex.quote(json.dumps(subdomain_values))}}}")
PY PY
)" )"
@ -182,6 +191,7 @@ export_homelab_inventory_tf_vars() {
export_if_unset TF_VAR_edge_user "${LAB_EDGE_USER:-}" export_if_unset TF_VAR_edge_user "${LAB_EDGE_USER:-}"
export_if_unset TF_VAR_edge_install_dir "${LAB_EDGE_INSTALL_DIR:-}" export_if_unset TF_VAR_edge_install_dir "${LAB_EDGE_INSTALL_DIR:-}"
export_if_unset TF_VAR_server_name "${LAB_DOMAIN:-}" export_if_unset TF_VAR_server_name "${LAB_DOMAIN:-}"
export_if_unset TF_VAR_additional_server_names "${LAB_ADDITIONAL_SERVER_NAMES_JSON:-}"
export_if_unset TF_VAR_backend_host "${LAB_TRAEFIK_LB_IP:-}" export_if_unset TF_VAR_backend_host "${LAB_TRAEFIK_LB_IP:-}"
export_if_unset TF_VAR_gitea_backend_host "${LAB_DEBIAN_TAILSCALE_IP:-}" export_if_unset TF_VAR_gitea_backend_host "${LAB_DEBIAN_TAILSCALE_IP:-}"
export_if_unset TF_VAR_gitea_backend_port "${LAB_GITEA_HTTP_PORT:-}" export_if_unset TF_VAR_gitea_backend_port "${LAB_GITEA_HTTP_PORT:-}"
@ -1363,7 +1373,7 @@ check_debian_tailscale_ip() {
} }
check_edge_ssh() { check_edge_ssh() {
local edge_host="${LAB_EDGE_HOST:-132.145.170.74}" local edge_host="${LAB_EDGE_HOST:?LAB_EDGE_HOST is required from homelab.yml}"
local edge_user="${LAB_EDGE_USER:-ubuntu}" local edge_user="${LAB_EDGE_USER:-ubuntu}"
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${edge_user}@${edge_host}" "true" ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${edge_user}@${edge_host}" "true"
@ -1466,7 +1476,7 @@ sudo rm -f \"\$root/.homelab-check/write-test\""
} }
check_edge_disk_free() { check_edge_disk_free() {
local edge_host="${LAB_EDGE_HOST:-132.145.170.74}" local edge_host="${LAB_EDGE_HOST:?LAB_EDGE_HOST is required from homelab.yml}"
local edge_user="${LAB_EDGE_USER:-ubuntu}" local edge_user="${LAB_EDGE_USER:-ubuntu}"
local min_gib="${LAB_PREFLIGHT_EDGE_MIN_FREE_GIB:-2}" local min_gib="${LAB_PREFLIGHT_EDGE_MIN_FREE_GIB:-2}"
@ -1536,7 +1546,7 @@ homelab_preflight() {
fi fi
preflight_warn "RPi Tailscale IP ${LAB_RPI_TAILSCALE_IP:-unset}" check_rpi_tailscale_ip preflight_warn "RPi Tailscale IP ${LAB_RPI_TAILSCALE_IP:-unset}" check_rpi_tailscale_ip
preflight_check "Pimox storage ${LAB_PIMOX_WORKER_STORAGE:-${TF_VAR_pimox_worker_storage:-opi5_ssd}} is active" check_pimox_storage || failures=$((failures + 1)) preflight_check "Pimox storage ${LAB_PIMOX_WORKER_STORAGE:-${TF_VAR_pimox_worker_storage:-opi5_ssd}} is active" check_pimox_storage || failures=$((failures + 1))
preflight_check "OCI edge SSH ${LAB_EDGE_USER:-ubuntu}@${LAB_EDGE_HOST:-132.145.170.74}" check_edge_ssh || failures=$((failures + 1)) preflight_check "OCI edge SSH ${LAB_EDGE_USER:-ubuntu}@${LAB_EDGE_HOST:?LAB_EDGE_HOST is required from homelab.yml}" check_edge_ssh || failures=$((failures + 1))
if ((failures > 0)); then if ((failures > 0)); then
echo "Preflight failed with ${failures} blocking check(s). Fix the inventory or host state before continuing." >&2 echo "Preflight failed with ${failures} blocking check(s). Fix the inventory or host state before continuing." >&2
@ -2830,8 +2840,8 @@ deploy_gitea() {
local image="${LAB_GITEA_IMAGE:-gitea/gitea:1.21.7}" local image="${LAB_GITEA_IMAGE:-gitea/gitea:1.21.7}"
local http_port="${LAB_GITEA_HTTP_PORT:-3000}" local http_port="${LAB_GITEA_HTTP_PORT:-3000}"
local ssh_port="${LAB_GITEA_SSH_PORT:-32222}" local ssh_port="${LAB_GITEA_SSH_PORT:-32222}"
local domain="${LAB_GITEA_DOMAIN:-${LAB_DOMAIN:-lab2025.duckdns.org}}" local domain="${LAB_GITEA_DOMAIN:-${LAB_DOMAIN:?LAB_DOMAIN is required from homelab.yml}}"
local root_url="${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/git/}" local root_url="${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:?LAB_PUBLIC_URL is required from homelab.yml}/git/}"
local container_name="${LAB_GITEA_CONTAINER_NAME:-homelab-gitea}" local container_name="${LAB_GITEA_CONTAINER_NAME:-homelab-gitea}"
local compose_file="${REPO_ROOT}/infra/gitea/docker-compose.yml" local compose_file="${REPO_ROOT}/infra/gitea/docker-compose.yml"
local install_docker="${LAB_GITEA_INSTALL_DOCKER:-false}" local install_docker="${LAB_GITEA_INSTALL_DOCKER:-false}"
@ -3248,7 +3258,7 @@ bootstrap_gitea_repo() {
local container_name="${LAB_GITEA_CONTAINER_NAME:-homelab-gitea}" local container_name="${LAB_GITEA_CONTAINER_NAME:-homelab-gitea}"
local http_port="${LAB_GITEA_HTTP_PORT:-3000}" local http_port="${LAB_GITEA_HTTP_PORT:-3000}"
local ssh_port="${LAB_GITEA_SSH_PORT:-32222}" local ssh_port="${LAB_GITEA_SSH_PORT:-32222}"
local root_url="${LAB_GITEA_ROOT_URL:-https://lab2025.duckdns.org/git/}" local root_url="${LAB_GITEA_ROOT_URL:?LAB_GITEA_ROOT_URL is required from homelab.yml}"
local repo_owner="${LAB_GITEA_REPO_OWNER:-jv}" local repo_owner="${LAB_GITEA_REPO_OWNER:-jv}"
local repo_name="${LAB_GITEA_REPO_NAME:-my-homelab-configs}" local repo_name="${LAB_GITEA_REPO_NAME:-my-homelab-configs}"
local default_branch="${LAB_GITEA_REPO_DEFAULT_BRANCH:-main}" local default_branch="${LAB_GITEA_REPO_DEFAULT_BRANCH:-main}"
@ -4659,7 +4669,7 @@ status_http_reachable() {
} }
status_gitea_public_ok() { status_gitea_public_ok() {
local root_url="${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/git/}" local root_url="${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:?LAB_PUBLIC_URL is required from homelab.yml}/git/}"
status_http_reachable "${root_url}" status_http_reachable "${root_url}"
} }
@ -4957,8 +4967,8 @@ status_cascade_report() {
status_section "Edge And Public URLs" status_section "Edge And Public URLs"
status_cascade_check "OCI edge SSH" check_edge_ssh || failures=$((failures + 1)) status_cascade_check "OCI edge SSH" check_edge_ssh || failures=$((failures + 1))
status_cascade_check "Traefik LoadBalancer HTTP" status_http_reachable "http://${LAB_TRAEFIK_LB_IP:-192.168.100.240}/" || failures=$((failures + 1)) status_cascade_check "Traefik LoadBalancer HTTP" status_http_reachable "http://${LAB_TRAEFIK_LB_IP:?LAB_TRAEFIK_LB_IP is required from homelab.yml}/" || failures=$((failures + 1))
status_cascade_check "Website public URL" status_http_ok "${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/" || failures=$((failures + 1)) status_cascade_check "Website public URL" status_http_ok "${LAB_PUBLIC_URL:?LAB_PUBLIC_URL is required from homelab.yml}/" || failures=$((failures + 1))
status_cascade_check "Gitea public route" status_gitea_public_ok || failures=$((failures + 1)) status_cascade_check "Gitea public route" status_gitea_public_ok || failures=$((failures + 1))
if ((failures > 0)); then if ((failures > 0)); then
@ -5311,10 +5321,10 @@ status_report() {
fi fi
status_section "HTTP" status_section "HTTP"
status_http "website public" "${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/" status_http "website public" "${LAB_PUBLIC_URL:?LAB_PUBLIC_URL is required from homelab.yml}/"
status_http "gitea public" "${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/git/}" status_http "gitea public" "${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:?LAB_PUBLIC_URL is required from homelab.yml}/git/}"
status_http "gitea local" "http://127.0.0.1:${LAB_GITEA_HTTP_PORT:-3000}/" status_http "gitea local" "http://127.0.0.1:${LAB_GITEA_HTTP_PORT:-3000}/"
status_http "traefik lb" "http://${LAB_TRAEFIK_LB_IP:-192.168.100.240}/" status_http "traefik lb" "http://${LAB_TRAEFIK_LB_IP:?LAB_TRAEFIK_LB_IP is required from homelab.yml}/"
status_http "arr radarr" "http://127.0.0.1:7878/" status_http "arr radarr" "http://127.0.0.1:7878/"
status_http "arr sonarr" "http://127.0.0.1:8989/" status_http "arr sonarr" "http://127.0.0.1:8989/"
status_http "arr prowlarr" "http://127.0.0.1:9696/" status_http "arr prowlarr" "http://127.0.0.1:9696/"
@ -5323,17 +5333,17 @@ status_report() {
} }
doctor_edge() { doctor_edge() {
local edge_host="${LAB_EDGE_HOST:-132.145.170.74}" local edge_host="${LAB_EDGE_HOST:?LAB_EDGE_HOST is required from homelab.yml}"
local edge_user="${LAB_EDGE_USER:-ubuntu}" local edge_user="${LAB_EDGE_USER:-ubuntu}"
local traefik_ip="${LAB_TRAEFIK_LB_IP:-192.168.100.240}" local traefik_ip="${LAB_TRAEFIK_LB_IP:?LAB_TRAEFIK_LB_IP is required from homelab.yml}"
local gitea_ts_ip="${LAB_GITEA_TAILSCALE_IP:-100.85.138.30}" local gitea_ts_ip="${LAB_GITEA_TAILSCALE_IP:?LAB_GITEA_TAILSCALE_IP is required from homelab.yml}"
local gitea_http_port="${LAB_GITEA_HTTP_PORT:-3000}" local gitea_http_port="${LAB_GITEA_HTTP_PORT:-3000}"
require_debian_server "doctor-edge" require_debian_server "doctor-edge"
status_section "Doctor Edge" status_section "Doctor Edge"
status_http "website public" "${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/" status_http "website public" "${LAB_PUBLIC_URL:?LAB_PUBLIC_URL is required from homelab.yml}/"
status_http "gitea public" "${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/git/}" status_http "gitea public" "${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:?LAB_PUBLIC_URL is required from homelab.yml}/git/}"
printf '\n-- edge backend reachability\n' printf '\n-- edge backend reachability\n'
if ! ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${edge_user}@${edge_host}" " if ! ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${edge_user}@${edge_host}" "
@ -5359,8 +5369,8 @@ EOF
backstage_brain_note "doctor-edge" <<EOF backstage_brain_note "doctor-edge" <<EOF
Doctor command: doctor-edge Doctor command: doctor-edge
Public domain: ${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/ Public domain: ${LAB_PUBLIC_URL:?LAB_PUBLIC_URL is required from homelab.yml}/
Public Gitea URL: ${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/git/} Public Gitea URL: ${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:?LAB_PUBLIC_URL is required from homelab.yml}/git/}
OCI edge SSH target: ${edge_user}@${edge_host} OCI edge SSH target: ${edge_user}@${edge_host}
Traefik backend from edge: http://${traefik_ip}:80/ Traefik backend from edge: http://${traefik_ip}:80/
Gitea backend from edge: http://${gitea_ts_ip}:${gitea_http_port}/ Gitea backend from edge: http://${gitea_ts_ip}:${gitea_http_port}/
@ -5369,9 +5379,9 @@ EOF
} }
doctor_gitea() { doctor_gitea() {
local edge_host="${LAB_EDGE_HOST:-132.145.170.74}" local edge_host="${LAB_EDGE_HOST:?LAB_EDGE_HOST is required from homelab.yml}"
local edge_user="${LAB_EDGE_USER:-ubuntu}" local edge_user="${LAB_EDGE_USER:-ubuntu}"
local gitea_ts_ip="${LAB_GITEA_TAILSCALE_IP:-100.85.138.30}" local gitea_ts_ip="${LAB_GITEA_TAILSCALE_IP:?LAB_GITEA_TAILSCALE_IP is required from homelab.yml}"
local gitea_host="${LAB_GITEA_HOST:-192.168.100.73}" local gitea_host="${LAB_GITEA_HOST:-192.168.100.73}"
local gitea_http_port="${LAB_GITEA_HTTP_PORT:-3000}" local gitea_http_port="${LAB_GITEA_HTTP_PORT:-3000}"
local gitea_ssh_port="${LAB_GITEA_SSH_PORT:-32222}" local gitea_ssh_port="${LAB_GITEA_SSH_PORT:-32222}"
@ -5380,7 +5390,7 @@ doctor_gitea() {
status_section "Doctor Gitea" status_section "Doctor Gitea"
status_http "gitea local" "http://127.0.0.1:${gitea_http_port}/" status_http "gitea local" "http://127.0.0.1:${gitea_http_port}/"
status_http "gitea public" "${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/git/}" status_http "gitea public" "${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:?LAB_PUBLIC_URL is required from homelab.yml}/git/}"
status_compose_stack "gitea" "/data/homelab-gitea" status_compose_stack "gitea" "/data/homelab-gitea"
status_run "gitea git remote" git ls-remote gitea main status_run "gitea git remote" git ls-remote gitea main
@ -5409,7 +5419,7 @@ Gitea local URL: http://127.0.0.1:${gitea_http_port}/
Gitea LAN host: ${gitea_host} Gitea LAN host: ${gitea_host}
Gitea Tailscale backend: http://${gitea_ts_ip}:${gitea_http_port}/ Gitea Tailscale backend: http://${gitea_ts_ip}:${gitea_http_port}/
Gitea SSH port: ${gitea_ssh_port} Gitea SSH port: ${gitea_ssh_port}
Gitea public URL: ${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/git/} Gitea public URL: ${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:?LAB_PUBLIC_URL is required from homelab.yml}/git/}
Known runbook: docs/runbooks/gitea-failures.md Known runbook: docs/runbooks/gitea-failures.md
EOF EOF
} }
@ -5445,7 +5455,7 @@ doctor_cluster() {
status_systemd_units kubelet containerd docker status_systemd_units kubelet containerd docker
status_kubernetes status_kubernetes
status_pimox_workers status_pimox_workers
status_http "traefik lb" "http://${LAB_TRAEFIK_LB_IP:-192.168.100.240}/" status_http "traefik lb" "http://${LAB_TRAEFIK_LB_IP:?LAB_TRAEFIK_LB_IP is required from homelab.yml}/"
printf '\n-- local CRI containers\n' printf '\n-- local CRI containers\n'
if command -v crictl >/dev/null 2>&1; then if command -v crictl >/dev/null 2>&1; then
@ -5467,7 +5477,7 @@ EOF
backstage_brain_note "doctor-cluster" <<EOF backstage_brain_note "doctor-cluster" <<EOF
Doctor command: doctor-cluster Doctor command: doctor-cluster
Kubeconfig path: ${KUBECONFIG_PATH} Kubeconfig path: ${KUBECONFIG_PATH}
Traefik LoadBalancer IP: ${LAB_TRAEFIK_LB_IP:-192.168.100.240} Traefik LoadBalancer IP: ${LAB_TRAEFIK_LB_IP:?LAB_TRAEFIK_LB_IP is required from homelab.yml}
Pimox host: ${LAB_PIMOX_USER:-${TF_VAR_pimox_user:-jv}}@${LAB_PIMOX_HOST:-${TF_VAR_pimox_host:-192.168.100.80}} Pimox host: ${LAB_PIMOX_USER:-${TF_VAR_pimox_user:-jv}}@${LAB_PIMOX_HOST:-${TF_VAR_pimox_host:-192.168.100.80}}
Worker storage: ${LAB_PIMOX_WORKER_STORAGE:-${TF_VAR_pimox_worker_storage:-opi5_ssd}} Worker storage: ${LAB_PIMOX_WORKER_STORAGE:-${TF_VAR_pimox_worker_storage:-opi5_ssd}}
Known runbook: docs/runbooks/cluster-stop-start-failures.md Known runbook: docs/runbooks/cluster-stop-start-failures.md

View File

@ -4,7 +4,7 @@ Production-shaped inference demo for the portfolio site. The model is
intentionally small: logistic regression coefficients trained with scikit-learn intentionally small: logistic regression coefficients trained with scikit-learn
and exported to JSON so the runtime stays light enough for the homelab. and exported to JSON so the runtime stays light enough for the homelab.
This directory contains the FastAPI service and model artifacts. It is not yet This lab directory contains the FastAPI service and model artifacts. It is not
registered as a Kubernetes application in `bootstrap/apps`; the public website registered as a Kubernetes application in `bootstrap/apps`; the public website
currently links to the reserved static placeholder under currently links to the reserved static placeholder under
`apps/demos-static/public/mlops-platform/`. `apps/demos-static/public/mlops-platform/`.
@ -24,7 +24,7 @@ currently links to the reserved static placeholder under
## Local Smoke Test ## Local Smoke Test
```bash ```bash
docker build -t mlops-platform:local apps/mlops-platform docker build -t mlops-platform:local labs/mlops-platform
docker run --rm -p 8080:8080 mlops-platform:local docker run --rm -p 8080:8080 mlops-platform:local
``` ```

View File

@ -16,6 +16,7 @@ from collections import Counter
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1] REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
DEFAULT_SOURCES_FILE = REPO_ROOT / "infra" / "ai" / "knowledge-sources.txt" DEFAULT_SOURCES_FILE = REPO_ROOT / "infra" / "ai" / "knowledge-sources.txt"
DEFAULT_EXCLUDES_FILE = REPO_ROOT / "infra" / "ai" / "knowledge-excludes.txt"
DEFAULT_INDEX_DIR = pathlib.Path(os.environ.get("LAB_AI_KNOWLEDGE_INDEX_DIR", "/data/homelab-ai/index")) DEFAULT_INDEX_DIR = pathlib.Path(os.environ.get("LAB_AI_KNOWLEDGE_INDEX_DIR", "/data/homelab-ai/index"))
TOKEN_RE = re.compile(r"[A-Za-z0-9_./:-]{2,}") TOKEN_RE = re.compile(r"[A-Za-z0-9_./:-]{2,}")
MAX_FILE_BYTES = 512 * 1024 MAX_FILE_BYTES = 512 * 1024
@ -54,7 +55,11 @@ def path_is_secret(path: pathlib.Path) -> bool:
return any(marker in lowered for marker in secret_markers) return any(marker in lowered for marker in secret_markers)
def candidate_files(patterns: list[str]) -> list[pathlib.Path]: def path_matches_any(rel_path: str, patterns: list[str]) -> bool:
return any(fnmatch.fnmatch(rel_path, pattern) for pattern in patterns)
def candidate_files(patterns: list[str], exclude_patterns: list[str]) -> list[pathlib.Path]:
files: set[pathlib.Path] = set() files: set[pathlib.Path] = set()
all_files = [path for path in REPO_ROOT.rglob("*") if path.is_file()] all_files = [path for path in REPO_ROOT.rglob("*") if path.is_file()]
@ -69,7 +74,12 @@ def candidate_files(patterns: list[str]) -> list[pathlib.Path]:
if path.is_file(): if path.is_file():
files.add(path) files.add(path)
return sorted(path for path in files if not path_is_secret(path)) return sorted(
path
for path in files
if not path_is_secret(path)
and not path_matches_any(path.relative_to(REPO_ROOT).as_posix(), exclude_patterns)
)
def read_text(path: pathlib.Path) -> str | None: def read_text(path: pathlib.Path) -> str | None:
@ -155,6 +165,7 @@ def build_index(files: list[pathlib.Path]) -> dict[str, object]:
def main() -> int: def main() -> int:
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--sources-file", type=pathlib.Path, default=DEFAULT_SOURCES_FILE) parser.add_argument("--sources-file", type=pathlib.Path, default=DEFAULT_SOURCES_FILE)
parser.add_argument("--excludes-file", type=pathlib.Path, default=DEFAULT_EXCLUDES_FILE)
parser.add_argument("--index-dir", type=pathlib.Path, default=DEFAULT_INDEX_DIR) parser.add_argument("--index-dir", type=pathlib.Path, default=DEFAULT_INDEX_DIR)
args = parser.parse_args() args = parser.parse_args()
@ -163,7 +174,8 @@ def main() -> int:
return 1 return 1
patterns = load_patterns(args.sources_file) patterns = load_patterns(args.sources_file)
files = candidate_files(patterns) exclude_patterns = load_patterns(args.excludes_file) if args.excludes_file.is_file() else []
files = candidate_files(patterns, exclude_patterns)
index = build_index(files) index = build_index(files)
args.index_dir.mkdir(parents=True, exist_ok=True) args.index_dir.mkdir(parents=True, exist_ok=True)
@ -174,6 +186,7 @@ def main() -> int:
json.dumps( json.dumps(
{ {
"sources_file": str(args.sources_file.relative_to(REPO_ROOT)), "sources_file": str(args.sources_file.relative_to(REPO_ROOT)),
"excludes_file": str(args.excludes_file.relative_to(REPO_ROOT)) if args.excludes_file.is_file() else None,
"file_count": len(files), "file_count": len(files),
"chunk_count": index["chunk_count"], "chunk_count": index["chunk_count"],
"repo_root": str(REPO_ROOT), "repo_root": str(REPO_ROOT),

View File

@ -38,7 +38,7 @@ worker_storage_warn = "worker storage" in lowered and "warn" in lowered
non_system_control_plane = False non_system_control_plane = False
control_plane_section = text.split("## control-plane", 1)[-1] control_plane_section = text.split("## control-plane", 1)[-1]
for line in control_plane_section.splitlines(): for line in control_plane_section.splitlines():
if re.match(r"\s*(argocd|website-production|demos-static|heimdall|arr-stack|security-lab|monitoring)\s+", line): if re.match(r"\s*(argocd|website-production|demos-static|heimdall|security-lab|monitoring)\s+", line):
non_system_control_plane = True non_system_control_plane = True
break break

View File

@ -34,7 +34,7 @@ EOF
is_read_only_command() { is_read_only_command() {
case "$1" in case "$1" in
status|capacity|recover-plan|scorecard|gitops-status|cert-check|backup-status|synthetic-checks|resource-budget|route-inventory|golden-ledger|workers|change-journal|map|validate|access-audit|doctor-versions|doctor-edge|doctor-gitea|doctor-rpi|doctor-cluster|preflight|doctor-preapply|inventory-check|secrets-check|tailnet-policy-check|ai-check|security-scan|security-host|security-trivy|security-secrets|security-nuclei|security-web|security-logs|security-attack-path|control-plane) status|capacity|capacity-advisor|capacity-limits|recover-plan|scorecard|gitops-status|cert-check|backup-status|synthetic-checks|resource-budget|route-inventory|golden-ledger|workers|change-journal|map|validate|access-audit|doctor-versions|doctor-edge|doctor-gitea|doctor-rpi|doctor-cluster|preflight|doctor-preapply|inventory-check|secrets-check|tailnet-policy-check|ai-check|ask|impact|review-last-change|security-scan|security-host|security-trivy|security-secrets|security-nuclei|security-web|security-logs|security-attack-path|control-plane)
return 0 return 0
;; ;;
*) *)