Repository: cloudnative-pg/cloudnative-pg

Issue:
[Bug]: failoverQuorum requires more than 2 instances
### 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

schneider@ardentperf.com

### Version

1.27 (latest patch)

### What version of Kubernetes are you using?

1.31

### What is your Kubernetes environment?

Self-managed: kind (evaluation)

### How did you install the operator?

YAML manifest

### What happened?

```
The Cluster "pg-eu" is invalid: spec.instances: Invalid value: 2: failoverQuorum requires more than 2 instances.
```

just now got this error, while trying to test if quorum failover prevents data loss with preferred dataDurability and 2 instances. we probably don't want that check. if we have `ANY 1` then we need both replicas available to safely failover (N=2 means three instances).  if we have ANY 2 (or first 2) then we can failover with a single replica. 

this design should work great even with two instances. if the second instance isn't yet added back in then we want to block failover. once the second instance is in synchronous_standby_names then we can failover

### Cluster resource

```shell
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pg-eu
  annotations:
    alpha.cnpg.io/failoverQuorum: "true"
spec:
  instances: 2
  imageName: ghcr.io/cloudnative-pg/postgresql:17-bookworm
...
```

### Relevant log output

```shell
The Cluster "pg-eu" is invalid: spec.instances: Invalid value: 2: failoverQuorum requires more than 2 instances.
```

### Code of Conduct

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

Hints from issue comments:
@dosubot: <!-- Answer -->
The validation requiring more than 2 instances for `failoverQuorum` in CloudNativePG is intentional and not a bug. This restriction is a safeguard to prevent unsafe failover scenarios and potential data loss in small clusters, especially when using `dataDurability: preferred` with only 2 nodes. The underlying logic is based on distributed systems theory: with just 2 instances, it's not possible to guarantee safe automatic promotion without risking split-brain or data loss if one node is unavailable. The quorum check (R + W > N) is designed to ensure that failover only happens when it's provably safe, which is generally only possible with 3 or more nodes in the cluster [[docs](https://cloudnative-pg.io/documentation/1.27/failover/)].

CloudNativePG maintainers and documentation recommend deploying clusters with at least 3 nodes for synchronous replication and failover quorum. Two-node clusters are inherently risky for automatic failover and are not fully supported for this feature [[discussion](https://github.com/cloudnative-pg/cloudnative-pg/issues/7481)][[discussion](https://github.com/cloudnative-pg/cloudnative-pg/discussions/7462)]. There are no official workarounds for enabling failoverQuorum with only 2 instances, and the restriction is enforced in the operator's validation logic [[PR #8589](https://github.com/cloudnative-pg/cloudnative-pg/pull/8589)].

If you need high availability with strong data durability guarantees, the best practice is to use at least 3 instances. For more details and design rationale, see the [failover quorum documentation](https://cloudnative-pg.io/documentation/1.27/failover/) and related [design discussions](https://github.com/cloudnative-pg/cloudnative-pg/issues/7481).


<!-- 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/60ddf090-509d-4d6f-8415-07fa99f37bf3?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/60ddf090-509d-4d6f-8415-07fa99f37bf3?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/60ddf090-509d-4d6f-8415-07fa99f37bf3?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/60ddf090-509d-4d6f-8415-07fa99f37bf3?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/60ddf090-509d-4d6f-8415-07fa99f37bf3?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/60ddf090-509d-4d6f-8415-07fa99f37bf3?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/60ddf090-509d-4d6f-8415-07fa99f37bf3?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/8679)

Solution patch:
diff --git a/internal/webhook/v1/cluster_webhook.go b/internal/webhook/v1/cluster_webhook.go
index 0e7a2b64..14a60d6f 100644
--- a/internal/webhook/v1/cluster_webhook.go
+++ b/internal/webhook/v1/cluster_webhook.go
@@ -1057,15 +1057,6 @@ func (v *ClusterCustomValidator) validateFailoverQuorum(r *apiv1.Cluster) field.
 		result = append(result, err)
 	}
 
-	if r.Spec.Instances <= 2 {
-		err := field.Invalid(
-			field.NewPath("spec", "instances"),
-			r.Spec.Instances,
-			"failoverQuorum requires more than 2 instances.",
-		)
-		result = append(result, err)
-	}
-
 	return result
 }
 



Test patch:
diff --git a/internal/webhook/v1/cluster_webhook_test.go b/internal/webhook/v1/cluster_webhook_test.go
index 8293e4cd..e8dbd74f 100644
--- a/internal/webhook/v1/cluster_webhook_test.go
+++ b/internal/webhook/v1/cluster_webhook_test.go
@@ -5841,10 +5841,10 @@ var _ = Describe("failoverQuorum validation", func() {
 		v = &ClusterCustomValidator{}
 	})
 
-	It("requires at least three instances", func() {
+	It("accepts two or more instances", func() {
 		cluster := &apiv1.Cluster{
 			Spec: apiv1.ClusterSpec{
-				Instances: 3,
+				Instances: 2,
 				PostgresConfiguration: apiv1.PostgresConfiguration{
 					Synchronous: &apiv1.SynchronousReplicaConfiguration{
 						Number:         1,
@@ -5857,9 +5857,9 @@ var _ = Describe("failoverQuorum validation", func() {
 		errList := v.validateFailoverQuorum(cluster)
 		Expect(errList).To(BeEmpty())
 
-		cluster.Spec.Instances = 2
+		cluster.Spec.Instances = 3
 		errList = v.validateFailoverQuorum(cluster)
-		Expect(errList).To(HaveLen(1))
+		Expect(errList).To(BeEmpty())
 	})
 
 	It("check if the number of external synchronous replicas is coherent", func() {