#!/bin/bash # Pre-flight checker for app installs. # # Each app declares its prerequisites in its .config as # CFG__REQUIRES="" # where 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__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_ 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$" }