Skip to content

fix(postgres): read the live Cluster when deciding the WAL-archive prefix - #4299

Merged
Aleksei Sviridkin (lexfrei) merged 1 commit into
cozystack:mainfrom
mattia-eleuteri:fix/cnpg-restore-servername-stale-read
Sep 29, 2026
Merged

Aleksei Sviridkin (lexfrei) merged 1 commit into
cozystack:mainfrom
mattia-eleuteri:fix/cnpg-restore-servername-stale-read

Conversation

@mattia-eleuteri

@mattia-eleuteri mattia-eleuteri commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

Fixes #4298.

applyClusterPluginBackup preserves the serverName already on the Cluster so that a backup does not flip the WAL-archive prefix mid-stream, which would split the archive across two prefixes. That rule is right; the read it rests on is not reliable in one window.

The value was read through the manager's cached client, and BackupJobReconciler.SetupWithManager registers For(&BackupJob{}) and nothing else, so a Cluster only reaches the cache through a lazily started informer with no resync tied to the Cluster's own changes. An in-place RestoreJob deletes the Cluster and the chart re-renders it with bootstrap.newServerName seconds later, so a read served during that window describes the object that was purged — and the source's serverName is written back over the freshly rendered one. barman-cloud-check-wal-archive then refuses the destination with Expected empty archive and the RestoreJob ends on its 30-minute deadline, which is the failure newServerName was added in #3988 to prevent.

The same stale read carries a stale UID, and that UID owns the ObjectStore this function applies, so the store is garbage-collected as soon as it is written.

Reading through the dynamic client this reconciler already carries keeps the preservation rule and makes the value it rests on truthful. There is precedent in the same file: the HelmRelease suspend/resume path reads that way for the same reason.

Observed on a v1.6.3 cluster, twice on separate runs with the same four-second gap between the chart's apply and the driver's. The regression test makes the two clients disagree the way they disagree in that window and asserts the driver believes the live one; reverting the fix fails it on the serverName assertion.

Screenshots

Downstream repositories

Release note

fix(postgres): an in-place restore no longer loses its fresh WAL-archive prefix to a stale Cluster read, which left the restored cluster archiving onto the source's prefix and the RestoreJob failing on its deadline

Summary by CodeRabbit

  • Bug Fixes
    • Backup processing now uses the latest CNPG Cluster state rather than potentially outdated cached information. This ensures the cluster’s current server name and identity are used when associating backup ObjectStore resources, including when the cached and live cluster details differ.

@mattia-eleuteri mattia-eleuteri added the kind/backport Categorizes issue or PR as requiring a backport to the current release line label Sep 16, 2026
@github-actions github-actions Bot added size/L This PR changes 100-499 lines, ignoring generated files area/database Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse) kind/bug Categorizes issue or PR as related to a bug labels Sep 16, 2026
@coderabbitai

coderabbitai Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: cozystack/cozystack/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: b69145d8-05f5-489b-9950-5efed257eab6

📥 Commits

Reviewing files that changed from the base of the PR and between db3343b and 67bf9ca.

📒 Files selected for processing (2)
  • internal/backupcontroller/cnpgstrategy_controller.go
  • internal/backupcontroller/cnpgstrategy_controller_test.go

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

The backup controller now reads CNPG Clusters through a dynamic client and converts the live objects to the typed Cluster model. Tests verify that backup application uses the live Cluster’s server name and UID.

Changes

CNPG live Cluster reads

Layer / File(s) Summary
Dynamic Cluster lookup
internal/backupcontroller/cnpgstrategy_controller.go
The controller defines the CNPG Cluster resource mapping. applyClusterPluginBackup reads the live Cluster through the dynamic client and converts it to the typed Cluster model.
Dynamic client test coverage
internal/backupcontroller/cnpgstrategy_controller_test.go
Tests configure the dynamic client and provide unstructured CNPG resources. A new test verifies that the live Cluster’s server name and UID are used for ObjectStore ownership. Existing tests cover missing Clusters, server-name preservation, and Cluster patching.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 67bf9

The change makes in-place PostgreSQL restores use the live Cluster's server name and UID, which fixes the reported failure. No merge-blocking risk was found.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 67bf9

The live read reduces the risk of a backup using a deleted Cluster’s archive prefix or ownership identity. The review found no new access path or material security exposure, but the multi-step update can still race with Cluster replacement.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • observed — The changed read and subsequent writes operate on the BackupJob’s namespace. The existing controller permission to get Clusters is not expanded by this source change.

Trust Boundaries and Controls

  • observed — The controller takes Cluster identity from a Kubernetes API read rather than its cached copy, then uses that identity for the ObjectStore owner reference. The test-only fake client adds no production caller.

Resilience and Maintainability Implications

  • inferred — Cluster replacement after the read can still leave an old owner UID on the ObjectStore while the later name-based patch reaches a new Cluster. The base implementation had the same separate writes, and the added test does not exercise an inter-operation replacement.

Hardening Proposals

  • proposed — For stronger recovery guarantees, exercise Cluster replacement and partial failures between the read and both applies, and consider an identity check or convergent retry before treating the multi-resource projection as complete.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: reading the live CNPG Cluster when determining the WAL-archive prefix.
Linked Issues check ✅ Passed Issue #4298 requires live Cluster values during backup configuration. The controller now reads the CNPG Cluster with the dynamic client, converts it to the typed object, and uses its live serverName…
Out of Scope Changes check ✅ Passed The changes are limited to the CNPG backup controller and its tests. The dynamic read, conversion error handling, test updates, and regression test directly support issue #4298. No unrelated changes a…
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 2 files.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mattia-eleuteri NOT LGTM, only for the misplaced doc comments below. The fix itself is right.

Business context: a BackupJob reconcile can read a stale cached Cluster while an in-place restore re-renders it, write the source's serverName back over bootstrap.newServerName, and own the ObjectStore by a UID that no longer exists.

The change is still needed after #4544 and #4443. The hold from #4544 gates only on StartedAt == nil, and reconcileCNPG calls applyClusterPluginBackup on every poll until the backup finishes. A run that started before the restore keeps re-applying the plugin through the whole purge and re-render window, so the stale read is still reachable. The ObjectStore owner UID part is independent of the hold anyway.

I merged the head onto current main locally (clean, both files auto-merge) and go test ./internal/backupcontroller/... passes. Swapping the dynamic read back to r.Get fails TestApplyClusterPluginBackup_ReadsTheLiveClusterNotTheCache on the serverName assertion, so the test pins the fix.

Blocker

The new declarations are inserted between an existing doc comment and the symbol it documents, in both files. Go attaches the whole comment block to the next declaration, so the docs end up on the wrong symbols.

  • In the controller, the first two paragraphs of the applyClusterPluginBackup doc ("wires the templated strategy...", "Returns an apierrors.IsNotFound error...") now belong to cnpgClusterGVR. go doc -u ./internal/backupcontroller cnpgClusterGVR prints them, and the function keeps only its second half.
  • In the test, testCNPGScheme's doc comment now opens the doc of TestApplyClusterPluginBackup_ReadsTheLiveClusterNotTheCache, and testCNPGScheme has none.

Fix: move cnpgClusterGVR above the applyClusterPluginBackup doc block (or below the function), and move the new test and cnpgDynamicFor above the testCNPGScheme comment.

Non-blocking

  1. The same rationale is repeated in the cnpgClusterGVR doc, the long comment inside the function, the test doc and the commit body. One short comment at the read is enough, something like "live read: the cached Cluster can predate a restore re-render, and both serverName and UID below must come from the current object". "This controller watches BackupJob and nothing else" will go stale the day someone adds a watch.
  2. E2E Tests is red on a run from 2026-09-16, before the fork e2e lane was fixed on 2026-09-27. It's not caused by this diff, but there is no e2e result for it yet. A rebase onto main gives it a real run.

// hard validation error (CNPG's Cluster CRD has many required fields the
// driver does not set), so we surface the precondition explicitly and let
// the caller treat it as a retryable wait.
// cnpgClusterGVR reads Clusters through the dynamic client. The typed client

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lands in the middle of the applyClusterPluginBackup doc block, so the paragraphs above it are now the godoc of cnpgClusterGVR (go doc -u shows it). Move the var above that block or below the function.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved cnpgClusterGVR above the applyClusterPluginBackup doc block in 67bf9ca. go doc -u ./internal/backupcontroller cnpgClusterGVR now prints only its own one-line doc, and the function has its full doc back.


