1
0
Fork 0

Add standalone Python entrypoint for EasyHAProxy and pip-based installation

- Introduced `/scripts/.venv/bin/easy-haproxy` as the default entrypoint for standalone environments.
- Added `_build_parser` and `start` functions to enable CLI-driven functionality configuration.
- Updated packaging to reflect new `easyhaproxy.main` module and adapted project name from `easymapping` to `easyhaproxy`.
- Modified `Dockerfile` to align with new script paths and entrypoints for better portability.
- Enhanced documentation with dedicated sections for pip-based installation, CLI references, and systemd setup for non-Docker environments.
- Adjusted helm, plugin, and environment-related docs for accurate sidebar ordering.
- Added a new GitHub workflow to automate package publishing to PyPI.
This commit is contained in:
Joao Gilberto Magalhaes 2026-02-17 21:41:33 -05:00
parent 3805100244
commit bfd38adea5
19 changed files with 497 additions and 20 deletions

View file

@ -220,6 +220,29 @@ jobs:
# chmod +x $HOME/.docker/cli-plugins/docker-pushrm # chmod +x $HOME/.docker/cli-plugins/docker-pushrm
# docker pushrm ${{ env.IMAGE_NAME }} # docker pushrm ${{ env.IMAGE_NAME }}
Publish-PyPI:
runs-on: ubuntu-latest
needs: [Test, Tests-E2E-Docker, Tests-E2E-Kubernetes, Tests-E2E-Additional]
if: startsWith(github.ref, 'refs/tags/')
permissions:
contents: read
id-token: write # Required for OIDC trusted publisher
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Build package
run: |
export PATH="$HOME/.local/bin:$PATH"
uv build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
Helm: Helm:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
needs: Build needs: Build

View file

@ -27,7 +27,7 @@ WORKDIR /scripts
COPY build/assets / COPY build/assets /
COPY pyproject.toml uv.lock LICENSE README.md /scripts/ COPY pyproject.toml uv.lock LICENSE README.md /scripts/
COPY src/ /scripts/ COPY src/ /scripts/src/
COPY tests/ /scripts/tests/ COPY tests/ /scripts/tests/
RUN cd /scripts && uv sync --frozen RUN cd /scripts && uv sync --frozen
@ -36,4 +36,4 @@ RUN apk del .build-deps
RUN cd /scripts && uv run pytest -s -vv tests/ && uv sync --no-dev RUN cd /scripts && uv run pytest -s -vv tests/ && uv sync --no-dev
CMD ["uv", "run", "python", "-u", "/scripts/main.py"] CMD ["/scripts/.venv/bin/easy-haproxy"]

View file

@ -1,5 +1,5 @@
--- ---
sidebar_position: 10 sidebar_position: 11
--- ---
# SSL - Automatic Certificate Management Environment (ACME) # SSL - Automatic Certificate Management Environment (ACME)

View file

@ -1,5 +1,5 @@
--- ---
sidebar_position: 11 sidebar_position: 12
--- ---
# Container Labels # Container Labels

View file

@ -1,5 +1,5 @@
--- ---
sidebar_position: 8 sidebar_position: 9
--- ---
# DigitalOcean # DigitalOcean

View file

@ -1,5 +1,5 @@
--- ---
sidebar_position: 7 sidebar_position: 8
--- ---
# Dokku # Dokku

View file

@ -1,5 +1,5 @@
--- ---
sidebar_position: 12 sidebar_position: 13
--- ---
# Docker environment variables # Docker environment variables

View file

@ -1,5 +1,5 @@
--- ---
sidebar_position: 5 sidebar_position: 6
--- ---
# Helm 3 # Helm 3

View file

@ -1,5 +1,5 @@
--- ---
sidebar_position: 6 sidebar_position: 7
--- ---
# Microk8s Add-ons # Microk8s Add-ons

211
docs/pip.md Normal file
View file

