fix(haproxyctl): Prevent config corruption from nested config_foreach

Pre-collect server sections to temp file before iterating backends to avoid
UCI state corruption. Nested config_foreach calls were causing server lines
to appear in wrong sections (userlist, global).

Changes:
- Add _precollect_server() to gather all server data before backend iteration
- Store server data as pipe-delimited records in temp file
- Replace _check_server_sections nested iteration with grep lookup
- Replace _add_server_to_backend nested iteration with grep + while read
- Mark old _add_server_to_backend as DEPRECATED

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-03-27 08:45:43 +01:00
parent 58758e2824
commit 6b7119d790

View File

@ -951,12 +951,56 @@ backend acme_challenge
EOF EOF
# PRE-COLLECT ALL SERVER SECTIONS TO AVOID NESTED config_foreach
# This prevents UCI state corruption during backend iteration
# Bug fix: nested config_foreach calls were interleaving output
_servers_data="/tmp/haproxy_servers.$$"
rm -f "$_servers_data"
_precollect_server() {
local section="$1"
local backend server_name address port weight check enabled
config_get backend "$section" backend
config_get enabled "$section" enabled "0"
[ "$enabled" = "1" ] || return
[ -n "$backend" ] || return
config_get server_name "$section" name "$section"
config_get address "$section" address
config_get port "$section" port "80"
config_get weight "$section" weight "100"
config_get check "$section" check "1"
[ -n "$address" ] || return
# Resolve hostname to IP if needed
if ! echo "$address" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
local resolved_ip=""
resolved_ip=$(nslookup "$address" 2>/dev/null | awk '/^Address: / { print $2; exit }')
if [ -z "$resolved_ip" ]; then
resolved_ip=$(getent hosts "$address" 2>/dev/null | awk '{print $1; exit}')
fi
if [ -z "$resolved_ip" ]; then
log_warn "Cannot resolve hostname '$address' for server $server_name in backend $backend - skipping"
return
fi
address="$resolved_ip"
fi
# Store in temp file: backend|server_name|address|port|weight|check
echo "${backend}|${server_name}|${address}|${port}|${weight}|${check}" >> "$_servers_data"
}
config_foreach _precollect_server server
# Track which backends are generated # Track which backends are generated
_generated_backends="" _generated_backends=""
# Generate each backend from UCI # Generate each backend from UCI (now safe - no nested config_foreach needed)
config_foreach _generate_backend backend config_foreach _generate_backend backend
# Cleanup server data temp file
rm -f "$_servers_data"
# Collect all backends referenced by vhosts # Collect all backends referenced by vhosts
_referenced_backends="" _referenced_backends=""
_collect_vhost_backend() { _collect_vhost_backend() {
@ -1039,17 +1083,12 @@ _generate_backend() {
fi fi
# Check if there are separate server sections for this backend # Check if there are separate server sections for this backend
# NOW USES PRE-COLLECTED DATA instead of nested config_foreach
# This avoids UCI state corruption that caused server lines in wrong sections
local has_server_sections=0 local has_server_sections=0
_check_server_sections() { if [ -f "$_servers_data" ] && grep -q "^${name}|" "$_servers_data"; then
local srv_section="$1" has_server_sections=1
local srv_backend fi
config_get srv_backend "$srv_section" backend
config_get srv_enabled "$srv_section" enabled "0"
if [ "$srv_backend" = "$name" ] && [ "$srv_enabled" = "1" ]; then
has_server_sections=1
fi
}
config_foreach _check_server_sections server
# Add inline server ONLY if no separate server sections exist # Add inline server ONLY if no separate server sections exist
# This prevents duplicate server names # This prevents duplicate server names
@ -1059,11 +1098,20 @@ _generate_backend() {
[ -n "$server_line" ] && echo " server $server_line" [ -n "$server_line" ] && echo " server $server_line"
fi fi
# Add servers from separate server UCI sections # Add servers from pre-collected data (avoiding nested config_foreach)
config_foreach _add_server_to_backend server "$name" if [ -f "$_servers_data" ]; then
grep "^${name}|" "$_servers_data" 2>/dev/null | while IFS='|' read -r _backend srv_name srv_addr srv_port srv_weight srv_check; do
local check_opt=""
[ "$srv_check" = "1" ] && check_opt="check"
echo " server $srv_name $srv_addr:$srv_port weight $srv_weight $check_opt"
done
fi
} }
_add_server_to_backend() { # DEPRECATED: This function is no longer used - server data is now pre-collected
# in _generate_backends() to avoid nested config_foreach bugs.
# Kept for reference only.
_add_server_to_backend_DEPRECATED() {
local section="$1" local section="$1"
local target_backend="$2" local target_backend="$2"
local backend server_name address port weight check enabled local backend server_name address port weight check enabled