1204 lines
26 KiB
YAML
1204 lines
26 KiB
YAML
#!/usr/bin/env bash
|
|
|
|
set -Eeuo pipefail
|
|
|
|
umask 077
|
|
|
|
|
|
# ============================================================
|
|
# 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
|
|
|
|
|
|
# ============================================================
|
|
# 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" <<META_EOF
|
|
timestamp=${STAMP}
|
|
|
|
current_image=${OLD_IMAGE}
|
|
target_image=${NEW_IMAGE}
|
|
|
|
current_nextcloud_version=${CURRENT_NC_VERSION}
|
|
target_nextcloud_version=${TARGET_NC_VERSION}
|
|
|
|
postgres_host=${POSTGRES_HOST}
|
|
postgres_port=${POSTGRES_PORT}
|
|
postgres_database=${POSTGRES_DB}
|
|
postgres_user=${POSTGRES_USER}
|
|
META_EOF
|
|
|
|
echo
|
|
echo "--- Test PostgreSQL connection ---"
|
|
|
|
DB_TEST="$(
|
|
psql \
|
|
-h "${POSTGRES_HOST}" \
|
|
-p "${POSTGRES_PORT}" \
|
|
-U "${POSTGRES_USER}" \
|
|
-d "${POSTGRES_DB}" \
|
|
-Atqc 'SELECT 1;'
|
|
)"
|
|
|
|
[[ "${DB_TEST}" == "1" ]] || \
|
|
fail "Cannot connect to PostgreSQL"
|
|
|
|
echo "PostgreSQL connection successful."
|
|
|
|
echo
|
|
echo "--- PostgreSQL version check ---"
|
|
|
|
SERVER_VERSION="$(
|
|
psql \
|
|
-h "${POSTGRES_HOST}" \
|
|
-p "${POSTGRES_PORT}" \
|
|
-U "${POSTGRES_USER}" \
|
|
-d "${POSTGRES_DB}" \
|
|
-Atqc 'SHOW server_version;'
|
|
)"
|
|
|
|
[[ -n "${SERVER_VERSION}" ]] || \
|
|
fail "Could not determine PostgreSQL server version"
|
|
|
|
printf '%s\n' "${SERVER_VERSION}" \
|
|
> "${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
|