feat(config): regenerate config files from template (batch add + delete)

Replaces the slow, interactive per-variable scan with a deterministic
reconcile: each live config is rebuilt from its (freshly-cloned) template —
keeping the user's existing values, adding new template keys
(CFG_REQUIREMENT_CONFIGS_AUTO_UPDATE), and dropping keys the template no
longer defines (new CFG_REQUIREMENT_CONFIGS_AUTO_DELETE, default true).
Structure/order/comments follow the template; non-interactive; atomic with a
.bak; refuses to act on a missing/empty template so a broken clone can't wipe
a config. Applies to both general and per-app configs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-05-22 11:38:22 +01:00
parent d0b7b1a32f
commit d3681163af
4 changed files with 80 additions and 306 deletions

View File

@ -5,7 +5,8 @@ CFG_REQUIREMENT_SUGGEST_INSTALLS=false # Install Suggestions
CFG_REQUIREMENT_SUGGEST_METRICS=true # Metrics Suggestions - Offer Prometheus and Grafana during first install (requires Install Suggestions enabled)
CFG_REQUIREMENT_CONTINUE_PROMPT=false # Continue Prompts - Show continue prompts during installation for user confirmation
CFG_REQUIREMENT_CONFIGS_CHECK=true # Config Validation - Validate configuration files on startup for errors and consistency
CFG_REQUIREMENT_CONFIGS_AUTO_UPDATE=true # Auto Config Updates - Automatically update configuration files when system changes are detected
CFG_REQUIREMENT_CONFIGS_AUTO_UPDATE=true # Auto Config Updates - Add new config options from the template (non-interactive)
CFG_REQUIREMENT_CONFIGS_AUTO_DELETE=true # Auto Config Deletes - Remove config options no longer present in the template
CFG_REQUIREMENT_MISSING_IPS=false # IP Configuration Check - Check for and alert about missing IP configurations
CFG_REQUIREMENT_DOCKER_NETWORK_PRUNE=true # Docker Network Cleanup - Enable automatic cleanup of unused Docker networks
CFG_REQUIREMENT_DOCKER_SWITCHER=true # Docker Switcher - Install Docker version switching utility for managing multiple Docker versions

View File

