Repository: cloudnative-pg/cloudnative-pg

Issue:
[Bug]: StorageSource not used for Replica Join when WAL archiving is enabled via Plugin
### Is there an existing issue already for this bug?

- [x] I have searched for an existing issue, and could not find anything. I believe this is a new bug.

### I have read the troubleshooting guide

- [x] I have read the troubleshooting guide and I think this is a new bug.

### I am running a supported version of CloudNativePG

- [x] I have read the troubleshooting guide and I think this is a new bug.

### Contact Details

gabriele.fedi@enterprisedb.com

### Version

1.27 (latest patch)

### What version of Kubernetes are you using?

1.33

### What is your Kubernetes environment?

Self-managed: kind (evaluation)

### How did you install the operator?

YAML manifest

### What happened?

When a CNPG Cluster is configured to perform WAL Archiving via CNPG-I Plugin, the CNPG operator doesn't behave as expected for new replicas creation.
The expectation is that, if a VolumeSnapshot backup is available and WAL archiving is enabled, the replica join operation is made through a restore from the VolumeSnapshot + WAL replication from the archive. The current behaviour is that even if the two mentioned options are valid, the join operation is done in the "standard way" through a pg_basebackup.  

### Cluster resource

```shell
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: my-cluster
spec:
  backup:
    retentionPolicy: 1d
    target: primary
    volumeSnapshot:
      className: gp3-vsc
      online: true
      onlineConfiguration:
        waitForArchive: true
      snapshotOwnerReference: backup
  plugins:
  - enabled: true
    isWALArchiver: true
    name: barman-cloud.cloudnative-pg.io
    parameters:
      barmanObjectName: my-object-store
```

### Relevant log output

```shell

```

### Code of Conduct

- [x] I agree to follow this project's Code of Conduct

Hints from issue comments:
@dosubot: <!-- Answer -->
This is a known limitation in CloudNativePG: when both VolumeSnapshot and WAL archiving via plugin (like barman-cloud) are enabled, the operator may still fall back to using pg_basebackup for replica join, even if a valid snapshot and WAL archive are available. This fallback is documented and is due to incomplete integration between VolumeSnapshot restore and plugin-based WAL catchup in the current operator logic. The codebase is structured to support this scenario in the future, but as of now, advanced coordination to use both methods together for replica join is not fully implemented, which matches what you're seeing in 1.27. This limitation is expected to be addressed in future releases with improved support for online backups and PVC cloning during scale-up. For now, there is no direct workaround to force the operator to use VolumeSnapshot plus WAL replay via plugin for replica join—pg_basebackup will be used if the logic cannot fully coordinate both restore paths. More details are in the [official documentation](https://cloudnative-pg.io/documentation/1.27/recovery/).


