Repository: cloudnative-pg/cloudnative-pg

Issue:
[Feature]: Isolation checker for the liveness probe of a primary
### Is there an existing issue already for this feature request/idea?

- [x] I have searched for an existing issue, and could not find anything. I believe this is a new feature request to be evaluated.

### What problem is this feature going to solve? Why should it be added?

Version 1.26 introduced opt-in experimental support for this feature in PR #7466. This feature can be explicitly activated via an annotation.

### Describe the solution you'd like

We need to define a clear API in the liveness probe to enable this feature and define the relevant timeouts.

Feedback coming from early adopters, preferably in this ticket, will shape the final result.

### Describe alternatives you've considered

Alternatives have been discussed in:

- #7465 
- #7462 


### Additional context

In the referenced tickets

### Backport?

No

### Are you willing to actively contribute to this feature?

Yes

### Code of Conduct

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

Hints from issue comments:
@gbartolini: I am trying to propose an interface for this:


```yaml
spec:
  probes:
    liveness:
      isolationCheck:
        enabled: true
        requestTimeout: "1000"
        connectionTimeout: "1000"
```

By default, CloudNativePG automatically configures the liveness probe parameters based on the value of `.spec.livenessProbeTimeout`, specifically:

- `failureThreshold`: FAILURE_THRESHOLD
- `periodSeconds`: 10
- `successThreshold`: 1
- `timeoutSeconds`: 5

Given that `.spec.livenessProbeTimeout` is by default set to `30`, the liveness probe will report a failure if it detects three consecutive probe failures, with a 10-second interval between each check.

We would first need to discuss whether this setting is appropriate when the isolation check is enabled.

Second, I would like to discuss whether isolationCheck is enabled by default starting from 1.27.

Please don't hesitate to propose a different interface and give feedback on the above.

Finally, we need to remove the support for the annotation and update the docs.@gbartolini: I spoke with @fcanovai today, and we agreed that keeping the default value at 30 seconds for detecting when the primary is isolated is appropriate, as we want to identify this issue as quickly as possible.

The only remaining decision is whether to set `probes.liveness.isolationCheck.enabled` to true by default.

Do you have any comments on this? Shall we start with "disabled" (as the patch does) and re-evaluate it when we have more use cases and feedback?@mnencia: Ultimately, I believe this feature should be enabled by default to promote best practices and improve cluster safety. However, I’m not yet confident that it has been tested broadly enough. As a next step, I suggest enabling it by default now and actively encouraging community members to test it in a variety of environments. If any significant issues or valid concerns arise, we can revisit the default setting and disable it before the final release of 1.27.0.

Solution patch:
diff --git a/.wordlist-en-custom.txt b/.wordlist-en-custom.txt
index db131a22..1ca2965c 100644
--- a/.wordlist-en-custom.txt
+++ b/.wordlist-en-custom.txt
@@ -208,6 +208,7 @@ InfoSec
 Innocenti
 InstanceID
 InstanceReportedState
+IsolationCheckConfiguration
 Istio
 Istio's
 JSON
@@ -698,6 +699,7 @@ conn
 connectionLimit
 connectionParameters
 connectionString
+connectionTimeout
 conninfo
 containerImage
 containerPort
@@ -910,6 +912,7 @@ ips
 isPrimary
 isTemplate
 isWALArchiver
+isolationCheck
 issuecomment
 italy
 jdbc
@@ -1193,6 +1196,7 @@ repmgr
 reportNonRedacted
 reportRedacted
 req
+requestTimeout
 requiredDuringSchedulingIgnoredDuringExecution
 resizeInUseVolumes
 resizingPVC
diff --git a/api/v1/cluster_defaults.go b/api/v1/cluster_defaults.go
index d026d70d..5a5607d8 100644
--- a/api/v1/cluster_defaults.go
+++ b/api/v1/cluster_defaults.go
@@ -20,6 +20,9 @@ SPDX-License-Identifier: Apache-2.0
 package v1
 
 import (
+	"encoding/json"
+	"fmt"
+
 	"github.com/cloudnative-pg/machinery/pkg/log"
 	"github.com/cloudnative-pg/machinery/pkg/stringset"
 	"k8s.io/utils/ptr"
@@ -138,6 +141,7 @@ func (r *Cluster) setDefaults(preserveUserSettings bool) {
 	}
 
 	r.setDefaultPlugins(configuration.Current)
+	r.setProbes()
 }
 
 func (r *Cluster) setDefaultPlugins(config *configuration.Data) {
@@ -269,3 +273,86 @@ func (r *Cluster) defaultPgBaseBackup() {
 		r.Spec.Bootstrap.PgBaseBackup.Owner = r.Spec.Bootstrap.PgBaseBackup.Database
 	}
 }
