librelad 4d7027258d feat(app): Wave B + C — collapse 28 per-app installers onto generic driver
Finishes the installApp refactor started in d941f59 (Wave A). Every app
whose <app>.sh was either pure boilerplate (Wave B) or boilerplate +
small custom logic (Wave C) now routes through the generic driver in
scripts/app/install/app_install.sh; bespoke logic moved to declarative
hooks in containers/<app>/scripts/<app>_install_hooks.sh.

Net: ~4,000 lines of duplicated 10-step sequence gone. From 31 per-app
.sh files (pre-Wave-A) down to 2 intentional keepers.

DELETED outright (pure boilerplate — driver replaces them identically):
  jellyfin, mastodon, focalboard, ipinfo, speedtest, dashy, invidious,
  nextcloud, ollama, vaultwarden, pihole

DELETED + hook-extracted (small bespoke step preserved in a hook):
  bookstack, moneyapp, owncloud, trilium, searxng, gitea, headscale,
  unbound, prometheus, grafana, gluetun, wireguard, jitsimeet, authelia,
  traefik, adguard, onlyoffice

KEPT (intentional special cases):
  crowdsec      — host-app pattern (no docker compose, runs as apt+
                   systemd via installCrowdsecHost; uninstall/stop/
                   restart hooks already live in this file and are
                   invoked by dockerUninstall/Stop/RestartApp directly).
  libreportal   — WebUI bootstrap. Pre-compose image build + post-install
                   webuiLibrePortalUpdate + bootstrap-time suppression of
                   menuShowFinalMessages don't fit the generic flow.

Driver change — scripts/app/install/app_install.sh:
  Moved monitoringToggleAppConfig "$app_name" "docker-compose.yml" from
  the post-start integrations block into the install body at post-compose
  (right after dockerComposeSetupFile, before docker-compose up). The
  toggle edits the compose file on disk — running it after start meant
  the container had already been brought up with the unmodified compose,
  so the metrics endpoint wouldn't reflect CFG_<APP>_MONITORING until
  the next restart. Matches the original ordering in every per-app .sh
  that used to call it inline.

Hook surface (declare-f-gated, silent no-op when absent):
  <slug>_install_pre              before any install work
  <slug>_install_post_setup       after dockerConfigSetupToContainer
  <slug>_install_post_compose     after dockerComposeSetupFile (+ the
                                  shared monitoring toggle on the compose)
  <slug>_install_post_start       after dockerComposeUpdateAndStartApp
  <slug>_install_message_data     echoes extra argv for menuShowFinalMessages
  <slug>_install_post             very last thing, after the final message
  + the existing _uninstall_pre/_post, _stop_post, _restart_post

Notable extractions:
  bookstack  — _install_post_start: probe :PORT_1/login until 200/302,
               then `bookstack:create-admin` inside the container with
               CFG_BOOKSTACK_ADMIN_{EMAIL,PASSWORD}; falls back to the
               seeded admin@admin.com on timeout.
  adguard    — _install_post_start drives the wizard's HTTP API
               (POST /control/install/configure) so the admin doesn't
               click through five pages, then pins the admin bind back
               to 0.0.0.0:3000 (matches the compose mapping) and health
               checks. _install_message_data echoes user/password to
               menuShowFinalMessages.
  authelia   — _install_pre requirements; _install_post_compose copies
               configuration.yml + users_database.yml, substitutes
               theme/domain/host, generates JWT/session/storage secrets,
               toggles monitoring on configuration.yml; _install_post_start
               argon2-hashes the admin password via the container, writes
               users_database.yml, restarts; _install_post echoes creds.
  traefik    — _install_pre prompts for the LE email if CFG_TRAEFIK_EMAIL
               is unset; _install_post_compose copies static + dynamic
               configs, wires CFG_TRAEFIK_DASHBOARD_ACCESS (local-only /
               domain-only / public), toggles monitoring on traefik.yml,
               then traefikUpdateWhitelist + traefikSetupLoginCredentials.
  wireguard  — _install_pre host-conflict guard (/etc/wireguard/params);
               _install_post_compose persists CFG_WIREGUARD_SUBNET,
               resolves WG_HOST (domain+traefik → host_setup, else IP),
               runs runAppCfg wireguard-ip-forward; _install_post_start
               restarts after wg-easy installs its iptables rules.
  jitsimeet  — _install_post_setup downloads the tagged release zip from
               GitHub; _install_post_compose mass-edits the .env and runs
               gen-passwords.sh; _install_post_start rewrites nginx
               default site to usedport1/2 + restart.
  prometheus — _install_post_compose seeds prometheus.yml under
               $containers_dir/prometheus/prometheus/; _install_post_start
               sets 0777 on storage dirs so the container TSDB can write
               regardless of host UID mapping.
  grafana    — _install_pre requirements; _install_post_start 0777 on
               grafana_storage.
  gluetun    — _install_post_start refreshes the provider snapshot,
               reattaches every routed app (the netns container ID is
               stale after gluetun gets recreated), then prompts to
               onboard any existing apps.
  + the smaller bookstack-shape extractions for owncloud (version scrape),
    trilium / searxng (wait-for-first-boot-config), gitea (Prometheus
    bearer token sync), headscale / unbound (config copy), moneyapp
    (Auth.js AUTH_URL), onlyoffice (compose-resolved user/pass into the
    final message).

