Skip to content

Commit c88f9bb

Browse files
authored
feat: add script to manage hpa (#1318)
1 parent 8276cb4 commit c88f9bb

File tree

1 file changed

+240
-0
lines changed

1 file changed

+240
-0
lines changed

scripts/manage_hpa.sh

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
#!/bin/bash
2+
3+
# --- Configuration ---
4+
# Default file to store the original HPA settings for reversal
5+
DEFAULT_HPA_CONFIG_FILE="hpa_originals.json"
6+
# Name of the temporary patch file
7+
PATCH_FILE="hpa-patch.json"
8+
9+
# --- Global Parameter ---
10+
DRY_RUN=false
11+
12+
# --- Functions ---
13+
14+
# Function to safely exit on error and cleanup
15+
function safe_exit {
16+
echo "ERROR: $1" >&2
17+
if [ -f "$PATCH_FILE" ]; then
18+
rm -f "$PATCH_FILE"
19+
fi
20+
exit 1
21+
}
22+
23+
# Function to execute kubectl patch based on DRY_RUN mode
24+
function execute_patch {
25+
local NAME="$1"
26+
local NAMESPACE="$2"
27+
local PATCH_TYPE="$3"
28+
local PATCH_FILE_NAME="$4"
29+
local ACTION_DESC="$5"
30+
31+
if $DRY_RUN; then
32+
echo " [DRY-RUN] Would have executed: kubectl patch hpa $NAME -n $NAMESPACE --type=$PATCH_TYPE --patch-file=$PATCH_FILE_NAME"
33+
echo " [DRY-RUN] Patch Content:"
34+
cat "$PATCH_FILE_NAME" | sed 's/^/ /'
35+
echo " [DRY-RUN] Action: $ACTION_DESC"
36+
return 0
37+
else
38+
echo " -> Applying patch: $ACTION_DESC"
39+
if kubectl patch hpa "$NAME" -n "$NAMESPACE" --type="$PATCH_TYPE" --patch-file="$PATCH_FILE_NAME"; then
40+
return 0
41+
else
42+
return 1
43+
fi
44+
fi
45+
}
46+
47+
# Function to display current HPA state (used in dry-run)
48+
function display_current_hpa_state {
49+
echo "--- CURRENT LIVE HPA STATE ---"
50+
51+
# Check for the lock file status
52+
if [ -f "$1" ]; then
53+
echo "Lock Status: LOCKED (Config file $1 EXISTS)"
54+
else
55+
echo "Lock Status: UNLOCKED (Config file $1 does NOT exist)"
56+
fi
57+
58+
# Display the current HPA table
59+
kubectl get hpa --all-namespaces --no-headers -o=custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,REPLICAS:.status.currentReplicas,MIN_REPLICAS:.spec.minReplicas,MAX_REPLICAS:.spec.maxReplicas' | awk '
60+
BEGIN {
61+
printf "%-20s %-30s %-10s %-12s %-12s\n", "NAMESPACE", "NAME", "CURRENT", "MIN_LIVE", "MAX_LIVE"
62+
print "-------------------- ------------------------------ ---------- ------------ ------------"
63+
}
64+
{ printf "%-20s %-30s %-10s %-12s %-12s\n", $1, $2, $3, $4, $5 }
65+
'
66+
echo "----------------------------------------------------------------------------------------"
67+
}
68+
69+
70+
# --- LOCK MODE: Save and Set MaxReplicas = MinReplicas ---
71+
function lock_hpas {
72+
local HPA_CONFIG_FILE="$1"
73+
74+
if $DRY_RUN; then
75+
display_current_hpa_state "$HPA_CONFIG_FILE"
76+
fi
77+
78+
if [ -f "$HPA_CONFIG_FILE" ] && ! $DRY_RUN; then
79+
safe_exit "The lock file ($HPA_CONFIG_FILE) already exists. Run 'unlock' first, or delete the file manually if the operation was interrupted."
80+
fi
81+
82+
echo "--- HPA LOCK MODE: Disabling Scaling ---"
83+
echo "Using config file: $HPA_CONFIG_FILE"
84+
85+
echo "1. Retrieving current HPA configurations..."
86+
HPA_DATA=$(kubectl get hpa --all-namespaces -o json || safe_exit "Failed to retrieve HPA configs. Check kubectl access.")
87+
88+
HPA_ORIGINALS=$(echo "$HPA_DATA" | jq '[
89+
.items[] | select(.spec.minReplicas and .spec.maxReplicas) | {
90+
namespace: .metadata.namespace,
91+
name: .metadata.name,
92+
original_min: .spec.minReplicas,
93+
original_max: .spec.maxReplicas
94+
}
95+
]')
96+
97+
if [ "$(echo "$HPA_ORIGINALS" | jq '. | length')" -eq 0 ]; then
98+
echo "No HPA resources found to process. Exiting."
99+
exit 0
100+
fi
101+
102+
if ! $DRY_RUN; then
103+
echo "$HPA_ORIGINALS" > "$HPA_CONFIG_FILE"
104+
echo "2. Successfully saved original settings to: $HPA_CONFIG_FILE"
105+
else
106+
echo "2. [DRY-RUN] Would have saved original settings to: $HPA_CONFIG_FILE"
107+
fi
108+
109+
echo "3. Patching HPAs to lock scaling (maxReplicas = minReplicas)..."
110+
111+
echo "$HPA_ORIGINALS" | jq -c '.[]' | while read -r HPA_ITEM; do
112+
NAMESPACE=$(echo "$HPA_ITEM" | jq -r '.namespace')
113+
NAME=$(echo "$HPA_ITEM" | jq -r '.name')
114+
MIN_REPLICAS=$(echo "$HPA_ITEM" | jq -r '.original_min')
115+
ORIGINAL_MAX=$(echo "$HPA_ITEM" | jq -r '.original_max')
116+
117+
ACTION_DESC="Setting maxReplicas from $ORIGINAL_MAX to $MIN_REPLICAS"
118+
119+
PATCH_CONTENT=$(cat <<EOF
120+
[
121+
{ "op": "replace", "path": "/spec/maxReplicas", "value": $MIN_REPLICAS }
122+
]
123+
EOF
124+
)
125+
echo "$PATCH_CONTENT" > "$PATCH_FILE"
126+
127+
if ! execute_patch "$NAME" "$NAMESPACE" "json" "$PATCH_FILE" "$ACTION_DESC"; then
128+
safe_exit "Failed to patch HPA $NAME in namespace $NAMESPACE."
129+
fi
130+
rm -f "$PATCH_FILE"
131+
done
132+
133+
echo "---"
134+
if $DRY_RUN; then
135+
echo "HPA Lock Dry Run Complete. NO changes were applied to the cluster."
136+
else
137+
echo "HPA Lock Complete. The cluster is ready for maintenance."
138+
fi
139+
}
140+
141+
# --- UNLOCK MODE: Restore Original MaxReplicas ---
142+
function unlock_hpas {
143+
local HPA_CONFIG_FILE="$1"
144+
145+
if $DRY_RUN; then
146+
display_current_hpa_state "$HPA_CONFIG_FILE"
147+
fi
148+
149+
echo "--- HPA UNLOCK MODE: Restoring Scaling ---"
150+
echo "Using config file: $HPA_CONFIG_FILE"
151+
152+
if [ ! -f "$HPA_CONFIG_FILE" ]; then
153+
safe_exit "Original config file $HPA_CONFIG_FILE not found. Cannot proceed with unlock. Did you run 'lock' first?"
154+
fi
155+
156+
echo "1. Reading original settings from $HPA_CONFIG_FILE"
157+
HPA_ORIGINALS=$(cat "$HPA_CONFIG_FILE")
158+
159+
echo "2. Patching HPAs to restore original maxReplicas..."
160+
161+
echo "$HPA_ORIGINALS" | jq -c '.[]' | while read -r HPA_ITEM; do
162+
NAMESPACE=$(echo "$HPA_ITEM" | jq -r '.namespace')
163+
NAME=$(echo "$HPA_ITEM" | jq -r '.name')
164+
ORIGINAL_MAX=$(echo "$HPA_ITEM" | jq -r '.original_max')
165+
ORIGINAL_MIN=$(echo "$HPA_ITEM" | jq -r '.original_min')
166+
167+
ACTION_DESC="Setting maxReplicas from $ORIGINAL_MIN to $ORIGINAL_MAX (Restoring HPA)"
168+
169+
PATCH_CONTENT=$(cat <<EOF
170+
[
171+
{ "op": "replace", "path": "/spec/maxReplicas", "value": $ORIGINAL_MAX }
172+
]
173+
EOF
174+
)
175+
echo "$PATCH_CONTENT" > "$PATCH_FILE"
176+
177+
if ! execute_patch "$NAME" "$NAMESPACE" "json" "$PATCH_FILE" "$ACTION_DESC"; then
178+
safe_exit "Failed to restore HPA $NAME in namespace $NAMESPACE."
179+
fi
180+
rm -f "$PATCH_FILE"
181+
done
182+
183+
if ! $DRY_RUN; then
184+
rm -f "$HPA_CONFIG_FILE"
185+
echo "3. Successfully removed the original config file: $HPA_CONFIG_FILE"
186+
else
187+
echo "3. [DRY-RUN] Would have removed the original config file: $HPA_CONFIG_FILE"
188+
fi
189+
190+
echo "---"
191+
if $DRY_RUN; then
192+
echo "HPA Unlock Dry Run Complete. NO changes were applied to the cluster."
193+
else
194+
echo "HPA Unlock Complete. Scaling is re-enabled."
195+
fi
196+
}
197+
198+
# --- Script Execution and Parameter Handling ---
199+
200+
ARGS=()
201+
for arg in "$@"; do
202+
if [ "$arg" == "--dry-run" ]; then
203+
DRY_RUN=true
204+
echo "========================================="
205+
echo "!!! DRY RUN MODE ENABLED - NO CHANGES WILL BE APPLIED !!!"
206+
echo "========================================="
207+
else
208+
ARGS+=("$arg")
209+
fi
210+
done
211+
212+
if [ "${#ARGS[@]}" -lt 1 ] || [ "${#ARGS[@]}" -gt 2 ]; then
213+
echo "Usage: $0 {lock|unlock} [OPTIONAL_CONFIG_FILE] [--dry-run]"
214+
echo " {lock|unlock}: The operation mode."
215+
echo " [OPTIONAL_CONFIG_FILE]: The file to store/read HPA settings (defaults to $DEFAULT_HPA_CONFIG_FILE)."
216+
echo " [--dry-run]: Optional flag to simulate changes without execution."
217+
exit 1
218+
fi
219+
220+
MODE="${ARGS[0]}"
221+
222+
if [ "${#ARGS[@]}" -eq 2 ]; then
223+
HPA_CONFIG_FILE="${ARGS[1]}"
224+
else
225+
HPA_CONFIG_FILE="$DEFAULT_HPA_CONFIG_FILE"
226+
fi
227+
228+
case "$MODE" in
229+
lock)
230+
lock_hpas "$HPA_CONFIG_FILE"
231+
;;
232+
unlock)
233+
unlock_hpas "$HPA_CONFIG_FILE"
234+
;;
235+
*)
236+
echo "Invalid argument: $MODE"
237+
echo "Usage: $0 {lock|unlock} [OPTIONAL_CONFIG_FILE] [--dry-run]"
238+
exit 1
239+
;;
240+
esac

0 commit comments

Comments
 (0)