Files
vpsmgr/upgrade-haproxy.sh
bluebluesodaandCommandCodeBot 4c93ae4be3 feat(haproxy): replace Traefik with a pinned HAProxy domain proxy
The panel now fronts user domains with HAProxy 3.4 LTS (official performance
packages) instead of Traefik: about 100 MB of RSS and 200 MB of disk saved for
a feature set that never used more than Host routing on :80 and SNI passthrough
on :443.

- hpx: publish the proxy configuration as immutable GENERATIONS. Every domain
  mutation renders the whole set from the DB, validates it with the pinned
  `haproxy -c`, and only then repoints the `current` symlink, so a rejected
  candidate leaves the running proxy untouched and a stale domain can never
  survive into a later generation. Routing is carried by two map files
  (Host -> backend, SNI -> backend): per-domain frontend rules cannot work from
  a second file because HAProxy rejects duplicate section names since 3.3 and
  attaches bare directives to the section opened last.
- mgr: republish and reload (SIGUSR2, i.e. `systemctl reload`) after every
  domain change, so a change takes effect without dropping established HTTP or
  TLS connections. Rollback semantics (DB first, then publish) are unchanged.
- cfg,db: `net.traefik` is renamed to `net.haproxy` with identical values. A
  pre-rename config.yaml key and the legacy VPSMGR_TRAEFIK env var are still
  read (new name wins), and schema migration v18 moves the settings mirror from
  `traefik` to `haproxy`, keeping a newer value when both rows exist.
- 30-haproxy.sh: install the pinned branch from the vendor repository and
  migrate a Traefik host in one run — install and validate HAProxy first, then
  remove Traefik's binary, unit, service account and dynamic rules (only its
  config file is kept, inert). No fallback keeps both proxies alive.
- upgrade-haproxy.sh: follow the pinned branch to its newest patch, with a
  configuration check and a graceful reload; `install.sh --update` runs it.
- tests: generation publish/rollback, map rendering, config-key and settings
  migrations, plus real-binary tests (skipped unless
  VPSMGR_TEST_HAPROXY_BIN is set) that run the generated configuration through
  `haproxy -c` and start it in a network namespace.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-09-16 12:59:13 +00:00

144 lines
6.5 KiB
Bash

