1
0
Fork 0
docker-easy-haproxy/src/easymapping/__init__.py
Joao Gilberto Magalhaes 5767e55dea 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.
2026-02-11 19:19:08 -05:00

336 lines
15 KiB
Python

import base64
import json
import os
import re
from jinja2 import Environment, FileSystemLoader
from functions import logger_easyhaproxy
class DockerLabelHandler:
def __init__(self, label):
self.__data = None
self.__label_base = label
def get_lookup_label(self):
return self.__label_base
def create(self, key):
if isinstance(key, str):
return f"{self.__label_base}.{key}"
return "{}.{}".format(self.__label_base, ".".join(key))
def get(self, label, default_value=""):
if self.has_label(label):
return self.__data[label]
return default_value
def get_bool(self, label, default_value=False):
if self.has_label(label):
return self.__data[label].lower() in ["true", "1", "yes"]
return default_value
def get_json(self, label, default_value={}):
if self.has_label(label):
value = self.__data[label]
if not value: # Handle empty strings
return default_value
try:
return json.loads(value)
except json.JSONDecodeError as e:
logger_easyhaproxy.error(
f"Invalid JSON in label '{label}': {value}. Error: {e}. Using default value."
)
return default_value
return default_value
def set_data(self, data):
self.__data = data
def has_label(self, label):
if label in self.__data:
return True
return False
class HaproxyConfigGenerator:
def __init__(self, mapping):
self.mapping = mapping
self.mapping.setdefault("ssl_mode", 'default')
self.mapping.setdefault("certbot", {"email": "", "server": False, "eab_kid": False, "eab_hmac_key": False})
self.mapping["ssl_mode"] = self.mapping["ssl_mode"].lower()
self.label = DockerLabelHandler(mapping['lookup_label'] if 'lookup_label' in mapping else "easyhaproxy")
self.certbot_hosts = []
self.serving_hosts = []
self.certs = {}
self.defaults_plugin_configs = []
# Initialize plugin system
try:
from plugins import PluginManager
self.plugin_manager = PluginManager(
plugins_dir="/etc/haproxy/plugins",
abort_on_error=self.mapping.get("plugins", {}).get("abort_on_error", False)
)
self.plugin_manager.load_plugins()
self.plugin_manager.configure_plugins(self.mapping.get("plugins", {}))
self.global_plugin_configs = []
except Exception as e:
# If plugin system fails to initialize, log but continue
logger_easyhaproxy.warning(f"Failed to initialize plugin system: {e}")
self.plugin_manager = None
self.global_plugin_configs = []
def generate(self, container_metadata={}):
self.mapping.setdefault("easymapping", [])
if container_metadata != {}:
self.mapping["easymapping"] = self.parse(container_metadata)
# Execute global plugins
if self.plugin_manager:
try:
from plugins import PluginContext
global_context = PluginContext(
parsed_object=container_metadata,
easymapping=self.mapping.get("easymapping", []),
container_env=self.mapping,
domain=None,
port=None,
host_config=None
)
# Get enabled plugins from config
enabled_list = self.mapping.get("plugins", {}).get("enabled", [])
# If enabled list contains only empty string, treat as no plugins enabled
if enabled_list and len(enabled_list) > 0 and enabled_list[0] == "":
enabled_list = []
global_results = self.plugin_manager.execute_global_plugins(global_context, enabled_list)
# 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}")
templates_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'templates')
file_loader = FileSystemLoader(templates_dir)
env = Environment(loader=file_loader)
env.trim_blocks = True
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,
defaults_plugin_configs=self.defaults_plugin_configs
)
def parse(self, container_metadata):
easymapping = dict()
for container in container_metadata:
d = container_metadata[container]
# Extract the definitions dynamically
definitions = {}
r = re.compile(self.label.get_lookup_label() + r"\.(.*)\..*")
for key in d.keys():
if r.match(key):
definitions[r.search(key).group(1)] = 1
if len(definitions.keys()) == 0:
continue
self.label.set_data(d)
# Parse each definition found.
for definition in sorted(definitions.keys()):
mode = self.label.get(
self.label.create([definition, "mode"]),
"http"
)
# TODO: we can ignore "host" in TCP, but it would break the template
host_label = self.label.create([definition, "host"])
if not self.label.has_label(host_label):
continue
port = self.label.get(
self.label.create([definition, "port"]),
"80"
)
certbot = self.label.get_bool(
self.label.create([definition, "certbot"]),
False
) and self.mapping["certbot"]["email"] != ""
clone_to_ssl = self.label.get_bool(
self.label.create([definition, "clone_to_ssl"])
)
if port not in easymapping:
easymapping[port] = {
"mode": mode,
"ssl-check": "",
"port": port,
"hosts": dict(),
"redirect": dict(),
}
# TODO: this could use `EXPOSE` from `Dockerfile`?
ct_port = self.label.get(
self.label.create([definition, "localport"]),
"80"
)
easymapping[port]["ssl-check"] = self.label.get(
self.label.create([definition, "ssl-check"]),
""
)
# Protocol for backend server communication (e.g., fcgi, h2)
proto = self.label.get(
self.label.create([definition, "proto"]),
""
)
# Unix socket path (alternative to host:port)
socket_path = self.label.get(
self.label.create([definition, "socket"]),
""
)
for hostname in sorted(d[host_label].split(",")):
hostname = hostname.strip()
self.serving_hosts.append(f"{hostname}:{port}")
easymapping[port]["hosts"].setdefault(hostname, {})
easymapping[port]["hosts"][hostname].setdefault("containers", [])
easymapping[port]["hosts"][hostname].setdefault("certbot", False)
easymapping[port]["hosts"][hostname].setdefault("proto", proto)
# Determine server address: Unix socket or TCP host:port
if socket_path:
server_address = socket_path
else:
server_address = f"{container}:{ct_port}"
easymapping[port]["hosts"][hostname]["containers"] += [server_address]
easymapping[port]["hosts"][hostname]["certbot"] = certbot
easymapping[port]["hosts"][hostname]["redirect_ssl"] = self.label.get_bool(
self.label.create([definition, "redirect_ssl"])
)
easymapping[port]["hosts"][hostname]["balance"] = self.label.get(
self.label.create([definition, "balance"]),
"roundrobin"
)
easymapping[port]["redirect"] = self.label.get_json(
self.label.create([definition, "redirect"])
)
# Execute domain plugins for this host
if self.plugin_manager:
try:
from plugins import PluginContext
domain_context = PluginContext(
parsed_object=container_metadata,
easymapping=easymapping,
container_env=self.mapping,
domain=hostname,
port=port,
host_config=easymapping[port]["hosts"][hostname]
)
# Check if plugins are enabled for this domain (from labels)
enabled_plugins = []
if self.label.has_label(self.label.create([definition, "plugins"])):
enabled_plugins = self.label.get(
self.label.create([definition, "plugins"]),
""
).split(",")
enabled_plugins = [p.strip() for p in enabled_plugins if p.strip()]
# Extract plugin configurations from labels
# Format: easyhaproxy.http.plugin.PLUGIN_NAME.CONFIG_KEY
plugin_configs = {}
for plugin_name in enabled_plugins:
plugin_configs[plugin_name] = {}
# Look for all labels matching easyhaproxy.{definition}.plugin.{plugin_name}.*
plugin_label_prefix = self.label.create([definition, "plugin", plugin_name])
for label_key in d.keys():
if label_key.startswith(plugin_label_prefix + "."):
# Extract config key (everything after plugin_label_prefix + ".")
config_key = label_key[len(plugin_label_prefix) + 1:]
plugin_configs[plugin_name][config_key] = d[label_key]
# Configure plugins with label-specific configs before execution
for plugin_name, config in plugin_configs.items():
if plugin_name in self.plugin_manager.plugins:
self.plugin_manager.plugins[plugin_name].configure(config)
domain_results = self.plugin_manager.execute_domain_plugins(
domain_context,
enabled_list=enabled_plugins
)
# Store domain plugin configs for this host
easymapping[port]["hosts"][hostname]["plugin_configs"] = [
r.haproxy_config for r in domain_results if r.haproxy_config
]
# Extract fcgi-app definitions from metadata and add to global configs
for result in domain_results:
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"] = []
else:
easymapping[port]["hosts"][hostname]["plugin_configs"] = []
if certbot or clone_to_ssl:
if "443" not in easymapping:
easymapping["443"] = {
"mode": "http",
"ssl-check": "ssl",
"port": "443",
"hosts": dict(),
"redirect": dict(),
}
easymapping["443"]["hosts"][hostname] = dict(easymapping[port]["hosts"][hostname])
easymapping["443"]["hosts"][hostname]["certbot"] = False
easymapping["443"]["hosts"][hostname]["redirect_ssl"] = False
easymapping["443"]["ssl"] = True
self.certbot_hosts.append(
hostname) if certbot and hostname not in self.certbot_hosts else self.certbot_hosts
# handle SSL
ssl_label = self.label.create([definition, "sslcert"])
if self.label.has_label(ssl_label):
filename = f"{d[host_label]}.pem"
easymapping[port]["ssl"] = True if not clone_to_ssl else False
self.certs[filename] = base64.b64decode(d[ssl_label]).decode('ascii')
if self.label.get_bool(self.label.create([definition, "ssl"])):
easymapping[port]["ssl"] = True if not clone_to_ssl else False
return easymapping.values()