<!-- Dosu Comment Footer -->
*To reply, just mention [@dosu](https://go.dosu.dev/dosubot).*

---
<sup>How did I do? [Good](https://app.dosu.dev/response-feedback/9330db38-63a0-4e09-ae1f-db2cf2a9e0f3?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/9330db38-63a0-4e09-ae1f-db2cf2a9e0f3?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/9330db38-63a0-4e09-ae1f-db2cf2a9e0f3?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/9330db38-63a0-4e09-ae1f-db2cf2a9e0f3?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/9330db38-63a0-4e09-ae1f-db2cf2a9e0f3?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/9330db38-63a0-4e09-ae1f-db2cf2a9e0f3?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/9330db38-63a0-4e09-ae1f-db2cf2a9e0f3?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/89b818f3-8b5e-449b-bfb3-64a9360d9665/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/cloudnative-pg/cloudnative-pg/issues/8507)

Solution patch:
diff --git a/pkg/reconciler/persistentvolumeclaim/storagesource.go b/pkg/reconciler/persistentvolumeclaim/storagesource.go
index 8f808f91..d3706f0a 100644
--- a/pkg/reconciler/persistentvolumeclaim/storagesource.go
+++ b/pkg/reconciler/persistentvolumeclaim/storagesource.go
@@ -85,8 +85,11 @@ func GetCandidateStorageSourceForReplica(
 	//    the cluster itself. Other backups are fine because the required
 	//    WALs have been archived in the cluster object store.
 
-	// Unless WAL archiving is active, we can't recover a replica from a backup
-	if cluster.Spec.Backup == nil || cluster.Spec.Backup.BarmanObjectStore == nil {
+	// Unless WAL archiving is active (via BarmanObjectStore or a WAL-archiver plugin),
+	// we can't recover a replica from a backup
+	walArchivingActive := (cluster.Spec.Backup != nil && cluster.Spec.Backup.BarmanObjectStore != nil) ||
+		cluster.GetEnabledWALArchivePluginName() != ""
+	if !walArchivingActive {
 		return nil
 	}
 



Test patch:
diff --git a/pkg/reconciler/persistentvolumeclaim/storagesource_test.go b/pkg/reconciler/persistentvolumeclaim/storagesource_test.go
index 6e33ce34..5044a0c9 100644
--- a/pkg/reconciler/persistentvolumeclaim/storagesource_test.go
+++ b/pkg/reconciler/persistentvolumeclaim/storagesource_test.go
@@ -96,6 +96,20 @@ var _ = Describe("Storage source", func() {
 		},
 	}
 
+	clusterWithPluginOnly := &apiv1.Cluster{
+		Spec: apiv1.ClusterSpec{
+			StorageConfiguration: apiv1.StorageConfiguration{},
+			WalStorage:           &apiv1.StorageConfiguration{},
+			Backup:               nil,
+			Plugins: []apiv1.PluginConfiguration{
+				{
+					Name:          "test-wal-archiver",
+					IsWALArchiver: ptr.To(true),
+				},
+			},
+		},
+	}
+
 	backupList := apiv1.BackupList{
 		Items: []apiv1.Backup{
 			{
@@ -193,6 +207,17 @@ var _ = Describe("Storage source", func() {
 			Expect(source).ToNot(BeNil())
 			Expect(source.Name).To(Equal("completed-backup"))
 		})
+
+		It("should return the backup as storage source when WAL archiving is via plugin only", func(ctx context.Context) {
+			source, err := NewPgDataCalculator().GetSource(GetCandidateStorageSourceForReplica(
+				ctx,
+				clusterWithPluginOnly,
+				backupList,
+			))
+			Expect(err).ToNot(HaveOccurred())
+			Expect(source).ToNot(BeNil())
+			Expect(source.Name).To(Equal("completed-backup"))
+		})
 	})
 
 	When("there's no WAL archiving", func() {
diff --git a/tests/e2e/volume_snapshot_test.go b/tests/e2e/volume_snapshot_test.go
index bfac1159..7ee4f33f 100644
--- a/tests/e2e/volume_snapshot_test.go
+++ b/tests/e2e/volume_snapshot_test.go
@@ -34,6 +34,7 @@ import (
 	k8client "sigs.k8s.io/controller-runtime/pkg/client"
 
 	apiv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
+	"github.com/cloudnative-pg/cloudnative-pg/pkg/specs"
 	"github.com/cloudnative-pg/cloudnative-pg/pkg/utils"
 	"github.com/cloudnative-pg/cloudnative-pg/tests"
 	"github.com/cloudnative-pg/cloudnative-pg/tests/utils/backups"
@@ -846,6 +847,18 @@ var _ = Describe("Verify Volume Snapshot",
 					AssertClusterIsReady(namespace, clusterToSnapshotName, testTimeouts[timeouts.ClusterIsReady], env)
 				})
 
+				By("checking the new replicas have been created using the snapshot", func() {
+					pvcList, err := storage.GetPVCList(env.Ctx, env.Client, namespace)
+					Expect(err).ToNot(HaveOccurred())
+					for _, pvc := range pvcList.Items {
+						if pvc.Labels[utils.ClusterInstanceRoleLabelName] == specs.ClusterRoleLabelReplica &&
+							pvc.Labels[utils.ClusterLabelName] == clusterToSnapshotName {
+							Expect(pvc.Spec.DataSource.Kind).To(Equal(apiv1.VolumeSnapshotKind))
+							Expect(pvc.Spec.DataSourceRef.Kind).To(Equal(apiv1.VolumeSnapshotKind))
+						}
+					}
+				})
+
 				// we need to verify the streaming replica continue works
 				By("verifying the correct data exists in the new pod of the scaled cluster", func() {
 					podList, err := clusterutils.GetReplicas(env.Ctx, env.Client, namespace,