#!/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 # 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_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" # 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" }