You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by th...@apache.org on 2021/01/21 01:04:01 UTC

[lucene-solr-operator] branch main updated: Override Prometheus exporter config XML via a user-supplied ConfigMap (#189)

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

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


The following commit(s) were added to refs/heads/main by this push:
     new 376d19a  Override Prometheus exporter config XML via a user-supplied ConfigMap (#189)
376d19a is described below

commit 376d19ac869fd1ac123804550119aba35c65434a
Author: Timothy Potter <th...@gmail.com>
AuthorDate: Wed Jan 20 18:03:55 2021 -0700

    Override Prometheus exporter config XML via a user-supplied ConfigMap (#189)
---
 controllers/solrprometheusexporter_controller.go   |  91 ++++++++++++++-
 .../solrprometheusexporter_controller_test.go      | 123 ++++++++++++++++++++-
 controllers/util/prometheus_exporter_util.go       |  32 ++++--
 3 files changed, 231 insertions(+), 15 deletions(-)

diff --git a/controllers/solrprometheusexporter_controller.go b/controllers/solrprometheusexporter_controller.go
index e422458..a453bc6 100644
--- a/controllers/solrprometheusexporter_controller.go
+++ b/controllers/solrprometheusexporter_controller.go
@@ -19,18 +19,25 @@ package controllers
 
 import (
 	"context"
+	"crypto/md5"
+	"fmt"
 	solrv1beta1 "github.com/apache/lucene-solr-operator/api/v1beta1"
 	"github.com/apache/lucene-solr-operator/controllers/util"
 	"github.com/go-logr/logr"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/fields"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
 	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/builder"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+	"sigs.k8s.io/controller-runtime/pkg/handler"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+	"sigs.k8s.io/controller-runtime/pkg/source"
 )
 
 // SolrPrometheusExporterReconciler reconciles a SolrPrometheusExporter object
@@ -78,6 +85,29 @@ func (r *SolrPrometheusExporterReconciler) Reconcile(req ctrl.Request) (ctrl.Res
 		return ctrl.Result{Requeue: true}, nil
 	}
 
+	configMapKey := util.PrometheusExporterConfigMapKey
+	configXmlMd5 := ""
+	if prometheusExporter.Spec.Config == "" && prometheusExporter.Spec.CustomKubeOptions.ConfigMapOptions != nil && prometheusExporter.Spec.CustomKubeOptions.ConfigMapOptions.ProvidedConfigMap != "" {
+		foundConfigMap := &corev1.ConfigMap{}
+		err = r.Get(context.TODO(), types.NamespacedName{Name: prometheusExporter.Spec.CustomKubeOptions.ConfigMapOptions.ProvidedConfigMap, Namespace: prometheusExporter.Namespace}, foundConfigMap)
+		if err != nil {
+			return ctrl.Result{}, err
+		}
+
+		if foundConfigMap.Data != nil {
+			configXml, ok := foundConfigMap.Data[configMapKey]
+			if ok {
+				configXmlMd5 = fmt.Sprintf("%x", md5.Sum([]byte(configXml)))
+			} else {
+				return ctrl.Result{}, fmt.Errorf("required '%s' key not found in provided ConfigMap %s",
+					configMapKey, prometheusExporter.Spec.CustomKubeOptions.ConfigMapOptions.ProvidedConfigMap)
+			}
+		} else {
+			return ctrl.Result{}, fmt.Errorf("provided ConfigMap %s has no data",
+				prometheusExporter.Spec.CustomKubeOptions.ConfigMapOptions.ProvidedConfigMap)
+		}
+	}
+
 	if prometheusExporter.Spec.Config != "" {
 		// Generate ConfigMap
 		configMap := util.GenerateMetricsConfigMap(prometheusExporter)
@@ -85,6 +115,11 @@ func (r *SolrPrometheusExporterReconciler) Reconcile(req ctrl.Request) (ctrl.Res
 			return ctrl.Result{}, err
 		}
 
+		// capture the MD5 for the default config XML, otherwise we already computed it above
+		if configXmlMd5 == "" {
+			configXmlMd5 = fmt.Sprintf("%x", md5.Sum([]byte(configMap.Data[configMapKey])))
+		}
+
 		// Check if the ConfigMap already exists
 		configMapLogger := logger.WithValues("configMap", configMap.Name)
 		foundConfigMap := &corev1.ConfigMap{}
@@ -130,7 +165,7 @@ func (r *SolrPrometheusExporterReconciler) Reconcile(req ctrl.Request) (ctrl.Res
 		return ctrl.Result{}, err
 	}
 
-	deploy := util.GenerateSolrPrometheusExporterDeployment(prometheusExporter, solrConnectionInfo)
+	deploy := util.GenerateSolrPrometheusExporterDeployment(prometheusExporter, solrConnectionInfo, configXmlMd5)
 	if err := controllerutil.SetControllerReference(prometheusExporter, deploy, r.scheme); err != nil {
 		return ctrl.Result{}, err
 	}
@@ -195,6 +230,60 @@ func (r *SolrPrometheusExporterReconciler) SetupWithManagerAndReconciler(mgr ctr
 		Owns(&corev1.Service{}).
 		Owns(&appsv1.Deployment{})
 
+	var err error
+	ctrlBuilder, err = r.indexAndWatchForProvidedConfigMaps(mgr, ctrlBuilder)
+	if err != nil {
+		return err
+	}
+
 	r.scheme = mgr.GetScheme()
 	return ctrlBuilder.Complete(reconciler)
 }
+
+func (r *SolrPrometheusExporterReconciler) indexAndWatchForProvidedConfigMaps(mgr ctrl.Manager, ctrlBuilder *builder.Builder) (*builder.Builder, error) {
+	providedConfigMapField := ".spec.customKubeOptions.configMapOptions.providedConfigMap"
+
+	if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &solrv1beta1.SolrPrometheusExporter{}, providedConfigMapField, func(rawObj runtime.Object) []string {
+		// grab the SolrCloud object, extract the used configMap...
+		exporter := rawObj.(*solrv1beta1.SolrPrometheusExporter)
+		if exporter.Spec.CustomKubeOptions.ConfigMapOptions == nil {
+			return nil
+		}
+		if exporter.Spec.CustomKubeOptions.ConfigMapOptions.ProvidedConfigMap == "" {
+			return nil
+		}
+		// ...and if so, return it
+		return []string{exporter.Spec.CustomKubeOptions.ConfigMapOptions.ProvidedConfigMap}
+	}); err != nil {
+		return ctrlBuilder, err
+	}
+
+	return ctrlBuilder.Watches(
+		&source.Kind{Type: &corev1.ConfigMap{}},
+		&handler.EnqueueRequestsFromMapFunc{
+			ToRequests: handler.ToRequestsFunc(func(a handler.MapObject) []reconcile.Request {
+				foundExporters := &solrv1beta1.SolrPrometheusExporterList{}
+				listOps := &client.ListOptions{
+					FieldSelector: fields.OneTermEqualSelector(providedConfigMapField, a.Meta.GetName()),
+					Namespace:     a.Meta.GetNamespace(),
+				}
+				err := r.List(context.TODO(), foundExporters, listOps)
+				if err != nil {
+					// if no exporters found, just no-op this
+					return []reconcile.Request{}
+				}
+
+				requests := make([]reconcile.Request, len(foundExporters.Items))
+				for i, item := range foundExporters.Items {
+					requests[i] = reconcile.Request{
+						NamespacedName: types.NamespacedName{
+							Name:      item.GetName(),
+							Namespace: item.GetNamespace(),
+						},
+					}
+				}
+				return requests
+			}),
+		},
+		builder.WithPredicates(predicate.ResourceVersionChangedPredicate{})), nil
+}
diff --git a/controllers/solrprometheusexporter_controller_test.go b/controllers/solrprometheusexporter_controller_test.go
index 4f11b2d..f24fd19 100644
--- a/controllers/solrprometheusexporter_controller_test.go
+++ b/controllers/solrprometheusexporter_controller_test.go
@@ -18,20 +18,22 @@
 package controllers
 
 import (
-	corev1 "k8s.io/api/core/v1"
-	"testing"
-
+	"crypto/md5"
+	"fmt"
 	solr "github.com/apache/lucene-solr-operator/api/v1beta1"
 	"github.com/apache/lucene-solr-operator/controllers/util"
 	"github.com/onsi/gomega"
 	"github.com/stretchr/testify/assert"
 	"golang.org/x/net/context"
+	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/manager"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+	"testing"
 )
 
 var _ reconcile.Reconciler = &SolrPrometheusExporterReconciler{}
@@ -191,7 +193,7 @@ func TestMetricsReconcileWithExporterConfig(t *testing.T) {
 	defer testClient.Delete(context.TODO(), instance)
 	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedMetricsRequest)))
 
-	configMap := expectConfigMap(t, g, requests, expectedMetricsRequest, metricsCMKey, map[string]string{"solr-prometheus-exporter.xml": testExporterConfig})
+	configMap := expectConfigMap(t, g, requests, expectedMetricsRequest, metricsCMKey, map[string]string{util.PrometheusExporterConfigMapKey: testExporterConfig})
 	testMapsEqual(t, "configMap labels", util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), testConfigMapLabels), configMap.Labels)
 	testMapsEqual(t, "configMap annotations", testConfigMapAnnotations, configMap.Annotations)
 
