Add control plane taint helper
This commit is contained in:
parent
709881be86
commit
0a166f3a5d
10
README.md
10
README.md
|
|
@ -282,6 +282,7 @@ Run a read-only health snapshot from the Debian server with:
|
||||||
./jeannie backup-status
|
./jeannie backup-status
|
||||||
./jeannie synthetic-checks
|
./jeannie synthetic-checks
|
||||||
./jeannie resource-budget
|
./jeannie resource-budget
|
||||||
|
./jeannie control-plane status
|
||||||
./jeannie artifact-cache status
|
./jeannie artifact-cache status
|
||||||
./jeannie blockchain-devnet status
|
./jeannie blockchain-devnet status
|
||||||
./jeannie blockchain-test
|
./jeannie blockchain-test
|
||||||
|
|
@ -368,7 +369,14 @@ registry push/pull smoke test is opt-in with
|
||||||
|
|
||||||
`resource-budget` checks the report-only policy in `infra/resource-budgets.yml`
|
`resource-budget` checks the report-only policy in `infra/resource-budgets.yml`
|
||||||
against Kubernetes pod requests/limits and Debian host disk/memory signals. Use
|
against Kubernetes pod requests/limits and Debian host disk/memory signals. Use
|
||||||
it before adding heavier workloads so the HP laptop remains usable.
|
it before adding heavier workloads so the HP laptop remains usable. By default
|
||||||
|
it summarizes missing requests/limits by namespace; use
|
||||||
|
`./jeannie resource-budget --details` for every pod/container.
|
||||||
|
|
||||||
|
`control-plane` shows or manages the Debian Kubernetes control-plane scheduling
|
||||||
|
taint. After Pimox workers are stable, use `./jeannie control-plane taint`
|
||||||
|
to keep normal app/data-plane pods off the Debian host while control-plane and
|
||||||
|
DaemonSet workloads continue using their normal tolerations.
|
||||||
|
|
||||||
`artifact-cache` manages an optional Debian-host cache stack in
|
`artifact-cache` manages an optional Debian-host cache stack in
|
||||||
`infra/artifact-cache` with apt-cacher-ng and a Docker Hub registry pull-through
|
`infra/artifact-cache` with apt-cacher-ng and a Docker Hub registry pull-through
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,7 @@ Run a read-only health snapshot from the Debian server with:
|
||||||
./{{ main_script }} backup-status
|
./{{ main_script }} backup-status
|
||||||
./{{ main_script }} synthetic-checks
|
./{{ main_script }} synthetic-checks
|
||||||
./{{ main_script }} resource-budget
|
./{{ main_script }} resource-budget
|
||||||
|
./{{ main_script }} control-plane status
|
||||||
./{{ main_script }} artifact-cache status
|
./{{ main_script }} artifact-cache status
|
||||||
./{{ main_script }} blockchain-devnet status
|
./{{ main_script }} blockchain-devnet status
|
||||||
./{{ main_script }} blockchain-test
|
./{{ main_script }} blockchain-test
|
||||||
|
|
@ -368,7 +369,14 @@ registry push/pull smoke test is opt-in with
|
||||||
|
|
||||||
`resource-budget` checks the report-only policy in `infra/resource-budgets.yml`
|
`resource-budget` checks the report-only policy in `infra/resource-budgets.yml`
|
||||||
against Kubernetes pod requests/limits and Debian host disk/memory signals. Use
|
against Kubernetes pod requests/limits and Debian host disk/memory signals. Use
|
||||||
it before adding heavier workloads so the HP laptop remains usable.
|
it before adding heavier workloads so the HP laptop remains usable. By default
|
||||||
|
it summarizes missing requests/limits by namespace; use
|
||||||
|
`./{{ main_script }} resource-budget --details` for every pod/container.
|
||||||
|
|
||||||
|
`control-plane` shows or manages the Debian Kubernetes control-plane scheduling
|
||||||
|
taint. After Pimox workers are stable, use `./{{ main_script }} control-plane taint`
|
||||||
|
to keep normal app/data-plane pods off the Debian host while control-plane and
|
||||||
|
DaemonSet workloads continue using their normal tolerations.
|
||||||
|
|
||||||
`artifact-cache` manages an optional Debian-host cache stack in
|
`artifact-cache` manages an optional Debian-host cache stack in
|
||||||
`infra/artifact-cache` with apt-cacher-ng and a Docker Hub registry pull-through
|
`infra/artifact-cache` with apt-cacher-ng and a Docker Hub registry pull-through
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,13 @@ opt-in through `LAB_SYNTHETIC_REGISTRY_PUSH=true`.
|
||||||
|
|
||||||
`resource-budget`
|
`resource-budget`
|
||||||
: Check the report-only resource policy in `infra/resource-budgets.yml` against
|
: Check the report-only resource policy in `infra/resource-budgets.yml` against
|
||||||
Kubernetes pod requests/limits and Debian disk/memory signals.
|
Kubernetes pod requests/limits and Debian disk/memory signals. Use
|
||||||
|
`resource-budget --details` for the full pod/container list.
|
||||||
|
|
||||||
|
`control-plane {status|taint|untaint|pods}`
|
||||||
|
: Show or manage the Debian Kubernetes control-plane scheduling taint. Use
|
||||||
|
`control-plane taint` after Pimox workers are stable to keep normal app/data
|
||||||
|
plane pods off the Debian host.
|
||||||
|
|
||||||
`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
|
||||||
|
|
|
||||||
14
jeannie
14
jeannie
|
|
@ -5999,7 +5999,12 @@ synthetic_checks() {
|
||||||
|
|
||||||
resource_budget() {
|
resource_budget() {
|
||||||
require_debian_server "resource-budget"
|
require_debian_server "resource-budget"
|
||||||
"${REPO_ROOT}/scripts/resource-budget"
|
"${REPO_ROOT}/scripts/resource-budget" "${@:2}"
|
||||||
|
}
|
||||||
|
|
||||||
|
control_plane() {
|
||||||
|
require_debian_server "control-plane"
|
||||||
|
"${REPO_ROOT}/scripts/control-plane" "${@:2}"
|
||||||
}
|
}
|
||||||
|
|
||||||
artifact_cache() {
|
artifact_cache() {
|
||||||
|
|
@ -6120,6 +6125,8 @@ Observability And GitOps
|
||||||
synthetic-checks Run end-to-end service probes.
|
synthetic-checks Run end-to-end service probes.
|
||||||
resource-budget Report Kubernetes resource request/limit gaps.
|
resource-budget Report Kubernetes resource request/limit gaps.
|
||||||
route-inventory Report routes and Uptime Kuma coverage.
|
route-inventory Report routes and Uptime Kuma coverage.
|
||||||
|
control-plane {status|taint|untaint|pods}
|
||||||
|
Keep normal pods off the Debian control plane.
|
||||||
|
|
||||||
Recovery And State
|
Recovery And State
|
||||||
backup-gitea Back up the Debian-hosted Gitea data.
|
backup-gitea Back up the Debian-hosted Gitea data.
|
||||||
|
|
@ -6221,7 +6228,10 @@ case "${1:-}" in
|
||||||
synthetic_checks
|
synthetic_checks
|
||||||
;;
|
;;
|
||||||
resource-budget)
|
resource-budget)
|
||||||
resource_budget
|
resource_budget "$@"
|
||||||
|
;;
|
||||||
|
control-plane)
|
||||||
|
control_plane "$@"
|
||||||
;;
|
;;
|
||||||
artifact-cache)
|
artifact-cache)
|
||||||
artifact_cache "$@"
|
artifact_cache "$@"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
KUBECONFIG_PATH="${KUBECONFIG:-${LAB_KUBECONFIG_PATH:-/home/jv/.kube/config}}"
|
||||||
|
CONTROL_PLANE_NODE="${LAB_CONTROL_PLANE_NODE:-debian}"
|
||||||
|
TAINT_KEY="${LAB_CONTROL_PLANE_TAINT_KEY:-node-role.kubernetes.io/control-plane}"
|
||||||
|
TAINT_EFFECT="${LAB_CONTROL_PLANE_TAINT_EFFECT:-NoSchedule}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: ./jeannie control-plane {status|taint|untaint|pods}
|
||||||
|
|
||||||
|
status Show node taints and non-system pods currently scheduled on ${CONTROL_PLANE_NODE}.
|
||||||
|
taint Add ${TAINT_KEY}:${TAINT_EFFECT} to keep normal app/data-plane pods off the control plane.
|
||||||
|
untaint Remove ${TAINT_KEY}:${TAINT_EFFECT} from the control plane.
|
||||||
|
pods List non-system pods currently scheduled on ${CONTROL_PLANE_NODE}.
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
LAB_CONTROL_PLANE_NODE=${CONTROL_PLANE_NODE}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
require_kubectl() {
|
||||||
|
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
|
||||||
|
if ! kubectl --kubeconfig "$KUBECONFIG_PATH" get node "$CONTROL_PLANE_NODE" >/dev/null 2>&1; then
|
||||||
|
echo "control-plane node not found: ${CONTROL_PLANE_NODE}" >&2
|
||||||
|
echo "Set LAB_CONTROL_PLANE_NODE if the node has a different name." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
show_taints() {
|
||||||
|
kubectl --kubeconfig "$KUBECONFIG_PATH" get node "$CONTROL_PLANE_NODE" \
|
||||||
|
-o jsonpath='{range .spec.taints[*]}{.key}{"="}{.value}{":"}{.effect}{"\n"}{end}' |
|
||||||
|
sed '/^$/d' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
show_non_system_pods() {
|
||||||
|
kubectl --kubeconfig "$KUBECONFIG_PATH" get pods -A \
|
||||||
|
--field-selector "spec.nodeName=${CONTROL_PLANE_NODE}" \
|
||||||
|
-o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,STATUS:.status.phase' --no-headers |
|
||||||
|
awk '$1 !~ /^(kube-system|calico-system|tigera-operator)$/ { print }'
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-status}" in
|
||||||
|
status)
|
||||||
|
require_kubectl
|
||||||
|
echo "Node: ${CONTROL_PLANE_NODE}"
|
||||||
|
echo
|
||||||
|
echo "Taints:"
|
||||||
|
if ! show_taints | sed 's/^/ /'; then
|
||||||
|
true
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo "Non-system pods currently on control plane:"
|
||||||
|
if ! show_non_system_pods | sed 's/^/ /'; then
|
||||||
|
true
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
taint)
|
||||||
|
require_kubectl
|
||||||
|
kubectl --kubeconfig "$KUBECONFIG_PATH" taint node "$CONTROL_PLANE_NODE" "${TAINT_KEY}:${TAINT_EFFECT}" --overwrite
|
||||||
|
echo
|
||||||
|
echo "Current taints:"
|
||||||
|
show_taints | sed 's/^/ /'
|
||||||
|
;;
|
||||||
|
untaint)
|
||||||
|
require_kubectl
|
||||||
|
kubectl --kubeconfig "$KUBECONFIG_PATH" taint node "$CONTROL_PLANE_NODE" "${TAINT_KEY}:${TAINT_EFFECT}-" || true
|
||||||
|
echo
|
||||||
|
echo "Current taints:"
|
||||||
|
show_taints | sed 's/^/ /'
|
||||||
|
;;
|
||||||
|
pods)
|
||||||
|
require_kubectl
|
||||||
|
show_non_system_pods
|
||||||
|
;;
|
||||||
|
-h|--help|help)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
@ -4,10 +4,40 @@ set -euo pipefail
|
||||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
BUDGET_FILE="${HOMELAB_RESOURCE_BUDGET_FILE:-${REPO_ROOT}/infra/resource-budgets.yml}"
|
BUDGET_FILE="${HOMELAB_RESOURCE_BUDGET_FILE:-${REPO_ROOT}/infra/resource-budgets.yml}"
|
||||||
KUBECONFIG_PATH="${KUBECONFIG:-${LAB_KUBECONFIG_PATH:-/home/jv/.kube/config}}"
|
KUBECONFIG_PATH="${KUBECONFIG:-${LAB_KUBECONFIG_PATH:-/home/jv/.kube/config}}"
|
||||||
|
DETAILS=false
|
||||||
|
MAX_EXAMPLES="${LAB_RESOURCE_BUDGET_EXAMPLES:-12}"
|
||||||
|
|
||||||
failures=0
|
failures=0
|
||||||
warnings=0
|
warnings=0
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: ./jeannie resource-budget [--details]
|
||||||
|
|
||||||
|
Print host and Kubernetes resource budget checks.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--details Show every pod/container missing requests or limits.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--details|--verbose)
|
||||||
|
DETAILS=true
|
||||||
|
;;
|
||||||
|
-h|--help|help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
section() {
|
section() {
|
||||||
printf '\n== %s ==\n' "$1"
|
printf '\n== %s ==\n' "$1"
|
||||||
}
|
}
|
||||||
|
|
@ -58,12 +88,15 @@ kubernetes_budget() {
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
python3 - "$BUDGET_FILE" "$KUBECONFIG_PATH" <<'PY'
|
python3 - "$BUDGET_FILE" "$KUBECONFIG_PATH" "$DETAILS" "$MAX_EXAMPLES" <<'PY'
|
||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
|
||||||
budget_file, kubeconfig = sys.argv[1:3]
|
budget_file, kubeconfig, details_arg, max_examples_arg = sys.argv[1:5]
|
||||||
|
show_details = details_arg.lower() == "true"
|
||||||
|
max_examples = int(max_examples_arg)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import yaml
|
import yaml
|
||||||
|
|
@ -120,6 +153,7 @@ raw = subprocess.check_output(
|
||||||
pods = json.loads(raw).get("items", [])
|
pods = json.loads(raw).get("items", [])
|
||||||
namespace_totals = {}
|
namespace_totals = {}
|
||||||
missing = []
|
missing = []
|
||||||
|
missing_by_namespace = defaultdict(Counter)
|
||||||
|
|
||||||
for pod in pods:
|
for pod in pods:
|
||||||
ns = pod["metadata"]["namespace"]
|
ns = pod["metadata"]["namespace"]
|
||||||
|
|
@ -134,8 +168,10 @@ for pod in pods:
|
||||||
namespace_totals[ns]["memory"] += mem_to_mib(requests.get("memory"))
|
namespace_totals[ns]["memory"] += mem_to_mib(requests.get("memory"))
|
||||||
if cluster_budget.get("require_requests", True) and ("cpu" not in requests or "memory" not in requests):
|
if cluster_budget.get("require_requests", True) and ("cpu" not in requests or "memory" not in requests):
|
||||||
missing.append(f"{ns}/{pod_name}:{name} missing requests")
|
missing.append(f"{ns}/{pod_name}:{name} missing requests")
|
||||||
|
missing_by_namespace[ns]["requests"] += 1
|
||||||
if cluster_budget.get("require_limits", True) and ("cpu" not in limits or "memory" not in limits):
|
if cluster_budget.get("require_limits", True) and ("cpu" not in limits or "memory" not in limits):
|
||||||
missing.append(f"{ns}/{pod_name}:{name} missing limits")
|
missing.append(f"{ns}/{pod_name}:{name} missing limits")
|
||||||
|
missing_by_namespace[ns]["limits"] += 1
|
||||||
|
|
||||||
total_cpu = sum(item["cpu"] for item in namespace_totals.values())
|
total_cpu = sum(item["cpu"] for item in namespace_totals.values())
|
||||||
total_memory = sum(item["memory"] for item in namespace_totals.values())
|
total_memory = sum(item["memory"] for item in namespace_totals.values())
|
||||||
|
|
@ -163,11 +199,30 @@ for ns, totals in sorted(namespace_totals.items()):
|
||||||
exit_code = 1
|
exit_code = 1
|
||||||
|
|
||||||
if missing:
|
if missing:
|
||||||
print("missing_resource_policy:")
|
print(f"missing_resource_policy: total={len(missing)}")
|
||||||
for item in missing[:50]:
|
print("by_namespace:")
|
||||||
|
for ns, counts in sorted(
|
||||||
|
missing_by_namespace.items(),
|
||||||
|
key=lambda item: (item[1]["requests"] + item[1]["limits"], item[0]),
|
||||||
|
reverse=True,
|
||||||
|
):
|
||||||
|
print(f" {ns}: requests_missing={counts['requests']} limits_missing={counts['limits']}")
|
||||||
|
|
||||||
|
if show_details:
|
||||||
|
print("details:")
|
||||||
|
for item in missing:
|
||||||
print(f" {item}")
|
print(f" {item}")
|
||||||
if len(missing) > 50:
|
else:
|
||||||
print(f" ... {len(missing) - 50} more")
|
print("examples:")
|
||||||
|
for item in missing[:max_examples]:
|
||||||
|
print(f" {item}")
|
||||||
|
if len(missing) > max_examples:
|
||||||
|
print(f" ... {len(missing) - max_examples} more")
|
||||||
|
print(" run ./jeannie resource-budget --details for the full list")
|
||||||
|
print("next_steps:")
|
||||||
|
print(" 1. Add requests before limits for app workloads you own.")
|
||||||
|
print(" 2. Leave kube-system/control-plane static pods for a separate pass.")
|
||||||
|
print(" 3. Use ./jeannie control-plane taint after Pimox workers are Ready.")
|
||||||
exit_code = 1
|
exit_code = 1
|
||||||
|
|
||||||
raise SystemExit(exit_code)
|
raise SystemExit(exit_code)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue