#!/usr/bin/env bash
set -euo pipefail
# Requirements: kubectl, jq, awk, sed
have_cmd() { command -v "$1" >/dev/null 2>&1; }
abort(){ echo "Error: $*" >&2; exit 1; }
need(){ for c in "$@"; do have_cmd "$c" || abort "Missing dependency: $c"; done; }
need kubectl jq awk sed
HAVE_TOP=1
kubectl top pods -A >/dev/null 2>&1 || HAVE_TOP=0
HAVE_DFPV=1
kubectl df-pv >/dev/null 2>&1 || HAVE_DFPV=0
TMPDIR="$(mktemp -d)"; trap 'rm -rf "$TMPDIR"' EXIT
# -------------------- POD COUNT --------------------
kubectl get pods -A --no-headers 2>/dev/null \
| awk '{cnt[$1]++} END{for (ns in cnt) print ns, cnt[ns]}' > "$TMPDIR/pods.txt" || true
# -------------------- Requests/Limits per container (normalized in jq) --------------------
# We compute CPU in CORES and Memory in Gi directly in jq to avoid shell math issues.
kubectl get pods -A -o json \
| jq -r '
def toGi:
if . == null then 0
elif type=="number" then (. / (1024*1024*1024))
elif test("m$") then (sub("m$";"") | tonumber) / 1000 / (1024*1024*1024) # milli-bytes -> Gi
elif test("Ki$") then (sub("Ki$";"")|tonumber) / 1024 / 1024
elif test("Mi$") then (sub("Mi$";"")|tonumber) / 1024
elif test("Gi$") then (sub("Gi$";"")|tonumber)
elif test("Ti$") then (sub("Ti$";"")|tonumber) * 1024
elif test("Pi$") then (sub("Pi$";"")|tonumber) * 1024 * 1024
elif test("Ei$") then (sub("Ei$";"")|tonumber) * 1024 * 1024 * 1024
elif test("K$") then (sub("K$";"") | tonumber) * 1000 / (1024*1024*1024)
elif test("M$") then (sub("M$";"") | tonumber) * 1000000 / (1024*1024*1024)
elif test("G$") then (sub("G$";"") | tonumber) * 1000000000 / (1024*1024*1024)
elif test("T$") then (sub("T$";"") | tonumber) * 1000000000000 / (1024*1024*1024)
else (tonumber / (1024*1024*1024)) # raw bytes
end;
def toCores:
if . == null then 0
elif type=="number" then .
elif test("m$") then (sub("m$";"")|tonumber)/1000
else (tonumber)
end;
.items[]
| .metadata.namespace as $ns
| ( [.spec.containers[]?] + [.spec.initContainers[]?] )[]
| [
$ns,
((.resources.requests.cpu // "0") | toCores), # cores
((.resources.limits.cpu // "0") | toCores), # cores
((.resources.requests.memory // "0") | toGi), # Gi
((.resources.limits.memory // "0") | toGi) # Gi
]
| @tsv
' > "$TMPDIR/rl_by_container.tsv"
# Sum to namespace totals
awk '{reqc[$1]+=$2} END{for (k in reqc) printf "%s %.6f\n", k, reqc[k]}' "$TMPDIR/rl_by_container.tsv" > "$TMPDIR/req_cpu_cores.txt"
awk '{limc[$1]+=$3} END{for (k in limc) printf "%s %.6f\n", k, limc[k]}' "$TMPDIR/rl_by_container.tsv" > "$TMPDIR/lim_cpu_cores.txt"
awk '{reqm[$1]+=$4} END{for (k in reqm) printf "%s %.6f\n", k, reqm[k]}' "$TMPDIR/rl_by_container.tsv" > "$TMPDIR/req_mem_gi.txt"
awk '{limm[$1]+=$5} END{for (k in limm) printf "%s %.6f\n", k, limm[k]}' "$TMPDIR/rl_by_container.tsv" > "$TMPDIR/lim_mem_gi.txt"
# -------------------- Actual usage (from metrics-server) --------------------
if [[ $HAVE_TOP -eq 1 ]]; then
kubectl top pods -A --no-headers 2>/dev/null \
| awk '
{
ns=$1; cpu=$3; mem=$4;
# CPU in millicores -> cores
gsub(/m$/,"",cpu);
if (cpu!="") cpu_m[ns]+=cpu;
# Memory: handle Gi/Mi/Ki or bytes
if (mem ~ /Gi$/) { sub(/Gi$/,"",mem); mem_gi[ns]+=mem+0; }
else if (mem ~ /Mi$/) { sub(/Mi$/,"",mem); mem_gi[ns]+=(mem+0)/1024.0; }
else if (mem ~ /Ki$/) { sub(/Ki$/,"",mem); mem_gi[ns]+=(mem+0)/(1024.0*1024.0); }
else { gsub(/[^0-9.]/,"",mem); if (mem!="") mem_gi[ns]+=(mem+0)/(1024.0*1024.0*1024.0); }
}
END{
for (ns in cpu_m) printf "CPU %s %.6f\n", ns, cpu_m[ns]/1000.0;
for (ns in mem_gi) printf "MEM %s %.6f\n", ns, mem_gi[ns];
}
' > "$TMPDIR/top_sums.txt" || true
awk '/^CPU /{print $2, $3}' "$TMPDIR/top_sums.txt" > "$TMPDIR/used_cpu_cores.txt" || true
awk '/^MEM /{print $2, $3}' "$TMPDIR/top_sums.txt" > "$TMPDIR/used_mem_gi.txt" || true
else
: > "$TMPDIR/used_cpu_cores.txt"
: > "$TMPDIR/used_mem_gi.txt"
fi
# -------------------- PVC count and usage --------------------
kubectl get pvc -A --no-headers 2>/dev/null | awk '{cnt[$1]++} END{for (ns in cnt) print ns, cnt[ns]}' > "$TMPDIR/pvc_count.txt" || true
PVC_NOTE=""
if [[ $HAVE_DFPV -eq 1 ]]; then
kubectl df-pv --no-headers 2>/dev/null \
| awk '
{
ns=$1; used=$6;
if (used ~ /Gi$/) { sub(/Gi$/,"",used); val=used+0; }
else if (used ~ /Mi$/) { sub(/Mi$/,"",used); val=(used+0)/1024.0; }
else if (used ~ /Ki$/) { sub(/Ki$/,"",used); val=(used+0)/(1024.0*1024.0); }
else { gsub(/[^0-9.]/,"",used); val=(used+0)/(1024.0*1024.0*1024.0); }
sum[ns]+=val;
}
END{ for (ns in sum) printf "%s %.6f\n", ns, sum[ns]; }
' > "$TMPDIR/pvc_used_gi.txt" || true
else
PVC_NOTE="(PVC Usage shows capacity because 'kubectl df-pv' is not installed)"
kubectl get pvc -A -o json 2>/dev/null \
| jq -r '
def toGi:
if . == null then 0
elif type=="number" then (. / (1024*1024*1024))
elif test("Ki$") then (sub("Ki$";"")|tonumber)/1024/1024
elif test("Mi$") then (sub("Mi$";"")|tonumber)/1024
elif test("Gi$") then (sub("Gi$";"")|tonumber)
elif test("Ti$") then (sub("Ti$";"")|tonumber)*1024
elif test("Pi$") then (sub("Pi$";"")|tonumber)*1024*1024
elif test("Ei$") then (sub("Ei$";"")|tonumber)*1024*1024*1024
elif test("K$") then (sub("K$";"") |tonumber)*1000/(1024*1024*1024)
elif test("M$") then (sub("M$";"") |tonumber)*1000000/(1024*1024*1024)
elif test("G$") then (sub("G$";"") |tonumber)*1000000000/(1024*1024*1024)
elif test("T$") then (sub("T$";"") |tonumber)*1000000000000/(1024*1024*1024)
else (tonumber / (1024*1024*1024))
end;
.items[]
| [.metadata.namespace, ((.status.capacity.storage // .spec.resources.requests.storage // "0") | toGi)]
| @tsv
' \
| awk '{sum[$1]+=$2} END{for (ns in sum) printf "%s %.6f\n", ns, sum[ns]}' > "$TMPDIR/pvc_used_gi.txt" || true
fi
# -------------------- Collect namespaces --------------------
awk '{print $1}' \
"$TMPDIR/pods.txt" \
"$TMPDIR/req_cpu_cores.txt" "$TMPDIR/lim_cpu_cores.txt" \
"$TMPDIR/used_cpu_cores.txt" "$TMPDIR/req_mem_gi.txt" \
"$TMPDIR/lim_mem_gi.txt" "$TMPDIR/used_mem_gi.txt" \
"$TMPDIR/pvc_count.txt" "$TMPDIR/pvc_used_gi.txt" 2>/dev/null \
| sort -u > "$TMPDIR/all_ns.txt"
# -------------------- Print table --------------------
awk -v pods="$TMPDIR/pods.txt" \
-v rcc="$TMPDIR/req_cpu_cores.txt" \
-v lcc="$TMPDIR/lim_cpu_cores.txt" \
-v ucc="$TMPDIR/used_cpu_cores.txt" \
-v rmg="$TMPDIR/req_mem_gi.txt" \
-v lmg="$TMPDIR/lim_mem_gi.txt" \
-v umg="$TMPDIR/used_mem_gi.txt" \
-v pvc="$TMPDIR/pvc_count.txt" \
-v pvcg="$TMPDIR/pvc_used_gi.txt" '
function load(file, arr){ while((getline < file)>0){ arr[$1]=$2 } close(file) }
function rep(n){ s=""; for(i=0;i<n;i++) s=s"-"; return s }
BEGIN{
load(pods,P); load(rcc,RCC); load(lcc,LCC); load(ucc,UCC);
load(rmg,RMG); load(lmg,LMG); load(umg,UMG); load(pvc,PVC); load(pvcg,PVCG);
printf "%-32s %6s %12s %12s %14s %14s %14s %14s %6s %14s\n",
"NAMESPACE","PODS","REQ_CPU","LIM_CPU","CPU_USED","REQ_MEM(Gi)","LIM_MEM(Gi)","MEM_USED(Gi)","PVCs","PVC_USED(Gi)";
printf "%-32s %6s %12s %12s %14s %14s %14s %14s %6s %14s\n",
rep(32),rep(6),rep(12),rep(12),rep(14),rep(14),rep(14),rep(14),rep(6),rep(14);
}
{
ns=$0
printf "%-32s %6s %12.3f %12.3f %14s %14.3f %14.3f %14s %6s %14.3f\n",
ns,
(ns in P ? P[ns] : 0),
(ns in RCC ? RCC[ns]+0.0 : 0.0),
(ns in LCC ? LCC[ns]+0.0 : 0.0),
(ns in UCC ? sprintf("%.3f", UCC[ns]+0.0) : "N/A"),
(ns in RMG ? RMG[ns]+0.0 : 0.0),
(ns in LMG ? LMG[ns]+0.0 : 0.0),
(ns in UMG ? sprintf("%.3f", UMG[ns]+0.0) : "N/A"),
(ns in PVC ? PVC[ns] : 0),
(ns in PVCG ? PVCG[ns]+0.0 : 0.0)
}
' "$TMPDIR/all_ns.txt" | column -t
# -------------------- Notes --------------------
if [[ $HAVE_TOP -eq 0 ]]; then
echo ""
echo "Note: 'kubectl top' not available — CPU_USED and MEM_USED show 'N/A'. Install/enable metrics-server to populate actual usage."
fi
if [[ -n "$PVC_NOTE" ]]; then
echo "Note: $PVC_NOTE"
fi