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
|
# 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
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
sidebar_position: 10
|
sidebar_position: 11
|
||||||
---
|
---
|
||||||
|
|
||||||
# SSL - Automatic Certificate Management Environment (ACME)
|
# SSL - Automatic Certificate Management Environment (ACME)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
sidebar_position: 11
|
sidebar_position: 12
|
||||||
---
|
---
|
||||||
|
|
||||||
# Container Labels
|
# Container Labels
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
sidebar_position: 8
|
sidebar_position: 9
|
||||||
---
|
---
|
||||||
|
|
||||||
# DigitalOcean
|
# DigitalOcean
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
sidebar_position: 7
|
sidebar_position: 8
|
||||||
---
|
---
|
||||||
|
|
||||||
# Dokku
|
# Dokku
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
sidebar_position: 12
|
sidebar_position: 13
|
||||||
---
|
---
|
||||||
|
|
||||||
# Docker environment variables
|
# Docker environment variables
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
sidebar_position: 5
|
sidebar_position: 6
|
||||||
---
|
---
|
||||||
|
|
||||||
# Helm 3
|
# Helm 3
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
sidebar_position: 6
|
sidebar_position: 7
|
||||||
---
|
---
|
||||||
|
|
||||||
# Microk8s Add-ons
|
# 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
|
# Plugin Development Guide
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
sidebar_position: 14
|
sidebar_position: 15
|
||||||
---
|
---
|
||||||
|
|
||||||
# Using Plugins
|
# Using Plugins
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
sidebar_position: 9
|
sidebar_position: 10
|
||||||
---
|
---
|
||||||
|
|
||||||
# Setup custom certificates
|
# Setup custom certificates
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
sidebar_position: 13
|
sidebar_position: 14
|
||||||
---
|
---
|
||||||
|
|
||||||
# Volumes
|
# Volumes
|
||||||
|
|
|
||||||
|
|
@ -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" = ""
|
||||||
|
|
|
||||||
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 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
2
uv.lock
generated
|
|
@ -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 = [
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue