You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ho...@apache.org on 2021/11/04 17:14:44 UTC

[solr-operator] branch main updated: Specify individual backupRepo availability in SolrCloud Status (#358)

This is an automated email from the ASF dual-hosted git repository.

houston pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-operator.git


The following commit(s) were added to refs/heads/main by this push:
     new 9860911  Specify individual backupRepo availability in SolrCloud Status (#358)
9860911 is described below

commit 9860911ac24479bc23d447a73115d74ae96b4eb2
Author: Houston Putman <ho...@apache.org>
AuthorDate: Thu Nov 4 13:14:39 2021 -0400

    Specify individual backupRepo availability in SolrCloud Status (#358)
---
 api/v1beta1/solrbackup_types.go                    |  8 ++
 api/v1beta1/solrcloud_types.go                     | 14 +++-
 api/v1beta1/zz_generated.deepcopy.go               |  7 ++
 config/crd/bases/solr.apache.org_solrbackups.yaml  |  6 ++
 config/crd/bases/solr.apache.org_solrclouds.yaml   | 14 +++-
 controllers/solrbackup_controller.go               | 86 ++++++++++++++++++----
 controllers/solrcloud_controller.go                | 44 ++++++-----
 controllers/solrcloud_controller_backup_test.go    | 54 +++++++++++++-
 .../solrprometheusexporter_controller_test.go      |  5 +-
 controllers/util/solr_api/cluster_status.go        | 48 ++++++------
 controllers/util/solr_backup_repo_util.go          | 28 +++++++
 controllers/util/solr_util.go                      |  2 +
 example/test_backup_gcs.yaml                       |  4 +-
 example/test_backup_managed.yaml                   |  2 +-
 example/test_solrcloud_backuprepos.yaml            | 27 ++++---
 helm/solr-operator/Chart.yaml                      |  7 ++
 helm/solr-operator/crds/crds.yaml                  | 20 ++++-
 17 files changed, 288 insertions(+), 88 deletions(-)

diff --git a/api/v1beta1/solrbackup_types.go b/api/v1beta1/solrbackup_types.go
index e81ee9e..ec0d151 100644
--- a/api/v1beta1/solrbackup_types.go
+++ b/api/v1beta1/solrbackup_types.go
@@ -27,10 +27,18 @@ import (
 // SolrBackupSpec defines the desired state of SolrBackup
 type SolrBackupSpec struct {
 	// A reference to the SolrCloud to create a backup for
+	//
+	// +kubebuilder:validation:Pattern:=[a-z0-9]([-a-z0-9]*[a-z0-9])?
+	// +kubebuilder:validation:MinLength:=1
+	// +kubebuilder:validation:MaxLength:=63
 	SolrCloud string `json:"solrCloud"`
 
 	// The name of the repository to use for the backup.  Defaults to "legacy_local_repository" if not specified (the
 	// auto-configured repository for legacy singleton volumes).
+	//
+	// +kubebuilder:validation:Pattern:=[a-zA-Z0-9]([-_a-zA-Z0-9]*[a-zA-Z0-9])?
+	// +kubebuilder:validation:MinLength:=1
+	// +kubebuilder:validation:MaxLength:=100
 	// +optional
 	RepositoryName string `json:"repositoryName,omitempty"`
 
diff --git a/api/v1beta1/solrcloud_types.go b/api/v1beta1/solrcloud_types.go
index 692afd4..ff265ec 100644
--- a/api/v1beta1/solrcloud_types.go
+++ b/api/v1beta1/solrcloud_types.go
@@ -385,6 +385,10 @@ type SolrBackupRestoreOptions struct {
 type SolrBackupRepository struct {
 	// A name used to identify this local storage profile.  Values should follow RFC-1123.  (See here for more details:
 	// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names)
+	//
+	// +kubebuilder:validation:Pattern:=[a-zA-Z0-9]([-_a-zA-Z0-9]*[a-zA-Z0-9])?
+	// +kubebuilder:validation:MinLength:=1
+	// +kubebuilder:validation:MaxLength:=100
 	Name string `json:"name"`
 
 	// A GCSRepository for Solr to use when backing up and restoring collections.
@@ -1013,16 +1017,16 @@ type SolrCloudStatus struct {
 	// SolrNodes contain the statuses of each solr node running in this solr cloud.
 	SolrNodes []SolrNodeStatus `json:"solrNodes"`
 
-	// Replicas is the number of number of desired replicas in the cluster
+	// Replicas is the number of desired replicas in the cluster
 	Replicas int32 `json:"replicas"`
 
 	// PodSelector for SolrCloud pods, required by the HPA
 	PodSelector string `json:"podSelector"`
 
-	// ReadyReplicas is the number of number of ready replicas in the cluster
+	// ReadyReplicas is the number of ready replicas in the cluster
 	ReadyReplicas int32 `json:"readyReplicas"`
 
-	// UpToDateNodes is the number of number of Solr Node pods that are running the latest pod spec
+	// UpToDateNodes is the number of Solr Node pods that are running the latest pod spec
 	UpToDateNodes int32 `json:"upToDateNodes"`
 
 	// The version of solr that the cloud is running
@@ -1047,6 +1051,10 @@ type SolrCloudStatus struct {
 	// BackupRestoreReady announces whether the solrCloud has the backupRestorePVC mounted to all pods
 	// and therefore is ready for backups and restores.
 	BackupRestoreReady bool `json:"backupRestoreReady"`
+
+	// BackupRepositoriesAvailable lists the backupRepositories specified in the SolrCloud and whether they are available across all Pods.
+	// +optional
+	BackupRepositoriesAvailable map[string]bool `json:"backupRepositoriesAvailable,omitempty"`
 }
 
 // SolrNodeStatus is the status of a solrNode in the cloud, with readiness status
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index 79d356a..31031ee 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -1020,6 +1020,13 @@ func (in *SolrCloudStatus) DeepCopyInto(out *SolrCloudStatus) {
 		**out = **in
 	}
 	in.ZookeeperConnectionInfo.DeepCopyInto(&out.ZookeeperConnectionInfo)
+	if in.BackupRepositoriesAvailable != nil {
+		in, out := &in.BackupRepositoriesAvailable, &out.BackupRepositoriesAvailable
+		*out = make(map[string]bool, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SolrCloudStatus.
diff --git a/config/crd/bases/solr.apache.org_solrbackups.yaml b/config/crd/bases/solr.apache.org_solrbackups.yaml
index ddb9ffe..248515a 100644
--- a/config/crd/bases/solr.apache.org_solrbackups.yaml
+++ b/config/crd/bases/solr.apache.org_solrbackups.yaml
@@ -1053,9 +1053,15 @@ spec:
                 type: object
               repositoryName:
                 description: The name of the repository to use for the backup.  Defaults to "legacy_local_repository" if not specified (the auto-configured repository for legacy singleton volumes).
+                maxLength: 100
+                minLength: 1
+                pattern: '[a-zA-Z0-9]([-_a-zA-Z0-9]*[a-zA-Z0-9])?'
                 type: string
               solrCloud:
                 description: A reference to the SolrCloud to create a backup for
+                maxLength: 63
+                minLength: 1
+                pattern: '[a-z0-9]([-a-z0-9]*[a-z0-9])?'
                 type: string
             required:
             - solrCloud
diff --git a/config/crd/bases/solr.apache.org_solrclouds.yaml b/config/crd/bases/solr.apache.org_solrclouds.yaml
index 4af3461..29ce391 100644
--- a/config/crd/bases/solr.apache.org_solrclouds.yaml
+++ b/config/crd/bases/solr.apache.org_solrclouds.yaml
@@ -1021,6 +1021,9 @@ spec:
                       type: object
                     name:
                       description: 'A name used to identify this local storage profile.  Values should follow RFC-1123.  (See here for more details: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names)'
+                      maxLength: 100
+                      minLength: 1
+                      pattern: '[a-zA-Z0-9]([-_a-zA-Z0-9]*[a-zA-Z0-9])?'
                       type: string
                     s3:
                       description: An S3Repository for Solr to use when backing up and restoring collections.
@@ -6927,6 +6930,11 @@ spec:
           status:
             description: SolrCloudStatus defines the observed state of SolrCloud
             properties:
+              backupRepositoriesAvailable:
+                additionalProperties:
+                  type: boolean
+                description: BackupRepositoriesAvailable lists the backupRepositories specified in the SolrCloud and whether they are available across all Pods.
+                type: object
               backupRestoreReady:
                 description: BackupRestoreReady announces whether the solrCloud has the backupRestorePVC mounted to all pods and therefore is ready for backups and restores.
                 type: boolean
@@ -6940,11 +6948,11 @@ spec:
                 description: PodSelector for SolrCloud pods, required by the HPA
                 type: string
               readyReplicas:
-                description: ReadyReplicas is the number of number of ready replicas in the cluster
+                description: ReadyReplicas is the number of ready replicas in the cluster
                 format: int32
                 type: integer
               replicas:
-                description: Replicas is the number of number of desired replicas in the cluster
+                description: Replicas is the number of desired replicas in the cluster
                 format: int32
                 type: integer
               solrNodes:
@@ -6986,7 +6994,7 @@ spec:
                 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:
-                description: UpToDateNodes is the number of number of Solr Node pods that are running the latest pod spec
+                description: UpToDateNodes is the number of Solr Node pods that are running the latest pod spec
                 format: int32
                 type: integer
               version:
diff --git a/controllers/solrbackup_controller.go b/controllers/solrbackup_controller.go
index cfee5d4..4e8d41b 100644
--- a/controllers/solrbackup_controller.go
+++ b/controllers/solrbackup_controller.go
@@ -20,7 +20,12 @@ package controllers
 import (
 	"context"
 	"fmt"
+	"k8s.io/apimachinery/pkg/fields"
 	"reflect"
+	"sigs.k8s.io/controller-runtime/pkg/builder"
+	"sigs.k8s.io/controller-runtime/pkg/handler"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
+	"sigs.k8s.io/controller-runtime/pkg/source"
 	"time"
 
 	"github.com/apache/solr-operator/controllers/util"
@@ -158,12 +163,10 @@ func (r *SolrBackupReconciler) reconcileSolrCloudBackup(ctx context.Context, bac
 			return solrCloud, actionTaken, err
 		}
 
-		// Make sure that all solr nodes are active and have the backupRestore shared volume mounted
-		// TODO: we do not need all replicas to be healthy. We should just check that leaders exist for all shards. (or just let Solr do that)
-		cloudReady := solrCloud.Status.BackupRestoreReady && (solrCloud.Status.Replicas == solrCloud.Status.ReadyReplicas)
-		if !cloudReady {
-			logger.Info("Cloud not ready for backup backup", "solrCloud", solrCloud.Name)
-			return solrCloud, actionTaken, errors.NewServiceUnavailable("Cloud is not ready for backups or restores")
+		// Make sure that all solr living Solr pods have the backupRepo configured
+		if !solrCloud.Status.BackupRepositoriesAvailable[backupRepository.Name] {
+			logger.Info("Cloud not ready for backup", "solrCloud", solrCloud.Name, "repository", backupRepository.Name)
+			return solrCloud, actionTaken, errors.NewServiceUnavailable(fmt.Sprintf("Cloud is not ready for backups in the %s repository", backupRepository.Name))
 		}
 
 		// Only set the solr version at the start of the backup. This shouldn't change throughout the backup.
@@ -173,7 +176,9 @@ func (r *SolrBackupReconciler) reconcileSolrCloudBackup(ctx context.Context, bac
 	// Go through each collection specified and reconcile the backup.
 	for _, collection := range backup.Spec.Collections {
 		// This will in-place update the CollectionBackupStatus in the backup object
-		_, err = reconcileSolrCollectionBackup(ctx, backup, solrCloud, backupRepository, collection, logger)
+		if _, err = reconcileSolrCollectionBackup(ctx, backup, solrCloud, backupRepository, collection, logger); err != nil {
+			break
+		}
 	}
 
 	// First check if the collection backups have been completed
@@ -198,7 +203,8 @@ func reconcileSolrCollectionBackup(ctx context.Context, backup *solrv1beta1.Solr
 	// If the collection backup hasn't started, start it
 	if !collectionBackupStatus.InProgress && !collectionBackupStatus.Finished {
 		// Start the backup by calling solr
-		started, err := util.StartBackupForCollection(ctx, solrCloud, backupRepository, backup, collection, logger)
+		var started bool
+		started, err = util.StartBackupForCollection(ctx, solrCloud, backupRepository, backup, collection, logger)
 		if err != nil {
 			return true, err
 		}
@@ -208,8 +214,10 @@ func reconcileSolrCollectionBackup(ctx context.Context, backup *solrv1beta1.Solr
 		}
 		collectionBackupStatus.BackupName = util.FullCollectionBackupName(collection, backup.Name)
 	} else if collectionBackupStatus.InProgress {
+		var successful bool
+		var asyncStatus string
 		// Check the state of the backup, when it is in progress, and update the state accordingly
-		finished, successful, asyncStatus, err := util.CheckBackupForCollection(ctx, solrCloud, collection, backup.Name, logger)
+		finished, successful, asyncStatus, err = util.CheckBackupForCollection(ctx, solrCloud, collection, backup.Name, logger)
 		if err != nil {
 			return false, err
 		}
@@ -240,11 +248,63 @@ func reconcileSolrCollectionBackup(ctx context.Context, backup *solrv1beta1.Solr
 }
 
 // SetupWithManager sets up the controller with the Manager.
-func (r *SolrBackupReconciler) SetupWithManager(mgr ctrl.Manager) error {
+func (r *SolrBackupReconciler) SetupWithManager(mgr ctrl.Manager) (err error) {
 	r.config = mgr.GetConfig()
 
-	return ctrl.NewControllerManagedBy(mgr).
+	ctrlBuilder := ctrl.NewControllerManagedBy(mgr).
 		For(&solrv1beta1.SolrBackup{}).
-		Owns(&batchv1.Job{}).
-		Complete(r)
+		Owns(&batchv1.Job{})
+
+	ctrlBuilder, err = r.indexAndWatchForSolrClouds(mgr, ctrlBuilder)
+	if err != nil {
+		return err
+	}
+
+	return ctrlBuilder.Complete(r)
+}
+
+func (r *SolrBackupReconciler) indexAndWatchForSolrClouds(mgr ctrl.Manager, ctrlBuilder *builder.Builder) (*builder.Builder, error) {
+	solrCloudField := ".spec.solrCloud"
+
+	if err := mgr.GetFieldIndexer().IndexField(context.Background(), &solrv1beta1.SolrBackup{}, solrCloudField, func(rawObj client.Object) []string {
+		// grab the SolrBackup object, extract the used SolrCloud...
+		return []string{rawObj.(*solrv1beta1.SolrBackup).Spec.SolrCloud}
+	}); err != nil {
+		return ctrlBuilder, err
+	}
+
+	return ctrlBuilder.Watches(
+		&source.Kind{Type: &solrv1beta1.SolrCloud{}},
+		handler.EnqueueRequestsFromMapFunc(func(obj client.Object) []reconcile.Request {
+			solrCloud := obj.(*solrv1beta1.SolrCloud)
+			foundBackups := &solrv1beta1.SolrBackupList{}
+			listOps := &client.ListOptions{
+				FieldSelector: fields.OneTermEqualSelector(solrCloudField, obj.GetName()),
+				Namespace:     obj.GetNamespace(),
+			}
+			err := r.List(context.Background(), foundBackups, listOps)
+			if err != nil {
+				// if no exporters found, just no-op this
+				return []reconcile.Request{}
+			}
+
+			requests := make([]reconcile.Request, 0)
+			for _, item := range foundBackups.Items {
+				// Only queue the request if the Cloud is ready.
+				cloudIsReady := solrCloud.Status.BackupRestoreReady
+				if item.Spec.RepositoryName != "" {
+					cloudIsReady = solrCloud.Status.BackupRepositoriesAvailable[item.Spec.RepositoryName]
+				}
+				if cloudIsReady {
+					requests = append(requests, reconcile.Request{
+						NamespacedName: types.NamespacedName{
+							Name:      item.GetName(),
+							Namespace: item.GetNamespace(),
+						},
+					})
+				}
+			}
+			return requests
+		}),
+		builder.WithPredicates(predicate.GenerationChangedPredicate{})), nil
 }
diff --git a/controllers/solrcloud_controller.go b/controllers/solrcloud_controller.go
index b854855..8d0153c 100644
--- a/controllers/solrcloud_controller.go
+++ b/controllers/solrcloud_controller.go
@@ -115,7 +115,7 @@ func (r *SolrCloudReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
 	newStatus := solrv1beta1.SolrCloudStatus{}
 
 	blockReconciliationOfStatefulSet := false
-	if err := r.reconcileZk(ctx, logger, instance, &newStatus); err != nil {
+	if err = r.reconcileZk(ctx, logger, instance, &newStatus); err != nil {
 		return requeueOrNot, err
 	}
 
@@ -511,9 +511,12 @@ func (r *SolrCloudReconciler) reconcileCloudStatus(ctx context.Context, solrClou
 		return outOfDatePods, outOfDatePodsNotStarted, availableUpdatedPodCount, err
 	}
 	newStatus.PodSelector = selector.String()
-	allPodsBackupReady := true
-	for idx, p := range foundPods.Items {
-		nodeNames[idx] = p.Name
+	backupReposAvailable := make(map[string]bool, len(solrCloud.Spec.BackupRepositories))
+	for _, repo := range solrCloud.Spec.BackupRepositories {
+		backupReposAvailable[repo.Name] = false
+	}
+	for podIdx, p := range foundPods.Items {
+		nodeNames[podIdx] = p.Name
 		nodeStatus := solrv1beta1.SolrNodeStatus{}
 		nodeStatus.Name = p.Name
 		nodeStatus.NodeName = p.Spec.NodeName
@@ -540,9 +543,10 @@ func (r *SolrCloudReconciler) reconcileCloudStatus(ctx context.Context, solrClou
 			newStatus.ReadyReplicas += 1
 		}
 
-		// Skip "backup-readiness" check for pod if we've already found a pod that's not ready
-		if allPodsBackupReady {
-			allPodsBackupReady = allPodsBackupReady && isPodReadyForBackup(&p, solrCloud)
+		// Merge BackupRepository availability for this pod
+		backupReposAvailableForPod := util.GetAvailableBackupRepos(&p)
+		for repo, availableSoFar := range backupReposAvailable {
+			backupReposAvailable[repo] = (availableSoFar || podIdx == 0) && backupReposAvailableForPod[repo]
 		}
 
 		// A pod is out of date if it's revision label is not equal to the statefulSetStatus' updateRevision.
@@ -581,10 +585,16 @@ func (r *SolrCloudReconciler) reconcileCloudStatus(ctx context.Context, solrClou
 	for idx, nodeName := range nodeNames {
 		newStatus.SolrNodes[idx] = nodeStatusMap[nodeName]
 	}
-	if allPodsBackupReady && len(foundPods.Items) > 0 {
-		newStatus.BackupRestoreReady = true
-	} else {
-		newStatus.BackupRestoreReady = false
+	if len(backupReposAvailable) > 0 {
+		newStatus.BackupRepositoriesAvailable = backupReposAvailable
+		allPodsBackupReady := len(backupReposAvailable) > 0
+		for _, backupRepo := range solrCloud.Spec.BackupRepositories {
+			allPodsBackupReady = allPodsBackupReady && backupReposAvailable[backupRepo.Name]
+			if !allPodsBackupReady {
+				break
+			}
+		}
+		newStatus.BackupRestoreReady = allPodsBackupReady
 	}
 
 	// If there are multiple versions of solr running, use the first otherVersion as the current running solr version of the cloud
@@ -605,18 +615,6 @@ func (r *SolrCloudReconciler) reconcileCloudStatus(ctx context.Context, solrClou
 	return outOfDatePods, outOfDatePodsNotStarted, availableUpdatedPodCount, nil
 }
 
-func isPodReadyForBackup(pod *corev1.Pod, solrCloud *solrv1beta1.SolrCloud) bool {
-	// If solrcloud doesn't request backup support then everything is 'ready' implicitly
-	if len(solrCloud.Spec.BackupRepositories) == 0 {
-		return false
-	}
-
-	// TODO: There is no way to possibly do this with the new S3 option.
-	// This is wrong, but not the end of the world.
-	// Replace with new functionality in https://github.com/apache/solr-operator/issues/326
-	return true
-}
-
 func (r *SolrCloudReconciler) reconcileNodeService(ctx context.Context, logger logr.Logger, instance *solrv1beta1.SolrCloud, nodeName string) (err error, ip string) {
 	// Generate Node Service
 	service := util.GenerateNodeService(instance, nodeName)
diff --git a/controllers/solrcloud_controller_backup_test.go b/controllers/solrcloud_controller_backup_test.go
index 55eef65..e70fe5c 100644
--- a/controllers/solrcloud_controller_backup_test.go
+++ b/controllers/solrcloud_controller_backup_test.go
@@ -75,6 +75,7 @@ var _ = FDescribe("SolrCloud controller - Backup Repositories", func() {
 					PodOptions: &solrv1beta1.PodOptions{
 						EnvVariables: extraVars,
 						Volumes:      extraVolumes,
+						Annotations:  testPodAnnotations,
 					},
 				},
 				BackupRepositories: []solrv1beta1.SolrBackupRepository{
@@ -98,7 +99,10 @@ var _ = FDescribe("SolrCloud controller - Backup Repositories", func() {
 
 			// Annotations for the solrxml configMap
 			solrXmlMd5 := fmt.Sprintf("%x", md5.Sum([]byte(configMap.Data[util.SolrXmlFile])))
-			Expect(statefulSet.Spec.Template.Annotations).To(HaveKeyWithValue(util.SolrXmlMd5Annotation, solrXmlMd5), "Wrong solr.xml MD5 annotation in the pod template!")
+			Expect(statefulSet.Spec.Template.Annotations).To(Equal(util.MergeLabelsOrAnnotations(testPodAnnotations, map[string]string{
+				"solr.apache.org/solrXmlMd5":          solrXmlMd5,
+				util.SolrBackupRepositoriesAnnotation: "test-repo",
+			})), "Incorrect pod annotations")
 
 			// Env Variable Tests
 			expectedEnvVars := map[string]string{
@@ -273,4 +277,52 @@ var _ = FDescribe("SolrCloud controller - Backup Repositories", func() {
 			})
 		})
 	})
+
+	FContext("Multiple Repositories - Annotations", func() {
+		BeforeEach(func() {
+			solrCloud.Spec = solrv1beta1.SolrCloudSpec{
+				ZookeeperRef: &solrv1beta1.ZookeeperRef{
+					ConnectionInfo: &solrv1beta1.ZookeeperConnectionInfo{
+						InternalConnectionString: "host:7271",
+					},
+				},
+				CustomSolrKubeOptions: solrv1beta1.CustomSolrKubeOptions{
+					PodOptions: &solrv1beta1.PodOptions{
+						EnvVariables: extraVars,
+						Volumes:      extraVolumes,
+					},
+				},
+				BackupRepositories: []solrv1beta1.SolrBackupRepository{
+					{
+						Name: "test-repo",
+						S3: &solrv1beta1.S3Repository{
+							Region: "test-region",
+							Bucket: "test-bucket",
+						},
+					},
+					{
+						Name: "another",
+						S3: &solrv1beta1.S3Repository{
+							Region: "test-region-2",
+							Bucket: "test-bucket-2",
+						},
+					},
+				},
+			}
+		})
+		FIt("has the correct resources", func() {
+			By("testing the Solr ConfigMap")
+			configMap := expectConfigMap(ctx, solrCloud, solrCloud.ConfigMapName(), map[string]string{"solr.xml": util.GenerateSolrXMLStringForCloud(solrCloud)})
+
+			By("testing the Solr StatefulSet with explicit volumes and envVars before adding S3Repo credentials")
+			// Make sure envVars and Volumes are correct be
+			statefulSet := expectStatefulSet(ctx, solrCloud, solrCloud.StatefulSetName())
+
+			// Annotations for the solrxml configMap
+			Expect(statefulSet.Spec.Template.Annotations).To(Equal(map[string]string{
+				"solr.apache.org/solrXmlMd5":          fmt.Sprintf("%x", md5.Sum([]byte(configMap.Data["solr.xml"]))),
+				util.SolrBackupRepositoriesAnnotation: "another,test-repo",
+			}), "Incorrect pod annotations")
+		})
+	})
 })
diff --git a/controllers/solrprometheusexporter_controller_test.go b/controllers/solrprometheusexporter_controller_test.go
index 7a6ae59..c9c8541 100644
--- a/controllers/solrprometheusexporter_controller_test.go
+++ b/controllers/solrprometheusexporter_controller_test.go
@@ -190,8 +190,9 @@ var _ = FDescribe("SolrPrometheusExporter controller - General", func() {
 			Expect(deployment.Labels).To(Equal(util.MergeLabelsOrAnnotations(expectedDeploymentLabels, testDeploymentLabels)), "Incorrect deployment labels")
 			Expect(deployment.Annotations).To(Equal(testDeploymentAnnotations), "Incorrect deployment annotations")
 			Expect(deployment.Spec.Template.ObjectMeta.Labels).To(Equal(util.MergeLabelsOrAnnotations(expectedDeploymentLabels, testPodLabels)), "Incorrect pod labels")
-			testPodAnnotations[util.PrometheusExporterConfigXmlMd5Annotation] = fmt.Sprintf("%x", md5.Sum([]byte(testExporterConfig)))
-			Expect(deployment.Spec.Template.ObjectMeta.Annotations).To(Equal(testPodAnnotations), "Incorrect pod annotations")
+			Expect(deployment.Spec.Template.ObjectMeta.Annotations).To(Equal(util.MergeLabelsOrAnnotations(testPodAnnotations, map[string]string{
+				util.PrometheusExporterConfigXmlMd5Annotation: fmt.Sprintf("%x", md5.Sum([]byte(testExporterConfig))),
+			})), "Incorrect pod annotations")
 
 			Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(len(extraContainers1)+1), "Wrong number of containers for the Deployment")
 			Expect(deployment.Spec.Template.Spec.Containers[1:]).To(Equal(extraContainers1), "Incorrect sidecar containers")
diff --git a/controllers/util/solr_api/cluster_status.go b/controllers/util/solr_api/cluster_status.go
index a1768e5..ac39bc7 100644
--- a/controllers/util/solr_api/cluster_status.go
+++ b/controllers/util/solr_api/cluster_status.go
@@ -17,73 +17,75 @@
 
 package solr_api
 
+import "k8s.io/apimachinery/pkg/util/intstr"
+
 type SolrOverseerStatusResponse struct {
 	ResponseHeader SolrResponseHeader `json:"responseHeader"`
 
 	// +optional
-	Leader string `json:"leader"`
+	Leader string `json:"leader,omitempty"`
 
 	// +optional
-	QueueSize int `json:"overseer_queue_size"`
+	QueueSize int `json:"overseer_queue_size,omitempty"`
 
 	// +optional
-	WorkQueueSize int `json:"overseer_work_queue_size"`
+	WorkQueueSize int `json:"overseer_work_queue_size,omitempty"`
 
 	// +optional
-	CollectionQueueSize int `json:"overseer_collection_queue_size"`
+	CollectionQueueSize int `json:"overseer_collection_queue_size,omitempty"`
 }
 
 type SolrClusterStatusResponse struct {
 	ResponseHeader SolrResponseHeader `json:"responseHeader"`
 
 	// +optional
-	ClusterStatus SolrClusterStatus `json:"cluster"`
+	ClusterStatus SolrClusterStatus `json:"cluster,omitempty"`
 }
 
 type SolrClusterStatus struct {
 	// +optional
-	Collections map[string]SolrCollectionStatus `json:"collections"`
+	Collections map[string]SolrCollectionStatus `json:"collections,omitempty"`
 
 	// +optional
-	Aliases map[string]string `json:"aliases"`
+	Aliases map[string]string `json:"aliases,omitempty"`
 
 	// +optional
-	Roles map[string][]string `json:"roles"`
+	Roles map[string][]string `json:"roles,omitempty"`
 
 	// +optional
-	LiveNodes []string `json:"live_nodes"`
+	LiveNodes []string `json:"live_nodes,omitempty"`
 }
 
 type SolrCollectionStatus struct {
 	// +optional
-	Shards map[string]SolrShardStatus `json:"shards"`
+	Shards map[string]SolrShardStatus `json:"shards,omitempty"`
 
 	// +optional
-	ConfigName string `json:"configName"`
+	ConfigName string `json:"configName,omitempty"`
 
 	// +optional
-	ZnodeVersion string `json:"znodeVersion"`
+	ZnodeVersion intstr.IntOrString `json:"znodeVersion,omitempty"`
 
 	// +optional
-	AutoAddReplicas string `json:"autoAddReplicas"`
+	AutoAddReplicas string `json:"autoAddReplicas,omitempty"`
 
 	// +optional
-	NrtReplicas int `json:"nrtReplicas"`
+	NrtReplicas intstr.IntOrString `json:"nrtReplicas,omitempty"`
 
 	// +optional
-	TLogReplicas int `json:"tlogReplicas"`
+	TLogReplicas intstr.IntOrString `json:"tlogReplicas,omitempty"`
 
 	// +optional
-	PullReplicas int `json:"pullReplicas"`
+	PullReplicas intstr.IntOrString `json:"pullReplicas,omitempty"`
 
 	// +optional
-	MaxShardsPerNode string `json:"maxShardsPerNode"`
+	MaxShardsPerNode intstr.IntOrString `json:"maxShardsPerNode,omitempty"`
 
 	// +optional
-	ReplicationFactor string `json:"replicationFactor"`
+	ReplicationFactor intstr.IntOrString `json:"replicationFactor,omitempty"`
 
 	// +optional
-	Router SolrCollectionRouter `json:"router"`
+	Router SolrCollectionRouter `json:"router,omitempty"`
 }
 
 type SolrCollectionRouter struct {
@@ -92,13 +94,13 @@ type SolrCollectionRouter struct {
 
 type SolrShardStatus struct {
 	// +optional
-	Replicas map[string]SolrReplicaStatus `json:"replicas"`
+	Replicas map[string]SolrReplicaStatus `json:"replicas,omitempty"`
 
 	// +optional
-	Range string `json:"range"`
+	Range string `json:"range,omitempty"`
 
 	// +optional
-	State SolrShardState `json:"state"`
+	State SolrShardState `json:"state,omitempty"`
 }
 
 type SolrShardState string
@@ -120,7 +122,7 @@ type SolrReplicaStatus struct {
 	Leader bool `json:"leader,string"`
 
 	// +optional
-	Type SolrReplicaType `json:"type"`
+	Type SolrReplicaType `json:"type,omitempty"`
 }
 
 type SolrReplicaState string
diff --git a/controllers/util/solr_backup_repo_util.go b/controllers/util/solr_backup_repo_util.go
index ced61cf..55f34f6 100644
--- a/controllers/util/solr_backup_repo_util.go
+++ b/controllers/util/solr_backup_repo_util.go
@@ -30,6 +30,8 @@ const (
 
 	GCSCredentialSecretKey = "service-account-key.json"
 	S3CredentialFileName   = "credentials"
+
+	SolrBackupRepositoriesAnnotation = "solr.apache.org/backupRepositories"
 )
 
 func RepoVolumeName(repo *solrv1beta1.SolrBackupRepository) string {
@@ -229,3 +231,29 @@ func BackupLocationPath(repo *solrv1beta1.SolrBackupRepository, backupLocation s
 	}
 	return backupLocation
 }
+
+func GetAvailableBackupRepos(pod *corev1.Pod) (repos map[string]bool) {
+	if availableRepos, hasAny := pod.Annotations[SolrBackupRepositoriesAnnotation]; hasAny {
+		repoNames := strings.Split(availableRepos, ",")
+		repos = make(map[string]bool, len(repoNames))
+		for _, repoName := range repoNames {
+			repos[repoName] = true
+		}
+	}
+	return
+}
+
+func SetAvailableBackupRepos(solrCloud *solrv1beta1.SolrCloud, podAnnotations map[string]string) map[string]string {
+	if len(solrCloud.Spec.BackupRepositories) > 0 {
+		if podAnnotations == nil {
+			podAnnotations = make(map[string]string, 1)
+		}
+		repoNames := make([]string, len(solrCloud.Spec.BackupRepositories))
+		for idx, repo := range solrCloud.Spec.BackupRepositories {
+			repoNames[idx] = repo.Name
+		}
+		sort.Strings(repoNames)
+		podAnnotations[SolrBackupRepositoriesAnnotation] = strings.Join(repoNames, ",")
+	}
+	return podAnnotations
+}
diff --git a/controllers/util/solr_util.go b/controllers/util/solr_util.go
index 5b6b41b..9b122bc 100644
--- a/controllers/util/solr_util.go
+++ b/controllers/util/solr_util.go
@@ -211,6 +211,8 @@ func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCl
 			backupEnvVars = append(backupEnvVars, repoEnvVars...)
 		}
 	}
+	// Add annotation specifying the backupRepositories available with this version of the Pod.
+	podAnnotations = SetAvailableBackupRepos(solrCloud, podAnnotations)
 
 	if nil != customPodOptions {
 		// Add Custom Volumes to pod
diff --git a/example/test_backup_gcs.yaml b/example/test_backup_gcs.yaml
index f3e4ee3..778fabc 100644
--- a/example/test_backup_gcs.yaml
+++ b/example/test_backup_gcs.yaml
@@ -21,7 +21,7 @@ metadata:
   name: main-collection-gcs-backups
   namespace: default
 spec:
-  repositoryName: main_collection_backup_repository
-  solrCloud: multiple-backup-repositories-solr
+  repositoryName: "main_collection_backup_repository"
+  solrCloud: multiple-backup-repos
   collections:
     - example
diff --git a/example/test_backup_managed.yaml b/example/test_backup_managed.yaml
index 3364c49..f66bd10 100644
--- a/example/test_backup_managed.yaml
+++ b/example/test_backup_managed.yaml
@@ -21,6 +21,6 @@ metadata:
   namespace: default
 spec:
   repositoryName: "managed_repository_1"
-  solrCloud: multiple-backup-repositories-solr
+  solrCloud: multiple-backup-repos
   collections:
     - example
diff --git a/example/test_solrcloud_backuprepos.yaml b/example/test_solrcloud_backuprepos.yaml
index 8de031c..6cb2922 100644
--- a/example/test_solrcloud_backuprepos.yaml
+++ b/example/test_solrcloud_backuprepos.yaml
@@ -16,7 +16,7 @@
 apiVersion: solr.apache.org/v1beta1
 kind: SolrCloud
 metadata:
-  name: multiple-backup-repositories-solr
+  name: multiple-backup-repos
 spec:
   replicas: 1
   solrImage:
@@ -54,19 +54,18 @@ spec:
         region: "us-west-2"
         bucket: "product-catalog"
         credentials:
-          credentials:
-            accessKeyIdSecret: # Optional
-              name: aws-secrets
-              key: access-key-id
-            secretAccessKeySecret: # Optional
-              name: aws-secrets
-              key: secret-access-key
-            sessionTokenSecret: # Optional
-              name: aws-secrets
-              key: session-token
-            credentialsFileSecret: # Optional
-              name: aws-credentials
-              key: credentials
+          accessKeyIdSecret: # Optional
+            name: aws-secrets
+            key: access-key-id
+          secretAccessKeySecret: # Optional
+            name: aws-secrets
+            key: secret-access-key
+          sessionTokenSecret: # Optional
+            name: aws-secrets
+            key: session-token
+          credentialsFileSecret: # Optional
+            name: aws-credentials
+            key: credentials
 
     - name: "main_collection_backup_repository_log"
       gcs:
diff --git a/helm/solr-operator/Chart.yaml b/helm/solr-operator/Chart.yaml
index 4f9bd1b..257cb62 100644
--- a/helm/solr-operator/Chart.yaml
+++ b/helm/solr-operator/Chart.yaml
@@ -176,6 +176,13 @@ annotations:
           url: https://github.com/apache/solr-operator/issues/352
         - name: Github PR
           url: https://github.com/apache/solr-operator/pull/361
+    - kind: added
+      description: Separate SolrCloud backup ready status by backup repository
+      links:
+        - name: Github Issue
+          url: https://github.com/apache/solr-operator/issues/326
+        - name: Github PR
+          url: https://github.com/apache/solr-operator/pull/358
   artifacthub.io/images: |
     - name: solr-operator
       image: apache/solr-operator:v0.5.0-prerelease
diff --git a/helm/solr-operator/crds/crds.yaml b/helm/solr-operator/crds/crds.yaml
index a2d4ccd..6ff1bd1 100644
--- a/helm/solr-operator/crds/crds.yaml
+++ b/helm/solr-operator/crds/crds.yaml
@@ -1053,9 +1053,15 @@ spec:
                 type: object
               repositoryName:
                 description: The name of the repository to use for the backup.  Defaults to "legacy_local_repository" if not specified (the auto-configured repository for legacy singleton volumes).
+                maxLength: 100
+                minLength: 1
+                pattern: '[a-zA-Z0-9]([-_a-zA-Z0-9]*[a-zA-Z0-9])?'
                 type: string
               solrCloud:
                 description: A reference to the SolrCloud to create a backup for
+                maxLength: 63
+                minLength: 1
+                pattern: '[a-z0-9]([-a-z0-9]*[a-z0-9])?'
                 type: string
             required:
             - solrCloud
@@ -2155,6 +2161,9 @@ spec:
                       type: object
                     name:
                       description: 'A name used to identify this local storage profile.  Values should follow RFC-1123.  (See here for more details: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names)'
+                      maxLength: 100
+                      minLength: 1
+                      pattern: '[a-zA-Z0-9]([-_a-zA-Z0-9]*[a-zA-Z0-9])?'
                       type: string
                     s3:
                       description: An S3Repository for Solr to use when backing up and restoring collections.
@@ -8061,6 +8070,11 @@ spec:
           status:
             description: SolrCloudStatus defines the observed state of SolrCloud
             properties:
+              backupRepositoriesAvailable:
+                additionalProperties:
+                  type: boolean
+                description: BackupRepositoriesAvailable lists the backupRepositories specified in the SolrCloud and whether they are available across all Pods.
+                type: object
               backupRestoreReady:
                 description: BackupRestoreReady announces whether the solrCloud has the backupRestorePVC mounted to all pods and therefore is ready for backups and restores.
                 type: boolean
@@ -8074,11 +8088,11 @@ spec:
                 description: PodSelector for SolrCloud pods, required by the HPA
                 type: string
               readyReplicas:
-                description: ReadyReplicas is the number of number of ready replicas in the cluster
+                description: ReadyReplicas is the number of ready replicas in the cluster
                 format: int32
                 type: integer
               replicas:
-                description: Replicas is the number of number of desired replicas in the cluster
+                description: Replicas is the number of desired replicas in the cluster
                 format: int32
                 type: integer
               solrNodes:
@@ -8120,7 +8134,7 @@ spec:
                 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:
-                description: UpToDateNodes is the number of number of Solr Node pods that are running the latest pod spec
+                description: UpToDateNodes is the number of Solr Node pods that are running the latest pod spec
                 format: int32
                 type: integer
               version: