From 66f981af817c9217d93b90f119be40ba26e144fb Mon Sep 17 00:00:00 2001 From: Damien Date: Fri, 31 Jul 2026 17:34:45 +0200 Subject: [PATCH 1/3] feat(lib): add ini_set idempotent INI merge helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openbao's "write config only if absent" pattern would silently skip newly-required keys on a rejoué script. ini_set() merges key by key instead: creates file/section/key as needed, replaces an existing key's value in place, no-ops when already correct, and never touches other sections — so a duplicate key name across sections (e.g. [metrics] ENABLED vs [actions] ENABLED) stays scoped correctly. Atomic write via tmpfile + mv. First OS-agnostic helper in the file, documented as such in the header contract. Refs #18 --- lib/common.sh | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/lib/common.sh b/lib/common.sh index 8caad69..605fea5 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,69 @@ 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") + + awk -v section="$section" -v key="$key" -v value="$value" ' + /^\[.*\]$/ { + if (in_section && !done) { + printf "%s = %s\n", key, value + done = 1 + } + cur = $0 + gsub(/^\[|\]$/, "", cur) + in_section = (cur == section) + if (in_section) section_found = 1 + print + next + } + { + if (in_section && !done && match($0, "^[ \t]*" key "[ \t]*=")) { + printf "%s = %s\n", key, value + done = 1 + next + } + print + } + END { + if (in_section && !done) { + printf "%s = %s\n", key, value + done = 1 + } + if (!section_found) { + if (NR > 0) print "" + printf "[%s]\n", section + printf "%s = %s\n", key, value + } + } + ' "$file" > "$tmp" + + mv "$tmp" "$file" +} -- 2.55.0 From 905166d1cca7dd256901113b3629e1d149ae8d79 Mon Sep 17 00:00:00 2001 From: Damien Date: Fri, 31 Jul 2026 18:46:33 +0200 Subject: [PATCH 2/3] fix(lib): preserve file mode/ownership across ini_set's atomic write MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mktemp defaults to 0600 root:root. The tmpfile+mv swap in ini_set was carrying that over onto the replaced config, silently locking out whatever service account owned the original file (e.g. gitea:www-data on Gitea's app.ini — the service failed to start with a permission denied on its own config after the very first ini_set call). Restore the original file's mode and ownership on the tmpfile before the mv. Verified on both BusyBox (Alpine) and GNU coreutils (Debian) stat -c. Refs #18 --- lib/common.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/common.sh b/lib/common.sh index 605fea5..39765c3 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -151,6 +151,14 @@ ini_set() { 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 -v section="$section" -v key="$key" -v value="$value" ' /^\[.*\]$/ { if (in_section && !done) { -- 2.55.0 From 1217b26ff968837b6b6688d8fd35766ea58554ea Mon Sep 17 00:00:00 2001 From: Damien Date: Sat, 1 Aug 2026 11:18:48 +0200 Subject: [PATCH 3/3] =?UTF-8?q?fix(lib):=20ini=5Fset=20n'=C3=A9crase=20plu?= =?UTF-8?q?s=20app.ini=20si=20awk=20=C3=A9choue,=20insertion=20avant=20les?= =?UTF-8?q?=20lignes=20vides?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deux corrections dans ini_set() suite à la review de #24 : - Le mv s'exécutait même si awk avait échoué en cours de route (OOM, signal, valeur exotique) : $tmp, créé par la redirection avant qu'awk ne tourne, pouvait être tronqué ou vide et venait quand même écraser le fichier original. mv est maintenant gaté sur le code de retour d'awk : en cas d'échec, $tmp est supprimé, une erreur est logguée, et la fonction retourne 1 sans toucher au fichier d'origine. - L'insertion d'une clé juste avant l'en-tête de section suivante atterrissait après la ou les lignes vides de fin de section plutôt qu'avant, rendant le fichier plus difficile à lire au fil des appels répétés. Les lignes vides rencontrées section active/clé pas encore trouvée sont désormais bufferisées et réémises juste après la clé insérée (ou en fin de fichier si la section n'est jamais refermée), sans toucher à l'ordre dans tous les autres cas. --- lib/common.sh | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/lib/common.sh b/lib/common.sh index 39765c3..23a11cf 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -159,12 +159,27 @@ ini_set() { chmod "$(stat -c '%a' "$file")" "$tmp" 2>/dev/null || true chown "$(stat -c '%u:%g' "$file")" "$tmp" 2>/dev/null || true - awk -v section="$section" -v key="$key" -v value="$value" ' + # 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) @@ -173,9 +188,19 @@ ini_set() { next } { - if (in_section && !done && match($0, "^[ \t]*" key "[ \t]*=")) { - printf "%s = %s\n", key, value - done = 1 + 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 @@ -185,13 +210,18 @@ ini_set() { 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" + ' "$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" } -- 2.55.0