@ -1,221 +1,15 @@
#!/bin/bash
# Reconcile each application's config ($containers_dir/<app>/<app>.config) against
# its freshly-cloned template ($install_containers_dir). See reconcileConfigFile.
checkApplicationsConfigFilesMissingVariables()
{
# Check if config checking is enabled
if [[ "$CFG_REQUIREMENT_CONFIGS_CHECK" == "true" ]]; then
isNotice "Scanning Application config files...please wait"
local container_configs=($(sudo find "$containers_dir" -maxdepth 2 -type f -name '*.config')) # Find .config files in immediate subdirectories of $containers_dir
local live app remote
while IFS= read -r live; do
app=$(basename "$live" .config)
remote="$install_containers_dir$app/$app.config"
reconcileConfigFile "$live" "$remote"
done < <(sudo find "$containers_dir" -maxdepth 2 -type f -name '*.config' ! -name '*.bak')
for container_config_file in "${container_configs[@]}"; do
local container_config_filename=$(basename "$container_config_file")
local config_app_name="${container_config_filename%.config}"
# Extract config variables from the local file
local local_variables=($(sudo grep -o 'CFG_[A-Za-z0-9_]*=' "$container_config_file" | sudo sed 's/=$//'))
# Find the corresponding .config file in $install_containers_dir
local remote_config_file="$install_containers_dir$config_app_name/$config_app_name.config"
if [ -f "$remote_config_file" ]; then
# Extract config variables from the remote file
local remote_variables=($(sudo grep -o 'CFG_[A-Za-z0-9_]*=' "$remote_config_file" | sudo sed 's/=$//'))
# Filter out empty variable names from the remote variables
local remote_variables=("${remote_variables[@]//[[:space:]]/}") # Remove whitespace
local remote_variables=($(echo "${remote_variables[@]}" | tr ' ' '\n' | sudo grep -v '^$' | tr '\n' ' '))
# Compare local and remote variables
for remote_var in "${remote_variables[@]}"; do
if ! [[ " ${local_variables[@]} " =~ " $remote_var " ]]; then
local var_line=$(sudo grep "${remote_var}=" "$remote_config_file")
# Check if auto-update is enabled
if [[ "$CFG_REQUIREMENT_CONFIGS_AUTO_UPDATE" == "true" ]]; then
# Auto-add the missing variable
if fileHasEmptyLine "$container_config_file"; then
echo "$var_line" | sudo tee -a "$container_config_file" > /dev/null 2>&1
else
echo "" | sudo tee -a "$container_config_file" > /dev/null 2>&1
echo "$var_line" | sudo tee -a "$container_config_file" > /dev/null 2>&1
fi
checkSuccess "Auto-added missing variable: $var_line to '$container_config_filename'"
source "$container_config_file"
# Auto-restart app if it's installed and the variable affects runtime
local app_dir=$install_containers_dir$config_app_name
if [ -d "$app_dir" ]; then
if [[ $remote_var == *"WHITELIST="* ]] || [[ $remote_var == *"PUBLIC="* ]] || [[ $remote_var == *"PORTS="* ]]; then
isNotice "Auto-restarting $config_app_name due to runtime configuration change..."
dockerComposeUpdateAndStartApp $config_app_name restart;
fi
fi
else
# Manual interaction mode
isHeader "Missing Application Config Variable Found"
isNotice "Variable '$remote_var' is missing in the local config file '$container_config_filename'."
echo ""
isOption "1. Add the '$var_line' to the '$container_config_filename'"
isOption "2. Add the '$remote_var' with my own value"
isOption "x. Skip"
echo ""
isQuestion "Enter your choice (1 or 2) or 'x' to skip : "
read -rp "" choice
case "$choice" in
1)
echo ""
if fileHasEmptyLine "$container_config_file"; then
echo "$var_line" | sudo tee -a "$container_config_file" > /dev/null 2>&1
else
echo "" | sudo tee -a "$container_config_file" > /dev/null 2>&1
echo "$var_line" | sudo tee -a "$container_config_file" > /dev/null 2>&1
fi
checkSuccess "Adding the $var_line to '$container_config_filename':"
source "$container_config_file"
if [[ $var_line == *"WHITELIST="* ]]; then
local app_dir=$install_containers_dir$config_app_name
# Check if app is installed
if [ -d "$app_dir" ]; then
echo ""
isNotice "Whitelist has been added to the $config_app_name."
echo ""
while true; do
isQuestion "Would you like to update the ${config_app_name}'s whitelist settings? (y/n): "
read -rp "" whitelistaccept
echo ""
case $whitelistaccept in
[yY])
isNotice "Updating ${config_app_name}'s whitelist settings..."
dockerComposeUpdateAndStartApp $config_app_name restart;
echo ""
break
;;
[nN])
break # Exit the loop without updating
;;
*)
isNotice "Please provide a valid input (y or n)."
;;
esac
done
fi
else
local app_dir=$install_containers_dir$config_app_name
# Check if app is installed
if [ -d "$app_dir" ]; then
echo ""
isNotice "A new config value has been added to $config_app_name."
echo ""
while true; do
isQuestion "Would you like to reinstall $config_app_name? (y/n): "
read -rp "" reinstallafterconfig
echo ""
case $reinstallafterconfig in
[yY])
isNotice "Reinstalling $config_app_name now..."
dockerInstallApp $config_app_name;
break # Exit the loop
;;
[nN])
break # Exit the loop
;;
*)
isNotice "Please provide a valid input (y or n)."
;;
esac
done
fi
fi
;;
2)
echo ""
isQuestion "Enter your value for $remote_var: "
read -p " " custom_value
echo ""
if fileHasEmptyLine "$container_config_file"; then
echo "${remote_var}=$custom_value" | sudo tee -a "$container_config_file" > /dev/null 2>&1
else
echo "" | sudo tee -a "$container_config_file" > /dev/null 2>&1
echo "${remote_var}=$custom_value" | sudo tee -a "$container_config_file" > /dev/null 2>&1
fi
checkSuccess "Adding the ${remote_var}=$custom_value to '$container_config_filename':"
source "$container_config_file"
if [[ $remote_var == *"WHITELIST="* ]]; then
local app_dir=$install_containers_dir$config_app_name
# Check if app is installed
if [ -d "$app_dir" ]; then
echo ""
isNotice "Whitelist has been added to the $config_app_name."
echo ""
while true; do
isQuestion "Would you like to update the ${config_app_name}'s whitelist settings? (y/n): "
read -rp "" whitelistaccept
echo ""
case $whitelistaccept in
[yY])
isNotice "Updating ${config_app_name}'s whitelist settings..."
dockerComposeUpdateAndStartApp $config_app_name restart;
break # Exit the loop
;;
[nN])
break # Exit the loop
;;
*)
isNotice "Please provide a valid input (y or n)."
;;
esac
done
fi
else
local app_dir=$install_containers_dir$config_app_name
# Check if app is installed
if [ -d "$app_dir" ]; then
echo ""
isNotice "A new config value has been added to $config_app_name."
echo ""
while true; do
isQuestion "Would you like to reinstall $config_app_name? (y/n): "
read -rp "" reinstallafterconfig
echo ""
case $reinstallafterconfig in
[yY])
isNotice "Reinstalling $config_app_name now..."
dockerInstallApp $config_app_name;
break # Exit the loop
;;
[nN])
break # Exit the loop
;;
*)
isNotice "Please provide a valid input (y or n)."
;;
esac
done
fi
fi
;;
[xX])
# User chose to skip
;;
*)
isNotice "Invalid choice. Skipping."
;;
esac
fi
fi
done
fi
done
isSuccessful "Application Config variable check completed." # Indicate completion
fi
isSuccessful "Application config reconciliation completed."
}

