From e75bfdf7066df233091c3b94c35771e79b7a3cae Mon Sep 17 00:00:00 2001 From: taylanbakircioglu Date: Mon, 26 Jan 2026 15:25:15 +0300 Subject: [PATCH] feat: dynamic HAProxy binary path from cluster configuration - /api/agents/{name}/config now returns haproxy_bin_path, haproxy_config_path, stats_socket_path from cluster - Agent daemon uses these dynamic paths from API response for validation - Fallback to local config file if API values not present - Allows cluster admin to change paths without reinstalling agents This fixes validation when HAProxy binary is in non-standard location --- backend/routers/agent.py | 11 +++++-- backend/utils/agent_scripts/linux_install.sh | 31 +++++++++++++++++--- backend/utils/agent_scripts/macos_install.sh | 31 +++++++++++++++++--- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/backend/routers/agent.py b/backend/routers/agent.py index 1844db7..8d6084b 100644 --- a/backend/routers/agent.py +++ b/backend/routers/agent.py @@ -1720,9 +1720,12 @@ async def get_agent_config(agent_name: str, x_api_key: Optional[str] = Header(No conn = await get_database_connection() # Get agent info first to check pool + # CRITICAL: Include cluster's haproxy_bin_path, haproxy_config_path, stats_socket_path + # These are needed for dynamic validation - cluster admin can change paths without reinstalling agent agent_info = await conn.fetchrow(""" SELECT a.id, a.name, a.pool_id, hc.id as cluster_id, hc.name as cluster_name, - COALESCE(a.enabled, TRUE) as enabled + COALESCE(a.enabled, TRUE) as enabled, + hc.haproxy_bin_path, hc.haproxy_config_path, hc.stats_socket_path FROM agents a LEFT JOIN haproxy_clusters hc ON hc.pool_id = a.pool_id WHERE a.name = $1 @@ -1817,7 +1820,11 @@ async def get_agent_config(agent_name: str, x_api_key: Optional[str] = Header(No "config_content": config_version['config_content'], "version": config_version['version_name'], "checksum": config_version['checksum'], - "status": "available" + "status": "available", + # Dynamic paths from cluster configuration - agent should use these for validation + "haproxy_bin_path": agent_info.get('haproxy_bin_path', '/usr/sbin/haproxy'), + "haproxy_config_path": agent_info.get('haproxy_config_path', '/etc/haproxy/haproxy.cfg'), + "stats_socket_path": agent_info.get('stats_socket_path', '/var/run/haproxy/admin.sock') } except HTTPException: diff --git a/backend/utils/agent_scripts/linux_install.sh b/backend/utils/agent_scripts/linux_install.sh index 45c3df9..00d3c4d 100644 --- a/backend/utils/agent_scripts/linux_install.sh +++ b/backend/utils/agent_scripts/linux_install.sh @@ -2855,10 +2855,33 @@ CONFIG_RESPONSE_EOF echo "$daemon_config_content" > /tmp/haproxy-new-config.cfg 2>/dev/null if [[ -f "/tmp/haproxy-new-config.cfg" && -s "/tmp/haproxy-new-config.cfg" ]]; then - # Get paths from config - HAPROXY_BIN=$(jq -r '.haproxy.bin_path' "$CONFIG_FILE" 2>/dev/null || echo "{{HAPROXY_BIN_PATH}}") - HAPROXY_CONFIG=$(jq -r '.haproxy.config_path' "$CONFIG_FILE" 2>/dev/null || echo "{{HAPROXY_CONFIG_PATH}}") - STATS_SOCKET=$(jq -r '.haproxy.stats_socket_path' "$CONFIG_FILE" 2>/dev/null || echo "{{STATS_SOCKET_PATH}}") + # Get paths DYNAMICALLY from API response first (cluster config can be changed without reinstall) + # Fallback to local config file if not present in API response + api_bin_path=$(echo "$config_response" | jq -r '.haproxy_bin_path // empty' 2>/dev/null) + api_config_path=$(echo "$config_response" | jq -r '.haproxy_config_path // empty' 2>/dev/null) + api_socket_path=$(echo "$config_response" | jq -r '.stats_socket_path // empty' 2>/dev/null) + + # Use API values if present, otherwise fallback to local config + if [[ -n "$api_bin_path" && "$api_bin_path" != "null" ]]; then + HAPROXY_BIN="$api_bin_path" + log "DEBUG" "DAEMON: Using dynamic haproxy_bin_path from cluster: $HAPROXY_BIN" + else + HAPROXY_BIN=$(jq -r '.haproxy.bin_path' "$CONFIG_FILE" 2>/dev/null || echo "{{HAPROXY_BIN_PATH}}") + fi + + if [[ -n "$api_config_path" && "$api_config_path" != "null" ]]; then + HAPROXY_CONFIG="$api_config_path" + log "DEBUG" "DAEMON: Using dynamic haproxy_config_path from cluster: $HAPROXY_CONFIG" + else + HAPROXY_CONFIG=$(jq -r '.haproxy.config_path' "$CONFIG_FILE" 2>/dev/null || echo "{{HAPROXY_CONFIG_PATH}}") + fi + + if [[ -n "$api_socket_path" && "$api_socket_path" != "null" ]]; then + STATS_SOCKET="$api_socket_path" + log "DEBUG" "DAEMON: Using dynamic stats_socket_path from cluster: $STATS_SOCKET" + else + STATS_SOCKET=$(jq -r '.haproxy.stats_socket_path' "$CONFIG_FILE" 2>/dev/null || echo "{{STATS_SOCKET_PATH}}") + fi # CRITICAL NEW FEATURE: Check if this is a partial config (only frontends/backends) # Backend now generates partial configs to preserve global/defaults sections diff --git a/backend/utils/agent_scripts/macos_install.sh b/backend/utils/agent_scripts/macos_install.sh index db7d2b6..5587a8e 100644 --- a/backend/utils/agent_scripts/macos_install.sh +++ b/backend/utils/agent_scripts/macos_install.sh @@ -2803,10 +2803,33 @@ CONFIG_RESPONSE_EOF echo "$daemon_config_content" > /tmp/haproxy-new-config.cfg 2>/dev/null if [[ -f "/tmp/haproxy-new-config.cfg" && -s "/tmp/haproxy-new-config.cfg" ]]; then - # Get paths from config - HAPROXY_BIN=$(jq -r '.haproxy.bin_path' "$CONFIG_FILE" 2>/dev/null || echo "{{HAPROXY_BIN_PATH}}") - HAPROXY_CONFIG=$(jq -r '.haproxy.config_path' "$CONFIG_FILE" 2>/dev/null || echo "{{HAPROXY_CONFIG_PATH}}") - STATS_SOCKET=$(jq -r '.haproxy.stats_socket_path' "$CONFIG_FILE" 2>/dev/null || echo "{{STATS_SOCKET_PATH}}") + # Get paths DYNAMICALLY from API response first (cluster config can be changed without reinstall) + # Fallback to local config file if not present in API response + api_bin_path=$(echo "$config_response" | jq -r '.haproxy_bin_path // empty' 2>/dev/null) + api_config_path=$(echo "$config_response" | jq -r '.haproxy_config_path // empty' 2>/dev/null) + api_socket_path=$(echo "$config_response" | jq -r '.stats_socket_path // empty' 2>/dev/null) + + # Use API values if present, otherwise fallback to local config + if [[ -n "$api_bin_path" && "$api_bin_path" != "null" ]]; then + HAPROXY_BIN="$api_bin_path" + log "DEBUG" "DAEMON: Using dynamic haproxy_bin_path from cluster: $HAPROXY_BIN" + else + HAPROXY_BIN=$(jq -r '.haproxy.bin_path' "$CONFIG_FILE" 2>/dev/null || echo "{{HAPROXY_BIN_PATH}}") + fi + + if [[ -n "$api_config_path" && "$api_config_path" != "null" ]]; then + HAPROXY_CONFIG="$api_config_path" + log "DEBUG" "DAEMON: Using dynamic haproxy_config_path from cluster: $HAPROXY_CONFIG" + else + HAPROXY_CONFIG=$(jq -r '.haproxy.config_path' "$CONFIG_FILE" 2>/dev/null || echo "{{HAPROXY_CONFIG_PATH}}") + fi + + if [[ -n "$api_socket_path" && "$api_socket_path" != "null" ]]; then + STATS_SOCKET="$api_socket_path" + log "DEBUG" "DAEMON: Using dynamic stats_socket_path from cluster: $STATS_SOCKET" + else + STATS_SOCKET=$(jq -r '.haproxy.stats_socket_path' "$CONFIG_FILE" 2>/dev/null || echo "{{STATS_SOCKET_PATH}}") + fi # CRITICAL NEW FEATURE: Check if this is a partial config (only frontends/backends) # Backend now generates partial configs to preserve global/defaults sections