diff --git a/scripts/source/files/arrays/function_manifest.sh b/scripts/source/files/arrays/function_manifest.sh index 6f327a3..f2d2297 100644 --- a/scripts/source/files/arrays/function_manifest.sh +++ b/scripts/source/files/arrays/function_manifest.sh @@ -676,10 +676,19 @@ declare -gA LP_FN_MAP=( [lpReleaseLatestFootprint]="source/fetch.sh" [lpReleaseLatestVersion]="source/fetch.sh" [_lpSha256]="source/fetch.sh" + [_lpvAppFiles]="validation/validate_config.sh" + [_lpvCheckAuthAdapter]="validation/validate_config.sh" + [_lpvCheckCompose]="validation/validate_config.sh" + [_lpvCheckConfigFile]="validation/validate_config.sh" + [_lpvCheckDeployedSecrets]="validation/validate_config.sh" + [_lpvCheckPlaceholders]="validation/validate_config.sh" [lpVerifyInstall]="source/verify.sh" [lpVerifyMinisig]="source/fetch.sh" [lpVerifyPubKeyPath]="source/verify.sh" [lpVersionGt]="source/fetch.sh" + [_lpvFail]="validation/validate_config.sh" + [_lpvLoadSources]="validation/validate_config.sh" + [_lpvWarn]="validation/validate_config.sh" [mainLoop]="task/crontab_task_processor.sh" [mainMenu]="menu/menu_main.sh" [manifestCollect]="backup/manifest/manifest_collect.sh" @@ -981,6 +990,7 @@ declare -gA LP_FN_MAP=( [tagsProcessorSpeedtestPass]="config/tags/processors/tags_processor_speedtest_pass.sh" [tagsProcessorTraefikControl]="config/tags/processors/tags_processor_traefik_control.sh" [tagsProcessorTrustedDomains]="config/tags/processors/tags_processor_trusted_domains.sh" + [tagsValidateShowValidationStatus]="validation/validate_config.sh" [tailscaleInstallToContainer]="headscale/scripts/tailscale_install.sh" [_taskDir]="cli/task/cli_task_run.sh" [toolArgsGet]="docker/app/functions/function_app_tool.sh" @@ -1056,6 +1066,8 @@ declare -gA LP_FN_MAP=( [updateTaskFields]="task/crontab_task_processor.sh" [_upReportComposeFailure]="docker/app/compose/up_app.sh" [userExists]="function/checks/user_exists.sh" + [validateAllConfigurations]="validation/validate_config.sh" + [validateAppConfiguration]="validation/validate_config.sh" [validateContainerHealth]="task/crontab_check_processor.sh" [validateDirectoryStructure]="task/crontab_check_processor.sh" [validateDiskSpace]="task/crontab_check_processor.sh" @@ -1064,6 +1076,7 @@ declare -gA LP_FN_MAP=( [validateLibrePortalInstallation]="task/crontab_check_processor.sh" [validateLogHealth]="task/crontab_check_processor.sh" [validatePermissions]="task/crontab_check_processor.sh" + [validateSystemConfiguration]="validation/validate_config.sh" [validateSystemHealth]="task/crontab_check_processor.sh" [validateSystemService]="task/crontab_check_processor.sh" [validateTaskSystem]="task/crontab_check_processor.sh" @@ -1807,10 +1820,19 @@ declare -gA LP_FN_ROOT=( [lpReleaseLatestFootprint]="scripts" [lpReleaseLatestVersion]="scripts" [_lpSha256]="scripts" + [_lpvAppFiles]="scripts" + [_lpvCheckAuthAdapter]="scripts" + [_lpvCheckCompose]="scripts" + [_lpvCheckConfigFile]="scripts" + [_lpvCheckDeployedSecrets]="scripts" + [_lpvCheckPlaceholders]="scripts" [lpVerifyInstall]="scripts" [lpVerifyMinisig]="scripts" [lpVerifyPubKeyPath]="scripts" [lpVersionGt]="scripts" + [_lpvFail]="scripts" + [_lpvLoadSources]="scripts" + [_lpvWarn]="scripts" [mainLoop]="scripts" [mainMenu]="scripts" [manifestCollect]="scripts" @@ -2112,6 +2134,7 @@ declare -gA LP_FN_ROOT=( [tagsProcessorSpeedtestPass]="scripts" [tagsProcessorTraefikControl]="scripts" [tagsProcessorTrustedDomains]="scripts" + [tagsValidateShowValidationStatus]="scripts" [tailscaleInstallToContainer]="containers" [_taskDir]="scripts" [toolArgsGet]="scripts" @@ -2187,6 +2210,8 @@ declare -gA LP_FN_ROOT=( [updateTaskFields]="scripts" [_upReportComposeFailure]="scripts" [userExists]="scripts" + [validateAllConfigurations]="scripts" + [validateAppConfiguration]="scripts" [validateContainerHealth]="scripts" [validateDirectoryStructure]="scripts" [validateDiskSpace]="scripts" @@ -2195,6 +2220,7 @@ declare -gA LP_FN_ROOT=( [validateLibrePortalInstallation]="scripts" [validateLogHealth]="scripts" [validatePermissions]="scripts" + [validateSystemConfiguration]="scripts" [validateSystemHealth]="scripts" [validateSystemService]="scripts" [validateTaskSystem]="scripts" @@ -2288,6 +2314,7 @@ LP_EAGER_FILES=( "scripts:source/artifacts.sh" "scripts:task/crontab_check_processor.sh" "scripts:task/crontab_task_processor.sh" + "scripts:validation/validate_config.sh" "scripts:webui/data/generators/apps/webui_config.sh" "scripts:webui/data/generators/apps/webui_services.sh" "scripts:webui/data/generators/categories/webui_create_app_field_mappings.sh" @@ -2973,10 +3000,19 @@ lpReleaseChannel() { unset -f lpReleaseChannel; __lpAutoload "${install_scripts_ lpReleaseLatestFootprint() { unset -f lpReleaseLatestFootprint; __lpAutoload "${install_scripts_dir}source/fetch.sh"; lpReleaseLatestFootprint "$@"; } lpReleaseLatestVersion() { unset -f lpReleaseLatestVersion; __lpAutoload "${install_scripts_dir}source/fetch.sh"; lpReleaseLatestVersion "$@"; } _lpSha256() { unset -f _lpSha256; __lpAutoload "${install_scripts_dir}source/fetch.sh"; _lpSha256 "$@"; } +_lpvAppFiles() { unset -f _lpvAppFiles; __lpAutoload "${install_scripts_dir}validation/validate_config.sh"; _lpvAppFiles "$@"; } +_lpvCheckAuthAdapter() { unset -f _lpvCheckAuthAdapter; __lpAutoload "${install_scripts_dir}validation/validate_config.sh"; _lpvCheckAuthAdapter "$@"; } +_lpvCheckCompose() { unset -f _lpvCheckCompose; __lpAutoload "${install_scripts_dir}validation/validate_config.sh"; _lpvCheckCompose "$@"; } +_lpvCheckConfigFile() { unset -f _lpvCheckConfigFile; __lpAutoload "${install_scripts_dir}validation/validate_config.sh"; _lpvCheckConfigFile "$@"; } +_lpvCheckDeployedSecrets() { unset -f _lpvCheckDeployedSecrets; __lpAutoload "${install_scripts_dir}validation/validate_config.sh"; _lpvCheckDeployedSecrets "$@"; } +_lpvCheckPlaceholders() { unset -f _lpvCheckPlaceholders; __lpAutoload "${install_scripts_dir}validation/validate_config.sh"; _lpvCheckPlaceholders "$@"; } lpVerifyInstall() { unset -f lpVerifyInstall; __lpAutoload "${install_scripts_dir}source/verify.sh"; lpVerifyInstall "$@"; } lpVerifyMinisig() { unset -f lpVerifyMinisig; __lpAutoload "${install_scripts_dir}source/fetch.sh"; lpVerifyMinisig "$@"; } lpVerifyPubKeyPath() { unset -f lpVerifyPubKeyPath; __lpAutoload "${install_scripts_dir}source/verify.sh"; lpVerifyPubKeyPath "$@"; } lpVersionGt() { unset -f lpVersionGt; __lpAutoload "${install_scripts_dir}source/fetch.sh"; lpVersionGt "$@"; } +_lpvFail() { unset -f _lpvFail; __lpAutoload "${install_scripts_dir}validation/validate_config.sh"; _lpvFail "$@"; } +_lpvLoadSources() { unset -f _lpvLoadSources; __lpAutoload "${install_scripts_dir}validation/validate_config.sh"; _lpvLoadSources "$@"; } +_lpvWarn() { unset -f _lpvWarn; __lpAutoload "${install_scripts_dir}validation/validate_config.sh"; _lpvWarn "$@"; } mainLoop() { unset -f mainLoop; __lpAutoload "${install_scripts_dir}task/crontab_task_processor.sh"; mainLoop "$@"; } mainMenu() { unset -f mainMenu; __lpAutoload "${install_scripts_dir}menu/menu_main.sh"; mainMenu "$@"; } manifestCollect() { unset -f manifestCollect; __lpAutoload "${install_scripts_dir}backup/manifest/manifest_collect.sh"; manifestCollect "$@"; } @@ -3278,6 +3314,7 @@ tagsProcessorSocketConfiguration() { unset -f tagsProcessorSocketConfiguration; tagsProcessorSpeedtestPass() { unset -f tagsProcessorSpeedtestPass; __lpAutoload "${install_scripts_dir}config/tags/processors/tags_processor_speedtest_pass.sh"; tagsProcessorSpeedtestPass "$@"; } tagsProcessorTraefikControl() { unset -f tagsProcessorTraefikControl; __lpAutoload "${install_scripts_dir}config/tags/processors/tags_processor_traefik_control.sh"; tagsProcessorTraefikControl "$@"; } tagsProcessorTrustedDomains() { unset -f tagsProcessorTrustedDomains; __lpAutoload "${install_scripts_dir}config/tags/processors/tags_processor_trusted_domains.sh"; tagsProcessorTrustedDomains "$@"; } +tagsValidateShowValidationStatus() { unset -f tagsValidateShowValidationStatus; __lpAutoload "${install_scripts_dir}validation/validate_config.sh"; tagsValidateShowValidationStatus "$@"; } tailscaleInstallToContainer() { unset -f tailscaleInstallToContainer; __lpAutoload "${install_containers_dir}headscale/scripts/tailscale_install.sh"; tailscaleInstallToContainer "$@"; } _taskDir() { unset -f _taskDir; __lpAutoload "${install_scripts_dir}cli/task/cli_task_run.sh"; _taskDir "$@"; } toolArgsGet() { unset -f toolArgsGet; __lpAutoload "${install_scripts_dir}docker/app/functions/function_app_tool.sh"; toolArgsGet "$@"; } @@ -3353,6 +3390,8 @@ updaterVersionLadder() { unset -f updaterVersionLadder; __lpAutoload "${install_ updateTaskFields() { unset -f updateTaskFields; __lpAutoload "${install_scripts_dir}task/crontab_task_processor.sh"; updateTaskFields "$@"; } _upReportComposeFailure() { unset -f _upReportComposeFailure; __lpAutoload "${install_scripts_dir}docker/app/compose/up_app.sh"; _upReportComposeFailure "$@"; } userExists() { unset -f userExists; __lpAutoload "${install_scripts_dir}function/checks/user_exists.sh"; userExists "$@"; } +validateAllConfigurations() { unset -f validateAllConfigurations; __lpAutoload "${install_scripts_dir}validation/validate_config.sh"; validateAllConfigurations "$@"; } +validateAppConfiguration() { unset -f validateAppConfiguration; __lpAutoload "${install_scripts_dir}validation/validate_config.sh"; validateAppConfiguration "$@"; } validateContainerHealth() { unset -f validateContainerHealth; __lpAutoload "${install_scripts_dir}task/crontab_check_processor.sh"; validateContainerHealth "$@"; } validateDirectoryStructure() { unset -f validateDirectoryStructure; __lpAutoload "${install_scripts_dir}task/crontab_check_processor.sh"; validateDirectoryStructure "$@"; } validateDiskSpace() { unset -f validateDiskSpace; __lpAutoload "${install_scripts_dir}task/crontab_check_processor.sh"; validateDiskSpace "$@"; } @@ -3361,6 +3400,7 @@ validateFileSystem() { unset -f validateFileSystem; __lpAutoload "${install_scri validateLibrePortalInstallation() { unset -f validateLibrePortalInstallation; __lpAutoload "${install_scripts_dir}task/crontab_check_processor.sh"; validateLibrePortalInstallation "$@"; } validateLogHealth() { unset -f validateLogHealth; __lpAutoload "${install_scripts_dir}task/crontab_check_processor.sh"; validateLogHealth "$@"; } validatePermissions() { unset -f validatePermissions; __lpAutoload "${install_scripts_dir}task/crontab_check_processor.sh"; validatePermissions "$@"; } +validateSystemConfiguration() { unset -f validateSystemConfiguration; __lpAutoload "${install_scripts_dir}validation/validate_config.sh"; validateSystemConfiguration "$@"; } validateSystemHealth() { unset -f validateSystemHealth; __lpAutoload "${install_scripts_dir}task/crontab_check_processor.sh"; validateSystemHealth "$@"; } validateSystemService() { unset -f validateSystemService; __lpAutoload "${install_scripts_dir}task/crontab_check_processor.sh"; validateSystemService "$@"; } validateTaskSystem() { unset -f validateTaskSystem; __lpAutoload "${install_scripts_dir}task/crontab_check_processor.sh"; validateTaskSystem "$@"; } diff --git a/scripts/validation/validate_config.sh b/scripts/validation/validate_config.sh new file mode 100644 index 0000000..fb66642 --- /dev/null +++ b/scripts/validation/validate_config.sh @@ -0,0 +1,283 @@ +#!/bin/bash + +# Configuration validation. +# --------------------------------------------------------------------------- +# Every check here exists because the bug it catches shipped at least once, and +# none of them announced themselves at runtime — a wrong config key does not +# crash anything, it just quietly stops working: +# +# * two keys sharing one RANDOMIZED placeholder produced the SAME secret +# for Gitea's metrics token and its admin password +# * an auth adapter writing CFG__ADMIN_PASSWORD while the config declared +# ADMIN_PASSWORD_1 made every password reset a silent no-op +# * a compose annotation whose value did not match the line body meant the tag +# never substituted, and Mastodon shipped its placeholder as a live password +# +# So the rule is: fail loudly here, in a command someone can run, rather than +# discover it months later. +# +# Checks run against the deployed install where it exists and fall back to the +# install templates, because both can be wrong in different ways: a template +# carries placeholders, a deployed file carries the substituted values. + +# Collected during a run. +_lpv_issues=0 +_lpv_checked=0 + +_lpvFail() { isError " $*"; ((_lpv_issues++)); } +_lpvWarn() { isNotice " $*"; } + +# Resolve an app's config / compose, preferring the deployed copy. +_lpvAppFiles() +{ + local app="$1" + _lpv_cfg_live="${containers_dir}${app}/${app}.config" + _lpv_cfg_tmpl="${install_containers_dir}${app}/${app}.config" + _lpv_comp_live="${containers_dir}${app}/docker-compose.yml" + _lpv_comp_tmpl="${install_containers_dir}${app}/docker-compose.yml" + [[ -f "$_lpv_cfg_live" ]] || _lpv_cfg_live="" + [[ -f "$_lpv_cfg_tmpl" ]] || _lpv_cfg_tmpl="" + [[ -f "$_lpv_comp_live" ]] || _lpv_comp_live="" + [[ -f "$_lpv_comp_tmpl" ]] || _lpv_comp_tmpl="" +} + +# --- config file: syntax, duplicates, prefix ------------------------------- +_lpvCheckConfigFile() +{ + local app="$1" file="$2" label="$3" + [[ -n "$file" ]] || return 0 + local up="${app^^}"; up="${up//-/_}" + + bash -n "$file" 2>/dev/null || _lpvFail "$app: $label does not parse as shell." + + local dup + dup=$(grep -oE '^CFG_[A-Z0-9_]+=' "$file" | sort | uniq -d | tr -d '=') + [[ -n "$dup" ]] && while IFS= read -r k; do + _lpvFail "$app: $k is defined more than once in $label." + done <<< "$dup" + + local wrong + wrong=$(grep -oE '^CFG_[A-Z0-9_]+=' "$file" | tr -d '=' | grep -v "^CFG_${up}_" || true) + [[ -n "$wrong" ]] && while IFS= read -r k; do + [[ -n "$k" ]] && _lpvFail "$app: $k in $label does not use this app's CFG_${up}_ prefix." + done <<< "$wrong" +} + +# --- template config: placeholder hygiene ---------------------------------- +# A generated value must carry a slot number, and no two keys may share one +# placeholder — the replacer mints one value per DISTINCT placeholder and +# substitutes every occurrence, so sharing means sharing the secret. +_lpvCheckPlaceholders() +{ + local app="$1" file="$2" + [[ -n "$file" ]] || return 0 + + local k + while IFS= read -r k; do + [[ -n "$k" ]] || continue + [[ "$k" =~ _[0-9]+$ ]] || \ + _lpvFail "$app: $k holds a generated value but has no slot number (expected ${k}_1)." + done < <(grep -oE '^CFG_[A-Z0-9_]+=RANDOMIZED' "$file" | sed 's/=RANDOMIZED//') + + local ph + while IFS= read -r ph; do + [[ -n "$ph" ]] || continue + local keys + keys=$(grep -E "^CFG_[A-Z0-9_]+=${ph}$" "$file" | cut -d= -f1 | tr '\n' ' ') + _lpvFail "$app: $ph is shared by ${keys}— they would all receive the same secret." + done < <(grep -oE '=RANDOMIZED[A-Z]*[0-9]+$' "$file" | tr -d '=' | sort | uniq -d) +} + +# --- deployed config: nothing left unfilled, no accidental twins ------------ +_lpvCheckDeployedSecrets() +{ + local app="$1" file="$2" + [[ -n "$file" ]] || return 0 + + local k + while IFS= read -r k; do + [[ -n "$k" ]] && _lpvFail "$app: $k still holds its RANDOMIZED placeholder — generation never ran." + done < <(grep -oE '^CFG_[A-Z0-9_]+="?RANDOMIZED' "$file" | sed 's/="\?RANDOMIZED//') + + # Two different keys holding one value is what a shared placeholder looks + # like after substitution. Short values are skipped: "true", "admin" and + # friends repeat legitimately. + local v + while IFS= read -r v; do + [[ ${#v} -ge 12 ]] || continue + local keys + keys=$(grep -F "=$v" "$file" | cut -d= -f1 | tr '\n' ' ') + _lpvFail "$app: these keys share one value: ${keys}— a secret should never be reused." + done < <(grep -oE '^CFG_[A-Z0-9_]+="[^"]{12,}"$' "$file" \ + | sed 's/^[^=]*=//' | tr -d '"' | sort | uniq -d) +} + +# --- compose: annotations substitutable, tags backed ----------------------- +_lpvCheckCompose() +{ + local app="$1" file="$2" label="$3" cfg="$4" + [[ -n "$file" ]] || return 0 + local up="${app^^}"; up="${up//-/_}" + + # The annotation value is the literal the tag manager searches for on that + # line. If it does not appear in the line body, nothing can ever substitute. + # index() rather than split(): awk reads split's separator as a regex, and + # "#LIBREPORTAL|" ends in an alternation operator with nothing after it. + local bad + bad=$(awk ' + { + marker = "#LIBREPORTAL|" + first = index($0, marker) + if (first == 0) next + body = substr($0, 1, first - 1) + rest = $0 + while ((p = index(rest, marker)) > 0) { + rest = substr(rest, p + length(marker)) + q = index(rest, "|") + if (q == 0) break + tag = substr(rest, 1, q - 1) + val = substr(rest, q + 1) + sub(/[ \t\r].*$/, "", val) + h = index(val, "#") + if (h > 0) val = substr(val, 1, h - 1) + if (val != "" && index(body, val) == 0) + printf "line %d: %s (value %s)\n", NR, tag, val + } + }' "$file") + [[ -n "$bad" ]] && while IFS= read -r l; do + _lpvFail "$app: $label $l never substitutes — the value is not in the line body." + done <<< "$bad" + + # Every app-prefixed tag needs a filler: a CFG key of the same name, or a + # script that writes the tag directly (version bumps, per-app hooks). + # *_VERSION_TAG is exempt: the updater builds both the CFG name and the tag + # name from the app slug at runtime (CFG_${up}_VERSION), so neither the key + # nor the literal tag appears anywhere to be found. + local tag key + while IFS= read -r tag; do + [[ -n "$tag" ]] || continue + [[ "$tag" == *_VERSION_TAG ]] && continue + key="CFG_${tag%_TAG}" + [[ -n "$cfg" ]] && grep -q "^${key}=" "$cfg" 2>/dev/null && continue + grep -q "$tag" <<< "$_lpv_src" && continue + _lpvFail "$app: $label tag $tag has no ${key} and no script that fills it." + done < <(grep -oE "#LIBREPORTAL\|${up}_[A-Z0-9_]+_TAG\|" "$file" | tr -d '#|' | sed 's/LIBREPORTAL//' | sort -u) +} + +# --- auth adapters: the key they write must exist -------------------------- +# authPersistCfg resolves a bare name to its numbered slot, but if neither form +# is declared the write is a silent no-op and the WebUI keeps showing a password +# that no longer works. +_lpvCheckAuthAdapter() +{ + local app="$1" cfg="$2" + local adapter="${install_containers_dir}${app}/scripts/${app}_auth.sh" + [[ -f "$adapter" && -n "$cfg" ]] || return 0 + local up="${up_override:-${app^^}}"; up="${up//-/_}" + + local key + while IFS= read -r key; do + [[ -n "$key" ]] || continue + grep -qE "^CFG_${up}_${key}=" "$cfg" && continue + grep -qE "^CFG_${up}_${key}_[0-9]+=" "$cfg" && continue + _lpvFail "$app: its auth adapter persists ${key}, but neither CFG_${up}_${key} nor a numbered slot is declared." + done < <(grep -oE "authPersistCfg ${app} [A-Z_]+" "$adapter" | awk '{print $3}' | sort -u) +} + +# validateAppConfiguration — every check for one app. +validateAppConfiguration() +{ + local app="$1" + [[ -n "$app" ]] || { isError "No app name given."; return 1; } + _lpvAppFiles "$app" + if [[ -z "$_lpv_cfg_live" && -z "$_lpv_cfg_tmpl" ]]; then + isError "No config found for '$app'." + return 1 + fi + ((_lpv_checked++)) + + _lpvCheckConfigFile "$app" "${_lpv_cfg_live:-$_lpv_cfg_tmpl}" "$( [[ -n "$_lpv_cfg_live" ]] && echo "deployed config" || echo "config template" )" + _lpvCheckPlaceholders "$app" "$_lpv_cfg_tmpl" + _lpvCheckDeployedSecrets "$app" "$_lpv_cfg_live" + _lpvCheckCompose "$app" "${_lpv_comp_live:-$_lpv_comp_tmpl}" \ + "$( [[ -n "$_lpv_comp_live" ]] && echo "deployed compose" || echo "compose template" )" \ + "${_lpv_cfg_live:-$_lpv_cfg_tmpl}" + _lpvCheckAuthAdapter "$app" "${_lpv_cfg_live:-$_lpv_cfg_tmpl}" + return 0 +} + +# Load every .sh once so the "is this tag filled by a script?" test is a string +# search rather than a find+grep per tag. +_lpvLoadSources() +{ + [[ -n "$_lpv_src" ]] && return 0 + _lpv_src=$(cat \ + $(find "${install_scripts_dir%/}" "${install_containers_dir%/}" \ + -name '*.sh' ! -name 'function_manifest.sh' -type f 2>/dev/null) 2>/dev/null) +} + +# validateSystemConfiguration — the shared config tree, not any one app. +validateSystemConfiguration() +{ + isHeader "Validating system configuration" + local before=$_lpv_issues + local f + while IFS= read -r f; do + [[ -f "$f" ]] || continue + bash -n "$f" 2>/dev/null || _lpvFail "$(basename "$f") does not parse as shell." + local dup + dup=$(grep -oE '^CFG_[A-Z0-9_]+=' "$f" | sort | uniq -d | tr -d '=') + [[ -n "$dup" ]] && while IFS= read -r k; do + [[ -n "$k" ]] && _lpvFail "$k is defined more than once in $(basename "$f")." + done <<< "$dup" + done < <(find "${configs_dir%/}" -maxdepth 2 -type f ! -name '*.category' ! -name '.*' ! -name '*.bak' 2>/dev/null) + + if [[ $_lpv_issues -eq $before ]]; then + isSuccessful "System configuration is consistent." + fi + return 0 +} + +# validateAllConfigurations — every app plus the system tree. +validateAllConfigurations() +{ + _lpv_issues=0; _lpv_checked=0 + _lpvLoadSources + + isHeader "Validating application configuration" + local d app + for d in "${install_containers_dir%/}"/*/; do + app="$(basename "$d")" + [[ -f "$d$app.config" ]] || continue + validateAppConfiguration "$app" + done + + validateSystemConfiguration + + echo "" + if [[ $_lpv_issues -eq 0 ]]; then + isSuccessful "$_lpv_checked apps validated — no configuration problems found." + return 0 + fi + isError "$_lpv_issues configuration problem(s) across $_lpv_checked apps." + isNotice "These do not surface at runtime: a mis-declared key silently stops working." + return 1 +} + +# tagsValidateShowValidationStatus — the summary `libreportal validation status` +# prints. Same checks, counts only. +tagsValidateShowValidationStatus() +{ + validateAllConfigurations >/dev/null 2>&1 + local rc=$? + isHeader "Configuration validation status" + echo " Apps checked : $_lpv_checked" + echo " Problems : $_lpv_issues" + echo "" + if [[ $_lpv_issues -eq 0 ]]; then + isSuccessful "No configuration problems found." + else + isNotice "Run 'libreportal validation all' for the detail." + fi + return $rc +}