Files
vpsmgr/docs/development.md
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

4.5 KiB

Development

Building

./build.sh [VERSION] compiles the Go binary into bin/vps. It requires any Go that supports toolchain auto-switch (≥ 1.21); src/go.mod pins the exact toolchain (go1.26.5).

./build.sh                 # current OS/arch
GOOS=linux GOARCH=arm64 ./build.sh   # cross-compile

Always CGO_ENABLED=0 (build.sh and the release workflow do this). The project is pure Go with no cgo; leaving cgo on pulls in Go's runtime/cgo bootstrap and a C toolchain whenever the standard net package is built, which fails on machines with a broken/absent gcc. Release binaries are therefore static and toolchain-free.

Testing

cd src
CGO_ENABLED=0 go vet ./...
CGO_ENABLED=0 go test ./...

The CI release workflow also runs vet+test with CGO_ENABLED=0 on amd64.

Developing against Incus

The panel talks to Incus over its Unix socket (/var/lib/incus/unix.socket, group incus-admin). For a local dev loop:

# add the Zabbly LTS repo and install Incus (Debian 12/13, Ubuntu 22.04+)
curl -fsSL https://pkgs.zabbly.com/key.asc -o /etc/apt/keyrings/zabbly.asc
echo 'deb [signed-by=/etc/apt/keyrings/zabbly.asc] https://pkgs.zabbly.com/incus/lts-7.0/ trixie main' > /etc/apt/sources.list.d/zabbly-incus.list
apt-get update && apt-get install -y incus-base   # container-only build (no VM stack);
# install the full `incus` package only if you also develop against VMs
incus admin init --preseed < ...   # or use scripts/10-incus.sh
usermod -aG incus-admin "$USER"    # socket access without root

vps install does all of this on a real host; the exec API is exercised by internal/lx over websockets (no incus CLI needed at runtime).

Releasing

Tag a version (v*); the .github/workflows/release.yml release action builds amd64/arm64 with CGO_ENABLED=0, runs vet+test, attests build provenance (SLSA) and uploads a release with checksums. install.sh downloads the prebuilt binary from the latest release, falling back to a local build.

The release page's notes are the tag's own annotation — write them at tag time with an annotated tag:

git tag -a v1.8.0 -m "知识库 + 快照分享

- 管理员可编写 Markdown 知识库文章,用户端可阅读
- 用户可分享检查点,他人凭分享码重装"
git push origin v1.8.0

A lightweight tag (git tag v1.8.0, no -a) carries no message, so the release body falls back to fix some bugs. Either way GitHub appends its auto-generated "Full changelog" compare link below the notes.

Conventions

  • Small, single-purpose commits — one commit per bug/feature.
  • Commit messages follow the existing style (short subject, then a bullet list of what and why).
  • Shell scripts: set -uo pipefail, helpers log/die/warn, bash -n checked, Python one-liners used only where the stdlib is enough.
  • The panel templates embed two languages (zh / en) via {{if eq .Lang "zh"}}...{{else}}...{{end}}.

Repository layout

install.sh / uninstall.sh / build.sh   # lifecycle + local build
upgrade-haproxy.sh                    # follow the pinned HAProxy branch (patches)
scripts/   00-check 10-incus 20-network 30-haproxy 40-panel 50-image 60-rhel 70-opensuse 80-debian-dev 90-arch
configs/   reference configs (haproxy / systemd / sudoers)
docs/      this documentation
src/       Go source (single binary: CLI + panel)

Domain proxy (HAProxy) — pinned branch

The proxy is HAProxy 3.4 LTS from the vendors' performance packages (haproxy-awslc). The project pins the BRANCH and follows its newest patch.

The branch appears in exactly three places — change all of them together:

Where Constant
scripts/30-haproxy.sh HAPROXY_BRANCH / HAPROXY_BRANCH_RE / HAPROXY_REPO_SLUG
upgrade-haproxy.sh the same three
src/internal/cfg/cfg.go HaproxyBranch

install.sh --update upgrades the panel binary and then runs upgrade-haproxy.sh, which validates the candidate configuration with haproxy -c before reloading (systemctl reload, never restart, so established connections drain instead of dropping).

Runtime configuration is published as immutable generations (internal/hpx): render the whole set from the DB, validate, then atomically repoint /etc/haproxy/current. Never edit a generation in place, and never write domain-specific settings into /etc/haproxy/haproxy.cfg (the static entry config a reload cannot change).

The panel's internal/hpx renderer and configs/haproxy/proxy.cfg (the bootstrap generation used before the first sync) are two copies of the same template — keep them in step.