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:
parent
3805100244
commit
bfd38adea5
19 changed files with 497 additions and 20 deletions
23
.github/workflows/build.yml
vendored
23
.github/workflows/build.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
sidebar_position: 10
|
||||
sidebar_position: 11
|
||||
---
|
||||
|
||||
# SSL - Automatic Certificate Management Environment (ACME)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
sidebar_position: 11
|
||||
sidebar_position: 12
|
||||
---
|
||||
|
||||
# Container Labels
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
sidebar_position: 8
|
||||
sidebar_position: 9
|
||||
---
|
||||
|
||||
# DigitalOcean
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
sidebar_position: 7
|
||||
sidebar_position: 8
|
||||
---
|
||||
|
||||
# Dokku
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
sidebar_position: 12
|
||||
sidebar_position: 13
|
||||
---
|
||||
|
||||
# Docker environment variables
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
sidebar_position: 5
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
# Helm 3
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
sidebar_position: 6
|
||||
sidebar_position: 7
|
||||
---
|
||||
|
||||
# Microk8s Add-ons
|
||||
|
|
|
|||
211
docs/pip.md
Normal file
211
docs/pip.md
Normal 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)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
sidebar_position: 15
|
||||
sidebar_position: 16
|
||||
---
|
||||
|
||||
# Plugin Development Guide
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
sidebar_position: 14
|
||||
sidebar_position: 15
|
||||
---
|
||||
|
||||
# Using Plugins
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
sidebar_position: 9
|
||||
sidebar_position: 10
|
||||
---
|
||||
|
||||
# Setup custom certificates
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
sidebar_position: 13
|
||||
sidebar_position: 14
|
||||
---
|
||||
|
||||
# Volumes
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ requires = ["hatchling"]
|
|||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "easymapping"
|
||||
name = "easyhaproxy"
|
||||
version = "5.0.0"
|
||||
description = "HAProxy label based routing with service discovery for Docker, Swarm, and Kubernetes"
|
||||
readme = "README.md"
|
||||
|
|
@ -44,10 +44,10 @@ dev = [
|
|||
]
|
||||
|
||||
[project.scripts]
|
||||
easy-haproxy = "main:main"
|
||||
easy-haproxy = "easyhaproxy.main:main"
|
||||
|
||||
[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]
|
||||
"src" = ""
|
||||
|
|
|
|||
0
src/easyhaproxy/__init__.py
Normal file
0
src/easyhaproxy/__init__.py
Normal file
234
src/easyhaproxy/main.py
Normal file
234
src/easyhaproxy/main.py
Normal 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()
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
2
uv.lock
generated
2
uv.lock
generated
|
|
@ -350,7 +350,7 @@ wheels = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "easymapping"
|
||||
name = "easyhaproxy"
|
||||
version = "5.0.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue