#!/usr/bin/env bash # Outputs per-pod CPU/MEM metrics and namespace quota as JSON. # Usage: k8s-metrics.sh set -euo pipefail NS="${1:-tenant-5}" # ── Unit normalization ─────────────────────────────────────────────────────── # Normalize CPU string to millicores integer normalize_cpu() { local val="$1" if [[ "$val" =~ ^([0-9]+)m$ ]]; then echo "${BASH_REMATCH[1]}" elif [[ "$val" =~ ^([0-9]+(\.[0-9]+)?)$ ]]; then awk "BEGIN { printf \"%d\", ${BASH_REMATCH[1]} * 1000 }" else echo 0 fi } # Normalize memory string to MiB integer normalize_mem() { local val="$1" if [[ "$val" =~ ^([0-9]+)Gi$ ]]; then echo $(( ${BASH_REMATCH[1]} * 1024 )) elif [[ "$val" =~ ^([0-9]+)Mi$ ]]; then echo "${BASH_REMATCH[1]}" elif [[ "$val" =~ ^([0-9]+)Ki$ ]]; then echo $(( ${BASH_REMATCH[1]} / 1024 )) elif [[ "$val" =~ ^([0-9]+)$ ]]; then echo $(( ${BASH_REMATCH[1]} / 1048576 )) else echo 0 fi } # Format millicores for display (always "Xm") fmt_cpu() { local m="$1" echo "${m}m" } # Format MiB for display ("343Mi" or "1.50Gi") fmt_mem() { local mib="$1" if [[ $mib -lt 1024 ]]; then echo "${mib}Mi" else awk "BEGIN { printf \"%.2fGi\", $mib / 1024 }" fi } # ── Fetch data ─────────────────────────────────────────────────────────────── pods_json=$(kubectl get pods -n "$NS" -o json 2>/dev/null) || exit 1 top_output=$(kubectl top pods -n "$NS" --no-headers 2>/dev/null) || top_output="" quota_json=$(kubectl get resourcequota -n "$NS" -o json 2>/dev/null) || quota_json='{"items":[]}' # ── Build podName → app map ────────────────────────────────────────────────── declare -A name_to_app while IFS= read -r line; do pod_name=$(echo "$line" | jq -r '.name') app=$(echo "$line" | jq -r '.app') name_to_app["$pod_name"]="$app" done < <(echo "$pods_json" | jq -c '.items[] | {name: .metadata.name, app: (.metadata.labels["app.kubernetes.io/instance"] // .metadata.name)}') # ── Parse kubectl top ──────────────────────────────────────────────────────── declare -A top_cpu # app → cpuM declare -A top_mem # app → memMi cpu_actual=0 mem_actual=0 while IFS= read -r line; do [[ -z "$line" ]] && continue read -r pod_name cpu_str mem_str <<< "$line" app="${name_to_app[$pod_name]:-}" [[ -z "$app" ]] && continue cpu_m=$(normalize_cpu "$cpu_str") mem_mib=$(normalize_mem "$mem_str") top_cpu["$app"]=$(( ${top_cpu[$app]:-0} + cpu_m )) top_mem["$app"]=$(( ${top_mem[$app]:-0} + mem_mib )) cpu_actual=$(( cpu_actual + cpu_m )) mem_actual=$(( mem_actual + mem_mib )) done <<< "$top_output" # ── Build podMetrics JSON array ────────────────────────────────────────────── pod_metrics="[" first=1 while IFS= read -r line; do app=$(echo "$line" | jq -r '.app') cpu_m="${top_cpu[$app]:-"-1"}" mem_mib="${top_mem[$app]:-"-1"}" [[ $first -eq 0 ]] && pod_metrics+="," pod_metrics+=$(jq -nc --arg app "$app" --argjson cpu "$cpu_m" --argjson mem "$mem_mib" \ '{app: $app, cpuM: $cpu, memMi: $mem}') first=0 done < <(echo "$pods_json" | jq -c '[.items[] | {app: (.metadata.labels["app.kubernetes.io/instance"] // .metadata.name)}] | unique_by(.app)[]') pod_metrics+="]" # ── Parse resourcequota ────────────────────────────────────────────────────── q_cpu_req_used=$(echo "$quota_json" | jq -r '.items[0].status.used["requests.cpu"] // "0"') q_cpu_req_hard=$(echo "$quota_json" | jq -r '.items[0].status.hard["requests.cpu"] // "0"') q_cpu_lim_used=$(echo "$quota_json" | jq -r '.items[0].status.used["limits.cpu"] // "0"') q_cpu_lim_hard=$(echo "$quota_json" | jq -r '.items[0].status.hard["limits.cpu"] // "0"') q_mem_req_used=$(echo "$quota_json" | jq -r '.items[0].status.used["requests.memory"] // "0"') q_mem_req_hard=$(echo "$quota_json" | jq -r '.items[0].status.hard["requests.memory"] // "0"') q_mem_lim_used=$(echo "$quota_json" | jq -r '.items[0].status.used["limits.memory"] // "0"') q_mem_lim_hard=$(echo "$quota_json" | jq -r '.items[0].status.hard["limits.memory"] // "0"') cpu_req_used_m=$(normalize_cpu "$q_cpu_req_used") cpu_req_hard_m=$(normalize_cpu "$q_cpu_req_hard") cpu_lim_used_m=$(normalize_cpu "$q_cpu_lim_used") cpu_lim_hard_m=$(normalize_cpu "$q_cpu_lim_hard") mem_req_used_mib=$(normalize_mem "$q_mem_req_used") mem_req_hard_mib=$(normalize_mem "$q_mem_req_hard") mem_lim_used_mib=$(normalize_mem "$q_mem_lim_used") mem_lim_hard_mib=$(normalize_mem "$q_mem_lim_hard") cpu_req_pct=$(awk "BEGIN { printf \"%.4f\", $cpu_req_used_m / ($cpu_req_hard_m > 0 ? $cpu_req_hard_m : 1) }") cpu_lim_pct=$(awk "BEGIN { printf \"%.4f\", $cpu_lim_used_m / ($cpu_lim_hard_m > 0 ? $cpu_lim_hard_m : 1) }") mem_req_pct=$(awk "BEGIN { printf \"%.4f\", $mem_req_used_mib / ($mem_req_hard_mib > 0 ? $mem_req_hard_mib : 1) }") mem_lim_pct=$(awk "BEGIN { printf \"%.4f\", $mem_lim_used_mib / ($mem_lim_hard_mib > 0 ? $mem_lim_hard_mib : 1) }") cpu_req_label="$(fmt_cpu $cpu_req_used_m) / $(fmt_cpu $cpu_req_hard_m)" cpu_lim_label="$(fmt_cpu $cpu_lim_used_m) / $(fmt_cpu $cpu_lim_hard_m)" mem_req_label="$(fmt_mem $mem_req_used_mib) / $(fmt_mem $mem_req_hard_mib)" mem_lim_label="$(fmt_mem $mem_lim_used_mib) / $(fmt_mem $mem_lim_hard_mib)" # ── Output ─────────────────────────────────────────────────────────────────── jq -nc \ --argjson podMetrics "$pod_metrics" \ --argjson cpuActualM "$cpu_actual" \ --argjson memActualMi "$mem_actual" \ --argjson cpuReqPct "$cpu_req_pct" \ --argjson cpuLimPct "$cpu_lim_pct" \ --argjson memReqPct "$mem_req_pct" \ --argjson memLimPct "$mem_lim_pct" \ --arg cpuReqLabel "$cpu_req_label" \ --arg cpuLimLabel "$cpu_lim_label" \ --arg memReqLabel "$mem_req_label" \ --arg memLimLabel "$mem_lim_label" \ '{ podMetrics: $podMetrics, quota: { cpuActualM: $cpuActualM, memActualMi: $memActualMi, cpuReqPct: $cpuReqPct, cpuLimPct: $cpuLimPct, memReqPct: $memReqPct, memLimPct: $memLimPct, cpuReqLabel: $cpuReqLabel, cpuLimLabel: $cpuLimLabel, memReqLabel: $memReqLabel, memLimLabel: $memLimLabel } }'