#!/usr/bin/env bash set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" run_check() { local name="$1" shift echo "## ${name}" set +e "$@" 2>&1 local status=$? set -e echo "exit_status=${status}" } tmp_file="$(mktemp "${TMPDIR:-/tmp}/jeannie-capacity-advisor.XXXXXX")" trap 'rm -f "$tmp_file"' EXIT { run_check "capacity" "${REPO_ROOT}/scripts/capacity-report" --only problems run_check "resource-budget" "${REPO_ROOT}/scripts/resource-budget" run_check "control-plane" "${REPO_ROOT}/scripts/control-plane" status } >"$tmp_file" python3 - "$tmp_file" <<'PY' import re import sys text = open(sys.argv[1], encoding="utf-8").read() lowered = text.lower() missing_match = re.search(r"(\d+)\s+pods missing requests/limits", text) missing_count = int(missing_match.group(1)) if missing_match else 0 pressure = any(token in lowered for token in ["diskpressure", "memorypressure", "node pressure"]) worker_storage_warn = "worker storage" in lowered and "warn" in lowered control_plane_section = text.split("## control-plane", 1)[-1] control_plane_tainted = "node-role.kubernetes.io/control-plane" in control_plane_section and ":NoSchedule" in control_plane_section app_control_plane_pods = [] legacy_control_plane_pods = [] for line in control_plane_section.splitlines(): match = re.match(r"\s*([a-z0-9-]+)\s+([^\s]+)\s+([A-Za-z]+)\s*$", line) if not match: continue namespace, pod_name, status = match.groups() if namespace == "arr-stack": legacy_control_plane_pods.append((namespace, pod_name, status)) elif namespace in {"argocd", "website-production", "demos-static", "heimdall", "n8n", "security-lab", "monitoring"}: app_control_plane_pods.append((namespace, pod_name, status)) print("Jeannie Capacity Advisor") print("========================") recommendations = [] if missing_count: recommendations.append( ( "Fix resource requests/limits first", f"{missing_count} pods are missing requests/limits, so scheduling and capacity math are not trustworthy yet.", "./jeannie resource-budget --details", "code/config: add resources to app manifests or Helm values", ) ) if legacy_control_plane_pods: count = len(legacy_control_plane_pods) recommendations.append( ( "Remove stale Kubernetes ARR workloads", f"{count} legacy arr-stack pod(s) still exist in Kubernetes even though ARR now lives in Docker Compose on Debian.", "kubectl delete namespace arr-stack", "cluster cleanup: active ARR state is infra/arr-stack, not archive/apps/arr-stack", ) ) if app_control_plane_pods and control_plane_tainted: count = len(app_control_plane_pods) namespaces = ", ".join(sorted({namespace for namespace, _, _ in app_control_plane_pods})) recommendations.append( ( "Reschedule remaining app/platform pods off Debian", f"Debian is already tainted, but {count} pod(s) still run there in: {namespaces}. Existing pods are not evicted by a NoSchedule taint.", "./jeannie control-plane pods", "scheduling: restart or drain selected workloads; if they return to Debian, check tolerations, node selectors, affinity, and worker readiness", ) ) elif app_control_plane_pods: recommendations.append( ( "Keep app/data-plane pods off Debian", "Non-system workloads are still scheduled on the Debian control-plane host.", "./jeannie control-plane taint", "scheduling: taint Debian after Pimox workers are Ready", ) ) if pressure or worker_storage_warn: recommendations.append( ( "Consider adding or recreating Pimox worker capacity", "Cluster pressure or worker storage pressure is visible after current placement checks.", "./jeannie workers list", "capacity: increase LAB_PIMOX_WORKER_COUNT or recreate the pressured worker", ) ) if not recommendations: recommendations.append( ( "No new Pimox VM needed from current signals", "No capacity warning, node pressure, or control-plane placement issue was detected in the sampled reports.", "./jeannie capacity", "watch: rerun after adding workloads", ) ) for index, (title, why, command, fix) in enumerate(recommendations, start=1): print(f"\n{index}. {title}") print(f" Why: {why}") print(f" Next command: {command}") print(f" Fix path: {fix}") print("\nRule of thumb") print("- Add requests before deciding you need more VMs.") print("- Taint Debian once Pimox workers are stable.") print("- Add a Pimox VM only when real pressure remains after placement and requests are cleaned up.") PY