From 1dcf45b1bb07975153e27bee76558e70e70144b1 Mon Sep 17 00:00:00 2001 From: taylanbakircioglu Date: Mon, 26 Jan 2026 15:25:15 +0300 Subject: [PATCH] fix: dynamic collision detection in config generation Query agent preserved_listen_blocks dynamically and skip conflicting entities to prevent HAProxy validation errors. Falls back to static reserved list if no agent data available. --- backend/services/haproxy_config.py | 60 ++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/backend/services/haproxy_config.py b/backend/services/haproxy_config.py index ac06a26..55baef0 100644 --- a/backend/services/haproxy_config.py +++ b/backend/services/haproxy_config.py @@ -168,6 +168,53 @@ async def generate_haproxy_config_for_cluster(cluster_id: int, conn: Optional[An # CRITICAL: Build backend mode lookup for validation backend_modes = {backend['name']: backend['mode'] for backend in backends} + # ENTERPRISE COLLISION PREVENTION: Get DYNAMIC list of conflicting names + # 1. Query ALL agents in this cluster for their preserved_listen_blocks + # 2. Build union of all preserved block names + # 3. Skip entities that would conflict with ANY agent's preserved blocks + # This prevents "proxy has same name" errors during HAProxy validation + + # Static fallback list (used if no agent data available) + static_reserved_names = {'stats', 'haproxy-stats', 'haproxy_stats', 'monitoring', 'admin', 'health', 'status'} + + # Dynamic: Query agents' actual preserved_listen_blocks + agent_preserved_names = set() + try: + agent_blocks = await db_conn.fetch(""" + SELECT a.name as agent_name, a.preserved_listen_blocks + FROM agents a + JOIN haproxy_clusters hc ON hc.pool_id = a.pool_id + WHERE hc.id = $1 AND a.preserved_listen_blocks IS NOT NULL AND a.is_active = TRUE + """, cluster_id) + + for agent in agent_blocks: + listen_blocks = agent['preserved_listen_blocks'] or [] + if isinstance(listen_blocks, str): + try: + listen_blocks = json.loads(listen_blocks) + except (ValueError, Exception): + listen_blocks = [] + + for block_name in listen_blocks: + if isinstance(block_name, str): + agent_preserved_names.add(block_name.lower()) + logger.debug(f"CONFIG GENERATION: Agent '{agent['agent_name']}' preserves listen block '{block_name}'") + + if agent_preserved_names: + logger.info(f"CONFIG GENERATION: Dynamic collision check - {len(agent_preserved_names)} preserved block names from agents: {agent_preserved_names}") + except Exception as e: + logger.warning(f"CONFIG GENERATION: Could not query agent preserved blocks (column may not exist): {e}") + + # Combine: Use dynamic names if available, otherwise use static fallback + # IMPORTANT: Only use static fallback if NO agent data available + # This allows users to create 'stats' frontend if their agents don't preserve it + if agent_preserved_names: + conflicting_names = agent_preserved_names + logger.info(f"CONFIG GENERATION: Using DYNAMIC collision list from agent data") + else: + conflicting_names = static_reserved_names + logger.info(f"CONFIG GENERATION: Using STATIC collision list (no agent preserved_listen_blocks data)") + # Add frontends with comprehensive configuration for frontend in frontends: # Validate required fields @@ -178,6 +225,13 @@ async def generate_haproxy_config_for_cluster(cluster_id: int, conn: Optional[An logger.error(f"Frontend '{frontend['name']}' has invalid bind configuration (address: {frontend.get('bind_address')}, port: {frontend.get('bind_port')}) - skipping") continue + # ENTERPRISE COLLISION CHECK: Skip entities that conflict with agent's preserved listen blocks + # This prevents HAProxy validation errors like "proxy has same name as proxy declared at..." + if frontend['name'].lower() in conflicting_names: + logger.warning(f"CONFIG GENERATION: Skipping frontend '{frontend['name']}' - name conflicts with agent's preserved listen block. " + f"To use this frontend, remove the corresponding listen block from agent's local haproxy.cfg first.") + continue + config_lines.append(f"frontend {frontend['name']}") # BIND DIRECTIVE - Single bind line with conditional SSL @@ -534,6 +588,12 @@ async def generate_haproxy_config_for_cluster(cluster_id: int, conn: Optional[An logger.error("Backend with missing name detected - skipping") continue + # ENTERPRISE COLLISION CHECK: Skip backends that conflict with agent's preserved listen blocks + if backend['name'].lower() in conflicting_names: + logger.warning(f"CONFIG GENERATION: Skipping backend '{backend['name']}' - name conflicts with agent's preserved listen block. " + f"To use this backend, remove the corresponding listen block from agent's local haproxy.cfg first.") + continue + # CRITICAL PRE-CHECK: Get servers BEFORE starting backend config # This allows us to skip backends with no servers before writing anything # CRITICAL FIX: Handle servers with NULL cluster_id (legacy data)