Refuse to auto-detect a container when the prefix is ambiguous

On this host "authelia" matches both Authelia and Authelia-Secondary, and taking the first
match writes the wrong instance into the conf every other script then trusts.
This commit is contained in:
Gmer4Lfe
2026-08-01 22:44:23 -04:00
parent 7669cd75b8
commit 132a657f00
+124 -3
View File
@@ -55,6 +55,97 @@
# GITEA_CONTAINER fuzzy match from docker ps
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# 1. Resolve this host's conf from MY_ID
# 2. Per service, locate its container and read its own config file:
# arrs → config.xml via the container's /config volume mount
# SABnzbd → sabnzbd.ini
# slskd → config.yml
# qBit → qBittorrent.conf (plaintext WebUI credentials only)
# Emby/JF → docker port bindings and /transcode mount
# 3. Write each value only if the conf field is EMPTY, unless --overwrite
# 4. Push the updated conf to partners via conf_sync.sh, unless --no-push
#
# Container names are resolved by _resolve_container(): an exact name match wins, otherwise
# a prefix match must be unambiguous or the field is skipped.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Never Overwrite a Human's Value
# Only empty fields are populated. A value already in the conf was either set deliberately
# or populated from a service that has since changed — either way, the file wins over
# detection. --overwrite exists for deliberate re-sync after a key rotation.
#
# Read From the Service, Not From Assumption
# Every value comes out of the service's own config file or docker metadata — ports from
# actual port bindings, paths from actual volume mounts. Nothing is derived from naming
# convention where the real value is readable.
#
# Refuse to Guess a Container
# An ambiguous prefix skips the field rather than picking one. Writing the wrong container
# name is worse than writing nothing: an empty field is visibly incomplete and gets fixed,
# while a wrong one silently points the whole stack at the wrong instance. This host has a
# live example — "authelia" prefix-matches both Authelia and Authelia-Secondary.
#
# Push Immediately After Populating
# Fresh credentials go to partners right away rather than waiting for the next scheduled
# conf sync, so a partner is never authenticating with a key this host has already rotated.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Root Enforcement
# Reads service config files owned by container users and writes the host conf.
#
# Host Detection
# detect_hosts() resolves MY_ID, which selects which host conf is written. Populating the
# wrong host's conf would write this machine's credentials into a partner's file.
#
# Conf Existence Guard
# Aborts if the resolved host conf does not exist, rather than creating a partial one.
#
# Empty-Field-Only Writes
# Existing values are preserved unless --overwrite is passed explicitly.
#
# Container Ambiguity Guard
# _resolve_container() refuses a prefix matching more than one container, warning with the
# full match list. Exact name matches short-circuit and are never treated as ambiguous.
#
# Missing Service Tolerance
# A service that is not installed on this host is skipped with a log line. Absence is a
# valid configuration, not a failure.
#
# Map Block Validation
# Associative-array entries are only inserted when the target map actually exists in the
# conf; a missing map warns and skips instead of appending an orphaned entry.
#
# Dry Run Support
# --dry-run reports every value it would write, truncated, and writes nothing.
#
# Credential Truncation in Output
# Detected secrets are printed truncated (first 8 chars) so a populate run can be pasted
# into a log or issue without leaking full API keys.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# Reads and writes: Configurations/<hostid>.conf and Configurations/master.conf
#
# master.conf
#
# DOCKER_APPDATA_BASE
# Fallback appdata root when a container exposes no /config mount to read from.
#
# Everything else this script touches is a field it populates rather than one it consumes —
# see AUTO-DETECTED FIELDS above for the full list.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
@@ -174,11 +265,41 @@ _set_conf_map_entry() {
(( UPDATED++ ))
}
# ── Helper: resolve a container name from a prefix, refusing ambiguity ────────
# grep -m1 silently returns whichever name docker happens to list first. On this very host
# "^authelia" matches both Authelia (9091, primary) and Authelia-Secondary (9092), and -m1
# picks the secondary — writing the wrong container into the conf that every other script
# then trusts. An exact match wins outright; otherwise a prefix match must be unambiguous.
# Same rule detect_hosts() applies to host identity: exactly one candidate, or none.
_resolve_container() {
local pattern="$1"
local all exact matches count
all=$(docker ps -a --format '{{.Names}}' 2>/dev/null)
[[ -z "$all" ]] && return 1
# Exact name match short-circuits — "Authelia" is not ambiguous with "Authelia-Secondary"
exact=$(printf '%s\n' "$all" | grep -ixm1 -- "$pattern")
[[ -n "$exact" ]] && { echo "$exact"; return 0; }
matches=$(printf '%s\n' "$all" | grep -i -- "^${pattern}")
count=$(printf '%s\n' "$matches" | grep -c .)
if [[ "$count" -gt 1 ]]; then
warn "Container prefix '${pattern}' is ambiguous — matches: $(printf '%s' "$matches" | tr '\n' ' ')"
warn " Refusing to guess. Set the container name manually in host*.conf."
return 1
fi
[[ "$count" -eq 1 ]] && { printf '%s\n' "$matches"; return 0; }
return 1
}
# ── Helper: find arr config dir via docker volume mount ───────────────────────
_arr_config_dir() {
local pattern="$1"
local container_name
container_name=$(docker ps -a --format '{{.Names}}' 2>/dev/null | grep -im1 "^${pattern}")
container_name=$(_resolve_container "$pattern") || return 1
[[ -z "$container_name" ]] && return 1
local config_path
@@ -239,7 +360,7 @@ _set_conf_var "${MY_ID}_SSH_KEY" "/root/.ssh/${owner}_rsync_automation" "
for arr in radarr sonarr lidarr; do
arr_upper="${arr^^}"
arr_container=$(docker ps -a --format '{{.Names}}' 2>/dev/null | grep -im1 "^${arr}")
arr_container=$(_resolve_container "$arr")
config_dir=$(_arr_config_dir "$arr") || {
log "${arr_upper}: no running container found — skipping"
continue
@@ -341,7 +462,7 @@ qbit_dir=$(_arr_config_dir "qbittorrent") && {
transcode_dir=""
for pattern in "emby" "jellyfin"; do
container=$(docker ps -a --format '{{.Names}}' 2>/dev/null | grep -im1 "^${pattern}")
container=$(_resolve_container "$pattern") || continue
[[ -z "$container" ]] && continue
case "$pattern" in