LibrePortal/scripts/app/install/app_install.sh
librelad d941f59388 feat(app): generic installApp driver + dispatcher fallback (Wave A)
The 31 containers/<app>/<app>.sh files each defined install<App>() with
the SAME 10-step sequence — ~4,000 lines of duplicated boilerplate.
Replaces all that with one generic driver + hook surface.

scripts/app/install/app_install.sh:
  installApp <slug> [config_variables]
    — Dispatches on $<slug> (c/u/s/r/i) the same way the per-app .sh
      files did. Same convention; dockerInstallApp's existing
      `declare $app=i` callsite needs no change.
    — Runs the standard sequence: dockerConfigSetupToContainer →
      dockerComposeSetupFile → optional .env copy → fixPermissions →
      dockerComposeUpdateAndStartApp → standard post-install steps
      (appUpdateSpecifics, setupHeadscale, databaseInstallApp,
      webuiContainerSetup, monitoring registration) → final message.
    — Hooks (all declare-f-gated, silent no-op when absent):
        <slug>_install_pre / _post_setup / _post_compose / _post_start
        <slug>_install_message_data   (echoes extra args for menu)
        <slug>_install_post
        <slug>_uninstall_pre / _post
        <slug>_stop_post
        <slug>_restart_post
      Hooks live in containers/<app>/tools/<app>_tools.sh (auto-sourced
      per the modular-per-app-tools convention).

function_install_app.sh:
  When no install<App>() function exists, fall through to
  `installApp <app_name>` instead of erroring. So an app with no .sh
  at all becomes a zero-byte addition — drop in <app>.config +
  docker-compose.yml + <app>.svg, done.

containers/linkding/linkding.sh:
  Deleted (canary). Linkding's body was 100% standard sequence;
  fallback handles it identically. Smoke-tested with stubbed helpers
  — dispatcher fires, generic runs full flow, monitoring integration
  + final-message hook plumbing all intact.

Wave B (next): delete the .sh for every other 'pure-boilerplate' app
(~15 candidates per the survey). Wave C: extract custom logic from
the 7 fat apps into hooks before deleting their .sh.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 01:43:08 +01:00

181 lines
7.2 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
# Monitoring registration — 6 of the 31 apps used to call these inline.
# Idempotent + no-op-safe for apps without monitoring tags, so we run
# them unconditionally now and let each function handle its own gating.
if declare -F monitoringToggleAppConfig >/dev/null 2>&1; then
monitoringToggleAppConfig "$app_name" "docker-compose.yml" 2>/dev/null || true
fi
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"
_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"
}