View File

@ -1,92 +1,18 @@
#!/bin/bash
# Reconcile the general LibrePortal config files ($configs_dir) against the
# freshly-cloned templates ($install_configs_dir). See reconcileConfigFile.
checkLibrePortalConfigFilesMissingVariables()
{
# Loop through local config files in $configs_dir (including subdirectories)
find "$configs_dir" -type f -name "*" ! -name ".category" | while read local_config_file; do
if [ -f "$local_config_file" ]; then
# Get relative path from configs_dir for matching with install files
local local_config_path=$(echo "$local_config_file" | sed "s|$configs_dir/||")
local local_config_filename=$(basename "$local_config_file")
local live fn rel remote
while IFS= read -r live; do
[[ -f "$live" ]] || continue
fn=$(basename "$live")
rel="${live#"$configs_dir"/}"
remote="$install_configs_dir$fn"
[[ -f "$remote" ]] || remote="$install_configs_dir$rel"
reconcileConfigFile "$live" "$remote"
done < <(find "$configs_dir" -type f ! -name ".category" ! -name "*.bak")
# Extract config variables from the local file
local local_variables=($(grep -o 'CFG_[A-Za-z0-9_]*=' "$local_config_file" | sed 's/=$//'))
# Find the corresponding .config file in $install_configs_dir
# Try both direct filename and subdirectory path
local remote_config_file="$install_configs_dir$local_config_filename"
if [ ! -f "$remote_config_file" ]; then
remote_config_file="$install_configs_dir$local_config_path"
fi
if [ -f "$remote_config_file" ]; then
# Extract config variables from the remote file
local remote_variables=($(grep -o 'CFG_[A-Za-z0-9_]*=' "$remote_config_file" | sed 's/=$//'))
# Filter out empty variable names from the remote variables
local remote_variables=("${remote_variables[@]//[[:space:]]/}") # Remove whitespace
local remote_variables=($(echo "${remote_variables[@]}" | tr ' ' '\n' | grep -v '^$' | tr '\n' ' '))
# Compare local and remote variables
for remote_var in "${remote_variables[@]}"; do
if ! [[ " ${local_variables[@]} " =~ " $remote_var " ]]; then
var_line=$(grep "${remote_var}=" "$remote_config_file")
isHeader "Missing LibrePortal Config Variable Found"
isNotice "Variable '$remote_var' is missing in the local config file '$local_config_filename'."
echo ""
isOption "1. Add the '$var_line' to the '$local_config_filename'"
isOption "2. Add the '$remote_var' with my own value"
isOption "x. Skip"
echo ""
isQuestion "Enter your choice (1 or 2) or 'x' to skip : "
read -rp "" choice
case "$choice" in
1)
echo ""
# Check if the file ends with an empty line
if fileHasEmptyLine "$local_config_file"; then
echo "$var_line" | sudo tee -a "$local_config_file" > /dev/null 2>&1
else
echo "" | sudo tee -a "$local_config_file" > /dev/null 2>&1
echo "$var_line" | sudo tee -a "$local_config_file" > /dev/null 2>&1
fi
checkSuccess "Adding the $var_line to '$local_config_filename':"
source "$local_config_file"
;;
2)
echo ""
isQuestion "Enter your value for $remote_var: "
read -p " " custom_value
echo ""
if fileHasEmptyLine "$local_config_file"; then
echo "${remote_var}=$custom_value" | sudo tee -a "$local_config_file" > /dev/null 2>&1
else
echo "" | sudo tee -a "$local_config_file" > /dev/null 2>&1
echo "${remote_var}=$custom_value" | sudo tee -a "$local_config_file" > /dev/null 2>&1
fi
checkSuccess "Adding the ${remote_var}=$custom_value to '$local_config_filename':"
source "$local_config_file"
;;
[xX])
# User chose to skip
;;
*)
isNotice "Invalid choice. Skipping."
;;
esac
fi
done
#else
#echo "Debug: Remote config file not found: $remote_config_file"
fi
fi
done
isSuccessful "LibrePortal Config variable check completed." # Indicate completion
isSuccessful "LibrePortal config reconciliation completed."
}

