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>
106 lines
4.9 KiB
Markdown
106 lines
4.9 KiB
Markdown
# AGENTS.md
|
|
|
|
Guide for AI coding agents working in this repo.
|
|
|
|
## Project
|
|
|
|
vpsmgr — a lightweight LXC hosting panel. Debian 13/Ubuntu host + Incus
|
|
containers, one Debian 13 container per user, managed via a web panel and a CLI
|
|
(single Go binary). Hosting-panel concepts, not an ordinary web app:
|
|
installers, containers, networking, storage.
|
|
|
|
## Commands (always set CGO_ENABLED=0)
|
|
|
|
The project is pure Go — no cgo, never enable it. `net` (stdlib) pulls in
|
|
`runtime/cgo` whenever a C toolchain is present, which fails on machines with
|
|
broken gcc.
|
|
|
|
```sh
|
|
cd src
|
|
CGO_ENABLED=0 go build ./...
|
|
CGO_ENABLED=0 go test ./...
|
|
CGO_ENABLED=0 go vet ./...
|
|
```
|
|
|
|
Shell scripts: check syntax with `bash -n scripts/*.sh`. Note: `install.sh`
|
|
and `00-ip-ask.sh` (the install-time network asks — IPv6 prefix + container
|
|
subnet octet) are `source`d, so they must use `return`, not `exit`.
|
|
|
|
## Incus (not LXD)
|
|
|
|
The container runtime is **Incus 7 LTS**, installed from the Zabbly package
|
|
repo (`10-incus.sh`) — never snap, never the `lxc` CLI.
|
|
|
|
- Daemon socket: `/var/lib/incus/unix.socket` (group `incus-admin`). The panel
|
|
daemon (`vps.service`) runs as the unprivileged `vps` user, which is a member
|
|
of `incus-admin`; all Incus access is via the REST API over that socket.
|
|
- `internal/lx` is the ONLY package that talks to Incus. Exec is done over the
|
|
API websocket transport (`Exec`/`ExecSH`/`RunInitScript`) — there must be no
|
|
`incus`/`lxc` CLI calls in the panel runtime path.
|
|
- Storage pool `vpsmgr` (ZFS by default, bridge `incusbr0`). `btrfs` is
|
|
supported as an alternative backend (on a btrfs host it becomes a native
|
|
subvolume; elsewhere a loop file) — `VPSMGR_STORAGE=btrfs`. A `dir` backend
|
|
exists only as an explicit `VPSMGR_STORAGE=dir` opt-in for test boxes — never
|
|
a fallback; a failed pool (ZFS/btrfs) must abort, not downgrade. Container
|
|
management is storage-driver-transparent in the Go panel; the only driver-
|
|
aware code is `lx.EnsureZFSRemoveSnapshots`, which is a no-op off ZFS.
|
|
- A remote-qualified fallback image (`images:debian/13`) must go through
|
|
`lx.EnsureImage` before `lx.Launch` — the API cannot auto-fetch it inside the
|
|
create call (unlike the old `lxc launch` CLI).
|
|
|
|
## Privilege model
|
|
|
|
- `vps install` runs as root (setup: user creation, units, kernel modules).
|
|
- `vps serve` runs as the unprivileged `vps` user. Root commands are pinned in
|
|
`/etc/sudoers.d/vps` (installed by `ensureSudoers`, validated with
|
|
`visudo -c`); the panel escalates only those exact commands via `internal/su`
|
|
(`sudo -n`). Never add a new `exec.Command("...")` that needs root without
|
|
also adding it to the whitelist.
|
|
|
|
## Documentation
|
|
|
|
- `docs/README.md` — index
|
|
- `docs/architecture.md` — system design (storage/network/security/bandwidth)
|
|
- `docs/configuration.md` — `/etc/vpsmgr/config.yaml` reference
|
|
- `docs/ipv6.md` — IPv6 pass-through design (large dev-branch feature)
|
|
- `docs/development.md` — build/test/release/conventions
|
|
|
|
Keep the top-level READMEs concise; technical detail belongs in `docs/`
|
|
(English).
|
|
|
|
## Key invariants — do not break
|
|
|
|
- `uninstall.sh` **without** `--purge` must keep `/etc/vpsmgr` (config/db)
|
|
and `/etc/haproxy` so reinstall adopts them; only `--purge` deletes them.
|
|
A pre-HAProxy install's `/etc/traefik` is kept by the same rule (its config
|
|
file is inert; the installer removes Traefik's binary/unit/account and the
|
|
per-domain dynamic directory).
|
|
- IPv6 bridge prefix is clamped to ≥ /64 (`bridgePrefixLen`): Incus's dnsmasq
|
|
only serves /64 networks. Container addresses always live in the first /64
|
|
of the configured prefix.
|
|
- IPv6 prefix length is required in `ipv6_subnet` — a bare address is
|
|
rejected, never silently treated as /64.
|
|
- Container hostnames are random (`vps-<8hex>`) and never equal the username.
|
|
- `install.sh --local-build` must always rebuild (never reuse an installed
|
|
binary) and warn about the branch. `--update` re-downloads the latest
|
|
prebuilt release over an existing binary and must keep it untouched on
|
|
download failure (no fallback rebuild).
|
|
- Image builds (`50-image.sh`, `60-rhel-image.sh`, `80-debian-dev-image.sh`)
|
|
must stay slim (apt/dnf/pacman clean) and delete the base image after
|
|
publishing. `60-rhel-image.sh`, `70-opensuse-image.sh`, `80-debian-dev-image.sh`
|
|
and `90-arch-image.sh` are optional and must never be part of `install.sh`
|
|
(small boxes stay lean). `90-arch-image.sh` is a rolling rebuild: it deletes
|
|
the existing image and re-pulls upstream on every run.
|
|
- Never add cgo or force C compilation.
|
|
- The panel service is `vps.service` (restart via `systemctl restart vps`).
|
|
Do not reintroduce `vpsmgr-*.service` names on the host. (The in-container
|
|
`vpsmgr-ipv6.service` helper is a different thing — it lives inside
|
|
container images.)
|
|
|
|
## Conventions
|
|
|
|
- One commit per small bug/feature; short subject + bullet body.
|
|
- Run `git log --oneline -10` for the current commit style before writing a
|
|
message.
|
|
- The test environment has ~2 GiB RAM; keep CI/local test workloads light.
|