@@ -200,6 +202,8 @@ func TestMetricsReconcileWithExporterConfig(t *testing.T) {
 	testMapsEqual(t, "deployment labels", util.MergeLabelsOrAnnotations(expectedDeploymentLabels, testDeploymentLabels), deployment.Labels)
 	testMapsEqual(t, "deployment annotations", testDeploymentAnnotations, deployment.Annotations)
 	testMapsEqual(t, "pod labels", util.MergeLabelsOrAnnotations(expectedDeploymentLabels, testPodLabels), deployment.Spec.Template.ObjectMeta.Labels)
+	expectedMd5 := fmt.Sprintf("%x", md5.Sum([]byte(testExporterConfig)))
+	testPodAnnotations[util.PrometheusExporterConfigXmlMd5Annotation] = expectedMd5
 	testMapsEqual(t, "pod annotations", testPodAnnotations, deployment.Spec.Template.ObjectMeta.Annotations)
 	assert.EqualValues(t, testPriorityClass, deployment.Spec.Template.Spec.PriorityClassName, "Incorrect Priority class name for Pod Spec")
 
@@ -306,7 +310,7 @@ func TestMetricsReconcileWithGivenZkAcls(t *testing.T) {
 	defer testClient.Delete(context.TODO(), instance)
 	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedMetricsRequest)))
 