View File

@ -1,12 +1,65 @@
#!/bin/bash
# Reconcile a live config file against its template (the freshly-cloned repo):
# - keep the user's existing value for any key the template still defines
# - add new template keys (CFG_REQUIREMENT_CONFIGS_AUTO_UPDATE)
# - drop keys the template no longer defines (CFG_REQUIREMENT_CONFIGS_AUTO_DELETE)
# Structure, ordering and comments follow the template. Non-interactive, atomic,
# and keeps a <file>.bak. Refuses to act on a missing/empty template so a broken
# clone can never wipe a live config.
reconcileConfigFile()
{
local live="$1" template="$2"
local do_add="${CFG_REQUIREMENT_CONFIGS_AUTO_UPDATE:-true}"
local do_delete="${CFG_REQUIREMENT_CONFIGS_AUTO_DELETE:-true}"
[[ -f "$live" ]] || return 0
sudo test -s "$template" 2>/dev/null || return 0
declare -A live_line emitted
local line key
while IFS= read -r line; do
[[ "$line" =~ ^(CFG_[A-Za-z0-9_]+)= ]] && live_line["${BASH_REMATCH[1]}"]="$line"
done < <(sudo cat "$live")
local tmp; tmp=$(mktemp)
while IFS= read -r line; do
if [[ "$line" =~ ^(CFG_[A-Za-z0-9_]+)= ]]; then
key="${BASH_REMATCH[1]}"
if [[ -n "${live_line[$key]+x}" ]]; then
printf '%s\n' "${live_line[$key]}" >> "$tmp" # keep the user's value
emitted["$key"]=1
elif [[ "$do_add" == "true" ]]; then
printf '%s\n' "$line" >> "$tmp" # new key, template default
fi
else
printf '%s\n' "$line" >> "$tmp" # comments / blanks / ordering
fi
done < <(sudo cat "$template")
if [[ "$do_delete" != "true" ]]; then # keep keys the template dropped
for key in "${!live_line[@]}"; do
[[ -n "${emitted[$key]+x}" ]] || printf '%s\n' "${live_line[$key]}" >> "$tmp"
done
fi
# Replace only when the result is sane (non-empty, has keys) and differs.
if [[ -s "$tmp" ]] && grep -q '^CFG_' "$tmp" && ! sudo cmp -s "$tmp" "$live"; then
sudo cp -a "$live" "${live}.bak"
sudo cp "$tmp" "$live"
isSuccessful "Reconciled config: $(basename "$live") (backup: $(basename "$live").bak)"
fi
rm -f "$tmp"
}
checkConfigFilesMissingVariables()
{
local showheader="$1"
if [[ $showheader == "true" ]]; then
isHeader "Scanning Config Files"
fi
[[ "$CFG_REQUIREMENT_CONFIGS_CHECK" == "true" ]] || return 0
[[ "$showheader" == "true" ]] && isHeader "Scanning Config Files"
isNotice "Reconciling config files against templates...please wait"
checkLibrePortalConfigFilesMissingVariables;
checkApplicationsConfigFilesMissingVariables;
}