LibrePortal/containers/adguard/scripts/adguard_install_hooks.sh
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

143 lines
6.6 KiB
Bash

#!/bin/bash
# AdGuard Home install hooks — drive the first-boot setup wizard via its
# HTTP API so the admin doesn't have to click through five pages, then
# pin the admin bind back to 0.0.0.0:3000 (matches the compose mapping)
# and health-check the result.
adguard_install_post_start()
{
local app_name="$1"
((menu_number++))
echo ""
echo "---- $menu_number. Completing AdGuardHome initial setup automatically"
echo ""
# The legacy `$usedport1` variable isn't populated by the current
# install pipeline; the resolved host port is stored in the PORTS_TAG_1
# docker-compose tag (format `external:internal`). Pull it from there
# so the curl + URL printout actually point somewhere real.
local adguard_compose_file="$containers_dir$app_name/docker-compose.yml"
local adguard_port_pair
adguard_port_pair=$(tagsManagerGetTagContent "$adguard_compose_file" "PORTS_TAG_1")
local adguard_admin_port="${adguard_port_pair%%:*}"
if [[ -n "$public_ip_v4" && -n "$adguard_admin_port" ]]; then
echo " External : http://$public_ip_v4:$adguard_admin_port/"
fi
if [[ -n "$host_setup" ]]; then
echo " Hostname : http://$host_setup/"
fi
echo ""
# AdGuardHome ships a setup wizard that normally needs five clicks in
# a browser before the daemon writes its config file. Same wizard is
# exposed as an HTTP API (POST /control/install/configure), so drive
# it from here and skip the manual interaction. Pre-poll the admin
# endpoint until the container is up, then send the form, then let
# the post-install sed edits run against the freshly written
# AdGuardHome.yaml.
local adguard_setup_url="http://127.0.0.1:${adguard_admin_port}"
local adguard_attempts=0
local adguard_max_attempts=60
while ((adguard_attempts < adguard_max_attempts)); do
if curl -fsS -o /dev/null --max-time 2 "${adguard_setup_url}/control/status" 2>/dev/null \
|| curl -fsS -o /dev/null --max-time 2 "${adguard_setup_url}/control/install/get_addresses" 2>/dev/null; then
break
fi
sleep 2
((adguard_attempts++))
done
if ((adguard_attempts >= adguard_max_attempts)); then
isError "AdGuardHome admin endpoint did not respond on $adguard_setup_url within $((adguard_max_attempts * 2))s — open the URL and complete setup manually, then re-run the installer to apply the post-setup tweaks."
else
local adguard_user="${CFG_ADGUARD_USER:-admin}"
local adguard_pass="${CFG_ADGUARD_PASSWORD:-}"
if [[ -z "$adguard_pass" ]]; then
adguard_pass=$(generateRandomPassword)
updateConfigOption "CFG_ADGUARD_PASSWORD" "$adguard_pass" >/dev/null 2>&1 || true
isNotice "Generated a random AdGuardHome admin password and saved it to CFG_ADGUARD_PASSWORD."
fi
# Internal container ports are fixed (3000 admin, 53 DNS); host
# mapping is what `usedport1` etc. handle.
local adguard_payload
adguard_payload=$(cat <<JSON
{
"web": { "ip": "0.0.0.0", "port": 3000, "autofix": false },
"dns": { "ip": "0.0.0.0", "port": 53, "autofix": false },
"username": "${adguard_user}",
"password": "${adguard_pass}"
}
JSON
)
if curl -fsS -X POST \
-H 'Content-Type: application/json' \
--data "$adguard_payload" \
--max-time 15 \
"${adguard_setup_url}/control/install/configure" >/dev/null 2>&1; then
isSuccessful "AdGuardHome admin setup completed automatically (user: $adguard_user)."
else
# 422/403 here typically means setup was already done on a
# previous install; the post-setup tweaks below are still
# safe to run against the existing yaml.
isNotice "AdGuardHome /control/install/configure rejected the request — assuming it's already configured. If this is a fresh install, complete setup manually at $adguard_setup_url."
fi
fi
local result
if [[ "$public" == "true" ]]; then
result=$(runFileOp sed -i "s|allow_unencrypted_doh: false|allow_unencrypted_doh: true|g" "$containers_dir$app_name/conf/AdGuardHome.yaml")
checkSuccess "Setting allow_unencrypted_doh to false for Traefik"
fi
result=$(runFileOp sed -i "s|anonymize_client_ip: false: false|anonymize_client_ip: true|g" "$containers_dir$app_name/conf/AdGuardHome.yaml")
checkSuccess "Setting anonymize_client_ip to true for privacy reasons"
# Force the admin web bind back to 0.0.0.0:3000 inside the container.
# The docker-compose mapping is `<host_port>:3000`, so the container
# MUST listen on 3000 internally for the host port to reach it. After
# the install API call AdGuardHome sometimes ends up bound to
# 0.0.0.0:80 (its build-time default) — exactly what causes "unable
# to connect" on the host port.
local adguard_yaml="$containers_dir$app_name/conf/AdGuardHome.yaml"
if [[ -f "$adguard_yaml" ]]; then
runFileOp sed -i 's|^\(\s*address:\s*\)0\.0\.0\.0:[0-9]\+|\10.0.0.0:3000|' "$adguard_yaml"
runFileOp sed -i 's|^\(\s*bind_host:\s*\).*|\10.0.0.0|' "$adguard_yaml"
runFileOp sed -i 's|^\(\s*bind_port:\s*\)[0-9]\+|\13000|' "$adguard_yaml"
checkSuccess "Pinned AdGuardHome admin bind to 0.0.0.0:3000 (matches the compose port mapping)."
fi
dockerComposeRestart "$app_name"
# Drop `-f` and accept any HTTP status code: now that the admin
# account is configured, /control/status returns 401 to an
# unauthenticated request — which is fine, it means the server is up
# and answering. We only care whether the connection succeeded at
# all, not what the response body says.
local adguard_health_attempts=0
local adguard_health_code
while ((adguard_health_attempts < 20)); do
adguard_health_code=$(curl -sS -o /dev/null --max-time 2 \
-w '%{http_code}' "${adguard_setup_url}/control/status" 2>/dev/null)
if [[ "$adguard_health_code" =~ ^[1-5][0-9][0-9]$ ]]; then
isSuccessful "AdGuardHome admin UI is reachable on $adguard_setup_url (HTTP $adguard_health_code)"
break
fi
sleep 1
((adguard_health_attempts++))
done
if ((adguard_health_attempts >= 20)); then
isError "AdGuardHome admin UI did not respond after restart on $adguard_setup_url. Check the container logs (\`docker logs adguard-service\`) and the conf/AdGuardHome.yaml bind address."
fi
}
adguard_install_message_data()
{
# Echo the admin user + password as space-separated tokens so they
# become $username $password positional args to menuShowFinalMessages.
echo "${CFG_ADGUARD_USER:-admin} $CFG_ADGUARD_PASSWORD"
}