Skip to content

Instantly share code, notes, and snippets.

@nicolargo
Last active April 30, 2026 11:21
Show Gist options
  • Select an option

  • Save nicolargo/a71b13a871b23eab36f4f43e705e104a to your computer and use it in GitHub Desktop.

Select an option

Save nicolargo/a71b13a871b23eab36f4f43e705e104a to your computer and use it in GitHub Desktop.
A simple VPA report shell script
#!/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