Gate a restore on every schema chain, not just the core one
The compatibility gate read alembic_version and nothing else, but plugin schemas move independently of core. Roll the code back to an older plugin revision and the core head can be identical, so a dump carrying plugin tables the code has never heard of passed the gate, loaded - destroying what was there - and only then failed in 'flask plugin upgrade-all' with "Can't locate revision". A failed restore that has already replaced what it was restoring over is the worst outcome this feature can produce, and it was reachable from an ordinary rollback. Backups now stamp the applied revision of every alembic_version_<plugin> table alongside the core one, read from the dumped database the same way. restore.sh checks each against that plugin's own versions directory and refuses before touching anything, naming the plugin and the revision to deploy past. A plugin in the dump that this build does not have at all is refused for the same reason. The list endpoint's compatibility badge asks the same question, so the UI cannot offer a set the restore will reject. Sets taken before the stamp existed carry no dataplugins and keep their old core-only verdict. Their plugin revisions are unknowable, and making every existing set unrestorable to close this would be a worse trade than the gap. Proven against a real stack, reproducing the exact case: core head identical, endpoints data stamped at a revision the build does not have. The restore refused, the live row was still there afterwards, and it stopped early enough that the pre-restore safety copy was never needed. The same set with its true revision restored normally. Suite 2605 passed / 9 skipped, with the gate covered both ways plus the legacy fallback. Scope: product
This commit is contained in:
@@ -101,6 +101,47 @@ if ! grep -rqE "^revision = ['\"]${SET_DATAREV}['\"]" /app/migrations/versions 2
|
||||
fi
|
||||
log "data revision ${SET_DATAREV} is known to this code - restore may proceed"
|
||||
|
||||
# The same gate for every PLUGIN chain, because the core chain alone cannot
|
||||
# answer this. Plugin schemas move independently of core: roll the code back to
|
||||
# an older plugin revision and the core head can be identical while the dump
|
||||
# carries plugin tables the code has never heard of. Gating on core only let
|
||||
# that pass, load the dump, and then fail in 'flask plugin upgrade-all' with
|
||||
# "Can't locate revision" and the data already replaced - a failed restore that
|
||||
# has destroyed what it was restoring over.
|
||||
#
|
||||
# A manifest with no dataplugins is a legacy set taken before this was stamped.
|
||||
# Its plugin revisions are unknowable, so it is allowed through on the core gate
|
||||
# alone (the behaviour before this check existed) rather than being made
|
||||
# unrestorable retroactively.
|
||||
SET_DATAPLUGINS="$(python3 -c \
|
||||
'import json,sys
|
||||
try:
|
||||
data = json.load(open(sys.argv[1])).get("dataplugins") or {}
|
||||
except Exception:
|
||||
data = {}
|
||||
for name, rev in sorted(data.items()):
|
||||
print(name + " " + rev)' \
|
||||
"${SETDIR}/manifest.json" 2>/dev/null || echo "")"
|
||||
|
||||
if [ -z "${SET_DATAPLUGINS}" ]; then
|
||||
log "manifest stamps no plugin revisions (legacy set) - core gate only"
|
||||
else
|
||||
while read -r plugin rev; do
|
||||
[ -n "${plugin}" ] || continue
|
||||
chain="/app/plugins/${plugin}/migrations/versions"
|
||||
if [ ! -d "${chain}" ]; then
|
||||
die "the backup carries data for plugin '${plugin}', which this build does not have - deploy a build that includes it before restoring"
|
||||
fi
|
||||
if ! grep -rqE "^revision = ['\"]${rev}['\"]" "${chain}" 2>/dev/null; then
|
||||
log "plugin ${plugin} data revision: ${rev}"
|
||||
die "plugin '${plugin}' data is at a revision this code does not have - deploy code at or past '${rev}' before restoring"
|
||||
fi
|
||||
log "plugin ${plugin} at ${rev} - known to this code"
|
||||
done <<PLUGINREVS
|
||||
${SET_DATAPLUGINS}
|
||||
PLUGINREVS
|
||||
fi
|
||||
|
||||
# Safety set FIRST. Everything below this point is destructive and none of it
|
||||
# is transactional: the dump carries DROP TABLE per table, and the instance tree
|
||||
# is replaced wholesale. Without a copy taken here, a restore of the wrong set -
|
||||
|
||||
Reference in New Issue
Block a user