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/28 22:44:49 UTC
[lucene-solr-operator] branch main updated: Support custom log4j2
config from user-provided ConfigMap (#193)
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 864ed23 Support custom log4j2 config from user-provided ConfigMap (#193)
864ed23 is described below
commit 864ed2369fc92b441537307238e7df82fbc3573d
Author: Timothy Potter <th...@gmail.com>
AuthorDate: Thu Jan 28 15:44:39 2021 -0700
Support custom log4j2 config from user-provided ConfigMap (#193)
---
controllers/solrcloud_controller.go | 56 ++++---
controllers/solrcloud_controller_test.go | 266 +++++++++++++++++++++++++++++--
controllers/suite_test.go | 4 +
controllers/util/solr_util.go | 59 ++++++-
docs/solr-cloud/solr-cloud-crd.md | 117 +++++++++++++-
5 files changed, 466 insertions(+), 36 deletions(-)
diff --git a/controllers/solrcloud_controller.go b/controllers/solrcloud_controller.go
index 9245deb..8505bd7 100644
--- a/controllers/solrcloud_controller.go
+++ b/controllers/solrcloud_controller.go
@@ -182,44 +182,62 @@ func (r *SolrCloudReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
}
}
- // Generate ConfigMap unless the user supplied a custom ConfigMap for solr.xml ... but the provided ConfigMap
- // might be for the Prometheus exporter, so we only care if they provide a solr.xml in the CM
- solrXmlConfigMapName := instance.ConfigMapName()
- solrXmlMd5 := ""
+ // Generate ConfigMap unless the user supplied a custom ConfigMap for solr.xml
+ configMapInfo := make(map[string]string)
if instance.Spec.CustomSolrKubeOptions.ConfigMapOptions != nil && instance.Spec.CustomSolrKubeOptions.ConfigMapOptions.ProvidedConfigMap != "" {
+ providedConfigMapName := instance.Spec.CustomSolrKubeOptions.ConfigMapOptions.ProvidedConfigMap
foundConfigMap := &corev1.ConfigMap{}
- nn := types.NamespacedName{Name: instance.Spec.CustomSolrKubeOptions.ConfigMapOptions.ProvidedConfigMap, Namespace: instance.Namespace}
+ nn := types.NamespacedName{Name: providedConfigMapName, Namespace: instance.Namespace}
err = r.Get(context.TODO(), nn, foundConfigMap)
if err != nil {
return requeueOrNot, err // if they passed a providedConfigMap name, then it must exist
}
- // ConfigMap doesn't have to have a solr.xml, but if it does, then it needs to be valid!
if foundConfigMap.Data != nil {
- solrXml, ok := foundConfigMap.Data["solr.xml"]
- if ok {
+ logXml, hasLogXml := foundConfigMap.Data[util.LogXmlFile]
+ solrXml, hasSolrXml := foundConfigMap.Data[util.SolrXmlFile]
+
+ // if there's a user-provided config, it must have one of the expected keys
+ if !hasLogXml && !hasSolrXml {
+ // TODO: Create event for the CRD.
+ return requeueOrNot, fmt.Errorf("User provided ConfigMap %s must have one of 'solr.xml' and/or 'log4j2.xml'",
+ providedConfigMapName)
+ }
+
+ if hasSolrXml {
+ // make sure the user-provided solr.xml is valid
if !strings.Contains(solrXml, "${hostPort:") {
return requeueOrNot,
fmt.Errorf("Custom solr.xml in ConfigMap %s must contain a placeholder for the 'hostPort' variable, such as <int name=\"hostPort\">${hostPort:80}</int>",
- instance.Spec.CustomSolrKubeOptions.ConfigMapOptions.ProvidedConfigMap)
+ providedConfigMapName)
}
// stored in the pod spec annotations on the statefulset so that we get a restart when solr.xml changes
- solrXmlMd5 = fmt.Sprintf("%x", md5.Sum([]byte(solrXml)))
- solrXmlConfigMapName = foundConfigMap.Name
- } else {
- return requeueOrNot, fmt.Errorf("Required 'solr.xml' key not found in provided ConfigMap %s",
- instance.Spec.CustomSolrKubeOptions.ConfigMapOptions.ProvidedConfigMap)
+ configMapInfo[util.SolrXmlMd5Annotation] = fmt.Sprintf("%x", md5.Sum([]byte(solrXml)))
+ configMapInfo[util.SolrXmlFile] = foundConfigMap.Name
+ }
+
+ if hasLogXml {
+ if !strings.Contains(logXml, "monitorInterval=") {
+ // stored in the pod spec annotations on the statefulset so that we get a restart when the log config changes
+ configMapInfo[util.LogXmlMd5Annotation] = fmt.Sprintf("%x", md5.Sum([]byte(logXml)))
+ } // else log4j will automatically refresh for us, so no restart needed
+ configMapInfo[util.LogXmlFile] = foundConfigMap.Name
}
+
} else {
- return requeueOrNot, fmt.Errorf("Provided ConfigMap %s has no data",
- instance.Spec.CustomSolrKubeOptions.ConfigMapOptions.ProvidedConfigMap)
+ return requeueOrNot, fmt.Errorf("Provided ConfigMap %s has no data", providedConfigMapName)
}
- } else {
+ }
+
+ if configMapInfo[util.SolrXmlFile] == "" {
+ // no user provided solr.xml, so create the default
configMap := util.GenerateConfigMap(instance)
if err := controllerutil.SetControllerReference(instance, configMap, r.scheme); err != nil {
return requeueOrNot, err
}
- solrXmlMd5 = fmt.Sprintf("%x", md5.Sum([]byte(configMap.Data["solr.xml"])))
+
+ configMapInfo[util.SolrXmlMd5Annotation] = fmt.Sprintf("%x", md5.Sum([]byte(configMap.Data[util.SolrXmlFile])))
+ configMapInfo[util.SolrXmlFile] = configMap.Name
// Check if the ConfigMap already exists
configMapLogger := logger.WithValues("configMap", configMap.Name)
@@ -248,7 +266,7 @@ func (r *SolrCloudReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
if !blockReconciliationOfStatefulSet {
// Generate StatefulSet
- statefulSet := util.GenerateStatefulSet(instance, &newStatus, hostNameIpMap, solrXmlConfigMapName, solrXmlMd5)
+ statefulSet := util.GenerateStatefulSet(instance, &newStatus, hostNameIpMap, configMapInfo)
if err := controllerutil.SetControllerReference(instance, statefulSet, r.scheme); err != nil {
return requeueOrNot, err
}
diff --git a/controllers/solrcloud_controller_test.go b/controllers/solrcloud_controller_test.go
index db3527a..1a8f2e8 100644
--- a/controllers/solrcloud_controller_test.go
+++ b/controllers/solrcloud_controller_test.go
@@ -540,7 +540,7 @@ func TestDefaults(t *testing.T) {
foundConfigMap := &corev1.ConfigMap{}
err = testClient.Get(context.TODO(), types.NamespacedName{Name: instance.ConfigMapName(), Namespace: instance.Namespace}, foundConfigMap)
- solrXmlMd5 := fmt.Sprintf("%x", md5.Sum([]byte(foundConfigMap.Data["solr.xml"])))
+ solrXmlMd5 := fmt.Sprintf("%x", md5.Sum([]byte(foundConfigMap.Data[util.SolrXmlFile])))
assert.True(t, stateful.Spec.Template.Annotations[util.SolrXmlMd5Annotation] == solrXmlMd5,
"solr.xml MD5 annotation should be set on the pod template!")
}
@@ -659,11 +659,6 @@ func TestCloudWithCustomSolrXmlConfigMapReconcile(t *testing.T) {
instance := &solr.SolrCloud{
ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
Spec: solr.SolrCloudSpec{
- ZookeeperRef: &solr.ZookeeperRef{
- ProvidedZookeeper: &solr.ZookeeperSpec{
- ChRoot: "solr-xml",
- },
- },
CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
ConfigMapOptions: &solr.ConfigMapOptions{
ProvidedConfigMap: testCustomSolrXmlConfigMap,
@@ -716,7 +711,7 @@ func TestCloudWithCustomSolrXmlConfigMapReconcile(t *testing.T) {
g.Expect(err).NotTo(gomega.HaveOccurred())
g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
- invalidConfigMap.Data["solr.xml"] = "Invalid XML"
+ invalidConfigMap.Data[util.SolrXmlFile] = "Invalid XML"
err = testClient.Update(context.TODO(), invalidConfigMap)
g.Expect(err).NotTo(gomega.HaveOccurred())
g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
@@ -759,18 +754,269 @@ func TestCloudWithCustomSolrXmlConfigMapReconcile(t *testing.T) {
// update the embedded solr.xml to trigger a pod rolling restart
foundConfigMap = &corev1.ConfigMap{}
err = testClient.Get(context.TODO(), types.NamespacedName{Name: testCustomSolrXmlConfigMap, Namespace: instance.Namespace}, foundConfigMap)
- updateSolrXml := foundConfigMap.Data["solr.xml"]
- foundConfigMap.Data["solr.xml"] = strings.Replace(updateSolrXml, "${zkClientTimeout:30000}", "${zkClientTimeout:15000}", 1)
+ updateSolrXml := foundConfigMap.Data[util.SolrXmlFile]
+ foundConfigMap.Data[util.SolrXmlFile] = strings.Replace(updateSolrXml, "${zkClientTimeout:30000}", "${zkClientTimeout:15000}", 1)
err = testClient.Update(context.TODO(), foundConfigMap)
g.Expect(err).NotTo(gomega.HaveOccurred())
g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
// Check the annotation on the pod template to make sure a rolling restart will take place
- updateSolrXml = foundConfigMap.Data["solr.xml"]
+ updateSolrXml = foundConfigMap.Data[util.SolrXmlFile]
updateSolrXmlMd5 := fmt.Sprintf("%x", md5.Sum([]byte(updateSolrXml)))
time.Sleep(time.Millisecond * 250)
err = testClient.Get(context.TODO(), cloudSsKey, stateful)
g.Expect(err).NotTo(gomega.HaveOccurred())
assert.Equal(t, updateSolrXmlMd5, stateful.Spec.Template.Annotations[util.SolrXmlMd5Annotation], "Custom solr.xml MD5 annotation should be updated on the pod template.")
}
+
+func TestCloudWithUserProvidedLogConfigMapReconcile(t *testing.T) {
+ UseZkCRD(true)
+ g := gomega.NewGomegaWithT(t)
+
+ testCustomLogXmlConfigMap := "my-custom-log4j2-xml"
+
+ instance := &solr.SolrCloud{
+ ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+ Spec: solr.SolrCloudSpec{
+ CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
+ ConfigMapOptions: &solr.ConfigMapOptions{
+ ProvidedConfigMap: testCustomLogXmlConfigMap,
+ },
+ },
+ },
+ }
+
+ // 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()
+
+ solrCloudReconciler := &SolrCloudReconciler{
+ Client: testClient,
+ Log: ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+ }
+ newRec, requests := SetupTestReconcile(solrCloudReconciler)
+ g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+ stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+ defer func() {
+ close(stopMgr)
+ mgrStopped.Wait()
+ }()
+
+ cleanupTest(g, instance.Namespace)
+
+ // Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+ err = testClient.Create(context.TODO(), instance)
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ defer testClient.Delete(context.TODO(), instance)
+
+ // reconcile will have failed b/c the provided ConfigMap doesn't exist ...
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+ userProvidedConfigMap := &corev1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: testCustomLogXmlConfigMap,
+ Namespace: instance.Namespace,
+ },
+ Data: map[string]string{
+ util.LogXmlFile: "<Configuration/>",
+ },
+ }
+ err = testClient.Create(context.TODO(), userProvidedConfigMap)
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ defer testClient.Delete(context.TODO(), userProvidedConfigMap)
+
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+ g.Eventually(func() error { return testClient.Get(context.TODO(), expectedCloudRequest.NamespacedName, instance) }, timeout).Should(gomega.Succeed())
+ emptyRequests(requests)
+
+ stateful := &appsv1.StatefulSet{}
+ g.Eventually(func() error { return testClient.Get(context.TODO(), cloudSsKey, stateful) }, timeout).Should(gomega.Succeed())
+ assert.NotEmpty(t, stateful.Spec.Template.Annotations[util.LogXmlMd5Annotation], "Custom log XML MD5 annotation should be set on the pod template!")
+
+ assert.NotNil(t, stateful.Spec.Template.Spec.Volumes)
+ var logXmlVol *corev1.Volume = nil
+ for _, vol := range stateful.Spec.Template.Spec.Volumes {
+ if vol.Name == "log4j2-xml" {
+ logXmlVol = &vol
+ break
+ }
+ }
+ assert.NotNil(t, logXmlVol, "Didn't find custom log4j2-xml volume in sts config!")
+ assert.NotNil(t, logXmlVol.VolumeSource.ConfigMap, "log4j2-xml Volume should have a ConfigMap source")
+ assert.Equal(t, logXmlVol.VolumeSource.ConfigMap.Name, testCustomLogXmlConfigMap, "log4j2-xml Volume should have a ConfigMap source")
+
+ var logXmlVolMount *corev1.VolumeMount = nil
+ for _, mount := range stateful.Spec.Template.Spec.Containers[0].VolumeMounts {
+ if mount.Name == "log4j2-xml" {
+ logXmlVolMount = &mount
+ break
+ }
+ }
+ assert.NotNil(t, logXmlVolMount, "Didn't find the log4j2-xml Volume mount")
+ expectedMountPath := fmt.Sprintf("/var/solr/%s", testCustomLogXmlConfigMap)
+ assert.Equal(t, expectedMountPath, logXmlVolMount.MountPath, "Custom log4j2.xml mount path is incorrect.")
+
+ solrXmlConfigName := fmt.Sprintf("%s-solrcloud-configmap", instance.GetName())
+ foundConfigMap := &corev1.ConfigMap{}
+ err = testClient.Get(context.TODO(), types.NamespacedName{Name: solrXmlConfigName, Namespace: instance.Namespace}, foundConfigMap)
+ g.Expect(err).NotTo(gomega.HaveOccurred(), "Built-in solr.xml ConfigMap should still exist! ")
+
+ emptyRequests(requests)
+ // update the user-provided log XML to trigger a pod rolling restart
+ updatedXml := "<Configuration>Updated!</Configuration>"
+ foundConfigMap = &corev1.ConfigMap{}
+ err = testClient.Get(context.TODO(), types.NamespacedName{Name: testCustomLogXmlConfigMap, Namespace: instance.Namespace}, foundConfigMap)
+ foundConfigMap.Data[util.LogXmlFile] = updatedXml
+ err = testClient.Update(context.TODO(), foundConfigMap)
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ // capture all reconcile requests
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+ // Check the annotation on the pod template to make sure a rolling restart will take place
+ updateLogXmlMd5 := fmt.Sprintf("%x", md5.Sum([]byte(updatedXml)))
+ time.Sleep(time.Millisecond * 250)
+ err = testClient.Get(context.TODO(), cloudSsKey, stateful)
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ assert.Equal(t, updateLogXmlMd5, stateful.Spec.Template.Annotations[util.LogXmlMd5Annotation], "Custom log XML MD5 annotation should be updated on the pod template.")
+ expectedEnvVars := map[string]string{"LOG4J_PROPS": fmt.Sprintf("%s/%s", expectedMountPath, util.LogXmlFile)}
+ testPodEnvVariables(t, expectedEnvVars, stateful.Spec.Template.Spec.Containers[0].Env)
+
+ // switch the log4j config over to using monitorInterval, so no more rolling restart on change to config
+ emptyRequests(requests)
+ // update the user-provided log XML to trigger a pod rolling restart
+ updatedXml = "<Configuration monitorInterval=\"30\">Updated!</Configuration>"
+ foundConfigMap = &corev1.ConfigMap{}
+ err = testClient.Get(context.TODO(), types.NamespacedName{Name: testCustomLogXmlConfigMap, Namespace: instance.Namespace}, foundConfigMap)
+ foundConfigMap.Data[util.LogXmlFile] = updatedXml
+ err = testClient.Update(context.TODO(), foundConfigMap)
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+ time.Sleep(time.Millisecond * 250)
+ err = testClient.Get(context.TODO(), cloudSsKey, stateful)
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ // log xml md5 annotation should no longer be on the pod template
+ assert.Equal(t, "", stateful.Spec.Template.Annotations[util.LogXmlMd5Annotation], "Custom log XML MD5 annotation should not be set on the pod template.")
+}
+
+func TestCloudWithUserProvidedSolrXmlAndLogConfigReconcile(t *testing.T) {
+ UseZkCRD(true)
+ g := gomega.NewGomegaWithT(t)
+
+ testCustomConfigMap := "my-custom-config-xml"
+
+ instance := &solr.SolrCloud{
+ ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+ Spec: solr.SolrCloudSpec{
+ CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
+ ConfigMapOptions: &solr.ConfigMapOptions{
+ ProvidedConfigMap: testCustomConfigMap,
+ },
+ },
+ },
+ }
+
+ // 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()
+
+ solrCloudReconciler := &SolrCloudReconciler{
+ Client: testClient,
+ Log: ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+ }
+ newRec, requests := SetupTestReconcile(solrCloudReconciler)
+ g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+ stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+ defer func() {
+ close(stopMgr)
+ mgrStopped.Wait()
+ }()
+
+ cleanupTest(g, instance.Namespace)
+
+ // Create the user-provided ConfigMap first to streamline reconcile,
+ // other tests cover creating it after the reconcile loop starts on the SolrCloud
+ userProvidedConfigMap := &corev1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: testCustomConfigMap,
+ Namespace: instance.Namespace,
+ },
+ Data: map[string]string{
+ util.LogXmlFile: "<Configuration/>",
+ util.SolrXmlFile: "<solr> ${hostPort: </solr>", // the controller checks for ${hostPort: in the solr.xml
+ },
+ }
+ err = testClient.Create(context.TODO(), userProvidedConfigMap)
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ defer testClient.Delete(context.TODO(), userProvidedConfigMap)
+
+ // Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+ err = testClient.Create(context.TODO(), instance)
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ defer testClient.Delete(context.TODO(), instance)
+
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+ g.Eventually(func() error { return testClient.Get(context.TODO(), expectedCloudRequest.NamespacedName, instance) }, timeout).Should(gomega.Succeed())
+ emptyRequests(requests)
+
+ stateful := &appsv1.StatefulSet{}
+ g.Eventually(func() error { return testClient.Get(context.TODO(), cloudSsKey, stateful) }, timeout).Should(gomega.Succeed())
+ assert.NotEmpty(t, stateful.Spec.Template.Annotations[util.LogXmlMd5Annotation], "Custom log XML MD5 annotation should be set on the pod template!")
+
+ assert.NotNil(t, stateful.Spec.Template.Spec.Volumes)
+ var customConfigVol *corev1.Volume = nil
+ for _, vol := range stateful.Spec.Template.Spec.Volumes {
+ if vol.Name == "solr-xml" {
+ customConfigVol = &vol
+ break
+ }
+ }
+ assert.NotNil(t, customConfigVol, "Didn't find custom solr-xml volume in sts config!")
+ assert.NotNil(t, customConfigVol.VolumeSource.ConfigMap, "solr-xml Volume should have a ConfigMap source")
+ assert.Equal(t, customConfigVol.VolumeSource.ConfigMap.Name, testCustomConfigMap, "solr-xml Volume should have a ConfigMap source")
+
+ var logXmlVolMount *corev1.VolumeMount = nil
+ for _, mount := range stateful.Spec.Template.Spec.Containers[0].VolumeMounts {
+ if mount.Name == "solr-xml" {
+ logXmlVolMount = &mount
+ break
+ }
+ }
+ assert.NotNil(t, logXmlVolMount, "Didn't find the log4j2-xml Volume mount")
+ expectedMountPath := fmt.Sprintf("/var/solr/%s", testCustomConfigMap)
+ assert.Equal(t, expectedMountPath, logXmlVolMount.MountPath)
+
+ solrXmlConfigName := fmt.Sprintf("%s-solrcloud-configmap", instance.GetName())
+ foundConfigMap := &corev1.ConfigMap{}
+ err = testClient.Get(context.TODO(), types.NamespacedName{Name: solrXmlConfigName, Namespace: instance.Namespace}, foundConfigMap)
+ g.Expect(err).To(gomega.HaveOccurred(), "Built-in solr.xml ConfigMap should NOT exist! ")
+
+ emptyRequests(requests)
+
+ customSolrXml := userProvidedConfigMap.Data[util.SolrXmlFile]
+ expectedSolrXmlMd5 := fmt.Sprintf("%x", md5.Sum([]byte(customSolrXml)))
+ assert.Equal(t, expectedSolrXmlMd5, stateful.Spec.Template.Annotations[util.SolrXmlMd5Annotation], "Custom solr.xml MD5 annotation should be set on the pod template.")
+
+ customLogXml := userProvidedConfigMap.Data[util.LogXmlFile]
+ expectedLogXmlMd5 := fmt.Sprintf("%x", md5.Sum([]byte(customLogXml)))
+ assert.Equal(t, expectedLogXmlMd5, stateful.Spec.Template.Annotations[util.LogXmlMd5Annotation], "Custom log4j2.xml MD5 annotation should be set on the pod template.")
+ expectedEnvVars := map[string]string{"LOG4J_PROPS": fmt.Sprintf("%s/%s", expectedMountPath, util.LogXmlFile)}
+ testPodEnvVariables(t, expectedEnvVars, stateful.Spec.Template.Spec.Containers[0].Env)
+}
diff --git a/controllers/suite_test.go b/controllers/suite_test.go
index 05efe38..1015fa9 100644
--- a/controllers/suite_test.go
+++ b/controllers/suite_test.go
@@ -18,6 +18,7 @@
package controllers
import (
+ "fmt"
stdlog "log"
"os"
"path/filepath"
@@ -76,6 +77,9 @@ func SetupTestReconcile(inner reconcile.Reconciler) (reconcile.Reconciler, chan
requests := make(chan reconcile.Request)
fn := reconcile.Func(func(req reconcile.Request) (reconcile.Result, error) {
result, err := inner.Reconcile(req)
+ if err != nil {
+ fmt.Printf("\n\nReconcile Error: %s\n\n", err)
+ }
requests <- req
return result, err
})
diff --git a/controllers/util/solr_util.go b/controllers/util/solr_util.go
index ae4a288..e6e8df6 100644
--- a/controllers/util/solr_util.go
+++ b/controllers/util/solr_util.go
@@ -48,6 +48,9 @@ const (
SolrCloudPVCDataStorage = "data"
SolrPVCInstanceLabel = "solr.apache.org/instance"
SolrXmlMd5Annotation = "solr.apache.org/solrXmlMd5"
+ SolrXmlFile = "solr.xml"
+ LogXmlMd5Annotation = "solr.apache.org/logXmlMd5"
+ LogXmlFile = "log4j2.xml"
DefaultStatefulSetPodManagementPolicy = appsv1.ParallelPodManagement
@@ -75,7 +78,7 @@ const (
// replicas: the number of replicas for the SolrCloud instance
// storage: the size of the storage for the SolrCloud instance (e.g. 100Gi)
// zkConnectionString: the connectionString of the ZK instance to connect to
-func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCloudStatus, hostNameIPs map[string]string, solrXmlConfigMapName string, solrXmlMd5 string) *appsv1.StatefulSet {
+func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCloudStatus, hostNameIPs map[string]string, configMapInfo map[string]string) *appsv1.StatefulSet {
gracePeriodTerm := int64(10)
solrPodPort := solrCloud.Spec.SolrAddressability.PodPort
fsGroup := int64(DefaultSolrGroup)
@@ -124,12 +127,12 @@ func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCl
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
- Name: solrXmlConfigMapName,
+ Name: configMapInfo[SolrXmlFile],
},
Items: []corev1.KeyToPath{
{
- Key: "solr.xml",
- Path: "solr.xml",
+ Key: SolrXmlFile,
+ Path: SolrXmlFile,
},
},
DefaultMode: &defaultMode,
@@ -327,13 +330,57 @@ func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCl
envVars = append(envVars, customPodOptions.EnvVariables...)
}
+ // Did the user provide a custom log config?
+ if configMapInfo[LogXmlFile] != "" {
+
+ if configMapInfo[LogXmlMd5Annotation] != "" {
+ if podAnnotations == nil {
+ podAnnotations = make(map[string]string, 1)
+ }
+ podAnnotations[LogXmlMd5Annotation] = configMapInfo[LogXmlMd5Annotation]
+ }
+
+ // cannot use /var/solr as a mountPath, so mount the custom log config
+ // in a sub-dir named after the user-provided ConfigMap
+ volName := "log4j2-xml"
+ mountPath := fmt.Sprintf("/var/solr/%s", configMapInfo[LogXmlFile])
+ log4jPropsEnvVarPath := fmt.Sprintf("%s/%s", mountPath, LogXmlFile)
+ appendedToExisting := false
+ if configMapInfo[LogXmlFile] == configMapInfo[SolrXmlFile] {
+ // the user provided a custom log4j2.xml and solr.xml, append to the volume for solr.xml created above
+ for _, vol := range solrVolumes {
+ if vol.Name == "solr-xml" {
+ vol.ConfigMap.Items = append(vol.ConfigMap.Items, corev1.KeyToPath{Key: LogXmlFile, Path: LogXmlFile})
+ appendedToExisting = true
+ volName = vol.Name
+ break
+ }
+ }
+ }
+
+ if !appendedToExisting {
+ solrVolumes = append(solrVolumes, corev1.Volume{
+ Name: volName,
+ VolumeSource: corev1.VolumeSource{
+ ConfigMap: &corev1.ConfigMapVolumeSource{
+ LocalObjectReference: corev1.LocalObjectReference{Name: configMapInfo[LogXmlFile]},
+ Items: []corev1.KeyToPath{{Key: LogXmlFile, Path: LogXmlFile}},
+ DefaultMode: &defaultMode,
+ },
+ },
+ })
+ }
+ volumeMounts = append(volumeMounts, corev1.VolumeMount{Name: volName, MountPath: mountPath})
+ envVars = append(envVars, corev1.EnvVar{Name: "LOG4J_PROPS", Value: log4jPropsEnvVarPath})
+ }
+
// track the MD5 of the custom solr.xml in the pod spec annotations,
// so we get a rolling restart when the configMap changes
- if solrXmlMd5 != "" {
+ if configMapInfo[SolrXmlMd5Annotation] != "" {
if podAnnotations == nil {
podAnnotations = make(map[string]string, 1)
}
- podAnnotations[SolrXmlMd5Annotation] = solrXmlMd5
+ podAnnotations[SolrXmlMd5Annotation] = configMapInfo[SolrXmlMd5Annotation]
}
if solrCloud.Spec.SolrOpts != "" {
diff --git a/docs/solr-cloud/solr-cloud-crd.md b/docs/solr-cloud/solr-cloud-crd.md
index 3f21116..d6ac2af 100644
--- a/docs/solr-cloud/solr-cloud-crd.md
+++ b/docs/solr-cloud/solr-cloud-crd.md
@@ -127,4 +127,119 @@ the solr-operator can manage Zookeeper ensemble(s) for you.
Using the [zookeeper-operator](https://github.com/pravega/zookeeper-operator), a new Zookeeper ensemble can be spun up for
each solrCloud that has this option specified.
-The startup parameter `zookeeper-operator` must be provided on startup of the solr-operator for this parameter to be available.
\ No newline at end of file
+The startup parameter `zookeeper-operator` must be provided on startup of the solr-operator for this parameter to be available.
+
+## Override Built-in Solr Configuration Files
+
+The Solr operator deploys well-configured SolrCloud instances with minimal input required from human operators.
+As such, the operator installs various configuration files automatically, including `solr.xml` for node-level settings and `log4j2.xml` for logging.
+However, there may come a time when you need to override the built-in configuration files with custom settings.
+
+In general, users can provide custom config files by providing a ConfigMap in the same namespace as the SolrCloud instance;
+all custom config files should be stored in the same user-provided ConfigMap under different keys.
+Point your SolrCloud definition to a user-provided ConfigMap using the following structure:
+```
+spec:
+ ...
+ customSolrKubeOptions:
+ configMapOptions:
+ providedConfigMap: <Custom-ConfigMap-Here>
+```
+
+### Custom solr.xml
+
+Solr pods load node-level configuration settings from `/var/solr/data/solr.xml`.
+This important configuration file gets created by the `cp-solr-xml` initContainer which bootstraps the `solr.home` directory on each pod before starting the main container.
+The default `solr.xml` is mounted into the `cp-solr-xml` initContainer from a ConfigMap named `<INSTANCE>-solrcloud-configmap` (where `<INSTANCE>` is the name of your SolrCloud instance) created by the Solr operator.
+
+_Note: The data in the default ConfigMap is not editable! Any changes to the `solr.xml` in the default ConfigMap created by the operator will be overwritten during the next reconcile cycle._
+
+Many of the specific values in `solr.xml` can be set using Java system properties; for instance, the following setting controls the read timeout for the HTTP client used by Solr's `HttpShardHandlerFactory`:
+```
+<int name="socketTimeout">${socketTimeout:600000}</int>
+```
+The `${socketTimeout:600000}` syntax means pull the value from a Java system property named `socketTimeout` with default `600000` if not set.
+
+You can set Java system properties using the `solrOpts` string in your SolrCloud definition, such as:
+```
+spec:
+ solrOpts: -DsocketTimeout=300000
+```
+This same approach works for a number of settings in `solrconfig.xml` as well.
+
+However, if you need to customize `solr.xml` beyond what can be accomplished with Java system properties,
+then you need to supply your own `solr.xml` in a ConfigMap in the same namespace where you deploy your SolrCloud instance.
+Provide your custom XML in the ConfigMap using `solr.xml` as the key as shown in the example below:
+```
+---
+kind: ConfigMap
+apiVersion: v1
+metadata:
+ name: custom-solr-xml
+data:
+ solr.xml: |
+ <?xml version="1.0" encoding="UTF-8" ?>
+ <solr>
+ ... CUSTOM CONFIG HERE ...
+ </solr>
+```
+**Important: Your custom `solr.xml` must include `<int name="hostPort">${hostPort:0}</int>` as the operator relies on this element to set the port Solr pods advertise to ZooKeeper. If this element is missing, then your Solr pods will not be created.**
+
+You can get the default `solr.xml` from a Solr pod as a starting point for creating a custom config using `kubectl cp` as shown in the example below:
+```
+SOLR_POD_ID=$(kubectl get pod -l technology=solr-cloud --no-headers -o custom-columns=":metadata.name" | head -1)
+kubectl cp $SOLR_POD_ID:/var/solr/data/solr.xml ./custom-solr.xml
+```
+This copies the default config from the first Solr pod found in the namespace and names it `custom-solr.xml`. Customize the settings in `custom-solr.xml` as needed and then create a ConfigMap using YAML.
+
+_Note: Using `kubectl create configmap --from-file` scrambles the XML formatting, so we recommend defining the configmap YAML as shown above to keep the XML formatted properly._
+
+Point your SolrCloud instance at the custom ConfigMap using:
+```
+ customSolrKubeOptions:
+ configMapOptions:
+ providedConfigMap: custom-solr-xml
+```
+_Note: If you set `providedConfigMap`, then the ConfigMap must include the `solr.xml` or `log4j2.xml` key, otherwise the SolrCloud will fail to reconcile._
+
+#### Changes to Custom Config Trigger Rolling Restarts
+
+The Solr operator stores the MD5 hash of your custom XML in the StatefulSet's pod spec annotations (`spec.template.metadata.annotations`). To see the current annotations for your Solr pods, you can do:
+```
+kubectl annotate pod -l technology=solr-cloud --list=true
+```
+If the custom `solr.xml` changes in the user-provided ConfigMap, then the operator triggers a rolling restart of Solr pods to apply the updated configuration settings automatically.
+
+To summarize, if you need to customize `solr.xml`, provide your own version in a ConfigMap and changes made to the XML in the ConfigMap are automatically applied to your Solr pods.
+
+### Custom Log Configuration
+
+By default, the Solr Docker image configures Solr to load its log configuration from `/var/solr/log4j2.xml`.
+If you need to fine-tune the log configuration, then you can provide a custom `log4j2.xml` in a ConfigMap using the same basic process as described in the previous section for customizing `solr.xml`. If supplied, the operator overrides the log config using the `LOG4J_PROPS` env var.
+
+As with custom `solr.xml`, the operator can track the MD5 hash of your `log4j2.xml` in the pod spec annotations to trigger a rolling restart if the log config changes.
+However, Log4j2 supports hot reloading of log configuration using the `monitorInterval` attribute on the root `<Configuration>` element. For more information on this, see: [Log4j Automatic Reconfiguration](https://logging.apache.org/log4j/2.x/manual/configuration.html#AutomaticReconfiguration).
+If your custom log config has a `monitorInterval` set, then the operator does not watch for changes to the log config and will not trigger a rolling restart if the config changes.
+Kubernetes will automatically update the file on each pod's filesystem when the data in the ConfigMap changes. Once Kubernetes updates the file, Log4j will pick up the changes and apply them without restarting the Solr pod.
+
+If you need to customize both `solr.xml` and `log4j2.xml` then you need to supply both in the same ConfigMap using multiple keys as shown below:
+```
+---
+kind: ConfigMap
+apiVersion: v1
+metadata:
+ name: custom-solr-xml
+data:
+ log4j2.xml: |
+ <?xml version="1.0" encoding="UTF-8"?>
+ <Configuration monitorInterval="30">
+ ... YOUR CUSTOM LOG4J CONFIG HERE ...
+ </Configuration>
+
+
+ solr.xml: |
+ <?xml version="1.0" encoding="UTF-8" ?>
+ <solr>
+ ... YOUR CUSTOM SOLR XML CONFIG HERE ...
+ </solr>
+```
\ No newline at end of file