From 31ca58198999a77c13186d274f5b4731292d001b Mon Sep 17 00:00:00 2001 From: Alexander Zubarev Date: Wed, 30 Sep 2026 13:30:48 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20scripts/deploy-nextcloud.sh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/deploy-nextcloud.sh | 811 ++++++++++++++++-------------------- 1 file changed, 366 insertions(+), 445 deletions(-) diff --git a/scripts/deploy-nextcloud.sh b/scripts/deploy-nextcloud.sh index 6fb56b3..d90bf2a 100755 --- a/scripts/deploy-nextcloud.sh +++ b/scripts/deploy-nextcloud.sh @@ -10,7 +10,6 @@ umask 077 # ============================================================ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" - SRC="${ROOT}/stacks/nextcloud" LIVE="/srv/nfs/infra/nextcloud" @@ -20,12 +19,28 @@ BACKUP_ROOT="/srv/nfs/infra/.deploy-backups/nextcloud" BACKUP_KEEP=3 STAMP="$(date '+%Y%m%d-%H%M%S')" - BACKUP_DIR="${BACKUP_ROOT}/${STAMP}" DB_BACKUP_DIR="${BACKUP_DIR}/database" - DB_BACKUP_FILE="${DB_BACKUP_DIR}/nextcloud.dump" +PREVIOUS_COMPOSE="/tmp/nextcloud-compose-${STAMP}-$$.yml" + +INIT_TIMEOUT=900 +INIT_INTERVAL=5 +OCC_TIMEOUT=300 +OCC_INTERVAL=5 + + +# ============================================================ +# Runtime state +# ============================================================ + +VERSION_CHANGED=false +PRODUCTION_CHANGED=false +MIGRATION_STARTED=false +BACKUP_COMPLETE=false +UPGRADE_STARTED="" + # ============================================================ # Helpers @@ -40,21 +55,42 @@ log() } -fail() +cleanup_temp() { - echo - echo "ERROR: $*" >&2 - exit 1 + rm -f -- "${PREVIOUS_COMPOSE}" 2>/dev/null || true } -on_error() +restore_previous_compose() { - rc=$? - failed_command="${BASH_COMMAND}" - failed_line="${BASH_LINENO[0]:-unknown}" + if [[ ! -f "${PREVIOUS_COMPOSE}" ]]; then + echo "Previous Compose file is unavailable; automatic restore skipped." + return 0 + fi - trap - ERR + echo + echo "--- Restore previous docker-compose.yml ---" + + cp \ + "${PREVIOUS_COMPOSE}" \ + "${LIVE}/docker-compose.yml" + + ( + cd "${LIVE}" + + docker compose \ + --env-file "${ENV_FILE}" \ + up -d \ + --remove-orphans + ) || true +} + + +print_failure_context() +{ + local RC="$1" + local FAILED_LINE="$2" + local FAILED_COMMAND="$3" echo echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" @@ -63,44 +99,81 @@ on_error() echo echo "Exit code:" - echo " ${rc}" + echo " ${RC}" echo echo "Failed at line:" - echo " ${failed_line}" + echo " ${FAILED_LINE}" echo echo "Failed command:" - echo " ${failed_command}" - - if [[ "${MIGRATION_STARTED:-false}" == "true" ]]; then + echo " ${FAILED_COMMAND}" + if [[ "${MIGRATION_STARTED}" == "true" ]]; then echo echo "Database migration MAY already have started." echo "Automatic rollback will NOT be performed." - if [[ -d "${BACKUP_DIR}" ]]; then + if [[ "${BACKUP_COMPLETE}" == "true" ]]; then echo echo "Pre-upgrade backup:" echo " ${BACKUP_DIR}" fi - elif [[ "${PRODUCTION_CHANGED:-false}" == "true" ]]; then - echo - echo "Production Compose configuration was changed," - echo "but database migration was NOT started." + echo "For rollback restore the PostgreSQL dump first." + + elif [[ "${PRODUCTION_CHANGED}" == "true" ]]; then + echo + echo "Database migration was NOT started." + echo "Restoring previous Compose configuration." + + restore_previous_compose else - echo echo "Production was NOT modified." echo "Database migration was NOT started." - echo "No rollback is required." - + echo "No service rollback is required." fi - exit "${rc}" + if [[ -d "${BACKUP_DIR}" ]] && + [[ "${BACKUP_COMPLETE}" != "true" ]] + then + echo + echo "Removing incomplete backup directory:" + echo " ${BACKUP_DIR}" + + rm -rf -- "${BACKUP_DIR}" || true + fi +} + + +on_error() +{ + local RC=$? + local FAILED_COMMAND="${BASH_COMMAND}" + local FAILED_LINE="${BASH_LINENO[0]:-unknown}" + + trap - ERR + + print_failure_context \ + "${RC}" \ + "${FAILED_LINE}" \ + "${FAILED_COMMAND}" + + exit "${RC}" +} + + +fail() +{ + local MESSAGE="$*" + + echo + echo "ERROR: ${MESSAGE}" >&2 + + return 1 } @@ -108,7 +181,7 @@ cleanup_old_backups() { echo echo "--- Backup retention ---" - echo "Keeping last ${BACKUP_KEEP} pre-upgrade backups." + echo "Keeping last ${BACKUP_KEEP} complete pre-upgrade backups." mkdir -p "${BACKUP_ROOT}" @@ -116,26 +189,22 @@ cleanup_old_backups() find "${BACKUP_ROOT}" \ -mindepth 1 \ -maxdepth 1 \ - -type d \ - -printf '%f\n' | + -type f \ + -name '.complete' \ + -printf '%h\n' | + sed 's#.*/##' | grep -E '^[0-9]{8}-[0-9]{6}$' | sort -r ) - if (( ${#BACKUPS[@]} <= BACKUP_KEEP )); then - - echo "Current backup count: ${#BACKUPS[@]}" + echo "Current complete backup count: ${#BACKUPS[@]}" echo "Nothing to delete." - return 0 - fi - for BACKUP_NAME in "${BACKUPS[@]:${BACKUP_KEEP}}" do - OLD_BACKUP="${BACKUP_ROOT}/${BACKUP_NAME}" echo @@ -143,7 +212,6 @@ cleanup_old_backups() echo " ${OLD_BACKUP}" rm -rf -- "${OLD_BACKUP}" - done } @@ -163,7 +231,103 @@ version_less_than() } +json_string_value() +{ + local KEY="$1" + + sed -nE \ + "s/.*\"${KEY}\"[[:space:]]*:[[:space:]]*\"([^\"]+)\".*/\1/p" | + head -n1 +} + + +get_occ_status_json() +{ + local OUTPUT + local RC + + set +e + + OUTPUT="$( + docker exec \ + -u www-data \ + nextcloud \ + php occ status \ + --output=json \ + 2>&1 + )" + + RC=$? + + set -e + + if [[ "${RC}" -ne 0 ]]; then + printf '%s\n' "${OUTPUT}" >&2 + return "${RC}" + fi + + printf '%s\n' "${OUTPUT}" +} + + +validate_occ_status_json() +{ + local STATUS_JSON="$1" + + echo "${STATUS_JSON}" | + grep -Eq '"installed"[[:space:]]*:[[:space:]]*true' || + fail "Nextcloud is not reported as installed" + + echo "${STATUS_JSON}" | + grep -Eq '"needsDbUpgrade"[[:space:]]*:[[:space:]]*false' || + fail "Nextcloud requires a database upgrade" + + echo "${STATUS_JSON}" | + grep -Eq '"maintenance"[[:space:]]*:[[:space:]]*false' || + fail "Nextcloud is in maintenance mode" +} + + +wait_for_occ_normal() +{ + local ELAPSED=0 + local STATUS_JSON="" + local RC=0 + + echo + echo "Waiting for Nextcloud OCC status..." + + while (( ELAPSED < OCC_TIMEOUT )) + do + set +e + STATUS_JSON="$(get_occ_status_json 2>&1)" + RC=$? + set -e + + if [[ "${RC}" -eq 0 ]] && + echo "${STATUS_JSON}" | grep -Eq '"needsDbUpgrade"[[:space:]]*:[[:space:]]*false' && + echo "${STATUS_JSON}" | grep -Eq '"maintenance"[[:space:]]*:[[:space:]]*false' + then + echo "Nextcloud OCC status is normal." + return 0 + fi + + sleep "${OCC_INTERVAL}" + ELAPSED=$((ELAPSED + OCC_INTERVAL)) + + echo "Waiting for OCC... ${ELAPSED}s" + done + + echo + echo "Last OCC output:" + echo "${STATUS_JSON}" + + fail "Timeout waiting for normal Nextcloud OCC status" +} + + trap on_error ERR +trap cleanup_temp EXIT # ============================================================ @@ -180,12 +344,15 @@ for CMD in \ sha256sum \ sort \ grep \ - find + sed \ + find \ + awk \ + du \ + diff \ + seq do - command -v "${CMD}" >/dev/null 2>&1 || \ fail "Required command not found: ${CMD}" - done @@ -207,6 +374,12 @@ docker compose version >/dev/null 2>&1 || \ fail "${ENV_FILE} not found" +# Preserve production Compose before any change. +cp \ + "${LIVE}/docker-compose.yml" \ + "${PREVIOUS_COMPOSE}" + + # ============================================================ # Backup directory access # ============================================================ @@ -219,15 +392,6 @@ mkdir -p "${BACKUP_ROOT}" # ============================================================ # Load PostgreSQL settings -# -# .env must be shell-compatible. -# -# Example: -# -# POSTGRES_HOST=192.168.1.20 -# POSTGRES_DB=nextcloud -# POSTGRES_USER=nextcloud -# POSTGRES_PASSWORD='complex@password' # ============================================================ set -a @@ -243,6 +407,7 @@ set +a : "${POSTGRES_USER:?POSTGRES_USER not defined}" : "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD not defined}" +POSTGRES_PORT="${POSTGRES_PORT:-5432}" export PGPASSWORD="${POSTGRES_PASSWORD}" export PGCONNECT_TIMEOUT=10 @@ -254,23 +419,39 @@ export PGCONNECT_TIMEOUT=10 log "Validate Docker Compose" + docker compose \ + --env-file "${ENV_FILE}" \ -f "${SRC}/docker-compose.yml" \ config >/dev/null + echo "Docker Compose configuration is valid." +# ============================================================ +# Show requested Compose changes +# ============================================================ + +log "Docker Compose changes" + +if diff -u \ + "${LIVE}/docker-compose.yml" \ + "${SRC}/docker-compose.yml" +then + echo "No docker-compose.yml text changes detected." +fi + + # ============================================================ # Detect requested Nextcloud image -# -# nextcloud and cron MUST use exactly the same image. # ============================================================ log "Detect requested Nextcloud image" mapfile -t NEXTCLOUD_IMAGES < <( docker compose \ + --env-file "${ENV_FILE}" \ -f "${SRC}/docker-compose.yml" \ config --images | grep '^nextcloud:' | @@ -279,7 +460,6 @@ mapfile -t NEXTCLOUD_IMAGES < <( if [[ "${#NEXTCLOUD_IMAGES[@]}" -ne 1 ]]; then - echo "Detected Nextcloud images:" if (( ${#NEXTCLOUD_IMAGES[@]} > 0 )); then @@ -289,12 +469,12 @@ if [[ "${#NEXTCLOUD_IMAGES[@]}" -ne 1 ]]; then fi fail "Services nextcloud and cron must use exactly the same Nextcloud image" - fi NEW_IMAGE="${NEXTCLOUD_IMAGES[0]}" + echo "Requested image:" echo " ${NEW_IMAGE}" @@ -306,18 +486,15 @@ echo " ${NEW_IMAGE}" docker inspect nextcloud >/dev/null 2>&1 || \ fail "Current Nextcloud container does not exist" - CURRENT_RUNNING="$( docker inspect \ -f '{{.State.Running}}' \ nextcloud )" - [[ "${CURRENT_RUNNING}" == "true" ]] || \ fail "Current Nextcloud container is not running" - OLD_IMAGE="$( docker inspect \ -f '{{.Config.Image}}' \ @@ -328,81 +505,75 @@ OLD_IMAGE="$( # ============================================================ # Detect current Nextcloud status/version # -# Use OCC instead of reading version.php directly. -# OCC is the supported machine-readable interface and also -# verifies that the currently running installation can load. +# IMPORTANT: +# Compare internal "version" (e.g. 35.0.1.1) with the target +# image's $OC_Version. Do NOT compare "versionstring" (35.0.1) +# against $OC_Version, otherwise an unchanged image looks like +# a version upgrade. # ============================================================ log "Detect current Nextcloud version" - set +e - -CURRENT_STATUS_JSON="$( - docker exec \ - -u www-data \ - nextcloud \ - php occ status \ - --output=json \ - 2>&1 -)" - +CURRENT_STATUS_JSON="$(get_occ_status_json 2>&1)" CURRENT_STATUS_RC=$? - set -e - -if [[ "${CURRENT_STATUS_RC}" -ne 0 ]] -then - +if [[ "${CURRENT_STATUS_RC}" -ne 0 ]]; then echo echo "--- OCC status output ---" echo "${CURRENT_STATUS_JSON}" fail \ "Could not get current Nextcloud status (exit code ${CURRENT_STATUS_RC})" - fi - CURRENT_NC_VERSION="$( printf '%s\n' "${CURRENT_STATUS_JSON}" | - sed -nE \ - 's/.*"versionstring"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' | - head -n1 + json_string_value version )" +CURRENT_NC_VERSIONSTRING="$( + printf '%s\n' "${CURRENT_STATUS_JSON}" | + json_string_value versionstring +)" -[[ -n "${CURRENT_NC_VERSION}" ]] || { +[[ -n "${CURRENT_NC_VERSION}" ]] || \ + fail "Could not determine current internal Nextcloud version" - echo - echo "OCC output:" - echo "${CURRENT_STATUS_JSON}" +validate_occ_status_json "${CURRENT_STATUS_JSON}" - fail "Could not determine current Nextcloud version" -} +docker exec \ + -u www-data \ + nextcloud \ + php occ status -e echo "Current image:" echo " ${OLD_IMAGE}" -echo "Current Nextcloud version:" +echo "Current Nextcloud internal version:" echo " ${CURRENT_NC_VERSION}" +if [[ -n "${CURRENT_NC_VERSIONSTRING}" ]]; then + echo "Current Nextcloud display version:" + echo " ${CURRENT_NC_VERSIONSTRING}" +fi + + # ============================================================ # Pull target Nextcloud image -# -# This does not modify the running container. # ============================================================ log "Pull target Nextcloud image" + docker pull "${NEW_IMAGE}" # ============================================================ -# Determine actual Nextcloud version in target image +# Determine exact target Nextcloud version # ============================================================ log "Detect target Nextcloud version" @@ -418,6 +589,16 @@ TARGET_NC_VERSION="$( ' )" +TARGET_NC_VERSIONSTRING="$( + docker run \ + --rm \ + --entrypoint php \ + "${NEW_IMAGE}" \ + -r ' + require "/usr/src/nextcloud/version.php"; + echo isset($OC_VersionString) ? $OC_VersionString : implode(".", $OC_Version); + ' +)" [[ -n "${TARGET_NC_VERSION}" ]] || \ fail "Could not determine target Nextcloud version" @@ -426,9 +607,12 @@ TARGET_NC_VERSION="$( echo "Target Docker image:" echo " ${NEW_IMAGE}" -echo "Target Nextcloud version:" +echo "Target Nextcloud internal version:" echo " ${TARGET_NC_VERSION}" +echo "Target Nextcloud display version:" +echo " ${TARGET_NC_VERSIONSTRING}" + # ============================================================ # Version safety checks @@ -436,115 +620,43 @@ echo " ${TARGET_NC_VERSION}" log "Validate version transition" - -# ------------------------------------------------------------ -# Downgrade protection -# ------------------------------------------------------------ - if version_less_than \ "${TARGET_NC_VERSION}" \ "${CURRENT_NC_VERSION}" then - fail \ "Nextcloud downgrade is not allowed: ${CURRENT_NC_VERSION} -> ${TARGET_NC_VERSION}" - fi - CURRENT_MAJOR="${CURRENT_NC_VERSION%%.*}" TARGET_MAJOR="${TARGET_NC_VERSION%%.*}" - -# ------------------------------------------------------------ -# Prevent skipping major versions -# ------------------------------------------------------------ - -if (( TARGET_MAJOR > CURRENT_MAJOR + 1 )) -then - +if (( TARGET_MAJOR > CURRENT_MAJOR + 1 )); then fail \ "Skipping Nextcloud major versions is forbidden: ${CURRENT_MAJOR} -> ${TARGET_MAJOR}" - fi - -# ------------------------------------------------------------ -# Determine whether actual Nextcloud version changed -# ------------------------------------------------------------ - -VERSION_CHANGED=false - -if [[ "${CURRENT_NC_VERSION}" != "${TARGET_NC_VERSION}" ]] -then +if [[ "${CURRENT_NC_VERSION}" != "${TARGET_NC_VERSION}" ]]; then VERSION_CHANGED=true fi -echo "Current version: ${CURRENT_NC_VERSION}" -echo "Target version: ${TARGET_NC_VERSION}" -echo "Version changed: ${VERSION_CHANGED}" - - -# ============================================================ -# Check current Nextcloud status -# ============================================================ - -log "Check current Nextcloud status" - -CURRENT_STATUS="$( - docker exec \ - -u www-data \ - nextcloud \ - php occ status -)" - - -echo "${CURRENT_STATUS}" - - -echo "${CURRENT_STATUS}" | - grep -Eq 'needsDbUpgrade:[[:space:]]*false' || \ - fail "Current Nextcloud already requires a DB upgrade" - - -echo "${CURRENT_STATUS}" | - grep -Eq 'maintenance:[[:space:]]*false' || \ - fail "Current Nextcloud is already in maintenance mode" - - -docker exec \ - -u www-data \ - nextcloud \ - php occ status -e +echo "Current internal version: ${CURRENT_NC_VERSION}" +echo "Target internal version: ${TARGET_NC_VERSION}" +echo "Version changed: ${VERSION_CHANGED}" # ============================================================ # PRE-UPGRADE BACKUP # -# Created ONLY if actual Nextcloud version changes. +# Only when the actual internal Nextcloud version changes. # ============================================================ -if [[ "${VERSION_CHANGED}" == "true" ]] -then - +if [[ "${VERSION_CHANGED}" == "true" ]]; then log "Prepare Nextcloud pre-upgrade backup" - - # -------------------------------------------------------- - # Create backup directories - # -------------------------------------------------------- - mkdir -p "${DB_BACKUP_DIR}" - - # -------------------------------------------------------- - # Save current Compose - # - # Do NOT use cp -a because this NFS does not support - # preserving all POSIX metadata. - # -------------------------------------------------------- - echo echo "--- Backup current docker-compose.yml ---" @@ -552,20 +664,10 @@ then "${LIVE}/docker-compose.yml" \ "${BACKUP_DIR}/docker-compose.yml" + printf '%s\n' "${CURRENT_STATUS_JSON}" \ + > "${BACKUP_DIR}/nextcloud-status-before.json" - # -------------------------------------------------------- - # Save current Nextcloud status - # -------------------------------------------------------- - - printf '%s\n' "${CURRENT_STATUS}" \ - > "${BACKUP_DIR}/nextcloud-status-before.txt" - - - # -------------------------------------------------------- - # Save metadata - # -------------------------------------------------------- - - cat > "${BACKUP_DIR}/metadata.txt" < "${BACKUP_DIR}/metadata.txt" < "${DB_BACKUP_DIR}/postgresql-version.txt" - SERVER_MAJOR="${SERVER_VERSION%%.*}" - PG_DUMP_VERSION="$( pg_dump --version | sed -E 's/.* ([0-9]+(\.[0-9]+)?).*/\1/' )" - PG_DUMP_MAJOR="${PG_DUMP_VERSION%%.*}" - echo "PostgreSQL server:" echo " ${SERVER_VERSION}" echo "pg_dump client:" echo " ${PG_DUMP_VERSION}" - - if (( PG_DUMP_MAJOR < SERVER_MAJOR )) - then - + if (( PG_DUMP_MAJOR < SERVER_MAJOR )); then fail \ "pg_dump ${PG_DUMP_VERSION} is older than PostgreSQL server ${SERVER_VERSION}" - fi - - # ======================================================== - # Database size - # ======================================================== - echo echo "--- Database size ---" DB_SIZE="$( psql \ -h "${POSTGRES_HOST}" \ + -p "${POSTGRES_PORT}" \ -U "${POSTGRES_USER}" \ -d "${POSTGRES_DB}" \ -Atqc \ 'SELECT pg_size_pretty(pg_database_size(current_database()));' )" - echo "Database:" echo " ${POSTGRES_DB}" echo "Size:" echo " ${DB_SIZE}" - - # ======================================================== - # PostgreSQL dump - # ======================================================== - log "Create PostgreSQL backup" pg_dump \ -h "${POSTGRES_HOST}" \ + -p "${POSTGRES_PORT}" \ -U "${POSTGRES_USER}" \ -d "${POSTGRES_DB}" \ --format=custom \ @@ -694,15 +768,9 @@ EOF --no-privileges \ --file="${DB_BACKUP_FILE}" - [[ -s "${DB_BACKUP_FILE}" ]] || \ fail "PostgreSQL dump does not exist or is empty" - - # ======================================================== - # Validate PostgreSQL dump - # ======================================================== - echo echo "--- Validate PostgreSQL dump ---" @@ -711,17 +779,11 @@ EOF "${DB_BACKUP_FILE}" \ > "${DB_BACKUP_DIR}/nextcloud.dump.list" - [[ -s "${DB_BACKUP_DIR}/nextcloud.dump.list" ]] || \ fail "pg_restore could not validate PostgreSQL dump" - - # ======================================================== - # SHA256 checksum - # ======================================================== - echo - echo "--- Generate SHA256 ---" + echo "--- Generate and verify SHA256 ---" ( cd "${DB_BACKUP_DIR}" @@ -729,18 +791,20 @@ EOF sha256sum \ nextcloud.dump \ > nextcloud.dump.sha256 + + # The checksum contains the relative name "nextcloud.dump", + # therefore validation MUST run from the same directory. + sha256sum \ + -c nextcloud.dump.sha256 ) - - sha256sum \ - -c "${DB_BACKUP_DIR}/nextcloud.dump.sha256" - - DB_BACKUP_SIZE="$( du -h "${DB_BACKUP_FILE}" | awk '{print $1}' )" + touch "${BACKUP_DIR}/.complete" + BACKUP_COMPLETE=true echo echo "PostgreSQL backup successful." @@ -761,23 +825,12 @@ EOF echo "SHA256:" cat "${DB_BACKUP_DIR}/nextcloud.dump.sha256" - - # ======================================================== - # Backup retention - # - # The new backup has already been successfully validated, - # so it is now safe to remove older backups. - # ======================================================== - cleanup_old_backups - fi # ============================================================ # Deploy requested Compose -# -# Production is not touched until pre-upgrade backup succeeds. # ============================================================ log "Deploy docker-compose.yml" @@ -786,6 +839,7 @@ cp \ "${SRC}/docker-compose.yml" \ "${LIVE}/docker-compose.yml" +PRODUCTION_CHANGED=true cd "${LIVE}" @@ -796,69 +850,48 @@ cd "${LIVE}" log "Pull Docker images" -docker compose pull + +docker compose \ + --env-file "${ENV_FILE}" \ + pull # ============================================================ # NEXTCLOUD VERSION UPGRADE # ============================================================ -if [[ "${VERSION_CHANGED}" == "true" ]] -then - +if [[ "${VERSION_CHANGED}" == "true" ]]; then log "Nextcloud upgrade ${CURRENT_NC_VERSION} -> ${TARGET_NC_VERSION}" - - # ======================================================== - # Stop cron during application/DB upgrade - # ======================================================== - echo echo "--- Stop Nextcloud cron ---" - docker compose stop cron || true - - - # ======================================================== - # Timestamp used for startup log analysis - # ======================================================== + docker compose \ + --env-file "${ENV_FILE}" \ + stop cron || true UPGRADE_STARTED="$( date -u '+%Y-%m-%dT%H:%M:%SZ' )" - - # ======================================================== - # Recreate ONLY main Nextcloud container first - # - # Redis remains running. - # Cron remains stopped. - # ======================================================== - echo echo "--- Recreate Nextcloud container ---" + # From this point the new image can modify the DB schema. + MIGRATION_STARTED=true + docker compose \ + --env-file "${ENV_FILE}" \ up -d \ --no-deps \ nextcloud - - # ======================================================== - # Wait for official Nextcloud Docker entrypoint - # - # The official image may execute occ upgrade automatically. - # - # 180 * 5 sec = 15 minutes maximum. - # ======================================================== - log "Wait for Nextcloud initialization" INITIALIZED=false - for _ in $(seq 1 180) + for _ in $(seq 1 $((INIT_TIMEOUT / INIT_INTERVAL))) do - RUNNING="$( docker inspect \ -f '{{.State.Running}}' \ @@ -866,10 +899,7 @@ then echo false )" - - if [[ "${RUNNING}" != "true" ]] - then - + if [[ "${RUNNING}" != "true" ]]; then echo echo "--- Nextcloud container log ---" @@ -878,48 +908,32 @@ then nextcloud || true fail "Nextcloud container stopped during upgrade" - fi - STARTUP_LOG="$( docker logs \ --since "${UPGRADE_STARTED}" \ nextcloud 2>&1 || true )" - - # Explicit failure from Nextcloud upgrade. if echo "${STARTUP_LOG}" | grep -q 'Update failed' then - echo "${STARTUP_LOG}" - fail "Nextcloud automatic upgrade failed" - fi - - # Standard completion marker of official image. if echo "${STARTUP_LOG}" | grep -q 'Initializing finished' then - INITIALIZED=true break - fi - - sleep 5 - + sleep "${INIT_INTERVAL}" done - - if [[ "${INITIALIZED}" != "true" ]] - then - + if [[ "${INITIALIZED}" != "true" ]]; then echo echo "--- Nextcloud container log ---" @@ -928,14 +942,8 @@ then nextcloud || true fail "Timeout waiting for Nextcloud initialization" - fi - - # ======================================================== - # Capture entrypoint upgrade result - # ======================================================== - log "Nextcloud startup upgrade output" STARTUP_LOG="$( @@ -944,34 +952,20 @@ then nextcloud 2>&1 )" - echo "${STARTUP_LOG}" - STARTUP_UPDATE_SUCCESS=false - if echo "${STARTUP_LOG}" | grep -q 'Update successful' then - STARTUP_UPDATE_SUCCESS=true - fi - echo echo "Startup log contains 'Update successful':" echo " ${STARTUP_UPDATE_SUCCESS}" - - # ======================================================== - # Explicit OCC upgrade - # - # If Docker entrypoint already performed the migration, - # occ upgrade can report that Nextcloud is already current. - # ======================================================== - log "Run explicit occ upgrade" set +e @@ -987,42 +981,29 @@ then set -e - echo "${OCC_UPGRADE_OUTPUT}" - - if [[ "${OCC_UPGRADE_RC}" -ne 0 ]] - then - + if [[ "${OCC_UPGRADE_RC}" -ne 0 ]]; then fail \ "Explicit occ upgrade returned exit code ${OCC_UPGRADE_RC}" - fi - EXPLICIT_UPDATE_SUCCESS=false EXPLICIT_ALREADY_CURRENT=false - if echo "${OCC_UPGRADE_OUTPUT}" | grep -q 'Update successful' then - EXPLICIT_UPDATE_SUCCESS=true - fi - if echo "${OCC_UPGRADE_OUTPUT}" | grep -Eqi \ 'already.*(latest|up.?to.?date)|(is|already).*up.?to.?date' then - EXPLICIT_ALREADY_CURRENT=true - fi - echo echo "Explicit occ upgrade contains 'Update successful':" echo " ${EXPLICIT_UPDATE_SUCCESS}" @@ -1030,101 +1011,48 @@ then echo "Explicit occ upgrade reports already current:" echo " ${EXPLICIT_ALREADY_CURRENT}" - - # ======================================================== - # Require confirmed update - # - # Either: - # Docker entrypoint -> Update successful - # - # or: - # explicit occ upgrade -> Update successful - # - # ======================================================== - if [[ "${STARTUP_UPDATE_SUCCESS}" != "true" ]] && [[ "${EXPLICIT_UPDATE_SUCCESS}" != "true" ]] then - - if [[ "${EXPLICIT_ALREADY_CURRENT}" == "true" ]] - then - + if [[ "${EXPLICIT_ALREADY_CURRENT}" == "true" ]]; then echo echo "occ reports that Nextcloud is already current," echo "but 'Update successful' was not found in either" echo "the startup log or explicit occ upgrade output." - fi fail "Could not confirm 'Update successful'" - fi - - # ======================================================== - # Validate Nextcloud state after DB migration - # ======================================================== - log "Validate Nextcloud status after upgrade" - FINAL_STATUS="$( - docker exec \ - -u www-data \ - nextcloud \ - php occ status - )" + wait_for_occ_normal + FINAL_STATUS_JSON="$(get_occ_status_json)" + echo "${FINAL_STATUS_JSON}" - echo "${FINAL_STATUS}" - - - echo "${FINAL_STATUS}" | - grep -Eq 'needsDbUpgrade:[[:space:]]*false' || \ - fail "needsDbUpgrade is not false" - - - echo "${FINAL_STATUS}" | - grep -Eq 'maintenance:[[:space:]]*false' || \ - fail "maintenance is not false" - - - # -------------------------------------------------------- - # Nextcloud: - # - # 0 = normal - # 1 = maintenance - # 2 = DB upgrade required - # -------------------------------------------------------- + validate_occ_status_json "${FINAL_STATUS_JSON}" docker exec \ -u www-data \ nextcloud \ php occ status -e - echo echo "Nextcloud database state validated successfully." - - # ======================================================== - # Start cron using upgraded image - # ======================================================== - log "Start Nextcloud cron" docker compose \ + --env-file "${ENV_FILE}" \ up -d \ --no-deps \ cron - - # ======================================================== - # Apply remaining Compose changes - # ======================================================== - log "Apply remaining Compose changes" docker compose \ + --env-file "${ENV_FILE}" \ up -d \ --remove-orphans @@ -1134,16 +1062,19 @@ then # ============================================================ else - log "No Nextcloud version change" - echo "Current version:" + echo "Current internal version:" echo " ${CURRENT_NC_VERSION}" + # Compose-only change, for example bind mount changes. + # No DB backup/occ upgrade is performed. docker compose \ + --env-file "${ENV_FILE}" \ up -d \ --remove-orphans + wait_for_occ_normal fi @@ -1158,7 +1089,6 @@ for CONTAINER in \ nextcloud-cron \ nextcloud-redis do - RUNNING="$( docker inspect \ -f '{{.State.Running}}' \ @@ -1166,45 +1096,27 @@ do echo false )" - - if [[ "${RUNNING}" != "true" ]] - then - + if [[ "${RUNNING}" != "true" ]]; then fail "${CONTAINER} is not running" - fi - echo "${CONTAINER}: running" - done # ============================================================ -# Final Nextcloud status +# Final OCC status/version # ============================================================ log "Final OCC status" -FINAL_STATUS="$( - docker exec \ - -u www-data \ - nextcloud \ - php occ status -)" +wait_for_occ_normal +FINAL_STATUS_JSON="$(get_occ_status_json)" -echo "${FINAL_STATUS}" +echo "${FINAL_STATUS_JSON}" - -echo "${FINAL_STATUS}" | - grep -Eq 'needsDbUpgrade:[[:space:]]*false' || \ - fail "Final needsDbUpgrade is not false" - - -echo "${FINAL_STATUS}" | - grep -Eq 'maintenance:[[:space:]]*false' || \ - fail "Final maintenance is not false" +validate_occ_status_json "${FINAL_STATUS_JSON}" docker exec \ @@ -1212,20 +1124,22 @@ docker exec \ nextcloud \ php occ status -e - -# ============================================================ -# Show actual running version -# ============================================================ - RUNNING_NC_VERSION="$( - docker exec \ - nextcloud \ - php -r ' - require "/var/www/html/version.php"; - echo implode(".", $OC_Version); - ' + printf '%s\n' "${FINAL_STATUS_JSON}" | + json_string_value version )" +RUNNING_NC_VERSIONSTRING="$( + printf '%s\n' "${FINAL_STATUS_JSON}" | + json_string_value versionstring +)" + +[[ -n "${RUNNING_NC_VERSION}" ]] || \ + fail "Could not determine final Nextcloud version" + +[[ "${RUNNING_NC_VERSION}" == "${TARGET_NC_VERSION}" ]] || \ + fail \ + "Running Nextcloud version ${RUNNING_NC_VERSION} does not match target ${TARGET_NC_VERSION}" RUNNING_IMAGE="$( docker inspect \ @@ -1240,13 +1154,20 @@ RUNNING_IMAGE="$( log "Nextcloud deployment successful" -echo "Previous Nextcloud version:" + +echo "Previous Nextcloud internal version:" echo " ${CURRENT_NC_VERSION}" echo -echo "Running Nextcloud version:" +echo "Running Nextcloud internal version:" echo " ${RUNNING_NC_VERSION}" +if [[ -n "${RUNNING_NC_VERSIONSTRING}" ]]; then + echo + echo "Running Nextcloud display version:" + echo " ${RUNNING_NC_VERSIONSTRING}" +fi + echo echo "Previous Docker image:" echo " ${OLD_IMAGE}" @@ -1255,10 +1176,7 @@ echo echo "Running Docker image:" echo " ${RUNNING_IMAGE}" - -if [[ "${VERSION_CHANGED}" == "true" ]] -then - +if [[ "${VERSION_CHANGED}" == "true" ]]; then echo echo "Pre-upgrade backup:" echo " ${BACKUP_DIR}" @@ -1269,14 +1187,17 @@ then echo echo "Backup retention:" - echo " last ${BACKUP_KEEP} backups" - + echo " last ${BACKUP_KEEP} complete backups" fi # ============================================================ -# Remove credentials from process environment +# Clear credentials # ============================================================ unset PGPASSWORD unset POSTGRES_PASSWORD + +PRODUCTION_CHANGED=false + +exit 0