-	configMap := expectConfigMap(t, g, requests, expectedMetricsRequest, metricsCMKey, map[string]string{"solr-prometheus-exporter.xml": testExporterConfig})
+	configMap := expectConfigMap(t, g, requests, expectedMetricsRequest, metricsCMKey, map[string]string{util.PrometheusExporterConfigMapKey: testExporterConfig})
 	testMapsEqual(t, "configMap labels", util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), testConfigMapLabels), configMap.Labels)
 	testMapsEqual(t, "configMap annotations", testConfigMapAnnotations, configMap.Annotations)
 
@@ -315,6 +319,9 @@ func TestMetricsReconcileWithGivenZkAcls(t *testing.T) {
 	testMapsEqual(t, "deployment labels", util.MergeLabelsOrAnnotations(expectedDeploymentLabels, testDeploymentLabels), deployment.Labels)
 	testMapsEqual(t, "deployment annotations", testDeploymentAnnotations, deployment.Annotations)
 	testMapsEqual(t, "pod labels", util.MergeLabelsOrAnnotations(expectedDeploymentLabels, testPodLabels), deployment.Spec.Template.ObjectMeta.Labels)
+
+	expectedMd5 := fmt.Sprintf("%x", md5.Sum([]byte(testExporterConfig)))
+	testPodAnnotations[util.PrometheusExporterConfigXmlMd5Annotation] = expectedMd5
 	testMapsEqual(t, "pod annotations", testPodAnnotations, deployment.Spec.Template.ObjectMeta.Annotations)
 
 	// Env Variable Tests
@@ -489,7 +496,7 @@ func TestMetricsReconcileWithSolrZkAcls(t *testing.T) {
 	defer testClient.Delete(context.TODO(), instance)
 	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedMetricsRequest)))
 
-	configMap := expectConfigMap(t, g, requests, expectedMetricsRequest, metricsCMKey, map[string]string{"solr-prometheus-exporter.xml": testExporterConfig})
+	configMap := expectConfigMap(t, g, requests, expectedMetricsRequest, metricsCMKey, map[string]string{util.PrometheusExporterConfigMapKey: testExporterConfig})
 	testMapsEqual(t, "configMap labels", instance.SharedLabelsWith(instance.Labels), configMap.Labels)
 
 	deployment := expectDeployment(t, g, requests, expectedMetricsRequest, metricsDKey, configMap.Name)
@@ -562,3 +569,107 @@ func TestMetricsReconcileWithSolrZkAcls(t *testing.T) {
 	testMapsEqual(t, "service labels", expectedServiceLabels, service.Labels)
 	testMapsEqual(t, "service annotations", expectedServiceAnnotations, service.Annotations)
 }
+
+func TestMetricsReconcileWithUserProvidedConfig(t *testing.T) {
+	g := gomega.NewGomegaWithT(t)
+
+	// Setup the Manager and Controller.  Wrap the Controller Reconcile function so it writes each request to a
+	// channel when it is finished.
+	mgr, err := manager.New(testCfg, manager.Options{})
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	testClient = mgr.GetClient()
+
+	solrPrometheusExporterReconciler := &SolrPrometheusExporterReconciler{
+		Client: testClient,
+		Log:    ctrl.Log.WithName("controllers").WithName("SolrPrometheusExporter"),
+	}
+	newRec, requests := SetupTestReconcile(solrPrometheusExporterReconciler)
+	g.Expect(solrPrometheusExporterReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+	stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+	defer func() {
+		close(stopMgr)
+		mgrStopped.Wait()
+	}()
+
+	cleanupTest(g, expectedMetricsRequest.Namespace)
+
+	// configure the exporter to pull config from a user-provided ConfigMap instead of the default
+	withUserProvidedConfigMapName := "custom-exporter-config"
+	instance := &solr.SolrPrometheusExporter{
+		ObjectMeta: metav1.ObjectMeta{Name: expectedMetricsRequest.Name, Namespace: expectedMetricsRequest.Namespace},
+		Spec: solr.SolrPrometheusExporterSpec{
+			CustomKubeOptions: solr.CustomExporterKubeOptions{
+				ConfigMapOptions: &solr.ConfigMapOptions{
+					ProvidedConfigMap: withUserProvidedConfigMapName,
+				},
+			},
+		},
+	}
+
+	// Create the SolrPrometheusExporter object and expect the Reconcile and Deployment to be created
+	err = testClient.Create(context.TODO(), instance)
+	// The instance object may not be a valid object because it might be missing some required fields.
+	// Please modify the instance object by adding required fields and then remove the following if statement.
+	if apierrors.IsInvalid(err) {
+		t.Logf("failed to create object, got an invalid object error: %v", err)
+		return
+	}
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	defer testClient.Delete(context.TODO(), instance)
+
+	// reconcile is happening but it can't proceed b/c the user-provided configmap doesn't exist
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedMetricsRequest)))
+
+	// create the user-provided ConfigMap but w/o the expected key
+	userProvidedConfigXml := "<config/>"
+	userProvidedConfigMap := &corev1.ConfigMap{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      withUserProvidedConfigMapName,
+			Namespace: expectedMetricsRequest.Namespace,
+		},
+		Data: map[string]string{
+			"foo": userProvidedConfigXml,
+		},
+	}
+	err = testClient.Create(context.TODO(), userProvidedConfigMap)
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	userProvidedConfigMapNN := types.NamespacedName{Name: userProvidedConfigMap.Name, Namespace: userProvidedConfigMap.Namespace}
+
+	// can't proceed b/c the user-provided ConfigMap is invalid
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedMetricsRequest)))
+
+	// update the config to fix the error
+	updateUserProvidedConfigMap(testClient, g, userProvidedConfigMapNN, map[string]string{util.PrometheusExporterConfigMapKey: userProvidedConfigXml})
+
+	// reconcile should pass now
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedMetricsRequest)))
+
+	deployment := expectDeployment(t, g, requests, expectedMetricsRequest, metricsDKey, userProvidedConfigMap.Name)
+	expectedAnnotations := map[string]string{
+		util.PrometheusExporterConfigXmlMd5Annotation: fmt.Sprintf("%x", md5.Sum([]byte(userProvidedConfigXml))),
+	}
+	testMapsEqual(t, "pod annotations", expectedAnnotations, deployment.Spec.Template.ObjectMeta.Annotations)
+
+	// update the user-provided ConfigMap to trigger reconcile on the deployment
+	updatedConfigXml := "<config>updated by user</config>"
+	updateUserProvidedConfigMap(testClient, g, userProvidedConfigMapNN, map[string]string{util.PrometheusExporterConfigMapKey: updatedConfigXml})
+
+	// reconcile should happen again
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedMetricsRequest)))
+
+	deployment = expectDeployment(t, g, requests, expectedMetricsRequest, metricsDKey, userProvidedConfigMap.Name)
+	expectedAnnotations = map[string]string{
+		util.PrometheusExporterConfigXmlMd5Annotation: fmt.Sprintf("%x", md5.Sum([]byte(updatedConfigXml))),
+	}
+	testMapsEqual(t, "pod annotations", expectedAnnotations, deployment.Spec.Template.ObjectMeta.Annotations)
+}
+
+func updateUserProvidedConfigMap(testClient client.Client, g *gomega.GomegaWithT, userProvidedConfigMapNN types.NamespacedName, dataMap map[string]string) {
+	foundConfigMap := &corev1.ConfigMap{}
+	g.Eventually(func() error { return testClient.Get(context.TODO(), userProvidedConfigMapNN, foundConfigMap) }, timeout).Should(gomega.Succeed())
+	foundConfigMap.Data = dataMap
+	err := testClient.Update(context.TODO(), foundConfigMap)
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+}
diff --git a/controllers/util/prometheus_exporter_util.go b/controllers/util/prometheus_exporter_util.go
index e7c7cb2..58e13ab 100644
--- a/controllers/util/prometheus_exporter_util.go
+++ b/controllers/util/prometheus_exporter_util.go
@@ -34,7 +34,9 @@ const (
 	SolrMetricsPortName = "solr-metrics"
 	ExtSolrMetricsPort  = 80
 
-	DefaultPrometheusExporterEntrypoint = "/opt/solr/contrib/prometheus-exporter/bin/solr-exporter"
+	DefaultPrometheusExporterEntrypoint      = "/opt/solr/contrib/prometheus-exporter/bin/solr-exporter"
+	PrometheusExporterConfigMapKey           = "solr-prometheus-exporter.xml"
+	PrometheusExporterConfigXmlMd5Annotation = "solr.apache.org/exporterConfigXmlMd5"
 )
 
 // SolrConnectionInfo defines how to connect to a cloud or standalone solr instance.
@@ -46,7 +48,7 @@ type SolrConnectionInfo struct {
 
 // GenerateSolrPrometheusExporterDeployment returns a new appsv1.Deployment pointer generated for the SolrCloud Prometheus Exporter instance
 // solrPrometheusExporter: SolrPrometheusExporter instance
-func GenerateSolrPrometheusExporterDeployment(solrPrometheusExporter *solr.SolrPrometheusExporter, solrConnectionInfo SolrConnectionInfo) *appsv1.Deployment {
+func GenerateSolrPrometheusExporterDeployment(solrPrometheusExporter *solr.SolrPrometheusExporter, solrConnectionInfo SolrConnectionInfo, configXmlMd5 string) *appsv1.Deployment {
 	gracePeriodTerm := int64(10)
 	singleReplica := int32(1)
 	fsGroup := int64(SolrMetricsPort)
@@ -103,18 +105,23 @@ func GenerateSolrPrometheusExporterDeployment(solrPrometheusExporter *solr.SolrP
 	}
 
 	// Only add the config if it is passed in from the user. Otherwise, use the default.
-	if solrPrometheusExporter.Spec.Config != "" {
+	if solrPrometheusExporter.Spec.Config != "" ||
+		(solrPrometheusExporter.Spec.CustomKubeOptions.ConfigMapOptions != nil && solrPrometheusExporter.Spec.CustomKubeOptions.ConfigMapOptions.ProvidedConfigMap != "") {
+		configMapName := solrPrometheusExporter.MetricsConfigMapName()
+		if solrPrometheusExporter.Spec.CustomKubeOptions.ConfigMapOptions != nil && solrPrometheusExporter.Spec.CustomKubeOptions.ConfigMapOptions.ProvidedConfigMap != "" {
+			configMapName = solrPrometheusExporter.Spec.CustomKubeOptions.ConfigMapOptions.ProvidedConfigMap
+		}
 		solrVolumes = []corev1.Volume{{
 			Name: "solr-prometheus-exporter-xml",
 			VolumeSource: corev1.VolumeSource{
 				ConfigMap: &corev1.ConfigMapVolumeSource{
 					LocalObjectReference: corev1.LocalObjectReference{
-						Name: solrPrometheusExporter.MetricsConfigMapName(),
+						Name: configMapName,
 					},
 					Items: []corev1.KeyToPath{
 						{
-							Key:  "solr-prometheus-exporter.xml",
-							Path: "solr-prometheus-exporter.xml",
+							Key:  PrometheusExporterConfigMapKey,
+							Path: PrometheusExporterConfigMapKey,
 						},
 					},
 				},
@@ -123,7 +130,7 @@ func GenerateSolrPrometheusExporterDeployment(solrPrometheusExporter *solr.SolrP
 
 		volumeMounts = []corev1.VolumeMount{{Name: "solr-prometheus-exporter-xml", MountPath: "/opt/solr-exporter", ReadOnly: true}}
 
-		exporterArgs = append(exporterArgs, "-f", "/opt/solr-exporter/solr-prometheus-exporter.xml")
+		exporterArgs = append(exporterArgs, "-f", "/opt/solr-exporter/"+PrometheusExporterConfigMapKey)
 	} else {
 		exporterArgs = append(exporterArgs, "-f", "/opt/solr/contrib/prometheus-exporter/conf/solr-exporter-config.xml")
 	}
@@ -201,6 +208,15 @@ func GenerateSolrPrometheusExporterDeployment(solrPrometheusExporter *solr.SolrP
 		}
 	}
 
+	// track the MD5 of the custom exporter config in the pod spec annotations,
+	// so we get a rolling restart when the configMap changes
+	if configXmlMd5 != "" {
+		if podAnnotations == nil {
+			podAnnotations = make(map[string]string, 1)
+		}
+		podAnnotations[PrometheusExporterConfigXmlMd5Annotation] = configXmlMd5
+	}
+
 	deployment := &appsv1.Deployment{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:        solrPrometheusExporter.MetricsDeploymentName(),
@@ -290,7 +306,7 @@ func GenerateMetricsConfigMap(solrPrometheusExporter *solr.SolrPrometheusExporte
 			Annotations: annotations,
 		},
 		Data: map[string]string{
-			"solr-prometheus-exporter.xml": solrPrometheusExporter.Spec.Config,
+			PrometheusExporterConfigMapKey: solrPrometheusExporter.Spec.Config,
 		},
 	}
 	return configMap