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

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


The following commit(s) were added to refs/heads/main by this push:
     new 44e7375  Add support for Solr Modules and additional libs (#332)
44e7375 is described below

commit 44e73756c56c9f42c58448d43ffbd6009d87cc7c
Author: Houston Putman <[email protected]>
AuthorDate: Wed Oct 13 11:34:18 2021 -0400

    Add support for Solr Modules and additional libs (#332)
---
 api/v1beta1/solrcloud_types.go                   |  12 +++
 api/v1beta1/zz_generated.deepcopy.go             |  10 ++
 config/crd/bases/solr.apache.org_solrclouds.yaml |  10 ++
 controllers/solrcloud_controller_test.go         |  46 ++++++++-
 controllers/util/solr_backup_repo_util.go        |  34 ++++++-
 controllers/util/solr_backup_repo_util_test.go   |  26 ++++-
 controllers/util/solr_util.go                    |  79 +++++++--------
 controllers/util/solr_util_test.go               | 116 ++++++++++++++++++++++-
 docs/solr-cloud/solr-cloud-crd.md                |  14 +++
 example/test_solrcloud.yaml                      |   3 +
 helm/solr-operator/Chart.yaml                    |   9 ++
 helm/solr-operator/crds/crds.yaml                |  10 ++
 helm/solr/README.md                              |   2 +
 helm/solr/templates/solrcloud.yaml               |  10 ++
 helm/solr/values.yaml                            |   2 +
 15 files changed, 333 insertions(+), 50 deletions(-)

diff --git a/api/v1beta1/solrcloud_types.go b/api/v1beta1/solrcloud_types.go
index 1522401..def3a62 100644
--- a/api/v1beta1/solrcloud_types.go
+++ b/api/v1beta1/solrcloud_types.go
@@ -126,6 +126,18 @@ type SolrCloudSpec struct {
        //+listType:=map
        //+listMapKey:=name
        BackupRepositories []SolrBackupRepository 
`json:"backupRepositories,omitempty"`
+
+       // List of Solr Modules to be loaded when starting Solr
+       // Note: You do not need to specify a module if it is required by 
another property (e.g. backupRepositories[].gcs)
+       //
+       //+optional
+       SolrModules []string `json:"solrModules,omitempty"`
+
+       // List of paths in the Solr Docker image to load in the classpath.
+       // Note: Solr Modules will be auto-loaded if specified in the 
"solrModules" property. There is no need to specify them here as well.
+       //
+       //+optional
+       AdditionalLibs []string `json:"additionalLibs,omitempty"`
 }
 
 func (spec *SolrCloudSpec) withDefaults() (changed bool) {
diff --git a/api/v1beta1/zz_generated.deepcopy.go 
b/api/v1beta1/zz_generated.deepcopy.go
index c8f06df..aacf29b 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -908,6 +908,16 @@ func (in *SolrCloudSpec) DeepCopyInto(out *SolrCloudSpec) {
                        (*in)[i].DeepCopyInto(&(*out)[i])
                }
        }
+       if in.SolrModules != nil {
+               in, out := &in.SolrModules, &out.SolrModules
+               *out = make([]string, len(*in))
+               copy(*out, *in)
+       }
+       if in.AdditionalLibs != nil {
+               in, out := &in.AdditionalLibs, &out.AdditionalLibs
+               *out = make([]string, len(*in))
+               copy(*out, *in)
+       }
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, 
creating a new SolrCloudSpec.
diff --git a/config/crd/bases/solr.apache.org_solrclouds.yaml 
b/config/crd/bases/solr.apache.org_solrclouds.yaml
index 2f80d77..edfb8f3 100644
--- a/config/crd/bases/solr.apache.org_solrclouds.yaml
+++ b/config/crd/bases/solr.apache.org_solrclouds.yaml
@@ -76,6 +76,11 @@ spec:
           spec:
             description: SolrCloudSpec defines the desired state of SolrCloud
             properties:
+              additionalLibs:
+                description: 'List of paths in the Solr Docker image to load 
in the classpath. Note: Solr Modules will be auto-loaded if specified in the 
"solrModules" property. There is no need to specify them here as well.'
+                items:
+                  type: string
+                type: array
               backupRepositories:
                 description: Allows specification of multiple different 
"repositories" for Solr to use when backing up data.
                 items:
@@ -5730,6 +5735,11 @@ spec:
               solrLogLevel:
                 description: Set the Solr Log level, defaults to INFO
                 type: string
+              solrModules:
+                description: 'List of Solr Modules to be loaded when starting 
Solr Note: You do not need to specify a module if it is required by another 
property (e.g. backupRepositories[].gcs)'
+                items:
+                  type: string
+                type: array
               solrOpts:
                 description: You can add common system properties to the 
SOLR_OPTS environment variable SolrOpts is the string interface for these 
optional settings
                 type: string
diff --git a/controllers/solrcloud_controller_test.go 
b/controllers/solrcloud_controller_test.go
index 17a410d..1c6dce4 100644
--- a/controllers/solrcloud_controller_test.go
+++ b/controllers/solrcloud_controller_test.go
@@ -203,7 +203,7 @@ var _ = FDescribe("SolrCloud controller - General", func() {
                })
                FIt("has the correct resources", func() {
                        By("testing the Solr ConfigMap")
-                       configMap := expectConfigMap(ctx, solrCloud, 
solrCloud.ConfigMapName(), map[string]string{"solr.xml": 
util.GenerateSolrXMLString("")})
+                       configMap := expectConfigMap(ctx, solrCloud, 
solrCloud.ConfigMapName(), map[string]string{"solr.xml": 
util.GenerateSolrXMLString("", []string{}, []string{})})
                        
Expect(configMap.Labels).To(Equal(util.MergeLabelsOrAnnotations(solrCloud.SharedLabelsWith(solrCloud.Labels),
 testConfigMapLabels)), "Incorrect configMap labels")
                        
Expect(configMap.Annotations).To(Equal(testConfigMapAnnotations), "Incorrect 
configMap annotations")
 
@@ -419,6 +419,46 @@ var _ = FDescribe("SolrCloud controller - General", func() 
{
                })
        })
 
+       FContext("Solr Cloud with changing generated SolrXML", func() {
+               BeforeEach(func() {
+                       solrCloud.Spec = solrv1beta1.SolrCloudSpec{
+                               ZookeeperRef: &solrv1beta1.ZookeeperRef{
+                                       ConnectionInfo: 
&solrv1beta1.ZookeeperConnectionInfo{
+                                               InternalConnectionString: 
"host:7271",
+                                       },
+                               },
+                               SolrModules: []string{"analytics", "ltr"},
+                               BackupRepositories: 
[]solrv1beta1.SolrBackupRepository{
+                                       {
+                                               Name: "test1",
+                                               GCS:  
&solrv1beta1.GcsRepository{},
+                                       },
+                               },
+                       }
+               })
+               FIt("has the correct resources", func() {
+                       By("testing the Solr ConfigMap")
+                       configMap := expectConfigMap(ctx, solrCloud, 
solrCloud.ConfigMapName(), map[string]string{"solr.xml": 
util.GenerateSolrXMLStringForCloud(solrCloud)})
+
+                       By("testing the Solr StatefulSet")
+                       statefulSet := expectStatefulSet(ctx, solrCloud, 
solrCloud.StatefulSetName())
+                       
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("making sure the solr.xml is updated and a rolling 
restart happens when libs change")
+                       foundSolrCloud := expectSolrCloudWithChecks(ctx, 
solrCloud, func(g Gomega, found *solrv1beta1.SolrCloud) {
+                               found.Spec.AdditionalLibs = 
[]string{"/ext/lib2", "/ext/lib1"}
+                               g.Expect(k8sClient.Update(ctx, 
found)).To(Succeed(), "Change the additionalLibs for the SolrCloud")
+                       })
+
+                       newConfigMap := expectConfigMap(ctx, solrCloud, 
solrCloud.ConfigMapName(), map[string]string{"solr.xml": 
util.GenerateSolrXMLStringForCloud(foundSolrCloud)})
+
+                       updateSolrXmlMd5 := fmt.Sprintf("%x", 
md5.Sum([]byte(newConfigMap.Data[util.SolrXmlFile])))
+                       expectStatefulSetWithChecks(ctx, solrCloud, 
solrCloud.StatefulSetName(), func(g Gomega, found *appsv1.StatefulSet) {
+                               
g.Expect(found.Spec.Template.Annotations).To(HaveKeyWithValue(util.SolrXmlMd5Annotation,
 updateSolrXmlMd5), "Custom solr.xml MD5 annotation should be updated on the 
pod template.")
+                       })
+               })
+       })
+
        FContext("Solr Cloud with a custom Solr XML ConfigMap", func() {
                testCustomSolrXmlConfigMap := "my-custom-solr-xml"
                BeforeEach(func() {
@@ -545,14 +585,14 @@ var _ = FDescribe("SolrCloud controller - General", 
func() {
                                g.Expect(logXmlVolMount).To(Not(BeNil()), 
"Didn't find the log4j2-xml Volume mount")
                                
g.Expect(logXmlVolMount.MountPath).To(Equal(expectedMountPath), "log4j2-xml 
Volume mount has the wrong path")
 
-                               
g.Expect(found.Spec.Template.Annotations).To(HaveKeyWithValue(util.SolrXmlMd5Annotation,
 fmt.Sprintf("%x", md5.Sum([]byte(util.GenerateSolrXMLString(""))))), "Custom 
solr.xml MD5 annotation should be set on the pod template.")
+                               
g.Expect(found.Spec.Template.Annotations).To(HaveKeyWithValue(util.SolrXmlMd5Annotation,
 fmt.Sprintf("%x", md5.Sum([]byte(util.GenerateSolrXMLString("", []string{}, 
[]string{}))))), "Custom solr.xml MD5 annotation should be set on the pod 
template.")
 
                                
g.Expect(found.Spec.Template.Annotations).To(HaveKeyWithValue(util.LogXmlMd5Annotation,
 fmt.Sprintf("%x", md5.Sum([]byte(configMap.Data[util.LogXmlFile])))), "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)}
                                testPodEnvVariablesWithGomega(g, 
expectedEnvVars, found.Spec.Template.Spec.Containers[0].Env)
                        })
 
-                       expectConfigMap(ctx, solrCloud, 
fmt.Sprintf("%s-solrcloud-configmap", solrCloud.GetName()), 
map[string]string{util.SolrXmlFile: util.GenerateSolrXMLString("")})
+                       expectConfigMap(ctx, solrCloud, 
fmt.Sprintf("%s-solrcloud-configmap", solrCloud.GetName()), 
map[string]string{util.SolrXmlFile: util.GenerateSolrXMLString("", []string{}, 
[]string{})})
 
                        By("updating the user-provided log XML to trigger a pod 
rolling restart")
                        configMap.Data[util.LogXmlFile] = 
"<Configuration>Updated!</Configuration>"
diff --git a/controllers/util/solr_backup_repo_util.go 
b/controllers/util/solr_backup_repo_util.go
index 306455f..f7b9b93 100644
--- a/controllers/util/solr_backup_repo_util.go
+++ b/controllers/util/solr_backup_repo_util.go
@@ -21,15 +21,14 @@ import (
        "fmt"
        solrv1beta1 "github.com/apache/solr-operator/api/v1beta1"
        corev1 "k8s.io/api/core/v1"
+       "sort"
+       "strings"
 )
 
 const (
        BaseBackupRestorePath = "/var/solr/data/backup-restore"
 
        GCSCredentialSecretKey = "service-account-key.json"
-
-       DistLibs    = "/opt/solr/dist"
-       ContribLibs = "/opt/solr/contrib/%s/lib"
 )
 
 func RepoVolumeName(repo *solrv1beta1.SolrBackupRepository) string {
@@ -85,13 +84,17 @@ func RepoVolumeSourceAndMount(repo 
*solrv1beta1.SolrBackupRepository, solrCloudN
        return
 }
 
-func AdditionalRepoLibs(repo *solrv1beta1.SolrBackupRepository) (libs 
[]string) {
+func RepoSolrModules(repo *solrv1beta1.SolrBackupRepository) (libs []string) {
        if repo.GCS != nil {
-               libs = []string{DistLibs, fmt.Sprintf(ContribLibs, 
"gcs-repository")}
+               libs = []string{"gcs-repository"}
        }
        return
 }
 
+func AdditionalRepoLibs(repo *solrv1beta1.SolrBackupRepository) (libs 
[]string) {
+       return
+}
+
 func RepoXML(repo *solrv1beta1.SolrBackupRepository) (xml string) {
        if repo.Managed != nil {
                xml = fmt.Sprintf(`<repository name="%s" 
class="org.apache.solr.core.backup.repository.LocalFileSystemRepository"/>`, 
repo.Name)
@@ -109,6 +112,27 @@ func RepoEnvVars(repo *solrv1beta1.SolrBackupRepository) 
(envVars []corev1.EnvVa
        return envVars
 }
 
+func GenerateBackupRepositoriesForSolrXml(backupRepos 
[]solrv1beta1.SolrBackupRepository) (repoXML string, solrModules []string, 
additionalLibs []string) {
+       if len(backupRepos) == 0 {
+               return
+       }
+       repoXMLs := make([]string, len(backupRepos))
+
+       for i, repo := range backupRepos {
+               solrModules = append(solrModules, RepoSolrModules(&repo)...)
+               additionalLibs = append(additionalLibs, 
AdditionalRepoLibs(&repo)...)
+               repoXMLs[i] = RepoXML(&repo)
+       }
+       sort.Strings(repoXMLs)
+
+       repoXML = fmt.Sprintf(
+               `<backup>
+               %s
+               </backup>`, strings.Join(repoXMLs, `
+`))
+       return
+}
+
 func IsBackupVolumePresent(repo *solrv1beta1.SolrBackupRepository, pod 
*corev1.Pod) bool {
        expectedVolumeName := RepoVolumeName(repo)
        for _, volume := range pod.Spec.Volumes {
diff --git a/controllers/util/solr_backup_repo_util_test.go 
b/controllers/util/solr_backup_repo_util_test.go
index 19f2925..b294ce9 100644
--- a/controllers/util/solr_backup_repo_util_test.go
+++ b/controllers/util/solr_backup_repo_util_test.go
@@ -71,7 +71,21 @@ func TestGCSRepoAdditionalLibs(t *testing.T) {
                        },
                },
        }
-       assert.EqualValues(t, []string{"/opt/solr/dist", 
"/opt/solr/contrib/gcs-repository/lib"}, AdditionalRepoLibs(repo), "GCS Repos 
require no additional libraries for Solr")
+       assert.Empty(t, AdditionalRepoLibs(repo), "GCS Repos require no 
additional libraries for Solr")
+}
+
+func TestGCSRepoSolrModules(t *testing.T) {
+       repo := &solr.SolrBackupRepository{
+               Name: "gcsrepository1",
+               GCS: &solr.GcsRepository{
+                       Bucket: "some-bucket-name1",
+                       GcsCredentialSecret: corev1.SecretKeySelector{
+                               LocalObjectReference: 
corev1.LocalObjectReference{Name: "some-secret-name1"},
+                               Key:                  "some-secret-key",
+                       },
+               },
+       }
+       assert.EqualValues(t, []string{"gcs-repository"}, 
RepoSolrModules(repo), "GCS Repos require the gcs-repository solr module")
 }
 
 func TestManagedRepoAdditionalLibs(t *testing.T) {
@@ -83,3 +97,13 @@ func TestManagedRepoAdditionalLibs(t *testing.T) {
        }
        assert.Empty(t, AdditionalRepoLibs(repo), "Managed Repos require no 
additional libraries for Solr")
 }
+
+func TestManagedRepoSolrModules(t *testing.T) {
+       repo := &solr.SolrBackupRepository{
+               Name: "managedrepository2",
+               Managed: &solr.ManagedRepository{
+                       Volume: corev1.VolumeSource{},
+               },
+       }
+       assert.Empty(t, RepoSolrModules(repo), "Managed Repos require no solr 
modules")
+}
diff --git a/controllers/util/solr_util.go b/controllers/util/solr_util.go
index e2d9c8a..db4455b 100644
--- a/controllers/util/solr_util.go
+++ b/controllers/util/solr_util.go
@@ -51,6 +51,9 @@ const (
        LogXmlFile                       = "log4j2.xml"
 
        DefaultStatefulSetPodManagementPolicy = appsv1.ParallelPodManagement
+
+       DistLibs    = "/opt/solr/dist"
+       ContribLibs = "/opt/solr/contrib/%s/lib"
 )
 
 // GenerateStatefulSet returns a new appsv1.StatefulSet pointer generated for 
the SolrCloud instance
@@ -594,41 +597,9 @@ func generateSolrSetupInitContainers(solrCloud 
*solr.SolrCloud, solrCloudStatus
        return containers
 }
 
-func GenerateBackupRepositoriesForSolrXml(backupRepos 
[]solr.SolrBackupRepository) string {
-       if len(backupRepos) == 0 {
-               return ""
-       }
-       libs := make(map[string]bool, 0)
-       repoXMLs := make([]string, len(backupRepos))
-
-       for i, repo := range backupRepos {
-               for _, lib := range AdditionalRepoLibs(&repo) {
-                       libs[lib] = true
-               }
-               repoXMLs[i] = RepoXML(&repo)
-       }
-       sort.Strings(repoXMLs)
-
-       libXml := ""
-       if len(libs) > 0 {
-               libList := make([]string, 0)
-               for lib := range libs {
-                       libList = append(libList, lib)
-               }
-               sort.Strings(libList)
-               libXml = fmt.Sprintf("<str name=\"sharedLib\">%s</str>", 
strings.Join(libList, ","))
-       }
-
-       return fmt.Sprintf(
-               `%s 
-               <backup>
-               %s
-               </backup>`, libXml, strings.Join(repoXMLs, `
-`))
-}
-
 const DefaultSolrXML = `<?xml version="1.0" encoding="UTF-8" ?>
 <solr>
+  %s
   <solrcloud>
     <str name="host">${host:}</str>
     <int name="hostPort">${hostPort:80}</int>
@@ -661,7 +632,6 @@ func GenerateConfigMap(solrCloud *solr.SolrCloud) 
*corev1.ConfigMap {
                annotations = MergeLabelsOrAnnotations(annotations, 
customOptions.Annotations)
        }
 
-       backupSection := 
GenerateBackupRepositoriesForSolrXml(solrCloud.Spec.BackupRepositories)
        configMap := &corev1.ConfigMap{
                ObjectMeta: metav1.ObjectMeta{
                        Name:        solrCloud.ConfigMapName(),
@@ -670,15 +640,50 @@ func GenerateConfigMap(solrCloud *solr.SolrCloud) 
*corev1.ConfigMap {
                        Annotations: annotations,
                },
                Data: map[string]string{
-                       "solr.xml": GenerateSolrXMLString(backupSection),
+                       "solr.xml": GenerateSolrXMLStringForCloud(solrCloud),
                },
        }
 
        return configMap
 }
 
-func GenerateSolrXMLString(backupSection string) string {
-       return fmt.Sprintf(DefaultSolrXML, backupSection)
+func GenerateSolrXMLStringForCloud(solrCloud *solr.SolrCloud) string {
+       backupSection, solrModules, additionalLibs := 
GenerateBackupRepositoriesForSolrXml(solrCloud.Spec.BackupRepositories)
+       solrModules = append(solrModules, solrCloud.Spec.SolrModules...)
+       additionalLibs = append(additionalLibs, 
solrCloud.Spec.AdditionalLibs...)
+       return GenerateSolrXMLString(backupSection, solrModules, additionalLibs)
+}
+
+func GenerateSolrXMLString(backupSection string, solrModules []string, 
additionalLibs []string) string {
+       return fmt.Sprintf(DefaultSolrXML, 
GenerateAdditionalLibXMLPart(solrModules, additionalLibs), backupSection)
+}
+
+func GenerateAdditionalLibXMLPart(solrModules []string, additionalLibs 
[]string) string {
+       libs := make(map[string]bool, 0)
+
+       // Add all module library locations
+       if len(solrModules) > 0 {
+               libs[DistLibs] = true
+       }
+       for _, module := range solrModules {
+               libs[fmt.Sprintf(ContribLibs, module)] = true
+       }
+
+       // Add all custom library locations
+       for _, libPath := range additionalLibs {
+               libs[libPath] = true
+       }
+
+       libXml := ""
+       if len(libs) > 0 {
+               libList := make([]string, 0)
+               for lib := range libs {
+                       libList = append(libList, lib)
+               }
+               sort.Strings(libList)
+               libXml = fmt.Sprintf("<str name=\"sharedLib\">%s</str>", 
strings.Join(libList, ","))
+       }
+       return libXml
 }
 
 // GenerateCommonService returns a new corev1.Service pointer generated for 
the entire SolrCloud instance
diff --git a/controllers/util/solr_util_test.go 
b/controllers/util/solr_util_test.go
index 5ebb728..5ffa8a9 100644
--- a/controllers/util/solr_util_test.go
+++ b/controllers/util/solr_util_test.go
@@ -25,7 +25,10 @@ import (
 )
 
 func TestNoRepositoryXmlGeneratedWhenNoRepositoriesExist(t *testing.T) {
-       assert.Equal(t, "", 
GenerateBackupRepositoriesForSolrXml(make([]solr.SolrBackupRepository, 0)), 
"There should be no backup XML when no backupRepos are specified")
+       xmlString, modules, libs := 
GenerateBackupRepositoriesForSolrXml(make([]solr.SolrBackupRepository, 0))
+       assert.Equal(t, "", xmlString, "There should be no backup XML when no 
backupRepos are specified")
+       assert.Empty(t, modules, "There should be no modules for the 
backupRepos when no backupRepos are specified")
+       assert.Empty(t, libs, "There should be no libs for the backupRepos when 
no backupRepos are specified")
 }
 
 func TestGeneratedSolrXmlContainsEntryForEachRepository(t *testing.T) {
@@ -64,7 +67,7 @@ func TestGeneratedSolrXmlContainsEntryForEachRepository(t 
*testing.T) {
                        },
                },
        }
-       xmlString := GenerateBackupRepositoriesForSolrXml(repos)
+       xmlString, modules, libs := GenerateBackupRepositoriesForSolrXml(repos)
 
        // These assertions don't fully guarantee valid XML, but they at least 
make sure each repo is defined and uses the correct class.
        // If we wanted to bring in an xpath library for assertions we could be 
a lot more comprehensive here.
@@ -73,6 +76,111 @@ func TestGeneratedSolrXmlContainsEntryForEachRepository(t 
*testing.T) {
        assert.Containsf(t, xmlString, "<repository name=\"gcsrepository1\" 
class=\"org.apache.solr.gcs.GCSBackupRepository\">", "Did not find '%s' in the 
list of backup repositories", "gcsrepository1")
        assert.Containsf(t, xmlString, "<repository name=\"gcsrepository2\" 
class=\"org.apache.solr.gcs.GCSBackupRepository\">", "Did not find '%s' in the 
list of backup repositories", "gcsrepository2")
 
-       // Since GCS repositories are defined, make sure the contrib is on the 
classpath
-       assert.Contains(t, xmlString, "<str 
name=\"sharedLib\">/opt/solr/contrib/gcs-repository/lib,/opt/solr/dist</str>")
+       assert.Contains(t, modules, "gcs-repository", "The modules for the 
backupRepos should contain gcs-repository")
+       assert.Empty(t, libs, "There should be no libs for the backupRepos")
+}
+
+func TestGenerateAdditionalLibXMLPart(t *testing.T) {
+       // Just 1 repeated solr module
+       xmlString := GenerateAdditionalLibXMLPart([]string{"gcs-repository", 
"gcs-repository"}, []string{})
+       assert.EqualValuesf(t, xmlString, "<str 
name=\"sharedLib\">/opt/solr/contrib/gcs-repository/lib,/opt/solr/dist</str>", 
"Wrong sharedLib xml for just 1 repeated solr module")
+
+       // Just 2 different solr modules
+       xmlString = GenerateAdditionalLibXMLPart([]string{"gcs-repository", 
"analytics"}, []string{})
+       assert.EqualValuesf(t, xmlString, "<str 
name=\"sharedLib\">/opt/solr/contrib/analytics/lib,/opt/solr/contrib/gcs-repository/lib,/opt/solr/dist</str>",
 "Wrong sharedLib xml for just 2 different solr modules")
+
+       // Just 2 repeated libs
+       xmlString = GenerateAdditionalLibXMLPart([]string{}, 
[]string{"/ext/lib", "/ext/lib"})
+       assert.EqualValuesf(t, xmlString, "<str 
name=\"sharedLib\">/ext/lib</str>", "Wrong sharedLib xml for just 1 repeated 
additional lib")
+
+       // Just 2 different libs
+       xmlString = GenerateAdditionalLibXMLPart([]string{}, 
[]string{"/ext/lib2", "/ext/lib1"})
+       assert.EqualValuesf(t, xmlString, "<str 
name=\"sharedLib\">/ext/lib1,/ext/lib2</str>", "Wrong sharedLib xml for just 2 
different additional libs")
+
+       // Combination of everything
+       xmlString = GenerateAdditionalLibXMLPart([]string{"gcs-repository", 
"analytics", "analytics"}, []string{"/ext/lib2", "/ext/lib2", "/ext/lib1"})
+       assert.EqualValuesf(t, xmlString, "<str 
name=\"sharedLib\">/ext/lib1,/ext/lib2,/opt/solr/contrib/analytics/lib,/opt/solr/contrib/gcs-repository/lib,/opt/solr/dist</str>",
 "Wrong sharedLib xml for mix of additional libs and solr modules")
+}
+
+func TestGenerateSolrXMLStringForCloud(t *testing.T) {
+       // All 3 options that factor into the sharedLib
+       solrCloud := &solr.SolrCloud{
+               Spec: solr.SolrCloudSpec{
+                       BackupRepositories: []solr.SolrBackupRepository{
+                               {
+                                       Name: "test",
+                                       GCS:  &solr.GcsRepository{},
+                               },
+                       },
+                       AdditionalLibs: []string{"/ext/lib2", "/ext/lib1"},
+                       SolrModules:    []string{"ltr", "analytics"},
+               },
+       }
+       assert.Containsf(t, GenerateSolrXMLStringForCloud(solrCloud), "<str 
name=\"sharedLib\">/ext/lib1,/ext/lib2,/opt/solr/contrib/analytics/lib,/opt/solr/contrib/gcs-repository/lib,/opt/solr/contrib/ltr/lib,/opt/solr/dist</str>",
 "Wrong sharedLib xml for a cloud with a backupRepo, additionalLibs and 
solrModules")
+
+       // Just SolrModules and AdditionalLibs
+       solrCloud = &solr.SolrCloud{
+               Spec: solr.SolrCloudSpec{
+                       AdditionalLibs: []string{"/ext/lib2", "/ext/lib1"},
+                       SolrModules:    []string{"ltr", "analytics"},
+               },
+       }
+       assert.Containsf(t, GenerateSolrXMLStringForCloud(solrCloud), "<str 
name=\"sharedLib\">/ext/lib1,/ext/lib2,/opt/solr/contrib/analytics/lib,/opt/solr/contrib/ltr/lib,/opt/solr/dist</str>",
 "Wrong sharedLib xml for a cloud with additionalLibs and solrModules")
+
+       // Just SolrModules and Backups
+       solrCloud = &solr.SolrCloud{
+               Spec: solr.SolrCloudSpec{
+                       BackupRepositories: []solr.SolrBackupRepository{
+                               {
+                                       Name: "test",
+                                       GCS:  &solr.GcsRepository{},
+                               },
+                       },
+                       SolrModules: []string{"ltr", "analytics"},
+               },
+       }
+       assert.Containsf(t, GenerateSolrXMLStringForCloud(solrCloud), "<str 
name=\"sharedLib\">/opt/solr/contrib/analytics/lib,/opt/solr/contrib/gcs-repository/lib,/opt/solr/contrib/ltr/lib,/opt/solr/dist</str>",
 "Wrong sharedLib xml for a cloud with a backupRepo and solrModules")
+
+       // Just AdditionalLibs and Backups
+       solrCloud = &solr.SolrCloud{
+               Spec: solr.SolrCloudSpec{
+                       BackupRepositories: []solr.SolrBackupRepository{
+                               {
+                                       Name: "test",
+                                       GCS:  &solr.GcsRepository{},
+                               },
+                       },
+                       AdditionalLibs: []string{"/ext/lib2", "/ext/lib1"},
+               },
+       }
+       assert.Containsf(t, GenerateSolrXMLStringForCloud(solrCloud), "<str 
name=\"sharedLib\">/ext/lib1,/ext/lib2,/opt/solr/contrib/gcs-repository/lib,/opt/solr/dist</str>",
 "Wrong sharedLib xml for a cloud with a backupRepo and additionalLibs")
+
+       // Just SolrModules
+       solrCloud = &solr.SolrCloud{
+               Spec: solr.SolrCloudSpec{
+                       SolrModules: []string{"ltr", "analytics"},
+               },
+       }
+       assert.Containsf(t, GenerateSolrXMLStringForCloud(solrCloud), "<str 
name=\"sharedLib\">/opt/solr/contrib/analytics/lib,/opt/solr/contrib/ltr/lib,/opt/solr/dist</str>",
 "Wrong sharedLib xml for a cloud with just solrModules")
+
+       // Just Backups
+       solrCloud = &solr.SolrCloud{
+               Spec: solr.SolrCloudSpec{
+                       BackupRepositories: []solr.SolrBackupRepository{
+                               {
+                                       Name: "test",
+                                       GCS:  &solr.GcsRepository{},
+                               },
+                       },
+               },
+       }
+       assert.Containsf(t, GenerateSolrXMLStringForCloud(solrCloud), "<str 
name=\"sharedLib\">/opt/solr/contrib/gcs-repository/lib,/opt/solr/dist</str>", 
"Wrong sharedLib xml for a cloud with just a backupRepo")
+
+       // Just AdditionalLibs
+       solrCloud = &solr.SolrCloud{
+               Spec: solr.SolrCloudSpec{
+                       AdditionalLibs: []string{"/ext/lib2", "/ext/lib1"},
+               },
+       }
+       assert.Containsf(t, GenerateSolrXMLStringForCloud(solrCloud), "<str 
name=\"sharedLib\">/ext/lib1,/ext/lib2</str>", "Wrong sharedLib xml for a cloud 
with a just additionalLibs")
 }
diff --git a/docs/solr-cloud/solr-cloud-crd.md 
b/docs/solr-cloud/solr-cloud-crd.md
index 783d5ed..fb3bea9 100644
--- a/docs/solr-cloud/solr-cloud-crd.md
+++ b/docs/solr-cloud/solr-cloud-crd.md
@@ -20,6 +20,20 @@
 The SolrCloud CRD allows users to spin up a Solr cloud in a very configurable 
way.
 Those configuration options are laid out on this page.
 
+## Solr Options
+
+The SolrCloud CRD gives users the ability to customize how Solr is run.
+
+### Solr Modules and Additional Libraries
+_Since v0.5.0_
+
+Solr comes packaged with modules that can be loaded optionally, known as 
either Solr Modules or Solr Contrib Modules.
+By default they are not included in the classpath of Solr, so they have to be 
explicitly enabled.
+Use the **`SolrCloud.spec.solrModules`** property to add a list of module 
names, not paths, and they will automatically be enabled for the solrCloud.
+
+However, users might want to include custom code that is not an official Solr 
Module.
+In order to facilitate this, the **`SolrCloud.spec.additionalLibs`** property 
takes a list of paths to folders, containing jars to load in the classpath of 
the SolrCloud.
+
 ## Data Storage
 
 The SolrCloud CRD gives the option for users to use either
diff --git a/example/test_solrcloud.yaml b/example/test_solrcloud.yaml
index 258ab9d..1757bb0 100644
--- a/example/test_solrcloud.yaml
+++ b/example/test_solrcloud.yaml
@@ -36,6 +36,9 @@ spec:
   solrImage:
     tag: 8.7.0
   solrJavaMem: "-Xms1g -Xmx3g"
+  solrModules:
+    - jaegertracer-configurator
+    - ltr
   customSolrKubeOptions:
     podOptions:
       resources:
diff --git a/helm/solr-operator/Chart.yaml b/helm/solr-operator/Chart.yaml
index 47d35fa..4d97f4f 100644
--- a/helm/solr-operator/Chart.yaml
+++ b/helm/solr-operator/Chart.yaml
@@ -100,6 +100,15 @@ annotations:
           url: https://github.com/apache/solr-operator/issues/322
         - name: Github PR
           url: https://github.com/apache/solr-operator/pull/324
+    - kind: added
+      description: Add support for using Solr Modules (contrib) and additional 
libraries
+      links:
+        - name: Github Issue
+          url: https://github.com/apache/solr-operator/issues/329
+        - name: Github PR
+          url: https://github.com/apache/solr-operator/pull/332
+        - name: Solr Modules
+          url: https://github.com/apache/solr/tree/main/solr/contrib
   artifacthub.io/images: |
     - name: solr-operator
       image: apache/solr-operator:v0.5.0-prerelease
diff --git a/helm/solr-operator/crds/crds.yaml 
b/helm/solr-operator/crds/crds.yaml
index 91dc769..d364816 100644
--- a/helm/solr-operator/crds/crds.yaml
+++ b/helm/solr-operator/crds/crds.yaml
@@ -1205,6 +1205,11 @@ spec:
           spec:
             description: SolrCloudSpec defines the desired state of SolrCloud
             properties:
+              additionalLibs:
+                description: 'List of paths in the Solr Docker image to load 
in the classpath. Note: Solr Modules will be auto-loaded if specified in the 
"solrModules" property. There is no need to specify them here as well.'
+                items:
+                  type: string
+                type: array
               backupRepositories:
                 description: Allows specification of multiple different 
"repositories" for Solr to use when backing up data.
                 items:
@@ -6859,6 +6864,11 @@ spec:
               solrLogLevel:
                 description: Set the Solr Log level, defaults to INFO
                 type: string
+              solrModules:
+                description: 'List of Solr Modules to be loaded when starting 
Solr Note: You do not need to specify a module if it is required by another 
property (e.g. backupRepositories[].gcs)'
+                items:
+                  type: string
+                type: array
               solrOpts:
                 description: You can add common system properties to the 
SOLR_OPTS environment variable SolrOpts is the string interface for these 
optional settings
                 type: string
diff --git a/helm/solr/README.md b/helm/solr/README.md
index 687a4ee..b001c2b 100644
--- a/helm/solr/README.md
+++ b/helm/solr/README.md
@@ -90,6 +90,8 @@ The command removes the SolrCloud resource, and then 
Kubernetes will garbage col
 | solrOptions.javaOpts | string | `""` | Additional java arguments to pass via 
the command line |
 | solrOptions.logLevel | string | `"INFO"` | Log level to run Solr under |
 | solrOptions.gcTune | string | `""` | GC Tuning parameters for Solr |
+| solrOptions.solrModules | []string | | List of packaged Solr Modules to load 
when running Solr. Note: There is no need to specify solr modules necessary for 
other parts of the Spec (i.e. `backupRepositories[].gcs`), those will be added 
automatically. |
+| solrOptions.additionalLibs | []string | | List of paths in the Solr Image to 
add to the classPath when running Solr. Note: There is no need to include paths 
for solrModules here if already listed in `solrModules`, those paths will be 
added automatically. |
 | solrOptions.security.authenticationType | string | `""` | Type of 
authentication to use for Solr |
 | solrOptions.security.basicAuthSecret | string | `""` | Name of Secret in the 
same namespace that stores the basicAuth information for the Solr user |
 | solrOptions.security.probesRequireAuth | boolean | | Whether the probes for 
the SolrCloud pod require auth |
diff --git a/helm/solr/templates/solrcloud.yaml 
b/helm/solr/templates/solrcloud.yaml
index 3916713..d05c08b 100644
--- a/helm/solr/templates/solrcloud.yaml
+++ b/helm/solr/templates/solrcloud.yaml
@@ -152,6 +152,16 @@ spec:
     {{- end }}
   {{- end }}
 
+  {{- if .Values.solrOptions.solrModules }}
+  solrModules:
+    {{- toYaml .Values.solrOptions.solrModules | nindent 4 }}
+  {{- end }}
+
+  {{- if .Values.solrOptions.additionalLibs }}
+  additionalLibs:
+    {{- toYaml .Values.solrOptions.additionalLibs | nindent 4 }}
+  {{- end }}
+
   {{- if .Values.backupRepositories }}
   backupRepositories:
     {{- toYaml .Values.backupRepositories | nindent 4 }}
diff --git a/helm/solr/values.yaml b/helm/solr/values.yaml
index cdb7354..773a5a5 100644
--- a/helm/solr/values.yaml
+++ b/helm/solr/values.yaml
@@ -54,6 +54,8 @@ solrOptions:
   javaOpts: ""
   logLevel: ""
   gcTune: ""
+  solrModules: []
+  additionalLibs: []
 
   # Enable authentication for the Solr Cloud
   # More information can be found at:

Reply via email to