Last active
April 30, 2026 11:21
-
-
Save nicolargo/a71b13a871b23eab36f4f43e705e104a to your computer and use it in GitHub Desktop.
A simple VPA report shell script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ============================================================================= | |
| # vpa-report.sh | |
| # Display VPA recommendations alongside currently configured resource values. | |
| # | |
| # Columns: | |
| # CURRENT : CPU req / Mem req / Mem limit (from Deployment or StatefulSet spec) | |
| # VPA : CPU req / Mem req / Mem limit suggestion (JVM-aware) | |
| # JVM HEAP : detected heap config | |
| # | |
| # Usage: | |
| # ./vpa-report.sh <namespace> | |
| # ./vpa-report.sh open4-leop-spi-eiffel | |
| # ============================================================================= | |
| set -euo pipefail | |
| NAMESPACE="${1:-open4-leop-spi-eiffel}" | |
| BOLD="\033[1m" | |
| CYAN="\033[36m" | |
| YELLOW="\033[33m" | |
| GREEN="\033[32m" | |
| BLUE="\033[34m" | |
| RED="\033[31m" | |
| RESET="\033[0m" | |
| # ============================================================================= | |
| # Convert a Kubernetes memory string to MiB integer | |
| # ============================================================================= | |
| to_mib() { | |
| 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]+)G$ ]]; then echo $(( ${BASH_REMATCH[1]} * 953 )) | |
| elif [[ "$val" =~ ^([0-9]+)M$ ]]; then echo "${BASH_REMATCH[1]}" | |
| elif [[ "$val" =~ ^([0-9]+)$ ]]; then echo $(( ${BASH_REMATCH[1]} / 1048576 )) | |
| else echo "0" | |
| fi | |
| } | |
| # ============================================================================= | |
| # Convert a Kubernetes CPU string to millicores integer | |
| # ============================================================================= | |
| to_millicores() { | |
| local val="$1" | |
| if [[ "$val" =~ ^([0-9]+)m$ ]]; then echo "${BASH_REMATCH[1]}" | |
| elif [[ "$val" =~ ^([0-9]+)$ ]]; then echo $(( ${BASH_REMATCH[1]} * 1000 )) | |
| else echo "0" | |
| fi | |
| } | |
| # ============================================================================= | |
| # Format millicores to human-readable | |
| # ============================================================================= | |
| fmt_millicores() { | |
| local mc="$1" | |
| if (( mc == 0 )); then echo "-" | |
| elif (( mc >= 1000 )) && (( mc % 1000 == 0 )); then echo "$(( mc / 1000 ))" | |
| else echo "${mc}m" | |
| fi | |
| } | |
| # ============================================================================= | |
| # Format MiB to human-readable | |
| # ============================================================================= | |
| fmt_mib() { | |
| local mib="$1" | |
| if (( mib == 0 )); then echo "-" | |
| elif (( mib >= 1024 )) && (( mib % 1024 == 0 )); then echo "$(( mib / 1024 ))Gi" | |
| else echo "${mib}Mi" | |
| fi | |
| } | |
| # ============================================================================= | |
| # Return colour code by comparing current value vs VPA target (both integers) | |
| # $1 = current, $2 = vpa_target | |
| # green = equal, red = current < vpa (under-provisioned), blue = current > vpa (over-provisioned) | |
| # ============================================================================= | |
| cmp_color() { | |
| local cur="$1" vpa="$2" | |
| if (( cur == vpa )); then echo "$GREEN" | |
| elif (( cur < vpa )); then echo "$RED" | |
| else echo "$BLUE" | |
| fi | |
| } | |
| # ============================================================================= | |
| # Fetch current resource values from a Deployment or StatefulSet (first container) | |
| # $1 = resource name, $2 = kind (Deployment | StatefulSet | ...) | |
| # Output: "<cpu_req_mc>|<mem_req_mib>|<mem_lim_mib>" | |
| # ============================================================================= | |
| get_current_resources() { | |
| local name="$1" | |
| local kind="${2:-Deployment}" | |
| local cpu_req cpu_lim mem_req mem_lim | |
| cpu_req=$(kubectl get "$kind" "$name" -n "$NAMESPACE" \ | |
| -o jsonpath='{.spec.template.spec.containers[0].resources.requests.cpu}' 2>/dev/null || true) | |
| cpu_lim=$(kubectl get "$kind" "$name" -n "$NAMESPACE" \ | |
| -o jsonpath='{.spec.template.spec.containers[0].resources.limits.cpu}' 2>/dev/null || true) | |
| mem_req=$(kubectl get "$kind" "$name" -n "$NAMESPACE" \ | |
| -o jsonpath='{.spec.template.spec.containers[0].resources.requests.memory}' 2>/dev/null || true) | |
| mem_lim=$(kubectl get "$kind" "$name" -n "$NAMESPACE" \ | |
| -o jsonpath='{.spec.template.spec.containers[0].resources.limits.memory}' 2>/dev/null || true) | |
| # Fallback: if requests not explicitly set, Kubernetes defaults them to limits | |
| [[ -z "$cpu_req" && -n "$cpu_lim" ]] && cpu_req="$cpu_lim" | |
| [[ -z "$mem_req" && -n "$mem_lim" ]] && mem_req="$mem_lim" | |
| local cpu_mc=0 mem_req_mib=0 mem_lim_mib=0 | |
| [[ -n "$cpu_req" ]] && cpu_mc=$(to_millicores "$cpu_req") | |
| [[ -n "$mem_req" ]] && mem_req_mib=$(to_mib "$mem_req") | |
| [[ -n "$mem_lim" ]] && mem_lim_mib=$(to_mib "$mem_lim") | |
| echo "${cpu_mc}|${mem_req_mib}|${mem_lim_mib}" | |
| } | |
| # ============================================================================= | |
| # Extract JVM heap config from a resource's first container env vars / args | |
| # $1 = resource name, $2 = kind | |
| # Output: "<xmx_mib>|<ram_pct>" | |
| # ============================================================================= | |
| get_jvm_heap_config() { | |
| local name="$1" | |
| local kind="${2:-Deployment}" | |
| local raw | |
| raw=$(kubectl get "$kind" "$name" -n "$NAMESPACE" \ | |
| -o jsonpath='{range .spec.template.spec.containers[0]}{range .env[*]}{.value}{" "}{end}{range .args[*]}{@}{" "}{end}{range .command[*]}{@}{" "}{end}{end}' \ | |
| 2>/dev/null || true) | |
| local pct_raw | |
| pct_raw=$(echo "$raw" | grep -oE '\-XX:MaxRAMPercentage=[0-9]+(\.[0-9]+)?' | head -1 || true) | |
| if [[ -n "$pct_raw" ]]; then | |
| local pct="${pct_raw#*=}" | |
| echo "0|${pct%%.*}" | |
| return | |
| fi | |
| local xmx_raw | |
| xmx_raw=$(echo "$raw" | grep -oE '\-Xmx[0-9]+[mMgGkK]?' | head -1 || true) | |
| if [[ -n "$xmx_raw" ]]; then | |
| local xmx="${xmx_raw#-Xmx}" | |
| local xmx_mib=0 | |
| if [[ "$xmx" =~ ^([0-9]+)[gG]$ ]]; then xmx_mib=$(( ${BASH_REMATCH[1]} * 1024 )) | |
| elif [[ "$xmx" =~ ^([0-9]+)[mM]$ ]]; then xmx_mib="${BASH_REMATCH[1]}" | |
| elif [[ "$xmx" =~ ^([0-9]+)[kK]$ ]]; then xmx_mib=$(( ${BASH_REMATCH[1]} / 1024 )) | |
| fi | |
| echo "${xmx_mib}|0" | |
| return | |
| fi | |
| echo "0|0" | |
| } | |
| # ============================================================================= | |
| # Compute memory limit suggestion, rounded up to nearest 64Mi | |
| # ============================================================================= | |
| compute_limit_mib() { | |
| local vpa_mib="$1" xmx_mib="$2" ram_pct="$3" | |
| local limit | |
| if (( ram_pct > 0 )); then | |
| limit=$(( (vpa_mib * 100 + ram_pct - 1) / ram_pct )) | |
| elif (( xmx_mib > 0 )); then | |
| local base=$(( xmx_mib > vpa_mib ? xmx_mib : vpa_mib )) | |
| limit=$(( base * 3 / 2 )) | |
| else | |
| limit=$(( vpa_mib * 3 / 2 )) | |
| fi | |
| local r=$(( limit % 64 )) | |
| (( r != 0 )) && limit=$(( limit + 64 - r )) | |
| echo "$limit" | |
| } | |
| # ============================================================================= | |
| # MAIN | |
| # ============================================================================= | |
| echo "" | |
| echo -e "${BOLD}${CYAN}VPA Recommendations — Namespace: ${NAMESPACE}${RESET}" | |
| echo "" | |
| C1=40; C2=12; C3=14; C4=16; C5=12; C6=14; C7=18; C8=14 | |
| TOTAL_W=$(( C1 + C2 + C3 + C4 + C5 + C6 + C7 + C8 + 7 )) | |
| printf "%-${C1}s " "" | |
| printf "${BLUE}${BOLD}%-$((C2+C3+C4+1))s${RESET} " "◀ CURRENT ▶" | |
| printf "${GREEN}${BOLD}%-$((C5+C6+C7+1))s${RESET} " "◀ VPA TARGET ▶" | |
| printf "%-${C8}s\n" "" | |
| printf "${BOLD}%-${C1}s %-${C2}s %-${C3}s %-${C4}s %-${C5}s %-${C6}s %-${C7}s %-${C8}s${RESET}\n" \ | |
| "DEPLOYMENT" "CPU REQ" "MEM REQ" "MEM LIMIT" "CPU REQ" "MEM REQ" "MEM LIMIT" "JVM HEAP CFG" | |
| printf '%0.s─' $(seq 1 $TOTAL_W); echo "" | |
| # Fetch VPA list — use | as field separator to avoid tab/newline ambiguity | |
| vpa_names=$(kubectl get vpa -n "$NAMESPACE" \ | |
| -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true) | |
| if [[ -z "$vpa_names" ]]; then | |
| echo -e "${RED}No VPA objects found in namespace ${NAMESPACE}.${RESET}" | |
| exit 0 | |
| fi | |
| total_cur_cpu_mc=0; total_cur_mem_mib=0; total_cur_lim_mib=0 | |
| total_vpa_cpu_mc=0; total_vpa_mem_mib=0; total_vpa_lim_mib=0 | |
| while IFS= read -r vpa_name; do | |
| [[ -z "$vpa_name" ]] && continue | |
| # Fetch all VPA fields individually — no separator issues | |
| target_kind=$(kubectl get vpa "$vpa_name" -n "$NAMESPACE" \ | |
| -o jsonpath='{.spec.targetRef.kind}' 2>/dev/null || true) | |
| target_name=$(kubectl get vpa "$vpa_name" -n "$NAMESPACE" \ | |
| -o jsonpath='{.spec.targetRef.name}' 2>/dev/null || true) | |
| cpu_target=$(kubectl get vpa "$vpa_name" -n "$NAMESPACE" \ | |
| -o jsonpath='{.status.recommendation.containerRecommendations[0].target.cpu}' 2>/dev/null || true) | |
| mem_target=$(kubectl get vpa "$vpa_name" -n "$NAMESPACE" \ | |
| -o jsonpath='{.status.recommendation.containerRecommendations[0].target.memory}' 2>/dev/null || true) | |
| # Current resources | |
| cur_res=$(get_current_resources "$target_name" "$target_kind") | |
| cur_cpu_mc=$(echo "$cur_res" | cut -d'|' -f1) | |
| cur_mem_mib=$(echo "$cur_res" | cut -d'|' -f2) | |
| cur_lim_mib=$(echo "$cur_res" | cut -d'|' -f3) | |
| cur_cpu_fmt=$(fmt_millicores "$cur_cpu_mc") | |
| cur_mem_fmt=$(fmt_mib "$cur_mem_mib") | |
| cur_lim_fmt=$(fmt_mib "$cur_lim_mib") | |
| # VPA not yet populated | |
| if [[ -z "$cpu_target" || "$cpu_target" == "null" ]]; then | |
| printf "%-${C1}s ${BLUE}%-${C2}s %-${C3}s %-${C4}s${RESET} ${YELLOW}%-${C5}s %-${C6}s %-${C7}s${RESET} %-${C8}s\n" \ | |
| "$target_name" "$cur_cpu_fmt" "$cur_mem_fmt" "$cur_lim_fmt" "n/a" "n/a" "n/a" "-" | |
| total_cur_cpu_mc=$(( total_cur_cpu_mc + cur_cpu_mc )) | |
| total_cur_mem_mib=$(( total_cur_mem_mib + cur_mem_mib )) | |
| total_cur_lim_mib=$(( total_cur_lim_mib + cur_lim_mib )) | |
| continue | |
| fi | |
| # VPA values | |
| vpa_mem_mib=$(to_mib "$mem_target") | |
| vpa_cpu_mc=$(to_millicores "$cpu_target") | |
| # JVM config | |
| jvm_cfg=$(get_jvm_heap_config "$target_name" "$target_kind") | |
| xmx_mib=$(echo "$jvm_cfg" | cut -d'|' -f1) | |
| ram_pct=$(echo "$jvm_cfg" | cut -d'|' -f2) | |
| vpa_lim_mib=$(compute_limit_mib "$vpa_mem_mib" "$xmx_mib" "$ram_pct") | |
| if (( ram_pct > 0 )); then jvm_display="RAM:${ram_pct}%" | |
| elif (( xmx_mib > 0 )); then jvm_display="Xmx:$(fmt_mib $xmx_mib)" | |
| else jvm_display="not found" | |
| fi | |
| total_cur_cpu_mc=$(( total_cur_cpu_mc + cur_cpu_mc )) | |
| total_cur_mem_mib=$(( total_cur_mem_mib + cur_mem_mib )) | |
| total_cur_lim_mib=$(( total_cur_lim_mib + cur_lim_mib )) | |
| total_vpa_cpu_mc=$(( total_vpa_cpu_mc + vpa_cpu_mc )) | |
| total_vpa_mem_mib=$(( total_vpa_mem_mib + vpa_mem_mib )) | |
| total_vpa_lim_mib=$(( total_vpa_lim_mib + vpa_lim_mib )) | |
| # Per-column colour based on current vs VPA comparison | |
| col_cpu=$(cmp_color "$cur_cpu_mc" "$vpa_cpu_mc") | |
| col_mem=$(cmp_color "$cur_mem_mib" "$vpa_mem_mib") | |
| col_lim=$(cmp_color "$cur_lim_mib" "$vpa_lim_mib") | |
| printf "%-${C1}s ${col_cpu}%-${C2}s${RESET} ${col_mem}%-${C3}s${RESET} ${col_lim}%-${C4}s${RESET} ${GREEN}%-${C5}s %-${C6}s %-${C7}s${RESET} %-${C8}s\n" \ | |
| "$target_name" \ | |
| "$cur_cpu_fmt" "$cur_mem_fmt" "$cur_lim_fmt" \ | |
| "$cpu_target" "$(fmt_mib $vpa_mem_mib)" "$(fmt_mib $vpa_lim_mib)" \ | |
| "$jvm_display" | |
| done <<< "$vpa_names" | |
| printf '%0.s─' $(seq 1 $TOTAL_W); echo "" | |
| printf "${BOLD}%-${C1}s ${BLUE}%-${C2}s %-${C3}s %-${C4}s${RESET}${BOLD} ${GREEN}%-${C5}s %-${C6}s %-${C7}s${RESET}${BOLD} %-${C8}s${RESET}\n" \ | |
| "TOTAL" \ | |
| "$(fmt_millicores $total_cur_cpu_mc)" \ | |
| "$(fmt_mib $total_cur_mem_mib)" \ | |
| "$(fmt_mib $total_cur_lim_mib)" \ | |
| "$(fmt_millicores $total_vpa_cpu_mc)" \ | |
| "$(fmt_mib $total_vpa_mem_mib)" \ | |
| "$(fmt_mib $total_vpa_lim_mib)" \ | |
| "" | |
| echo "" | |
| echo -e "${BOLD}Legend:${RESET}" | |
| echo -e " CURRENT CPU REQ / MEM REQ / MEM LIMIT → ${GREEN}green${RESET}=on target ${RED}red${RESET}=under VPA ${BLUE}blue${RESET}=over VPA" | |
| echo -e " ${GREEN}VPA${RESET} CPU REQ / MEM REQ → VPA recommender target" | |
| echo -e " ${GREEN}VPA${RESET} MEM LIMIT → JVM-aware suggestion:" | |
| echo -e " RAM% mode : ceil(VPA / MaxRAMPercentage)" | |
| echo -e " Xmx mode : max(VPA, Xmx) × 1.5" | |
| echo -e " None mode : VPA × 1.5" | |
| echo -e " JVM HEAP CFG → RAM:<n>% = MaxRAMPercentage | Xmx:<val> = absolute heap" | |
| echo "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment