Skip to content

Commit 07b2d15

Browse files
danielmeintclaude
andcommitted
Add Solr 10 compatibility support
Solr 10 introduced several breaking changes that prevent the operator from successfully starting and managing SolrClouds. This change adds version-conditional behavior for Solr 10 while preserving full backwards compatibility with Solr 9.x. Major changes covered: * solr.xml — Solr 10 removed several `<solrcloud>` parameters (genericCoreNodeNames, hostContext, allowPaths, metricsEnabled). A new `DefaultSolrXMLForSolr10` template is selected when the image tag indicates Solr 10+. * Host advertise — the `host` system property was renamed to `solr.host.advertise`. A `SOLR_HOST_ADVERTISE` env var is now set on Solr 10 pods. * Modules — Solr 10 removed `/opt/solr/contrib/<module>/lib` and `/opt/solr/dist`. Modules are now loaded via the `SOLR_MODULES` env var, and the operator no longer emits contrib paths in `sharedLib` for Solr 10. * hostPort sysprop — `-DhostPort` is no longer needed in Solr 10 and is skipped. * zkcli.sh removed — `setUrlSchemeClusterPropCmd` (TLS setup) now uses `solr zk cp` for Solr 10 instead of the removed `cloud-scripts/zkcli.sh`. * `solr api` CLI — `-get URL` was replaced with `--solr-url URL`. Secure probes (`useSecureProbe`) and the e2e helper (`callSolrApiInPod`) emit the new flag for Solr 10. * Basic auth — Solr 10 no longer honors the deprecated `-Dbasicauth=user:pass` JAVA_TOOL_OPTIONS path. The e2e helper now uses the native `--credentials user:pass` flag for Solr 10. Version detection lives on the `SolrCloud` type as `(*SolrCloud).IsSolr10OrLater()`, which parses the major version from the image tag and treats unparseable tags (e.g. "latest", "nightly") and a nil `SolrImage` as pre-10 for backwards compatibility. A package-level `IsSolr10OrLater(imageTag string)` is also exported for callers that only have a raw image string. Unit tests cover the version parser, both `solr.xml` templates, and both branches of `useSecureProbe`. End-to-end tests have been verified against Solr 10.0.0 across Basic, Scaling (with replica migration), Security JSON (provided + bootstrapped), TLS (Secrets and Mounted Dir, including ClientAuth Need/Want, CheckPeerName, VerifyClientHostname), Local-directory backups (recurring + single), Ingress, and Managed Rolling Upgrades. Solr 9.8.0 Basic was verified as a regression baseline. The Prometheus exporter is not covered: `solr-exporter` was removed from the Solr distribution in 10, and metrics are now expected to be scraped from Solr's built-in endpoint. That work is tracked separately in #820. Refs #821, #826. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent e81458c commit 07b2d15

7 files changed

Lines changed: 319 additions & 45 deletions

File tree

api/v1beta1/solrcloud_types.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,6 +1327,42 @@ func (zkInfo ZookeeperConnectionInfo) ZkConnectionString() string {
13271327
return zkInfo.InternalConnectionString + zkInfo.ChRoot
13281328
}
13291329

1330+
// Solr10MajorVersion is the version threshold for Solr 10+ incompatible changes.
1331+
const Solr10MajorVersion = 10
1332+
1333+
// SolrMajorVersion extracts the major version number from a Solr image tag.
1334+
// Returns 0 if the tag cannot be parsed (e.g. "latest", "nightly", custom tags).
1335+
func SolrMajorVersion(imageTag string) int {
1336+
tag := strings.TrimPrefix(imageTag, "v")
1337+
if idx := strings.Index(tag, "-"); idx >= 0 {
1338+
tag = tag[:idx]
1339+
}
1340+
major := tag
1341+
if idx := strings.Index(tag, "."); idx >= 0 {
1342+
major = tag[:idx]
1343+
}
1344+
v, err := strconv.Atoi(major)
1345+
if err != nil {
1346+
return 0
1347+
}
1348+
return v
1349+
}
1350+
1351+
// IsSolr10OrLater returns true if the given image tag represents Solr 10.0 or later.
1352+
// Unparseable tags (e.g. "latest") are treated as pre-10 for backwards compatibility.
1353+
func IsSolr10OrLater(imageTag string) bool {
1354+
return SolrMajorVersion(imageTag) >= Solr10MajorVersion
1355+
}
1356+
1357+
// IsSolr10OrLater returns true if this SolrCloud's image tag represents Solr 10.0 or later.
1358+
// A nil SolrImage is treated as pre-10.
1359+
func (sc *SolrCloud) IsSolr10OrLater() bool {
1360+
if sc.Spec.SolrImage == nil {
1361+
return false
1362+
}
1363+
return IsSolr10OrLater(sc.Spec.SolrImage.Tag)
1364+
}
1365+
13301366
// UsesHeadlessService returns whether the given solrCloud requires a headless service to be created for it.
13311367
// solrCloud: SolrCloud instance
13321368
func (sc *SolrCloud) UsesHeadlessService() bool {

controllers/solrcloud_controller_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ var _ = FDescribe("SolrCloud controller - General", func() {
250250
})
251251
FIt("has the correct resources", func(ctx context.Context) {
252252
By("testing the Solr ConfigMap")
253-
configMap := expectConfigMap(ctx, solrCloud, solrCloud.ConfigMapName(), map[string]string{"solr.xml": util.GenerateSolrXMLString("", []string{}, []string{})})
253+
configMap := expectConfigMap(ctx, solrCloud, solrCloud.ConfigMapName(), map[string]string{"solr.xml": util.GenerateSolrXMLString("", []string{}, []string{}, false)})
254254
Expect(configMap.Labels).To(Equal(util.MergeLabelsOrAnnotations(solrCloud.SharedLabelsWith(solrCloud.Labels), testConfigMapLabels)), "Incorrect configMap labels")
255255
Expect(configMap.Annotations).To(Equal(testConfigMapAnnotations), "Incorrect configMap annotations")
256256

@@ -661,14 +661,14 @@ var _ = FDescribe("SolrCloud controller - General", func() {
661661
g.Expect(logXmlVolMount).To(Not(BeNil()), "Didn't find the log4j2-xml Volume mount")
662662
g.Expect(logXmlVolMount.MountPath).To(Equal(expectedMountPath), "log4j2-xml Volume mount has the wrong path")
663663

664-
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.")
664+
g.Expect(found.Spec.Template.Annotations).To(HaveKeyWithValue(util.SolrXmlMd5Annotation, fmt.Sprintf("%x", md5.Sum([]byte(util.GenerateSolrXMLString("", []string{}, []string{}, false))))), "Custom solr.xml MD5 annotation should be set on the pod template.")
665665

666666
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.")
667667
expectedEnvVars := map[string]string{"LOG4J_PROPS": fmt.Sprintf("%s/%s", expectedMountPath, util.LogXmlFile)}
668668
testPodEnvVariablesWithGomega(g, expectedEnvVars, found.Spec.Template.Spec.Containers[0].Env)
669669
})
670670

671-
expectConfigMap(ctx, solrCloud, fmt.Sprintf("%s-solrcloud-configmap", solrCloud.GetName()), map[string]string{util.SolrXmlFile: util.GenerateSolrXMLString("", []string{}, []string{})})
671+
expectConfigMap(ctx, solrCloud, fmt.Sprintf("%s-solrcloud-configmap", solrCloud.GetName()), map[string]string{util.SolrXmlFile: util.GenerateSolrXMLString("", []string{}, []string{}, false)})
672672

673673
By("updating the user-provided log XML to trigger a pod rolling restart")
674674
configMap.Data[util.LogXmlFile] = "<Configuration>Updated!</Configuration>"

controllers/util/solr_security_util.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,11 @@ func useSecureProbe(solrCloud *solr.SolrCloud, probe *corev1.Probe, mountPath st
520520
javaToolOptionsOutputFilter = ""
521521
}
522522

523-
probeCommand := fmt.Sprintf("%ssolr api -get \"%s://${SOLR_HOST}:%d%s\"%s", javaToolOptionsStr, solrCloud.UrlScheme(false), probe.HTTPGet.Port.IntVal, probe.HTTPGet.Path, javaToolOptionsOutputFilter)
523+
apiUrlFlag := "-get"
524+
if solrCloud.IsSolr10OrLater() {
525+
apiUrlFlag = "--solr-url"
526+
}
527+
probeCommand := fmt.Sprintf("%ssolr api %s \"%s://${SOLR_HOST}:%d%s\"%s", javaToolOptionsStr, apiUrlFlag, solrCloud.UrlScheme(false), probe.HTTPGet.Port.IntVal, probe.HTTPGet.Path, javaToolOptionsOutputFilter)
524528
probeCommand = regexp.MustCompile(`\s+`).ReplaceAllString(strings.TrimSpace(probeCommand), " ")
525529

526530
// use an Exec instead of an HTTP GET

controllers/util/solr_tls_util.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -756,8 +756,23 @@ func mountedTLSPath(dir *solr.MountedTLSDirectory, fileName string, defaultName
756756
return fmt.Sprintf("%s/%s", dir.Path, fileName)
757757
}
758758

759-
// Command to set the urlScheme cluster prop to "https"
760-
func setUrlSchemeClusterPropCmd() string {
759+
// Command to set the urlScheme cluster prop to "https".
760+
// Solr 10 removed zkcli.sh, so we use solr zk commands instead.
761+
func setUrlSchemeClusterPropCmd(isSolr10 bool) string {
762+
if isSolr10 {
763+
// Use solr zk to read the current clusterprops.json, merge urlScheme, and write it back.
764+
// If /clusterprops.json doesn't exist yet, create it with just the urlScheme property.
765+
return "solr zk cp zk:/clusterprops.json /tmp/clusterprops.json -z ${ZK_HOST} >/dev/null 2>&1 || echo '{}' > /tmp/clusterprops.json; " +
766+
// Use a simple sed/awk to inject or update the urlScheme property
767+
"if grep -q 'urlScheme' /tmp/clusterprops.json; then " +
768+
" sed -i 's/\"urlScheme\":\"[^\"]*\"/\"urlScheme\":\"https\"/' /tmp/clusterprops.json; " +
769+
"else " +
770+
" sed -i 's/^{/{\"urlScheme\":\"https\",/' /tmp/clusterprops.json; " +
771+
" sed -i 's/,}/}/' /tmp/clusterprops.json; " +
772+
"fi; " +
773+
"solr zk cp /tmp/clusterprops.json zk:/clusterprops.json -z ${ZK_HOST}; " +
774+
"solr zk cp zk:/clusterprops.json /dev/stdout -z ${ZK_HOST}; "
775+
}
761776
return "/opt/solr/server/scripts/cloud-scripts/zkcli.sh -zkhost ${ZK_HOST} -cmd clusterprop -name urlScheme -val https" +
762777
"; /opt/solr/server/scripts/cloud-scripts/zkcli.sh -zkhost ${ZK_HOST} -cmd get /clusterprops.json;"
763778
}

controllers/util/solr_util.go

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,15 @@ func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCl
148148
},
149149
}
150150

151+
isSolr10 := solrCloud.IsSolr10OrLater()
152+
151153
// Keep track of the SolrOpts that the Solr Operator needs to set
152154
// These will be added to the SolrOpts given by the user.
153-
allSolrOpts := []string{"-DhostPort=$(SOLR_NODE_PORT)"}
155+
allSolrOpts := []string{}
156+
if !isSolr10 {
157+
// The hostPort sysprop is only needed for Solr 9 and earlier
158+
allSolrOpts = append(allSolrOpts, "-DhostPort=$(SOLR_NODE_PORT)")
159+
}
154160

155161
// Volumes & Mounts
156162
solrVolumes := []corev1.Volume{
@@ -377,19 +383,43 @@ func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCl
377383
Name: "SOLR_HOST",
378384
Value: solrHostName,
379385
},
380-
{
386+
}
387+
388+
// Solr 10+ uses SOLR_HOST_ADVERTISE instead of the host setting in solr.xml
389+
if isSolr10 {
390+
envVars = append(envVars, corev1.EnvVar{
391+
Name: "SOLR_HOST_ADVERTISE",
392+
Value: solrHostName,
393+
})
394+
}
395+
396+
// Solr 10+ loads modules via SOLR_MODULES env var instead of sharedLib contrib paths
397+
if isSolr10 {
398+
backupSection, solrModules, _ := GenerateBackupRepositoriesForSolrXml(solrCloud.Spec.BackupRepositories)
399+
_ = backupSection
400+
solrModules = append(solrModules, solrCloud.Spec.SolrModules...)
401+
if len(solrModules) > 0 {
402+
envVars = append(envVars, corev1.EnvVar{
403+
Name: "SOLR_MODULES",
404+
Value: strings.Join(solrModules, ","),
405+
})
406+
}
407+
}
408+
409+
envVars = append(envVars,
410+
corev1.EnvVar{
381411
Name: "SOLR_LOG_LEVEL",
382412
Value: solrCloud.Spec.SolrLogLevel,
383413
},
384-
{
414+
corev1.EnvVar{
385415
Name: "GC_TUNE",
386416
Value: solrCloud.Spec.SolrGCTune,
387417
},
388-
{
418+
corev1.EnvVar{
389419
Name: "SOLR_STOP_WAIT",
390420
Value: strconv.FormatInt(solrStopWait, 10),
391421
},
392-
}
422+
)
393423

394424
// Add all necessary information for connection to Zookeeper
395425
zkEnvVars, zkSolrOpt, _ := createZkConnectionEnvVars(solrCloud, solrCloudStatus)
@@ -832,6 +862,37 @@ const DefaultSolrXML = `<?xml version="1.0" encoding="UTF-8" ?>
832862
</solr>
833863
`
834864

865+
// DefaultSolrXMLForSolr10 is the solr.xml template for Solr 10+.
866+
// Removed settings that no longer exist in Solr 10:
867+
// - hostContext (always "solr", no longer configurable)
868+
// - genericCoreNodeNames (always true, no longer configurable)
869+
// - allowPaths (removed)
870+
// - metrics enabled (removed, metrics always enabled)
871+
//
872+
// Note: "host" is still required in solr.xml. Solr 10 renamed the system property
873+
// from "host" to "solr.host.advertise" but the XML element is still needed.
874+
const DefaultSolrXMLForSolr10 = `<?xml version="1.0" encoding="UTF-8" ?>
875+
<solr>
876+
%s
877+
<solrcloud>
878+
<str name="host">${solr.host.advertise:}</str>
879+
<int name="hostPort">${solr.port.advertise:80}</int>
880+
<int name="zkClientTimeout">${zkClientTimeout:30000}</int>
881+
<int name="distribUpdateSoTimeout">${distribUpdateSoTimeout:600000}</int>
882+
<int name="distribUpdateConnTimeout">${distribUpdateConnTimeout:60000}</int>
883+
<str name="zkCredentialsProvider">${zkCredentialsProvider:org.apache.solr.common.cloud.DefaultZkCredentialsProvider}</str>
884+
<str name="zkACLProvider">${zkACLProvider:org.apache.solr.common.cloud.DefaultZkACLProvider}</str>
885+
</solrcloud>
886+
<shardHandlerFactory name="shardHandlerFactory"
887+
class="HttpShardHandlerFactory">
888+
<int name="socketTimeout">${socketTimeout:600000}</int>
889+
<int name="connTimeout">${connTimeout:60000}</int>
890+
</shardHandlerFactory>
891+
<int name="maxBooleanClauses">${solr.max.booleanClauses:1024}</int>
892+
%s
893+
</solr>
894+
`
895+
835896
// GenerateConfigMap returns a new corev1.ConfigMap pointer generated for the SolrCloud instance solr.xml
836897
// solrCloud: SolrCloud instance
837898
func GenerateConfigMap(solrCloud *solr.SolrCloud) *corev1.ConfigMap {
@@ -863,28 +924,36 @@ func GenerateSolrXMLStringForCloud(solrCloud *solr.SolrCloud) string {
863924
backupSection, solrModules, additionalLibs := GenerateBackupRepositoriesForSolrXml(solrCloud.Spec.BackupRepositories)
864925
solrModules = append(solrModules, solrCloud.Spec.SolrModules...)
865926
additionalLibs = append(additionalLibs, solrCloud.Spec.AdditionalLibs...)
866-
return GenerateSolrXMLString(backupSection, solrModules, additionalLibs)
927+
return GenerateSolrXMLString(backupSection, solrModules, additionalLibs, solrCloud.IsSolr10OrLater())
867928
}
868929

869-
func GenerateSolrXMLString(backupSection string, solrModules []string, additionalLibs []string) string {
870-
return fmt.Sprintf(DefaultSolrXML, GenerateAdditionalLibXMLPart(solrModules, additionalLibs), backupSection)
930+
func GenerateSolrXMLString(backupSection string, solrModules []string, additionalLibs []string, isSolr10 bool) string {
931+
template := DefaultSolrXML
932+
if isSolr10 {
933+
template = DefaultSolrXMLForSolr10
934+
}
935+
return fmt.Sprintf(template, GenerateAdditionalLibXMLPart(solrModules, additionalLibs, isSolr10), backupSection)
871936
}
872937

873-
func GenerateAdditionalLibXMLPart(solrModules []string, additionalLibs []string) string {
938+
func GenerateAdditionalLibXMLPart(solrModules []string, additionalLibs []string, isSolr10 bool) string {
874939
libs := make(map[string]bool, 0)
875940

876941
// Placeholder for users to specify libs via sysprop
877942
libs[SysPropLibPlaceholder] = true
878943

879-
// Add all module library locations
880-
if len(solrModules) > 0 {
881-
libs[DistLibs] = true
882-
}
883-
for _, module := range solrModules {
884-
libs[fmt.Sprintf(ContribLibs, module)] = true
944+
if !isSolr10 {
945+
// Solr 9 and earlier: modules are loaded via sharedLib paths to contrib directories
946+
if len(solrModules) > 0 {
947+
libs[DistLibs] = true
948+
}
949+
for _, module := range solrModules {
950+
libs[fmt.Sprintf(ContribLibs, module)] = true
951+
}
885952
}
953+
// Solr 10+: modules are loaded via SOLR_MODULES env var, not sharedLib contrib paths.
954+
// The contrib directory no longer exists in the Solr 10 Docker image.
886955

887-
// Add all custom library locations
956+
// Add all custom library locations (these still work in Solr 10)
888957
for _, libPath := range additionalLibs {
889958
libs[libPath] = true
890959
}
@@ -1250,7 +1319,7 @@ func generateZKInteractionInitContainer(solrCloud *solr.SolrCloud, solrCloudStat
12501319
}
12511320

12521321
if solrCloud.Spec.SolrTLS != nil {
1253-
cmd += setUrlSchemeClusterPropCmd()
1322+
cmd += setUrlSchemeClusterPropCmd(solrCloud.IsSolr10OrLater())
12541323
}
12551324

12561325
if security != nil && security.SecurityJson != "" {

0 commit comments

Comments
 (0)