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

janhoy 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 4c9b7db  Solr xml subpath mount (#874)
4c9b7db is described below

commit 4c9b7db3c0f6c8d48aba6f0ecb305c66b538c7a6
Author: Jan Høydahl <[email protected]>
AuthorDate: Thu Nov 5 23:27:51 2026 +0100

    Solr xml subpath mount (#874)
    
    * Mount solr.xml via a subPath volumeMount instead of an initContainer
---
 controllers/solrcloud_controller_backup_test.go    | 53 ++++++++++++++----
 .../solrcloud_controller_basic_auth_test.go        |  2 +-
 controllers/solrcloud_controller_storage_test.go   | 14 ++---
 controllers/solrcloud_controller_test.go           | 50 ++++++++++++-----
 controllers/solrcloud_controller_zk_test.go        |  6 +--
 controllers/util/solr_util.go                      | 62 +++++++++++-----------
 .../solr-cloud/pages/custom-solr-config.adoc       |  6 ++-
 .../modules/upgrade-notes/pages/upgrade-notes.adoc | 15 +++++-
 helm/solr-operator/Chart.yaml                      |  5 ++
 9 files changed, 145 insertions(+), 68 deletions(-)

diff --git a/controllers/solrcloud_controller_backup_test.go 
b/controllers/solrcloud_controller_backup_test.go
index 7f223fc..d7531c7 100644
--- a/controllers/solrcloud_controller_backup_test.go
+++ b/controllers/solrcloud_controller_backup_test.go
@@ -118,8 +118,8 @@ var _ = FDescribe("SolrCloud controller - Backup 
Repositories", func() {
 
                        // Check Volumes
                        extraVolumes[0].DefaultContainerMount.Name = 
extraVolumes[0].Name
-                       
Expect(statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(len(extraVolumes)+1),
 "Container has wrong number of volumeMounts")
-                       
Expect(statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts[1]).To(Equal(*extraVolumes[0].DefaultContainerMount),
 "Additional Volume from podOptions not mounted into container properly.")
+                       
Expect(statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(len(extraVolumes)+2),
 "Container has wrong number of volumeMounts")
+                       
Expect(statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts[2]).To(Equal(*extraVolumes[0].DefaultContainerMount),
 "Additional Volume from podOptions not mounted into container properly.")
                        
Expect(statefulSet.Spec.Template.Spec.Volumes).To(HaveLen(len(extraVolumes)+3), 
"Pod has wrong number of volumes")
                        
Expect(statefulSet.Spec.Template.Spec.Volumes[3].Name).To(Equal(extraVolumes[0].Name),
 "Additional Volume from podOptions not loaded into pod properly.")
                        
Expect(statefulSet.Spec.Template.Spec.Volumes[3].VolumeSource).To(Equal(extraVolumes[0].Source),
 "Additional Volume from podOptions not loaded into pod properly.")
@@ -182,8 +182,8 @@ var _ = FDescribe("SolrCloud controller - Backup 
Repositories", func() {
 
                                // Check that no additional volumes have been 
added
                                extraVolumes[0].DefaultContainerMount.Name = 
extraVolumes[0].Name
-                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(len(extraVolumes)+1),
 "Container has wrong number of volumeMounts")
-                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[1]).To(Equal(*extraVolumes[0].DefaultContainerMount),
 "Additional Volume from podOptions not mounted into container properly.")
+                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(len(extraVolumes)+2),
 "Container has wrong number of volumeMounts")
+                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[2]).To(Equal(*extraVolumes[0].DefaultContainerMount),
 "Additional Volume from podOptions not mounted into container properly.")
                                
g.Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(len(extraVolumes)+3), 
"Pod has wrong number of volumes")
                                
g.Expect(found.Spec.Template.Spec.Volumes[3].Name).To(Equal(extraVolumes[0].Name),
 "Additional Volume from podOptions not loaded into pod properly.")
                                
g.Expect(found.Spec.Template.Spec.Volumes[3].VolumeSource).To(Equal(extraVolumes[0].Source),
 "Additional Volume from podOptions not loaded into pod properly.")
@@ -241,11 +241,11 @@ var _ = FDescribe("SolrCloud controller - Backup 
Repositories", func() {
 
                                // Check that no additional volumes have been 
added
                                extraVolumes[0].DefaultContainerMount.Name = 
extraVolumes[0].Name
-                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(len(extraVolumes)+2),
 "Container has wrong number of volumeMounts")
-                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[1].Name).To(Equal("backup-repository-test-repo"),
 "S3Credentials file volumeMount has wrong name.")
-                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[1].MountPath).To(Equal("/var/solr/data/backup-restore/test-repo/s3credential"),
 "S3Credentials file volumeMount has wrong mount path.")
-                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[1].ReadOnly).To(BeTrue(),
 "S3Credentials file volumeMount must be read-only.")
-                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[2]).To(Equal(*extraVolumes[0].DefaultContainerMount),
 "Additional Volume from podOptions not mounted into container properly.")
+                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(len(extraVolumes)+3),
 "Container has wrong number of volumeMounts")
+                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[2].Name).To(Equal("backup-repository-test-repo"),
 "S3Credentials file volumeMount has wrong name.")
+                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[2].MountPath).To(Equal("/var/solr/data/backup-restore/test-repo/s3credential"),
 "S3Credentials file volumeMount has wrong mount path.")
+                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[2].ReadOnly).To(BeTrue(),
 "S3Credentials file volumeMount must be read-only.")
+                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[3]).To(Equal(*extraVolumes[0].DefaultContainerMount),
 "Additional Volume from podOptions not mounted into container properly.")
                                
g.Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(len(extraVolumes)+4), 
"Pod has wrong number of volumes")
                                
g.Expect(found.Spec.Template.Spec.Volumes[3].Name).To(Equal("backup-repository-test-repo"),
 "S3Credentials file volume has wrong name.")
                                
g.Expect(found.Spec.Template.Spec.Volumes[3].VolumeSource.Secret).To(Not(BeNil()),
 "S3Credentials file has to be loaded via a secret volume.")
@@ -278,6 +278,41 @@ var _ = FDescribe("SolrCloud controller - Backup 
Repositories", func() {
                })
        })
 
+       FContext("Volume Repository", func() {
+               BeforeEach(func() {
+                       solrCloud.Spec = solrv1beta1.SolrCloudSpec{
+                               ZookeeperRef: &solrv1beta1.ZookeeperRef{
+                                       ConnectionInfo: 
&solrv1beta1.ZookeeperConnectionInfo{
+                                               InternalConnectionString: 
"host:7271",
+                                       },
+                               },
+                               BackupRepositories: 
[]solrv1beta1.SolrBackupRepository{
+                                       {
+                                               Name: "volume-repo",
+                                               Volume: 
&solrv1beta1.VolumeRepository{
+                                                       Source: 
corev1.VolumeSource{
+                                                               EmptyDir: 
&corev1.EmptyDirVolumeSource{},
+                                                       },
+                                               },
+                                       },
+                               },
+                       }
+               })
+               FIt("has the correct resources", func(ctx context.Context) {
+                       By("testing the Solr StatefulSet")
+                       statefulSet := expectStatefulSet(ctx, solrCloud, 
solrCloud.StatefulSetName())
+
+                       By("making sure an initContainer is still created to 
fix the permissions of the backup volume")
+                       
Expect(statefulSet.Spec.Template.Spec.InitContainers).To(HaveLen(1), "A volume 
backup repository requires an initContainer to set up the volume permissions")
+                       volumePrepContainer := 
statefulSet.Spec.Template.Spec.InitContainers[0]
+                       
Expect(volumePrepContainer.Name).To(Equal("volume-prep"), "Wrong name for the 
volume prep initContainer")
+                       
Expect(volumePrepContainer.Command[2]).To(Not(ContainSubstring("solr.xml")), 
"The volume prep initContainer should no longer copy the solr.xml, it is 
mounted directly into the Solr home directory")
+                       
Expect(volumePrepContainer.Command[2]).To(ContainSubstring(util.VolumeRepoVolumeMountPath(&solrCloud.Spec.BackupRepositories[0])),
 "The volume prep initContainer should set up the permissions of the backup 
volume")
+                       Expect(volumePrepContainer.VolumeMounts).To(HaveLen(1), 
"The volume prep initContainer only needs the backup volume mounted")
+                       
Expect(volumePrepContainer.VolumeMounts[0].Name).To(Equal(util.RepoVolumeName(&solrCloud.Spec.BackupRepositories[0])),
 "Wrong volume mounted in the volume prep initContainer")
+               })
+       })
+
        FContext("Multiple Repositories - Annotations", func() {
                BeforeEach(func() {
                        solrCloud.Spec = solrv1beta1.SolrCloudSpec{
diff --git a/controllers/solrcloud_controller_basic_auth_test.go 
b/controllers/solrcloud_controller_basic_auth_test.go
index 1129560..4b04906 100644
--- a/controllers/solrcloud_controller_basic_auth_test.go
+++ b/controllers/solrcloud_controller_basic_auth_test.go
@@ -311,7 +311,6 @@ func expectBasicAuthConfigOnPodTemplateWithGomega(g Gomega, 
solrCloud *solrv1bet
        // if no user-provided auth secret, then check that security.json gets 
bootstrapped correctly
        if solrCloud.Spec.SolrSecurity.BasicAuthSecret == "" || 
solrCloud.Spec.SolrSecurity.BootstrapSecurityJson != nil {
                // initContainers
-               g.Expect(podTemplate.Spec.InitContainers).To(Not(BeEmpty()), 
"The Solr Pod template requires an init container to bootstrap the 
security.json")
                var expInitContainer *corev1.Container = nil
                for _, cnt := range podTemplate.Spec.InitContainers {
                        if cnt.Name == "setup-zk" {
@@ -321,6 +320,7 @@ func expectBasicAuthConfigOnPodTemplateWithGomega(g Gomega, 
solrCloud *solrv1bet
                }
 
                if expectBootstrapSecret || 
solrCloud.Spec.SolrSecurity.BootstrapSecurityJson != nil {
+                       g.Expect(expInitContainer).To(Not(BeNil()), "The Solr 
Pod template requires a setup-zk init container to bootstrap the security.json")
                        // if the zookeeperRef has ACLs set, verify the env 
vars were set correctly for this initContainer
                        allACL, _ := solrCloud.Spec.ZookeeperRef.GetACLs()
                        if allACL != nil {
diff --git a/controllers/solrcloud_controller_storage_test.go 
b/controllers/solrcloud_controller_storage_test.go
index b60aa30..87a5c35 100644
--- a/controllers/solrcloud_controller_storage_test.go
+++ b/controllers/solrcloud_controller_storage_test.go
@@ -111,7 +111,7 @@ var _ = FDescribe("SolrCloud controller - Storage", func() {
                                
g.Expect(found.Spec.VolumeClaimTemplates[0].Labels[util.SolrPVCInstanceLabel]).To(Equal(solrCloud.Name),
 "PVC Instance label doesn't match")
                                
g.Expect(found.Spec.VolumeClaimTemplates[0].Labels["base"]).To(Equal("here"), 
"Additional PVC label doesn't match")
                                
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[0].Name).To(Equal(found.Spec.VolumeClaimTemplates[0].Name),
 "Custom PVC name not used in volume mount")
-                               
g.Expect(found.Spec.Template.Spec.InitContainers[0].VolumeMounts[1].Name).To(Equal(found.Spec.VolumeClaimTemplates[0].Name),
 "Custom PVC name not used in volume mount")
+                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[1]).To(Equal(corev1.VolumeMount{Name:
 "solr-xml", MountPath: "/var/solr/data/solr.xml", SubPath: util.SolrXmlFile, 
ReadOnly: true}), "The solr.xml must be mounted into the Solr home directory 
via a subPath")
                                
g.Expect(found.Spec.Template.Spec.ReadinessGates).To(Not(ContainElement(corev1.PodReadinessGate{ConditionType:
 util.SolrReplicasNotEvictedReadinessCondition})), "Persistent Data using Pods 
should not have a readiness gate to evict replicas.")
                                
g.Expect(found.Spec.Template.Spec.ReadinessGates).To(ContainElement(corev1.PodReadinessGate{ConditionType:
 util.SolrIsNotStoppedReadinessCondition}), "All pods should contain the 
isNotStopped readinessGate.")
                        })
@@ -155,7 +155,7 @@ var _ = FDescribe("SolrCloud controller - Storage", func() {
                                
g.Expect(found.Spec.VolumeClaimTemplates[0].Labels[util.SolrPVCInstanceLabel]).To(Equal(solrCloud.Name),
 "PVC Instance label doesn't match")
                                
g.Expect(found.Spec.VolumeClaimTemplates[0].Labels["base"]).To(Equal("here"), 
"Additional PVC label doesn't match")
                                
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[0].Name).To(Equal(found.Spec.VolumeClaimTemplates[0].Name),
 "Custom PVC name not used in volume mount")
-                               
g.Expect(found.Spec.Template.Spec.InitContainers[0].VolumeMounts[1].Name).To(Equal(found.Spec.VolumeClaimTemplates[0].Name),
 "Custom PVC name not used in volume mount")
+                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[1]).To(Equal(corev1.VolumeMount{Name:
 "solr-xml", MountPath: "/var/solr/data/solr.xml", SubPath: util.SolrXmlFile, 
ReadOnly: true}), "The solr.xml must be mounted into the Solr home directory 
via a subPath")
                                
g.Expect(found.Spec.Template.Spec.ReadinessGates).To(Not(ContainElement(corev1.PodReadinessGate{ConditionType:
 util.SolrReplicasNotEvictedReadinessCondition})), "Persistent Data using Pods 
should not have a readiness gate to evict replicas.")
                                
g.Expect(found.Spec.Template.Spec.ReadinessGates).To(ContainElement(corev1.PodReadinessGate{ConditionType:
 util.SolrIsNotStoppedReadinessCondition}), "All pods should contain the 
isNotStopped readinessGate.")
                        })
@@ -189,7 +189,7 @@ var _ = FDescribe("SolrCloud controller - Storage", func() {
                                g.Expect(dataVolume.EmptyDir).To(Not(BeNil()), 
"The data volume should be an empty-dir.")
                                g.Expect(dataVolume.HostPath).To(BeNil(), "The 
data volume should not be a hostPath volume.")
                                
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[0].Name).To(Equal(dataVolume.Name),
 "Ephemeral Data volume name not used in volume mount")
-                               
g.Expect(found.Spec.Template.Spec.InitContainers[0].VolumeMounts[1].Name).To(Equal(dataVolume.Name),
 "Ephemeral Data volume name not used in volume mount")
+                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[1]).To(Equal(corev1.VolumeMount{Name:
 "solr-xml", MountPath: "/var/solr/data/solr.xml", SubPath: util.SolrXmlFile, 
ReadOnly: true}), "The solr.xml must be mounted into the Solr home directory 
via a subPath")
                                
g.Expect(found.Spec.Template.Spec.ReadinessGates).To(ContainElement(corev1.PodReadinessGate{ConditionType:
 util.SolrReplicasNotEvictedReadinessCondition}), "Ephemeral Data using Pods 
should have a readiness gate to evict replicas.")
                                
g.Expect(found.Spec.Template.Spec.ReadinessGates).To(ContainElement(corev1.PodReadinessGate{ConditionType:
 util.SolrIsNotStoppedReadinessCondition}), "All pods should contain the 
isNotStopped readinessGate.")
                        })
@@ -218,7 +218,7 @@ var _ = FDescribe("SolrCloud controller - Storage", func() {
                                g.Expect(dataVolume.EmptyDir).To(Not(BeNil()), 
"The data volume should be an empty-dir.")
                                g.Expect(dataVolume.HostPath).To(BeNil(), "The 
data volume should not be a hostPath volume.")
                                
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[0].Name).To(Equal(dataVolume.Name),
 "Ephemeral Data volume name not used in volume mount")
-                               
g.Expect(found.Spec.Template.Spec.InitContainers[0].VolumeMounts[1].Name).To(Equal(dataVolume.Name),
 "Ephemeral Data volume name not used in volume mount")
+                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[1]).To(Equal(corev1.VolumeMount{Name:
 "solr-xml", MountPath: "/var/solr/data/solr.xml", SubPath: util.SolrXmlFile, 
ReadOnly: true}), "The solr.xml must be mounted into the Solr home directory 
via a subPath")
                                
g.Expect(found.Spec.Template.Spec.ReadinessGates).To(ContainElement(corev1.PodReadinessGate{ConditionType:
 util.SolrReplicasNotEvictedReadinessCondition}), "Ephemeral Data using Pods 
should have a readiness gate to evict replicas.")
                                
g.Expect(found.Spec.Template.Spec.ReadinessGates).To(ContainElement(corev1.PodReadinessGate{ConditionType:
 util.SolrIsNotStoppedReadinessCondition}), "All pods should contain the 
isNotStopped readinessGate.")
                        })
@@ -251,7 +251,7 @@ var _ = FDescribe("SolrCloud controller - Storage", func() {
                                g.Expect(dataVolume.HostPath).To(BeNil(), "The 
data volume should not be a hostPath volume.")
                                
g.Expect(dataVolume.EmptyDir).To(Equal(solrCloud.Spec.StorageOptions.EphemeralStorage.EmptyDir),
 "The empty dir settings do not match with what was provided.")
                                
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[0].Name).To(Equal(dataVolume.Name),
 "Ephemeral Data volume name not used in volume mount")
-                               
g.Expect(found.Spec.Template.Spec.InitContainers[0].VolumeMounts[1].Name).To(Equal(dataVolume.Name),
 "Ephemeral Data volume name not used in volume mount")
+                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[1]).To(Equal(corev1.VolumeMount{Name:
 "solr-xml", MountPath: "/var/solr/data/solr.xml", SubPath: util.SolrXmlFile, 
ReadOnly: true}), "The solr.xml must be mounted into the Solr home directory 
via a subPath")
                                
g.Expect(found.Spec.Template.Spec.ReadinessGates).To(ContainElement(corev1.PodReadinessGate{ConditionType:
 util.SolrReplicasNotEvictedReadinessCondition}), "Ephemeral Data using Pods 
should have a readiness gate to evict replicas.")
                                
g.Expect(found.Spec.Template.Spec.ReadinessGates).To(ContainElement(corev1.PodReadinessGate{ConditionType:
 util.SolrIsNotStoppedReadinessCondition}), "All pods should contain the 
isNotStopped readinessGate.")
                        })
@@ -285,7 +285,7 @@ var _ = FDescribe("SolrCloud controller - Storage", func() {
                                g.Expect(dataVolume.HostPath).To(Not(BeNil()), 
"The data volume should be a hostPath volume.")
                                
g.Expect(dataVolume.HostPath).To(Equal(solrCloud.Spec.StorageOptions.EphemeralStorage.HostPath),
 "The hostPath  settings do not match with what was provided.")
                                
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[0].Name).To(Equal(dataVolume.Name),
 "Ephemeral Data volume name not used in volume mount")
-                               
g.Expect(found.Spec.Template.Spec.InitContainers[0].VolumeMounts[1].Name).To(Equal(dataVolume.Name),
 "Ephemeral Data volume name not used in volume mount")
+                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[1]).To(Equal(corev1.VolumeMount{Name:
 "solr-xml", MountPath: "/var/solr/data/solr.xml", SubPath: util.SolrXmlFile, 
ReadOnly: true}), "The solr.xml must be mounted into the Solr home directory 
via a subPath")
                                
g.Expect(found.Spec.Template.Spec.ReadinessGates).To(ContainElement(corev1.PodReadinessGate{ConditionType:
 util.SolrReplicasNotEvictedReadinessCondition}), "Ephemeral Data using Pods 
should have a readiness gate to evict replicas.")
                                
g.Expect(found.Spec.Template.Spec.ReadinessGates).To(ContainElement(corev1.PodReadinessGate{ConditionType:
 util.SolrIsNotStoppedReadinessCondition}), "All pods should contain the 
isNotStopped readinessGate.")
                        })
@@ -323,7 +323,7 @@ var _ = FDescribe("SolrCloud controller - Storage", func() {
                                g.Expect(dataVolume.HostPath).To(Not(BeNil()), 
"The data volume should be a hostPath volume.")
                                
g.Expect(dataVolume.HostPath).To(Equal(solrCloud.Spec.StorageOptions.EphemeralStorage.HostPath),
 "The hostPath  settings do not match with what was provided.")
                                
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[0].Name).To(Equal(dataVolume.Name),
 "Ephemeral Data volume name not used in volume mount")
-                               
g.Expect(found.Spec.Template.Spec.InitContainers[0].VolumeMounts[1].Name).To(Equal(dataVolume.Name),
 "Ephemeral Data volume name not used in volume mount")
+                               
g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[1]).To(Equal(corev1.VolumeMount{Name:
 "solr-xml", MountPath: "/var/solr/data/solr.xml", SubPath: util.SolrXmlFile, 
ReadOnly: true}), "The solr.xml must be mounted into the Solr home directory 
via a subPath")
                                
g.Expect(found.Spec.Template.Spec.ReadinessGates).To(ContainElement(corev1.PodReadinessGate{ConditionType:
 util.SolrReplicasNotEvictedReadinessCondition}), "Ephemeral Data using Pods 
should have a readiness gate to evict replicas.")
                                
g.Expect(found.Spec.Template.Spec.ReadinessGates).To(ContainElement(corev1.PodReadinessGate{ConditionType:
 util.SolrIsNotStoppedReadinessCondition}), "All pods should contain the 
isNotStopped readinessGate.")
                        })
diff --git a/controllers/solrcloud_controller_test.go 
b/controllers/solrcloud_controller_test.go
index 42a7d64..abca59a 100644
--- a/controllers/solrcloud_controller_test.go
+++ b/controllers/solrcloud_controller_test.go
@@ -108,8 +108,8 @@ var _ = FDescribe("SolrCloud controller - General", func() {
                        
Expect(statefulSet.Spec.Template.Spec.Containers).To(HaveLen(3), "Solr 
StatefulSet requires the solr container plus the desired sidecars.")
                        
Expect(statefulSet.Spec.Template.Spec.Containers[1:]).To(Equal(extraContainers1),
 "Solr StatefulSet has incorrect extra containers")
 
-                       
Expect(statefulSet.Spec.Template.Spec.InitContainers).To(HaveLen(3), "Solr 
StatefulSet requires a default initContainer plus the additional specified.")
-                       
Expect(statefulSet.Spec.Template.Spec.InitContainers[1:]).To(Equal(extraContainers2),
 "Solr StatefulSet has incorrect extra initContainers")
+                       
Expect(statefulSet.Spec.Template.Spec.InitContainers).To(HaveLen(2), "Solr 
StatefulSet should only have the additional initContainers specified, no 
default ones are needed.")
+                       
Expect(statefulSet.Spec.Template.Spec.InitContainers).To(Equal(extraContainers2),
 "Solr StatefulSet has incorrect extra initContainers")
 
                        // Check the update strategy
                        
Expect(statefulSet.Spec.UpdateStrategy.Type).To(Equal(appsv1.OnDeleteStatefulSetStrategyType),
 "Incorrect statefulset update strategy")
@@ -145,8 +145,8 @@ var _ = FDescribe("SolrCloud controller - General", func() {
                        
Expect(statefulSet.Spec.Template.Spec.Containers[0].Resources.Requests).To(Equal(testResources.Requests),
 "Resources.Requests is not the same as the one provided in podOptions")
                        
Expect(statefulSet.Spec.Template.Spec.Containers[0].SecurityContext).To(BeNil(),
 "SecurityContext is not the expected default value, 'nil'")
                        extraVolumes[0].DefaultContainerMount.Name = 
extraVolumes[0].Name
-                       
Expect(statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(len(extraVolumes)+1),
 "Container has wrong number of volumeMounts")
-                       
Expect(statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts[1]).To(Equal(*extraVolumes[0].DefaultContainerMount),
 "Additional Volume from podOptions not mounted into container properly.")
+                       
Expect(statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(len(extraVolumes)+2),
 "Container has wrong number of volumeMounts")
+                       
Expect(statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts[2]).To(Equal(*extraVolumes[0].DefaultContainerMount),
 "Additional Volume from podOptions not mounted into container properly.")
                        
Expect(statefulSet.Spec.Template.Spec.Volumes).To(HaveLen(len(extraVolumes)+3), 
"Pod has wrong number of volumes")
                        
Expect(statefulSet.Spec.Template.Spec.Volumes[3].Name).To(Equal(extraVolumes[0].Name),
 "Additional Volume from podOptions not loaded into pod properly.")
                        
Expect(statefulSet.Spec.Template.Spec.Volumes[3].VolumeSource).To(Equal(extraVolumes[0].Source),
 "Additional Volume from podOptions not loaded into pod properly.")
@@ -277,6 +277,27 @@ var _ = FDescribe("SolrCloud controller - General", func() 
{
 
                        
Expect(statefulSet.Spec.Template.Annotations).To(HaveKeyWithValue(util.SolrXmlMd5Annotation,
 fmt.Sprintf("%x", md5.Sum([]byte(configMap.Data[util.SolrXmlFile])))), "Wrong 
solr.xml MD5 annotation in the pod template!")
 
+                       By("testing that the solr.xml is mounted into the Solr 
home directory, without an initContainer")
+                       for _, initContainer := range 
statefulSet.Spec.Template.Spec.InitContainers {
+                               
Expect(initContainer.Name).To(Not(Or(Equal("cp-solr-xml"), 
Equal("volume-prep"))), "No volume prep initContainer is needed, the solr.xml 
is mounted directly into the Solr home directory")
+                       }
+                       var solrXmlVol *corev1.Volume = nil
+                       for _, vol := range 
statefulSet.Spec.Template.Spec.Volumes {
+                               if vol.Name == "solr-xml" {
+                                       solrXmlVol = &vol
+                                       break
+                               }
+                       }
+                       Expect(solrXmlVol).To(Not(BeNil()), "Didn't find the 
solr-xml volume in the pod template!")
+                       Expect(solrXmlVol.ConfigMap).To(Not(BeNil()), "The 
solr-xml volume should have a ConfigMap source")
+                       
Expect(solrXmlVol.ConfigMap.Name).To(Equal(solrCloud.ConfigMapName()), "The 
solr-xml volume has the wrong configMap as its source")
+                       
Expect(statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts).To(ContainElement(corev1.VolumeMount{
+                               Name:      "solr-xml",
+                               MountPath: "/var/solr/data/solr.xml",
+                               SubPath:   util.SolrXmlFile,
+                               ReadOnly:  true,
+                       }), "The solr.xml must be mounted into the Solr home 
directory via a subPath, so that the rest of the directory is untouched")
+
                        
Expect(statefulSet.Spec.Template.Spec.Containers).To(HaveLen(1), "Solr 
StatefulSet requires a container.")
                        expectedEnvVars := map[string]string{
                                "ZK_HOST":             "host:7271/test",
@@ -312,17 +333,12 @@ var _ = FDescribe("SolrCloud controller - General", 
func() {
                        
Expect(statefulSet.Spec.Template.Spec.Containers[0].SecurityContext.Capabilities.Add).To(HaveLen(1))
                        
Expect(statefulSet.Spec.Template.Spec.Containers[0].SecurityContext.Capabilities.Add[0]).To(Equal(corev1.Capability("someCapability")))
                        
Expect(statefulSet.Spec.Template.Spec.InitContainers[0].Resources).To(Equal(testResources2),
 "Incorrect initContainer[0] resources")
-                       
Expect(statefulSet.Spec.Template.Spec.InitContainers[1].Resources).To(Equal(testResources2),
 "Incorrect initContainer[1] resources")
-                       
Expect(statefulSet.Spec.Template.Spec.InitContainers[2].Resources).ToNot(Equal(testResources2),
 "Incorrect initContainer[2] resources, should not use the default override")
+                       
Expect(statefulSet.Spec.Template.Spec.InitContainers[1].Resources).ToNot(Equal(testResources2),
 "Incorrect initContainer[1] resources, should not use the default override")
                        
Expect(statefulSet.Spec.Template.Spec.InitContainers[0].SecurityContext).To(Not(BeNil()),
 "InitContainer[0] should have security context")
                        
Expect(statefulSet.Spec.Template.Spec.InitContainers[0].SecurityContext.RunAsNonRoot).To(PointTo(BeTrue()),
 "Incorrect initContainer[0] security context runAsNonRoot")
                        
Expect(statefulSet.Spec.Template.Spec.InitContainers[0].SecurityContext.Capabilities.Add).To(HaveLen(2),
 "Incorrect number of capabilities in initContainer[0]")
                        
Expect(statefulSet.Spec.Template.Spec.InitContainers[0].SecurityContext.Capabilities.Add).To(ContainElements(corev1.Capability("CHOWN"),
 corev1.Capability("DAC_OVERRIDE")), "Incorrect capabilities in 
initContainer[0]")
-                       
Expect(statefulSet.Spec.Template.Spec.InitContainers[1].SecurityContext).To(Not(BeNil()),
 "InitContainer[1] should have security context")
-                       
Expect(statefulSet.Spec.Template.Spec.InitContainers[1].SecurityContext.RunAsNonRoot).To(PointTo(BeTrue()),
 "Incorrect initContainer[1] security context runAsNonRoot")
-                       
Expect(statefulSet.Spec.Template.Spec.InitContainers[1].SecurityContext.Capabilities.Add).To(HaveLen(2),
 "Incorrect number of capabilities in initContainer[1]")
-                       
Expect(statefulSet.Spec.Template.Spec.InitContainers[1].SecurityContext.Capabilities.Add).To(ContainElements(corev1.Capability("CHOWN"),
 corev1.Capability("DAC_OVERRIDE")), "Incorrect capabilities in 
initContainer[1]")
-                       
Expect(statefulSet.Spec.Template.Spec.InitContainers[2].SecurityContext).To(BeNil(),
 "InitContainer[2] should not have custom security context since it's 
user-provided")
+                       
Expect(statefulSet.Spec.Template.Spec.InitContainers[1].SecurityContext).To(BeNil(),
 "InitContainer[1] should not have custom security context since it's 
user-provided")
                        
Expect(statefulSet.Spec.Template.Spec.Tolerations).To(Equal(testTolerations), 
"Incorrect Tolerations for Pod")
                        
Expect(statefulSet.Spec.Template.Spec.PriorityClassName).To(Equal(testPriorityClass),
 "Incorrect Priority class name for Pod Spec")
                        
Expect(statefulSet.Spec.Template.Spec.ImagePullSecrets).To(ConsistOf(append(testAdditionalImagePullSecrets,
 corev1.LocalObjectReference{Name: testImagePullSecretName})), "Incorrect 
imagePullSecrets")
@@ -769,13 +785,21 @@ var _ = FDescribe("SolrCloud controller - General", 
func() {
                                
g.Expect(customConfigVol.VolumeSource.ConfigMap, "solr-xml Volume should have a 
ConfigMap source")
                                
g.Expect(customConfigVol.VolumeSource.ConfigMap.Name).To(Equal(testCustomConfigMap),
 "solr-xml has the wrong configMap as its source")
 
+                               // The "solr-xml" volume is mounted twice in 
this case, once for the solr.xml (via a subPath)
+                               // and once for the directory containing the 
log4j2.xml.
                                var logXmlVolMount *corev1.VolumeMount = nil
+                               var solrXmlVolMount *corev1.VolumeMount = nil
                                for _, mount := range 
found.Spec.Template.Spec.Containers[0].VolumeMounts {
                                        if mount.Name == "solr-xml" {
-                                               logXmlVolMount = &mount
-                                               break
+                                               if mount.SubPath == 
util.SolrXmlFile {
+                                                       solrXmlVolMount = &mount
+                                               } else {
+                                                       logXmlVolMount = &mount
+                                               }
                                        }
                                }
+                               g.Expect(solrXmlVolMount).To(Not(BeNil()), 
"Didn't find the solr-xml Volume mount")
+                               
g.Expect(solrXmlVolMount.MountPath).To(Equal("/var/solr/data/solr.xml"), 
"solr-xml Volume mount has the wrong path")
                                g.Expect(logXmlVolMount).To(Not(BeNil()), 
"Didn't find the log4j2-xml Volume mount")
                                expectedMountPath := 
fmt.Sprintf("/var/solr/%s", testCustomConfigMap)
                                
g.Expect(logXmlVolMount.MountPath).To(Equal(expectedMountPath), "log4j2-xml 
Volume mount has the wrong path")
diff --git a/controllers/solrcloud_controller_zk_test.go 
b/controllers/solrcloud_controller_zk_test.go
index 7193420..1619e76 100644
--- a/controllers/solrcloud_controller_zk_test.go
+++ b/controllers/solrcloud_controller_zk_test.go
@@ -270,8 +270,8 @@ var _ = FDescribe("SolrCloud controller - Zookeeper", 
func() {
                        
Expect(statefulSet.Annotations).To(Equal(util.MergeLabelsOrAnnotations(testSSAnnotations,
 expectedStatefulSetAnnotations)), "Wrong statefulSet annotations")
                        
Expect(statefulSet.Spec.Template.Spec.Containers[0].Lifecycle.PostStart).To(BeNil(),
 "There should be no postStart command, even if there is a chRoot for ZK")
                        
Expect(statefulSet.Spec.Template.Spec.ServiceAccountName).To(BeEmpty(), "No 
custom serviceAccountName specified, so the field should be empty.")
-                       
Expect(statefulSet.Spec.Template.Spec.InitContainers).To(HaveLen(2), "Solr 
StatefulSet requires 2 init containers, one for the solr.xml and one for ZK.")
-                       
Expect(statefulSet.Spec.Template.Spec.InitContainers[1].Command).To(Equal([]string{"sh",
 "-c", "solr zk ls ${ZK_CHROOT} -z ${ZK_SERVER} || solr zk mkroot ${ZK_CHROOT} 
-z ${ZK_SERVER}; "}), "Wrong command for the ZK init container")
+                       
Expect(statefulSet.Spec.Template.Spec.InitContainers).To(HaveLen(1), "Solr 
StatefulSet requires 1 init container, for ZK.")
+                       
Expect(statefulSet.Spec.Template.Spec.InitContainers[0].Command).To(Equal([]string{"sh",
 "-c", "solr zk ls ${ZK_CHROOT} -z ${ZK_SERVER} || solr zk mkroot ${ZK_CHROOT} 
-z ${ZK_SERVER}; "}), "Wrong command for the ZK init container")
 
                        // Check the update strategy
                        
Expect(statefulSet.Spec.UpdateStrategy.Type).To(Equal(appsv1.OnDeleteStatefulSetStrategyType),
 "Incorrect statefulset update strategy")
@@ -587,7 +587,7 @@ var _ = FDescribe("SolrCloud controller - Zookeeper", 
func() {
                                "SOLR_OPTS":      "$(SOLR_ZK_OPTS) " + 
testSolrOpts,
                                "SOLR_TOOL_OPTS": "$(SOLR_ZK_OPTS) " + 
testSolrOpts,
                        }
-                       testPodEnvVariables(expectedInitContainerEnvVars, 
statefulSet.Spec.Template.Spec.InitContainers[1].Env)
+                       testPodEnvVariables(expectedInitContainerEnvVars, 
statefulSet.Spec.Template.Spec.InitContainers[0].Env)
                })
        })
 })
diff --git a/controllers/util/solr_util.go b/controllers/util/solr_util.go
index 33def13..64c0b92 100644
--- a/controllers/util/solr_util.go
+++ b/controllers/util/solr_util.go
@@ -251,7 +251,13 @@ func GenerateStatefulSet(solrCloud *solr.SolrCloud, 
solrCloudStatus *solr.SolrCl
                })
        }
 
-       volumeMounts := []corev1.VolumeMount{{Name: solrDataVolumeName, 
MountPath: "/var/solr/data"}}
+       volumeMounts := []corev1.VolumeMount{
+               {Name: solrDataVolumeName, MountPath: "/var/solr/data"},
+               // Only the solr.xml file is mounted into the Solr home 
directory, via a subPath, so that
+               // the rest of the directory is untouched. subPath mounts do 
not see ConfigMap updates,
+               // instead the solr.xml MD5 pod annotation set below restarts 
the pods.
+               {Name: "solr-xml", MountPath: "/var/solr/data/" + SolrXmlFile, 
SubPath: SolrXmlFile, ReadOnly: true},
+       }
 
        // Add necessary specs for backupRepos
        backupEnvVars := make([]corev1.EnvVar, 0)
@@ -457,7 +463,7 @@ func GenerateStatefulSet(solrCloud *solr.SolrCloud, 
solrCloudStatus *solr.SolrCl
                Value: strings.Join(allSolrOpts, " "),
        })
 
-       initContainers := generateSolrSetupInitContainers(solrCloud, 
solrCloudStatus, solrDataVolumeName, security)
+       initContainers := generateSolrSetupInitContainers(solrCloud, 
solrCloudStatus, security)
 
        // Add user defined additional init containers
        if customPodOptions != nil && len(customPodOptions.InitContainers) > 0 {
@@ -721,19 +727,11 @@ func MaintainPreservedStatefulSetFields(expected, found 
*appsv1.StatefulSet) {
        expected.Spec.Replicas = found.Spec.Replicas
 }
 
-func generateSolrSetupInitContainers(solrCloud *solr.SolrCloud, 
solrCloudStatus *solr.SolrCloudStatus, solrDataVolumeName string, security 
*SecurityConfig) (containers []corev1.Container) {
-       // The setup of the solr.xml will always be necessary
-       volumeMounts := []corev1.VolumeMount{
-               {
-                       Name:      "solr-xml",
-                       MountPath: "/tmp",
-               },
-               {
-                       Name:      solrDataVolumeName,
-                       MountPath: "/tmp-config",
-               },
-       }
-       setupCommands := []string{"cp /tmp/solr.xml /tmp-config/solr.xml"}
+func generateSolrSetupInitContainers(solrCloud *solr.SolrCloud, 
solrCloudStatus *solr.SolrCloudStatus, security *SecurityConfig) (containers 
[]corev1.Container) {
+       // The solr.xml is mounted into the Solr home directory, so the only 
thing left to prepare is
+       // the permissions of backup repository volumes.
+       var volumeMounts []corev1.VolumeMount
+       var setupCommands []string
 
        // Figure out the solrUser and solrGroup to use
        solrUser := DefaultSolrUser
@@ -787,23 +785,25 @@ func generateSolrSetupInitContainers(solrCloud 
*solr.SolrCloud, solrCloudStatus
                }
        }
 
-       volumePrepResources := corev1.ResourceList{
-               corev1.ResourceCPU:    *DefaultSolrVolumePrepInitContainerCPU,
-               corev1.ResourceMemory: 
*DefaultSolrVolumePrepInitContainerMemory,
-       }
-       volumePrepInitContainer := corev1.Container{
-               Name:            "cp-solr-xml",
-               Image:           solrCloud.Spec.BusyBoxImage.ToImageName(),
-               ImagePullPolicy: solrCloud.Spec.BusyBoxImage.PullPolicy,
-               Command:         []string{"sh", "-c", 
strings.Join(setupCommands, " && ")},
-               VolumeMounts:    volumeMounts,
-               Resources: corev1.ResourceRequirements{
-                       Requests: volumePrepResources,
-                       Limits:   volumePrepResources,
-               },
-       }
+       if len(setupCommands) > 0 {
+               volumePrepResources := corev1.ResourceList{
+                       corev1.ResourceCPU:    
*DefaultSolrVolumePrepInitContainerCPU,
+                       corev1.ResourceMemory: 
*DefaultSolrVolumePrepInitContainerMemory,
+               }
+               volumePrepInitContainer := corev1.Container{
+                       Name:            "volume-prep",
+                       Image:           
solrCloud.Spec.BusyBoxImage.ToImageName(),
+                       ImagePullPolicy: solrCloud.Spec.BusyBoxImage.PullPolicy,
+                       Command:         []string{"sh", "-c", 
strings.Join(setupCommands, " && ")},
+                       VolumeMounts:    volumeMounts,
+                       Resources: corev1.ResourceRequirements{
+                               Requests: volumePrepResources,
+                               Limits:   volumePrepResources,
+                       },
+               }
 
-       containers = append(containers, volumePrepInitContainer)
+               containers = append(containers, volumePrepInitContainer)
+       }
 
        if hasZKSetupContainer, zkSetupContainer := 
generateZKInteractionInitContainer(solrCloud, solrCloudStatus, security); 
hasZKSetupContainer {
                containers = append(containers, zkSetupContainer)
diff --git a/docs/modules/solr-cloud/pages/custom-solr-config.adoc 
b/docs/modules/solr-cloud/pages/custom-solr-config.adoc
index ca73387..eda4955 100644
--- a/docs/modules/solr-cloud/pages/custom-solr-config.adoc
+++ b/docs/modules/solr-cloud/pages/custom-solr-config.adoc
@@ -36,10 +36,12 @@ spec:
 == 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.
+It is mounted there from a ConfigMap, via a `subPath` volumeMount so that only 
this one file is mounted into the `solr.home` directory.
+The default `solr.xml` comes from a ConfigMap named 
`<INSTANCE>-solrcloud-configmap` (where `<INSTANCE>` is the name of your 
SolrCloud instance) created by the Solr operator.
+The mount only exists in the Solr container, so a sidecar or custom 
initContainer that needs the `solr.xml` has to mount the ConfigMap itself.
 
 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.
+The `solr.xml` in a running pod is read-only, and ConfigMap changes only reach 
pods through the restart described below.
 
 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`:
 
diff --git a/docs/modules/upgrade-notes/pages/upgrade-notes.adoc 
b/docs/modules/upgrade-notes/pages/upgrade-notes.adoc
index 6a6d5f7..c4caa07 100644
--- a/docs/modules/upgrade-notes/pages/upgrade-notes.adoc
+++ b/docs/modules/upgrade-notes/pages/upgrade-notes.adoc
@@ -137,12 +137,23 @@ _Note that the Helm chart version does not contain a `v` 
prefix, which the downl
 === v0.10.0
 
 * The default Solr version has been upgraded from `8.11` to `9.11.0`.
-This only affects new resources, as default versions are hard-written to the 
resources immediately.
-Note that Solr `>=9.10` will not start a node older than any Solr node already 
running in the same cluster, so a rolling downgrade of Solr requires setting 
the `SOLR_CLOUD_DOWNGRADE_ENABLED` envVar to `true` on the pods.
+This only affects newly created resources, since the operator writes the 
resolved Solr version into the resource's spec on the first reconcile, so an 
operator upgrade never changes the Solr version of a running cluster.
+Note that a Solr `>=9.10` node refuses to start if its `major.minor` version 
is lower than the highest version already running in the same Solr cluster, so 
a rolling downgrade of Solr requires setting the `SOLR_CLOUD_DOWNGRADE_ENABLED` 
envVar to `true` on the pods.
 
 * Operator logs now default to `json` encoding at `info` level.
 Set the new helm value `development` to `true` (CLI flag `--zap-devel`) for 
the previous `console` encoding at `debug` level.
 
+* The `cp-solr-xml` initContainer has been removed.
+The `solr.xml` is now mounted read-only into `/var/solr/data/solr.xml` from 
the ConfigMap, via a `subPath` volumeMount, instead of being copied there 
before Solr starts.
+** This changes the Solr pod spec, so the Solr pods are restarted when the 
operator is upgraded.
+SolrClouds using `updateStrategy.method: Manual` are the exception, since 
neither the operator nor Kubernetes deletes their pods: they keep running with 
the old layout until you delete the pods yourself.
+** The `solr.xml` is now only visible inside the Solr container.
+Sidecars and custom initContainers that mount the data volume no longer get it 
from there.
+With persistent or `hostPath` data storage they do still see the `solr.xml` 
that `cp-solr-xml` copied onto the volume before the upgrade, but that copy is 
stale and will never be updated again.
+Mount the `solr.xml` ConfigMap into such a container if it needs the current 
configuration.
+** The permission fixing that `cp-solr-xml` also did for `volume` 
`backupRepositories` has moved to a new `volume-prep` initContainer, which is 
only created for SolrClouds that have a `volume` `backupRepository`.
+All other Solr pods now start without any volume prep initContainer.
+
 === v0.8.0
 
 * *The minimum supported Solr version is now 8.11*
diff --git a/helm/solr-operator/Chart.yaml b/helm/solr-operator/Chart.yaml
index 1fc22f4..9b5e6ff 100644
--- a/helm/solr-operator/Chart.yaml
+++ b/helm/solr-operator/Chart.yaml
@@ -155,6 +155,11 @@ annotations:
           url: https://solr.apache.org/docs/9_11_0/changes/Changes.html
         - name: Solr Security News
           url: https://solr.apache.org/security-news.html
+    - kind: changed
+      description: The `solr.xml` is now mounted into the Solr home directory 
from the ConfigMap, via a subPath volumeMount, instead of being copied there by 
the `cp-solr-xml` initContainer. That initContainer is only created for 
`volume` backupRepositories now, renamed to `volume-prep`
+      links:
+        - name: Github Issue
+          url: https://github.com/apache/solr-operator/issues/161
     - kind: fixed
       description: A cluster operation (scale up/down, rolling update, replica 
balancing, PVC expansion) that fails, e.g. because Solr is temporarily 
unreachable, is now always retried instead of stalling the SolrCloud until 
something else triggers a reconcile
       links:

Reply via email to