From 0fc18fde388e665eb0b16c92a86807a52f0797ed Mon Sep 17 00:00:00 2001 From: taylanbakircioglu Date: Wed, 12 Nov 2025 15:27:05 +0300 Subject: [PATCH] feat: Add HAProxy options support for backends and frontends Implemented comprehensive HAProxy options field support for both backend and frontend entities to enable standard HAProxy directives like 'option http-keep-alive', 'option httplog', 'option forwardfor', etc. Changes: - Database: Added 'options' TEXT column to backends and frontends tables - Models: Added options field to BackendConfig, BackendConfigUpdate, and FrontendConfig - API Endpoints: Updated CREATE, UPDATE, and GET endpoints to handle options field * Backend: CREATE/UPDATE/GET with options support * Frontend: CREATE/UPDATE/GET with options support (fixed 5 SELECT queries) - Config Generator: Added options block generation for both backends and frontends - Bulk Import Parser: * Added options field to ParsedBackend and ParsedFrontend dataclasses * Implemented option directive parsing with validation * Added unknown option warnings * Fixed bulk parse response to include options field - Bulk Import Merge: Added options field comparison in UPDATE logic - UI Components: * BackendServers.js: Added options TextArea form field * FrontendManagement.js: Added options TextArea form field Features: - Multi-line options support (newline-separated format) - Option validation with known HAProxy options list - Backward compatible (NULL options for existing entities) - Bulk import support with merge strategy - Full CRUD support for both manual and bulk operations Technical Details: - Format: Newline-separated TEXT field for multiple options - Validation: Warns about unknown options but allows them - Config Generation: Each option written as separate directive - Agent: Standard HAProxy config validation applies Total: 10 files modified, ~195 lines added, 26 integration points verified --- backend/database/migrations.py | 70 +++++++++++++++++++ backend/models/backend.py | 2 + backend/models/frontend.py | 1 + backend/routers/backend.py | 16 ++--- backend/routers/config.py | 24 +++++-- backend/routers/frontend.py | 28 ++++---- backend/services/haproxy_config.py | 16 +++++ backend/utils/haproxy_config_parser.py | 67 +++++++++++++----- frontend/src/components/BackendServers.js | 15 ++++ frontend/src/components/FrontendManagement.js | 20 ++++++ 10 files changed, 217 insertions(+), 42 deletions(-) diff --git a/backend/database/migrations.py b/backend/database/migrations.py index c0c61b1..98c7753 100644 --- a/backend/database/migrations.py +++ b/backend/database/migrations.py @@ -1555,6 +1555,8 @@ async def run_all_migrations(): await add_cluster_id_to_agent_config_requests() await fix_users_unique_constraints_for_soft_delete() await add_ssl_certificate_id_to_backend_servers() + await add_options_to_backends() + await add_options_to_frontends() logger.info("Database migrations completed successfully.") @@ -2754,3 +2756,71 @@ async def fix_users_unique_constraints_for_soft_delete(): finally: if conn: await close_database_connection(conn) + +async def add_options_to_backends(): + """Add options column to backends table for HAProxy backend options (option http-keep-alive, etc.)""" + conn = None + try: + conn = await get_database_connection() + + # Check if options column already exists + column_exists = await conn.fetchval(""" + SELECT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'backends' + AND column_name = 'options' + ) + """) + + if not column_exists: + # Add options column as TEXT (multi-line HAProxy options) + await conn.execute(""" + ALTER TABLE backends + ADD COLUMN options TEXT + """) + + logger.info("✅ Added options column to backends table for HAProxy backend options") + else: + logger.info("ℹ️ options column already exists in backends table") + + await close_database_connection(conn) + + except Exception as e: + if conn: + await close_database_connection(conn) + logger.error(f"❌ Error adding options to backends: {e}") + # Don't raise - we'll try to proceed + +async def add_options_to_frontends(): + """Add options column to frontends table for HAProxy frontend options (option httplog, option forwardfor, etc.)""" + conn = None + try: + conn = await get_database_connection() + + # Check if options column already exists + column_exists = await conn.fetchval(""" + SELECT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'frontends' + AND column_name = 'options' + ) + """) + + if not column_exists: + # Add options column as TEXT (multi-line HAProxy options) + await conn.execute(""" + ALTER TABLE frontends + ADD COLUMN options TEXT + """) + + logger.info("✅ Added options column to frontends table for HAProxy frontend options") + else: + logger.info("ℹ️ options column already exists in frontends table") + + await close_database_connection(conn) + + except Exception as e: + if conn: + await close_database_connection(conn) + logger.error(f"❌ Error adding options to frontends: {e}") + # Don't raise - we'll try to proceed diff --git a/backend/models/backend.py b/backend/models/backend.py index 015aa32..b405941 100644 --- a/backend/models/backend.py +++ b/backend/models/backend.py @@ -38,6 +38,7 @@ class BackendConfig(BaseModel): timeout_connect: Optional[int] = 10000 timeout_server: Optional[int] = 60000 timeout_queue: Optional[int] = 60000 + options: Optional[str] = None servers: List[ServerConfig] = [] @validator('health_check_interval', 'timeout_connect', 'timeout_server', 'timeout_queue', 'fullconn') @@ -70,6 +71,7 @@ class BackendConfigUpdate(BaseModel): timeout_connect: Optional[int] = None timeout_server: Optional[int] = None timeout_queue: Optional[int] = None + options: Optional[str] = None servers: Optional[List[ServerConfig]] = None @validator('health_check_interval', 'timeout_connect', 'timeout_server', 'timeout_queue', 'fullconn') diff --git a/backend/models/frontend.py b/backend/models/frontend.py index de006cb..e7e84ba 100644 --- a/backend/models/frontend.py +++ b/backend/models/frontend.py @@ -23,6 +23,7 @@ class FrontendConfig(BaseModel): use_backend_rules: Any = [] # Changed from Optional[str] to Any for list support request_headers: Optional[str] = None response_headers: Optional[str] = None + options: Optional[str] = None tcp_request_rules: Optional[str] = None timeout_client: Optional[int] = None timeout_http_request: Optional[int] = None diff --git a/backend/routers/backend.py b/backend/routers/backend.py index 85f53e9..0b51ceb 100644 --- a/backend/routers/backend.py +++ b/backend/routers/backend.py @@ -184,7 +184,7 @@ async def get_backends(cluster_id: Optional[int] = None, include_inactive: bool SELECT id, name, balance_method, mode, health_check_uri, health_check_interval, health_check_expected_status, fullconn, cookie_name, cookie_options, default_server_inter, default_server_fall, default_server_rise, - request_headers, response_headers, + request_headers, response_headers, options, is_active, created_at, updated_at, cluster_id, last_config_status, timeout_connect, timeout_server, timeout_queue FROM backends WHERE cluster_id = $1 ORDER BY name @@ -194,7 +194,7 @@ async def get_backends(cluster_id: Optional[int] = None, include_inactive: bool SELECT id, name, balance_method, mode, health_check_uri, health_check_interval, health_check_expected_status, fullconn, cookie_name, cookie_options, default_server_inter, default_server_fall, default_server_rise, - request_headers, response_headers, + request_headers, response_headers, options, is_active, created_at, updated_at, cluster_id, last_config_status, timeout_connect, timeout_server, timeout_queue FROM backends WHERE cluster_id = $1 AND is_active = TRUE ORDER BY name @@ -205,7 +205,7 @@ async def get_backends(cluster_id: Optional[int] = None, include_inactive: bool SELECT id, name, balance_method, mode, health_check_uri, health_check_interval, health_check_expected_status, fullconn, cookie_name, cookie_options, default_server_inter, default_server_fall, default_server_rise, - request_headers, response_headers, + request_headers, response_headers, options, is_active, created_at, updated_at, cluster_id, last_config_status, timeout_connect, timeout_server, timeout_queue FROM backends ORDER BY name @@ -215,7 +215,7 @@ async def get_backends(cluster_id: Optional[int] = None, include_inactive: bool SELECT id, name, balance_method, mode, health_check_uri, health_check_interval, health_check_expected_status, fullconn, cookie_name, cookie_options, default_server_inter, default_server_fall, default_server_rise, - request_headers, response_headers, + request_headers, response_headers, options, is_active, created_at, updated_at, cluster_id, last_config_status, timeout_connect, timeout_server, timeout_queue FROM backends WHERE is_active = TRUE ORDER BY name @@ -446,14 +446,14 @@ async def create_backend(backend: BackendConfig, authorization: str = Header(Non INSERT INTO backends (name, balance_method, mode, health_check_uri, health_check_interval, health_check_expected_status, fullconn, cookie_name, cookie_options, default_server_inter, default_server_fall, default_server_rise, - request_headers, response_headers, + request_headers, response_headers, options, timeout_connect, timeout_server, timeout_queue, cluster_id) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) RETURNING id + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) RETURNING id """, backend.name, backend.balance_method, backend.mode, backend.health_check_uri, backend.health_check_interval, backend.health_check_expected_status, backend.fullconn, backend.cookie_name, backend.cookie_options, backend.default_server_inter, backend.default_server_fall, backend.default_server_rise, - backend.request_headers, backend.response_headers, + backend.request_headers, backend.response_headers, backend.options, backend.timeout_connect, backend.timeout_server, backend.timeout_queue, backend.cluster_id) # If cluster_id provided, create new config version for agents @@ -746,7 +746,7 @@ async def update_backend(backend_id: int, backend_update: BackendConfigUpdate, r if field in ['name', 'balance_method', 'mode', 'health_check_uri', 'health_check_interval', 'health_check_expected_status', 'fullconn', 'cookie_name', 'cookie_options', 'default_server_inter', 'default_server_fall', 'default_server_rise', - 'request_headers', 'response_headers', 'timeout_connect', 'timeout_server', 'timeout_queue']: + 'request_headers', 'response_headers', 'options', 'timeout_connect', 'timeout_server', 'timeout_queue']: update_fields.append(f"{field} = ${param_idx}") update_values.append(value) param_idx += 1 diff --git a/backend/routers/config.py b/backend/routers/config.py index 499e294..2aba57a 100644 --- a/backend/routers/config.py +++ b/backend/routers/config.py @@ -804,6 +804,7 @@ async def parse_bulk_config( "maxconn": frontend.maxconn, "request_headers": frontend.request_headers, "response_headers": frontend.response_headers, + "options": frontend.options, "tcp_request_rules": frontend.tcp_request_rules, "acl_rules": frontend.acl_rules, "use_backend_rules": frontend.use_backend_rules @@ -890,6 +891,7 @@ async def parse_bulk_config( "default_server_rise": backend.default_server_rise, "request_headers": backend.request_headers, "response_headers": backend.response_headers, + "options": backend.options, "timeout_connect": backend.timeout_connect, "timeout_server": backend.timeout_server, "timeout_queue": backend.timeout_queue, @@ -1280,6 +1282,12 @@ async def bulk_create_entities( update_values.append(backend_data["response_headers"]) param_index += 1 + # NEW: Options field support (option http-keep-alive, etc.) + if backend_data.get("options") and backend_data["options"] != existing_full.get("options"): + update_fields.append(f"options = ${param_index}") + update_values.append(backend_data["options"]) + param_index += 1 + # NOTE: maxconn field exists in database but is not used in normal backend UPDATE # Preserving consistency with existing backend UPDATE endpoint (backend.py) # maxconn field intentionally excluded from bulk import UPDATE @@ -1331,9 +1339,9 @@ async def bulk_create_entities( health_check_interval, health_check_expected_status, fullconn, cookie_name, cookie_options, default_server_inter, default_server_fall, default_server_rise, request_headers, - response_headers, timeout_connect, timeout_server, timeout_queue, cluster_id + response_headers, options, timeout_connect, timeout_server, timeout_queue, cluster_id ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) RETURNING id """, backend_data["name"], @@ -1350,6 +1358,7 @@ async def bulk_create_entities( backend_data.get("default_server_rise"), backend_data.get("request_headers"), backend_data.get("response_headers"), + backend_data.get("options"), backend_data.get("timeout_connect", 10000), backend_data.get("timeout_server", 60000), backend_data.get("timeout_queue", 60000), @@ -1494,6 +1503,12 @@ async def bulk_create_entities( update_values.append(frontend_data["tcp_request_rules"]) param_index += 1 + # NEW: Options field support (option httplog, option forwardfor, etc.) + if frontend_data.get("options") and frontend_data["options"] != existing_full.get("options"): + update_fields.append(f"options = ${param_index}") + update_values.append(frontend_data["options"]) + param_index += 1 + if frontend_data.get("timeout_http_request") and frontend_data["timeout_http_request"] != existing_full["timeout_http_request"]: update_fields.append(f"timeout_http_request = ${param_index}") update_values.append(frontend_data["timeout_http_request"]) @@ -1561,11 +1576,11 @@ async def bulk_create_entities( ssl_enabled, ssl_certificate_id, ssl_certificate_ids, ssl_port, ssl_cert_path, ssl_cert, ssl_verify, timeout_client, timeout_http_request, maxconn, - request_headers, response_headers, tcp_request_rules, + request_headers, response_headers, tcp_request_rules, options, rate_limit, compression, log_separate, monitor_uri, cluster_id, acl_rules, use_backend_rules, redirect_rules, updated_at ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, CURRENT_TIMESTAMP) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, CURRENT_TIMESTAMP) RETURNING id """, frontend_data["name"], @@ -1586,6 +1601,7 @@ async def bulk_create_entities( frontend_data.get("request_headers"), frontend_data.get("response_headers"), frontend_data.get("tcp_request_rules"), + frontend_data.get("options"), frontend_data.get("rate_limit"), frontend_data.get("compression", False), frontend_data.get("log_separate", False), diff --git a/backend/routers/frontend.py b/backend/routers/frontend.py index f9e70f3..12dcb0a 100644 --- a/backend/routers/frontend.py +++ b/backend/routers/frontend.py @@ -145,7 +145,7 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool SELECT id, name, bind_address, bind_port, default_backend, mode, ssl_enabled, ssl_certificate_id, ssl_certificate_ids, ssl_port, ssl_cert_path, ssl_cert, ssl_verify, acl_rules, redirect_rules, use_backend_rules, - request_headers, response_headers, tcp_request_rules, + request_headers, response_headers, options, tcp_request_rules, timeout_client, timeout_http_request, rate_limit, compression, log_separate, monitor_uri, maxconn, is_active, created_at, updated_at, cluster_id, last_config_status @@ -160,7 +160,7 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool SELECT id, name, bind_address, bind_port, default_backend, mode, ssl_enabled, ssl_cert_path, ssl_cert, ssl_verify, acl_rules, redirect_rules, use_backend_rules, - request_headers, response_headers, tcp_request_rules, + request_headers, response_headers, options, tcp_request_rules, timeout_client, timeout_http_request, rate_limit, compression, log_separate, monitor_uri, maxconn, is_active, created_at, updated_at, cluster_id, last_config_status @@ -188,7 +188,7 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool SELECT id, name, bind_address, bind_port, default_backend, mode, ssl_enabled, ssl_certificate_id, ssl_certificate_ids, ssl_port, ssl_cert_path, ssl_cert, ssl_verify, acl_rules, redirect_rules, use_backend_rules, - request_headers, response_headers, tcp_request_rules, + request_headers, response_headers, options, tcp_request_rules, timeout_client, timeout_http_request, rate_limit, compression, log_separate, monitor_uri, maxconn, is_active, created_at, updated_at, cluster_id, last_config_status @@ -199,7 +199,7 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool SELECT id, name, bind_address, bind_port, default_backend, mode, ssl_enabled, ssl_certificate_id, ssl_certificate_ids, ssl_port, ssl_cert_path, ssl_cert, ssl_verify, acl_rules, redirect_rules, use_backend_rules, - request_headers, response_headers, tcp_request_rules, + request_headers, response_headers, options, tcp_request_rules, timeout_client, timeout_http_request, rate_limit, compression, log_separate, monitor_uri, maxconn, is_active, created_at, updated_at, cluster_id, last_config_status @@ -213,7 +213,7 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool SELECT id, name, bind_address, bind_port, default_backend, mode, ssl_enabled, ssl_cert_path, ssl_cert, ssl_verify, acl_rules, redirect_rules, use_backend_rules, - request_headers, response_headers, tcp_request_rules, + request_headers, response_headers, options, tcp_request_rules, timeout_client, timeout_http_request, rate_limit, compression, log_separate, monitor_uri, maxconn, is_active, created_at, updated_at, cluster_id, last_config_status @@ -224,7 +224,7 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool SELECT id, name, bind_address, bind_port, default_backend, mode, ssl_enabled, ssl_cert_path, ssl_cert, ssl_verify, acl_rules, redirect_rules, use_backend_rules, - request_headers, response_headers, tcp_request_rules, + request_headers, response_headers, options, tcp_request_rules, timeout_client, timeout_http_request, rate_limit, compression, log_separate, monitor_uri, maxconn, is_active, created_at, updated_at, cluster_id, last_config_status @@ -382,16 +382,16 @@ async def create_frontend(frontend: FrontendConfig, request: Request, authorizat name, bind_address, bind_port, default_backend, mode, ssl_enabled, ssl_certificate_id, ssl_certificate_ids, ssl_port, ssl_cert_path, ssl_cert, ssl_verify, acl_rules, redirect_rules, use_backend_rules, - request_headers, response_headers, tcp_request_rules, timeout_client, timeout_http_request, + request_headers, response_headers, options, tcp_request_rules, timeout_client, timeout_http_request, rate_limit, compression, log_separate, monitor_uri, cluster_id, maxconn, updated_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, CURRENT_TIMESTAMP) + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, CURRENT_TIMESTAMP) RETURNING id """, frontend.name, frontend.bind_address, frontend.bind_port, frontend.default_backend, frontend.mode, frontend.ssl_enabled, frontend.ssl_certificate_id, ssl_cert_ids_json, frontend.ssl_port, frontend.ssl_cert_path, frontend.ssl_cert, frontend.ssl_verify, json.dumps(frontend.acl_rules or []), json.dumps(frontend.redirect_rules or []), json.dumps(frontend.use_backend_rules or []), - frontend.request_headers, frontend.response_headers, frontend.tcp_request_rules, frontend.timeout_client, frontend.timeout_http_request, + frontend.request_headers, frontend.response_headers, frontend.options, frontend.tcp_request_rules, frontend.timeout_client, frontend.timeout_http_request, frontend.rate_limit, frontend.compression, frontend.log_separate, frontend.monitor_uri, frontend.cluster_id, frontend.maxconn) @@ -630,15 +630,15 @@ async def update_frontend(frontend_id: int, frontend: FrontendConfig, request: R default_backend = $4, mode = $5, ssl_enabled = $6, ssl_certificate_id = $7, ssl_certificate_ids = $8, ssl_port = $9, ssl_cert_path = $10, ssl_cert = $11, ssl_verify = $12, acl_rules = $13, redirect_rules = $14, use_backend_rules = $15, - request_headers = $16, response_headers = $17, tcp_request_rules = $18, timeout_client = $19, timeout_http_request = $20, - rate_limit = $21, compression = $22, log_separate = $23, monitor_uri = $24, - cluster_id = $25, maxconn = $26, updated_at = CURRENT_TIMESTAMP - WHERE id = $27 + request_headers = $16, response_headers = $17, options = $18, tcp_request_rules = $19, timeout_client = $20, timeout_http_request = $21, + rate_limit = $22, compression = $23, log_separate = $24, monitor_uri = $25, + cluster_id = $26, maxconn = $27, updated_at = CURRENT_TIMESTAMP + WHERE id = $28 """, frontend.name, frontend.bind_address, frontend.bind_port, frontend.default_backend, frontend.mode, ssl_enabled, ssl_certificate_id, ssl_cert_ids_json, ssl_port, ssl_cert_path, ssl_cert, ssl_verify, json.dumps(frontend.acl_rules or []), json.dumps(frontend.redirect_rules or []), json.dumps(frontend.use_backend_rules or []), - frontend.request_headers, frontend.response_headers, frontend.tcp_request_rules, frontend.timeout_client, frontend.timeout_http_request, + frontend.request_headers, frontend.response_headers, frontend.options, frontend.tcp_request_rules, frontend.timeout_client, frontend.timeout_http_request, frontend.rate_limit, frontend.compression, frontend.log_separate, frontend.monitor_uri, frontend.cluster_id, frontend.maxconn, frontend_id) diff --git a/backend/services/haproxy_config.py b/backend/services/haproxy_config.py index 414d663..bf5e053 100644 --- a/backend/services/haproxy_config.py +++ b/backend/services/haproxy_config.py @@ -282,6 +282,14 @@ async def generate_haproxy_config_for_cluster(cluster_id: int, conn: Optional[An if frontend.get('monitor_uri'): config_lines.append(f" monitor-uri {frontend['monitor_uri']}") + # Frontend Options (NEW: option httplog, option forwardfor, etc.) + if frontend.get('options'): + for line in frontend['options'].split('\n'): + if line.strip(): + # Lines are complete HAProxy option directives + # Examples: "option httplog", "option forwardfor", "option dontlognull" + config_lines.append(f" {line.strip()}") + # Request Headers (includes options, http-request directives - already formatted) if frontend.get('request_headers'): for line in frontend['request_headers'].split('\n'): @@ -459,6 +467,14 @@ async def generate_haproxy_config_for_cluster(cluster_id: int, conn: Optional[An default_server_line += f" rise {backend['default_server_rise']}" config_lines.append(default_server_line) + # Backend Options (NEW: option http-keep-alive, option forwardfor, etc.) + if backend.get('options'): + for line in backend['options'].split('\n'): + if line.strip(): + # Lines are complete HAProxy option directives + # Examples: "option http-keep-alive", "option forwardfor", "option httpchk" + config_lines.append(f" {line.strip()}") + # Backend Request Headers (new field - includes options, http-request directives) if backend.get('request_headers'): for line in backend['request_headers'].split('\n'): diff --git a/backend/utils/haproxy_config_parser.py b/backend/utils/haproxy_config_parser.py index bf8cd7f..5ae42b5 100644 --- a/backend/utils/haproxy_config_parser.py +++ b/backend/utils/haproxy_config_parser.py @@ -47,6 +47,7 @@ class ParsedBackend: default_server_rise: Optional[int] = None # Default rise value for all servers request_headers: Optional[str] = None # HTTP request headers (backend level) response_headers: Optional[str] = None # HTTP response headers (backend level) + options: Optional[str] = None # HAProxy backend options (option http-keep-alive, option forwardfor, etc.) timeout_connect: Optional[int] = 10000 timeout_server: Optional[int] = 60000 timeout_queue: Optional[int] = 60000 @@ -83,6 +84,7 @@ class ParsedFrontend: redirect_rules: Optional[list] = None request_headers: Optional[str] = None response_headers: Optional[str] = None + options: Optional[str] = None # HAProxy frontend options (option httplog, option forwardfor, etc.) tcp_request_rules: Optional[str] = None # TCP request directives (for TCP mode) @@ -228,9 +230,10 @@ class HAProxyConfigParser: frontend = ParsedFrontend(name=name) ssl_cert_found = False - # Collect HTTP headers, ACL rules, use_backend rules, and TCP rules + # Collect HTTP headers, ACL rules, use_backend rules, TCP rules, and options request_headers_list = [] response_headers_list = [] + options_list = [] acl_rules_list = [] use_backend_rules_list = [] tcp_request_rules_list = [] @@ -306,6 +309,35 @@ class HAProxyConfigParser: if maxconn_match: frontend.maxconn = int(maxconn_match.group(1)) + # Parse options (httplog, forwardfor, etc.) - NEW for frontend support + if line.startswith('option '): + # Validate option directive - check for known HAProxy options + option_match = re.match(r'^option\s+(\S+)', line, re.IGNORECASE) + if option_match: + option_name = option_match.group(1) + # List of valid HAProxy frontend options + valid_options = [ + 'httplog', 'tcplog', 'dontlognull', 'http-keep-alive', 'http-server-close', + 'forwardfor', 'redispatch', 'http-use-proxy-header', 'httpchk', 'tcp-check', + 'contstats', 'http-pretend-keepalive', 'logasap', 'nolinger', 'persist', + 'prefer-last-server', 'splice-auto', 'splice-request', 'splice-response', + 'transparent', 'abortonclose', 'allbackups', 'checkcache', 'clitcpka', + 'srvtcpka', 'http-no-delay', 'socket-stats', 'tcp-smart-accept', + 'tcp-smart-connect', 'independant-streams', 'log-separate-errors', + 'log-health-checks', 'accept-invalid-http-request', 'accept-invalid-http-response' + ] + + if option_name not in valid_options: + # Unknown/invalid option - add warning but still collect it + self.warnings.append( + f"Frontend '{name}': Unknown or invalid option '{option_name}' detected. " + "Please verify this directive is supported by your HAProxy version." + ) + + # Collect option for frontend.options field + options_list.append(line.strip()) + continue # Skip options - don't add to request_headers + # Parse HTTP request headers if line.startswith('http-request '): # Check for unsupported capture directives @@ -394,13 +426,17 @@ class HAProxyConfigParser: if line.startswith('tcp-request '): tcp_request_rules_list.append(line) - # Combine collected headers and rules + # Combine collected headers, rules, and options if request_headers_list: frontend.request_headers = '\n'.join(request_headers_list) if response_headers_list: frontend.response_headers = '\n'.join(response_headers_list) + # NEW: Assign collected options to frontend + if options_list: + frontend.options = '\n'.join(options_list) + if acl_rules_list: frontend.acl_rules = acl_rules_list @@ -422,9 +458,10 @@ class HAProxyConfigParser: try: backend = ParsedBackend(name=name, servers=[]) - # Collect HTTP headers + # Collect HTTP headers and options request_headers_list = [] response_headers_list = [] + options_list = [] for line in lines: # Parse mode @@ -495,9 +532,7 @@ class HAProxyConfigParser: # Parse options (http-keep-alive, tcp-check, etc.) # Note: option httpchk is handled separately above - # CRITICAL FIX: Do NOT add option directives to request_headers - # Options are separate HAProxy config directives, not HTTP headers - # They are skipped during bulk import as they need special handling + # NEW: Collect options for backend.options field if line.startswith('option ') and 'httpchk' not in line: # Validate option directive - check for known HAProxy options option_match = re.match(r'^option\s+(\S+)', line, re.IGNORECASE) @@ -516,19 +551,15 @@ class HAProxyConfigParser: ] if option_name not in valid_options: - # Unknown/invalid option - add warning and skip + # Unknown/invalid option - add warning but still collect it self.warnings.append( - f"Backend '{name}': Unknown or invalid option '{option_name}' detected and skipped. " + f"Backend '{name}': Unknown or invalid option '{option_name}' detected. " "Please verify this directive is supported by your HAProxy version." ) - else: - # Valid option but skip it - bulk import doesn't support custom options yet - self.warnings.append( - f"Backend '{name}': HAProxy option '{option_name}' detected and skipped. " - f"Custom HAProxy options are not supported in bulk import. " - f"Configure options manually after import if needed." - ) - continue # Always skip options - don't add to request_headers + + # Collect option for backend.options field + options_list.append(line.strip()) + continue # Skip options - don't add to request_headers # Parse HTTP request headers if line.startswith('http-request '): @@ -607,6 +638,10 @@ class HAProxyConfigParser: logger.warning(f"Backend '{name}': Excluded {len(portless_servers)} portless servers (TCP mode requires ports)") + # Assign collected options to backend (NEW) + if options_list: + backend.options = '\n'.join(options_list) + # DNS HOSTNAME DETECTION: Warn if servers use hostnames instead of IPs if backend.servers: hostname_servers = [] diff --git a/frontend/src/components/BackendServers.js b/frontend/src/components/BackendServers.js index 0a49634..f9abb16 100644 --- a/frontend/src/components/BackendServers.js +++ b/frontend/src/components/BackendServers.js @@ -1779,6 +1779,21 @@ const BackendServers = () => { + + + + + + + +