@ -0,0 +1,211 @@
---
sidebar_position: 5
---
# Install via pip / uv
EasyHAProxy can run directly on any Linux or macOS host without Docker, using the `easyhaproxy` Python package.
## Prerequisites
HAProxy must be installed and available in your system `PATH` before running `easy-haproxy`. EasyHAProxy will refuse to start with a clear error message if HAProxy is not found.
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
<Tabs>
<TabItem value="debian" label="Debian / Ubuntu" default>
```bash
sudo apt install haproxy
```
</TabItem>
<TabItem value="rhel" label="RHEL / Fedora">
```bash
sudo dnf install haproxy
```
</TabItem>
<TabItem value="macos" label="macOS">
```bash
brew install haproxy
```
</TabItem>
</Tabs>
## Installation
### Recommended: `uv tool` (system-wide, isolated)
[`uv`](https://docs.astral.sh/uv/) installs `easyhaproxy` into its own isolated environment and exposes the `easy-haproxy` binary in `~/.local/bin/`, similar to `pipx`.
```bash
# Install uv (if not already installed)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Install easyhaproxy as a tool
uv tool install easyhaproxy
# Make sure ~/.local/bin is in PATH (one-time setup)
uv tool update-shell
```
After installation:
```bash
easy-haproxy --help
```
### Alternative: `pip`
```bash
pip install easyhaproxy
```
:::note Virtual environments
When installing inside a virtual environment, `easy-haproxy` is only available while the environment is activated. For system-wide use, prefer `uv tool install` or install with `pip` at the system/user level.
:::
## CLI Reference
Every configuration option can be set via a CLI flag **or** an environment variable. CLI flags take precedence over environment variables.
```
easy-haproxy [OPTIONS]
```
### Core
| Flag | Environment Variable | Default | Description |
|--------------------------|----------------------------|------------------------------------------------------|-----------------------------------------------------------|
| `--discover MODE` | `EASYHAPROXY_DISCOVER` | **required** | Discovery mode: `static`, `docker`, `swarm`, `kubernetes` |
| `--base-path PATH` | `EASYHAPROXY_BASE_PATH` | `/etc/easyhaproxy` (root) `~/easyhaproxy` (non-root) | Base directory for all EasyHAProxy files |
| `--label-prefix PREFIX` | `EASYHAPROXY_LABEL_PREFIX` | `easyhaproxy` | Label/annotation prefix used to discover services |
| `--ssl-mode MODE` | `EASYHAPROXY_SSL_MODE` | `default` | TLS policy: `strict`, `default`, or `loose` |
| `--refresh-conf SECONDS` | `EASYHAPROXY_REFRESH_CONF` | `10` | Polling interval for configuration changes |
| `--customer-errors BOOL` | `HAPROXY_CUSTOMERRORS` | `false` | Enable custom HAProxy HTML error pages |
### Logging
| Flag | Environment Variable | Default | Description |
|-----------------------------|-------------------------|----------|---------------------------|
| `--log-level LEVEL` | `EASYHAPROXY_LOG_LEVEL` | `DEBUG` | EasyHAProxy log level |
| `--haproxy-log-level LEVEL` | `HAPROXY_LOG_LEVEL` | `INFO` | HAProxy process log level |
| `--certbot-log-level LEVEL` | `CERTBOT_LOG_LEVEL` | `DEBUG` | Certbot log level |
Valid levels: `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`
### Stats Dashboard
| Flag | Environment Variable | Default | Description |
|--------------------------------------|-----------------------------|--------------|---------------------------------------------|
| `--haproxy-password PASSWORD` | `HAPROXY_PASSWORD` | *(disabled)* | Enable stats dashboard with this password |
| `--haproxy-username USERNAME` | `HAPROXY_USERNAME` | `admin` | Stats dashboard username |
| `--haproxy-stats-port PORT` | `HAPROXY_STATS_PORT` | `1936` | Stats dashboard port |
| `--haproxy-stats-cors-origin ORIGIN` | `HAPROXY_STATS_CORS_ORIGIN` | *(none)* | Allowed CORS origin for the stats dashboard |
:::tip
The stats dashboard is only enabled when `--haproxy-password` (or `HAPROXY_PASSWORD`) is set.
:::
### ACME / Certbot (SSL certificates)
| Flag | Environment Variable | Default | Description |
|---------------------------------------|--------------------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `--certbot-email EMAIL` | `EASYHAPROXY_CERTBOT_EMAIL` | *(none)* | Contact email — enables ACME when set |
| `--certbot-autoconfig CA` | `EASYHAPROXY_CERTBOT_AUTOCONFIG` | *(none)* | Well-known CA shorthand: `letsencrypt`, `letsencrypt_test`, `buypass`, `buypass_test`, `sslcom_rca`, `sslcom_ecc`, `google`, `google_test`, `zerossl` |
| `--certbot-server URL` | `EASYHAPROXY_CERTBOT_SERVER` | *(none)* | Custom ACME server directory URL |
| `--certbot-eab-kid KID` | `EASYHAPROXY_CERTBOT_EAB_KID` | *(none)* | External Account Binding key ID |
| `--certbot-eab-hmac-key KEY` | `EASYHAPROXY_CERTBOT_EAB_HMAC_KEY` | *(none)* | External Account Binding HMAC key |
| `--certbot-retry-count N` | `EASYHAPROXY_CERTBOT_RETRY_COUNT` | `60` | Iterations before retrying after a rate limit |
| `--certbot-preferred-challenges TYPE` | `EASYHAPROXY_CERTBOT_PREFERRED_CHALLENGES` | `http` | ACME challenge type |
| `--certbot-manual-auth-hook SCRIPT` | `EASYHAPROXY_CERTBOT_MANUAL_AUTH_HOOK` | *(none)* | Path to a manual auth hook script for certbot |
See the full [ACME documentation](acme.md) for details.
### Plugins
| Flag | Environment Variable | Default | Description |
|---------------------------------|--------------------------------------|----------|-------------------------------------------|
| `--plugins-enabled LIST` | `EASYHAPROXY_PLUGINS_ENABLED` | *(none)* | Comma-separated list of plugins to enable |
| `--plugins-abort-on-error BOOL` | `EASYHAPROXY_PLUGINS_ABORT_ON_ERROR` | `false` | Abort startup if a plugin fails to load |
See the [plugins documentation](plugins.md) for available plugins.
### Kubernetes
| Flag | Environment Variable | Default | Description |
|--------------------------------------------|--------------------------------------|----------|----------------------------------------------|
| `--update-ingress-status BOOL` | `EASYHAPROXY_UPDATE_INGRESS_STATUS` | `true` | Update Ingress status with load-balancer IP |
| `--deployment-mode MODE` | `EASYHAPROXY_DEPLOYMENT_MODE` | `auto` | Deployment mode: `auto`, `single`, `cluster` |
| `--external-hostname HOSTNAME` | `EASYHAPROXY_EXTERNAL_HOSTNAME` | *(none)* | External hostname reported in Ingress status |
| `--ingress-status-update-interval SECONDS` | `EASYHAPROXY_STATUS_UPDATE_INTERVAL` | `30` | Interval to update Ingress status |
## Quick-start examples
### Static mode (bare-metal / VM)
```bash
mkdir -p ~/easyhaproxy/static
cat > ~/easyhaproxy/static/config.yml <<EOF
containers:
"myapp.example.com:80":
ip: ["127.0.0.1:3000"]
EOF
easy-haproxy --discover static
```
### Static mode with stats and HTTPS redirect
```bash
easy-haproxy \
--discover static \
--haproxy-password mysecret \
--ssl-mode default \
--log-level INFO
```
### Let's Encrypt (ACME)
```bash
easy-haproxy \
--discover static \
--certbot-email admin@example.com \
--certbot-autoconfig letsencrypt
```
## Running as a systemd service
To keep `easy-haproxy` running across reboots, create a systemd unit:
```ini title="/etc/systemd/system/easy-haproxy.service"
[Unit]
Description=EasyHAProxy
After=network.target
[Service]
ExecStart=/usr/local/bin/easy-haproxy --discover static --haproxy-password mysecret
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now easy-haproxy
```
:::tip Adjust ExecStart path
Run `which easy-haproxy` to get the correct binary path for `ExecStart`. If you installed with `uv tool`, it is typically `/root/.local/bin/easy-haproxy` when running as root.
:::
----
[Open source ByJG](http://opensource.byjg.com)

View file

@ -1,5 +1,5 @@
--- ---
sidebar_position: 15 sidebar_position: 16
--- ---
# Plugin Development Guide # Plugin Development Guide

View file

@ -1,5 +1,5 @@
--- ---
sidebar_position: 14 sidebar_position: 15
--- ---
# Using Plugins # Using Plugins

View file

@ -1,5 +1,5 @@
--- ---
sidebar_position: 9 sidebar_position: 10
--- ---
# Setup custom certificates # Setup custom certificates

View file

@ -1,5 +1,5 @@
--- ---
sidebar_position: 13 sidebar_position: 14
--- ---
# Volumes # Volumes

View file

@ -3,7 +3,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[project] [project]
name = "easymapping" name = "easyhaproxy"
version = "5.0.0" version = "5.0.0"
description = "HAProxy label based routing with service discovery for Docker, Swarm, and Kubernetes" description = "HAProxy label based routing with service discovery for Docker, Swarm, and Kubernetes"
readme = "README.md" readme = "README.md"
@ -44,10 +44,10 @@ dev = [
] ]
[project.scripts] [project.scripts]
easy-haproxy = "main:main" easy-haproxy = "easyhaproxy.main:main"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/easymapping", "src/functions", "src/processor", "src/plugins", "src/templates"] packages = ["src/easyhaproxy", "src/easymapping", "src/functions", "src/processor", "src/plugins", "src/templates"]
[tool.hatch.build.targets.wheel.sources] [tool.hatch.build.targets.wheel.sources]
"src" = "" "src" = ""

View file

234
src/easyhaproxy/main.py Normal file
View file

@ -0,0 +1,234 @@
import argparse
import os
import shutil
import sys
from deepdiff import DeepDiff
from functions import (
Certbot,
Consts,
DaemonizeHAProxy,
Functions,
logger_easyhaproxy,
logger_init,
)
from processor import ProcessorInterface
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="easy-haproxy",
description="HAProxy label-based routing with service discovery for Docker, Swarm, and Kubernetes.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
# Core
parser.add_argument("--discover", metavar="MODE",
choices=["static", "docker", "swarm", "kubernetes"],
help="Service discovery mode. Also set by EASYHAPROXY_DISCOVER.")
parser.add_argument("--base-path", metavar="PATH",
help="Base directory for all EasyHAProxy files. Also set by EASYHAPROXY_BASE_PATH.")
parser.add_argument("--label-prefix", metavar="PREFIX",
help="Label/annotation prefix used to discover services. Also set by EASYHAPROXY_LABEL_PREFIX.")
parser.add_argument("--ssl-mode", metavar="MODE",
choices=["strict", "default", "loose"],
help="TLS policy: strict (TLS 1.3 only), default, or loose (all). Also set by EASYHAPROXY_SSL_MODE.")
parser.add_argument("--refresh-conf", metavar="SECONDS", type=int,
help="Interval in seconds to poll for configuration changes. Also set by EASYHAPROXY_REFRESH_CONF.")
parser.add_argument("--customer-errors", metavar="BOOL",
choices=["true", "false"],
help="Enable custom HAProxy HTML error pages. Also set by HAPROXY_CUSTOMERRORS.")
# Logging
log_levels = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"]
parser.add_argument("--log-level", metavar="LEVEL", choices=log_levels,
help="EasyHAProxy log level. Also set by EASYHAPROXY_LOG_LEVEL.")
parser.add_argument("--haproxy-log-level", metavar="LEVEL", choices=log_levels,
help="HAProxy process log level. Also set by HAPROXY_LOG_LEVEL.")
parser.add_argument("--certbot-log-level", metavar="LEVEL", choices=log_levels,
help="Certbot log level. Also set by CERTBOT_LOG_LEVEL.")
# Stats
parser.add_argument("--haproxy-password", metavar="PASSWORD",
help="Enable HAProxy stats dashboard with this password. Also set by HAPROXY_PASSWORD.")
parser.add_argument("--haproxy-username", metavar="USERNAME",
help="HAProxy stats dashboard username. Also set by HAPROXY_USERNAME.")
parser.add_argument("--haproxy-stats-port", metavar="PORT",
help="HAProxy stats dashboard port. Also set by HAPROXY_STATS_PORT.")
parser.add_argument("--haproxy-stats-cors-origin", metavar="ORIGIN",
help="Allowed CORS origin for the stats dashboard. Also set by HAPROXY_STATS_CORS_ORIGIN.")
# ACME / Certbot
parser.add_argument("--certbot-email", metavar="EMAIL",
help="Contact email for ACME/Let's Encrypt. Enables certbot when set. Also set by EASYHAPROXY_CERTBOT_EMAIL.")
parser.add_argument("--certbot-autoconfig", metavar="CA",
choices=["letsencrypt", "letsencrypt_test", "buypass", "buypass_test",
"sslcom_rca", "sslcom_ecc", "google", "google_test", "zerossl"],
help="Shorthand to configure a well-known ACME CA. Also set by EASYHAPROXY_CERTBOT_AUTOCONFIG.")
parser.add_argument("--certbot-server", metavar="URL",
help="Custom ACME server directory URL. Also set by EASYHAPROXY_CERTBOT_SERVER.")
parser.add_argument("--certbot-eab-kid", metavar="KID",
help="External Account Binding key ID (required by some CAs). Also set by EASYHAPROXY_CERTBOT_EAB_KID.")
parser.add_argument("--certbot-eab-hmac-key", metavar="KEY",
help="External Account Binding HMAC key. Also set by EASYHAPROXY_CERTBOT_EAB_HMAC_KEY.")
parser.add_argument("--certbot-retry-count", metavar="N", type=int,
help="Iterations before retrying after a rate limit. Also set by EASYHAPROXY_CERTBOT_RETRY_COUNT.")
parser.add_argument("--certbot-preferred-challenges", metavar="TYPE",
help="ACME challenge type (default: http). Also set by EASYHAPROXY_CERTBOT_PREFERRED_CHALLENGES.")
parser.add_argument("--certbot-manual-auth-hook", metavar="SCRIPT",
help="Path to manual auth hook script for certbot. Also set by EASYHAPROXY_CERTBOT_MANUAL_AUTH_HOOK.")
# Plugins
parser.add_argument("--plugins-enabled", metavar="LIST",
help="Comma-separated list of plugins to enable. Also set by EASYHAPROXY_PLUGINS_ENABLED.")
parser.add_argument("--plugins-abort-on-error", metavar="BOOL",
choices=["true", "false"],
help="Abort startup if a plugin fails to load. Also set by EASYHAPROXY_PLUGINS_ABORT_ON_ERROR.")
# Kubernetes
parser.add_argument("--update-ingress-status", metavar="BOOL",
choices=["true", "false"],
help="Update Kubernetes Ingress status with load-balancer IP. Also set by EASYHAPROXY_UPDATE_INGRESS_STATUS.")
parser.add_argument("--deployment-mode", metavar="MODE",
choices=["auto", "single", "cluster"],
help="Kubernetes deployment mode. Also set by EASYHAPROXY_DEPLOYMENT_MODE.")
parser.add_argument("--external-hostname", metavar="HOSTNAME",
help="External hostname reported in Ingress status. Also set by EASYHAPROXY_EXTERNAL_HOSTNAME.")
parser.add_argument("--ingress-status-update-interval", metavar="SECONDS", type=int,
help="Interval in seconds to update Ingress status. Also set by EASYHAPROXY_STATUS_UPDATE_INTERVAL.")
return parser
def _apply_args_to_env(args: argparse.Namespace) -> None:
"""Write non-None CLI arguments into os.environ so the rest of the code reads them."""
mapping = {
"discover": "EASYHAPROXY_DISCOVER",
"base_path": "EASYHAPROXY_BASE_PATH",
"label_prefix": "EASYHAPROXY_LABEL_PREFIX",
"ssl_mode": "EASYHAPROXY_SSL_MODE",
"refresh_conf": "EASYHAPROXY_REFRESH_CONF",
"customer_errors": "HAPROXY_CUSTOMERRORS",
"log_level": "EASYHAPROXY_LOG_LEVEL",
"haproxy_log_level": "HAPROXY_LOG_LEVEL",
"certbot_log_level": "CERTBOT_LOG_LEVEL",
"haproxy_password": "HAPROXY_PASSWORD",
"haproxy_username": "HAPROXY_USERNAME",
"haproxy_stats_port": "HAPROXY_STATS_PORT",
"haproxy_stats_cors_origin": "HAPROXY_STATS_CORS_ORIGIN",
"certbot_email": "EASYHAPROXY_CERTBOT_EMAIL",
"certbot_autoconfig": "EASYHAPROXY_CERTBOT_AUTOCONFIG",
"certbot_server": "EASYHAPROXY_CERTBOT_SERVER",
"certbot_eab_kid": "EASYHAPROXY_CERTBOT_EAB_KID",
"certbot_eab_hmac_key": "EASYHAPROXY_CERTBOT_EAB_HMAC_KEY",
"certbot_retry_count": "EASYHAPROXY_CERTBOT_RETRY_COUNT",
"certbot_preferred_challenges": "EASYHAPROXY_CERTBOT_PREFERRED_CHALLENGES",
"certbot_manual_auth_hook": "EASYHAPROXY_CERTBOT_MANUAL_AUTH_HOOK",
"plugins_enabled": "EASYHAPROXY_PLUGINS_ENABLED",
"plugins_abort_on_error": "EASYHAPROXY_PLUGINS_ABORT_ON_ERROR",
"update_ingress_status": "EASYHAPROXY_UPDATE_INGRESS_STATUS",
"deployment_mode": "EASYHAPROXY_DEPLOYMENT_MODE",
"external_hostname": "EASYHAPROXY_EXTERNAL_HOSTNAME",
"ingress_status_update_interval": "EASYHAPROXY_STATUS_UPDATE_INTERVAL",
}
for arg_name, env_name in mapping.items():
value = getattr(args, arg_name, None)
if value is not None:
os.environ[env_name] = str(value)
def start():
processor_obj = ProcessorInterface.factory(os.getenv("EASYHAPROXY_DISCOVER"))
if processor_obj is None:
exit(1)
os.makedirs(Consts.certs_certbot, exist_ok=True)
os.makedirs(Consts.certs_haproxy, exist_ok=True)
processor_obj.save_config(Consts.haproxy_config)
processor_obj.save_certs(Consts.certs_haproxy)
certbot_certs_found = processor_obj.get_certbot_hosts()
logger_easyhaproxy.info(f'Found hosts: {", ".join(processor_obj.get_hosts())}') # Needs to run after save_config
logger_easyhaproxy.debug(f'Object Found: {processor_obj.get_parsed_object()}')
old_haproxy = None
haproxy = DaemonizeHAProxy()
current_custom_config_files = haproxy.get_custom_config_files()
haproxy.haproxy(DaemonizeHAProxy.HAPROXY_START)
haproxy.sleep()
certbot = Certbot(Consts.certs_certbot)
# Check ACME environment readiness if Certbot is configured
if certbot.email != "":
is_ready, error_msg = Certbot.check_acme_environment_ready(certbot.email, certbot.acme_server)
if not is_ready:
logger_easyhaproxy.warning(f"ACME environment not ready: {error_msg}")
logger_easyhaproxy.warning("Certificate auto-renewal may fail. Verify ACME server configuration.")
else:
logger_easyhaproxy.info("ACME environment validated and ready")
while True:
if old_haproxy is not None:
old_haproxy.kill()
old_haproxy = None
try:
old_parsed = processor_obj.get_parsed_object()
processor_obj.refresh()
if certbot.check_certificates(certbot_certs_found) or DeepDiff(old_parsed, processor_obj.get_parsed_object()) != {} or not haproxy.is_alive() or DeepDiff(current_custom_config_files, haproxy.get_custom_config_files()) != {}:
logger_easyhaproxy.info('New configuration found. Reloading...')
logger_easyhaproxy.debug(f'Object Found: {processor_obj.get_parsed_object()}')
processor_obj.save_config(Consts.haproxy_config)
processor_obj.save_certs(Consts.certs_haproxy)
certbot_certs_found = processor_obj.get_certbot_hosts()
logger_easyhaproxy.info(f'Found hosts: {", ".join(processor_obj.get_hosts())}') # Needs to after save_config
old_haproxy = haproxy
haproxy = DaemonizeHAProxy()
current_custom_config_files = haproxy.get_custom_config_files()
haproxy.haproxy(DaemonizeHAProxy.HAPROXY_RELOAD)
old_haproxy.terminate()
except Exception as e:
logger_easyhaproxy.fatal(f"Err: {e}")
logger_easyhaproxy.info('Heartbeat')
haproxy.sleep()
def main():
haproxy_bin = shutil.which('haproxy')
if haproxy_bin is None:
print("ERROR: HAProxy is not installed or not in PATH.")
print("Please install HAProxy before running easy-haproxy.")
print(" Debian/Ubuntu: sudo apt install haproxy")
print(" RHEL/Fedora: sudo dnf install haproxy")
print(" macOS: brew install haproxy")
sys.exit(1)
args = _build_parser().parse_args()
_apply_args_to_env(args)
# Reset cached base_path so it re-evaluates after --base-path may have been applied
Consts.reset()
Functions.run_bash(logger_init, f'{haproxy_bin} -v')
logger_init.info(r".........................__.....................................")
logger_init.info(r"..___ ____ ________ __/ /_ ____ _____ _________ _ ____ __")
logger_init.info(r"./ _ \/ __ `/ ___/ / / / __ \/ __ `/ __ \/ ___/ __ \| |/_/ / / /")
logger_init.info(r"/ __/ /_/ (__ ) /_/ / / / / /_/ / /_/ / / / /_/ /> </ /_/ /.")
logger_init.info(r"\___/\__,_/____/\__, /_/ /_/\__,_/ .___/_/ \____/_/|_|\__, /..")
logger_init.info(r".............../____/.........../_/..................../____/...")
logger_init.info(f"Release: {os.getenv('RELEASE_VERSION')}")
logger_init.debug('Environment:')
for name, value in os.environ.items():
if "HAPROXY" in name:
logger_init.debug(f"- {name}: {value}")
start()
if __name__ == '__main__':
main()

View file

@ -1,11 +1,13 @@
import logging import logging
import os import os
import shlex import shlex
import shutil
import subprocess import subprocess
import sys import sys
import time import time
from datetime import datetime from datetime import datetime
from multiprocessing import Process from multiprocessing import Process
from pathlib import Path
from typing import Final from typing import Final
import psutil import psutil
@ -291,7 +293,13 @@ class Consts:
def base_path(cls): def base_path(cls):
"""Base directory for all EasyHAProxy files.""" """Base directory for all EasyHAProxy files."""
if cls._base_path is None: if cls._base_path is None:
cls._base_path = os.getenv("EASYHAPROXY_BASE_PATH", "/etc/easyhaproxy") if os.getenv("EASYHAPROXY_BASE_PATH"):
default = os.getenv("EASYHAPROXY_BASE_PATH")
elif os.getuid() == 0:
default = "/etc/easyhaproxy"
else:
default = str(Path.home() / "easyhaproxy")
cls._base_path = default
return cls._base_path return cls._base_path
@classmethod @classmethod
@ -347,17 +355,18 @@ class DaemonizeHAProxy:
self.thread.start() self.thread.start()
def get_haproxy_command(self, action, pid_file="/run/haproxy.pid"): def get_haproxy_command(self, action, pid_file="/run/haproxy.pid"):
haproxy_bin = shutil.which('haproxy') or '/usr/sbin/haproxy'
custom_config_files = "" custom_config_files = ""
if len(list(self.get_custom_config_files().keys())) != 0: if len(list(self.get_custom_config_files().keys())) != 0:
custom_config_files = f"-f {self.custom_config_folder}" custom_config_files = f"-f {self.custom_config_folder}"
if action == DaemonizeHAProxy.HAPROXY_START or not os.path.exists(pid_file): if action == DaemonizeHAProxy.HAPROXY_START or not os.path.exists(pid_file):
return f"/usr/sbin/haproxy -W -f {Consts.haproxy_config} {custom_config_files} -p {pid_file} -S /var/run/haproxy.sock" return f"{haproxy_bin} -W -f {Consts.haproxy_config} {custom_config_files} -p {pid_file} -S /var/run/haproxy.sock"
else: else:
return_code, output = Functions().run_bash(logger_haproxy, f"cat {pid_file}", log_output=False) return_code, output = Functions().run_bash(logger_haproxy, f"cat {pid_file}", log_output=False)
pid = "".join(output).rstrip() pid = "".join(output).rstrip()
if psutil.pid_exists(int(pid)): if psutil.pid_exists(int(pid)):
return f"/usr/sbin/haproxy -W -f {Consts.haproxy_config} {custom_config_files} -p {pid_file} -x /var/run/haproxy.sock -sf {pid}" return f"{haproxy_bin} -W -f {Consts.haproxy_config} {custom_config_files} -p {pid_file} -x /var/run/haproxy.sock -sf {pid}"
else: else:
os.unlink(pid_file) os.unlink(pid_file)
logger_haproxy.warning( logger_haproxy.warning(

2
uv.lock generated
View file

@ -350,7 +350,7 @@ wheels = [
] ]
[[package]] [[package]]
name = "easymapping" name = "easyhaproxy"
version = "5.0.0" version = "5.0.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [