1
0
Fork 0

Refactor Docker Compose examples and plugins, add pytest tests

- Adjusted Compose files to build images locally instead of pulling.
- Simplified examples by removing `/etc/hosts` entry instructions.
- Enhanced `cloudflare.py` plugin with a configurable log format.
- Improved `generate-keys.sh` for portability using `$SCRIPT_DIR`.
- Added end-to-end tests for Compose examples using pytest.
This commit is contained in:
Joao Gilberto Magalhaes 2026-02-11 19:19:08 -05:00
parent 343987ab58
commit 5767e55dea
19 changed files with 1162 additions and 89 deletions

View file

@ -65,6 +65,7 @@ class HaproxyConfigGenerator:
self.certbot_hosts = []
self.serving_hosts = []
self.certs = {}
self.defaults_plugin_configs = []
# Initialize plugin system
try:
@ -111,6 +112,13 @@ class HaproxyConfigGenerator:
# Extend instead of replace to preserve fcgi-app definitions from domain plugins
global_configs = [r.haproxy_config for r in global_results if r.haproxy_config]
self.global_plugin_configs.extend(global_configs)
# Extract defaults-level configs from global plugins
for result in global_results:
if result.metadata and "defaults_config" in result.metadata:
config = result.metadata["defaults_config"]
if config and config not in self.defaults_plugin_configs:
self.defaults_plugin_configs.append(config)
except Exception as e:
logger_easyhaproxy.warning(f"Failed to execute global plugins: {e}")
@ -121,7 +129,11 @@ class HaproxyConfigGenerator:
env.lstrip_blocks = True
env.rstrip_blocks = True
template = env.get_template('haproxy.cfg.j2')
return template.render(data=self.mapping, global_plugin_configs=self.global_plugin_configs)
return template.render(
data=self.mapping,
global_plugin_configs=self.global_plugin_configs,
defaults_plugin_configs=self.defaults_plugin_configs
)
def parse(self, container_metadata):
easymapping = dict()
@ -282,6 +294,13 @@ class HaproxyConfigGenerator:
if result.metadata and "fcgi_app_definition" in result.metadata:
if result.metadata["fcgi_app_definition"] not in self.global_plugin_configs:
self.global_plugin_configs.append(result.metadata["fcgi_app_definition"])
# Extract defaults-level config from metadata (e.g., log-format from Cloudflare plugin)
for result in domain_results:
if result.metadata and "defaults_config" in result.metadata:
config = result.metadata["defaults_config"]
if config and config not in self.defaults_plugin_configs:
self.defaults_plugin_configs.append(config)
except Exception as e:
logger_easyhaproxy.warning(f"Failed to execute domain plugins for {hostname}: {e}")
easymapping[port]["hosts"][hostname]["plugin_configs"] = []

View file

@ -10,6 +10,7 @@ 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)
- 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)
Example YAML config:
plugins:
@ -17,14 +18,21 @@ Example YAML config:
enabled: true
ip_list_path: /etc/haproxy/cloudflare_ips.lst
use_builtin_ips: true
update_log_format: true
Example Container Label:
easyhaproxy.http.plugins: "cloudflare"
easyhaproxy.http.plugin.cloudflare.update_log_format: "true"
HAProxy Config Generated:
# Cloudflare - Restore original visitor IP
acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst
http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] if from_cloudflare
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
Log Format (when update_log_format=true):
Shows real visitor IP alongside connection IP for debugging
Format: real_ip/connection_ip [timestamp] request status bytes ...
"""
import os
@ -73,6 +81,7 @@ class CloudflarePlugin(PluginInterface):
self.ip_list_path = "/etc/haproxy/cloudflare_ips.lst"
self.enabled = True
self.use_builtin_ips = True
self.update_log_format = True
@property
def name(self) -> str:
@ -91,6 +100,7 @@ class CloudflarePlugin(PluginInterface):
- ip_list_path: Path to Cloudflare IP list file
- enabled: Whether plugin is enabled
- use_builtin_ips: Use built-in Cloudflare IP ranges (default: true)
- update_log_format: Update HAProxy log format to show real IP (default: true)
"""
if "ip_list_path" in config:
self.ip_list_path = config["ip_list_path"]
@ -101,6 +111,9 @@ class CloudflarePlugin(PluginInterface):
if "use_builtin_ips" in config:
self.use_builtin_ips = str(config["use_builtin_ips"]).lower() in ["true", "1", "yes"]
if "update_log_format" in config:
self.update_log_format = str(config["update_log_format"]).lower() in ["true", "1", "yes"]
def process(self, context: PluginContext) -> PluginResult:
"""
Generate HAProxy config to restore original IP from Cloudflare
@ -131,10 +144,19 @@ class CloudflarePlugin(PluginInterface):
except Exception as e:
logger_easyhaproxy.warning(f"Cloudflare plugin: Failed to write IP list to {self.ip_list_path}: {e}")
# Generate HAProxy config snippet
# Generate HAProxy config snippet for backend
haproxy_config = f"""# Cloudflare - Restore original visitor IP
acl from_cloudflare src -f {self.ip_list_path}
http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] if from_cloudflare"""
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"""
# Generate log format config (frontend level)
# Industry-standard format: real_ip/proxy_ip [time] request status bytes timings
# Based on HAProxy HTTP log format with real IP shown first
log_format_config = None
if self.update_log_format:
log_format_config = """# Cloudflare - Enhanced log format showing real visitor IP
log-format "%{+Q}[var(txn.real_ip)]:-/%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r\""""
return PluginResult(
haproxy_config=haproxy_config,
@ -143,6 +165,8 @@ http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] if from_clo
"domain": context.domain,
"ip_list_path": self.ip_list_path,
"use_builtin_ips": self.use_builtin_ips,
"update_log_format": self.update_log_format,
"defaults_config": log_format_config,
"ip_count": len(self.CLOUDFLARE_IPS) if self.use_builtin_ips else None
}
)

View file

@ -39,6 +39,13 @@ defaults
errorfile 503 /etc/haproxy/errors-custom/503.http
errorfile 504 /etc/haproxy/errors-custom/504.http
{% endif %}
{% if defaults_plugin_configs %}
# Defaults Plugin Configurations
{% for config in defaults_plugin_configs %}
{{ config }}
{% endfor %}
{% endif %}
{% if global_plugin_configs %}
# Global Plugin Configurations