Manifest + arrays regenerated. Verified end-to-end:
  - bash -n on every hook file + the driver: clean
  - Each hook file sources cleanly in a subshell, exposes only the
    intended functions, flagged lazy-loadable (not eager)
  - Smoke-stubbed install run for jellyfin (pure), nextcloud (pure),
    bookstack (hooked), crowdsec (kept): correct dispatch in all cases —
    deleted apps route to installApp, kept apps still hit their real
    function

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 13:26:49 +01:00

191 lines
7.7 KiB
Bash

#!/bin/bash
# Generic per-app install/uninstall/start/stop/restart/edit driver.
#
# The 31 containers/<app>/<app>.sh files used to each define their own
# install<App>() with the SAME 10-step sequence. ~4,000 lines of duplicated
# boilerplate. This is the one place that sequence lives now; per-app
# customisation lands in declarative hooks in containers/<app>/tools/
# <app>_tools.sh (or wherever the app's tools.sh lives — auto-sourced).
#
# Dispatch is driven by the `$<slug>` global variable (set by dockerInstallApp
# in scripts/docker/app/functions/function_install_app.sh — `declare $app=i`).
# Same convention the per-app .sh files used; nothing changes for the caller.
# Actions are letters: c (config edit), u (uninstall), s (stop), r (restart),
# i (install), t (treated like c — legacy alias).
#
# Hook surface — all are `declare -f`-gated, silent no-op when absent:
#
# <slug>_install_pre before any install work
# <slug>_install_post_setup after dockerConfigSetupToContainer
# (install folder + .config exist; compose
# file not yet written)
# <slug>_install_post_compose after dockerComposeSetupFile
# (docker-compose.yml has been written +
# tag-substituted; container not yet up)
# <slug>_install_post_start after dockerComposeUpdateAndStartApp
# (container is up; the place for
# wait-for-ready + post-up API calls)
# <slug>_install_message_data echoes extra args for menuShowFinalMessages
# (typically credentials / URLs)
# <slug>_install_post very last thing, after the final message
#
# <slug>_uninstall_pre / _post around dockerUninstallApp
# <slug>_stop_post after dockerComposeDown
# <slug>_restart_post after dockerComposeRestart
#
# Hooks receive $app_name as $1 (and stay un-namespaced — they're already
# slug-prefixed). Return code is ignored unless they isError; the install
# continues regardless. Use that escape hatch for non-fatal app-specific
# refinements (rotate a key, patch a yaml after start, etc.).
_appCallHook()
{
local hook_name="$1"; shift
if declare -F "$hook_name" >/dev/null 2>&1; then
"$hook_name" "$@"
fi
}
# Standard "post-start integration" steps. Same for every app. Lives in a
# helper so the generic install body stays readable; safe for apps that
# don't tag for monitoring (the helpers no-op gracefully).
_appPostStartIntegrations()
{
local app_name="$1"
appUpdateSpecifics "$app_name"
setupHeadscale "$app_name"
databaseInstallApp "$app_name"
webuiContainerSetup "$app_name" install
# Scrape-target + dashboard re-gather. The compose-level toggle ran
# already (post-compose, so the running container reflects it).
# monitoringRefreshAll is self-correcting and no-ops when Prometheus
# / Grafana aren't installed.
if declare -F monitoringRefreshAll >/dev/null 2>&1; then
monitoringRefreshAll 2>/dev/null || true
fi
}
installApp()
{
local app_slug="$1"
local config_variables="$2"
# APP_NAME comes from the app's CFG_<APP>_APP_NAME (the user's chosen
# subdomain / install name). Fall back to the slug if unset.
local app_name_var="CFG_${app_slug^^}_APP_NAME"
local app_name="${!app_name_var:-$app_slug}"
# Dispatch flags live in the $<slug> global, e.g. linkding=i. Default to
# install if nothing set — installApp called directly without flag = install.
local actions="${!app_slug:-i}"
# Setup phase shared by every action (folder + variables).
if [[ "$actions" == *[cCtTuUsSrRiI]* ]]; then
dockerConfigSetupToContainer silent "$app_slug"
initializeAppVariables "$app_name"
fi
if [[ "$actions" == *[cCtT]* ]]; then
editAppConfig "$app_name"
fi
if [[ "$actions" == *[uU]* ]]; then
_appCallHook "${app_slug}_uninstall_pre" "$app_name"
dockerUninstallApp "$app_name"
_appCallHook "${app_slug}_uninstall_post" "$app_name"
fi
if [[ "$actions" == *[sS]* ]]; then
dockerComposeDown "$app_name"
_appCallHook "${app_slug}_stop_post" "$app_name"
fi
if [[ "$actions" == *[rR]* ]]; then
dockerComposeRestart "$app_name"
_appCallHook "${app_slug}_restart_post" "$app_name"
fi
if [[ "$actions" == *[iI]* ]]; then
isHeader "Install $app_name"
_appCallHook "${app_slug}_install_pre" "$app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Setting up install folder and config for $app_name."
echo ""
dockerConfigSetupToContainer "loud" "$app_name" "install" "$config_variables"
isSuccessful "Install folders and Config files set up for $app_name."
_appCallHook "${app_slug}_install_post_setup" "$app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Setting up the $app_name docker-compose.yml."
echo ""
dockerComposeSetupFile "$app_name"
# Compose-level monitoring toggle MUST run before docker-compose up
# — the compose file is the source of truth for the running
# container, so editing it post-start wouldn't take effect until
# the next restart. Idempotent + no-op for apps without a marker
# block; apps that toggle additional files (authelia config.yml,
# traefik traefik.yml, unbound unbound.conf …) call it again from
# their _install_post_compose hook.
if declare -F monitoringToggleAppConfig >/dev/null 2>&1; then
monitoringToggleAppConfig "$app_name" "docker-compose.yml" 2>/dev/null || true
fi
_appCallHook "${app_slug}_install_post_compose" "$app_name"
# Optional .env handling — apps that ship a .env in their template
# dir get it copied + tag-substituted. No-op for apps without one.
if [[ -f "${install_containers_dir}${app_slug}/.env" ]]; then
local result
result=$(copyResource "$app_name" ".env" "")
checkSuccess "Copying .env for $app_name"
configSetupFileWithData "$app_name" ".env"
fi
((menu_number++))
echo ""
echo "---- $menu_number. Updating file permissions before starting."
echo ""
fixPermissionsBeforeStart "$app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Running docker-compose to install + start $app_name."
echo ""
dockerComposeUpdateAndStartApp "$app_name" install
_appCallHook "${app_slug}_install_post_start" "$app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Running post-install integrations."
echo ""
_appPostStartIntegrations "$app_name"
((menu_number++))
echo ""
echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name"
echo ""
# Final-message data — apps that want extra args (creds, URLs, etc.)
# printed in the menu output echo them from their hook. Word-split is
# intentional: each space-separated token becomes a positional arg.
local msg_data=""
if declare -F "${app_slug}_install_message_data" >/dev/null 2>&1; then
msg_data=$("${app_slug}_install_message_data" "$app_name")
fi
# shellcheck disable=SC2086 # intentional split — hook returns "u p" etc.
menuShowFinalMessages "$app_name" $msg_data
_appCallHook "${app_slug}_install_post" "$app_name"
menu_number=0
fi
# Reset the dispatch flag so a stale value doesn't trip a later call.
eval "$app_slug=n"
}