Migrate configuration paths to /etc/easyhaproxy and improve health check support in E2E tests
- Refactored HAProxy configuration files, templates, and paths to use `/etc/easyhaproxy` instead of `/etc/haproxy`. - Updated Dockerfile to generate DH params and placeholder certificates in the new configuration directory. - Added health check support with timeout to `DockerComposeFixture` in E2E test utilities. - Adjusted tests, templates, and plugins to use the new `Consts`-based configuration paths. - Introduced pytest fixtures for environment isolation and temporary directory management.
This commit is contained in:
parent
3e963228f3
commit
045dd3817e
73 changed files with 600 additions and 287 deletions
|
|
@ -271,12 +271,55 @@ class Functions:
|
|||
return [-99, e]
|
||||
|
||||
|
||||
class classproperty:
|
||||
"""Decorator for class-level properties."""
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
||||
def __get__(self, obj, owner):
|
||||
return self.func(owner)
|
||||
|
||||
|
||||
class Consts:
|
||||
easyhaproxy_config = "/etc/haproxy/static/config.yml"
|
||||
haproxy_config = "/etc/haproxy/haproxy.cfg"
|
||||
custom_config_folder = "/etc/haproxy/conf.d"
|
||||
certs_certbot = "/certs/certbot"
|
||||
certs_haproxy = "/certs/haproxy"
|
||||
"""Configuration constants with dynamic path resolution based on EASYHAPROXY_BASE_PATH."""
|
||||
_base_path = None
|
||||
|
||||
@classproperty
|
||||
def base_path(cls):
|
||||
"""Base directory for all EasyHAProxy files."""
|
||||
if cls._base_path is None:
|
||||
cls._base_path = os.getenv("EASYHAPROXY_BASE_PATH", "/etc/easyhaproxy")
|
||||
return cls._base_path
|
||||
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
"""Reset cached base path to pick up environment variable changes."""
|
||||
cls._base_path = None
|
||||
|
||||
@classproperty
|
||||
def easyhaproxy_config(cls):
|
||||
"""Path to static configuration file."""
|
||||
return f"{cls.base_path}/static/config.yml"
|
||||
|
||||
@classproperty
|
||||
def haproxy_config(cls):
|
||||
"""Path to generated HAProxy configuration file."""
|
||||
return f"{cls.base_path}/haproxy/haproxy.cfg"
|
||||
|
||||
@classproperty
|
||||
def custom_config_folder(cls):
|
||||
"""Path to custom HAProxy config snippets directory."""
|
||||
return f"{cls.base_path}/haproxy/conf.d"
|
||||
|
||||
@classproperty
|
||||
def certs_certbot(cls):
|
||||
"""Path to Certbot/ACME certificates directory."""
|
||||
return f"{cls.base_path}/certs/certbot"
|
||||
|
||||
@classproperty
|
||||
def certs_haproxy(cls):
|
||||
"""Path to user-provided certificates directory."""
|
||||
return f"{cls.base_path}/certs/haproxy"
|
||||
|
||||
|
||||
class DaemonizeHAProxy:
|
||||
|
|
@ -304,12 +347,12 @@ class DaemonizeHAProxy:
|
|||
custom_config_files = f"-f {self.custom_config_folder}"
|
||||
|
||||
if action == DaemonizeHAProxy.HAPROXY_START or not os.path.exists(pid_file):
|
||||
return f"/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg {custom_config_files} -p {pid_file} -S /var/run/haproxy.sock"
|
||||
return f"/usr/sbin/haproxy -W -f {Consts.haproxy_config} {custom_config_files} -p {pid_file} -S /var/run/haproxy.sock"
|
||||
else:
|
||||
return_code, output = Functions().run_bash(logger_haproxy, f"cat {pid_file}", log_output=False)
|
||||
pid = "".join(output).rstrip()
|
||||
if psutil.pid_exists(int(pid)):
|
||||
return f"/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg {custom_config_files} -p {pid_file} -x /var/run/haproxy.sock -sf {pid}"
|
||||
return f"/usr/sbin/haproxy -W -f {Consts.haproxy_config} {custom_config_files} -p {pid_file} -x /var/run/haproxy.sock -sf {pid}"
|
||||
else:
|
||||
os.unlink(pid_file)
|
||||
logger_haproxy.warning(
|
||||
|
|
@ -440,6 +483,9 @@ class Certbot:
|
|||
renew_certs.append(host_arg)
|
||||
|
||||
certbot_certonly = ('/usr/bin/certbot certonly {acme_server}'
|
||||
' --config-dir {base_path}/certs'
|
||||
' --work-dir {base_path}/certs/work'
|
||||
' --logs-dir {base_path}/certs/logs'
|
||||
' --preferred-challenges {challenge}'
|
||||
' --agree-tos'
|
||||
' --issuance-timeout 90'
|
||||
|
|
@ -452,7 +498,8 @@ class Certbot:
|
|||
certs=' '.join(request_certs),
|
||||
email=self.email,
|
||||
challenge=self.certbot_preferred_challenges,
|
||||
acme_server=self.acme_server)
|
||||
acme_server=self.acme_server,
|
||||
base_path=Consts.base_path)
|
||||
)
|
||||
|
||||
if 'http' in self.certbot_preferred_challenges:
|
||||
|
|
@ -476,7 +523,8 @@ class Certbot:
|
|||
ret_reload = True
|
||||
|
||||
if len(renew_certs) > 0:
|
||||
return_code_renew, output = Functions.run_bash(logger_certbot, "/usr/bin/certbot renew", return_result=False)
|
||||
certbot_renew = f"/usr/bin/certbot renew --config-dir {Consts.base_path}/certs --work-dir {Consts.base_path}/certs/work --logs-dir {Consts.base_path}/certs/logs"
|
||||
return_code_renew, output = Functions.run_bash(logger_certbot, certbot_renew, return_result=False)
|
||||
ret_reload = True
|
||||
|
||||
if ret_reload:
|
||||
|
|
@ -497,7 +545,7 @@ class Certbot:
|
|||
Functions.save(filename, cert + key)
|
||||
|
||||
def find_live_certificates(self):
|
||||
certbot_certs = "/etc/letsencrypt/live/"
|
||||
certbot_certs = f"{Consts.base_path}/certs/live/"
|
||||
if not os.path.exists(certbot_certs):
|
||||
return
|
||||
for item in os.listdir(certbot_certs):
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from dataclasses import dataclass, field
|
|||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from functions import logger_easyhaproxy
|
||||
from functions import logger_easyhaproxy, Consts
|
||||
|
||||
|
||||
class PluginType(Enum):
|
||||
|
|
@ -108,12 +108,12 @@ class PluginManager:
|
|||
Initialize the plugin manager
|
||||
|
||||
Args:
|
||||
plugins_dir: Directory containing plugin files (defaults to EASYHAPROXY_PLUGINS_DIR env var or /etc/haproxy/plugins)
|
||||
plugins_dir: Directory containing plugin files (defaults to EASYHAPROXY_PLUGINS_DIR env var or /etc/easyhaproxy/plugins)
|
||||
abort_on_error: If True, abort on plugin errors; if False, log and continue
|
||||
"""
|
||||
self.plugins_dir = plugins_dir or os.getenv(
|
||||
"EASYHAPROXY_PLUGINS_DIR",
|
||||
"/etc/haproxy/plugins"
|
||||
Consts.base_path + "/plugins"
|
||||
)
|
||||
self.abort_on_error = abort_on_error
|
||||
self.plugins: dict[str, PluginInterface] = {}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ The plugin includes built-in Cloudflare IP ranges that are automatically
|
|||
updated and written to the IP list file.
|
||||
|
||||
Configuration:
|
||||
- ip_list_path: Path to file containing Cloudflare IP ranges (default: /etc/haproxy/cloudflare_ips.lst)
|
||||
- ip_list_path: Path to file containing Cloudflare IP ranges (default: /etc/easyhaproxy/cloudflare_ips.lst)
|
||||
- ip_list: Base64-encoded list of IP ranges (one per line), takes precedence over ip_list_path
|
||||
- use_builtin_ips: Use built-in Cloudflare IP ranges (default: true)
|
||||
- update_log_format: Update HAProxy log format to show real visitor IP (default: true)
|
||||
|
|
@ -17,7 +17,7 @@ Example YAML config:
|
|||
plugins:
|
||||
cloudflare:
|
||||
enabled: true
|
||||
ip_list_path: /etc/haproxy/cloudflare_ips.lst
|
||||
ip_list_path: /etc/easyhaproxy/cloudflare_ips.lst
|
||||
use_builtin_ips: true
|
||||
update_log_format: true
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ Example Container Label:
|
|||
|
||||
HAProxy Config Generated:
|
||||
# Cloudflare - Restore original visitor IP
|
||||
acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst
|
||||
acl from_cloudflare src -f /etc/easyhaproxy/cloudflare_ips.lst
|
||||
http-request set-var(txn.real_ip) req.hdr(CF-Connecting-IP) if from_cloudflare
|
||||
http-request set-header X-Forwarded-For %[var(txn.real_ip)] if from_cloudflare
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ import sys
|
|||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from functions import logger_easyhaproxy
|
||||
from functions import logger_easyhaproxy, Consts
|
||||
from plugins import InitializationResult, PluginContext, PluginInterface, PluginResult, PluginType, ResourceRequest
|
||||
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ class CloudflarePlugin(PluginInterface):
|
|||
]
|
||||
|
||||
def __init__(self):
|
||||
self.ip_list_path = "/etc/haproxy/cloudflare_ips.lst"
|
||||
self.ip_list_path = Consts.base_path + "/cloudflare_ips.lst"
|
||||
self.enabled = True
|
||||
self.use_builtin_ips = True
|
||||
self.update_log_format = True
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ The plugin creates:
|
|||
|
||||
Configuration:
|
||||
- enabled: Enable/disable the plugin (default: true)
|
||||
- document_root: Document root path (default: /var/www/html)
|
||||
- document_root: Document root path (default: /etc/easyhaproxy/www)
|
||||
- script_filename: Pattern for SCRIPT_FILENAME (default: %[path])
|
||||
- index_file: Default index file (default: index.php)
|
||||
- path_info: Enable PATH_INFO support (default: true)
|
||||
|
|
@ -39,6 +39,8 @@ Example Kubernetes Annotation:
|
|||
import os
|
||||
import sys
|
||||
|
||||
from functions import Consts
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
|
@ -50,7 +52,7 @@ class FastcgiPlugin(PluginInterface):
|
|||
|
||||
def __init__(self):
|
||||
self.enabled = True
|
||||
self.document_root = "/var/www/html"
|
||||
self.document_root = Consts.base_path + "/www"
|
||||
self.script_filename = "%[path]"
|
||||
self.index_file = "index.php"
|
||||
self.path_info = True
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ Example YAML config:
|
|||
algorithm: RS256
|
||||
issuer: https://myaccount.auth0.com/
|
||||
audience: https://api.mywebsite.com
|
||||
pubkey_path: /etc/haproxy/jwt_keys/pubkey.pem
|
||||
pubkey_path: /etc/easyhaproxy/jwt_keys/pubkey.pem
|
||||
paths:
|
||||
- /api/admin
|
||||
- /api/sensitive
|
||||
|
|
@ -58,7 +58,7 @@ Example Container Label:
|
|||
easyhaproxy.http.plugin.jwt_validator.algorithm: RS256
|
||||
easyhaproxy.http.plugin.jwt_validator.issuer: https://auth.example.com/
|
||||
easyhaproxy.http.plugin.jwt_validator.audience: https://api.example.com
|
||||
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
|
||||
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/easyhaproxy/jwt_keys/api_pubkey.pem
|
||||
easyhaproxy.http.plugin.jwt_validator.paths: /api/admin,/api/sensitive
|
||||
easyhaproxy.http.plugin.jwt_validator.only_paths: true
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ HAProxy Config Generated:
|
|||
http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless { var(txn.alg) -m str RS256 }
|
||||
http-request deny content-type 'text/html' string 'Invalid JWT issuer' unless { var(txn.iss) -m str https://auth.example.com/ }
|
||||
http-request deny content-type 'text/html' string 'Invalid JWT audience' unless { var(txn.aud) -m str https://api.example.com }
|
||||
http-request deny content-type 'text/html' string 'Invalid JWT signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/etc/haproxy/jwt_keys/api_pubkey.pem") -m int 1 }
|
||||
http-request deny content-type 'text/html' string 'Invalid JWT signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/etc/easyhaproxy/jwt_keys/api_pubkey.pem") -m int 1 }
|
||||
|
||||
# Validate expiration
|
||||
http-request set-var(txn.now) date()
|
||||
|
|
@ -100,7 +100,7 @@ import sys
|
|||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from functions import Functions, logger_easyhaproxy
|
||||
from functions import Functions, logger_easyhaproxy, Consts
|
||||
from plugins import InitializationResult, PluginContext, PluginInterface, PluginResult, PluginType, ResourceRequest
|
||||
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ class JwtValidatorPlugin(PluginInterface):
|
|||
self.only_paths = False # If true, only specified paths are accessible
|
||||
self.allow_anonymous = False # If true, allow requests without Authorization header
|
||||
# Make JWT_KEYS_DIR configurable via environment variable (for testing)
|
||||
self.jwt_keys_dir = os.getenv("EASYHAPROXY_JWT_KEYS_DIR", "/etc/haproxy/jwt_keys")
|
||||
self.jwt_keys_dir = os.getenv("EASYHAPROXY_JWT_KEYS_DIR", Consts.base_path + "/jwt_keys")
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{% if "ssl" in o %}
|
||||
bind *:{{ o["port"] }} ssl crt /certs/certbot/ alpn h2,http/1.1 crt /certs/haproxy/ alpn h2,http/1.1
|
||||
bind *:{{ o["port"] }} ssl crt /etc/easyhaproxy/certs/certbot/ alpn h2,http/1.1 crt /etc/easyhaproxy/certs/haproxy/ alpn h2,http/1.1
|
||||
{% elif "h2" in o and o["h2"] %}
|
||||
bind *:{{ o["port"] }} proto h2
|
||||
option http-use-htx
|
||||
|
|
|
|||
|
|
@ -31,13 +31,13 @@ defaults
|
|||
timeout client 10s
|
||||
timeout server 10m
|
||||
{% if data["customerrors"] %}
|
||||
errorfile 400 /etc/haproxy/errors-custom/400.http
|
||||
errorfile 403 /etc/haproxy/errors-custom/403.http
|
||||
errorfile 408 /etc/haproxy/errors-custom/408.http
|
||||
errorfile 500 /etc/haproxy/errors-custom/500.http
|
||||
errorfile 502 /etc/haproxy/errors-custom/502.http
|
||||
errorfile 503 /etc/haproxy/errors-custom/503.http
|
||||
errorfile 504 /etc/haproxy/errors-custom/504.http
|
||||
errorfile 400 /etc/easyhaproxy/haproxy/errors-custom/400.http
|
||||
errorfile 403 /etc/easyhaproxy/haproxy/errors-custom/403.http
|
||||
errorfile 408 /etc/easyhaproxy/haproxy/errors-custom/408.http
|
||||
errorfile 500 /etc/easyhaproxy/haproxy/errors-custom/500.http
|
||||
errorfile 502 /etc/easyhaproxy/haproxy/errors-custom/502.http
|
||||
errorfile 503 /etc/easyhaproxy/haproxy/errors-custom/503.http
|
||||
errorfile 504 /etc/easyhaproxy/haproxy/errors-custom/504.http
|
||||
{% endif %}
|
||||
{% if defaults_plugin_configs %}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@
|
|||
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
|
||||
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
|
||||
|
||||
ssl-dh-param-file /etc/haproxy/dhparam
|
||||
ssl-dh-param-file /etc/easyhaproxy/haproxy/dhparam
|
||||
|
|
|
|||
|
|
@ -6,5 +6,5 @@
|
|||
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
|
||||
ssl-default-server-options no-sslv3 no-tls-tickets
|
||||
|
||||
ssl-dh-param-file /etc/haproxy/dhparam-1024
|
||||
ssl-dh-param-file /etc/easyhaproxy/haproxy/dhparam-1024
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue