#!/bin/bash # Bring config options added to an app's template into an already-deployed copy. # # The deployed config at /docker/containers//.config is written once, # on first install, and never touched again — dockerConfigSetupToContainer only # copies when the file is absent, precisely so a LibrePortal update can never # overwrite the values someone has edited. That is the right default, but it has # a consequence nobody chose: an app that gains a CFG_ option in a new release # has that option on every FRESH install and on no EXISTING one. # # The failure is silent, which is the worst part. Nothing errors. The new key # simply reads as empty, and whatever depends on it does something quietly # different — a default it did not mean to take, or a value it needed and did # not get. It only surfaces as "why does this work on my other box". # # So: copy across keys the template has and the deployed file does not, and # nothing else. # # Deliberately NOT a merge or a regenerate. Rebuilding the deployed file from # the template would place new keys in their proper section and refresh the # documentation with them, which is genuinely nicer to read — but it would put a # whole-file rewrite of every app's config in the path of every app action, and # the worst case of a bug there is silently corrupting settings across the whole # install. Appending cannot lose an existing line. That trade is not close. # # Existing keys are never touched, and keys the deployed file has but the # template no longer does are left exactly where they are: a removed option is # usually a rename, and deleting the user's value is not recoverable. configBackfillMissingKeys() { local app_name="$1" local template="$2" local deployed="$3" local silent_flag="${4:-silent}" [[ -f "$template" ]] || return 0 [[ -f "$deployed" ]] || return 0 # Same file, or a fresh copy of it — nothing can be missing. runFileOp cmp -s "$template" "$deployed" && return 0 local template_keys deployed_keys missing template_keys=$(grep -oE '^CFG_[A-Z0-9_]+=' "$template" 2>/dev/null | sed 's/=$//' | sort -u) deployed_keys=$(runFileOp grep -oE '^CFG_[A-Z0-9_]+=' "$deployed" 2>/dev/null | sed 's/=$//' | sort -u) [[ -z "$template_keys" ]] && return 0 missing=$(comm -23 <(printf '%s\n' "$template_keys") <(printf '%s\n' "$deployed_keys")) [[ -z "$missing" ]] && return 0 # Carry each key's comment block over with it. A bare `CFG_X=value` appended # to the end of a heavily-documented file is an option nobody can act on; # the comment above it in the template is the only explanation that exists. local missing_csv missing_csv=$(printf '%s\n' "$missing" | paste -sd, -) local block block=$(awk -v keys="$missing_csv" ' BEGIN { n = split(keys, K, ","); for (i = 1; i <= n; i++) want[K[i]] = 1 } # Accumulate the contiguous comment block sitting directly above a key. /^[[:space:]]*#/ { buf = buf $0 "\n"; next } # A blank line ends a block — the comments above it belong to something else. /^[[:space:]]*$/ { buf = ""; next } /^CFG_[A-Z0-9_]+=/ { k = $0; sub(/=.*/, "", k) if (k in want) printf "%s%s\n\n", buf, $0 buf = "" next } { buf = "" } ' "$template") [[ -z "$block" ]] && return 0 { printf '\n#\n' printf '# =============================================================================\n' printf '# ADDED BY A LIBREPORTAL UPDATE\n' printf '# =============================================================================\n' printf '# These options did not exist when this app was installed. They are set to the\n' printf '# defaults shipped with the new version — review them, they are yours to change.\n' printf '#\n' printf '%s\n' "$block" } | runFileWrite -a "$deployed" local count count=$(printf '%s\n' "$missing" | grep -c .) # Counter rather than a return code: the callers use this as a bare # statement, and a non-zero "nothing to do" would read as a failure to any # of them running under errexit. LP_BACKFILL_ADDED=$(( ${LP_BACKFILL_ADDED:-0} + count )) if [[ "$silent_flag" == "loud" ]]; then isSuccessful "Added $count new config option(s) to $app_name:" printf '%s\n' "$missing" | sed 's/^/ /' else isNotice "Added $count new config option(s) to $app_name's config." fi return 0 } # Sweep every installed app after a LibrePortal update. # # The per-app backfill above only fires when an app's config is set up, which # happens on install — and an update installs exactly one app, LibrePortal # itself. So without this sweep, an option added to some app in a new release # reaches that app only when someone next reinstalls it, which for a working app # may be never. Since the whole point is that new options arrive on upgrade, # upgrade is where this has to run. configBackfillAllApps() { local dir app template deployed LP_BACKFILL_ADDED=0 # Driven from the template directory, not from the deployed one. Under # rootless the container tree is mode drwxr-x--x and owned by the docker # install user, so the manager running this can traverse it but cannot LIST # it — a glob over it silently expands to nothing and the sweep would report # success having examined no apps at all. The template dir is manager-owned # and readable, and it defines the same set of apps; whether each one is # actually installed is then just "does its deployed config exist", which is # a traverse, not a list. for dir in "$install_containers_dir"*/; do [[ -d "$dir" ]] || continue app="${dir%/}"; app="${app##*/}" [[ "$app" == "template" ]] && continue template="${dir}${app}.config" deployed="${containers_dir}${app}/${app}.config" [[ -f "$template" ]] || continue runFileOp test -f "$deployed" || continue configBackfillMissingKeys "$app" "$template" "$deployed" "silent" # A backfilled key whose default is a RANDOMIZED* placeholder has to be # given a real value here — nothing else will run over this file until # the app is next installed, and a placeholder left in place is a # credential that is identical on every install that took this upgrade. if runFileOp grep -qE 'RANDOMIZED(PASSWORD|USERNAME|BCRYPTPASSWORD|HEX|VAPID|APPKEY)[0-9]*' "$deployed" 2>/dev/null; then scanFileForRandomPasswordKeysUsers "$deployed" fi done if (( ${LP_BACKFILL_ADDED:-0} > 0 )); then isSuccessful "Carried $LP_BACKFILL_ADDED new config option(s) into your existing apps." isNotice " They are set to the defaults shipped with this version — review them" isNotice " in each app's Config tab." fi return 0 }