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
persistent volumes under `/data`, where large media paths, qBittorrent writes,
and service config directories can survive cluster rebuilds without OpenEBS
node-affinity or PVC migration concerns. The legacy `apps/arr-stack` manifests
are retained only as migration reference. The active host-level Compose
node-affinity or PVC migration concerns. The active host-level Compose
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
@ -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
`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
deployment does not depend on it.
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
persistent volumes under `/data`, where large media paths, qBittorrent writes,
and service config directories can survive cluster rebuilds without OpenEBS
node-affinity or PVC migration concerns. The legacy `apps/arr-stack` manifests
are retained only as migration reference. The active host-level Compose
node-affinity or PVC migration concerns. The active host-level Compose
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
@ -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
`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
deployment does not depend on it.
Use `ask` as the runbook and command finder. It returns the closest indexed

View File

@ -1,6 +1,11 @@
variable "edge_host" {
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" {
@ -25,19 +30,17 @@ variable "edge_install_docker" {
variable "server_name" {
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" {
type = list(string)
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",
]
type = list(string)
default = []
}
variable "enable_letsencrypt" {
@ -57,7 +60,12 @@ variable "letsencrypt_staging" {
variable "backend_host" {
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" {
@ -77,7 +85,12 @@ variable "gitea_backend_port" {
variable "gitea_backend_host" {
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" {

View File

@ -147,7 +147,9 @@ plane pods off the Debian host.
including the meaning, diagnostic commands, whether a Jeannie fix command
already exists, risk, and auto-fix safety. This command is read-only and refuses
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}`
: 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_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:
website-production:
request_cpu_m: 75
@ -20,11 +23,6 @@ namespaces:
request_memory_mib: 128
limit_cpu_multiplier: 4
limit_memory_multiplier: 2
arr-stack:
request_cpu_m: 150
request_memory_mib: 512
limit_cpu_multiplier: 3
limit_memory_multiplier: 2
argocd:
request_cpu_m: 100
request_memory_mib: 256

60
jeannie
View File

@ -29,6 +29,7 @@ load_homelab_inventory_defaults() {
rendered_defaults="$(python3 - "${inventory_file}" <<'PY'
import re
import json
import shlex
import sys
@ -140,6 +141,14 @@ if debian_ip and gitea_http:
gitea_ts = values.get("hosts.debian.tailscale_ip")
if 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
)"
@ -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_install_dir "${LAB_EDGE_INSTALL_DIR:-}"
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_gitea_backend_host "${LAB_DEBIAN_TAILSCALE_IP:-}"
export_if_unset TF_VAR_gitea_backend_port "${LAB_GITEA_HTTP_PORT:-}"
@ -1363,7 +1373,7 @@ check_debian_tailscale_ip() {
}
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}"
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() {
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 min_gib="${LAB_PREFLIGHT_EDGE_MIN_FREE_GIB:-2}"
@ -1536,7 +1546,7 @@ homelab_preflight() {
fi
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 "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
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 http_port="${LAB_GITEA_HTTP_PORT:-3000}"
local ssh_port="${LAB_GITEA_SSH_PORT:-32222}"
local domain="${LAB_GITEA_DOMAIN:-${LAB_DOMAIN:-lab2025.duckdns.org}}"
local root_url="${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/git/}"
local domain="${LAB_GITEA_DOMAIN:-${LAB_DOMAIN:?LAB_DOMAIN is required from homelab.yml}}"
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 compose_file="${REPO_ROOT}/infra/gitea/docker-compose.yml"
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 http_port="${LAB_GITEA_HTTP_PORT:-3000}"
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_name="${LAB_GITEA_REPO_NAME:-my-homelab-configs}"
local default_branch="${LAB_GITEA_REPO_DEFAULT_BRANCH:-main}"
@ -4659,7 +4669,7 @@ status_http_reachable() {
}
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}"
}
@ -4957,8 +4967,8 @@ status_cascade_report() {
status_section "Edge And Public URLs"
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 "Website public URL" status_http_ok "${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/" || 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:?LAB_PUBLIC_URL is required from homelab.yml}/" || failures=$((failures + 1))
status_cascade_check "Gitea public route" status_gitea_public_ok || failures=$((failures + 1))
if ((failures > 0)); then
@ -5311,10 +5321,10 @@ status_report() {
fi
status_section "HTTP"
status_http "website public" "${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/"
status_http "gitea public" "${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/git/}"
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:?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 "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 sonarr" "http://127.0.0.1:8989/"
status_http "arr prowlarr" "http://127.0.0.1:9696/"
@ -5323,17 +5333,17 @@ status_report() {
}
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 traefik_ip="${LAB_TRAEFIK_LB_IP:-192.168.100.240}"
local gitea_ts_ip="${LAB_GITEA_TAILSCALE_IP:-100.85.138.30}"
local traefik_ip="${LAB_TRAEFIK_LB_IP:?LAB_TRAEFIK_LB_IP is required from homelab.yml}"
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}"
require_debian_server "doctor-edge"
status_section "Doctor Edge"
status_http "website public" "${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/"
status_http "gitea public" "${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/git/}"
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:?LAB_PUBLIC_URL is required from homelab.yml}/git/}"
printf '\n-- edge backend reachability\n'
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
Doctor command: doctor-edge
Public domain: ${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/
Public Gitea URL: ${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/git/}
Public domain: ${LAB_PUBLIC_URL:?LAB_PUBLIC_URL is required from homelab.yml}/
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}
Traefik backend from edge: http://${traefik_ip}:80/
Gitea backend from edge: http://${gitea_ts_ip}:${gitea_http_port}/
@ -5369,9 +5379,9 @@ EOF
}
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 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_http_port="${LAB_GITEA_HTTP_PORT:-3000}"
local gitea_ssh_port="${LAB_GITEA_SSH_PORT:-32222}"
@ -5380,7 +5390,7 @@ doctor_gitea() {
status_section "Doctor Gitea"
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_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 Tailscale backend: http://${gitea_ts_ip}:${gitea_http_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
EOF
}
@ -5445,7 +5455,7 @@ doctor_cluster() {
status_systemd_units kubelet containerd docker
status_kubernetes
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'
if command -v crictl >/dev/null 2>&1; then
@ -5467,7 +5477,7 @@ EOF
backstage_brain_note "doctor-cluster" <<EOF
Doctor command: doctor-cluster
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}}
Worker storage: ${LAB_PIMOX_WORKER_STORAGE:-${TF_VAR_pimox_worker_storage:-opi5_ssd}}
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
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
currently links to the reserved static placeholder under
`apps/demos-static/public/mlops-platform/`.
@ -24,7 +24,7 @@ currently links to the reserved static placeholder under
## Local Smoke Test
```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
```

View File

@ -16,6 +16,7 @@ from collections import Counter
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
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"))
TOKEN_RE = re.compile(r"[A-Za-z0-9_./:-]{2,}")
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)
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()
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():
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:
@ -155,6 +165,7 @@ def build_index(files: list[pathlib.Path]) -> dict[str, object]:
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
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)
args = parser.parse_args()
@ -163,7 +174,8 @@ def main() -> int:
return 1
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)
args.index_dir.mkdir(parents=True, exist_ok=True)
@ -174,6 +186,7 @@ def main() -> int:
json.dumps(
{
"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),
"chunk_count": index["chunk_count"],
"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
control_plane_section = text.split("## control-plane", 1)[-1]
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
break

View File

@ -34,7 +34,7 @@ EOF
is_read_only_command() {
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
;;
*)