#!/usr/bin/env bash set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" KUBECONFIG_PATH="${KUBECONFIG:-${LAB_KUBECONFIG_PATH:-/home/jv/.kube/config}}" PROFILE_FILE="${HOMELAB_RESOURCE_RECOMMENDATIONS:-${REPO_ROOT}/infra/resource-recommendations.yml}" NAMESPACE="${1:-}" PODS_JSON="" cleanup() { if [ -n "$PODS_JSON" ] && [ -f "$PODS_JSON" ]; then rm -f "$PODS_JSON" fi } trap cleanup EXIT usage() { cat <<'EOF' Usage: ./jeannie capacity-limits [namespace] Recommend Kubernetes requests/limits for containers missing them. This command prints YAML snippets only; it does not edit manifests. EOF } case "${1:-}" in -h|--help|help) usage exit 0 ;; esac if ! command -v kubectl >/dev/null 2>&1; then echo "kubectl is required." >&2 exit 1 fi if [ ! -s "$KUBECONFIG_PATH" ]; then echo "kubeconfig is missing: $KUBECONFIG_PATH" >&2 exit 1 fi PODS_JSON="$(mktemp "${TMPDIR:-/tmp}/jeannie-capacity-limits.XXXXXX")" if [ -n "$NAMESPACE" ]; then kubectl --kubeconfig "$KUBECONFIG_PATH" -n "$NAMESPACE" get pods -o json >"$PODS_JSON" else kubectl --kubeconfig "$KUBECONFIG_PATH" get pods -A -o json >"$PODS_JSON" fi python3 - "$PROFILE_FILE" "$PODS_JSON" <<'PY' import json import sys profile_file, pods_json = sys.argv[1:3] try: import yaml except ImportError: yaml = None profiles = { "defaults": { "request_cpu_m": 50, "request_memory_mib": 128, "limit_cpu_multiplier": 4, "limit_memory_multiplier": 2, }, "namespaces": {}, } if yaml: with open(profile_file, encoding="utf-8") as handle: loaded = yaml.safe_load(handle) or {} profiles["defaults"].update(loaded.get("defaults", {})) profiles["namespaces"].update(loaded.get("namespaces", {})) with open(pods_json, encoding="utf-8") as handle: pods = json.load(handle).get("items", []) seen = set() def profile_for(namespace): profile = dict(profiles["defaults"]) profile.update(profiles["namespaces"].get(namespace, {})) return profile print("Jeannie Capacity Limits") print("=======================") print("mode: recommendations only") print(f"profile_file: {profile_file}") count = 0 for pod in pods: namespace = pod["metadata"].get("namespace", "default") owner = namespace for ref in pod["metadata"].get("ownerReferences", []): if ref.get("kind") in {"ReplicaSet", "StatefulSet", "DaemonSet", "Job"}: owner = ref.get("name", owner) break for container in pod.get("spec", {}).get("containers", []): resources = container.get("resources", {}) requests = resources.get("requests", {}) limits = resources.get("limits", {}) if requests.get("cpu") and requests.get("memory") and limits.get("cpu") and limits.get("memory"): continue key = (namespace, owner, container["name"]) if key in seen: continue seen.add(key) profile = profile_for(namespace) request_cpu = int(profile["request_cpu_m"]) request_memory = int(profile["request_memory_mib"]) limit_cpu = request_cpu * int(profile["limit_cpu_multiplier"]) limit_memory = request_memory * int(profile["limit_memory_multiplier"]) count += 1 print(f"\n{count}. {namespace}/{owner}:{container['name']}") print(" Add or tune this resources block in the owning manifest/chart:") print(" resources:") print(" requests:") print(f" cpu: {request_cpu}m") print(f" memory: {request_memory}Mi") print(" limits:") print(f" cpu: {limit_cpu}m") print(f" memory: {limit_memory}Mi") if count == 0: print("\nNo containers missing requests/limits were found.") else: print("\nNotes:") print("- Treat these as starting values, not truth.") print("- Prefer app namespaces first; be careful with kube-system/control-plane pods.") print("- After changes, run ./jeannie resource-budget and ./jeannie capacity.") PY