feat(validation): implement the config checks the CLI already advertised
`libreportal validation app|system|all|status` dispatched to four functions that
were never defined anywhere and were absent from the manifest, so every
subcommand failed. They exist now.
The checks are the ones that would have caught the bugs found while auditing the
credential rework, all of which were invisible at runtime — a mis-declared key
does not crash, it silently stops working:
* two keys sharing one RANDOMIZED<n>, which gave Gitea's metrics token and its
admin password the same value
* a generated key with no slot number
* an annotation whose value is absent from its line body, so the tag can never
substitute — how 0.1.0 Mastodon shipped a placeholder as its live password
* an auth adapter persisting a key the config does not declare, making every
password reset a silent no-op
* duplicate keys, keys under the wrong app prefix, and compose tags with
nothing to fill them
Verified both directions: clean across all 39 apps today, and each of the seven
bug classes above is caught when reintroduced into a scratch copy of the catalog
(including the real 0.1.0 mastodon compose pulled from git history).
Version tags are exempt from the backing-key check: the updater builds both the
CFG name and the tag name from the slug at runtime, so neither literal exists to
find.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
e73f47ad46
commit
81c672c474
@ -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 "$@"; }
|
||||
|
||||
283
scripts/validation/validate_config.sh
Normal file
283
scripts/validation/validate_config.sh
Normal file
@ -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<n> placeholder produced the SAME secret
|
||||
# for Gitea's metrics token and its admin password
|
||||
# * an auth adapter writing CFG_<APP>_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 <app> — 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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user