diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ddc6084..433a875 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -220,6 +220,29 @@ jobs:
# chmod +x $HOME/.docker/cli-plugins/docker-pushrm
# 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:
runs-on: 'ubuntu-latest'
needs: Build
diff --git a/build/Dockerfile b/build/Dockerfile
index 55d225a..feb3676 100644
--- a/build/Dockerfile
+++ b/build/Dockerfile
@@ -27,7 +27,7 @@ WORKDIR /scripts
COPY build/assets /
COPY pyproject.toml uv.lock LICENSE README.md /scripts/
-COPY src/ /scripts/
+COPY src/ /scripts/src/
COPY tests/ /scripts/tests/
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
-CMD ["uv", "run", "python", "-u", "/scripts/main.py"]
+CMD ["/scripts/.venv/bin/easy-haproxy"]
diff --git a/docs/acme.md b/docs/acme.md
index f290306..b75dbc5 100644
--- a/docs/acme.md
+++ b/docs/acme.md
@@ -1,5 +1,5 @@
---
-sidebar_position: 10
+sidebar_position: 11
---
# SSL - Automatic Certificate Management Environment (ACME)
diff --git a/docs/container-labels.md b/docs/container-labels.md
index a87eea7..c2ab3ac 100644
--- a/docs/container-labels.md
+++ b/docs/container-labels.md
@@ -1,5 +1,5 @@
---
-sidebar_position: 11
+sidebar_position: 12
---
# Container Labels
diff --git a/docs/digitalocean.md b/docs/digitalocean.md
index de48e9f..aab938f 100644
--- a/docs/digitalocean.md
+++ b/docs/digitalocean.md
@@ -1,5 +1,5 @@
---
-sidebar_position: 8
+sidebar_position: 9
---
# DigitalOcean
diff --git a/docs/dokku.md b/docs/dokku.md
index 9b5490c..c995b5d 100644
--- a/docs/dokku.md
+++ b/docs/dokku.md
@@ -1,5 +1,5 @@
---
-sidebar_position: 7
+sidebar_position: 8
---
# Dokku
diff --git a/docs/environment-variable.md b/docs/environment-variable.md
index 5b4a7e5..16dabc1 100644
--- a/docs/environment-variable.md
+++ b/docs/environment-variable.md
@@ -1,5 +1,5 @@
---
-sidebar_position: 12
+sidebar_position: 13
---
# Docker environment variables
diff --git a/docs/helm.md b/docs/helm.md
index 9e61d5e..055f938 100644
--- a/docs/helm.md
+++ b/docs/helm.md
@@ -1,5 +1,5 @@
---
-sidebar_position: 5
+sidebar_position: 6
---
# Helm 3
diff --git a/docs/microk8s.md b/docs/microk8s.md
index 316cf1a..7326a33 100644
--- a/docs/microk8s.md
+++ b/docs/microk8s.md
@@ -1,5 +1,5 @@
---
-sidebar_position: 6
+sidebar_position: 7
---
# Microk8s Add-ons
diff --git a/docs/pip.md b/docs/pip.md
new file mode 100644
index 0000000..f0b9b64
--- /dev/null
+++ b/docs/pip.md
@@ -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';
+
+
+
+
+```bash
+sudo apt install haproxy
+```
+
+
+
+
+```bash
+sudo dnf install haproxy
+```
+
+
+
+
+```bash
+brew install haproxy
+```
+
+
+
+
+## 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 < 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()
\ No newline at end of file
diff --git a/src/functions/__init__.py b/src/functions/__init__.py
index 7030186..62cf6f8 100644
--- a/src/functions/__init__.py
+++ b/src/functions/__init__.py
@@ -1,11 +1,13 @@
import logging
import os
import shlex
+import shutil
import subprocess
import sys
import time
from datetime import datetime
from multiprocessing import Process
+from pathlib import Path
from typing import Final
import psutil
@@ -291,7 +293,13 @@ class Consts:
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")
+ 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
@classmethod
@@ -347,17 +355,18 @@ class DaemonizeHAProxy:
self.thread.start()
def get_haproxy_command(self, action, pid_file="/run/haproxy.pid"):
+ haproxy_bin = shutil.which('haproxy') or '/usr/sbin/haproxy'
custom_config_files = ""
if len(list(self.get_custom_config_files().keys())) != 0:
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 {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:
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 {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:
os.unlink(pid_file)
logger_haproxy.warning(
diff --git a/uv.lock b/uv.lock
index c26c943..970a3c8 100644
--- a/uv.lock
+++ b/uv.lock
@@ -350,7 +350,7 @@ wheels = [
]
[[package]]
-name = "easymapping"
+name = "easyhaproxy"
version = "5.0.0"
source = { editable = "." }
dependencies = [