Restore accepts an archive from a newer Postulo and leaves the database on a schema the code does not know #479

Closed
opened 2026-10-01 10:46:59 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading how restore checks an archive's manifest.

Checked again on 2026-10-01 against main at 0a5cad587: still present.

read_manifest gates only on backup_format and never reads the Postulo version the manifest records. Restoring an archive written by a newer Postulo onto an older one replaces the database with one carrying migrations this code does not have; migrate cannot go backwards and ignores applied migrations it does not know, so the restore reports success and the application then runs against tables and columns it does not expect.

  • Where: core/backup.py:405 read_manifest(), whose only version check is 1 <= fmt <= BACKUP_FORMAT; core/backup.py:349 writes "version": __version__ into the manifest and nothing reads it back; core/backup.py:565 runs migrate after loading.
  • What happens: an operator upgrades, takes a backup, finds a problem and rolls the image back to the previous release, then restores the backup taken after the upgrade. The backup format has not changed between the two releases, so the archive is accepted. A column the newer release added as NOT NULL without a database default makes every insert on that table fail; a column it renamed or removed makes reads fail. The restore itself said nothing.
  • Why it matters: the failure surfaces later, on the first page that writes, far from its cause, and the one tool that could have said "install version X first" had the information and did not use it.
  • Fix: in restore_backup, compare the manifest's postulo.version with __version__ and refuse a newer archive with a message that names the version to install; after loading, also refuse (and say so) when django_migrations names migrations absent from the code, which covers development builds with the same version string. Allow it only with an explicit flag, separate from --force, which already means "replace a non-empty instance".

Related to #32 (closed), which promised that restore "checks the manifest against the installed version".

Done when

  • tests/test_backup.py: an archive whose manifest names a version newer than __version__ is refused before the database is touched, and the message names that version.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading how `restore` checks an archive's manifest. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. `read_manifest` gates only on `backup_format` and never reads the Postulo version the manifest records. Restoring an archive written by a newer Postulo onto an older one replaces the database with one carrying migrations this code does not have; `migrate` cannot go backwards and ignores applied migrations it does not know, so the restore reports success and the application then runs against tables and columns it does not expect. - **Where:** `core/backup.py:405` `read_manifest()`, whose only version check is `1 <= fmt <= BACKUP_FORMAT`; `core/backup.py:349` writes `"version": __version__` into the manifest and nothing reads it back; `core/backup.py:565` runs `migrate` after loading. - **What happens:** an operator upgrades, takes a backup, finds a problem and rolls the image back to the previous release, then restores the backup taken after the upgrade. The backup format has not changed between the two releases, so the archive is accepted. A column the newer release added as `NOT NULL` without a database default makes every insert on that table fail; a column it renamed or removed makes reads fail. The restore itself said nothing. - **Why it matters:** the failure surfaces later, on the first page that writes, far from its cause, and the one tool that could have said "install version X first" had the information and did not use it. - **Fix:** in `restore_backup`, compare the manifest's `postulo.version` with `__version__` and refuse a newer archive with a message that names the version to install; after loading, also refuse (and say so) when `django_migrations` names migrations absent from the code, which covers development builds with the same version string. Allow it only with an explicit flag, separate from `--force`, which already means "replace a non-empty instance". Related to #32 (closed), which promised that `restore` "checks the manifest against the installed version". ## Done when - `tests/test_backup.py`: an archive whose manifest names a version newer than `__version__` is refused before the database is touched, and the message names that version.
tiagoagueda added this to the 0.6.0 milestone 2026-10-01 10:46:59 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#479
No description provided.