240 lines
11 KiB
Bash
Executable File
240 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
|
# ==============================================================================================
|
|
# ============================= conf_upgrade.sh ================================================
|
|
# ==============================================================================================
|
|
#
|
|
# PURPOSE
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Merges a new conf template into an existing user conf while preserving every
|
|
# value the user has already set. Run manually when the conf schema changes
|
|
# between versions — adds new keys, removes deprecated ones, and keeps the
|
|
# structure of the new template exactly.
|
|
#
|
|
# Keys in template only → ADDED (placeholder/default — user fills in once)
|
|
# Keys in target only → REMOVED (deprecated in new version)
|
|
# Keys in both → KEPT (target's value always wins, template ignored)
|
|
# Comments / blank lines → always from template (structure follows new version)
|
|
#
|
|
# Supports all conf variable patterns: simple scalars, indexed arrays, and
|
|
# associative arrays (declare -A).
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL SAFEGUARDS
|
|
# ==============================================================================================
|
|
#
|
|
# Dry-run Mode
|
|
# --dry-run prints the full change report (ADDED / REMOVED / KEPT) then exits
|
|
# without writing anything. Always preview before applying to production confs.
|
|
#
|
|
# Backup Option
|
|
# --backup writes a .bak copy of the target before overwriting. Use when
|
|
# applying to a conf that has never been upgraded before.
|
|
#
|
|
# File Existence Guards
|
|
# Both --template and --target are validated before any parsing begins.
|
|
# Missing files abort immediately with a clear error.
|
|
#
|
|
# Atomic Write
|
|
# Merged output is written to a tempfile first, then copied to the target.
|
|
# A partial write cannot corrupt the original.
|
|
#
|
|
# ==============================================================================================
|
|
# CONFIGURATION
|
|
# ==============================================================================================
|
|
#
|
|
# No conf vars. All inputs are CLI flags.
|
|
#
|
|
# --template <file> New version conf file (source of structure and defaults)
|
|
# --target <file> Existing user conf (source of real values — always preserved)
|
|
# --dry-run Show what would change without writing
|
|
# --backup Write a .bak copy of target before modifying
|
|
#
|
|
# ==============================================================================================
|
|
# RUNTIME MODES
|
|
# ==============================================================================================
|
|
#
|
|
# conf_upgrade.sh --template Deployment/master.conf.template --target Configurations/master.conf --dry-run
|
|
# Preview what would be added, removed, and kept — no changes written.
|
|
#
|
|
# conf_upgrade.sh --template Deployment/master.conf.template --target Configurations/master.conf --backup
|
|
# Apply the upgrade, writing a .bak first.
|
|
#
|
|
# conf_upgrade.sh --template Deployment/master.conf.template --target Configurations/master.conf
|
|
# Apply the upgrade in-place with no backup.
|
|
#
|
|
# ==============================================================================================
|
|
|
|
set -uo pipefail
|
|
|
|
# ── Arguments ────────────────────────────────────────────────────────────────────────────────
|
|
|
|
TEMPLATE=""
|
|
TARGET=""
|
|
DRY_RUN=false
|
|
BACKUP=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--template) TEMPLATE="$2"; shift 2 ;;
|
|
--target) TARGET="$2"; shift 2 ;;
|
|
--dry-run) DRY_RUN=true; shift ;;
|
|
--backup) BACKUP=true; shift ;;
|
|
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
[[ -z "$TEMPLATE" ]] && { echo "Error: --template required" >&2; exit 1; }
|
|
[[ -z "$TARGET" ]] && { echo "Error: --target required" >&2; exit 1; }
|
|
[[ -f "$TEMPLATE" ]] || { echo "Error: template not found: $TEMPLATE" >&2; exit 1; }
|
|
[[ -f "$TARGET" ]] || { echo "Error: target not found: $TARGET" >&2; exit 1; }
|
|
|
|
# ── Parse target → KEY → full definition block ───────────────────────────────────────────────
|
|
|
|
declare -A HOST_MAP # KEY → complete definition line(s) from user's conf
|
|
|
|
_parse_target() {
|
|
local in_block=false cur_key="" cur_block="" line
|
|
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
if [[ "$in_block" == true ]]; then
|
|
cur_block+="$line"$'\n'
|
|
# Closing ) — optional trailing whitespace and comment
|
|
if [[ "$line" =~ ^[[:space:]]*\)[[:space:]]*(#.*)?$ ]]; then
|
|
HOST_MAP["$cur_key"]="$cur_block"
|
|
in_block=false; cur_key=""; cur_block=""
|
|
fi
|
|
else
|
|
# declare -A KEY=(
|
|
if [[ "$line" =~ ^[[:space:]]*declare[[:space:]]+-[a-zA-Z]+[[:space:]]+([A-Z0-9_]+)[[:space:]]*=\( ]]; then
|
|
cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n'
|
|
# KEY=(
|
|
elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*=\( ]]; then
|
|
cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n'
|
|
# KEY=value (simple scalar)
|
|
elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*= ]]; then
|
|
HOST_MAP["${BASH_REMATCH[1]}"]="$line"$'\n'
|
|
fi
|
|
fi
|
|
done < "$TARGET"
|
|
}
|
|
|
|
# ── Walk template — collect stats (must run in current shell so arrays persist) ──────────────
|
|
|
|
declare -a ADDED=() KEPT=() REMOVED=()
|
|
declare -A TMPL_SEEN=()
|
|
|
|
_collect_stats() {
|
|
local in_block=false cur_key="" line
|
|
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
if [[ "$in_block" == true ]]; then
|
|
if [[ "$line" =~ ^[[:space:]]*\)[[:space:]]*(#.*)?$ ]]; then
|
|
in_block=false
|
|
TMPL_SEEN["$cur_key"]=1
|
|
if [[ -n "${HOST_MAP[$cur_key]+_}" ]]; then KEPT+=("$cur_key")
|
|
else ADDED+=("$cur_key"); fi
|
|
cur_key=""
|
|
fi
|
|
else
|
|
if [[ "$line" =~ ^[[:space:]]*declare[[:space:]]+-[a-zA-Z]+[[:space:]]+([A-Z0-9_]+)[[:space:]]*=\( ]]; then
|
|
cur_key="${BASH_REMATCH[1]}"; in_block=true
|
|
elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*=\( ]]; then
|
|
cur_key="${BASH_REMATCH[1]}"; in_block=true
|
|
elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*= ]]; then
|
|
local k="${BASH_REMATCH[1]}"
|
|
TMPL_SEEN["$k"]=1
|
|
if [[ -n "${HOST_MAP[$k]+_}" ]]; then KEPT+=("$k")
|
|
else ADDED+=("$k"); fi
|
|
fi
|
|
fi
|
|
done < "$TEMPLATE"
|
|
|
|
for key in "${!HOST_MAP[@]}"; do
|
|
[[ -z "${TMPL_SEEN[$key]+_}" ]] && REMOVED+=("$key")
|
|
done
|
|
}
|
|
|
|
# ── Walk template — write merged output ──────────────────────────────────────────────────────
|
|
# Runs in a subshell (stdout redirected) — array mutations are intentionally discarded here.
|
|
|
|
_write_merged() {
|
|
local in_block=false cur_key="" cur_block="" line
|
|
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
if [[ "$in_block" == true ]]; then
|
|
cur_block+="$line"$'\n'
|
|
if [[ "$line" =~ ^[[:space:]]*\)[[:space:]]*(#.*)?$ ]]; then
|
|
in_block=false
|
|
if [[ -n "${HOST_MAP[$cur_key]+_}" ]]; then printf '%s' "${HOST_MAP[$cur_key]}"
|
|
else printf '%s' "$cur_block"; fi
|
|
cur_key=""; cur_block=""
|
|
fi
|
|
else
|
|
if [[ "$line" =~ ^[[:space:]]*declare[[:space:]]+-[a-zA-Z]+[[:space:]]+([A-Z0-9_]+)[[:space:]]*=\( ]]; then
|
|
cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n'
|
|
elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*=\( ]]; then
|
|
cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n'
|
|
elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*= ]]; then
|
|
local k="${BASH_REMATCH[1]}"
|
|
if [[ -n "${HOST_MAP[$k]+_}" ]]; then printf '%s' "${HOST_MAP[$k]}"
|
|
else printf '%s\n' "$line"; fi
|
|
else
|
|
printf '%s\n' "$line"
|
|
fi
|
|
fi
|
|
done < "$TEMPLATE"
|
|
}
|
|
|
|
# ── Run ───────────────────────────────────────────────────────────────────────────────────────
|
|
|
|
_parse_target
|
|
_collect_stats
|
|
|
|
# ── Report ────────────────────────────────────────────────────────────────────────────────────
|
|
|
|
TARGET_NAME="$(basename "$TARGET")"
|
|
echo ""
|
|
echo "── conf_upgrade: $TARGET_NAME ──────────────────────────────────────────"
|
|
|
|
if [[ ${#ADDED[@]} -gt 0 ]]; then
|
|
echo " ADDED (new — fill in your values where needed):"
|
|
for k in "${ADDED[@]}"; do echo " + $k"; done
|
|
fi
|
|
|
|
if [[ ${#REMOVED[@]} -gt 0 ]]; then
|
|
echo " REMOVED (deprecated — no longer in this version):"
|
|
for k in "${REMOVED[@]}"; do echo " - $k"; done
|
|
fi
|
|
|
|
echo " KEPT ${#KEPT[@]} existing vars — your values preserved"
|
|
|
|
if [[ ${#ADDED[@]} -eq 0 && ${#REMOVED[@]} -eq 0 ]]; then
|
|
echo " Already up to date — no changes needed."
|
|
echo "────────────────────────────────────────────────────────────────────────"
|
|
echo ""
|
|
exit 0
|
|
fi
|
|
|
|
echo "────────────────────────────────────────────────────────────────────────"
|
|
echo ""
|
|
|
|
# ── Apply ─────────────────────────────────────────────────────────────────────────────────────
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
echo "(dry-run — no changes written)"
|
|
exit 0
|
|
fi
|
|
|
|
TMPOUT="$(mktemp)"
|
|
trap 'rm -f "$TMPOUT"' EXIT
|
|
|
|
_write_merged > "$TMPOUT"
|
|
|
|
if [[ "$BACKUP" == true ]]; then
|
|
cp "$TARGET" "${TARGET}.bak"
|
|
echo "Backup: ${TARGET}.bak"
|
|
fi
|
|
|
|
cp "$TMPOUT" "$TARGET"
|
|
echo "Updated: $TARGET"
|