// testCNPGScheme returns a runtime.Scheme that knows the unstructured
// HelmRelease GVK used by the dynamic-client tests above.
// TestApplyClusterPluginBackup_ReadsTheLiveClusterNotTheCache pins the read

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: the testCNPGScheme comment just above now documents this test, and testCNPGScheme is left without one. Put the new test and cnpgDynamicFor above that comment.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the new test and cnpgDynamicFor above the testCNPGScheme comment in 67bf9ca, so testCNPGScheme has its doc back and the test has its own.

…efix

applyClusterPluginBackup preserves the serverName already on the Cluster, so
that a backup does not flip the archive prefix mid-stream. It read that value
through the manager's cached client, and this controller registers
For(&BackupJob{}) and nothing else, so a Cluster only reaches the cache
through a lazily started informer with no resync tied to its own changes.

An in-place RestoreJob deletes the Cluster and the chart re-renders it with
bootstrap.newServerName seconds later. A read served during that window
describes the object that was purged, so the source's serverName is written
back over the freshly rendered one and the restored cluster archives onto the
source's WAL prefix. barman-cloud-check-wal-archive then refuses to start with
"Expected empty archive" and the RestoreJob ends on its deadline -- the
failure newServerName was added to prevent.

The stale read carries a stale UID too, and that UID owns the ObjectStore this
function applies, so the store is garbage-collected as soon as it is written.

Reading through the dynamic client keeps the preservation rule and makes the
value it rests on truthful.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <[email protected]>
@mattia-eleuteri
mattia-eleuteri force-pushed the fix/cnpg-restore-servername-stale-read branch from db3343b to 67bf9ca Compare September 29, 2026 15:06
@github-actions github-actions Bot added size/M This PR changes 30-99 lines, ignoring generated files and removed size/L This PR changes 100-499 lines, ignoring generated files labels Sep 29, 2026
@mattia-eleuteri

Copy link
Copy Markdown
Collaborator Author

Pushed 67bf9ca, rebased onto current main (clean, still one commit) so the fork e2e lane gets a real run. On the non-blocking comment: the rationale is now a single two-line comment at the read, the cnpgClusterGVR and test docs say only what those symbols are, and the "watches BackupJob and nothing else" wording is gone from the code. go test ./internal/backupcontroller/... passes, and swapping the dynamic read back to r.Get still fails TestApplyClusterPluginBackup_ReadsTheLiveClusterNotTheCache on the serverName assertion.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mattia-eleuteri LGTM. The doc comments are fixed. go doc -u now shows cnpgClusterGVR with its own two lines and applyClusterPluginBackup with its full doc, and in the test file testCNPGScheme has its own doc again, separate from the new helper.

The fix still holds on current main. BackupJobReconciler sets up its dynamic client in SetupWithManager, so the live read works in the real binary and not only in tests, and the ObjectStore owner UID now comes from the same live object. go test ./internal/backupcontroller/... passes on 67bf9ca. When I put the cached r.Get back in place of the dynamic read, TestApplyClusterPluginBackup_ReadsTheLiveClusterNotTheCache fails with "effective serverName = postgres-app".

CI is red, but not because of this diff. "Unit & controller tests" failed while downloading k8s.io/kube-openapi for the kube-ovn-webhook module (a stream error from proxy.golang.org). internal/backupcontroller passed in that same job. Because the Pull Request run failed, the fork e2e lane posted "e2e not run", so E2E Tests is red without a real e2e run (run 36589358812, attempt 1). The rebase was meant to get a real e2e run, so that job needs a rerun before merge.

@lexfrei
Aleksei Sviridkin (lexfrei) merged commit 7d44372 into cozystack:main Sep 29, 2026
100 of 102 checks passed
@github-actions

Copy link
Copy Markdown

Created backport PR for release-1.6:

Please cherry-pick the changes locally and resolve any conflicts.

git fetch origin backport-4299-to-release-1.6
git worktree add --checkout .worktree/backport-4299-to-release-1.6 backport-4299-to-release-1.6
cd .worktree/backport-4299-to-release-1.6
git reset --hard HEAD^
git cherry-pick -x 67bf9ca317879299241bea62b0a172dfc01ea5a0
git push --force-with-lease

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/database Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse) kind/backport Categorizes issue or PR as requiring a backport to the current release line kind/bug Categorizes issue or PR as related to a bug size/M This PR changes 30-99 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(postgres): in-place restore loses newServerName to a stale Cluster read

3 participants