diff --git a/stacks/nextcloud/docker-compose.yml b/stacks/nextcloud/docker-compose.yml index 3509328..d90bf2a 100644 --- a/stacks/nextcloud/docker-compose.yml +++ b/stacks/nextcloud/docker-compose.yml @@ -1,64 +1,1203 @@ -services: +#!/usr/bin/env bash - nextcloud: - image: nextcloud:35.0.1 - container_name: nextcloud - restart: unless-stopped +set -Eeuo pipefail - env_file: - - /srv/nfs/infra/nextcloud/.env - - environment: - OVERWRITEHOST: "nc.zk-tech.info" - OVERWRITEPROTOCOL: "https" - - TRUSTED_PROXIES: "127.0.0.1,172.19.0.0/16,172.19.0.15" - NEXTCLOUD_TRUSTED_DOMAINS: "nc.zk-tech.info" - - UPLOAD_MAX_FILESIZE: "16G" - POST_MAX_FILE_SIZE: "16G" - - PHP_MEMORY_LIMIT: "4G" - PHP_UPLOAD_LIMIT: "16G" - - REDIS_HOST: "redis" - - volumes: - - /srv/nfs/infra/nextcloud/config:/var/www/html/config - - /srv/nfs/infra/nextcloud/data:/var/www/html/data - - /srv/nfs/infra/nextcloud/apps:/var/www/html/custom_apps - - /srv/nfs/infra/nextcloud/themes:/var/www/html/themes - - /srv/nfs/infra/nextcloud/config/php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:ro - - depends_on: - - redis - - networks: - - default - - interdock - - redis: - image: redis:rc-32bit-buster - container_name: nextcloud-redis - restart: unless-stopped - - command: - - redis-server - - --appendonly - - "yes" - - volumes: - - redis-data:/data - - networks: - - default +umask 077 -volumes: - redis-data: - driver: local +# ============================================================ +# Configuration +# ============================================================ + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SRC="${ROOT}/stacks/nextcloud" + +LIVE="/srv/nfs/infra/nextcloud" +ENV_FILE="${LIVE}/.env" + +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 -networks: - interdock: - external: true +# ============================================================ +# Runtime state +# ============================================================ + +VERSION_CHANGED=false +PRODUCTION_CHANGED=false +MIGRATION_STARTED=false +BACKUP_COMPLETE=false +UPGRADE_STARTED="" + + +# ============================================================ +# Helpers +# ============================================================ + +log() +{ + echo + echo "============================================================" + echo "$*" + echo "============================================================" +} + + +cleanup_temp() +{ + rm -f -- "${PREVIOUS_COMPOSE}" 2>/dev/null || true +} + + +restore_previous_compose() +{ + if [[ ! -f "${PREVIOUS_COMPOSE}" ]]; then + echo "Previous Compose file is unavailable; automatic restore skipped." + return 0 + fi + + 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 "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + echo " Nextcloud deployment FAILED" + echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + + echo + echo "Exit code:" + echo " ${RC}" + + echo + echo "Failed at line:" + echo " ${FAILED_LINE}" + + echo + echo "Failed command:" + echo " ${FAILED_COMMAND}" + + if [[ "${MIGRATION_STARTED}" == "true" ]]; then + echo + echo "Database migration MAY already have started." + echo "Automatic rollback will NOT be performed." + + if [[ "${BACKUP_COMPLETE}" == "true" ]]; then + echo + echo "Pre-upgrade backup:" + echo " ${BACKUP_DIR}" + fi + + echo + 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 service rollback is required." + fi + + 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 +} + + +cleanup_old_backups() +{ + echo + echo "--- Backup retention ---" + echo "Keeping last ${BACKUP_KEEP} complete pre-upgrade backups." + + mkdir -p "${BACKUP_ROOT}" + + mapfile -t BACKUPS < <( + find "${BACKUP_ROOT}" \ + -mindepth 1 \ + -maxdepth 1 \ + -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 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 + echo "Removing old backup:" + echo " ${OLD_BACKUP}" + + rm -rf -- "${OLD_BACKUP}" + done +} + + +version_less_than() +{ + local A="$1" + local B="$2" + + [[ "${A}" == "${B}" ]] && return 1 + + [[ "$( + printf '%s\n%s\n' "${A}" "${B}" | + sort -V | + head -n1 + )" == "${A}" ]] +} + + +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 + + +# ============================================================ +# Required commands +# ============================================================ + +log "Pre-flight checks" + +for CMD in \ + docker \ + psql \ + pg_dump \ + pg_restore \ + sha256sum \ + sort \ + grep \ + sed \ + find \ + awk \ + du \ + diff \ + seq +do + command -v "${CMD}" >/dev/null 2>&1 || \ + fail "Required command not found: ${CMD}" +done + + +docker compose version >/dev/null 2>&1 || \ + fail "Docker Compose plugin is not available" + + +# ============================================================ +# Required files +# ============================================================ + +[[ -f "${SRC}/docker-compose.yml" ]] || \ + fail "${SRC}/docker-compose.yml not found" + +[[ -f "${LIVE}/docker-compose.yml" ]] || \ + fail "${LIVE}/docker-compose.yml not found" + +[[ -f "${ENV_FILE}" ]] || \ + fail "${ENV_FILE} not found" + + +# Preserve production Compose before any change. +cp \ + "${LIVE}/docker-compose.yml" \ + "${PREVIOUS_COMPOSE}" + + +# ============================================================ +# Backup directory access +# ============================================================ + +mkdir -p "${BACKUP_ROOT}" + +[[ -w "${BACKUP_ROOT}" ]] || \ + fail "${BACKUP_ROOT} is not writable" + + +# ============================================================ +# Load PostgreSQL settings +# ============================================================ + +set -a + +# shellcheck disable=SC1090 +source "${ENV_FILE}" + +set +a + + +: "${POSTGRES_HOST:?POSTGRES_HOST not defined}" +: "${POSTGRES_DB:?POSTGRES_DB not defined}" +: "${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 + + +# ============================================================ +# Validate requested Compose +# ============================================================ + +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 +# ============================================================ + +log "Detect requested Nextcloud image" + +mapfile -t NEXTCLOUD_IMAGES < <( + docker compose \ + --env-file "${ENV_FILE}" \ + -f "${SRC}/docker-compose.yml" \ + config --images | + grep '^nextcloud:' | + sort -u +) + + +if [[ "${#NEXTCLOUD_IMAGES[@]}" -ne 1 ]]; then + echo "Detected Nextcloud images:" + + if (( ${#NEXTCLOUD_IMAGES[@]} > 0 )); then + printf ' %s\n' "${NEXTCLOUD_IMAGES[@]}" + else + echo " none" + 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}" + + +# ============================================================ +# Current Nextcloud container +# ============================================================ + +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}}' \ + nextcloud +)" + + +# ============================================================ +# Detect current Nextcloud status/version +# +# 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="$(get_occ_status_json 2>&1)" +CURRENT_STATUS_RC=$? +set -e + +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}" | + json_string_value version +)" + +CURRENT_NC_VERSIONSTRING="$( + printf '%s\n' "${CURRENT_STATUS_JSON}" | + json_string_value versionstring +)" + +[[ -n "${CURRENT_NC_VERSION}" ]] || \ + fail "Could not determine current internal Nextcloud version" + +validate_occ_status_json "${CURRENT_STATUS_JSON}" + + +docker exec \ + -u www-data \ + nextcloud \ + php occ status -e + + +echo "Current image:" +echo " ${OLD_IMAGE}" + +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 +# ============================================================ + +log "Pull target Nextcloud image" + + +docker pull "${NEW_IMAGE}" + + +# ============================================================ +# Determine exact target Nextcloud version +# ============================================================ + +log "Detect target Nextcloud version" + +TARGET_NC_VERSION="$( + docker run \ + --rm \ + --entrypoint php \ + "${NEW_IMAGE}" \ + -r ' + require "/usr/src/nextcloud/version.php"; + echo implode(".", $OC_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" + + +echo "Target Docker image:" +echo " ${NEW_IMAGE}" + +echo "Target Nextcloud internal version:" +echo " ${TARGET_NC_VERSION}" + +echo "Target Nextcloud display version:" +echo " ${TARGET_NC_VERSIONSTRING}" + + +# ============================================================ +# Version safety checks +# ============================================================ + +log "Validate version transition" + +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%%.*}" + +if (( TARGET_MAJOR > CURRENT_MAJOR + 1 )); then + fail \ + "Skipping Nextcloud major versions is forbidden: ${CURRENT_MAJOR} -> ${TARGET_MAJOR}" +fi + +if [[ "${CURRENT_NC_VERSION}" != "${TARGET_NC_VERSION}" ]]; then + VERSION_CHANGED=true +fi + + +echo "Current internal version: ${CURRENT_NC_VERSION}" +echo "Target internal version: ${TARGET_NC_VERSION}" +echo "Version changed: ${VERSION_CHANGED}" + + +# ============================================================ +# PRE-UPGRADE BACKUP +# +# Only when the actual internal Nextcloud version changes. +# ============================================================ + +if [[ "${VERSION_CHANGED}" == "true" ]]; then + log "Prepare Nextcloud pre-upgrade backup" + + mkdir -p "${DB_BACKUP_DIR}" + + echo + echo "--- Backup current docker-compose.yml ---" + + cp \ + "${LIVE}/docker-compose.yml" \ + "${BACKUP_DIR}/docker-compose.yml" + + printf '%s\n' "${CURRENT_STATUS_JSON}" \ + > "${BACKUP_DIR}/nextcloud-status-before.json" + + cat > "${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 + fail \ + "pg_dump ${PG_DUMP_VERSION} is older than PostgreSQL server ${SERVER_VERSION}" + fi + + 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}" + + log "Create PostgreSQL backup" + + pg_dump \ + -h "${POSTGRES_HOST}" \ + -p "${POSTGRES_PORT}" \ + -U "${POSTGRES_USER}" \ + -d "${POSTGRES_DB}" \ + --format=custom \ + --no-owner \ + --no-privileges \ + --file="${DB_BACKUP_FILE}" + + [[ -s "${DB_BACKUP_FILE}" ]] || \ + fail "PostgreSQL dump does not exist or is empty" + + echo + echo "--- Validate PostgreSQL dump ---" + + pg_restore \ + --list \ + "${DB_BACKUP_FILE}" \ + > "${DB_BACKUP_DIR}/nextcloud.dump.list" + + [[ -s "${DB_BACKUP_DIR}/nextcloud.dump.list" ]] || \ + fail "pg_restore could not validate PostgreSQL dump" + + echo + echo "--- Generate and verify SHA256 ---" + + ( + cd "${DB_BACKUP_DIR}" + + 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 + ) + + DB_BACKUP_SIZE="$( + du -h "${DB_BACKUP_FILE}" | + awk '{print $1}' + )" + + touch "${BACKUP_DIR}/.complete" + BACKUP_COMPLETE=true + + echo + echo "PostgreSQL backup successful." + + echo + echo "Backup directory:" + echo " ${BACKUP_DIR}" + + echo + echo "Database dump:" + echo " ${DB_BACKUP_FILE}" + + echo + echo "Dump size:" + echo " ${DB_BACKUP_SIZE}" + + echo + echo "SHA256:" + cat "${DB_BACKUP_DIR}/nextcloud.dump.sha256" + + cleanup_old_backups +fi + + +# ============================================================ +# Deploy requested Compose +# ============================================================ + +log "Deploy docker-compose.yml" + +cp \ + "${SRC}/docker-compose.yml" \ + "${LIVE}/docker-compose.yml" + +PRODUCTION_CHANGED=true + +cd "${LIVE}" + + +# ============================================================ +# Pull all Compose images +# ============================================================ + +log "Pull Docker images" + + +docker compose \ + --env-file "${ENV_FILE}" \ + pull + + +# ============================================================ +# NEXTCLOUD VERSION UPGRADE +# ============================================================ + +if [[ "${VERSION_CHANGED}" == "true" ]]; then + log "Nextcloud upgrade ${CURRENT_NC_VERSION} -> ${TARGET_NC_VERSION}" + + echo + echo "--- Stop Nextcloud cron ---" + + docker compose \ + --env-file "${ENV_FILE}" \ + stop cron || true + + UPGRADE_STARTED="$( + date -u '+%Y-%m-%dT%H:%M:%SZ' + )" + + 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 + + log "Wait for Nextcloud initialization" + + INITIALIZED=false + + for _ in $(seq 1 $((INIT_TIMEOUT / INIT_INTERVAL))) + do + RUNNING="$( + docker inspect \ + -f '{{.State.Running}}' \ + nextcloud 2>/dev/null || + echo false + )" + + if [[ "${RUNNING}" != "true" ]]; then + echo + echo "--- Nextcloud container log ---" + + docker logs \ + --since "${UPGRADE_STARTED}" \ + nextcloud || true + + fail "Nextcloud container stopped during upgrade" + fi + + STARTUP_LOG="$( + docker logs \ + --since "${UPGRADE_STARTED}" \ + nextcloud 2>&1 || true + )" + + if echo "${STARTUP_LOG}" | + grep -q 'Update failed' + then + echo "${STARTUP_LOG}" + fail "Nextcloud automatic upgrade failed" + fi + + if echo "${STARTUP_LOG}" | + grep -q 'Initializing finished' + then + INITIALIZED=true + break + fi + + sleep "${INIT_INTERVAL}" + done + + if [[ "${INITIALIZED}" != "true" ]]; then + echo + echo "--- Nextcloud container log ---" + + docker logs \ + --since "${UPGRADE_STARTED}" \ + nextcloud || true + + fail "Timeout waiting for Nextcloud initialization" + fi + + log "Nextcloud startup upgrade output" + + STARTUP_LOG="$( + docker logs \ + --since "${UPGRADE_STARTED}" \ + 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}" + + log "Run explicit occ upgrade" + + set +e + + OCC_UPGRADE_OUTPUT="$( + docker exec \ + -u www-data \ + nextcloud \ + php occ upgrade 2>&1 + )" + + OCC_UPGRADE_RC=$? + + set -e + + echo "${OCC_UPGRADE_OUTPUT}" + + 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}" + + echo "Explicit occ upgrade reports already current:" + echo " ${EXPLICIT_ALREADY_CURRENT}" + + if [[ "${STARTUP_UPDATE_SUCCESS}" != "true" ]] && + [[ "${EXPLICIT_UPDATE_SUCCESS}" != "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 + + log "Validate Nextcloud status after upgrade" + + wait_for_occ_normal + + FINAL_STATUS_JSON="$(get_occ_status_json)" + echo "${FINAL_STATUS_JSON}" + + validate_occ_status_json "${FINAL_STATUS_JSON}" + + docker exec \ + -u www-data \ + nextcloud \ + php occ status -e + + echo + echo "Nextcloud database state validated successfully." + + log "Start Nextcloud cron" + + docker compose \ + --env-file "${ENV_FILE}" \ + up -d \ + --no-deps \ + cron + + log "Apply remaining Compose changes" + + docker compose \ + --env-file "${ENV_FILE}" \ + up -d \ + --remove-orphans + + +# ============================================================ +# NO NEXTCLOUD VERSION CHANGE +# ============================================================ + +else + log "No Nextcloud version change" + + 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 + + +# ============================================================ +# Final container checks +# ============================================================ + +log "Final container checks" + +for CONTAINER in \ + nextcloud \ + nextcloud-cron \ + nextcloud-redis +do + RUNNING="$( + docker inspect \ + -f '{{.State.Running}}' \ + "${CONTAINER}" 2>/dev/null || + echo false + )" + + if [[ "${RUNNING}" != "true" ]]; then + fail "${CONTAINER} is not running" + fi + + echo "${CONTAINER}: running" +done + + +# ============================================================ +# Final OCC status/version +# ============================================================ + +log "Final OCC status" + +wait_for_occ_normal + +FINAL_STATUS_JSON="$(get_occ_status_json)" + +echo "${FINAL_STATUS_JSON}" + +validate_occ_status_json "${FINAL_STATUS_JSON}" + + +docker exec \ + -u www-data \ + nextcloud \ + php occ status -e + +RUNNING_NC_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 \ + -f '{{.Config.Image}}' \ + nextcloud +)" + + +# ============================================================ +# Success +# ============================================================ + +log "Nextcloud deployment successful" + + +echo "Previous Nextcloud internal version:" +echo " ${CURRENT_NC_VERSION}" + +echo +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}" + +echo +echo "Running Docker image:" +echo " ${RUNNING_IMAGE}" + +if [[ "${VERSION_CHANGED}" == "true" ]]; then + echo + echo "Pre-upgrade backup:" + echo " ${BACKUP_DIR}" + + echo + echo "PostgreSQL dump:" + echo " ${DB_BACKUP_FILE}" + + echo + echo "Backup retention:" + echo " last ${BACKUP_KEEP} complete backups" +fi + + +# ============================================================ +# Clear credentials +# ============================================================ + +unset PGPASSWORD +unset POSTGRES_PASSWORD + +PRODUCTION_CHANGED=false + +exit 0