Repository: mariadb-operator/mariadb-operator

Issue:
[Bug] Physicalbackup Recover replica fails due ignored tpl schedule spec
**Documentation**
- [x] I acknowledge that I have read the relevant [documentation](https://github.com/mariadb-operator/mariadb-operator/tree/main/docs).

**Describe the bug**
The recovery replica controller is ignoring the schedule spec set in the manifest.
For some reason is adding the following spec:
```
schedule:
  cron: ""
  suspend: false
  immediate: true
```

instead of the expected spec:
```
schedule:
  cron: "30 23 * * *"
  suspend: false
  immediate: true
```
This blocks the pod recovery of the replica pod.

**Expected behaviour**
Be able to create a replica from a physical backup of master and sync the gtid.

**Steps to reproduce the bug**

1. Create an empty chart
2. Add the following dependency
```
dependencies:
- name: mariadb-operator
  version: "25.10.1"
  repository: "https://helm.mariadb.com/mariadb-operator"
```
3. Add a db manifest to the templates folder with the following spec:
```
apiVersion: k8s.mariadb.com/v1alpha1
kind: MariaDB
metadata:
  name: mariadb
  namespace: {{ .Release.Namespace}}
spec:
  updateStrategy:
    type: ReplicasFirstPrimaryLast
    autoUpdateDataPlane: true
  rootPasswordSecretKeyRef:
    name: database-secrets
    key: root-password

  port: 3306

  storage:
    size: 10Gi

  myCnf: |
    [mariadb]
    bind-address=*
    default_storage_engine=InnoDB
    binlog_format=row
    innodb_autoinc_lock_mode=2
    innodb_buffer_pool_size=1024M
    max_allowed_packet=256M
  resources:
    requests:
      cpu: 100m
      memory: 128Mi
    limits:
      memory: 16Gi

  replicas: 1
  replication:
    enabled: false
    gtidStrictMode: true
    syncBinlog: 1
    primary:
      autoFailover: true
      autoFailoverDelay: 0s
    replica:
      bootstrapFrom:
        physicalBackupTemplateRef:
          name: s3-backup-physical
      recovery:
        enabled: true

  affinity:
    antiAffinityEnabled: true

  metrics:
    enabled: true

  livenessProbe:
    exec:
      command:
        - bash
        - -c
        - mariadb -u root -p"${MARIADB_ROOT_PASSWORD}" -e "SELECT 1;"
    initialDelaySeconds: 20
    periodSeconds: 5
    timeoutSeconds: 5

  readinessProbe:
    exec:
      command:
        - bash
        - -c
        - mariadb -u root -p"${MARIADB_ROOT_PASSWORD}" -e "SELECT 1;"
    initialDelaySeconds: 20
    periodSeconds: 5
    timeoutSeconds: 5
```

4. Add the following physicalbackup manifest to the templates folder:
```
apiVersion: k8s.mariadb.com/v1alpha1
kind: PhysicalBackup
metadata:
  name: s3-backup-physical
  namespace: {{ .Release.Namespace}}
spec:
  mariaDbRef:
    name: mariadb
  storage:
    s3:
      bucket: backupdatabase
      prefix: database-physical
      endpoint: internal.s3storage.example # placeolder of our internal s3 strorage
      region:  eu-central-2
      accessKeyIdSecretKeyRef:
        name: s3-access
        key: keyId
      secretAccessKeySecretKeyRef:
        name: s3-access
        key: secretKey
      tls:
        enabled: true
  schedule:
    cron: "30 23 * * *"
    suspend: false
    immediate: true
  maxRetention: 72h # 3 days
  compression: bzip2
```

5. Add a secret manifests with the S3 and database credentials.
6. Deploy the helm without replication
7. Populate the database with some schema and data 
8. Be sure that the mariadb-0 pod does not have any bin logs by running:
```
FLUSH LOGS; PURGE BINARY LOGS BEFORE NOW();
```
9. Change the mariadb manifest in helm to:
```
  replicas: 2
  replication:
    enabled: true
```
10. Wait for the operator to fail to create the replicas due the missing GTID in master and the replica is not able to start a PVC from a physical backup of master.


**Debug information**
Operator controller produces the following logs:
```
{"level":"info","ts":1761313306.766979,"logger":"switchover","msg":"Locking primary with read lock","controller":"mariadb","controllerGroup":"k8s.mariadb.com","controllerKind":"MariaDB","MariaDB":{"name":"mariadb","namespace":"mlflow-database"},"namespace":"mlflow-database","name":"mariadb","reconcileID":"3b1ae232-df10-4151-a482-4292e68ded94","mariadb":"mariadb","primary":0,"new-primary":1}

{"level":"info","ts":1761313306.7680252,"logger":"switchover","msg":"Enabling readonly mode in primary","controller":"mariadb","controllerGroup":"k8s.mariadb.com","controllerKind":"MariaDB","MariaDB":{"name":"mariadb","namespace":"mlflow-database"},"namespace":"mlflow-database","name":"mariadb","reconcileID":"3b1ae232-df10-4151-a482-4292e68ded94","mariadb":"mariadb","primary":0,"new-primary":1}

{"level":"info","ts":1761313306.8172264,"logger":"replica-recovery","msg":"Creating PhysicalBackup","controller":"mariadb","controllerGroup":"k8s.mariadb.com","controllerKind":"MariaDB","MariaDB":{"name":"mariadb","namespace":"mlflow-database"},"namespace":"mlflow-database","name":"mariadb","reconcileID":"3f1a7b2c-c48b-4bec-b4ee-16458f68392a","replicas":["mariadb-1"],"name":"mariadb-physicalbackup-replica-recovery"}

{"level":"error","ts":1761313306.8712344,"msg":"Reconciler error","controller":"mariadb","controllerGroup":"k8s.mariadb.com","controllerKind":"MariaDB","MariaDB":{"name":"mariadb","namespace":"mlflow-database"},"namespace":"mlflow-database","name":"mariadb","reconcileID":"3b1ae232-df10-4151-a482-4292e68ded94","error":"error reconciling Replication: 1 error occurred:\n\t* error in Wait sync switchover reconcile phase: primary GTID (gtid_binlog_pos) is empty\n\n","stacktrace":"sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).reconcileHandler\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:353\nsigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).processNextWorkItem\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:300\nsigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).Start.func2.1\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:202"}

{"level":"error","ts":1761313306.9186723,"msg":"Reconciler error","controller":"mariadb","controllerGroup":"k8s.mariadb.com","controllerKind":"MariaDB","MariaDB":{"name":"mariadb","namespace":"mlflow-database"},"namespace":"mlflow-database","name":"mariadb","reconcileID":"3f1a7b2c-c48b-4bec-b4ee-16458f68392a","error":"error reconciling Replica recovery: 1 error occurred:\n\t* admission webhook \"vphysicalbackup-v1alpha1.kb.io\" denied the request: spec: Invalid value: {\"mariaDbRef\":{\"name\":\"mariadb\",\"waitForIt\":false},\"storage\":{\"s3\":{\"bucket\":\"backupmysql\",\"endpoint\":\"REDACTED\",\"region\":\"eu-central-2\",\"prefix\":\"mlflow-database-physical\",\"accessKeyIdSecretKeyRef\":{\"name\":\"s3-access\",\"key\":\"keyId\"},\"secretAccessKeySecretKeyRef\":{\"name\":\"s3-access\",\"key\":\"secretKey\"},\"tls\":{\"enabled\":true}}},\"schedule\":{\"cron\":\"\",\"suspend\":false,\"immediate\":true},\"maxRetention\":\"0s\",\"restartPolicy\":\"OnFailure\"}: invalid PhysicalBackup: invalid Schedule: either cron or suspend must be set\n\n","stacktrace":"sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).reconcileHandler\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:353\nsigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).processNextWorkItem\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:300\nsigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).Start.func2.1\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:202"}

{"level":"info","ts":1761313307.1775758,"logger":"replica-recovery","msg":"Creating PhysicalBackup","controller":"mariadb","controllerGroup":"k8s.mariadb.com","controllerKind":"MariaDB","MariaDB":{"name":"mariadb","namespace":"mlflow-database"},"namespace":"mlflow-database","name":"mariadb","reconcileID":"e58db8cb-ef84-41d0-b7ee-90b62429cb2a","replicas":["mariadb-1"],"name":"mariadb-physicalbackup-replica-recovery"}
{"level":"error","ts":1761313307.218297,"msg":"Reconciler error","controller":"mariadb","controllerGroup":"k8s.mariadb.com","controllerKind":"MariaDB","MariaDB":{"name":"mariadb","namespace":"mlflow-database"},"namespace":"mlflow-database","name":"mariadb","reconcileID":"e58db8cb-ef84-41d0-b7ee-90b62429cb2a","error":"error reconciling Replica recovery: 1 error occurred:\n\t* admission webhook \"vphysicalbackup-v1alpha1.kb.io\" denied the request: spec: Invalid value: {\"mariaDbRef\":{\"name\":\"mariadb\",\"waitForIt\":false},\"storage\":{\"s3\":{\"bucket\":\"backupmysql\",\"endpoint\":\"REDACTED\",\"region\":\"eu-central-2\",\"prefix\":\"mlflow-database-physical\",\"accessKeyIdSecretKeyRef\":{\"name\":\"s3-access\",\"key\":\"keyId\"},\"secretAccessKeySecretKeyRef\":{\"name\":\"s3-access\",\"key\":\"secretKey\"},\"tls\":{\"enabled\":true}}},\"schedule\":{\"cron\":\"\",\"suspend\":false,\"immediate\":true},\"maxRetention\":\"0s\",\"restartPolicy\":\"OnFailure\"}: invalid PhysicalBackup: invalid Schedule: either cron or suspend must be set\n\n","stacktrace":"sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).reconcileHandler\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:353\nsigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).processNextWorkItem\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:300\nsigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).Start.func2.1\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:202"}
```

**Environment details**:
- Kubernetes version:  v1.33.3+k3s1
- Kubernetes distribution: k3s
- MariaDB Operator version: v25.10.1
- MariaDB Server version: 11.8.2
- MaxScale version: 23.08.5
- Install method: Helm

**Additional context**
<!--Add any other context  here.-->


Discussion:
@mmontes11: ACK! working on a fix now.

Thanks for reporting @Ru13en !@mmontes11: > Wait for the operator to fail to create the replicas due the missing GTID in master and the replica is not able to start a PVC from a physical backup of master.

@Ru13en as of today, backups are taken in ready replicas and never in the primary to avoid disrupting writes. Although the webhook issue you reported is legit (and fixed by https://github.com/mariadb-operator/mariadb-operator/pull/1489) this will not succeed. After deploying the `25.10.2`, the `PhysicalBackup` will be created, but it will not be scheduled with the cluster you reported. The only replica would be `mariadb-repl-1`, which is the one to be recovered, therefore, there are no replicas left to take the physical backup from. To solve this, please provision a cluster with at least 3 replicas.

Feel free to create another issue to evaluate taking backups in primary and let's keep this one focused on the webhook issue.@Ru13en: Thank you @mmontes11 for the fast fix and the extra info. 

Solution patch:
diff --git a/api/v1alpha1/physicalbackup_types.go b/api/v1alpha1/physicalbackup_types.go
index 2c1da3c1..0e2da209 100644
--- a/api/v1alpha1/physicalbackup_types.go
+++ b/api/v1alpha1/physicalbackup_types.go
@@ -85,9 +85,6 @@ type PhysicalBackupSchedule struct {
 
 // Validate determines whether a PhysicalBackupSchedule is valid.
 func (s *PhysicalBackupSchedule) Validate() error {
-	if s.Cron == "" && !s.Suspend {
-		return fmt.Errorf("either cron or suspend must be set")
-	}
 	if s.Cron != "" {
 		_, err := CronParser.Parse(s.Cron)
 		return err
diff --git a/docs/releases/RELEASE_25.10.2_HEADER.md.gotmpl b/docs/releases/RELEASE_25.10.2_HEADER.md.gotmpl
new file mode 100644
index 00000000..a4a06b27
--- /dev/null
+++ b/docs/releases/RELEASE_25.10.2_HEADER.md.gotmpl
@@ -0,0 +1,189 @@
+**`{{ .ProjectName }}` [25.10.2](https://github.com/mariadb-operator/mariadb-operator/releases/tag/25.10.2) is here!** 🦭
+
+
+We are thrilled to announce that our __highly available, replication-based topology is now generally available!__ 🎉  It is been quite a journey and we are very excited about this milestone. A huge thank-you to everyone involved, specially for those who actively contributed to the development of this feature, namely @hedgieinsocks and @Michaelpalacce! 🙇🏼 Please refer to the __[Pull Request for additional details](https://github.com/mariadb-operator/mariadb-operator/pull/1441)__ about all the changes introduced in this release. 
+
+If you're upgrading from previous versions, __do not miss the [UPGRADE GUIDE](https://github.com/mariadb-operator/mariadb-operator/blob/main/docs/releases/UPGRADE_25.10.0.md)__ for a smooth transition.
+
+## Provisioning a replication cluster
+
+In order to provision a replication cluster, you need to configure a number of `replicas` greater than `1` and set the `replication.enabled=true` in the `MariaDB` CR:
+
+```yaml
+apiVersion: k8s.mariadb.com/v1alpha1
+kind: MariaDB
+metadata:
+  name: mariadb-repl
+spec:
+  replicas: 3
+  replication:
+    enabled: true
+```
+After applying the previous CR, the operator will provision a replication cluster with one primary and two replicas. The operator will take care of setting up replication, configuring the replication user and monitoring the replication status:
+
+```bash
+kubectl get pods
+NAME                                    READY   STATUS    RESTARTS   AGE
+mariadb-repl-0                          2/2     Running   0          2d19h
+mariadb-repl-1                          2/2     Running   0          2d19h
+mariadb-repl-2                          2/2     Running   0          2d19h
+mariadb-repl-metrics-56865fff65-t72kc   1/1     Running   0          2d20h
+
+kubectl get mariadb
+NAME           READY   STATUS    PRIMARY          UPDATES                    AGE
+mariadb-repl   True    Running   mariadb-repl-0   ReplicasFirstPrimaryLast   2d20h
+```
+
+As you can see, the primary can be identified in the `PRIMARY` column of the `kubectl get mariadb` output. You may also inspect the current replication status by checking the `MariaDB` status.
+
+## Primary failover
+
+Whenever the primary `Pod` goes down, the operator will automatically trigger a failover to the furthest advanced replica. This is the default behaviour, and can be configured by setting:
+
+```yaml
+apiVersion: k8s.mariadb.com/v1alpha1
+kind: MariaDB
+metadata:
+  name: mariadb-repl
+spec:
+  replicas: 3
+  replication:
+    enabled: true
+    primary:
+      autoFailover: true
+      autoFailoverDelay: 0s
+```
+
+In this situation, the following status will be reported in the `MariaDB` CR:
+
+```bash
+kubectl get mariadb
+NAME           READY   STATUS                                  PRIMARY          UPDATES                    AGE
+mariadb-repl   False   Switching primary to 'mariadb-repl-1'   mariadb-repl-0   ReplicasFirstPrimaryLast   3d2h 
+
+kubectl get mariadb
+NAME           READY   STATUS    PRIMARY          UPDATES                    AGE
+mariadb-repl   True    Running   mariadb-repl-1   ReplicasFirstPrimaryLast   3d2h
+```
+
+## Scaling out
+
+The operator will scale horizontaly the replication cluster whenever you increase the number of `spec.replicas`. Before doing so, you should specify a template `PhysicalBackup` resource that the operator will use to create the actual `PhysicalBackup` objects to set up new replicas during scaling out events:
+
+```yaml
+apiVersion: k8s.mariadb.com/v1alpha1
+kind: MariaDB
+metadata:
+  name: mariadb-repl
+spec:
+  replication:
+    enabled: true
+    replica:
+      bootstrapFrom:
+        physicalBackupTemplateRef:
+          name: physicalbackup-tpl
+---
+apiVersion: k8s.mariadb.com/v1alpha1
+kind: PhysicalBackup
+metadata:
+  name: physicalbackup-tpl
+spec:
+  mariaDbRef:
+    name: mariadb-repl
+  schedule:
+    suspend: true
+  storage:
+    volumeSnapshot:
+      volumeSnapshotClassName: csi-hostpath-snapclass
+```
+
+After configuring this, you will be able to scale the cluster simply by:
+
+```bash
+kubectl scale mariadb mariadb-repl --replicas=4
+
+kubectl get mariadb
+NAME           READY   STATUS        PRIMARY          UPDATES                    AGE
+mariadb-repl   False   Scaling out   mariadb-repl-1   ReplicasFirstPrimaryLast   3d5h
+
+kubectl get pods
+NAME                                    READY   STATUS    RESTARTS   AGE
+mariadb-repl-0                          2/2     Running   0          137m
+mariadb-repl-1                          2/2     Running   0          3d5h
+mariadb-repl-2                          2/2     Running   0          3d5h
+mariadb-repl-3                          2/2     Running   0          40s
+```
+
+## Replica recovery
+
+The operator has the ability to automatically recover replicas that become unavailable and report a specific error code in the replication status.  For doing so, the operator continiously monitors the replication status of each replica, and whenever they report a known error code, a recovery operation will be triggered.
+
+Similarly to the scaling out operation, you need to define a `PhysicalBackup` template and set a reference to it in the `spec.replication.replica.bootstrapFrom` field of the `MariaDB` CR. Additionally, you need to explicitly enable the replica recovery, as it is disabled by default:
+
+```yaml
+apiVersion: k8s.mariadb.com/v1alpha1
+kind: MariaDB
+metadata:
+  name: mariadb-repl
+spec:
+  replication:
+    enabled: true
+    replica:
+      bootstrapFrom:
+        physicalBackupTemplateRef:
+          name: physicalbackup-tpl
+      recovery:
+        enabled: true
+        errorDurationThreshold: 5m
+```
+
+We will be simulating a [`1236` error](https://mariadb.com/docs/server/reference/error-codes/mariadb-error-codes-1200-to-1299/e1236) in a replica to demostrate how the recovery process works:
+
+> [!CAUTION]
+> Do not perform the following steps in a production environment.
+
+-  Purge the binary logs in the primary:
+```bash
+PRIMARY=$(kubectl get mariadb mariadb-repl -o jsonpath="{.status.currentPrimary}")
+echo "Purging binary logs in primary $PRIMARY"
+kubectl exec -it $PRIMARY -c mariadb -- mariadb -u root -p'MariaDB11!' --ssl=false -e "FLUSH LOGS;"
+kubectl exec -it $PRIMARY -c mariadb -- mariadb -u root -p'MariaDB11!' --ssl=false -e "PURGE BINARY LOGS BEFORE NOW();"
+kubectl exec -it $PRIMARY -c mariadb -- mariadb -u root -p'MariaDB11!' --ssl=false -e "SHOW BINARY LOGS;"
+```
+
+- Delete the PVC and restart one of the replicas:
+```bash
+REPLICA=$(kubectl get mariadb mariadb-repl -o jsonpath='{.status.replication.replicas}' | jq -r 'keys[]' | head -n1)
+echo "Deleting PVC and restarting replica $REPLICA"
+kubectl delete pvc storage-$REPLICA --wait=false 
+kubectl delete pod $REPLICA --wait=false 
+```
+
+This will trigger a replica recovery operation:
+
+```bash
+kubectl get mariadb
+NAME           READY   STATUS                PRIMARY          UPDATES                    AGE
+mariadb-repl   False   Recovering replicas   mariadb-repl-1   ReplicasFirstPrimaryLast   3d6h
+
+kubectl get pods
+NAME                                                              READY   STATUS            RESTARTS       AGE
+mariadb-repl-0                                                    0/2     PodInitializing   0              22s
+mariadb-repl-0-physicalbackup-init-qn79f                          0/1     Completed         0              8s
+mariadb-repl-1                                                    2/2     Running           0              3d6h
+mariadb-repl-2                                                    2/2     Running           0              3d6h
+mariadb-repl-metrics-56865fff65-t72kc                             1/1     Running           0              3d6h
+mariadb-repl-physicalbackup-replica-recovery-2025102020270r98zr   0/1     Completed         0              31s
+
+kubectl get mariadb
+NAME           READY   STATUS    PRIMARY          UPDATES                    AGE
+mariadb-repl   True    Running   mariadb-repl-1   ReplicasFirstPrimaryLast   3d6h
+```
+
+## Documentation
+
+Please refer to the __[replication docs](https://github.com/mariadb-operator/mariadb-operator/blob/main/docs/replication.md)__ for further details.
+
+---
+
+__`{{ .ProjectName }}` needs your support! Please consider contributing by adding yourself to the [list of adopters](https://github.com/mariadb-operator/mariadb-operator/blob/main/ADOPTERS.md) or starring the project! 🌟__
\ No newline at end of file
diff --git a/docs/replication.md b/docs/replication.md
index 078a6402..6e10f55b 100644
--- a/docs/replication.md
+++ b/docs/replication.md
@@ -480,10 +480,12 @@ We will be simulating a `1236` error in a replica to demostrate how the recovery
 ```bash
 PRIMARY=$(kubectl get mariadb mariadb-repl -o jsonpath="{.status.currentPrimary}")
 echo "Purging binary logs in primary $PRIMARY"
-kubectl exec -it $PRIMARY -c mariadb -- mariadb -u root -p'MariaDB11!' --ssl=false -e "FLUSH LOGS; PURGE BINARY LOGS BEFORE NOW();"
+kubectl exec -it $PRIMARY -c mariadb -- mariadb -u root -p'MariaDB11!' --ssl=false -e "FLUSH LOGS;"
+kubectl exec -it $PRIMARY -c mariadb -- mariadb -u root -p'MariaDB11!' --ssl=false -e "PURGE BINARY LOGS BEFORE NOW();"
+kubectl exec -it $PRIMARY -c mariadb -- mariadb -u root -p'MariaDB11!' --ssl=false -e "SHOW BINARY LOGS;"
 ```
 
-- Delete the PVC and restart one of the replicas. If this is a brand new `MariaDB` instance, you might need to re-attempt this step:
+- Delete the PVC and restart one of the replicas:
 ```bash
 REPLICA=$(kubectl get mariadb mariadb-repl -o jsonpath='{.status.replication.replicas}' | jq -r 'keys[]' | head -n1)
 echo "Deleting PVC and restarting replica $REPLICA"
diff --git a/internal/webhook/v1alpha1/mariadb_webhook.go b/internal/webhook/v1alpha1/mariadb_webhook.go
index dcd8714c..821e7121 100644
--- a/internal/webhook/v1alpha1/mariadb_webhook.go
+++ b/internal/webhook/v1alpha1/mariadb_webhook.go
@@ -94,9 +94,6 @@ func (v *MariaDBCustomValidator) ValidateUpdate(ctx context.Context, oldObj, new
 		}
 	}
 
-	if err := validatePrimarySwitchover(mariadb, oldMariadb); err != nil {
-		return nil, err
-	}
 	return nil, validateUpdateStorage(mariadb, oldMariadb)
 }
 
@@ -232,29 +229,6 @@ func validateReplication(mariadb *v1alpha1.MariaDB) error {
 	return nil
 }
 
-func validatePrimarySwitchover(mariadb, old *v1alpha1.MariaDB) error {
-	if old.IsReplicationEnabled() && old.IsSwitchingPrimary() {
-		oldReplication := ptr.Deref(old.Spec.Replication, v1alpha1.Replication{})
-		mariadbReplication := ptr.Deref(mariadb.Spec.Replication, v1alpha1.Replication{})
-		if *oldReplication.Primary.PodIndex != *mariadbReplication.Primary.PodIndex {
-			return field.Invalid(
-				field.NewPath("spec").Child("replication").Child("primary").Child("podIndex"),
-				mariadbReplication.Primary.PodIndex,
-				"'spec.replication.primary.podIndex' cannot be updated during a primary switchover",
-			)
-		}
-		if *oldReplication.Primary.AutoFailover != *mariadbReplication.Primary.AutoFailover &&
-			*mariadbReplication.Primary.AutoFailover {
-			return field.Invalid(
-				field.NewPath("spec").Child("replication").Child("primary").Child("autoFailover"),
-				mariadbReplication.Primary.PodIndex,
-				"'spec.replication.primary.autoFailover' cannot be enabled during a primary switchover",
-			)
-		}
-	}
-	return nil
-}
-
 func validateBootstrapFrom(mariadb *v1alpha1.MariaDB) error {
 	if mariadb.Spec.BootstrapFrom == nil {
 		return nil



Test patch:
diff --git a/internal/webhook/v1alpha1/mariadb_webhook_test.go b/internal/webhook/v1alpha1/mariadb_webhook_test.go
index 52831518..61ba4a0b 100644
--- a/internal/webhook/v1alpha1/mariadb_webhook_test.go
+++ b/internal/webhook/v1alpha1/mariadb_webhook_test.go
@@ -847,158 +847,4 @@ var _ = Describe("v1alpha1.MariaDB webhook", func() {
 			),
 		)
 	})
-
-	Context("When updating v1alpha1.MariaDB primary pod index", Ordered, func() {
-		noSwitchoverKey := types.NamespacedName{
-			Name:      "mariadb-no-switchover-webhook",
-			Namespace: testNamespace,
-		}
-		switchoverKey := types.NamespacedName{
-			Name:      "mariadb-switchover-webhook",
-			Namespace: testNamespace,
-		}
-		BeforeAll(func() {
-			test := "test"
-			mariaDb := v1alpha1.MariaDB{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      noSwitchoverKey.Name,
-					Namespace: noSwitchoverKey.Namespace,
-				},
-				Spec: v1alpha1.MariaDBSpec{
-					Database: &test,
-					Username: &test,
-					PasswordSecretKeyRef: &v1alpha1.GeneratedSecretKeyRef{
-						SecretKeySelector: v1alpha1.SecretKeySelector{
-							LocalObjectReference: v1alpha1.LocalObjectReference{
-								Name: "secret",
-							},
-							Key: "password",
-						},
-					},
-					Replication: &v1alpha1.Replication{
-						ReplicationSpec: v1alpha1.ReplicationSpec{
-							Primary: v1alpha1.PrimaryReplication{
-								PodIndex:     func() *int { i := 0; return &i }(),
-								AutoFailover: func() *bool { f := false; return &f }(),
-							},
-						},
-						Enabled: true,
-					},
-					Replicas: 3,
-					Storage: v1alpha1.Storage{
-						Size: ptr.To(resource.MustParse("100Mi")),
-					},
-				},
-			}
-			mariaDbSwitchover := v1alpha1.MariaDB{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      switchoverKey.Name,
-					Namespace: switchoverKey.Namespace,
-				},
-				Spec: v1alpha1.MariaDBSpec{
-					Database: &test,
-					Username: &test,
-					PasswordSecretKeyRef: &v1alpha1.GeneratedSecretKeyRef{
-						SecretKeySelector: v1alpha1.SecretKeySelector{
-							LocalObjectReference: v1alpha1.LocalObjectReference{
-								Name: "secret",
-							},
-							Key: "password",
-						},
-					},
-					Replication: &v1alpha1.Replication{
-						ReplicationSpec: v1alpha1.ReplicationSpec{
-							Primary: v1alpha1.PrimaryReplication{
-								PodIndex:     func() *int { i := 0; return &i }(),
-								AutoFailover: func() *bool { f := false; return &f }(),
-							},
-						},
-						Enabled: true,
-					},
-					Replicas: 3,
-					Storage: v1alpha1.Storage{
-						Size: ptr.To(resource.MustParse("100Mi")),
-					},
-				},
-				Status: v1alpha1.MariaDBStatus{
-					Conditions: []metav1.Condition{
-						{
-							Type:    v1alpha1.ConditionTypePrimarySwitched,
-							Status:  metav1.ConditionFalse,
-							Reason:  v1alpha1.ConditionReasonSwitchPrimary,
-							Message: "Switching primary",
-						},
-					},
-				},
-			}
-			Expect(k8sClient.Create(testCtx, &mariaDb)).To(Succeed())
-			Expect(k8sClient.Create(testCtx, &mariaDbSwitchover)).To(Succeed())
-
-			By("Updating status conditions")
-			Expect(k8sClient.Get(testCtx, switchoverKey, &mariaDbSwitchover)).To(Succeed())
-			mariaDbSwitchover.Status.Conditions = []metav1.Condition{
-				{
-					Type:               v1alpha1.ConditionTypePrimarySwitched,
-					Status:             metav1.ConditionFalse,
-					Reason:             v1alpha1.ConditionReasonSwitchPrimary,
-					Message:            "Switching primary",
-					LastTransitionTime: metav1.Now(),
-				},
-			}
-			Expect(k8sClient.Status().Update(testCtx, &mariaDbSwitchover)).To(Succeed())
-		})
-		DescribeTable(
-			"Should validate",
-			func(key types.NamespacedName, patchFn func(mdb *v1alpha1.MariaDB), wantErr bool) {
-				var mdb v1alpha1.MariaDB
-				Expect(k8sClient.Get(testCtx, key, &mdb)).To(Succeed())
-
-				patch := client.MergeFrom(mdb.DeepCopy())
-				patchFn(&mdb)
-
-				err := k8sClient.Patch(testCtx, &mdb, patch)
-				if wantErr {
-					Expect(err).To(HaveOccurred())
-				} else {
-					Expect(err).ToNot(HaveOccurred())
-				}
-			},
-			Entry(
-				"Updating primary pod index",
-				noSwitchoverKey,
-				func(mdb *v1alpha1.MariaDB) {
-					i := 1
-					mdb.Spec.Replication.Primary.PodIndex = &i
-				},
-				false,
-			),
-			Entry(
-				"Updating automatic failover",
-				noSwitchoverKey,
-				func(mdb *v1alpha1.MariaDB) {
-					f := true
-					mdb.Spec.Replication.Primary.AutoFailover = &f
-				},
-				false,
-			),
-			Entry(
-				"Updating primary pod index when switching",
-				switchoverKey,
-				func(mdb *v1alpha1.MariaDB) {
-					i := 1
-					mdb.Spec.Replication.Primary.PodIndex = &i
-				},
-				true,
-			),
-			Entry(
-				"Updating automatic failover when switching",
-				switchoverKey,
-				func(mdb *v1alpha1.MariaDB) {
-					f := true
-					mdb.Spec.Replication.Primary.AutoFailover = &f
-				},
-				true,
-			),
-		)
-	})
 })
diff --git a/internal/webhook/v1alpha1/physicalbackup_webhook_test.go b/internal/webhook/v1alpha1/physicalbackup_webhook_test.go
index 94c5e889..88742dc1 100644
--- a/internal/webhook/v1alpha1/physicalbackup_webhook_test.go
+++ b/internal/webhook/v1alpha1/physicalbackup_webhook_test.go
@@ -166,7 +166,7 @@ var _ = Describe("PhysicalBackup webhook", func() {
 				"Invalid cron",
 				&v1alpha1.PhysicalBackup{
 					ObjectMeta: metav1.ObjectMeta{
-						Name:      "physicalbackup-invalid-schedule",
+						Name:      "physicalbackup-invalid-cron",
 						Namespace: testNamespace,
 					},
 					Spec: v1alpha1.PhysicalBackupSpec{
@@ -200,10 +200,44 @@ var _ = Describe("PhysicalBackup webhook", func() {
 				true,
 			),
 			Entry(
-				"Invalid schedule",
+				"No schedule (defaults to immediate)",
 				&v1alpha1.PhysicalBackup{
 					ObjectMeta: metav1.ObjectMeta{
-						Name:      "physicalbackup-invalid-schedule",
+						Name:      "physicalbackup-immediate-default",
+						Namespace: testNamespace,
+					},
+					Spec: v1alpha1.PhysicalBackupSpec{
+						JobContainerTemplate: v1alpha1.JobContainerTemplate{
+							Resources: &v1alpha1.ResourceRequirements{
+								Requests: corev1.ResourceList{
+									"cpu": resource.MustParse("100m"),
+								},
+							},
+						},
+						Compression: v1alpha1.CompressGzip,
+						Storage: v1alpha1.PhysicalBackupStorage{
+							S3: &v1alpha1.S3{
+								Bucket:   "test",
+								Endpoint: "test",
+							},
+						},
+						MariaDBRef: v1alpha1.MariaDBRef{
+							ObjectReference: v1alpha1.ObjectReference{
+								Name: "mariadb-webhook",
+							},
+							WaitForIt: true,
+						},
+						BackoffLimit:  10,
+						RestartPolicy: corev1.RestartPolicyOnFailure,
+					},
+				},
+				false,
+			),
+			Entry(
+				"Immediate schedule",
+				&v1alpha1.PhysicalBackup{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "physicalbackup-immediate",
 						Namespace: testNamespace,
 					},
 					Spec: v1alpha1.PhysicalBackupSpec{
@@ -215,8 +249,9 @@ var _ = Describe("PhysicalBackup webhook", func() {
 							},
 						},
 						Schedule: &v1alpha1.PhysicalBackupSchedule{
-							Cron:    "",
-							Suspend: false,
+							Cron:      "",
+							Suspend:   false,
+							Immediate: ptr.To(true),
 						},
 						Compression: v1alpha1.CompressGzip,
 						Storage: v1alpha1.PhysicalBackupStorage{
@@ -235,13 +270,13 @@ var _ = Describe("PhysicalBackup webhook", func() {
 						RestartPolicy: corev1.RestartPolicyOnFailure,
 					},
 				},
-				true,
+				false,
 			),
 			Entry(
 				"Suspended schedule",
 				&v1alpha1.PhysicalBackup{
 					ObjectMeta: metav1.ObjectMeta{
-						Name:      "physicalbackup-invalid-schedule",
+						Name:      "physicalbackup-suspended",
 						Namespace: testNamespace,
 					},
 					Spec: v1alpha1.PhysicalBackupSpec{