mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-02 18:17:11 +09:00
Hardening plan §6. Reduces the distro-specific if/else surface and handles
edge environments:
- Dependency manifest (scripts/deps/manifest.yaml): declarative per-family
package/python/node/docker/init spec. Consumed directly by the Phase 5
Python bootstrap; install.sh mirrors it inline (it can't parse YAML before
Python exists in the curl|bash path).
- New sourceable libs (built in parallel, each shellcheck-clean + unit-tested):
- scripts/lib/pkg.sh — pkg_detect/refresh/install/is_installed across
apt/dnf/yum/zypper/pacman/apk (PKG_MGR_OVERRIDE + PKG_DRY_RUN for tests).
- scripts/lib/init.sh — init_detect + start/stop/enable/disable/reload/
is_active across systemd/openrc/runit/sysvinit/none (INIT_OVERRIDE/DRY_RUN).
- scripts/lib/env.sh — is_container/is_wsl/has_systemd (override-friendly).
- ID_LIKE handling (G5/6.5): install.sh's os_family_from() (now a pure,
testable function reading ${SERVERKIT_OS_RELEASE}) maps unknown IDs via
ID_LIKE to debian/rhel/fedora/suse/arch/alpine — rhel before fedora so RHEL
clones pick the RHEL Docker repo.
- zypper + pacman + apk (G6/6.6): choose_pkg_manager/refresh_pkg_index/pkg_add,
provision_docker and provision_python gained suse/arch/alpine branches;
Docker enable/start is now init-aware (systemd OR OpenRC) so it no longer
aborts on Alpine.
- Templated systemd unit (6.7): serverkit-backend.service → templates/
serverkit-backend.service.in, rendered (render_service_unit) at install and
in refresh_config so a custom SERVERKIT_DIR/venv/log path is honored.
install_service warns-and-skips enable when has_systemd is false (G5).
- nginx TLS floor as a reversible conf.d snippet (6.8): install.sh + update.sh
prefer /etc/nginx/conf.d/serverkit-tls.conf when nginx.conf doesn't already
declare ssl_protocols (RHEL/Fedora/SUSE), falling back to the in-place edit
on Debian/Ubuntu to avoid a 'duplicate ssl_protocols' error. uninstall
removes only that snippet.
Tests: new scripts/test/test_lib.sh (13) + 4 new test_install.sh cases
(os_family_from, render_service_unit, harden_global_tls A/B). 13+15+10 pass;
shellcheck error+warning clean across all new/changed shell.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
163 lines
6.5 KiB
Bash
163 lines
6.5 KiB
Bash
# shellcheck shell=bash
|
|
#
|
|
# scripts/lib/pkg.sh — sourceable cross-distro package-management abstraction.
|
|
#
|
|
# The ServerKit installer/updater needs to refresh the package index, install a
|
|
# handful of OS packages, and check whether something is already present — and it
|
|
# has to do this identically on Debian/Ubuntu (apt), RHEL/Rocky/Alma/Fedora
|
|
# (dnf, or yum on older boxes), openSUSE (zypper), Arch (pacman) and Alpine
|
|
# (apk). Sprinkling per-distro `if command -v apt` branches through every script
|
|
# rots fast and is impossible to unit-test, so we centralise the dispatch here.
|
|
#
|
|
# This bash helper mirrors the semantics of the Python PackageManager class in
|
|
# backend/app/utils/system.py (same detect → install/is_installed contract,
|
|
# same sudo-only-when-needed rule) so the host and the running panel agree on
|
|
# what "installed" means. It covers the three managers the Python side knows
|
|
# (apt/dnf/yum) plus the three install-time-only ones (zypper/pacman/apk) that
|
|
# the shell installer can hit before the panel is even up.
|
|
#
|
|
# Contract (these names/behaviours are depended on by callers and unit tests —
|
|
# do not rename them or add extra public functions):
|
|
# pkg_detect → echoes: apt|dnf|yum|zypper|pacman|apk|"" (none)
|
|
# pkg_refresh → refresh the package index (best-effort)
|
|
# pkg_install <pkg> [pkg...] → install packages non-interactively
|
|
# pkg_is_installed <pkg> → return 0 if installed, 1 otherwise
|
|
#
|
|
# Honors:
|
|
# PKG_DRY_RUN=1 print intended commands instead of running them
|
|
# PKG_MGR_OVERRIDE=... force the detected manager (used by unit tests)
|
|
#
|
|
# Everything mutating is best-effort and privilege-aware: a refresh/install we
|
|
# can't run cleanly degrades to a non-zero return rather than aborting the
|
|
# caller, and sudo is prepended only when we are not already root.
|
|
|
|
# Prepend sudo for a mutating command when we are not root AND sudo exists;
|
|
# otherwise run the command as-is. Mirrors _needs_sudo() in system.py — minimal
|
|
# containers run as root without sudo installed, and Windows dev boxes have no
|
|
# sudo at all, so blindly prefixing it would break those.
|
|
_pkg_sudo() {
|
|
if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
|
|
sudo "$@"
|
|
return $?
|
|
fi
|
|
"$@"
|
|
}
|
|
|
|
# Run a (privileged) mutating package command, or just print it under dry-run.
|
|
# Kept tiny so the dry-run output is exactly the command that would otherwise
|
|
# execute — which is what the installer unit tests assert against.
|
|
_pkg_run() {
|
|
if [ "${PKG_DRY_RUN:-0}" = "1" ]; then
|
|
printf ' [pkg] would run: %s\n' "$*"
|
|
return 0
|
|
fi
|
|
# Discard the tool's own chatter (progress bars, "Nothing to do" noise) here
|
|
# so callers don't redirect — that redirection would also swallow the
|
|
# dry-run line above, which the installer unit tests assert on.
|
|
_pkg_sudo "$@" >/dev/null 2>&1
|
|
}
|
|
|
|
# Detect the active package manager. Order matters and is fixed (apt first):
|
|
# a box can have several of these binaries installed, so we pick the first that
|
|
# is present in the documented precedence. An explicit override short-circuits
|
|
# detection entirely — that is how the unit tests force a given manager.
|
|
pkg_detect() {
|
|
if [ -n "${PKG_MGR_OVERRIDE:-}" ]; then
|
|
printf '%s\n' "$PKG_MGR_OVERRIDE"
|
|
return 0
|
|
fi
|
|
local mgr
|
|
for mgr in apt dnf yum zypper pacman apk; do
|
|
if command -v "$mgr" >/dev/null 2>&1; then
|
|
printf '%s\n' "$mgr"
|
|
return 0
|
|
fi
|
|
done
|
|
# No supported manager found: echo nothing (empty string).
|
|
printf '\n'
|
|
return 0
|
|
}
|
|
|
|
# Refresh the package index for the detected manager. Best-effort: index
|
|
# refreshes routinely fail on a flaky mirror and that should not abort an
|
|
# install, so we never propagate the manager's exit here.
|
|
pkg_refresh() {
|
|
local mgr
|
|
mgr="$(pkg_detect)"
|
|
case "$mgr" in
|
|
apt) _pkg_run apt-get update ;;
|
|
dnf) _pkg_run dnf makecache --refresh ;;
|
|
yum) _pkg_run yum makecache ;;
|
|
zypper) _pkg_run zypper --non-interactive refresh ;;
|
|
pacman) _pkg_run pacman -Sy --noconfirm ;;
|
|
apk) _pkg_run apk update ;;
|
|
*) return 0 ;;
|
|
esac
|
|
}
|
|
|
|
# Install one or more packages non-interactively for the detected manager.
|
|
# Propagates the manager's non-zero exit on failure (callers want to know an
|
|
# install failed); returns 1 with a stderr note when no manager was detected.
|
|
pkg_install() {
|
|
if [ "$#" -eq 0 ]; then
|
|
return 0
|
|
fi
|
|
local mgr
|
|
mgr="$(pkg_detect)"
|
|
case "$mgr" in
|
|
# Deliberate word-splitting of the package list via "$@" — each argument
|
|
# is already a separate, individually-quoted package name.
|
|
apt) _pkg_run apt-get install -y "$@" ;;
|
|
dnf) _pkg_run dnf install -y "$@" ;;
|
|
yum) _pkg_run yum install -y "$@" ;;
|
|
zypper) _pkg_run zypper --non-interactive install "$@" ;;
|
|
pacman) _pkg_run pacman -S --noconfirm "$@" ;;
|
|
apk) _pkg_run apk add "$@" ;;
|
|
*)
|
|
printf ' [pkg] no supported package manager found — cannot install: %s\n' "$*" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Return 0 if <pkg> is installed, 1 otherwise. Query-only (no sudo): we use the
|
|
# distro's low-level package database rather than the high-level manager so the
|
|
# answer matches what an install would see. Each branch tolerates the query tool
|
|
# being absent (it returns non-zero / 1 rather than aborting the caller).
|
|
pkg_is_installed() {
|
|
local pkg="$1"
|
|
local mgr
|
|
mgr="$(pkg_detect)"
|
|
case "$mgr" in
|
|
apt)
|
|
# dpkg can report a removed-but-configured package with returncode 0,
|
|
# so additionally require the "install ok installed" status line —
|
|
# matching PackageManager.is_installed() in system.py.
|
|
local out
|
|
if ! command -v dpkg >/dev/null 2>&1; then
|
|
return 1
|
|
fi
|
|
out="$(dpkg -s "$pkg" 2>/dev/null)" || return 1
|
|
case "$out" in
|
|
*'install ok installed'*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
;;
|
|
dnf|yum|zypper)
|
|
command -v rpm >/dev/null 2>&1 || return 1
|
|
rpm -q "$pkg" >/dev/null 2>&1
|
|
;;
|
|
pacman)
|
|
command -v pacman >/dev/null 2>&1 || return 1
|
|
pacman -Q "$pkg" >/dev/null 2>&1
|
|
;;
|
|
apk)
|
|
command -v apk >/dev/null 2>&1 || return 1
|
|
apk info -e "$pkg" >/dev/null 2>&1
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|