diff --git a/lib/common.sh b/lib/common.sh index 8caad69..23a11cf 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -17,6 +17,9 @@ # config`. Reusable as-is by any LXC creator script. # - refresh_os_packages(): Alpine only (apk update && apk upgrade). A # future Debian-based script needs its own apt-get variant. +# - ini_set(): OS-agnostic (plain awk/sed, no OS-specific assumptions). +# Reusable as-is by any script that manages an INI-style config file, +# regardless of the underlying distro. # # Does not set shell options (set -e/-u/-o pipefail): a sourced file must # not impose those on the caller's shell. Both openbao/install.sh and @@ -118,3 +121,107 @@ refresh_os_packages() { log_info "Refreshing Alpine packages..." apk update >/dev/null && apk upgrade >/dev/null } + +# ============================================================ +# #18 - Idempotently set KEY = VALUE in SECTION of an INI-style config file +# (e.g. Gitea's app.ini). Merges key by key rather than overwriting the +# whole file, so a rejoué script can add newly-required keys to an already +# customized config without clobbering it. +# +# Usage: ini_set
+# +# Behavior: +# - Missing file/section/key: created. +# - Key present with a different value: replaced in place. +# - Key present with the same value: no-op (byte-identical output). +# - Other sections/keys: never touched — the match is scoped to the +# given section, so the same key name in a different section (e.g. +# ENABLED in both [metrics] and [actions]) is left alone. +# Comments, blank lines and section order are preserved. Written atomically +# (tmpfile + mv) so an interrupted run can't leave a corrupt config. +# ============================================================ +ini_set() { + local file="$1" section="$2" key="$3" value="$4" + local tmp + + if [[ ! -f "$file" ]]; then + mkdir -p "$(dirname "$file")" + : > "$file" + fi + + tmp=$(mktemp "${file}.tmp.XXXXXX") + + # mktemp defaults to 0600 root:root, which would silently lock the + # service account that owns $file (e.g. gitea:www-data on Gitea's + # app.ini) out of the config this function just wrote. Carry the + # original file's mode/ownership onto the replacement before it lands. + # `stat -c` works identically on GNU coreutils and BusyBox. + chmod "$(stat -c '%a' "$file")" "$tmp" 2>/dev/null || true + chown "$(stat -c '%u:%g' "$file")" "$tmp" 2>/dev/null || true + + # awk writes into $tmp regardless of its own exit status — a mid-stream + # death (OOM, signal, an exotic value tripping the regex) would still + # leave a truncated-but-nonempty $tmp for `mv` to install over $file. + # Gate the mv on awk's exit code so a failure leaves the original config + # untouched instead of silently destroying it. + if ! awk -v section="$section" -v key="$key" -v value="$value" ' + # Blank lines are buffered rather than printed immediately while a + # section is still awaiting insertion: without this, a key inserted + # right before the next section header lands *after* that section'"'"'s + # trailing blank line(s) instead of before them. Flushed as soon as + # either a non-blank line or the insertion itself happens, so this + # never reorders anything except relative to that pending insertion. + function flush_blanks() { + while (blank_count > 0) { print ""; blank_count-- } + } + /^\[.*\]$/ { + if (in_section && !done) { + printf "%s = %s\n", key, value + done = 1 + } + flush_blanks() + cur = $0 + gsub(/^\[|\]$/, "", cur) + in_section = (cur == section) + if (in_section) section_found = 1 + print + next + } + { + if (in_section && !done) { + if (match($0, "^[ \t]*" key "[ \t]*=")) { + flush_blanks() + printf "%s = %s\n", key, value + done = 1 + next + } + if ($0 ~ /^[ \t]*$/) { + blank_count++ + next + } + flush_blanks() + print + next + } + print + } + END { + if (in_section && !done) { + printf "%s = %s\n", key, value + done = 1 + } + flush_blanks() + if (!section_found) { + if (NR > 0) print "" + printf "[%s]\n", section + printf "%s = %s\n", key, value + } + } + ' "$file" > "$tmp"; then + rm -f "$tmp" + log_error "ini_set: awk failed on ${file} (${section}.${key}), config left untouched." + return 1 + fi + + mv "$tmp" "$file" +}