Instance install (bugs found by running one end to end): - The cloned compose kept the TYPE's tag namespace (#LIBREPORTAL|BOOKSTACK_APP_KEY_1_TAG|...) while the config had been re-namespaced to CFG_<SLUG>_*, so tagsProcessorAppConfigValues matched nothing, the placeholders survived and the pre-start guard refused to launch. Rewrite the tag names and *_DATA tokens too — narrowly, so an app whose compose sets a real env var named after itself is untouched. - Tools/hooks kept uppercase CFG_<TYPE>_ reads, so an instance provisioned itself from the type's config and ignored its own values. - Cloned hooks were never loaded: both loaders run at startup, before the instance dir exists, so _appCallHook's `declare -F` found nothing and every <slug>_install_* hook silently no-opped — for bookstack that is the readiness probe and the admin bootstrap. Source the instance's own scripts in-process, then regen arrays + manifest for later runs. - bookstack's hook hardcoded the container name after `docker exec -e ...` flags, where the rewriter can't see it, so an instance's admin bootstrap ran against the BASE app's container — including a tinker DELETE of a user. Target "$app_name" instead, and teach the rewriter the container="<type>" assignment form used by auth adapters. network_resources uniqueness: UNIQUE(resource_type, resource_value) is right for 'ip' and 'port' but the port-tag writer stores descriptive rows in the same table with INSERT OR REPLACE, so every install DELETED the matching row from whichever app held it. traefik_managed and url_accessible are booleans, so the whole table could only ever hold one row of each. Observed live: installing a second bookstack took all four traefik_managed/url_accessible rows from stoat and bookstack, and removing that instance took the stolen rows with it. Replace it with a partial unique index scoped to ip/port, and migrate existing databases in place (SQLite can't drop a constraint, so the table is rebuilt inside a transaction). The migration is invoked from portUpdateComposeTags, not just databaseCreateTables — the latter only runs from startPreInstall, which a working install never re-runs. Verified: two bookstacks now hold port_tag_internal=80, traefik_managed and url_accessible simultaneously; duplicate host ports and IPs are still rejected; instance installs, serves HTTP 200, provisions its own admin in its own database, and removes cleanly with no orphan rows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
100 lines
4.6 KiB
Bash
100 lines
4.6 KiB
Bash
#!/bin/bash
|
|
|
|
# Bookstack install hooks — drive the post-start admin account bootstrap.
|
|
# Generic installApp driver handles compose / start / db / monitoring; this
|
|
# adds the readiness probe + first-admin provisioning the original
|
|
# installBookstack() did inline.
|
|
|
|
bookstack_install_post_start()
|
|
{
|
|
local app_name="$1"
|
|
|
|
local bookstack_target_email="${CFG_BOOKSTACK_ADMIN_EMAIL:-admin@admin.com}"
|
|
local bookstack_target_pass="${CFG_BOOKSTACK_ADMIN_PASSWORD_1:-password}"
|
|
|
|
local bookstack_compose_file="$containers_dir$app_name/docker-compose.yml"
|
|
local bookstack_port_pair
|
|
bookstack_port_pair=$(tagsManagerGetTagContent "$bookstack_compose_file" "PORTS_TAG_1")
|
|
local bookstack_host_port="${bookstack_port_pair%%:*}"
|
|
local bookstack_probe_url="http://127.0.0.1:${bookstack_host_port}/login"
|
|
|
|
isNotice "Waiting for Bookstack to come online at ${bookstack_probe_url} ..."
|
|
isNotice "This may take up to 20 seconds, please wait..."
|
|
|
|
local bookstack_attempts=0
|
|
local bookstack_ready=0
|
|
local bookstack_http_code
|
|
while ((bookstack_attempts < 60)); do
|
|
bookstack_http_code=$(curl -sS -o /dev/null --max-time 3 -w '%{http_code}' "$bookstack_probe_url" 2>/dev/null)
|
|
if [[ "$bookstack_http_code" =~ ^(200|302)$ ]]; then
|
|
bookstack_ready=1
|
|
break
|
|
fi
|
|
sleep 2
|
|
((bookstack_attempts++))
|
|
done
|
|
|
|
if ((bookstack_ready == 0)); then
|
|
isNotice "Bookstack did not respond on ${bookstack_probe_url} within $((60 * 2))s — admin account left at upstream defaults."
|
|
echo ""
|
|
isNotice "Bookstack admin login (default):"
|
|
echo ""
|
|
echo " Email : admin@admin.com"
|
|
echo " Password : password"
|
|
echo ""
|
|
return 0
|
|
fi
|
|
isSuccessful "Bookstack is online (HTTP ${bookstack_http_code})."
|
|
|
|
# Target the container by $app_name, never the literal "bookstack": under
|
|
# multi-instance this hook is cloned for each instance, and a hardcoded name
|
|
# pointed every instance's admin bootstrap at the BASE app's container —
|
|
# provisioning (and, in the branch below, DELETING) users in the wrong
|
|
# database. instanceCreate's rewriter can't catch it either, since the
|
|
# container name here doesn't directly follow `docker exec`.
|
|
local bookstack_create_output
|
|
bookstack_create_output=$(runFileOp docker exec \
|
|
-e EZ_BS_NEW_EMAIL="$bookstack_target_email" \
|
|
-e EZ_BS_NEW_PASS="$bookstack_target_pass" \
|
|
"$app_name" sh -c 'cd /app/www && s6-setuidgid abc php artisan bookstack:create-admin --no-ansi --email="$EZ_BS_NEW_EMAIL" --name=Admin --password="$EZ_BS_NEW_PASS" 2>&1')
|
|
local bookstack_create_rc=$?
|
|
if [[ $bookstack_create_rc -eq 0 ]]; then
|
|
isSuccessful "Bookstack admin account created (email: $bookstack_target_email)."
|
|
|
|
if [[ "$bookstack_target_email" != "admin@admin.com" ]]; then
|
|
runFileOp docker exec -i "$app_name" php /app/www/artisan tinker --no-ansi >/dev/null 2>&1 <<'PHP'
|
|
$c = class_exists('\BookStack\Users\Models\User') ? '\BookStack\Users\Models\User' : '\BookStack\Auth\User';
|
|
optional($c::where('email', 'admin@admin.com')->first())->delete();
|
|
PHP
|
|
isSuccessful "Removed seeded admin@admin.com account."
|
|
fi
|
|
|
|
echo ""
|
|
isNotice "Bookstack admin login:"
|
|
echo ""
|
|
echo " Email : ${bookstack_target_email}"
|
|
echo " Password : ${bookstack_target_pass}"
|
|
echo ""
|
|
elif [[ "$bookstack_create_output" == *"already exists"* ]]; then
|
|
# Reinstall over existing data: the account is already provisioned, so
|
|
# create-admin refuses. NOT a failure, and printing the upstream
|
|
# defaults here would be actively wrong — those credentials were
|
|
# replaced on the first install, and the password in the config was
|
|
# never re-applied to the live account.
|
|
isSuccessful "Bookstack admin account already exists (email: $bookstack_target_email) — kept as-is."
|
|
isNotice "Its password was NOT reset by this install. Sign in with the credentials from the original install, or reset from inside Bookstack."
|
|
echo ""
|
|
else
|
|
isNotice "Bookstack admin auto-create failed (exit $bookstack_create_rc). Output:"
|
|
echo "$bookstack_create_output" | sed 's/^/ /'
|
|
echo ""
|
|
isNotice "Falling back to upstream defaults — update from inside Bookstack."
|
|
echo ""
|
|
isNotice "Bookstack admin login (default):"
|
|
echo ""
|
|
echo " Email : admin@admin.com"
|
|
echo " Password : password"
|
|
echo ""
|
|
fi
|
|
}
|