From d941f593886eb9b6d51aded50b153f54b83ccb2b Mon Sep 17 00:00:00 2001 From: librelad Date: Wed, 27 May 2026 01:43:08 +0100 Subject: [PATCH] feat(app): generic installApp driver + dispatcher fallback (Wave A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 31 containers//.sh files each defined install() 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 [config_variables] — Dispatches on $ (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): _install_pre / _post_setup / _post_compose / _post_start _install_message_data (echoes extra args for menu) _install_post _uninstall_pre / _post _stop_post _restart_post Hooks live in containers//tools/_tools.sh (auto-sourced per the modular-per-app-tools convention). function_install_app.sh: When no install() function exists, fall through to `installApp ` instead of erroring. So an app with no .sh at all becomes a zero-byte addition — drop in .config + docker-compose.yml + .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 --- containers/linkding/linkding.sh | 118 ------------ scripts/app/install/app_install.sh | 180 ++++++++++++++++++ .../app/functions/function_install_app.sh | 12 +- scripts/source/files/arrays/files_app.sh | 1 + .../source/files/arrays/function_manifest.sh | 12 +- 5 files changed, 201 insertions(+), 122 deletions(-) delete mode 100755 containers/linkding/linkding.sh create mode 100644 scripts/app/install/app_install.sh diff --git a/containers/linkding/linkding.sh b/containers/linkding/linkding.sh deleted file mode 100755 index e3d03a2..0000000 --- a/containers/linkding/linkding.sh +++ /dev/null @@ -1,118 +0,0 @@ -#!/bin/bash - -# Category : Knowledge Management -# Description : Linkding - Bookmark Manager (c/u/s/r/i): - -installLinkding() -{ - local config_variables="$1" - - if [[ "$linkding" == *[cCtTuUsSrRiI]* ]]; then - dockerConfigSetupToContainer silent linkding; - local app_name=$CFG_LINKDING_APP_NAME - initializeAppVariables $app_name; - fi - - if [[ "$linkding" == *[cC]* ]]; then - editAppConfig $app_name; - fi - - if [[ "$linkding" == *[uU]* ]]; then - dockerUninstallApp $app_name; - fi - - if [[ "$linkding" == *[sS]* ]]; then - dockerComposeDown $app_name; - fi - - if [[ "$linkding" == *[rR]* ]]; then - dockerComposeRestart $app_name; - fi - - if [[ "$linkding" == *[iI]* ]]; then - isHeader "Install $app_name" - - ((menu_number++)) - echo "" - echo "---- $menu_number. Setting up install folder and config file for $app_name." - echo "" - - dockerConfigSetupToContainer "loud" "$app_name" "install" "$config_variables"; - isSuccessful "Install folders and Config files have been setup for $app_name." - - ((menu_number++)) - echo "" - - - ((menu_number++)) - echo "" - echo "---- $menu_number. Setting up the $app_name docker-compose.yml file." - echo "" - - dockerComposeSetupFile $app_name; - - local result=$(copyResource "$app_name" ".env" "") - checkSuccess "Copying the .env for $app_name" - - configSetupFileWithData $app_name ".env"; - - ((menu_number++)) - echo "" - echo "---- $menu_number. Updating file permissions before starting." - echo "" - - fixPermissionsBeforeStart $app_name; - - ((menu_number++)) - echo "" - echo "---- $menu_number. Running the docker-compose.yml to install and start $app_name" - echo "" - - dockerComposeUpdateAndStartApp $app_name install; - - ((menu_number++)) - echo "" - echo "---- $menu_number. Running Application specific updates (if required)" - echo "" - - appUpdateSpecifics $app_name; - - ((menu_number++)) - echo "" - echo "---- $menu_number. Running Headscale setup (if required)" - echo "" - - setupHeadscale $app_name; - - ((menu_number++)) - echo "" - echo "---- $menu_number. Adding $app_name to the Apps Database table." - echo "" - - databaseInstallApp $app_name; - - ((menu_number++)) - echo "" - echo "---- $menu_number. Updating WebUI config file." - echo "" - - webuiContainerSetup $app_name install; - - ((menu_number++)) - echo "" - echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name" - echo "" - echo " You can now navigate to your new service using one of the options below : " - echo "" - - LD_SUPERUSER_NAME=$(appGetKeyData "$app_name" ".env" "LD_SUPERUSER_NAME") - LD_SUPERUSER_PASSWORD=$(appGetKeyData "$app_name" ".env" "LD_SUPERUSER_PASSWORD") - - menuShowFinalMessages $app_name $LD_SUPERUSER_NAME $LD_SUPERUSER_PASSWORD; - - menu_number=0 - #sleep 3s - cd - fi - linkding=n -} diff --git a/scripts/app/install/app_install.sh b/scripts/app/install/app_install.sh new file mode 100644 index 0000000..2cda39a --- /dev/null +++ b/scripts/app/install/app_install.sh @@ -0,0 +1,180 @@ +#!/bin/bash + +# Generic per-app install/uninstall/start/stop/restart/edit driver. +# +# The 31 containers//.sh files used to each define their own +# install() 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//tools/ +# _tools.sh (or wherever the app's tools.sh lives — auto-sourced). +# +# Dispatch is driven by the `$` 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: +# +# _install_pre before any install work +# _install_post_setup after dockerConfigSetupToContainer +# (install folder + .config exist; compose +# file not yet written) +# _install_post_compose after dockerComposeSetupFile +# (docker-compose.yml has been written + +# tag-substituted; container not yet up) +# _install_post_start after dockerComposeUpdateAndStartApp +# (container is up; the place for +# wait-for-ready + post-up API calls) +# _install_message_data echoes extra args for menuShowFinalMessages +# (typically credentials / URLs) +# _install_post very last thing, after the final message +# +# _uninstall_pre / _post around dockerUninstallApp +# _stop_post after dockerComposeDown +# _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_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 $ 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" +} diff --git a/scripts/docker/app/functions/function_install_app.sh b/scripts/docker/app/functions/function_install_app.sh index 75bcabe..51bc8cf 100755 --- a/scripts/docker/app/functions/function_install_app.sh +++ b/scripts/docker/app/functions/function_install_app.sh @@ -21,7 +21,17 @@ dockerInstallApp() fi if ! declare -F "$installFuncName" >/dev/null 2>&1; then - isError "No installer function found for app '${app_name}' (looked for ${installFuncName})." + # No per-app install function (no containers//.sh defining + # one). Fall through to the generic installApp driver — it does the + # standard 10-step sequence and picks up any _install_* hooks + # the app declares in tools/_tools.sh. This is the path taken + # by every app that doesn't need bespoke install code: their + # .sh has been deleted and the dispatcher routes them here. + if declare -F installApp >/dev/null 2>&1; then + installApp "$app_name" "$config_variables" + return $? + fi + isError "No installer function found for app '${app_name}' (looked for ${installFuncName}; installApp fallback unavailable)." return 1 fi diff --git a/scripts/source/files/arrays/files_app.sh b/scripts/source/files/arrays/files_app.sh index cf0b5f2..cf64447 100755 --- a/scripts/source/files/arrays/files_app.sh +++ b/scripts/source/files/arrays/files_app.sh @@ -9,5 +9,6 @@ app_scripts=( "app/app_status.sh" "app/app_update_specifics.sh" "app/auth_adapter.sh" + "app/install/app_install.sh" ) diff --git a/scripts/source/files/arrays/function_manifest.sh b/scripts/source/files/arrays/function_manifest.sh index c56ec47..dd39f24 100644 --- a/scripts/source/files/arrays/function_manifest.sh +++ b/scripts/source/files/arrays/function_manifest.sh @@ -20,6 +20,7 @@ declare -gA LP_FN_MAP=( [appBookstackListUsers]="bookstack/tools/bookstack_list_users.sh" [appBookstackResetPassword]="bookstack/tools/bookstack_reset_password.sh" [appBookstackSetAdmin]="bookstack/tools/bookstack_set_admin.sh" + [_appCallHook]="app/install/app_install.sh" [appCrowdSecAlertsList]="crowdsec/scripts/crowdsec_alerts_list.sh" [appCrowdSecConsoleDisable]="crowdsec/scripts/crowdsec_console_disable.sh" [appCrowdSecConsoleEnroll]="crowdsec/scripts/crowdsec_console_enroll.sh" @@ -72,6 +73,7 @@ declare -gA LP_FN_MAP=( [appNextcloudToggleMaintenance]="nextcloud/tools/nextcloud_toggle_maintenance.sh" [appOwnCloudSetupConfig]="owncloud/scripts/owncloud_setup_config.sh" [appPiholeApplyDnsUpdater]="pihole/tools/pihole_apply_dns_updater.sh" + [_appPostStartIntegrations]="app/install/app_install.sh" [_appReqHasDomain]="checks/requirements/check_app_install.sh" [_appReqServiceInstalled]="checks/requirements/check_app_install.sh" [_appReqServiceMsg]="checks/requirements/check_app_install.sh" @@ -447,6 +449,7 @@ declare -gA LP_FN_MAP=( [hostSshUser]="ssh/host_access.sh" [initializeAppVariables]="network/variables/variables_init_app.sh" [installAdguard]="adguard/adguard.sh" + [installApp]="app/install/app_install.sh" [installArch]="os/install/arch.sh" [installAuthelia]="authelia/authelia.sh" [installBookstack]="bookstack/bookstack.sh" @@ -475,7 +478,6 @@ declare -gA LP_FN_MAP=( [installLibrePortalAppWebUI]="webui/webui_install_app.sh" [installLibrePortalImageWebUI]="webui/webui_install_image.sh" [installLibrePortalWebUITaskService]="webui/webui_install_systemd.sh" - [installLinkding]="linkding/linkding.sh" [installMastodon]="mastodon/mastodon.sh" [installMoneyapp]="moneyapp/moneyapp.sh" [installNextcloud]="nextcloud/nextcloud.sh" @@ -901,6 +903,7 @@ declare -gA LP_FN_ROOT=( [appBookstackListUsers]="containers" [appBookstackResetPassword]="containers" [appBookstackSetAdmin]="containers" + [_appCallHook]="scripts" [appCrowdSecAlertsList]="containers" [appCrowdSecConsoleDisable]="containers" [appCrowdSecConsoleEnroll]="containers" @@ -953,6 +956,7 @@ declare -gA LP_FN_ROOT=( [appNextcloudToggleMaintenance]="containers" [appOwnCloudSetupConfig]="containers" [appPiholeApplyDnsUpdater]="containers" + [_appPostStartIntegrations]="scripts" [_appReqHasDomain]="scripts" [_appReqServiceInstalled]="scripts" [_appReqServiceMsg]="scripts" @@ -1328,6 +1332,7 @@ declare -gA LP_FN_ROOT=( [hostSshUser]="scripts" [initializeAppVariables]="scripts" [installAdguard]="containers" + [installApp]="scripts" [installArch]="scripts" [installAuthelia]="containers" [installBookstack]="containers" @@ -1356,7 +1361,6 @@ declare -gA LP_FN_ROOT=( [installLibrePortalAppWebUI]="scripts" [installLibrePortalImageWebUI]="scripts" [installLibrePortalWebUITaskService]="scripts" - [installLinkding]="containers" [installMastodon]="containers" [installMoneyapp]="containers" [installNextcloud]="containers" @@ -1802,6 +1806,7 @@ appBookstackDeleteUser() { source "${install_containers_dir}bookstack/tools/book appBookstackListUsers() { source "${install_containers_dir}bookstack/tools/bookstack_list_users.sh"; appBookstackListUsers "$@"; } appBookstackResetPassword() { source "${install_containers_dir}bookstack/tools/bookstack_reset_password.sh"; appBookstackResetPassword "$@"; } appBookstackSetAdmin() { source "${install_containers_dir}bookstack/tools/bookstack_set_admin.sh"; appBookstackSetAdmin "$@"; } +_appCallHook() { source "${install_scripts_dir}app/install/app_install.sh"; _appCallHook "$@"; } appCrowdSecAlertsList() { source "${install_containers_dir}crowdsec/scripts/crowdsec_alerts_list.sh"; appCrowdSecAlertsList "$@"; } appCrowdSecConsoleDisable() { source "${install_containers_dir}crowdsec/scripts/crowdsec_console_disable.sh"; appCrowdSecConsoleDisable "$@"; } appCrowdSecConsoleEnroll() { source "${install_containers_dir}crowdsec/scripts/crowdsec_console_enroll.sh"; appCrowdSecConsoleEnroll "$@"; } @@ -1854,6 +1859,7 @@ appNextcloudTailLogs() { source "${install_containers_dir}nextcloud/tools/nextcl appNextcloudToggleMaintenance() { source "${install_containers_dir}nextcloud/tools/nextcloud_toggle_maintenance.sh"; appNextcloudToggleMaintenance "$@"; } appOwnCloudSetupConfig() { source "${install_containers_dir}owncloud/scripts/owncloud_setup_config.sh"; appOwnCloudSetupConfig "$@"; } appPiholeApplyDnsUpdater() { source "${install_containers_dir}pihole/tools/pihole_apply_dns_updater.sh"; appPiholeApplyDnsUpdater "$@"; } +_appPostStartIntegrations() { source "${install_scripts_dir}app/install/app_install.sh"; _appPostStartIntegrations "$@"; } _appReqHasDomain() { source "${install_scripts_dir}checks/requirements/check_app_install.sh"; _appReqHasDomain "$@"; } _appReqServiceInstalled() { source "${install_scripts_dir}checks/requirements/check_app_install.sh"; _appReqServiceInstalled "$@"; } _appReqServiceMsg() { source "${install_scripts_dir}checks/requirements/check_app_install.sh"; _appReqServiceMsg "$@"; } @@ -2229,6 +2235,7 @@ hostSshSetPasswordAuth() { source "${install_scripts_dir}ssh/host_access.sh"; ho hostSshUser() { source "${install_scripts_dir}ssh/host_access.sh"; hostSshUser "$@"; } initializeAppVariables() { source "${install_scripts_dir}network/variables/variables_init_app.sh"; initializeAppVariables "$@"; } installAdguard() { source "${install_containers_dir}adguard/adguard.sh"; installAdguard "$@"; } +installApp() { source "${install_scripts_dir}app/install/app_install.sh"; installApp "$@"; } installArch() { source "${install_scripts_dir}os/install/arch.sh"; installArch "$@"; } installAuthelia() { source "${install_containers_dir}authelia/authelia.sh"; installAuthelia "$@"; } installBookstack() { source "${install_containers_dir}bookstack/bookstack.sh"; installBookstack "$@"; } @@ -2257,7 +2264,6 @@ installLibrePortal() { source "${install_containers_dir}libreportal/libreportal. installLibrePortalAppWebUI() { source "${install_scripts_dir}webui/webui_install_app.sh"; installLibrePortalAppWebUI "$@"; } installLibrePortalImageWebUI() { source "${install_scripts_dir}webui/webui_install_image.sh"; installLibrePortalImageWebUI "$@"; } installLibrePortalWebUITaskService() { source "${install_scripts_dir}webui/webui_install_systemd.sh"; installLibrePortalWebUITaskService "$@"; } -installLinkding() { source "${install_containers_dir}linkding/linkding.sh"; installLinkding "$@"; } installMastodon() { source "${install_containers_dir}mastodon/mastodon.sh"; installMastodon "$@"; } installMoneyapp() { source "${install_containers_dir}moneyapp/moneyapp.sh"; installMoneyapp "$@"; } installNextcloud() { source "${install_containers_dir}nextcloud/nextcloud.sh"; installNextcloud "$@"; }