LibrePortal/scripts/checks/requirements/check_app_install.sh
librelad 3be119af13 refactor(checks): data-driven app requirements (collapse per-service case arms)
The 5 service arms in appInstallCheckRequirements (traefik/gluetun/authelia/
headscale/prometheus) were identical _appReqServiceInstalled calls. Collapse them
into one generic default: any requirement naming a real container is a service
prerequisite — so a new service requirement now needs NO code here, just list it
in the app's CFG_<APP>_REQUIRES. domain + mail stay as their own special types; a
requirement that isn't a known app is still treated as a typo and ignored (safety
net preserved). Flavor messages kept via a small optional reason map
(_appReqServiceMsg); unknown-to-the-map services get a clean generic message.

Stays central (it's the requirements engine, not per-app logic) but is now
extensible without edits. Verified with stubs: met→rc0, absent service→flavor or
generic msg, brand-new container service→generic (zero code), typo→ignored.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-26 01:29:37 +01:00

129 lines
4.5 KiB
Bash

#!/bin/bash
# Pre-flight checker for app installs.
#
# Each app declares its prerequisites in its .config as
# CFG_<APP>_REQUIRES="<csv>"
# where <csv> is a comma-separated list of requirement keys: the special types
# domain — at least one CFG_DOMAIN_N is set
# mail — global mail is enabled
# or the name of any other app/service that must be installed first. The service
# case is data-driven — any real container name works with NO code change here,
# just list it. Examples:
# CFG_AUTHELIA_REQUIRES="domain,traefik"
# CFG_BOOKSTACK_REQUIRES="domain,traefik"
#
# Call this as the first step inside an app's install lifecycle:
#
# if ! appInstallCheckRequirements "$app_name" "$CFG_AUTHELIA_REQUIRES"; then
# return 1
# fi
#
# Returns 0 if all requirements are met, 1 if any are missing. On
# failure, prints one line per missing requirement so the user can
# resolve them in order.
appInstallCheckRequirements()
{
local app_name="$1"
local reqs_csv="$2"
[[ -z "$reqs_csv" ]] && return 0
local missing=()
local req
local IFS=','
for req in $reqs_csv; do
# Trim whitespace.
req="${req#"${req%%[![:space:]]*}"}"
req="${req%"${req##*[![:space:]]}"}"
[[ -z "$req" ]] && continue
case "$req" in
"domain")
if ! _appReqHasDomain; then
missing+=("Set at least one CFG_DOMAIN_N (General → Network) before installing $app_name.")
fi
;;
"mail")
if [[ "$CFG_MAIL_ENABLED" != "true" ]]; then
missing+=("Configure global mail (CFG_MAIL_ENABLED=true under General → Mail) first.")
fi
;;
*)
# Any other requirement names a service/app that must be installed.
# Data-driven: if it's a real container it's a service prerequisite —
# adding a new one needs NO code here, just list it in the app's
# CFG_<APP>_REQUIRES. A name that's not a known app is a typo → ignore.
if [[ -d "${install_containers_dir}${req}" ]]; then
if ! _appReqServiceInstalled "$req"; then
missing+=("$(_appReqServiceMsg "$req" "$app_name")")
fi
else
isNotice "Unknown requirement '$req' declared by $app_name — ignoring."
fi
;;
esac
done
unset IFS
if [[ ${#missing[@]} -gt 0 ]]; then
isError "Cannot install $app_name — prerequisites are not met:"
local m
for m in "${missing[@]}"; do
isError "$m"
done
return 1
fi
return 0
}
# Human-friendly "install X first" line for a service prerequisite. Known services
# get a reason; anything else falls back to a generic message — so a NEW service
# requirement works with no code change (add a reason here only if you want flavor).
_appReqServiceMsg()
{
local svc="$1" app="$2" reason=""
case "$svc" in
traefik) reason="a reverse proxy to publish itself" ;;
gluetun) reason="a VPN gateway to route through" ;;
authelia) reason="its auth integration" ;;
prometheus) reason="something to query" ;;
esac
local disp="$(tr '[:lower:]' '[:upper:]' <<< ${svc:0:1})${svc:1}"
if [[ -n "$reason" ]]; then
echo "Install $disp first — $app needs $reason."
else
echo "Install $disp first — required by $app."
fi
}
# True if any CFG_DOMAIN_<n> is set to a non-empty value.
_appReqHasDomain()
{
local i var val
for i in 1 2 3 4 5 6 7 8 9; do
var="CFG_DOMAIN_$i"
val="${!var}"
[[ -n "$val" ]] && return 0
done
return 1
}
# Thin wrapper so we can use either dockerCheckAppInstalled or whatever
# convenience helper a future refactor introduces, without rewriting the
# call sites in here.
_appReqServiceInstalled()
{
local svc="$1"
if declare -f checkServiceInstalled >/dev/null 2>&1; then
checkServiceInstalled "$svc" && return 0 || return 1
fi
if declare -f dockerCheckAppInstalled >/dev/null 2>&1; then
local status
status=$(dockerCheckAppInstalled "$svc" "docker" 2>/dev/null)
[[ "$status" == "installed" ]] && return 0 || return 1
fi
# Fallback: ask docker directly (mode-aware: rootless hits the rootless socket).
runFileOp docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${svc}-service$"
}