The main sweep — ~260 call sites across ~100 files move from string
concatenation on a single root to appDir/storageAppDirs/storageAppConfigs.
On a single-root install the resolved paths are identical, so this is a
no-op until a location is registered.
Enumerators were the interesting half. `for d in "$containers_dir"/*/`
appears in the menus, the registry/artifact scanners and the DNS setup —
and a shell glob cannot list a rootless 751 tree at all, which is the
same bug config_find_file.sh already documents in a comment. Routing them
through storageAppDirs (which enumerates as the owning user) fixes that
alongside the multi-root work.
Three places needed judgement rather than substitution:
db_app_scan.sh deletes database rows and port allocations for apps whose
folder is missing, and reaps "empty" app dirs. With a storage location
unmounted, every app on it looks exactly like that. Each of those
branches now gates on appStorageAvailable first — an app on an unplugged
drive is skipped with a notice, never deleted.
instance_create.sh rewrites cloned hooks so an instance touches its own
directory instead of the base app's. Its sed matched ${containers_dir}<type>,
which this sweep just replaced with $(appDir <type>) — so it would have
silently stopped redirecting, and an instance would have written to the
original's files (the adguard auth adapter case its own comment warns
about). Now matches both appDir forms, verified against bare, quoted,
unrelated-app, legacy and prose cases.
peer_shell/peer_pull streamed and extracted relative to the primary root.
Both now use the app's own root, and peer_shell keeps a single-root
fallback since it runs as a restricted SSH shell with no LibrePortal env.
Also fixes a pre-existing bug found on the way: webui_app_config.sh
tested "$containers_dir/frontend/data/last_update", one level short of the
real tree under the libreportal app dir, so the WebUI refresh trigger
after a config update has never once fired.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
175 lines
9.1 KiB
Bash
Executable File
175 lines
9.1 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Print the compose output when `up -d` failed, so the log records WHY.
|
|
# Without this the only trace of a failed app install was "Started container for
|
|
# <app> (exit 1, up_app.sh:130)" — the compose output was captured into a
|
|
# variable and dropped. That is the difference between "navidrome's install
|
|
# failed because the image pull died" and a mystery nobody can answer ten days
|
|
# later. Tail-capped: a Dockerfile build can emit hundreds of lines, and the
|
|
# failure is always at the end. Called BEFORE checkSuccess, which may exit.
|
|
_upReportComposeFailure()
|
|
{
|
|
local app_name="$1" rc="$2" output="$3"
|
|
(( rc == 0 )) && return 0
|
|
[[ -z "$output" ]] && { isNotice "docker compose gave no output for $app_name (exit $rc)."; return 0; }
|
|
isNotice "docker compose output for $app_name (exit $rc):"
|
|
local _l
|
|
while IFS= read -r _l; do
|
|
[[ -n "$_l" ]] && isNotice " $_l"
|
|
done <<< "$(printf '%s\n' "$output" | tail -20)"
|
|
}
|
|
|
|
dockerComposeUp()
|
|
{
|
|
local app_name="$1"
|
|
local custom_compose="$2"
|
|
local type="$3"
|
|
|
|
if [[ "$app_name" == "" ]]; then
|
|
isError "Something went wrong...No app name provided..."
|
|
return 1
|
|
fi
|
|
|
|
# Real exit status of the compose command, so callers that gate on it (the
|
|
# updater's rollback, the artifact apply pipeline) aren't fail-open. checkSuccess
|
|
# only logs; it does NOT set the function's return — without this the function
|
|
# always returned 0 and a failed `up -d` looked like success.
|
|
local _rc=0
|
|
|
|
isHeader "Docker Compose Up $app_name"
|
|
|
|
# Make sure we are able to get the compose file
|
|
if [[ $compose_setup == "" ]]; then
|
|
setupBasicScanVariables "$app_name"
|
|
fi
|
|
|
|
# Compose file public variable for restarting etc
|
|
if [[ $compose_setup == "default" ]]; then
|
|
local setup_compose="-f docker-compose.yml"
|
|
local compose_file="docker-compose.yml"
|
|
elif [[ $compose_setup == "app" ]]; then
|
|
local setup_compose="-f docker-compose.yml -f docker-compose.$app_name.yml"
|
|
local compose_file="docker-compose.$app_name.yml"
|
|
fi
|
|
if [[ $custom_compose != "" ]]; then
|
|
local setup_compose="-f docker-compose.yml -f $custom_compose"
|
|
local compose_file="$custom_compose"
|
|
fi
|
|
|
|
if [[ "$OS_TYPE" == "Ubuntu" || "$OS_TYPE" == "Debian" ]]; then
|
|
if [ -f "$(appDir "$app_name")/$compose_file" ]; then
|
|
# Quiet pull + plain progress so progress redraws don't flood the log.
|
|
local _compose_quiet="--quiet-pull"
|
|
export COMPOSE_PROGRESS=plain
|
|
# Force a rebuild when the app ships a Dockerfile. Without
|
|
# --build, `up -d` reuses the cached image and silently
|
|
# ignores any edits to Dockerfile / source between installs.
|
|
local _compose_build_flag=""
|
|
local _is_local_build=0
|
|
if [[ -f "$(appDir "$app_name")/Dockerfile" ]]; then
|
|
_compose_build_flag="--build"
|
|
_is_local_build=1
|
|
fi
|
|
# Used for the standard LibrePortal app
|
|
if [[ "$type" == "" ]]; then
|
|
# Refuse to start if the runtime compose still has raw
|
|
# `#LIBREPORTAL|TAG|VALUE` placeholders on the value side.
|
|
# Happens when the template was copied over the runtime
|
|
# compose without the tag processors running (manual
|
|
# copy, partial restore, aborted install). Without this
|
|
# guard docker errors with "invalid boolean:
|
|
# HEALTHCHECK_DATA" or similar.
|
|
#
|
|
# Two-pass: detect → attempt self-heal by re-running the
|
|
# full tag-processor pipeline → re-detect. Only refuse
|
|
# if heal didn't clear all placeholders.
|
|
_scanStaleTags() {
|
|
awk '
|
|
{
|
|
idx = index($0, "#LIBREPORTAL|")
|
|
if (idx == 0) next
|
|
# Skip whole-line YAML comments (e.g. "# - PORTS_DATA_4 ...")
|
|
value_part = substr($0, 1, idx - 1)
|
|
trimmed = value_part
|
|
sub(/^[ \t]+/, "", trimmed)
|
|
if (substr(trimmed, 1, 1) == "#") next
|
|
marker = substr($0, idx + length("#LIBREPORTAL|"))
|
|
if (split(marker, parts, "|") < 2) next
|
|
placeholder = parts[2]
|
|
gsub(/[ \t]+$/, "", placeholder)
|
|
if (placeholder ~ /^[A-Z][A-Z0-9_]*_DATA(_[0-9]+)?$/) {
|
|
printf " line %d: %s\n", NR, $0
|
|
}
|
|
}
|
|
' "$1"
|
|
}
|
|
local _compose_path="$(appDir "$app_name")/$compose_file"
|
|
local _stale_tags
|
|
_stale_tags=$(_scanStaleTags "$_compose_path")
|
|
if [[ -n "$_stale_tags" ]]; then
|
|
isNotice "$app_name compose has unsubstituted LibrePortal tag placeholders — attempting self-heal via tag processors."
|
|
if declare -F dockerConfigSetupFileWithData >/dev/null 2>&1; then
|
|
# The processors read per-app env vars
|
|
# (healthcheck, host_setup, app_category, …) set
|
|
# by initializeAppVariables. Without it some
|
|
# tags get filled with empty strings and the
|
|
# heal leaves placeholders behind.
|
|
if declare -F initializeAppVariables >/dev/null 2>&1; then
|
|
initializeAppVariables "$app_name" >/dev/null 2>&1 || true
|
|
fi
|
|
dockerConfigSetupFileWithData "$app_name" >/dev/null 2>&1 || true
|
|
_stale_tags=$(_scanStaleTags "$_compose_path")
|
|
fi
|
|
if [[ -n "$_stale_tags" ]]; then
|
|
isError "$app_name compose still has unsubstituted LibrePortal tag placeholders after self-heal — refusing to start."
|
|
isNotice "File: $_compose_path"
|
|
while IFS= read -r _l; do isNotice "$_l"; done <<< "$_stale_tags"
|
|
isNotice "Fix: run 'libreportal app install $app_name' to re-apply the full install pipeline."
|
|
unset -f _scanStaleTags
|
|
return 1
|
|
fi
|
|
isSuccessful "Tag processors re-applied — placeholders resolved, continuing start."
|
|
fi
|
|
unset -f _scanStaleTags
|
|
|
|
# Local-build apps take noticeably longer than image-pull
|
|
# apps. Specific heads-up so the user doesn't wonder if
|
|
# it's stuck during a multi-minute Dockerfile build.
|
|
if (( _is_local_build )); then
|
|
isNotice "$app_name has a Dockerfile — building image locally."
|
|
isNotice "First build can take a few minutes."
|
|
fi
|
|
if [[ $CFG_DOCKER_INSTALL_TYPE == "rootless" ]]; then
|
|
isNotice "Starting container for $app_name, this may take a while..."
|
|
local result; result=$(dockerCommandRunInstallUser "cd $(appDir "$app_name") && COMPOSE_PROGRESS=plain docker compose $setup_compose up $_compose_quiet $_compose_build_flag -d" 2>&1)
|
|
_rc=$?
|
|
_upReportComposeFailure "$app_name" "$_rc" "$result"
|
|
# Restore $? to the compose exit code — a bare `checkSuccess`
|
|
# after `_rc=$?` would read the assignment's 0 and always print
|
|
# success, hiding a failed `up -d` (e.g. an image pull EOF).
|
|
( exit "$_rc" ); checkSuccess "Started container for $app_name"
|
|
elif [[ $CFG_DOCKER_INSTALL_TYPE == "rooted" ]]; then
|
|
isNotice "Starting container for $app_name, this may take a while..."
|
|
local result; result=$(cd "$(appDir "$app_name")" && COMPOSE_PROGRESS=plain docker compose $setup_compose up $_compose_quiet $_compose_build_flag -d 2>&1)
|
|
_rc=$?
|
|
_upReportComposeFailure "$app_name" "$_rc" "$result"
|
|
( exit "$_rc" ); checkSuccess "Started container for $app_name"
|
|
fi
|
|
# Used for the CLI dockertype switcher.
|
|
else
|
|
if [[ $type == "rootless" ]]; then
|
|
local result; result=$(dockerCommandRunInstallUser "cd $(appDir "$app_name") && docker compose $setup_compose down"); _rc=$?
|
|
checkSuccess "Shutting down container for $app_name"
|
|
elif [[ $type == "rooted" ]]; then
|
|
local result; result=$(cd "$(appDir "$app_name")" && docker compose $setup_compose down); _rc=$?
|
|
checkSuccess "Shutting down container for $app_name"
|
|
fi
|
|
fi
|
|
else
|
|
isNotice "Unable to find the compose file to docker compose up this application."
|
|
_rc=1
|
|
fi
|
|
fi
|
|
return $_rc
|
|
}
|