#!/usr/bin/env bash
# upgrade-haproxy.sh — follow the pinned HAProxy branch to its newest patch.
#
# Usage:
# ./upgrade-haproxy.sh # upgrade if a newer patch exists
# ./upgrade-haproxy.sh --check # report the current/available versions, change nothing
#
# WHY A SEPARATE SCRIPT
# ---------------------------------------------------------------------------
# vpsmgr pins the HAProxy MAJOR BRANCH (3.4 today) but deliberately follows the
# newest PATCH within it: patch releases only carry fixes, never behaviour
# changes, and staying current keeps security fixes flowing without touching
# the configuration semantics the panel generates.
#
# `install.sh --update` upgrades the panel binary AND calls this script, so a
# normal upgrade keeps both in step. Run it on its own to pick up an HAProxy
# patch without touching the panel.
#
# UPGRADING ACROSS BRANCHES IS A DELIBERATE, MANUAL EDIT
# ---------------------------------------------------------------------------
# The branch lives in ONE place: HAPROXY_BRANCH below (mirrored by
# 30-haproxy.sh and cfg.HaproxyBranch in the panel — change all three). This
# script never moves to another branch on its own; it only moves within the
# pinned one, and refuses to continue if the installed binary is not on it.
# ---------------------------------------------------------------------------
set -euo pipefail
log(){ echo "[upgrade] $*"; }
die(){ echo "[upgrade] error: $*" >&2; exit 1; }
CHECK_ONLY=0
[[ "${1:-}" == "--check" ]] && CHECK_ONLY=1
# --- pinned branch (see the header) ---------------------------------------
HAPROXY_BRANCH="3.4"
HAPROXY_BRANCH_RE='^3\.4\.'
HAPROXY_REPO_SLUG="ha34"
HAPROXY_PKG="haproxy-awslc"
HAPROXY_DIR="/etc/haproxy"
ENTRY_CFG="$HAPROXY_DIR/haproxy.cfg"
BACKUP_DIR="/etc/vpsmgr/haproxy-backup"
[[ $EUID -eq 0 ]] || die "must run as root"
# --- resolve the distribution keys of the vendor repository ---------------
OS_ID="${ID:-}"; OS_CODENAME="${VERSION_CODENAME:-}"
if [[ -r /etc/os-release ]]; then
# shellcheck disable=SC1091
. /etc/os-release
OS_ID="${ID:-}"; OS_CODENAME="${VERSION_CODENAME:-}"
fi
[[ -n "$OS_CODENAME" ]] || die "could not determine VERSION_CODENAME"
installed_version(){
# "HAProxy version 3.4.4-0+ha34+deb13u1 2026/08/27" -> 3.4.4-0+ha34+deb13u1
[[ -x /usr/sbin/haproxy ]] || { echo ""; return; }
/usr/sbin/haproxy -v 2>/dev/null | awk 'NR==1{print $3}'
}
current="$(installed_version)"
[[ -n "$current" ]] || die "HAProxy is not installed at /usr/sbin/haproxy (run ./install.sh first)"
# An install still on the pre-HAProxy branch is a BRANCH change, not a patch
# upgrade: refuse rather than silently migrating the domain proxy's version.
if [[ ! "$current" =~ $HAPROXY_BRANCH_RE ]]; then
die "installed HAProxy '$current' is not on the pinned branch $HAPROXY_BRANCH.
A branch change is a deliberate migration: update HAPROXY_BRANCH /
HAPROXY_BRANCH_RE / HAPROXY_REPO_SLUG here, in scripts/30-haproxy.sh and
in src/internal/cfg (HaproxyBranch), then re-run ./install.sh."
fi
# --- make sure the apt source points at the pinned branch ------------------
src="/etc/apt/sources.list.d/haproxy.list"
want="deb [signed-by=/usr/share/keyrings/HAPROXY-key-community.asc] https://www.haproxy.com/download/haproxy/performance/$OS_ID/$HAPROXY_REPO_SLUG $OS_CODENAME main"
if [[ ! -f "$src" ]] || [[ "$(cat "$src")" != "$want" ]]; then
log "fixing the HAProxy apt source (it did not point at branch $HAPROXY_BRANCH)"
# Leftover lists from another branch must not be merged into this one.
rm -f /var/lib/apt/lists/*haproxy* 2>/dev/null || true
printf '%s\n' "$want" > "$src"
chmod 0644 "$src"
fi
apt-get update -qq || die "apt-get update failed (check the HAProxy repository)"
candidate="$(apt-cache policy "$HAPROXY_PKG" 2>/dev/null | awk '/Candidate:/{print $2}')"
[[ -n "$candidate" ]] || die "no $HAPROXY_PKG candidate in the configured repository"
log "installed: $current"
log "candidate: $candidate"
if [[ "$CHECK_ONLY" == "1" ]]; then
exit 0
fi
# Nothing to do when the candidate is not newer (or is unknown to dpkg, which
# happens when the installed build came from another source).
if dpkg --compare-versions "$candidate" le "$current" 2>/dev/null; then
log "already on the newest $HAPROXY_BRANCH patch — nothing to do"
exit 0
fi
# --- back up before changing anything -------------------------------------
# The panel can rebuild every generation from the database, but the STATIC
# entry config and the unit are vpsmgr-owned files that a package upgrade could
# touch, so keep a copy to diff against if something goes wrong.
mkdir -p "$BACKUP_DIR"
cp -a "$ENTRY_CFG" "$BACKUP_DIR/haproxy.cfg.$current" 2>/dev/null || true
cp -a /etc/systemd/system/haproxy.service "$BACKUP_DIR/haproxy.service.$current" 2>/dev/null || true
log "backup: $BACKUP_DIR (entry config + unit as of $current)"
# --- upgrade, then prove the result --------------------------------------
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --only-upgrade "$HAPROXY_PKG" \
|| die "package upgrade failed — the previous version is still installed and untouched"
new="$(installed_version)"
if [[ ! "$new" =~ $HAPROXY_BRANCH_RE ]]; then
die "upgraded binary reports '$new', which is not on branch $HAPROXY_BRANCH"
fi
log "upgraded: $current -> $new"
# The candidate configuration must still be valid under the new binary before
# it is allowed to serve traffic.
if ! /usr/sbin/haproxy -c -q -f "$ENTRY_CFG"; then
log "configuration check FAILED under $new — rolling the package back to $current"
apt-get install -y -qq --allow-downgrades "$HAPROXY_PKG=$current" || log "warn: rollback failed, fix by hand"
die "HAProxy upgrade rolled back (the new binary rejected the configuration)"
fi
log "configuration check passed under $new"
# --- graceful reload: existing connections are NOT dropped ----------------
# SIGUSR2 tells the master to load the new binary + configuration into a fresh
# worker; the old worker drains its established connections and exits. A
# `systemctl restart` would kill them, so it is never used here.
if systemctl is-active --quiet haproxy.service; then
systemctl reload haproxy.service || die "graceful reload failed — service left as it was"
sleep 1
systemctl is-active --quiet haproxy.service || die "service is not active after the reload"
log "haproxy reloaded without dropping established connections"
else
# Installed but intentionally disabled (net.haproxy false, or 80/443 taken):
# do not start it behind the operator's back.
log "haproxy is not running — binary upgraded, service left stopped"
fi
log "done: HAProxy $new on branch $HAPROXY_BRANCH"