feat: native Alpine Linux (OpenRC/busybox) support#23
Conversation
… helpers Add is_alpine(), a _privileged() wrapper (skips sudo when already root, as on Alpine images), and service_command/service_enable_command/service_running helpers that dispatch to rc-service/rc-update on Alpine and systemctl elsewhere. Add ApkPackageManager with canonical->apk package aliases, and route get_package_manager() to it on Alpine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MariaDB: install mariadb/mariadb-client via apk, initialise the empty data dir with mariadb-install-db and enable the service; start/stop via service_command (rc-service on Alpine, systemctl elsewhere). Node.js: install nodejs/npm via apk on Alpine instead of the nodesource apt repo. nginx: default config dir to /etc/nginx/http.d on Alpine, start/enable the service on first reload, and use _privileged for symlink + nginx -t so it works as root without sudo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add OpenRCProcessManager, which renders one supervise-daemon init script per bench process into config/openrc/, symlinks them into /etc/init.d/, and enables them in the default runlevel. Add a shared ProcessManager._split_command helper to map Procfile entries onto init-script env/directory/command fields. Wire 'openrc' through ProductionConfig, the ProcessManagerFactory (create + detect_running), and the init / setup production dispatch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bench init installs the full musl build header set (linux-headers, libffi-dev, openssl-dev, libxml2/xslt, image libs) plus bash and tzdata so frappe's wheels compile and its runtime assumptions hold on Alpine. install.sh is now POSIX sh (runs under Alpine's busybox ash with no bash preinstalled), apk-installs its own base deps on Alpine, tolerates a non-tracking clone on re-run, and writes PATH to ~/.profile for ash/sh shells. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Alpine ships /etc/nginx/http.d/default.conf with a 'listen 80/[::]:80 default_server' that returns 404 to block stray virtualhosts. It sits in the same include dir bench uses and owns the IPv6 default_server, so it shadows the bench site for IPv6 clients (e.g. curl localhost -> ::1). Remove it on Alpine so the bench server block is the sole :80 listener. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bench status hardcoded a systemd/supervisor label and shelled out to 'systemctl is-active' for nginx, which errors on Alpine (no systemd). Map the process-manager label for openrc and probe service state via platform.service_running (rc-service on Alpine, systemctl elsewhere). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
hey can you re base and fix this? it has diverged too much? seems straightforward, maybe merge it as well after fixing. |
Okay. I'll pull and merge it myself tomorrow. |
Upstream advanced ~200 commits since the merge base (f3c67a0), heavily refactoring the process managers, production setup, mariadb/redis, nginx, and the CLI command structure. The seven Alpine/OpenRC commits were built against the old tree and no longer apply. This merge adopts upstream/main's tree wholesale as the new base; the Alpine/OpenRC support is re-applied, adapted to the new abstractions, in the small atomic commits that follow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… helpers Re-applies the Alpine platform layer on top of upstream's refactored platform.py: - is_alpine()/is_root() detection and a _privileged() helper that drops the sudo prefix when already root (Alpine commonly runs as root). - service_command/service_enable_command/service_disable_command/ service_running dispatch to rc-service/rc-update on Alpine, systemctl elsewhere — the single seam every manager goes through. - default_nginx_config_dir() returns /etc/nginx/http.d on Alpine. - A package-alias mechanism plus ApkPackageManager, selected by get_package_manager() on Alpine. has_passwordless_sudo() now short-circuits to True when root and tolerates a missing sudo binary, so the init pre-flight check passes on a root Alpine box. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…g dir - VALID_PROCESS_MANAGERS gains "openrc", the Alpine counterpart of systemd. - NginxConfig.config_dir defaults via platform.default_nginx_config_dir(), so Alpine benches write server blocks to /etc/nginx/http.d. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…visor OpenRCProcessManager is the Alpine counterpart of SystemdProcessManager: it generates one supervise-daemon-backed init script per process under config/openrc/, symlinks them into /etc/init.d/, and enables them with rc-update. supervise-daemon provides the keep-alive that systemd's Restart=on-failure / supervisor's autorestart give elsewhere. Like the other production managers it separates the workload from the admin control plane, so: - stop() stops only the workload; the admin keeps serving, - stop_admin() stops the admin, - setup_admin() brings up just the admin for the pre-init setup wizard, - start()/restart()/is_running()/admin_is_running()/reload_web() mirror the systemd/supervisor semantics, - remove_services() tears everything down for `bench remove production`. OpenRC has no socket activation, so the admin runs as a plain supervised Flask process on admin.port (the dev/supervisor definition); nginx proxies straight to it. generate_config() regenerates the redis and gunicorn configs just like the other managers, so upgrades self-heal. Wired into ProcessManagerFactory.create/detect_running and the explicit manager selection in `bench start`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route MariaDB through the platform service helpers and an Alpine install path: - install() on Alpine apk-installs mariadb + mariadb-client (no MariaDB apt repo there), runs mariadb-install-db to seed the empty datadir, and enables the service — Alpine's package does none of that automatically. Idempotent. - start/stop/service_is_active/stop_shared go through service_command / service_running / service_disable_command, so they speak rc-service on Alpine and systemctl elsewhere. - secure_installation's superuser mariadb client runs via _privileged, so it works as root on Alpine and via sudo otherwise. Dedicated mariadb@<instance> deployments stay systemd-only (they rely on the mariadb@.service template); Alpine benches use the shared server, like macOS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- reload() enables nginx and starts it the first time on Alpine (the package doesn't auto-start), then reloads in place; uses service_command, so it's rc-service on Alpine and systemctl on other Linux. macOS still uses `nginx -s reload`. - The catch-all default_server and its error pages install into the distro's include dir (http.d on Alpine, conf.d on Debian) via default_nginx_config_dir. - The stock default vhost that would conflict with our default_server is now removed on both distros — Debian's sites-enabled/default and Alpine's http.d/default.conf. - All privileged file ops (ln/unlink/cp/install/rm/test) go through _privileged so they run sudo-less as root on Alpine and via sudo otherwise. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- python_env_manager installs Node via apk (nodejs + npm) on Alpine; the nodesource setup ships no musl builds. - letsencrypt_manager's certbot --deploy-hook reloads nginx with rc-service on Alpine, systemctl elsewhere, and all privileged certbot/openssl/mkdir calls go through _privileged. - volume.py stops/starts the shared mariadb via service_command instead of a hardcoded systemctl call. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nches - _install_system_packages installs the full Alpine build/runtime header set (musl has no manylinux wheels) instead of the Debian build-essential set. - _setup_process_manager handles openrc (install + enable the init scripts) and registers an openrc rollback that tears the services back down on a failed init. - _remove_nginx_symlink rolls back via _privileged so it works as root. - `bench new` skips the dedicated mariadb@<name> instance on Alpine (no systemd template units), falling back to the shared server like macOS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- `bench setup production` accepts --process-manager openrc, defaults to openrc on Alpine (systemd elsewhere), deploys via _setup_openrc, and detects an existing openrc deployment for migration. _installed_manager probes only OpenRC on Alpine (the systemd/supervisor CLIs aren't there). - `bench remove production` tears down an openrc deployment via remove_services. - `bench status` reports service state through platform.service_running, so the Nginx line works without systemctl on Alpine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- /api/benches/new validates the process manager against VALID_PROCESS_MANAGERS (so openrc is accepted) and, on Alpine, deploys with OpenRC regardless of the UI's systemd/supervisor choice — neither exists there, and the prebuilt UI can't offer openrc. - The new-bench admin bring-up picks OpenRCProcessManager when configured. - ProcessReader reads OpenRC service state (rc-service + /run/<svc>.pid) so the monitor page shows live processes for openrc benches instead of falling back to the empty dev pid reader. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…penRC install.sh: - POSIX sh (#!/bin/sh) with no bashisms ([[ ]] -> case, source -> ., the password read uses stty instead of `read -s`), so a bare Alpine box can bootstrap via `wget -qO- ... | sh` before bash exists. - Alpine bootstrap: apk-installs git, curl, bash, sudo, shadow (for useradd/usermod/visudo), python3 + build deps up front. - ~/.profile gets the PATH entry for ash/sh login shells. docs/README: - Document Alpine support, the busybox bootstrap, and OpenRC as the Alpine process manager across README, production.md, config.md and commands.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
rc-service lives in /sbin, which a non-root login PATH omits, and it reads root-owned run state. service_running() ran it bare, so as the bench user every rc-service call was "command not found" and every service looked stopped — `bench status` reported a running production bench as down. Route the status probe through _privileged like the start/stop/enable calls. Found while testing the OpenRC production flow end-to-end on Alpine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Narrow pid before computing stats so the type checker is satisfied and the intent (no stats for a stopped/pidless service) is explicit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_stock_default_sites only added http.d/default.conf for Alpine; on Debian default_nginx_config_dir() is conf.d, so the prior version would have removed /etc/nginx/conf.d/default.conf where upstream removed only sites-enabled/default. Keep non-Alpine behaviour identical to upstream. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single source of truth for the host's production init system: OpenRC on Alpine, systemd elsewhere. Lets the UI and deploy paths offer the right default per platform instead of assuming systemd. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Saving production.process_manager=openrc was rejected (only none/supervisor/ systemd were accepted) and a settings change that needs a restart did nothing under OpenRC. Accept the full set of valid managers, restart the OpenRC workload via the manager, and surface the host's native manager in the settings response so the UI can offer it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The setup wizard (/api/setup/config) and the new-bench dialog (/api/status) now report the host's native process manager so the UI defaults to OpenRC on Alpine. The Alpine coercion in /api/benches/new becomes a safety net for stale clients (only a stray systemd request is rewritten). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The setup wizard, new-bench dialog and settings modal hardcoded Systemd as the recommended manager, so Alpine users were shown an unavailable option. Each now reads native_process_manager from the backend and offers OpenRC (recommended) on Alpine, systemd elsewhere, with supervisor as the cross-platform alternative — full UI parity with systemd. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
systemd benches get their own mariadb@<name> instance; Alpine benches were forced onto the shared server because provision_instance was systemd-only (mariadb@.service template, systemd drop-in, Debian conf paths). The wizard still offered 'Dedicated (recommended)' on Alpine, so choosing it failed at init. Add an OpenRC provisioning path: a supervise-daemon init script runs a second mariadbd with the bench's datadir/socket/port (explicit flags override the shared [mariadbd] group, so no fragile include ordering). service_unit() now yields mariadb-<name> on Alpine, _install_alpine skips shared setup for a dedicated bench, and 'bench new' provisions a per-bench instance on Alpine too — full parity with systemd. Only macOS stays on the shared server. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dedicated MariaDB now works on Alpine too (mariadb-<instance> under OpenRC), and the admin Process Manager dropdown offers the host's native manager (OpenRC on Alpine). Update config.md, admin.md and the README so the docs match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bench init compiled frappe's wheels against the system python that `uv venv` reuses, but never installed its dev headers — so mysqlclient (and any C extension) failed with 'Python.h: No such file or directory' on a clean box. It only worked when install.sh's bootstrap had already pulled python3-dev in. Add python3-dev to the Alpine build deps and the Debian set so init is self-sufficient regardless of how the host was bootstrapped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… module Volume setup hardcoded the Debian package name 'zfsutils-linux', so enabling volumes on Alpine crashed with a raw 'no such package' apk error. Alpine splits ZFS across 'zfs' (userland), 'zfs-openrc' (import/mount services) and a per-kernel module package 'zfs-<flavor>' (zfs-lts, zfs-virt, …). Add an Alpine path that installs those, loads the module and enables the boot services, and verifies ZFS is actually usable — raising a clear, actionable VolumeError (boot a ZFS-capable kernel or disable volumes) when the module can't load, instead of crashing. Debian still uses zfsutils-linux. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@rmehta more or less done. |
There was a problem hiding this comment.
Pull request overview
Adds first-class Alpine Linux support to bench-cli by introducing Alpine detection, apk package management, OpenRC service control, and an OpenRC-backed production process manager—plus aligned MariaDB/Nginx/Let’s Encrypt/ZFS behavior and admin UI updates.
Changes:
- Introduces
bench_cli.platformhelpers for Alpine detection,apkpackage management, and init/service utilities (OpenRC vs systemd). - Adds
OpenRCProcessManagerand routes production setup/start/restart/status/admin UI to the correct native process manager per host. - Extends Alpine-specific behavior for ZFS installation checks, per-bench MariaDB instances, nginx config paths, and certbot reload hooks; adds regression tests.
Reviewed changes
Copilot reviewed 34 out of 34 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_volume_discovery.py | Adds tests for Alpine ZFS install routing and kernel flavor parsing. |
| tests/test_mariadb_manager.py | Adds tests for Alpine/OpenRC MariaDB instance naming and init script generation. |
| tests/test_commands.py | Adds regression test ensuring Alpine build deps include python3-dev and Alpine dedicated MariaDB defaults. |
| README.md | Documents Alpine requirements and installer bootstrap via `wget |
| install.sh | Switches installer to /bin/sh and adds Alpine bootstrap + POSIX-ish adjustments. |
| docs/production.md | Documents openrc as a production process manager and describes OpenRC behavior. |
| docs/config.md | Updates config docs to include openrc and Alpine MariaDB instance semantics. |
| docs/commands.md | Extends bench setup production --process-manager docs to include openrc. |
| docs/admin.md | Updates admin/settings behavior docs to include OpenRC/native process manager selection. |
| bench_cli/platform.py | Adds is_alpine, OpenRC/systemd service helpers, apk package manager, nginx config dir defaulting. |
| bench_cli/managers/volume_manager.py | Adds Alpine ZFS install path and kernel/module usability checks. |
| bench_cli/managers/python_env_manager.py | Uses Alpine nodejs/npm packages (no NodeSource musl builds). |
| bench_cli/managers/process_manager.py | Adds factory support for OpenRCProcessManager. |
| bench_cli/managers/openrc_process_manager.py | New OpenRC backend: generates supervise-daemon init scripts and manages lifecycle. |
| bench_cli/managers/nginx_manager.py | Uses distro-specific nginx include dir; Alpine service enable/start/reload via OpenRC. |
| bench_cli/managers/mariadb_manager.py | Adds Alpine install/init logic and OpenRC per-bench MariaDB instance provisioning. |
| bench_cli/managers/letsencrypt_manager.py | Uses OpenRC nginx reload hook on Alpine; privileges certbot/openssl invocations via _privileged. |
| bench_cli/config/production_config.py | Adds openrc to valid production process managers. |
| bench_cli/config/nginx_config.py | Defaults nginx config dir via platform-aware helper. |
| bench_cli/commands/volume.py | Uses platform service helpers to start/stop MariaDB. |
| bench_cli/commands/status.py | Uses platform service detection for service status display. |
| bench_cli/commands/start.py | Adds OpenRC branch for production start path. |
| bench_cli/commands/setup/production.py | Supports openrc in production setup and uses Alpine-specific defaults/probing. |
| bench_cli/commands/restart.py | Updates restart messaging to include OpenRC. |
| bench_cli/commands/remove/production.py | Removes OpenRC services when removing production. |
| bench_cli/commands/new.py | Clarifies Linux per-bench MariaDB instance behavior incl. Alpine OpenRC naming. |
| bench_cli/commands/init.py | Adds Alpine build deps and OpenRC service teardown hook on rollback. |
| admin/frontend/src/pages/Setup.vue | Wizard now uses backend-provided native_process_manager to offer correct defaults. |
| admin/frontend/src/components/SettingsModal.vue | Settings modal now shows native process manager option per host (systemd/OpenRC). |
| admin/frontend/src/components/NewBenchDialog.vue | New-bench dialog now defaults to host-native process manager. |
| admin/backend/views/setup.py | Exposes native_process_manager to setup wizard defaults. |
| admin/backend/views/settings.py | Validates openrc, exposes native_process_manager, restarts OpenRC workload safely. |
| admin/backend/readers/process_reader.py | Adds OpenRC process reading path using service status + pidfiles. |
| admin/backend/app.py | Validates openrc, coerces stale systemd requests on Alpine, and sets up admin via correct PM. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| #!/bin/sh | ||
| set -e | ||
|
|
||
| # POSIX sh (not bash) so a bare Alpine box can bootstrap via `wget -qO- ... | sh` | ||
| # before bash exists. The Ubuntu one-liner still pipes to bash explicitly. | ||
|
|
| @@ -123,7 +138,10 @@ authenticate_sudo() { | |||
| local pass | |||
| flavor = self._alpine_kernel_flavor() | ||
| if flavor: | ||
| try: | ||
| pkg.install(f"zfs-{flavor}") | ||
| except Exception: | ||
| pass # no matching module package — caught by the usability check |
| - The Alpine counterpart of systemd. Writes one `supervise-daemon` init script per process to `config/openrc/`, symlinks them into `/etc/init.d/`, and enables them with `rc-update`. | ||
| - `supervise-daemon` keeps each process alive (the equivalent of systemd's `Restart=on-failure`). | ||
| - The admin runs as a plain supervised Flask process on `admin.port` (no socket activation); nginx proxies straight to it. The workload and admin are separate services, so `bench stop` stops the workload while the admin keeps serving. | ||
| - Selected automatically on Alpine; `mariadb` stays on the shared system server (no `mariadb@.service` template units). |
| if shutil.which("zfs") and (not is_alpine() or self._zfs_usable()): | ||
| return |
| if not (shutil.which("zfs") and self._zfs_usable()): | ||
| raise VolumeError( |
| if [ -f /etc/alpine-release ] && command -v apk >/dev/null 2>&1; then | ||
| echo "Alpine detected — installing base dependencies via apk..." | ||
| if [ "$(id -u)" -eq 0 ]; then APK_SUDO=""; else APK_SUDO="sudo"; fi | ||
| $APK_SUDO apk add --no-cache \ | ||
| git curl bash sudo shadow python3 python3-dev build-base linux-headers tzdata | ||
| fi |
| def _patch_zfs_install(monkeypatch, *, zpool_rc: int, installed: list) -> None: | ||
| pkg = SimpleNamespace(install=lambda *p: installed.extend(p)) | ||
| monkeypatch.setattr(volume_manager, "get_package_manager", lambda: pkg) | ||
| monkeypatch.setattr(volume_manager, "service_enable_command", lambda s: ["rc-update", "add", s]) | ||
| monkeypatch.setattr(volume_manager, "_privileged", lambda c: c) | ||
| monkeypatch.setattr(volume_manager.shutil, "which", lambda n: "/usr/sbin/zfs") | ||
| monkeypatch.setattr( | ||
| volume_manager.subprocess, "run", | ||
| lambda *a, **k: SimpleNamespace(returncode=zpool_rc, stdout=b"", stderr=b""), | ||
| ) |
| service = self._service_name(pd.name) | ||
| lines = [ | ||
| "#!/sbin/openrc-run", | ||
| f"# {self.bench.config.name} {pd.name} — generated by bench, do not edit", | ||
| f'description="{self.bench.config.name} {pd.name}"', | ||
| "supervisor=supervise-daemon", | ||
| ] |
| default_pm = "openrc" if is_alpine() else "systemd" | ||
| pm = BenchConfig._normalize_process_manager(self._pm_arg or self.bench.config.production.process_manager) or default_pm | ||
| if pm not in VALID_PROCESS_MANAGERS: | ||
| raise BenchError(f"Invalid process manager '{pm}'. Must be one of {', '.join(VALID_PROCESS_MANAGERS)}.") | ||
| self.bench.config.production.process_manager = pm |
| if "process_manager" in production: | ||
| from bench_cli.config.production_config import VALID_PROCESS_MANAGERS | ||
|
|
||
| process_manager = str(production["process_manager"]) | ||
| if process_manager not in ("none", "supervisor", "systemd"): | ||
| return "process_manager must be none, supervisor, or systemd" | ||
| valid = ("none", *VALID_PROCESS_MANAGERS) | ||
| if process_manager not in valid: | ||
| return f"process_manager must be one of: {', '.join(valid)}" | ||
| pm = "" if process_manager == "none" else process_manager | ||
| self.config.production.process_manager = pm |
platform.which() calls shutil.which(name, path=...) with a keyword argument. The test mocks used single-arg lambdas (lambda n: ...) that don't accept keyword arguments, causing TypeError. Signed-off-by: ayushschaudhari1904@gmail.com <ayushschaudhari1904@gmail.com>
The Alpine bootstrap set APK_SUDO="sudo" for any non-root user and ran it immediately. On a bare Alpine box sudo isn't installed yet, so this failed with a confusing `sudo: not found` before the later explicit sudo check. Guard the apk call: if a non-root user has no sudo, point them at the root bootstrap path (which installs sudo and prepares the bench user) and exit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`bench setup production --process-manager systemd` was accepted on Alpine and proceeded into the systemd path, which shells out to systemctl (absent there) and yields an unmanageable deployment. Reject it with a clear message pointing to OpenRC, the native Alpine manager. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The settings endpoint accepted any VALID_PROCESS_MANAGERS value, including systemd. On Alpine a client could persist production.process_manager = "systemd", which breaks later because the systemd backend shells out to systemctl (absent there). Coerce systemd to OpenRC on Alpine, matching the new-bench endpoint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OpenRC init scripts are started as root, so supervise-daemon ran every bench process (web, workers, scheduler, socketio, redis, node, admin) as root — unlike the systemd (--user) and supervisor backends, which run the workload unprivileged. That leaves root-owned files under the bench dir/logs and an unnecessary privilege for the web/admin processes. Render command_user so supervise-daemon drops to the bench user, matching the other managers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
a5c2a65 to
fb673ef
Compare
shutil.which only searches the caller's PATH, so on Alpine (and other minimal hosts) a non-root user whose PATH lacks /sbin /usr/sbin sees an installed ZFS as "missing", triggering needless reinstalls/errors. Use platform.which, which also searches the sbin dirs. Also drop the bare `except Exception: pass` around the zfs-<flavor> module install so a real apk failure surfaces instead of being mistaken for a missing kernel module later. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The script targets /bin/sh, but `local` is a bashism that errors under dash (Debian/Ubuntu's /bin/sh) with `local: not found`. Replace the declarations in write_sudoers, authenticate_sudo and add_to_path with plain assignments so the installer runs under POSIX shells too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The OpenRC section claimed MariaDB stays on the shared system server on Alpine, but the Alpine path now provisions dedicated per-bench `mariadb-<instance>` OpenRC services (see mariadb_manager). Update the doc to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This PR adds native support for Alpine Linux to bench-cli. Alpine uses OpenRC instead of systemd, apk instead of apt, and busybox sh -- all of which differ from the Debian/Ubuntu-centric defaults.
Summary of changes
Platform detection & base tooling
platform.pyhelper that detects Alpine, exposesapkas a package manager, and provides OpenRC service utilitiesinstall.shthat bootstraps the environment on Alpine (installspython3,curl, etc. viaapk)Process manager parity
openrc_process_manager.py-- a full OpenRC backend equivalent to the systemd/supervisor process managersMariaDB
db_socket), and log directory setuppython3-devso C-extension wheels (e.g.mysqlclient) compile on AlpineNginx
/etc/nginx) rather than assuming Debian pathsLet's Encrypt
rc-service nginx reloadon Alpine instead ofsystemctlVolumes & ZFS
zfspackages with a clear error if unavailableInstaller & init
initpython3-devpulled in souv/pip can compile native extensionsAdmin UI
Tests