Skip to content

bug(backups/postgres): CNPG in-place restore purge-skip relies on creationTimestamp, not true provenance #3322

Closed

Description

@IvanHunters

Summary

The CNPG in-place restore reconciler decides whether to skip the destructive Cluster + PVC purge using the live Cluster's creationTimestamp relative to the RestoreJob's StartedAt (internal/backupcontroller/cnpgstrategy_controller.go, cnpgClusterFreshlyRecovered + the cnpgPurgeNeeded call site). The timestamp is a heuristic for "this restore produced the live recovery Cluster", not true provenance. Two edge cases can defeat it and let a restore report Succeeded against the wrong/stale data.

Exposure

  1. Concurrent RestoreJobs to the same app. The RestoreJob controller is a plain reconciler with no per-target mutual exclusion. If a second restore starts before the first restore's replacement Cluster is rendered, the timestamp cannot prove which job produced that Cluster; the second job may skip its purge and succeed against the first job's backup.

  2. External / chart re-creation of the old Cluster during the pre-purge window. StartedAt is stamped on the first reconcile, but the purge happens on a later reconcile and the window can span the WAL-archive gate (up to cnpgWALArchiveDeadline, ~3m). If the old recovery Cluster is deleted during that window (operator action, a competing controller) the HelmRelease is still active and re-renders it from the previous restore's config, giving it a creationTimestamp after StartedAt. The reconciler then treats it as freshly-recovered, skips patch + purge, and can report success on stale data.

Not a regression

This is pre-existing. The guard before #3318 keyed only on hasRecovery and had the same (larger) exposure. #3318 narrowed it to a strict subset (hasRecovery && createdAt > StartedAt), which removes the common repeat-restore no-op (#3311) but does not establish true provenance. This issue tracks closing the residual gap, not fixing a regression introduced by #3318.

Impact

Data integrity: in the two edge cases above a restore can report Succeeded while the database holds data from the wrong backup or the pre-restore state. Requires concurrency or external Cluster deletion, so it is not the common path, but the failure is silent.

Proposed hardening (any of, in rough order of robustness)

  • Record the purged Cluster's UID (or a restore-specific marker) on the TargetPurged condition at purge time, and only skip re-purge when the live Cluster is confirmed to be the object this restore produced.
  • Additionally gate the skip on the live Cluster's spec.bootstrap.recovery.source (and recovery target time) matching this restore's intended source, so a Cluster re-created from a different prior-restore config is still purged.
  • Enforce target-level mutual exclusion for RestoreJobs against the same Postgres app so concurrent restores cannot interleave.

References

Activity

  1. added
    kind/bugCategorizes issue or PR as related to a bug
    area/databaseIssues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse)
    on Jul 16, 2026
  2. added
    triage/acceptedIndicates an issue is ready to be actively worked on
    priority/important-longtermImportant over the long term, but may not be staffed and/or may need multiple releases to complete
    and removed
    triage/needs-triageIndicates an issue needs triage by a maintainer
    on Sep 23, 2026
  3. added a commit that references this issue on Oct 11, 2026
    1d813c9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/databaseIssues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse)kind/bugCategorizes issue or PR as related to a bugpriority/important-longtermImportant over the long term, but may not be staffed and/or may need multiple releases to completetriage/acceptedIndicates an issue is ready to be actively worked on

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions