Add built-in plugin framework and initial plugins
- Introduced a plugin system enabling EasyHAProxy extensions. - Added `CleanupPlugin` (GLOBAL): Handles temporary file cleanup. - Added `CloudflarePlugin` (DOMAIN): Restores visitor IP from Cloudflare headers. - Added `DenyPagesPlugin` (DOMAIN): Blocks access to specified paths. - Documented the plugin architecture, configuration options, and built-in plugins in `docs/plugin-development.md` and `docs/plugins.md`.
This commit is contained in:
parent
fdb6ddf3b9
commit
90e3df2b75
12 changed files with 1341 additions and 1 deletions
294
docs/plugin-development.md
Normal file
294
docs/plugin-development.md
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
---
|
||||
sidebar_position: 17
|
||||
---
|
||||
|
||||
# EasyHAProxy Plugin System - Developer Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The EasyHAProxy plugin system allows you to extend HAProxy configuration generation with custom functionality. Plugins are invoked during the discovery cycle and can inject HAProxy configuration snippets or modify the discovery data.
|
||||
|
||||
## Plugin Types
|
||||
|
||||
### GLOBAL Plugins
|
||||
- **Execution**: Once per discovery cycle
|
||||
- **Use Cases**: Cleanup tasks, global configuration, monitoring, DNS updates
|
||||
- **Example**: `CleanupPlugin`
|
||||
|
||||
### DOMAIN Plugins
|
||||
- **Execution**: Once for each discovered domain/host
|
||||
- **Use Cases**: Domain-specific configuration, IP restoration, path blocking, custom headers
|
||||
- **Example**: `CloudflarePlugin`, `DenyPagesPlugin`
|
||||
|
||||
## Creating a Custom Plugin
|
||||
|
||||
### 1. Plugin Structure
|
||||
|
||||
Create a Python file in `/etc/haproxy/plugins/` or `/scripts/plugins/builtin/`:
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from plugins import PluginInterface, PluginType, PluginContext, PluginResult
|
||||
|
||||
|
||||
class MyCustomPlugin(PluginInterface):
|
||||
"""Your plugin description"""
|
||||
|
||||
def __init__(self):
|
||||
# Initialize plugin state
|
||||
self.enabled = True
|
||||
self.my_config_value = "default"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return unique plugin name"""
|
||||
return "my_custom_plugin"
|
||||
|
||||
@property
|
||||
def plugin_type(self) -> PluginType:
|
||||
"""Return GLOBAL or DOMAIN"""
|
||||
return PluginType.DOMAIN
|
||||
|
||||
def configure(self, config: dict) -> None:
|
||||
"""
|
||||
Configure plugin from YAML/env/labels
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary
|
||||
"""
|
||||
if "enabled" in config:
|
||||
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
|
||||
|
||||
if "my_config_value" in config:
|
||||
self.my_config_value = config["my_config_value"]
|
||||
|
||||
def process(self, context: PluginContext) -> PluginResult:
|
||||
"""
|
||||
Process plugin logic
|
||||
|
||||
Args:
|
||||
context: PluginContext with execution data
|
||||
|
||||
Returns:
|
||||
PluginResult with HAProxy config and/or metadata
|
||||
"""
|
||||
if not self.enabled:
|
||||
return PluginResult()
|
||||
|
||||
# Generate HAProxy configuration snippet
|
||||
haproxy_config = """# My Custom Plugin
|
||||
# Add your HAProxy directives here
|
||||
http-request set-header X-Custom-Header "value"
|
||||
"""
|
||||
|
||||
return PluginResult(
|
||||
haproxy_config=haproxy_config,
|
||||
modified_easymapping=None, # Optional: modify discovery data
|
||||
metadata={"info": "my metadata"} # Optional: logging info
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Plugin Context Data
|
||||
|
||||
The `PluginContext` object provides:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class PluginContext:
|
||||
parsed_object: dict # {IP: labels} from container discovery
|
||||
easymapping: list # Current HAProxy mapping structure
|
||||
container_env: dict # Environment configuration
|
||||
domain: Optional[str] # Domain name (DOMAIN plugins only)
|
||||
port: Optional[str] # Port (DOMAIN plugins only)
|
||||
host_config: Optional[dict] # Domain config (DOMAIN plugins only)
|
||||
```
|
||||
|
||||
**Example parsed_object:**
|
||||
```python
|
||||
{
|
||||
"192.168.1.10": {
|
||||
"easyhaproxy.http.host": "example.com",
|
||||
"easyhaproxy.http.port": "80",
|
||||
"easyhaproxy.http.localport": "8080"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example easymapping:**
|
||||
```python
|
||||
[
|
||||
{
|
||||
"port": "80",
|
||||
"mode": "http",
|
||||
"hosts": {
|
||||
"example.com": {
|
||||
"containers": ["192.168.1.10:8080"],
|
||||
"certbot": False,
|
||||
"balance": "roundrobin"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 3. Plugin Configuration
|
||||
|
||||
Plugins can be configured via:
|
||||
|
||||
#### YAML (`/etc/haproxy/static/config.yaml`):
|
||||
```yaml
|
||||
plugins:
|
||||
my_custom_plugin:
|
||||
enabled: true
|
||||
my_config_value: "custom"
|
||||
```
|
||||
|
||||
#### Environment Variables:
|
||||
```bash
|
||||
EASYHAPROXY_PLUGINS_ENABLED=my_custom_plugin
|
||||
EASYHAPROXY_PLUGIN_MY_CUSTOM_PLUGIN_MY_CONFIG_VALUE=custom
|
||||
```
|
||||
|
||||
#### Container Labels:
|
||||
```yaml
|
||||
labels:
|
||||
easyhaproxy.http.plugins: "my_custom_plugin"
|
||||
easyhaproxy.http.plugin.my_custom_plugin.my_config_value: "custom"
|
||||
```
|
||||
|
||||
## Built-in Plugins
|
||||
|
||||
### CloudflarePlugin (DOMAIN)
|
||||
- **Purpose**: Restore original visitor IP from Cloudflare headers
|
||||
- **Config**: `ip_list_path` - Path to Cloudflare IP list
|
||||
- **Location**: `/scripts/plugins/builtin/cloudflare.py`
|
||||
|
||||
### CleanupPlugin (GLOBAL)
|
||||
- **Purpose**: Cleanup temporary files during discovery
|
||||
- **Config**: `max_idle_time`, `cleanup_temp_files`
|
||||
- **Location**: `/scripts/plugins/builtin/cleanup.py`
|
||||
|
||||
### DenyPagesPlugin (DOMAIN)
|
||||
- **Purpose**: Block access to specific paths
|
||||
- **Config**: `paths`, `status_code`
|
||||
- **Location**: `/scripts/plugins/builtin/deny_pages.py`
|
||||
|
||||
## Plugin Lifecycle
|
||||
|
||||
```
|
||||
Discovery Cycle
|
||||
├─ Container/Service Discovery
|
||||
├─ Parse Metadata → easymapping
|
||||
├─ Execute GLOBAL plugins (once)
|
||||
├─ For each domain:
|
||||
│ ├─ Execute DOMAIN plugins
|
||||
│ └─ Store plugin configs
|
||||
├─ Render Jinja2 template with plugin snippets
|
||||
└─ Generate final HAProxy config
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Default Behavior (Log and Continue)
|
||||
```yaml
|
||||
plugins:
|
||||
abort_on_error: false # Default
|
||||
```
|
||||
- Plugin errors are logged as warnings
|
||||
- Discovery cycle continues
|
||||
- HAProxy config generation proceeds
|
||||
|
||||
### Abort on Error
|
||||
```yaml
|
||||
plugins:
|
||||
abort_on_error: true
|
||||
```
|
||||
- Plugin errors stop the discovery cycle
|
||||
- Previous HAProxy config remains active
|
||||
- Useful for critical plugins
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep It Simple**: Plugins should do one thing well
|
||||
2. **Error Handling**: Use try/except and return empty PluginResult on errors
|
||||
3. **Logging**: Use `from functions import loggerEasyHaproxy` for logging
|
||||
4. **Configuration**: Provide sensible defaults
|
||||
5. **Documentation**: Add docstrings explaining configuration options
|
||||
6. **Testing**: Test your plugin with various configurations
|
||||
|
||||
## Debugging
|
||||
|
||||
Enable debug logging:
|
||||
```bash
|
||||
EASYHAPROXY_LOG_LEVEL=DEBUG
|
||||
```
|
||||
|
||||
Check plugin loading:
|
||||
```
|
||||
# Look for log messages:
|
||||
Loaded builtin plugin: cloudflare (domain)
|
||||
Loaded external plugin: my_plugin (global)
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Add Custom Header (DOMAIN)
|
||||
|
||||
```python
|
||||
class CustomHeaderPlugin(PluginInterface):
|
||||
def __init__(self):
|
||||
self.header_name = "X-Custom"
|
||||
self.header_value = "value"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "custom_header"
|
||||
|
||||
@property
|
||||
def plugin_type(self) -> PluginType:
|
||||
return PluginType.DOMAIN
|
||||
|
||||
def configure(self, config: dict) -> None:
|
||||
self.header_name = config.get("header_name", self.header_name)
|
||||
self.header_value = config.get("header_value", self.header_value)
|
||||
|
||||
def process(self, context: PluginContext) -> PluginResult:
|
||||
return PluginResult(
|
||||
haproxy_config=f'http-request set-header {self.header_name} "{self.header_value}"'
|
||||
)
|
||||
```
|
||||
|
||||
### Example 2: DNS Update (GLOBAL)
|
||||
|
||||
```python
|
||||
class DNSUpdatePlugin(PluginInterface):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "dns_update"
|
||||
|
||||
@property
|
||||
def plugin_type(self) -> PluginType:
|
||||
return PluginType.GLOBAL
|
||||
|
||||
def configure(self, config: dict) -> None:
|
||||
self.dns_server = config.get("dns_server", "8.8.8.8")
|
||||
|
||||
def process(self, context: PluginContext) -> PluginResult:
|
||||
# Update DNS records based on discovered hosts
|
||||
for ip, labels in context.parsed_object.items():
|
||||
# Your DNS update logic here
|
||||
pass
|
||||
|
||||
return PluginResult(metadata={"dns_updates": "completed"})
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
- GitHub: https://github.com/byjg/docker-easy-haproxy
|
||||
- Documentation: https://byjg.github.io/docker-easy-haproxy
|
||||
341
docs/plugins.md
Normal file
341
docs/plugins.md
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
---
|
||||
sidebar_position: 16
|
||||
---
|
||||
|
||||
# Plugins
|
||||
|
||||
EasyHAProxy supports a flexible plugin system that allows you to extend HAProxy configuration with custom functionality. Plugins are automatically invoked during the discovery cycle and can inject HAProxy configuration directives or modify discovery data.
|
||||
|
||||
## Plugin Types
|
||||
|
||||
### Global Plugins
|
||||
Global plugins execute **once per discovery cycle**, regardless of how many domains are discovered. They're ideal for:
|
||||
- Cleanup tasks
|
||||
- Global monitoring
|
||||
- DNS updates
|
||||
- Log management
|
||||
|
||||
### Domain Plugins
|
||||
Domain plugins execute **once for each discovered domain/host**. They're ideal for:
|
||||
- Domain-specific configuration
|
||||
- IP restoration (e.g., Cloudflare)
|
||||
- Path blocking
|
||||
- Custom headers per domain
|
||||
|
||||
## Built-in Plugins
|
||||
|
||||
### Cloudflare (Domain Plugin)
|
||||
|
||||
Restores the original visitor IP address from Cloudflare's `CF-Connecting-IP` header when requests come through Cloudflare's CDN.
|
||||
|
||||
**Configuration:**
|
||||
```yaml
|
||||
# /etc/haproxy/static/config.yaml
|
||||
plugins:
|
||||
cloudflare:
|
||||
enabled: true
|
||||
ip_list_path: /etc/haproxy/cloudflare_ips.lst
|
||||
```
|
||||
|
||||
**Container Label:**
|
||||
```yaml
|
||||
services:
|
||||
myapp:
|
||||
labels:
|
||||
easyhaproxy.http.host: example.com
|
||||
easyhaproxy.http.plugins: cloudflare
|
||||
```
|
||||
|
||||
**Environment Variable:**
|
||||
```bash
|
||||
EASYHAPROXY_PLUGINS_ENABLED=cloudflare
|
||||
EASYHAPROXY_PLUGIN_CLOUDFLARE_IP_LIST_PATH=/etc/haproxy/cloudflare_ips.lst
|
||||
```
|
||||
|
||||
### Cleanup (Global Plugin)
|
||||
|
||||
Performs cleanup tasks during each discovery cycle, such as removing old temporary files.
|
||||
|
||||
**Configuration:**
|
||||
```yaml
|
||||
# /etc/haproxy/static/config.yaml
|
||||
plugins:
|
||||
cleanup:
|
||||
enabled: true
|
||||
max_idle_time: 300 # seconds
|
||||
cleanup_temp_files: true
|
||||
```
|
||||
|
||||
**Environment Variable:**
|
||||
```bash
|
||||
EASYHAPROXY_PLUGINS_ENABLED=cleanup
|
||||
EASYHAPROXY_PLUGIN_CLEANUP_MAX_IDLE_TIME=600
|
||||
```
|
||||
|
||||
### Deny Pages (Domain Plugin)
|
||||
|
||||
Blocks access to specific paths for a domain.
|
||||
|
||||
**Configuration:**
|
||||
```yaml
|
||||
# /etc/haproxy/static/config.yaml
|
||||
plugins:
|
||||
deny_pages:
|
||||
enabled: true
|
||||
paths: /admin,/private,/internal
|
||||
status_code: 403
|
||||
```
|
||||
|
||||
**Container Label:**
|
||||
```yaml
|
||||
services:
|
||||
myapp:
|
||||
labels:
|
||||
easyhaproxy.http.host: example.com
|
||||
easyhaproxy.http.plugins: deny_pages
|
||||
easyhaproxy.http.plugin.deny_pages.paths: /admin,/private
|
||||
easyhaproxy.http.plugin.deny_pages.status_code: 403
|
||||
```
|
||||
|
||||
## Configuration Methods
|
||||
|
||||
Plugins can be configured using three methods. Configuration from YAML takes precedence over environment variables.
|
||||
|
||||
### 1. Static YAML Configuration
|
||||
|
||||
Configure plugins in `/etc/haproxy/static/config.yaml`:
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
# Global settings
|
||||
abort_on_error: false # Log and continue on plugin errors (default)
|
||||
|
||||
# Plugin-specific configuration
|
||||
cloudflare:
|
||||
enabled: true
|
||||
ip_list_path: /etc/haproxy/cloudflare_ips.lst
|
||||
|
||||
cleanup:
|
||||
enabled: true
|
||||
max_idle_time: 300
|
||||
|
||||
deny_pages:
|
||||
enabled: false # Disable globally, can still be enabled per domain
|
||||
```
|
||||
|
||||
### 2. Environment Variables
|
||||
|
||||
Configure plugins via environment variables:
|
||||
|
||||
```bash
|
||||
# Global settings
|
||||
EASYHAPROXY_PLUGINS_ABORT_ON_ERROR=false
|
||||
EASYHAPROXY_PLUGINS_ENABLED=cloudflare,cleanup
|
||||
|
||||
# Plugin-specific configuration
|
||||
EASYHAPROXY_PLUGIN_CLOUDFLARE_ENABLED=true
|
||||
EASYHAPROXY_PLUGIN_CLOUDFLARE_IP_LIST_PATH=/etc/haproxy/cloudflare_ips.lst
|
||||
EASYHAPROXY_PLUGIN_CLEANUP_MAX_IDLE_TIME=600
|
||||
```
|
||||
|
||||
### 3. Container Labels (Domain Plugins Only)
|
||||
|
||||
Enable domain plugins for specific containers:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
webapp:
|
||||
image: myapp:latest
|
||||
labels:
|
||||
easyhaproxy.http.host: example.com
|
||||
easyhaproxy.http.plugins: cloudflare,deny_pages
|
||||
easyhaproxy.http.plugin.deny_pages.paths: /admin,/api/internal
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Log and Continue (Default)
|
||||
|
||||
By default, plugin errors are logged as warnings and the discovery cycle continues:
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
abort_on_error: false # Default
|
||||
```
|
||||
|
||||
This ensures that a failing plugin doesn't prevent HAProxy configuration updates.
|
||||
|
||||
### Abort on Error
|
||||
|
||||
For critical plugins, you can stop the discovery cycle on errors:
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
abort_on_error: true
|
||||
```
|
||||
|
||||
With this setting, any plugin error will halt configuration generation and the previous HAProxy config remains active until the issue is resolved.
|
||||
|
||||
## Custom Plugins
|
||||
|
||||
You can create custom plugins by placing Python files in `/etc/haproxy/plugins/`.
|
||||
|
||||
### Simple Custom Plugin Example
|
||||
|
||||
Create `/etc/haproxy/plugins/custom_header.py`:
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from plugins import PluginInterface, PluginType, PluginContext, PluginResult
|
||||
|
||||
|
||||
class CustomHeaderPlugin(PluginInterface):
|
||||
def __init__(self):
|
||||
self.enabled = True
|
||||
self.header_name = "X-Custom-App"
|
||||
self.header_value = "EasyHAProxy"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "custom_header"
|
||||
|
||||
@property
|
||||
def plugin_type(self) -> PluginType:
|
||||
return PluginType.DOMAIN
|
||||
|
||||
def configure(self, config: dict) -> None:
|
||||
if "enabled" in config:
|
||||
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
|
||||
if "header_name" in config:
|
||||
self.header_name = config["header_name"]
|
||||
if "header_value" in config:
|
||||
self.header_value = config["header_value"]
|
||||
|
||||
def process(self, context: PluginContext) -> PluginResult:
|
||||
if not self.enabled:
|
||||
return PluginResult()
|
||||
|
||||
haproxy_config = f'http-request set-header {self.header_name} "{self.header_value}"'
|
||||
|
||||
return PluginResult(
|
||||
haproxy_config=haproxy_config,
|
||||
metadata={"domain": context.domain}
|
||||
)
|
||||
```
|
||||
|
||||
### Enable Your Custom Plugin
|
||||
|
||||
**YAML:**
|
||||
```yaml
|
||||
plugins:
|
||||
custom_header:
|
||||
enabled: true
|
||||
header_name: X-My-Header
|
||||
header_value: MyValue
|
||||
```
|
||||
|
||||
**Container Label:**
|
||||
```yaml
|
||||
labels:
|
||||
easyhaproxy.http.plugins: custom_header
|
||||
easyhaproxy.http.plugin.custom_header.header_value: CustomValue
|
||||
```
|
||||
|
||||
## Plugin Development
|
||||
|
||||
For detailed information on developing plugins, see the [Plugin Developer Guide](plugin-development.md).
|
||||
|
||||
Key points:
|
||||
- Plugins must inherit from `PluginInterface`
|
||||
- Implement required methods: `name`, `plugin_type`, `configure`, `process`
|
||||
- Return `PluginResult` with HAProxy config snippets
|
||||
- Use `loggerEasyHaproxy` for logging
|
||||
- Handle errors gracefully
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Cloudflare IP Restoration
|
||||
|
||||
Restore original visitor IPs when using Cloudflare:
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
cloudflare:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
Requires Cloudflare IP list at `/etc/haproxy/cloudflare_ips.lst`. See [Cloudflare documentation](https://support.cloudflare.com/hc/en-us/articles/200170786-Restoring-original-visitor-IPs).
|
||||
|
||||
### 2. Path Protection
|
||||
|
||||
Block sensitive paths from public access:
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
easyhaproxy.http.host: myapp.com
|
||||
easyhaproxy.http.plugins: deny_pages
|
||||
easyhaproxy.http.plugin.deny_pages.paths: /admin,/config,/debug
|
||||
```
|
||||
|
||||
### 3. Automatic Cleanup
|
||||
|
||||
Keep your system clean:
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
cleanup:
|
||||
enabled: true
|
||||
max_idle_time: 3600 # 1 hour
|
||||
```
|
||||
|
||||
### 4. Custom Authentication
|
||||
|
||||
Create a plugin to add basic auth:
|
||||
|
||||
```python
|
||||
def process(self, context: PluginContext) -> PluginResult:
|
||||
return PluginResult(
|
||||
haproxy_config="""http-request auth realm MyApp unless { http_auth(user_list) }"""
|
||||
)
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
Enable debug logging to see plugin execution:
|
||||
|
||||
```bash
|
||||
EASYHAPROXY_LOG_LEVEL=DEBUG
|
||||
```
|
||||
|
||||
Look for log messages:
|
||||
```
|
||||
INFO: Loaded builtin plugin: cloudflare (domain)
|
||||
DEBUG: Executing domain plugin: cloudflare for domain: example.com
|
||||
DEBUG: Plugin cloudflare metadata: {'domain': 'example.com'}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Test plugins independently** before deploying to production
|
||||
2. **Use log-and-continue mode** unless a plugin is critical
|
||||
3. **Keep plugin logic simple** - one plugin should do one thing well
|
||||
4. **Document configuration options** in plugin docstrings
|
||||
5. **Handle errors gracefully** - return empty PluginResult on errors
|
||||
6. **Use sensible defaults** so plugins work out of the box
|
||||
|
||||
## Limitations
|
||||
|
||||
- Plugins are Python-only (no shell scripts)
|
||||
- Plugins cannot modify the HAProxy template structure
|
||||
- Domain plugins are executed for each domain, so keep them lightweight
|
||||
- Plugin errors in abort mode will halt configuration updates
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Plugin Developer Guide](plugin-development.md)
|
||||
- [Container Labels](container-labels.md)
|
||||
- [Environment Variables](environment-variable.md)
|
||||
- [Static Configuration](static.md)
|
||||
Loading…
Add table
Add a link
Reference in a new issue