+
+const (
+	// defaultRequestTimeout is the default value of the request timeout
+	defaultRequestTimeout = 1000
+
+	// defaultConnectionTimeout is the default value of the connection timeout
+	defaultConnectionTimeout = 1000
+)
+
+func (r *Cluster) setProbes() {
+	if r.Spec.Probes == nil {
+		r.Spec.Probes = &ProbesConfiguration{}
+	}
+
+	if r.Spec.Probes.Liveness == nil {
+		r.Spec.Probes.Liveness = &LivenessProbe{}
+	}
+
+	// we don't override the isolation check if it is already set
+	if r.Spec.Probes.Liveness.IsolationCheck != nil {
+		return
+	}
+
+	// STEP 1: check if the alpha annotation is present, in that case convert it to spec
+	r.tryConvertAlphaLivenessPinger()
+
+	if r.Spec.Probes.Liveness.IsolationCheck != nil {
+		return
+	}
+
+	// STEP 2: set defaults.
+	r.Spec.Probes.Liveness.IsolationCheck = &IsolationCheckConfiguration{
+		Enabled:           ptr.To(true),
+		RequestTimeout:    defaultRequestTimeout,
+		ConnectionTimeout: defaultConnectionTimeout,
+	}
+}
+
+func (r *Cluster) tryConvertAlphaLivenessPinger() {
+	if _, ok := r.Annotations[utils.LivenessPingerAnnotationName]; !ok {
+		return
+	}
+	v, err := NewLivenessPingerConfigFromAnnotations(r.Annotations)
+	if err != nil || v == nil {
+		// the error will be raised by the validation webhook
+		return
+	}
+
+	r.Spec.Probes.Liveness.IsolationCheck = &IsolationCheckConfiguration{
+		Enabled:           v.Enabled,
+		RequestTimeout:    v.RequestTimeout,
+		ConnectionTimeout: v.ConnectionTimeout,
+	}
+}
+
+// NewLivenessPingerConfigFromAnnotations creates a new pinger configuration from the annotations
+// in the cluster definition
+func NewLivenessPingerConfigFromAnnotations(
+	annotations map[string]string,
+) (*IsolationCheckConfiguration, error) {
+	v, ok := annotations[utils.LivenessPingerAnnotationName]
+	if !ok {
+		return nil, nil
+	}
+
+	var cfg IsolationCheckConfiguration
+	if err := json.Unmarshal([]byte(v), &cfg); err != nil {
+		return nil, fmt.Errorf("while unmarshalling pinger config: %w", err)
+	}
+
+	if cfg.Enabled == nil {
+		return nil, fmt.Errorf("pinger config is missing the enabled field")
+	}
+
+	if cfg.RequestTimeout == 0 {
+		cfg.RequestTimeout = defaultRequestTimeout
+	}
+	if cfg.ConnectionTimeout == 0 {
+		cfg.ConnectionTimeout = defaultConnectionTimeout
+	}
+
+	return &cfg, nil
+}
diff --git a/api/v1/cluster_types.go b/api/v1/cluster_types.go
index 66c16160..b7a781b5 100644
--- a/api/v1/cluster_types.go
+++ b/api/v1/cluster_types.go
@@ -490,7 +490,7 @@ type ProbesConfiguration struct {
 	Startup *ProbeWithStrategy `json:"startup,omitempty"`
 
 	// The liveness probe configuration
-	Liveness *Probe `json:"liveness,omitempty"`
+	Liveness *LivenessProbe `json:"liveness,omitempty"`
 
 	// The readiness probe configuration
 	Readiness *ProbeWithStrategy `json:"readiness,omitempty"`
@@ -568,6 +568,39 @@ type Probe struct {
 	TerminationGracePeriodSeconds *int64 `json:"terminationGracePeriodSeconds,omitempty"`
 }
 
+// LivenessProbe is the configuration of the liveness probe
+type LivenessProbe struct {
+	// Probe is the standard probe configuration
+	Probe `json:",inline"`
+
+	// Configure the feature that extends the liveness probe for a primary
+	// instance. In addition to the basic checks, this verifies whether the
+	// primary is isolated from the Kubernetes API server and from its
+	// replicas, ensuring that it can be safely shut down if network
+	// partition or API unavailability is detected. Enabled by default.
+	// +optional
+	IsolationCheck *IsolationCheckConfiguration `json:"isolationCheck,omitempty"`
+}
+
+// IsolationCheckConfiguration contains the configuration for the isolation check
+// functionality in the liveness probe
+type IsolationCheckConfiguration struct {
+	// Whether primary isolation checking is enabled for the liveness probe
+	// +optional
+	// +kubebuilder:default:=true
+	Enabled *bool `json:"enabled,omitempty"`
+
+	// Timeout in milliseconds for requests during the primary isolation check
+	// +optional
+	// +kubebuilder:default:=1000
+	RequestTimeout int `json:"requestTimeout,omitempty"`
+
+	// Timeout in milliseconds for connections during the primary isolation check
+	// +optional
+	// +kubebuilder:default:=1000
+	ConnectionTimeout int `json:"connectionTimeout,omitempty"`
+}
+
 const (
 	// PhaseSwitchover when a cluster is changing the primary node
 	PhaseSwitchover = "Switchover in progress"
diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go
index c8b3917c..042fe567 100644
--- a/api/v1/zz_generated.deepcopy.go
+++ b/api/v1/zz_generated.deepcopy.go
@@ -1525,6 +1525,26 @@ func (in *InstanceReportedState) DeepCopy() *InstanceReportedState {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *IsolationCheckConfiguration) DeepCopyInto(out *IsolationCheckConfiguration) {
+	*out = *in
+	if in.Enabled != nil {
+		in, out := &in.Enabled, &out.Enabled
+		*out = new(bool)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IsolationCheckConfiguration.
+func (in *IsolationCheckConfiguration) DeepCopy() *IsolationCheckConfiguration {
+	if in == nil {
+		return nil
+	}
+	out := new(IsolationCheckConfiguration)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *LDAPBindAsAuth) DeepCopyInto(out *LDAPBindAsAuth) {
 	*out = *in
@@ -1585,6 +1605,27 @@ func (in *LDAPConfig) DeepCopy() *LDAPConfig {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LivenessProbe) DeepCopyInto(out *LivenessProbe) {
+	*out = *in
+	in.Probe.DeepCopyInto(&out.Probe)
+	if in.IsolationCheck != nil {
+		in, out := &in.IsolationCheck, &out.IsolationCheck
+		*out = new(IsolationCheckConfiguration)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LivenessProbe.
+func (in *LivenessProbe) DeepCopy() *LivenessProbe {
+	if in == nil {
+		return nil
+	}
+	out := new(LivenessProbe)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ManagedConfiguration) DeepCopyInto(out *ManagedConfiguration) {
 	*out = *in
@@ -2322,7 +2363,7 @@ func (in *ProbesConfiguration) DeepCopyInto(out *ProbesConfiguration) {
 	}
 	if in.Liveness != nil {
 		in, out := &in.Liveness, &out.Liveness
-		*out = new(Probe)
+		*out = new(LivenessProbe)
 		(*in).DeepCopyInto(*out)
 	}
 	if in.Readiness != nil {
diff --git a/config/crd/bases/postgresql.cnpg.io_clusters.yaml b/config/crd/bases/postgresql.cnpg.io_clusters.yaml
index d2236b83..4be1f4b2 100644
--- a/config/crd/bases/postgresql.cnpg.io_clusters.yaml
+++ b/config/crd/bases/postgresql.cnpg.io_clusters.yaml
@@ -4249,6 +4249,30 @@ spec:
                           More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes
                         format: int32
                         type: integer
+                      isolationCheck:
+                        description: |-
+                          Configure the feature that extends the liveness probe for a primary
+                          instance. In addition to the basic checks, this verifies whether the
+                          primary is isolated from the Kubernetes API server and from its
+                          replicas, ensuring that it can be safely shut down if network
+                          partition or API unavailability is detected. Enabled by default.
+                        properties:
+                          connectionTimeout:
+                            default: 1000
+                            description: Timeout in milliseconds for connections during
+                              the primary isolation check
+                            type: integer
+                          enabled:
+                            default: true
+                            description: Whether primary isolation checking is enabled
+                              for the liveness probe
+                            type: boolean
+                          requestTimeout:
+                            default: 1000
+                            description: Timeout in milliseconds for requests during
+                              the primary isolation check
+                            type: integer
+                        type: object
                       periodSeconds:
                         description: |-
                           How often (in seconds) to perform the probe.
diff --git a/docs/src/cloudnative-pg.v1.md b/docs/src/cloudnative-pg.v1.md
index 77744325..86ed453e 100644
--- a/docs/src/cloudnative-pg.v1.md
+++ b/docs/src/cloudnative-pg.v1.md
@@ -3210,6 +3210,45 @@ conflict with the operator's intended functionality or design.</p>
 </tbody>
 </table>
 
+## IsolationCheckConfiguration     {#postgresql-cnpg-io-v1-IsolationCheckConfiguration}
+
+
+**Appears in:**
+
+- [LivenessProbe](#postgresql-cnpg-io-v1-LivenessProbe)
+
+
+<p>IsolationCheckConfiguration contains the configuration for the isolation check
+functionality in the liveness probe</p>
+
+
+<table class="table">
+<thead><tr><th width="30%">Field</th><th>Description</th></tr></thead>
+<tbody>
+<tr><td><code>enabled</code><br/>
+<i>bool</i>
+</td>
+<td>
+   <p>Whether primary isolation checking is enabled for the liveness probe</p>
+</td>
+</tr>
+<tr><td><code>requestTimeout</code><br/>
+<i>int</i>
+</td>
+<td>
+   <p>Timeout in milliseconds for requests during the primary isolation check</p>
+</td>
+</tr>
+<tr><td><code>connectionTimeout</code><br/>
+<i>int</i>
+</td>
+<td>
+   <p>Timeout in milliseconds for connections during the primary isolation check</p>
+</td>
+</tr>
+</tbody>
+</table>
+
 ## LDAPBindAsAuth     {#postgresql-cnpg-io-v1-LDAPBindAsAuth}
 
 
@@ -3368,6 +3407,41 @@ the bind+search LDAP authentication process</p>
 
 
 
+## LivenessProbe     {#postgresql-cnpg-io-v1-LivenessProbe}
+
+
+**Appears in:**
+
+- [ProbesConfiguration](#postgresql-cnpg-io-v1-ProbesConfiguration)
+
+
+<p>LivenessProbe is the configuration of the liveness probe</p>
+
+
+<table class="table">
+<thead><tr><th width="30%">Field</th><th>Description</th></tr></thead>
+<tbody>
+<tr><td><code>Probe</code><br/>
+<a href="#postgresql-cnpg-io-v1-Probe"><i>Probe</i></a>
+</td>
+<td>(Members of <code>Probe</code> are embedded into this type.)
+   <p>Probe is the standard probe configuration</p>
+</td>
+</tr>
+<tr><td><code>isolationCheck</code><br/>
+<a href="#postgresql-cnpg-io-v1-IsolationCheckConfiguration"><i>IsolationCheckConfiguration</i></a>
+</td>
+<td>
+   <p>Configure the feature that extends the liveness probe for a primary
+instance. In addition to the basic checks, this verifies whether the
+primary is isolated from the Kubernetes API server and from its
+replicas, ensuring that it can be safely shut down if network
+partition or API unavailability is detected. Enabled by default.</p>
+</td>
+</tr>
+</tbody>
+</table>
+
 ## ManagedConfiguration     {#postgresql-cnpg-io-v1-ManagedConfiguration}
 
 
@@ -4412,9 +4486,9 @@ the primary server of the cluster as part of rolling updates</p>
 
 **Appears in:**
 
-- [ProbeWithStrategy](#postgresql-cnpg-io-v1-ProbeWithStrategy)
+- [LivenessProbe](#postgresql-cnpg-io-v1-LivenessProbe)
 
-- [ProbesConfiguration](#postgresql-cnpg-io-v1-ProbesConfiguration)
+- [ProbeWithStrategy](#postgresql-cnpg-io-v1-ProbeWithStrategy)
 
 
 <p>Probe describes a health check to be performed against a container to determine whether it is
@@ -4560,7 +4634,7 @@ to be injected in the PostgreSQL Pods</p>
 </td>
 </tr>
 <tr><td><code>liveness</code> <B>[Required]</B><br/>
-<a href="#postgresql-cnpg-io-v1-Probe"><i>Probe</i></a>
+<a href="#postgresql-cnpg-io-v1-LivenessProbe"><i>LivenessProbe</i></a>
 </td>
 <td>
    <p>The liveness probe configuration</p>
diff --git a/docs/src/installation_upgrade.md b/docs/src/installation_upgrade.md
index dfedef1b..6ce97887 100644
--- a/docs/src/installation_upgrade.md
+++ b/docs/src/installation_upgrade.md
@@ -254,6 +254,31 @@ removed before installing the new one. This won't affect user data but
 only the operator itself.
 
 
+<!--
+### Upgrading to 1.27.0 or 1.26.2
+
+!!! Important
+    We strongly recommend that all CloudNativePG users upgrade to version
+    1.27.0, or at least to the latest stable version of your current minor release
+    (e.g., 1.26.x).
+
+Version 1.27 introduces a change in the default behavior of the
+[liveness probe](instance_manager.md#liveness-probe): it now enforces the
+[shutdown of an isolated primary](instance_manager.md#primary-isolation)
+within the `livenessProbeTimeout` (30 seconds).
+
+If this behavior is not suitable for your environment, you can disable the
+*isolation check* in the liveness probe with the following configuration:
+
+```yaml
+spec:
+  probes:
+    liveness:
+      isolationCheck:
+        enabled: false
+```
+-->
+
 ### Upgrading to 1.26.0 or 1.25.2
 
 !!! Important
diff --git a/docs/src/instance_manager.md b/docs/src/instance_manager.md
index a914637b..e7ac50f1 100644
--- a/docs/src/instance_manager.md
+++ b/docs/src/instance_manager.md
@@ -188,9 +188,9 @@ spec:
       failureThreshold: 10
 ```
 
-### Primary Isolation (alpha)
+### Primary Isolation
 
-CloudNativePG 1.26 introduces an opt-in experimental behavior for the liveness
+CloudNativePG 1.27 introduces an additional behavior for the liveness
 probe of a PostgreSQL primary, which will report a failure if **both** of the
 following conditions are met:
 
@@ -199,35 +199,34 @@ following conditions are met:
 
 The effect of this behavior is to consider an isolated primary to be not alive and subsequently **shut it down** when the liveness probe fails.
 
-It is **disabled by default** and can be enabled by adding the following
-annotation to the `Cluster` resource:
+It is **enabled by default** and can be disabled by adding the following:
 
 ```yaml
-metadata:
-  annotations:
-    alpha.cnpg.io/livenessPinger: '{"enabled": true}'
+spec:
+  probes:
+    liveness:
+      isolationCheck:
+        enabled: false
 ```
 
-!!! Warning
-    This feature is experimental and will be introduced in a future CloudNativePG
-    release with a new API. If you decide to use it now, note that the API **will
-    change**.
-
 !!! Important
-    If you plan to enable this experimental feature, be aware that the default
-    liveness probe settings—automatically derived from `livenessProbeTimeout`—might
+    Be aware that the default liveness probe settings—automatically derived from `livenessProbeTimeout`—might
     be aggressive (30 seconds). As such, we recommend explicitly setting the
     liveness probe configuration to suit your environment.
 
-The annotation also accepts two optional network settings: `requestTimeout`
-and `connectionTimeout`, both defaulting to `500` (in milliseconds).
+The spec also accepts two optional network settings: `requestTimeout`
+and `connectionTimeout`, both defaulting to `1000` (in milliseconds).
 In cloud environments, you may need to increase these values.
 For example:
 
 ```yaml
-metadata:
-  annotations:
-    alpha.cnpg.io/livenessPinger: '{"enabled": true,"requestTimeout":1000,"connectionTimeout":1000}'
+spec:
+  probes:
+    liveness:
+      isolationCheck:
+        enabled: true
+        requestTimeout: "2000"
+        connectionTimeout: "2000"
 ```
 
 ## Readiness Probe
diff --git a/internal/webhook/v1/cluster_webhook.go b/internal/webhook/v1/cluster_webhook.go
index 70914579..ecf19a6b 100644
--- a/internal/webhook/v1/cluster_webhook.go
+++ b/internal/webhook/v1/cluster_webhook.go
@@ -49,7 +49,6 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
 	apiv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
-	"github.com/cloudnative-pg/cloudnative-pg/pkg/management/postgres/webserver/probes"
 	"github.com/cloudnative-pg/cloudnative-pg/pkg/postgres"
 	"github.com/cloudnative-pg/cloudnative-pg/pkg/specs"
 	"github.com/cloudnative-pg/cloudnative-pg/pkg/utils"
@@ -2508,7 +2507,7 @@ func (v *ClusterCustomValidator) validateLivenessPingerProbe(r *apiv1.Cluster) f
 		return nil
 	}
 
-	_, err := probes.NewLivenessPingerConfigFromAnnotations(context.Background(), r.Annotations)
+	_, err := apiv1.NewLivenessPingerConfigFromAnnotations(r.Annotations)
 	if err != nil {
 		return field.ErrorList{
 			field.Invalid(
diff --git a/pkg/management/postgres/webserver/probes/liveness.go b/pkg/management/postgres/webserver/probes/liveness.go
index bbb07601..04e661e2 100644
--- a/pkg/management/postgres/webserver/probes/liveness.go
+++ b/pkg/management/postgres/webserver/probes/liveness.go
@@ -21,6 +21,7 @@ package probes
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net/http"
 	"time"
@@ -102,7 +103,8 @@ func (e *livenessExecutor) IsHealthy(
 		// quickly as possible.
 		if err := evaluateLivenessPinger(ctx, e.lastestKnownCluster.DeepCopy()); err != nil {
 			contextLogger.Warning(
-				"Instance connectivity error - liveness probe failing but API server is reachable",
+				"Instance connectivity error - liveness probe succeeding because "+
+					"the API server is reachable",
 				"err",
 				err.Error(),
 			)
@@ -151,11 +153,20 @@ func evaluateLivenessPinger(
 ) error {
 	contextLogger := log.FromContext(ctx)
 
-	cfg, err := NewLivenessPingerConfigFromAnnotations(ctx, cluster.Annotations)
-	if err != nil {
-		return err
+	var cfg *apiv1.IsolationCheckConfiguration
+	if cluster.Spec.Probes != nil && cluster.Spec.Probes.Liveness != nil {
+		cfg = cluster.Spec.Probes.Liveness.IsolationCheck
+	}
+	if cfg == nil {
+		return nil
 	}
-	if !cfg.isEnabled() {
+
+	// This should never happen given that we set a default value. Fail fast.
+	if cfg.Enabled == nil {
+		return errors.New("enabled field is not set in the liveness isolation check configuration")
+	}
+
+	if !*cfg.Enabled {
 		contextLogger.Debug("pinger config not enabled, skipping")
 		return nil
 	}
@@ -165,7 +176,7 @@ func evaluateLivenessPinger(
 		return nil
 	}
 
-	checker, err := buildInstanceReachabilityChecker(*cfg)
+	checker, err := buildInstanceReachabilityChecker(cfg)
 	if err != nil {
 		return fmt.Errorf("failed to build instance reachability checker: %w", err)
 	}
diff --git a/pkg/management/postgres/webserver/probes/pinger.go b/pkg/management/postgres/webserver/probes/pinger.go
index a6647032..67fc2aac 100644
--- a/pkg/management/postgres/webserver/probes/pinger.go
+++ b/pkg/management/postgres/webserver/probes/pinger.go
@@ -20,9 +20,8 @@ SPDX-License-Identifier: Apache-2.0
 package probes
 
 import (
-	"context"
 	"crypto/x509"
-	"encoding/json"
+	"errors"
 	"fmt"
 	"net"
 	"net/http"
@@ -30,89 +29,29 @@ import (
 	"os"
 	"time"
 
-	"github.com/cloudnative-pg/machinery/pkg/log"
-	"k8s.io/utils/ptr"
-
 	apiv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
 	"github.com/cloudnative-pg/cloudnative-pg/pkg/certs"
 	cnpgUrl "github.com/cloudnative-pg/cloudnative-pg/pkg/management/url"
 	postgresSpec "github.com/cloudnative-pg/cloudnative-pg/pkg/postgres"
-	"github.com/cloudnative-pg/cloudnative-pg/pkg/utils"
 )
 
-// LivenessPingerCfg if the configuration of the instance
-// reachability checker
-type LivenessPingerCfg struct {
-	Enabled           *bool `json:"enabled"`
-	RequestTimeout    int   `json:"requestTimeout,omitempty"`
-	ConnectionTimeout int   `json:"connectionTimeout,omitempty"`
-}
-
-func (probe *LivenessPingerCfg) isEnabled() bool {
-	if probe == nil || probe.Enabled == nil {
-		return false
-	}
-
-	return *probe.Enabled
-}
-
-// NewLivenessPingerConfigFromAnnotations creates a new pinger configuration from the annotations
-// in the cluster definition
-func NewLivenessPingerConfigFromAnnotations(
-	ctx context.Context,
-	annotations map[string]string,
-) (*LivenessPingerCfg, error) {
-	const (
-		// defaultRequestTimeout is the default value of the request timeout
-		defaultRequestTimeout = 500
-
-		// defaultConnectionTimeout is the default value of the connection timeout
-		defaultConnectionTimeout = 1000
-	)
-
-	contextLogger := log.FromContext(ctx)
-
-	v, ok := annotations[utils.LivenessPingerAnnotationName]
-	if !ok {
-		contextLogger.Debug("pinger config not found in the cluster annotations")
-		return &LivenessPingerCfg{
-			Enabled: ptr.To(false),
-		}, nil
-	}
-
-	var cfg LivenessPingerCfg
-	if err := json.Unmarshal([]byte(v), &cfg); err != nil {
-		contextLogger.Error(err, "failed to unmarshal pinger config")
-		return nil, fmt.Errorf("while unmarshalling pinger config: %w", err)
-	}
-
-	if cfg.Enabled == nil {
-		return nil, fmt.Errorf("pinger config is missing the enabled field")
-	}
-
-	if cfg.RequestTimeout == 0 {
-		cfg.RequestTimeout = defaultRequestTimeout
-	}
-	if cfg.ConnectionTimeout == 0 {
-		cfg.ConnectionTimeout = defaultConnectionTimeout
-	}
-
-	return &cfg, nil
-}
-
 // pinger can check if a certain instance is reachable by using
 // the failsafe REST endpoint
 type pinger struct {
 	dialer *net.Dialer
 	client *http.Client
 
-	config LivenessPingerCfg
+	config *apiv1.IsolationCheckConfiguration
 }
 
 // buildInstanceReachabilityChecker creates a new instance reachability checker by loading
 // the server CA certificate from the same location that will be used by PostgreSQL.
 // In this case, we avoid using the API Server as it may be unreliable.
-func buildInstanceReachabilityChecker(cfg LivenessPingerCfg) (*pinger, error) {
+func buildInstanceReachabilityChecker(cfg *apiv1.IsolationCheckConfiguration) (*pinger, error) {
+	if cfg == nil {
+		return nil, errors.New("isolation check configuration is nil")
+	}
+
 	certificateLocation := postgresSpec.ServerCACertificateLocation
 	caCertificate, err := os.ReadFile(certificateLocation) //nolint:gosec
 	if err != nil {
@@ -155,6 +94,7 @@ func (e *pinger) ping(host, ip string) error {
 	if res, err = e.client.Get(failsafeURL.String()); err != nil {
 		return &pingError{
 			host:   host,
+			ip:     ip,
 			err:    err,
 			config: e.config,
 		}
@@ -182,7 +122,7 @@ type pingError struct {
 	host string
 	ip   string
 
-	config LivenessPingerCfg
+	config *apiv1.IsolationCheckConfiguration
 
 	err error
 }
diff --git a/pkg/management/postgres/webserver/remote.go b/pkg/management/postgres/webserver/remote.go
index ef76017b..13163c3d 100644
--- a/pkg/management/postgres/webserver/remote.go
+++ b/pkg/management/postgres/webserver/remote.go
@@ -231,7 +231,7 @@ func (ws *remoteWebserverEndpoints) failSafe(w http.ResponseWriter, _ *http.Requ
 	_, _ = fmt.Fprint(w, "OK")
 }
 
-// This is the failsafe probe
+// This is the liveness probe
 func (ws *remoteWebserverEndpoints) isServerHealthy(w http.ResponseWriter, req *http.Request) {
 	ws.livenessChecker.IsHealthy(req.Context(), w)
 }



Test patch:
diff --git a/api/v1/cluster_defaults_test.go b/api/v1/cluster_defaults_test.go
index 4250ef8b..608e9940 100644
--- a/api/v1/cluster_defaults_test.go
+++ b/api/v1/cluster_defaults_test.go
@@ -20,9 +20,11 @@ SPDX-License-Identifier: Apache-2.0
 package v1
 
 import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/utils/ptr"
 
 	"github.com/cloudnative-pg/cloudnative-pg/internal/configuration"
+	"github.com/cloudnative-pg/cloudnative-pg/pkg/utils"
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
@@ -359,3 +361,157 @@ var _ = Describe("default dataDurability", func() {
 		Expect(cluster.Spec.PostgresConfiguration.Synchronous.DataDurability).To(Equal(DataDurabilityLevelPreferred))
 	})
 })
+
+var _ = Describe("NewLivenessPingerConfigFromAnnotations", func() {
+	It("returns a nil configuration when annotation is not present", func() {
+		annotations := map[string]string{}
+
+		config, err := NewLivenessPingerConfigFromAnnotations(annotations)
+
+		Expect(err).ToNot(HaveOccurred())
+		Expect(config).To(BeNil())
+	})
+
+	It("returns an error when annotation contains invalid JSON", func() {
+		annotations := map[string]string{
+			utils.LivenessPingerAnnotationName: "{invalid_json",
+		}
+
+		config, err := NewLivenessPingerConfigFromAnnotations(annotations)
+
+		Expect(err).To(HaveOccurred())
+		Expect(config).To(BeNil())
+	})
+
+	It("applies default values when timeouts are not specified", func() {
+		annotations := map[string]string{
+			utils.LivenessPingerAnnotationName: `{"enabled": true}`,
+		}
+
+		config, err := NewLivenessPingerConfigFromAnnotations(annotations)
+
+		Expect(err).ToNot(HaveOccurred())
+		Expect(config).ToNot(BeNil())
+		Expect(config.Enabled).To(HaveValue(BeTrue()))
+		Expect(config.RequestTimeout).To(Equal(1000))
+		Expect(config.ConnectionTimeout).To(Equal(1000))
+	})
+
+	It("preserves values when all fields are specified", func() {
+		annotations := map[string]string{
+			utils.LivenessPingerAnnotationName: `{"enabled": true, "requestTimeout": 300, "connectionTimeout": 600}`,
+		}
+
+		config, err := NewLivenessPingerConfigFromAnnotations(annotations)
+
+		Expect(err).ToNot(HaveOccurred())
+		Expect(config).ToNot(BeNil())
+		Expect(config.Enabled).To(HaveValue(BeTrue()))
+		Expect(config.RequestTimeout).To(Equal(300))
+		Expect(config.ConnectionTimeout).To(Equal(600))
+	})
+
+	It("correctly sets enabled to false when specified", func() {
+		annotations := map[string]string{
+			utils.LivenessPingerAnnotationName: `{"enabled": false, "requestTimeout": 300, "connectionTimeout": 600}`,
+		}
+
+		config, err := NewLivenessPingerConfigFromAnnotations(annotations)
+
+		Expect(err).ToNot(HaveOccurred())
+		Expect(config).ToNot(BeNil())
+		Expect(config.Enabled).To(HaveValue(BeFalse()))
+		Expect(config.RequestTimeout).To(Equal(300))
+		Expect(config.ConnectionTimeout).To(Equal(600))
+	})
+
+	It("correctly handles zero values for timeouts", func() {
+		annotations := map[string]string{
+			utils.LivenessPingerAnnotationName: `{"enabled": true, "requestTimeout": 0, "connectionTimeout": 0}`,
+		}
+
+		config, err := NewLivenessPingerConfigFromAnnotations(annotations)
+
+		Expect(err).ToNot(HaveOccurred())
+		Expect(config).ToNot(BeNil())
+		Expect(config.RequestTimeout).To(Equal(1000))
+		Expect(config.ConnectionTimeout).To(Equal(1000))
+	})
+})
+
+var _ = Describe("probe defaults", func() {
+	It("should set isolationCheck probe to true by default when no probes are specified", func() {
+		cluster := &Cluster{}
+		cluster.Default()
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck).ToNot(BeNil())
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck.Enabled).To(HaveValue(BeTrue()))
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck.RequestTimeout).To(Equal(1000))
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck.ConnectionTimeout).To(Equal(1000))
+	})
+
+	It("should not override isolationCheck probe if already set", func() {
+		cluster := &Cluster{
+			Spec: ClusterSpec{
+				Probes: &ProbesConfiguration{
+					Liveness: &LivenessProbe{
+						IsolationCheck: &IsolationCheckConfiguration{
+							Enabled:           ptr.To(false),
+							RequestTimeout:    300,
+							ConnectionTimeout: 600,
+						},
+					},
+				},
+			},
+		}
+		cluster.Default()
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck).ToNot(BeNil())
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck.Enabled).To(HaveValue(BeFalse()))
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck.RequestTimeout).To(Equal(300))
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck.ConnectionTimeout).To(Equal(600))
+	})
+
+	It("should set isolationCheck probe when it is not set but liveness probe is present", func() {
+		cluster := &Cluster{
+			Spec: ClusterSpec{
+				Probes: &ProbesConfiguration{
+					Liveness: &LivenessProbe{},
+				},
+			},
+		}
+		cluster.Default()
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck).ToNot(BeNil())
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck.Enabled).To(HaveValue(BeTrue()))
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck.RequestTimeout).To(Equal(1000))
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck.ConnectionTimeout).To(Equal(1000))
+	})
+
+	It("should convert the existing annotations if set to true", func() {
+		cluster := &Cluster{
+			ObjectMeta: metav1.ObjectMeta{
+				Annotations: map[string]string{
+					utils.LivenessPingerAnnotationName: `{"enabled": true, "requestTimeout": 300, "connectionTimeout": 600}`,
+				},
+			},
+		}
+		cluster.Default()
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck).ToNot(BeNil())
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck.Enabled).To(HaveValue(BeTrue()))
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck.RequestTimeout).To(Equal(300))
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck.ConnectionTimeout).To(Equal(600))
+	})
+
+	It("should convert the existing annotations if set to false", func() {
+		cluster := &Cluster{
+			ObjectMeta: metav1.ObjectMeta{
+				Annotations: map[string]string{
+					utils.LivenessPingerAnnotationName: `{"enabled": false, "requestTimeout": 300, "connectionTimeout": 600}`,
+				},
+			},
+		}
+		cluster.Default()
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck).ToNot(BeNil())
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck.Enabled).To(HaveValue(BeFalse()))
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck.RequestTimeout).To(Equal(300))
+		Expect(cluster.Spec.Probes.Liveness.IsolationCheck.ConnectionTimeout).To(Equal(600))
+	})
+})
diff --git a/tests/e2e/probes_test.go b/tests/e2e/probes_test.go
index b43ed7b9..6d15855d 100644
--- a/tests/e2e/probes_test.go
+++ b/tests/e2e/probes_test.go
@@ -62,7 +62,9 @@ var _ = Describe("Probes configuration tests", Label(tests.LabelBasic), func() {
 			Startup: &apiv1.ProbeWithStrategy{
 				Probe: probeConfiguration,
 			},
-			Liveness: probeConfiguration.DeepCopy(),
+			Liveness: &apiv1.LivenessProbe{
+				Probe: probeConfiguration,
+			},
 			Readiness: &apiv1.ProbeWithStrategy{
 				Probe: probeConfiguration,
 			},