Patch Solr resource statuses instead of updating them (#545)

diff --git a/api/v1beta1/solrbackup_types.go b/api/v1beta1/solrbackup_types.go
index d0cf6a2..2b8c6cf 100644
--- a/api/v1beta1/solrbackup_types.go
+++ b/api/v1beta1/solrbackup_types.go
@@ -130,6 +130,7 @@
 	Successful *bool `json:"successful,omitempty"`
 
 	// Whether the backup has finished
+	// +optional
 	Finished bool `json:"finished,omitempty"`
 }
 
diff --git a/api/v1beta1/solrcloud_types.go b/api/v1beta1/solrcloud_types.go
index 53dfa97..e7671ef 100644
--- a/api/v1beta1/solrcloud_types.go
+++ b/api/v1beta1/solrcloud_types.go
@@ -1071,18 +1071,26 @@
 // SolrCloudStatus defines the observed state of SolrCloud
 type SolrCloudStatus struct {
 	// SolrNodes contain the statuses of each solr node running in this solr cloud.
+	//+listType:=map
+	//+listMapKey:=name
 	SolrNodes []SolrNodeStatus `json:"solrNodes"`
 
 	// Replicas is the number of desired replicas in the cluster
+	// +kubebuilder:validation:Minimum=0
+	// +kubebuilder:default=0
 	Replicas int32 `json:"replicas"`
 
 	// PodSelector for SolrCloud pods, required by the HPA
 	PodSelector string `json:"podSelector"`
 
 	// ReadyReplicas is the number of ready replicas in the cluster
+	// +kubebuilder:validation:Minimum=0
+	// +kubebuilder:default=0
 	ReadyReplicas int32 `json:"readyReplicas"`
 
 	// UpToDateNodes is the number of Solr Node pods that are running the latest pod spec
+	// +kubebuilder:validation:Minimum=0
+	// +kubebuilder:default=0
 	UpToDateNodes int32 `json:"upToDateNodes"`
 
 	// The version of solr that the cloud is running
@@ -1106,6 +1114,7 @@
 
 	// BackupRestoreReady announces whether the solrCloud has the backupRestorePVC mounted to all pods
 	// and therefore is ready for backups and restores.
+	// +optional
 	BackupRestoreReady bool `json:"backupRestoreReady"`
 
 	// BackupRepositoriesAvailable lists the backupRepositories specified in the SolrCloud and whether they are available across all Pods.
diff --git a/config/crd/bases/solr.apache.org_solrclouds.yaml b/config/crd/bases/solr.apache.org_solrclouds.yaml
index 1a18705..dd860e4 100644
--- a/config/crd/bases/solr.apache.org_solrclouds.yaml
+++ b/config/crd/bases/solr.apache.org_solrclouds.yaml
@@ -11719,13 +11719,17 @@
                 description: PodSelector for SolrCloud pods, required by the HPA
                 type: string
               readyReplicas:
+                default: 0
                 description: ReadyReplicas is the number of ready replicas in the
                   cluster
                 format: int32
+                minimum: 0
                 type: integer
               replicas:
+                default: 0
                 description: Replicas is the number of desired replicas in the cluster
                 format: int32
+                minimum: 0
                 type: integer
               solrNodes:
                 description: SolrNodes contain the statuses of each solr node running
@@ -11772,14 +11776,19 @@
                   - version
                   type: object
                 type: array
+                x-kubernetes-list-map-keys:
+                - name
+                x-kubernetes-list-type: map
               targetVersion:
                 description: The version of solr that the cloud is meant to be running.
                   Will only be provided when the cloud is migrating between versions
                 type: string
               upToDateNodes:
+                default: 0
                 description: UpToDateNodes is the number of Solr Node pods that are
                   running the latest pod spec
                 format: int32
+                minimum: 0
                 type: integer
               version:
                 description: The version of solr that the cloud is running
@@ -11850,7 +11859,6 @@
                     type: object
                 type: object
             required:
-            - backupRestoreReady
             - internalCommonAddress
             - podSelector
             - readyReplicas
diff --git a/controllers/controller_utils_test.go b/controllers/controller_utils_test.go
index e909a3c..7aa663a 100644
--- a/controllers/controller_utils_test.go
+++ b/controllers/controller_utils_test.go
@@ -708,7 +708,7 @@
 		&corev1.ConfigMap{}, &netv1.Ingress{},
 		&corev1.PersistentVolumeClaim{}, &corev1.PersistentVolume{},
 		&appsv1.StatefulSet{}, &appsv1.Deployment{}, &appsv1.ReplicaSet{}, &corev1.Pod{}, &corev1.PersistentVolumeClaim{},
-		&corev1.Secret{},
+		&corev1.Secret{}, &policyv1.PodDisruptionBudget{},
 	}
 	By("deleting all managed resources")
 	for _, obj := range cleanupObjects {
diff --git a/controllers/solrbackup_controller.go b/controllers/solrbackup_controller.go
index e5c6981..eafd6b2 100644
--- a/controllers/solrbackup_controller.go
+++ b/controllers/solrbackup_controller.go
@@ -88,7 +88,7 @@
 		return reconcile.Result{Requeue: true}, nil
 	}
 
-	oldStatus := backup.Status.DeepCopy()
+	unmodifiedBackupResource := backup.DeepCopy()
 
 	requeueOrNot := reconcile.Result{}
 
@@ -159,9 +159,9 @@
 		}
 	}
 
