diff --git a/scripts/cli/commands/instance/cli_instance_commands.sh b/scripts/cli/commands/instance/cli_instance_commands.sh index f25f176..86dd98d 100644 --- a/scripts/cli/commands/instance/cli_instance_commands.sh +++ b/scripts/cli/commands/instance/cli_instance_commands.sh @@ -68,6 +68,14 @@ cliHandleInstanceCommands() instanceList "$type" # $type optional = filter by app type ;; + "repair") + # Rewrites files in the instance's TEMPLATE dir only — no container is + # touched, nothing is reinstalled — so it does not go through the task + # system the way create/remove do. $type holds the optional slug + # (positional reuse), $name an optional --dry-run. + instanceRepair "$type" "$name" + ;; + *) cliShowInstanceHelp ;; diff --git a/scripts/cli/commands/instance/cli_instance_header.sh b/scripts/cli/commands/instance/cli_instance_header.sh index 7368b28..2d52842 100644 --- a/scripts/cli/commands/instance/cli_instance_header.sh +++ b/scripts/cli/commands/instance/cli_instance_header.sh @@ -24,5 +24,12 @@ cliShowInstanceHelp() echo " CFG_DOMAIN_n is configured." echo " libreportal instance remove [slug*] - Uninstall + remove an instance (e.g. bookstack_blog)" echo " libreportal instance list [type] - List instances (all, or just for one app type)" + echo " libreportal instance repair [slug] [--dry-run]" + echo " - Fix instances cloned before the tool/auth" + echo " renames landed: their Tools actions error with" + echo " 'has no tool', and their admin-password resets" + echo " write to the BASE app's config. Rewrites the" + echo " template only — no container is touched. Safe" + echo " to run repeatedly; healthy instances are a no-op." echo "" } diff --git a/scripts/instance/instance_create.sh b/scripts/instance/instance_create.sh index 5ec10b4..d1ef5e6 100644 --- a/scripts/instance/instance_create.sh +++ b/scripts/instance/instance_create.sh @@ -612,6 +612,135 @@ instanceList() { done } +# --------------------------------------------------------------------------- +# Repair instances cloned before _instanceRewriteTools learned three of its +# renames — the infix form (authAdapter__), the bare app argument +# to the auth helpers, and the ucfirst tool entry point. A clone made by the old +# code is not broken in a way that announces itself: +# +# * every Tools action answered "App '' has no tool ''", because +# dockerAppRunTool wants app and the clone kept the type's +# * authPersistCfg wrote the instance's new admin credential into the +# BASE app's config, so the password on screen did not open either app +# * the clone defined the base app's adapter and tool names while its bodies +# pointed at the instance's container. Both definitions were live and the +# loader kept whichever it sourced last, i.e. whichever order find(1) +# happened to return — so the BASE app's user tools could administer the +# instance's container, silently, on nothing but a filesystem coincidence. +# +# Fixing the generator does nothing for a clone already on disk, hence this. +# +# instanceRepair [slug] [--dry-run] — all instances when no slug is given. +_instanceRepairFile() { + local type="$1" slug="$2" f="$3" + local g=$'\001' + + # Two of the three renames match their own output — appMattermost_teest… + # still starts with appMattermost, and _mattermost_teest_ still contains + # _mattermost_ — so a second pass would append the id half again + # (appMattermost_teest_teestListUsers). Park the already-correct spellings + # behind a sentinel first and restore them after, which makes the whole + # repair idempotent: running it on a healthy instance is a no-op, and a run + # interrupted halfway can simply be run again. \001 never occurs in shell or + # JSON source, and _instanceRewriteTools already uses it for the same reason. + # + # Three spellings, not two. A clone from the old code is only PARTLY wrong — + # its suffix hooks (appSetupComposeTags_) were always renamed correctly, + # and those end at the slug with no trailing underscore, so guard A misses + # them while the infix rule happily reads __ + the id half + () and + # appends the id a second time: appSetupComposeTags_nextcloud_family_family. + # Guard C is what keeps the repair from breaking the half that was fine. + sed -i -E "s/_${slug}_/${g}A${g}/g; s/\bapp${slug^}/${g}B${g}/g; s/_${slug}\(\)/${g}C${g}/g" "$f" + + sed -i -E "s/\b([A-Za-z_][A-Za-z0-9_]*)_${type}_([A-Za-z0-9_]+)(\(\))/\1_${slug}_\2\3/g" "$f" + sed -i -E "s/(\b(authAdapterCall|authAdapterCanDo|authPersistCfg)[[:space:]]+)${type}\b/\1${slug}/g" "$f" + sed -i -E "s/\bapp${type^}([A-Za-z0-9_]*)(\(\))/app${slug^}\1\2/g" "$f" + + sed -i -E "s/${g}A${g}/_${slug}_/g; s/${g}B${g}/app${slug^}/g; s/${g}C${g}/_${slug}()/g" "$f" +} + +instanceRepair() { + local want_slug="" dry="false" a + for a in "$@"; do + case "$a" in + --dry-run|-n) dry="true" ;; + "") ;; + *) want_slug="$a" ;; + esac + done + + local dir folder instance_of cfg f d + local scanned=0 touched=0 files=0 + for dir in "${install_containers_dir%/}"/*/; do + folder="$(basename "$dir")" + [[ -n "$want_slug" && "$folder" != "$want_slug" ]] && continue + cfg="$dir$folder.config" + [[ -f "$cfg" ]] || continue + instance_of=$(grep -E "^CFG_${folder^^}_INSTANCE_OF=" "$cfg" 2>/dev/null | head -n1) + [[ -z "$instance_of" ]] && continue + instance_of="${instance_of#*=}"; instance_of="${instance_of//\"/}"; instance_of="${instance_of//$'\r'/}" + [[ -n "$instance_of" ]] || continue + scanned=$((scanned + 1)) + + local before after changed_here=0 + for d in "$dir/tools" "$dir/scripts"; do + [[ -d "$d" ]] || continue + for f in "$d"/*.sh "$d"/*.json; do + [[ -e "$f" ]] || continue + before="$(cksum < "$f")" + if [[ "$dry" == "true" ]]; then + # Repair a copy so the report is exact without writing. + local tmp; tmp="$(mktemp)" || continue + cp "$f" "$tmp" + _instanceRepairFile "$instance_of" "$folder" "$tmp" + after="$(cksum < "$tmp")" + rm -f "$tmp" + else + _instanceRepairFile "$instance_of" "$folder" "$f" + after="$(cksum < "$f")" + fi + [[ "$before" == "$after" ]] && continue + changed_here=1 + files=$((files + 1)) + isNotice " ${folder}: $(basename "$f")" + done + done + + if (( changed_here )); then + touched=$((touched + 1)) + else + isSuccessful "$folder (instance of $instance_of) — already correct." + fi + done + + if (( scanned == 0 )); then + [[ -n "$want_slug" ]] && { isError "No such instance '$want_slug'."; return 1; } + isNotice "No instances found — nothing to repair." + return 0 + fi + + if (( files == 0 )); then + isSuccessful "Checked $scanned instance(s); all correct." + return 0 + fi + + if [[ "$dry" == "true" ]]; then + isNotice "Dry run — $files file(s) across $touched instance(s) WOULD be rewritten. Nothing was changed." + return 0 + fi + + isSuccessful "Repaired $files file(s) across $touched instance(s)." + # The function names just changed, so anything keyed on them is stale: the + # manifest maps names to files, and the collisions this clears are precisely + # the ones that made the base app's tools reachable under the instance's. + if declare -F lpRegenArrays >/dev/null 2>&1; then + isNotice "Rebuilding the function manifest…" + lpRegenArrays force >/dev/null 2>&1 || \ + isNotice "Manifest rebuild reported an error — run 'libreportal regen arrays --force'." + fi + isNotice "Restart the WebUI/task processor (or wait for the next poll) so the long-running loaders pick up the new names." +} + # Remove an instance: standard uninstall (deployed dir + DB + compose down) then # drop the instance's template clone. Refuses to touch a non-instance app. instanceRemove() { diff --git a/scripts/source/files/arrays/function_manifest.sh b/scripts/source/files/arrays/function_manifest.sh index e376820..e0b66c8 100644 --- a/scripts/source/files/arrays/function_manifest.sh +++ b/scripts/source/files/arrays/function_manifest.sh @@ -589,6 +589,8 @@ declare -gA LP_FN_MAP=( [instanceIdPart]="instance/instance_create.sh" [instanceList]="instance/instance_create.sh" [instanceRemove]="instance/instance_create.sh" + [instanceRepair]="instance/instance_create.sh" + [_instanceRepairFile]="instance/instance_create.sh" [_instanceRewriteCompose]="instance/instance_create.sh" [_instanceRewriteConfigPorts]="instance/instance_create.sh" [_instanceRewriteTools]="instance/instance_create.sh" @@ -681,6 +683,7 @@ declare -gA LP_FN_MAP=( [lpNotifyTaskResult]="cli/commands/notify/cli_notify_commands.sh" [lpRegen]="webui/webui_regen.sh" [lpRegenArrays]="webui/webui_regen.sh" + [_lpRegenOrphanedApp]="webui/webui_regen.sh" [_lpRegenStale]="webui/webui_regen.sh" [lpRegenWebui]="webui/webui_regen.sh" [lpReleaseBaseUrl]="source/fetch.sh" @@ -1768,6 +1771,8 @@ declare -gA LP_FN_ROOT=( [instanceIdPart]="scripts" [instanceList]="scripts" [instanceRemove]="scripts" + [instanceRepair]="scripts" + [_instanceRepairFile]="scripts" [_instanceRewriteCompose]="scripts" [_instanceRewriteConfigPorts]="scripts" [_instanceRewriteTools]="scripts" @@ -1860,6 +1865,7 @@ declare -gA LP_FN_ROOT=( [lpNotifyTaskResult]="scripts" [lpRegen]="scripts" [lpRegenArrays]="scripts" + [_lpRegenOrphanedApp]="scripts" [_lpRegenStale]="scripts" [lpRegenWebui]="scripts" [lpReleaseBaseUrl]="scripts" @@ -2984,6 +2990,8 @@ instanceCreate() { unset -f instanceCreate; __lpAutoload "${install_scripts_dir} 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 "$@"; } instanceRemove() { unset -f instanceRemove; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; instanceRemove "$@"; } +instanceRepair() { unset -f instanceRepair; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; instanceRepair "$@"; } +_instanceRepairFile() { unset -f _instanceRepairFile; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; _instanceRepairFile "$@"; } _instanceRewriteCompose() { unset -f _instanceRewriteCompose; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; _instanceRewriteCompose "$@"; } _instanceRewriteConfigPorts() { unset -f _instanceRewriteConfigPorts; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; _instanceRewriteConfigPorts "$@"; } _instanceRewriteTools() { unset -f _instanceRewriteTools; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; _instanceRewriteTools "$@"; } @@ -3076,6 +3084,7 @@ lpNotifySend() { unset -f lpNotifySend; __lpAutoload "${install_scripts_dir}cli/ lpNotifyTaskResult() { unset -f lpNotifyTaskResult; __lpAutoload "${install_scripts_dir}cli/commands/notify/cli_notify_commands.sh"; lpNotifyTaskResult "$@"; } lpRegen() { unset -f lpRegen; __lpAutoload "${install_scripts_dir}webui/webui_regen.sh"; lpRegen "$@"; } lpRegenArrays() { unset -f lpRegenArrays; __lpAutoload "${install_scripts_dir}webui/webui_regen.sh"; lpRegenArrays "$@"; } +_lpRegenOrphanedApp() { unset -f _lpRegenOrphanedApp; __lpAutoload "${install_scripts_dir}webui/webui_regen.sh"; _lpRegenOrphanedApp "$@"; } _lpRegenStale() { unset -f _lpRegenStale; __lpAutoload "${install_scripts_dir}webui/webui_regen.sh"; _lpRegenStale "$@"; } lpRegenWebui() { unset -f lpRegenWebui; __lpAutoload "${install_scripts_dir}webui/webui_regen.sh"; lpRegenWebui "$@"; } lpReleaseBaseUrl() { unset -f lpReleaseBaseUrl; __lpAutoload "${install_scripts_dir}source/fetch.sh"; lpReleaseBaseUrl "$@"; }