diff --git a/scripts/backup/Dockerfile.backup b/scripts/backup/Dockerfile.backup index dcef98cb..bf0e8c56 100644 --- a/scripts/backup/Dockerfile.backup +++ b/scripts/backup/Dockerfile.backup @@ -18,4 +18,8 @@ RUN bunzip2 /tmp/restic.bz2 \ && rm -f /tmp/restic \ && restic version +# Copy backup scripts into image +COPY scripts/backup/*.sh /opt/backup/ +RUN chmod +x /opt/backup/*.sh + ENTRYPOINT ["/opt/backup/backup.sh"] diff --git a/scripts/backup/backup.sh b/scripts/backup/backup.sh index 110f427b..71604e44 100755 --- a/scripts/backup/backup.sh +++ b/scripts/backup/backup.sh @@ -76,26 +76,42 @@ DATAREV="$(mysql \ [ -n "${DATAREV}" ] || log "WARN could not read alembic_version - datarev will be empty" log "writing manifest" -python3 /opt/backup/manifest.py \ - --out "${SETDIR}/manifest.json" \ - --takenat "${STAMP}" \ - --site "${SITE}" \ - --imagetag "${SHOPDB_IMAGE_TAG:-source}" \ - --datarev "${DATAREV}" \ - --migrations /app/migrations \ - --plugins /app/plugins +# Manifest generation requires Python. If not available (offline build), create +# a minimal manifest with just the essentials for restore identification. +if command -v python3 >/dev/null 2>&1; then + python3 /opt/backup/manifest.py \ + --out "${SETDIR}/manifest.json" \ + --takenat "${STAMP}" \ + --site "${SITE}" \ + --imagetag "${SHOPDB_IMAGE_TAG:-source}" \ + --datarev "${DATAREV}" \ + --migrations /app/migrations \ + --plugins /app/plugins -# Consistency guard: if the DATA revision does not match the CODE head, this -# backup captures a database the running image has outrun. It is still stored -# (losing it would be worse) but flagged loudly so a restore of it is a -# deliberate choice, not a surprise. -CODEREV="$(python3 -c \ - 'import json,sys;print(",".join(sorted(json.load(open(sys.argv[1]))["schemarev"]["core"])))' \ - "${SETDIR}/manifest.json" 2>/dev/null || echo "")" -if [ -n "${DATAREV}" ] && [ "${DATAREV}" != "${CODEREV}" ]; then - log "WARN schema drift: database is at '${DATAREV}' but code head is '${CODEREV}'" - log "WARN this DB has not run 'flask db upgrade' to the running image; restore will upgrade it" - history_append backup warning "${SETNAME}" "Schema drift: data ${DATAREV} vs code ${CODEREV}" + # Consistency guard: if the DATA revision does not match the CODE head, this + # backup captures a database the running image has outrun. It is still stored + # (losing it would be worse) but flagged loudly so a restore of it is a + # deliberate choice, not a surprise. + CODEREV="$(python3 -c \ + 'import json,sys;print(",".join(sorted(json.load(open(sys.argv[1]))["schemarev"]["core"])))' \ + "${SETDIR}/manifest.json" 2>/dev/null || echo "")" + if [ -n "${DATAREV}" ] && [ "${DATAREV}" != "${CODEREV}" ]; then + log "WARN schema drift: database is at '${DATAREV}' but code head is '${CODEREV}'" + log "WARN this DB has not run 'flask db upgrade' to the running image; restore will upgrade it" + history_append backup warning "${SETNAME}" "Schema drift: data ${DATAREV} vs code ${CODEREV}" + fi +else + # No Python available - create minimal manifest with bash + log "Python not available - creating minimal manifest" + cat > "${SETDIR}/manifest.json" <<-EOF + { + "takenat": "${STAMP}", + "site": "${SITE}", + "imagetag": "${SHOPDB_IMAGE_TAG:-source}", + "datarev": "${DATAREV}", + "note": "Minimal manifest - Python not available in offline build" + } + EOF fi # 3. restic snapshot: db dump + instance tree + manifest, one point in time. @@ -137,9 +153,16 @@ else fi # Record the verification result into the manifest so the UI can badge the set. -python3 /opt/backup/manifest.py \ - --amend "${SETDIR}/manifest.json" \ - --ischeckpassed "${IS_CHECK_PASSED}" +if command -v python3 >/dev/null 2>&1; then + python3 /opt/backup/manifest.py \ + --amend "${SETDIR}/manifest.json" \ + --ischeckpassed "${IS_CHECK_PASSED}" +else + # Update manifest with check result using bash (no Python needed) + TMPFILE="${SETDIR}/manifest.json.tmp" + cat "${SETDIR}/manifest.json" | sed "s/}$/, \"ischeckpassed\": ${IS_CHECK_PASSED}\n}/" > "${TMPFILE}" + mv "${TMPFILE}" "${SETDIR}/manifest.json" +fi # Clear the request sentinel the UI drops when an operator clicks "Back up now". rm -f "${BACKUPS_ROOT}/.backup-requested" 2>/dev/null || true