-	if !reflect.DeepEqual(*oldStatus, backup.Status) {
-		logger.Info("Updating status for solr-backup", "newStatus", backup.Status, "oldStatus", oldStatus)
-		err = r.Status().Update(ctx, backup)
+	if !reflect.DeepEqual(unmodifiedBackupResource.Status, backup.Status) {
+		logger.Info("Updating status for solr-backup", "newStatus", backup.Status, "oldStatus", unmodifiedBackupResource.Status)
+		err = r.Status().Patch(ctx, backup, client.MergeFrom(unmodifiedBackupResource))
 	}
 
 	return requeueOrNot, err
diff --git a/controllers/solrcloud_controller.go b/controllers/solrcloud_controller.go
index 392923b..8b02e99 100644
--- a/controllers/solrcloud_controller.go
+++ b/controllers/solrcloud_controller.go
@@ -522,9 +522,10 @@
 	}
 
 	if !reflect.DeepEqual(instance.Status, newStatus) {
+		logger.Info("Updating SolrCloud Status", "status", newStatus)
+		oldInstance := instance.DeepCopy()
 		instance.Status = newStatus
-		logger.Info("Updating SolrCloud Status", "status", instance.Status)
-		err = r.Status().Update(ctx, instance)
+		err = r.Status().Patch(ctx, instance, client.MergeFrom(oldInstance))
 		if err != nil {
 			return requeueOrNot, err
 		}
diff --git a/controllers/solrprometheusexporter_controller.go b/controllers/solrprometheusexporter_controller.go
index 9adc3f4..afe7778 100644
--- a/controllers/solrprometheusexporter_controller.go
+++ b/controllers/solrprometheusexporter_controller.go
@@ -262,9 +262,10 @@
 	}
 
 	if ready != prometheusExporter.Status.Ready {
+		originalPrometheusExporter := prometheusExporter.DeepCopy()
 		prometheusExporter.Status.Ready = ready
 		logger.Info("Updating status for solr-prometheus-exporter")
-		err = r.Status().Update(ctx, prometheusExporter)
+		err = r.Status().Patch(ctx, prometheusExporter, client.MergeFrom(originalPrometheusExporter))
 	}
 
 	return requeueOrNot, err
diff --git a/helm/solr-operator/Chart.yaml b/helm/solr-operator/Chart.yaml
index 419e588..325c6f9 100644
--- a/helm/solr-operator/Chart.yaml
+++ b/helm/solr-operator/Chart.yaml
@@ -132,6 +132,13 @@
           url: https://github.com/apache/solr-operator/issues/483
         - name: GitHub PR
           url: https://github.com/apache/solr-operator/pull/539
+    - kind: fixed
+      description: Solr resource status are now patched instead of updated, this should reduce "error" logging in the operator.
+      links:
+        - name: GitHub Issue
+          url: https://github.com/apache/solr-operator/issues/544
+        - name: GitHub PR
+          url: https://github.com/apache/solr-operator/pull/545
   artifacthub.io/images: |
     - name: solr-operator
       image: apache/solr-operator:v0.7.0-prerelease
diff --git a/helm/solr-operator/crds/crds.yaml b/helm/solr-operator/crds/crds.yaml
index 29c8485..ac4b910 100644
--- a/helm/solr-operator/crds/crds.yaml
+++ b/helm/solr-operator/crds/crds.yaml
@@ -11968,13 +11968,17 @@
                 description: PodSelector for SolrCloud pods, required by the HPA
                 type: string
               readyReplicas:
+                default: 0
                 description: ReadyReplicas is the number of ready replicas in the
                   cluster
                 format: int32
+                minimum: 0
                 type: integer
               replicas:
+                default: 0
                 description: Replicas is the number of desired replicas in the cluster
                 format: int32
+                minimum: 0
                 type: integer
               solrNodes:
                 description: SolrNodes contain the statuses of each solr node running
@@ -12021,14 +12025,19 @@
                   - version
                   type: object
                 type: array
+                x-kubernetes-list-map-keys:
+                - name
+                x-kubernetes-list-type: map
               targetVersion:
                 description: The version of solr that the cloud is meant to be running.
                   Will only be provided when the cloud is migrating between versions
                 type: string
               upToDateNodes:
+                default: 0
                 description: UpToDateNodes is the number of Solr Node pods that are
                   running the latest pod spec
                 format: int32
+                minimum: 0
                 type: integer
               version:
                 description: The version of solr that the cloud is running
@@ -12099,7 +12108,6 @@
                     type: object
                 type: object
             required:
-            - backupRestoreReady
             - internalCommonAddress
             - podSelector
             - readyReplicas
diff --git a/tests/e2e/resource_utils_test.go b/tests/e2e/resource_utils_test.go
index 5b59fbe..bb32f59 100644
--- a/tests/e2e/resource_utils_test.go
+++ b/tests/e2e/resource_utils_test.go
@@ -728,7 +728,7 @@
 		&corev1.ConfigMap{}, &netv1.Ingress{},
 		&corev1.PersistentVolumeClaim{}, &corev1.PersistentVolume{},
 		&appsv1.StatefulSet{}, &appsv1.Deployment{}, &appsv1.ReplicaSet{}, &corev1.Pod{}, &corev1.PersistentVolumeClaim{},
-		&corev1.Secret{},
+		&corev1.Secret{}, &policyv1.PodDisruptionBudget{},
 	}
 	By("deleting all managed resources")
 	for _, obj := range cleanupObjects {