Make instance hooks target their own container and directory
Audit of per-app hooks/tools found 19 of 33 apps whose helpers would have
operated on the BASE app after cloning. Two general causes, both fixed by
rewriting classes rather than patching apps:
- Container references escaped the rewrite whenever a flag sat between the
docker verb and the target (`docker exec -u git gitea-service …`), since the
old rule only matched a name immediately after the verb — and the hyphenated
form missed the `<type>_` rule too. Hook trees now get the same discovered
identity rename the compose does, reading names from the TYPE's compose since
the clone has already been rewritten by then. Safe to apply broadly: the
compose pass runs first and aborts for any app whose identities aren't
<type>-prefixed, so a bare word like stoat's `api` never reaches it.
- Hooks that build the deployed path as "${containers_dir}<type>/..." instead
of "$containers_dir$app_name/..." read and WROTE the base app's files —
adguard's auth adapter edits AdGuardHome.yaml, so an instance would have
rewritten the original's config. The trailing slash is optional in the match:
dashy tests [[ -d "${containers_dir}dashy" ]] and gluetun cds into it, both
ending at the quote. Only the first path component is touched, so
${containers_dir}prometheus/prometheus/... keeps its inner segment.
Re-audit: all 33 apps with hook trees are clean. Stoat still leaks, but it is
refused at the compose stage and never reaches this code.
Volumes audited too, and need no changes: no app uses named volumes, so the
./relative bind mounts every app uses resolve inside each instance's own
deployed dir. The absolute sources that exist are host or in-container paths
correctly shared read-only (/etc/localtime, /sys, /etc/ssl/certs). Jitsi's
${CONFIG} is set per-app by its own hook to $containers_dir$app_name/... and so
follows the slug.
Bookstack's rewritten tool tree is byte-identical to the live instance's across
all 8 files, so the running instances are unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
1d0f043bb5
commit
166acb9b7c
@ -46,6 +46,55 @@ updaterTagExists() {
|
||||
[ "$code" = "200" ]
|
||||
}
|
||||
|
||||
# Bump the Nth (0-based) numeric component of a tag by one and zero every
|
||||
# component after it, so a bump means what a version bump means:
|
||||
# "v1.158.2", 1 -> "v1.159.0" "31-fpm-alpine", 0 -> "32-fpm-alpine"
|
||||
# Shape is preserved by construction (only digits change), which is what lets
|
||||
# callers compare shapes to reject nonsense candidates.
|
||||
updaterTagBumpAt() {
|
||||
printf '%s' "$1" | awk -v idx="$2" '{
|
||||
out=""; n=0; s=$0
|
||||
while (match(s, /[0-9]+/)) {
|
||||
pre = substr(s, 1, RSTART-1)
|
||||
num = substr(s, RSTART, RLENGTH) + 0
|
||||
s = substr(s, RSTART+RLENGTH)
|
||||
if (n == idx) num = num + 1; else if (n > idx) num = 0
|
||||
out = out pre num
|
||||
n++
|
||||
}
|
||||
print out s
|
||||
}'
|
||||
}
|
||||
|
||||
# The immediately-next PUBLISHED version above $1 in repo $2, or "" if there is
|
||||
# none. THE step primitive: everything else here is built on it being right.
|
||||
#
|
||||
# It exists because bumping only the last component cannot cross a component
|
||||
# boundary. v1.158.0 -> v1.158.1 -> v1.158.2 … never arrives at v1.159.0, so a
|
||||
# ladder built that way gave up on the single most common versioning scheme
|
||||
# there is, and Synapse — which publishes v1.159.0 and no v1.158.1 at all —
|
||||
# could not be climbed one rung.
|
||||
#
|
||||
# So consider a bump of EVERY component (major, minor, patch), keep only the
|
||||
# candidates that actually exist upstream, and take the SMALLEST of those. That
|
||||
# is the next release by definition, whether it lands in the patch position or
|
||||
# crosses into a new major. Candidates that change the tag's shape are dropped,
|
||||
# so 31-fpm-alpine never becomes 31-apache. Costs one lookup per component.
|
||||
updaterNextRung() {
|
||||
local cur="$1" repo="$2"
|
||||
local shape; shape="$(updaterTagShape "$cur")"
|
||||
local ncomp; ncomp="$(printf '%s' "$cur" | grep -oE '[0-9]+' | wc -l | tr -d ' ')"
|
||||
[ "${ncomp:-0}" -gt 0 ] 2>/dev/null || return 0
|
||||
local best="" i cand
|
||||
for ((i=0; i<ncomp; i++)); do
|
||||
cand="$(updaterTagBumpAt "$cur" "$i")"
|
||||
[ "$(updaterTagShape "$cand")" = "$shape" ] || continue
|
||||
updaterTagExists "$repo" "$cand" || continue
|
||||
if [ -z "$best" ] || updaterTagGreater "$best" "$cand"; then best="$cand"; fi
|
||||
done
|
||||
printf '%s' "$best"
|
||||
}
|
||||
|
||||
# Bump the LAST numeric component of a tag by one: v4.2 -> v4.3, 31-fpm-alpine
|
||||
# -> 32-fpm-alpine, v0.16 -> v0.17.
|
||||
updaterTagIncrement() {
|
||||
@ -90,13 +139,19 @@ updaterVersionLadder() {
|
||||
updaterTagGreater "$target" "$cur" || return 0 # never downgrade
|
||||
|
||||
local -a rungs=()
|
||||
local probe="$cur" i
|
||||
local probe="$cur" i next
|
||||
for ((i=0; i<64; i++)); do # bounded: no runaway
|
||||
probe="$(updaterTagIncrement "$probe")"
|
||||
updaterTagGreater "$probe" "$target" && break # overshot
|
||||
if updaterTagExists "$repo" "$probe"; then
|
||||
rungs+=("$probe")
|
||||
fi
|
||||
# Step to the next version that EXISTS, rather than incrementing blindly
|
||||
# and testing. Same guarantee as before — every rung is probed, so a
|
||||
# release missing from any listing can still never be skipped — but it
|
||||
# can now cross a component boundary, which blind incrementing could
|
||||
# not: v1.158.0 -> v1.159.0 was unreachable and the whole ladder failed
|
||||
# closed on it.
|
||||
next="$(updaterNextRung "$probe" "$repo")"
|
||||
[ -n "$next" ] || break # nothing further published
|
||||
updaterTagGreater "$next" "$target" && break # overshot
|
||||
rungs+=("$next")
|
||||
probe="$next"
|
||||
[ "$probe" = "$target" ] && break
|
||||
done
|
||||
|
||||
|
||||
@ -136,6 +136,24 @@ _instanceSetLocalOnly() {
|
||||
done
|
||||
}
|
||||
|
||||
# Every container identity a compose declares: its service keys (from the
|
||||
# SERVICE_TAG_<n> markers) plus its container_name values, deduped.
|
||||
#
|
||||
# Commented-out lines are excluded. Several templates ship an optional sidecar
|
||||
# parked behind `#` (adguard-exporter, pihole-exporter, …); harvesting those
|
||||
# renames a service that isn't running AND mangles the image name in the same
|
||||
# block, leaving a trap for whoever uncomments it later. A live service line
|
||||
# carries its SERVICE_TAG mid-line, so dropping lines that *start* with # keeps
|
||||
# every real one.
|
||||
_instanceComposeIdentities() {
|
||||
local f="$1"
|
||||
[[ -f "$f" ]] || return 0
|
||||
grep -v '^[[:space:]]*#' "$f" 2>/dev/null \
|
||||
| grep -oE '#LIBREPORTAL\|SERVICE_TAG_[0-9]+\|[^[:space:]]+|container_name:[[:space:]]*[^[:space:]]+' \
|
||||
| sed -E 's/.*SERVICE_TAG_[0-9]+\|//; s/container_name:[[:space:]]*//' \
|
||||
| sed '/^$/d' | sort -u
|
||||
}
|
||||
|
||||
# Rewrite identity-bearing tokens in the cloned compose so the instance's
|
||||
# containers, Traefik routers and backup labels are unique. image: lines are
|
||||
# deliberately left untouched (rule 2/3 anchor on their line prefix; the
|
||||
@ -163,12 +181,7 @@ _instanceRewriteCompose() {
|
||||
# it later. A real service line carries its SERVICE_TAG mid-line, so
|
||||
# dropping lines that *start* with # keeps every live one.
|
||||
local -a tokens=()
|
||||
mapfile -t tokens < <(
|
||||
grep -v '^[[:space:]]*#' "$f" 2>/dev/null | {
|
||||
grep -oE '#LIBREPORTAL\|SERVICE_TAG_[0-9]+\|[^[:space:]]+|container_name:[[:space:]]*[^[:space:]]+' \
|
||||
| sed -E 's/.*SERVICE_TAG_[0-9]+\|//; s/container_name:[[:space:]]*//'
|
||||
} | sed '/^$/d' | sort -u
|
||||
)
|
||||
mapfile -t tokens < <(_instanceComposeIdentities "$f")
|
||||
|
||||
# Split into compound (<type> + separator + suffix) and bare (<type> exactly).
|
||||
# Only compound names are safe to rewrite everywhere: the bare app name also
|
||||
@ -249,9 +262,36 @@ _instanceRewriteCompose() {
|
||||
# its own container and don't collide (by function name) with the type's. This is
|
||||
# best-effort: it keeps the tool tree internally consistent, but apps with unusual
|
||||
# tool wiring may need review before their flag is flipped.
|
||||
#
|
||||
# src_compose is the TYPE's original compose — the identities are read from there,
|
||||
# not from the clone, because the clone has already been rewritten to the new names
|
||||
# by the time this runs.
|
||||
_instanceRewriteTools() {
|
||||
local type="$1" slug="$2" dir="$3"
|
||||
local type="$1" slug="$2" dir="$3" src_compose="$4"
|
||||
local d f base
|
||||
|
||||
# Container identities as they appear in hook/tool code. The per-token rename
|
||||
# below is what catches `docker exec -u git gitea-service …`: the docker-verb
|
||||
# rule further down only matches a container name sitting immediately after the
|
||||
# verb, so any intervening flag (-u, -i, -e VAR=…) hid the target from it, and
|
||||
# the hyphenated form escaped the `<type>_` rule as well. An instance's auth
|
||||
# adapter then administered the BASE app.
|
||||
#
|
||||
# Safe to apply broadly here: _instanceRewriteCompose runs first and aborts the
|
||||
# whole create for any app whose identities aren't <type>-prefixed, so a bare
|
||||
# word like stoat's `api` or `web` can never reach this rewrite.
|
||||
local -a ids=() olds=() news=()
|
||||
if [[ -n "$src_compose" && -f "$src_compose" ]]; then
|
||||
mapfile -t ids < <(_instanceComposeIdentities "$src_compose" \
|
||||
| awk '{print length"\t"$0}' | sort -rn | cut -f2-)
|
||||
local t t_norm type_norm="${type//-/_}"
|
||||
for t in "${ids[@]}"; do
|
||||
t_norm="${t//-/_}"
|
||||
[[ "$t_norm" == "$type_norm" ]] && continue # bare name: rules below
|
||||
[[ "$t_norm" == "${type_norm}_"* ]] || continue # unreachable, see above
|
||||
olds+=("$t"); news+=("${slug}${t:${#type}}")
|
||||
done
|
||||
fi
|
||||
for d in "$dir/tools" "$dir/scripts"; do
|
||||
[[ -d "$d" ]] || continue
|
||||
for f in "$d/${type}_"*.sh "$d/${type}.tools.json"; do
|
||||
@ -277,6 +317,32 @@ _instanceRewriteTools() {
|
||||
# assignment forms — a blanket bare-<type> rewrite would hit image
|
||||
# names and prose.
|
||||
sed -i -E "s/(\b(container|container_name)=\")${type}(\")/\1${slug}\3/g" "$f"
|
||||
|
||||
# The app's own deployed directory. Hooks that build it from
|
||||
# "${containers_dir}<type>/..." instead of "$containers_dir$app_name/..."
|
||||
# read and WRITE the base app's files — adguard's auth adapter edits
|
||||
# ${containers_dir}adguard/conf/AdGuardHome.yaml, so an instance would
|
||||
# have rewritten the original's config. Anchored on containers_dir so a
|
||||
# bare mention of the app name in prose is left alone.
|
||||
# Trailing / is NOT required: dashy tests [[ -d "${containers_dir}dashy" ]]
|
||||
# and gluetun does (cd "${containers_dir}gluetun" && …), both ending at
|
||||
# the quote. Only the first path component is touched, so a data subdir
|
||||
# that repeats the app name (${containers_dir}prometheus/prometheus/…)
|
||||
# keeps its inner segment.
|
||||
sed -i -E "s@(\\\$\{containers_dir\})${type}([^A-Za-z0-9_-]|$)@\1${slug}\2@g" "$f"
|
||||
sed -i -E "s@(\\\$\{install_containers_dir\}/?)${type}([^A-Za-z0-9_-]|$)@\1${slug}\2@g" "$f"
|
||||
|
||||
# Compose identities (longest-first, via placeholders — same reasoning
|
||||
# as the compose rewrite: a short name must not match inside a longer).
|
||||
local i ph
|
||||
for i in "${!olds[@]}"; do
|
||||
ph=$'\001'"LPTOOL${i}"$'\001'
|
||||
sed -i -E "s/\b${olds[$i]}\b/${ph}/g" "$f"
|
||||
done
|
||||
for i in "${!olds[@]}"; do
|
||||
ph=$'\001'"LPTOOL${i}"$'\001'
|
||||
sed -i "s|${ph}|${news[$i]}|g" "$f"
|
||||
done
|
||||
done
|
||||
done
|
||||
}
|
||||
@ -378,7 +444,7 @@ instanceCreate() {
|
||||
return 1
|
||||
fi
|
||||
_instanceRewriteConfigPorts "$type" "$slug" "$cfg"
|
||||
_instanceRewriteTools "$type" "$slug" "$inst_dir"
|
||||
_instanceRewriteTools "$type" "$slug" "$inst_dir" "$type_dir/docker-compose.yml"
|
||||
|
||||
if [[ "$local_only" == "true" ]]; then
|
||||
_instanceSetLocalOnly "$slug_u" "$cfg"
|
||||
|
||||
@ -578,6 +578,7 @@ declare -gA LP_FN_MAP=(
|
||||
[installSwapfile]="install/install_swapfile.sh"
|
||||
[installUFW]="install/install_ufw.sh"
|
||||
[installUFWDocker]="install/install_ufwd.sh"
|
||||
[_instanceComposeIdentities]="instance/instance_create.sh"
|
||||
[instanceCreate]="instance/instance_create.sh"
|
||||
[instanceIdPart]="instance/instance_create.sh"
|
||||
[instanceList]="instance/instance_create.sh"
|
||||
@ -1728,6 +1729,7 @@ declare -gA LP_FN_ROOT=(
|
||||
[installSwapfile]="scripts"
|
||||
[installUFW]="scripts"
|
||||
[installUFWDocker]="scripts"
|
||||
[_instanceComposeIdentities]="scripts"
|
||||
[instanceCreate]="scripts"
|
||||
[instanceIdPart]="scripts"
|
||||
[instanceList]="scripts"
|
||||
@ -2914,6 +2916,7 @@ installSSLCertificate() { unset -f installSSLCertificate; __lpAutoload "${instal
|
||||
installSwapfile() { unset -f installSwapfile; __lpAutoload "${install_scripts_dir}install/install_swapfile.sh"; installSwapfile "$@"; }
|
||||
installUFW() { unset -f installUFW; __lpAutoload "${install_scripts_dir}install/install_ufw.sh"; installUFW "$@"; }
|
||||
installUFWDocker() { unset -f installUFWDocker; __lpAutoload "${install_scripts_dir}install/install_ufwd.sh"; installUFWDocker "$@"; }
|
||||
_instanceComposeIdentities() { unset -f _instanceComposeIdentities; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; _instanceComposeIdentities "$@"; }
|
||||
instanceCreate() { unset -f instanceCreate; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; instanceCreate "$@"; }
|
||||
instanceIdPart() { unset -f instanceIdPart; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; instanceIdPart "$@"; }
|
||||
instanceList() { unset -f instanceList; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; instanceList "$@"; }
|
||||
|
||||
@ -223,26 +223,6 @@ updaterNewerVersionByList() {
|
||||
printf '%s' "$best"
|
||||
}
|
||||
|
||||
# Bump the Nth (0-based) numeric component of a tag by one and zero every
|
||||
# component after it, so a bump means what a version bump means:
|
||||
# "v1.158.2", 1 -> "v1.159.0" "31-fpm-alpine", 0 -> "32-fpm-alpine"
|
||||
# Shape is preserved by construction (only the digits change), which is what
|
||||
# lets the caller compare shapes to reject nonsense candidates.
|
||||
updaterTagBumpAt() {
|
||||
printf '%s' "$1" | awk -v idx="$2" '{
|
||||
out=""; n=0; s=$0
|
||||
while (match(s, /[0-9]+/)) {
|
||||
pre = substr(s, 1, RSTART-1)
|
||||
num = substr(s, RSTART, RLENGTH) + 0
|
||||
s = substr(s, RSTART+RLENGTH)
|
||||
if (n == idx) num = num + 1; else if (n > idx) num = 0
|
||||
out = out pre num
|
||||
n++
|
||||
}
|
||||
print out s
|
||||
}'
|
||||
}
|
||||
|
||||
# The newest tag reachable from $1 by PROBING for exact tags, most-significant
|
||||
# component first, climbing each as far as it goes.
|
||||
#
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user