1
0
Fork 0

Add support for CORS in HAProxy stats dashboard

- Introduced `HAPROXY_STATS_CORS_ORIGIN` environment variable for enabling CORS in HAProxy stats.
- Updated HAProxy configuration template to handle CORS preflight and response headers.
- Added tests for static configuration mode with CORS enabled.
- Updated documentation to include new CORS configuration details and examples.
This commit is contained in:
Joao Gilberto Magalhaes 2026-02-16 20:41:49 -05:00
parent 353fd392f6
commit 491f4a8c09
8 changed files with 203 additions and 23 deletions

View file

@ -39,6 +39,7 @@ class ContainerEnv:
"username": os.getenv("HAPROXY_USERNAME") if os.getenv("HAPROXY_USERNAME") else "admin",
"password": os.getenv("HAPROXY_PASSWORD"),
"port": os.getenv("HAPROXY_STATS_PORT") if os.getenv("HAPROXY_STATS_PORT") else "1936",
"cors_origin": os.getenv("HAPROXY_STATS_CORS_ORIGIN", ""),
}
env_vars["lookup_label"] = os.getenv("EASYHAPROXY_LABEL_PREFIX") if os.getenv(
@ -149,6 +150,8 @@ class ContainerEnv:
os.environ['HAPROXY_PASSWORD'] = str(stats['password'])
if 'port' in stats:
os.environ['HAPROXY_STATS_PORT'] = str(stats['port'])
if 'cors_origin' in stats:
os.environ['HAPROXY_STATS_CORS_ORIGIN'] = str(stats['cors_origin'])
# Convert logLevel
if 'logLevel' in yaml_config:
@ -333,10 +336,12 @@ class DaemonizeHAProxy:
self.custom_config_folder = custom_config_folder if custom_config_folder is not None else Consts.custom_config_folder
def haproxy(self, action):
self.__prepare(self.get_haproxy_command(action))
error = self.__prepare(self.get_haproxy_command(action), action)
if self.process is None:
return
if error or self.process is None:
logger_haproxy.fatal(f"Failed to start HAProxy ({action}). Exiting.")
import sys
sys.exit(1)
self.thread = Process(target=self.__start, args=())
self.thread.start()
@ -360,10 +365,42 @@ class DaemonizeHAProxy:
)
return self.get_haproxy_command(DaemonizeHAProxy.HAPROXY_START, pid_file)
def __prepare(self, command):
def __validate_config(self):
"""Validate HAProxy configuration before starting."""
validation_cmd = ["haproxy", "-c", "-f", Consts.haproxy_config]
# Add custom config files if they exist
for config_file in self.get_custom_config_files().keys():
validation_cmd.extend(["-f", config_file])
try:
result = subprocess.run(
validation_cmd,
capture_output=True,
text=True,
timeout=10
)
if result.returncode != 0:
return result.stderr if result.stderr else result.stdout
return None
except subprocess.TimeoutExpired:
return "HAProxy configuration validation timed out"
except Exception as e:
return f"Error validating configuration: {e}"
def __prepare(self, command, action=None):
if not isinstance(command, (list, tuple)):
command = shlex.split(command)
# Validate HAProxy config before starting (but not on reload - HAProxy validates itself during reload)
if action == DaemonizeHAProxy.HAPROXY_START:
validation_error = self.__validate_config()
if validation_error:
logger_haproxy.fatal(f"HAProxy configuration validation failed:\n{validation_error}")
return validation_error
try:
logger_haproxy.debug(f"HAPROXY command: {command}")
self.process = subprocess.Popen(command,
@ -372,9 +409,12 @@ class DaemonizeHAProxy:
stderr=subprocess.PIPE,
bufsize=-1,
universal_newlines=True)
return None
except Exception as e:
logger_haproxy.error(f"{e}")
error_msg = f"Failed to start HAProxy process: {e}"
logger_haproxy.error(error_msg)
return error_msg
def __start(self):
try:

View file

@ -62,6 +62,23 @@ frontend stats
bind *:{{ data_stats["port"] | default(1936) }}
mode http
http-request use-service prometheus-exporter if { path /metrics }
{%- if data_stats["cors_origin"] | default("") != "" %}
# CORS for stats dashboard (only for configured origin)
acl from_ui hdr(Origin) -i {{ data_stats["cors_origin"] }}
acl preflight method OPTIONS
# Preflight response
http-request return status 204 hdr "Access-Control-Allow-Origin" "%[req.hdr(Origin)]" hdr "Access-Control-Allow-Methods" "GET, OPTIONS" hdr "Access-Control-Allow-Headers" "Authorization, Content-Type" hdr "Access-Control-Max-Age" "86400" hdr "Vary" "Origin" if from_ui preflight
# Actual response headers
http-after-response set-header Access-Control-Allow-Origin "{{ data_stats["cors_origin"] }}"
http-after-response set-header Access-Control-Allow-Methods "GET, OPTIONS"
http-after-response set-header Access-Control-Allow-Headers "Authorization, Content-Type"
http-after-response set-header Access-Control-Expose-Headers "X-Request-ID"
http-after-response set-header Vary "Origin"
{% endif %}
stats enable
stats hide-version
stats realm Haproxy\ Statistics