From 81bada5bb4f8ad23bb2e9aeacb4ed9135cca5588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Mon, 9 Nov 2026 01:25:45 +0100 Subject: [PATCH 1/2] Expose the node's zone label to Solr as availability_zone New SolrCloud option `availabilityZoneFromNodeLabel` sets the `availability_zone` system property from the pod's `topology.kubernetes.io/zone` label, which Kubernetes copies from the node since the PodTopologyLabels feature (default since v1.35). Solr's AffinityPlacementFactory uses that property to spread replicas across zones, which previously needed a hand-written init container with node-read RBAC. Closes #556 --- api/v1beta1/solrcloud_types.go | 10 ++++++ api/v1beta1/zz_generated.deepcopy.go | 5 +++ .../crd/bases/solr.apache.org_solrclouds.yaml | 9 ++++++ controllers/solrcloud_controller_test.go | 31 +++++++++++++++++++ controllers/util/solr_util.go | 12 +++++++ .../solr-cloud/pages/solr-cloud-crd.adoc | 15 +++++++++ helm/solr-operator/Chart.yaml | 5 +++ helm/solr-operator/crds/crds.yaml | 9 ++++++ helm/solr/README.md | 1 + helm/solr/templates/solrcloud.yaml | 4 +++ helm/solr/values.yaml | 5 +++ 11 files changed, 106 insertions(+) diff --git a/api/v1beta1/solrcloud_types.go b/api/v1beta1/solrcloud_types.go index 493d304e..2b986eb4 100644 --- a/api/v1beta1/solrcloud_types.go +++ b/api/v1beta1/solrcloud_types.go @@ -154,6 +154,16 @@ type SolrCloudSpec struct { // //+optional AdditionalLibs []string `json:"additionalLibs,omitempty"` + + // AvailabilityZoneFromNodeLabel exposes the "topology.kubernetes.io/zone" label of the Kubernetes node + // that each Solr pod runs on as the "availability_zone" system property, which Solr's + // AffinityPlacementFactory uses to spread replicas across availability zones. + // + // Requires Kubernetes v1.33+ with the PodTopologyLabels feature enabled, which is the default since v1.35. + // On clusters without it the system property is set to an empty value. + // + //+optional + AvailabilityZoneFromNodeLabel *bool `json:"availabilityZoneFromNodeLabel,omitempty"` } func (spec *SolrCloudSpec) withDefaults(logger logr.Logger) (changed bool) { diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index b3e4dc64..1b7a61ed 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -959,6 +959,11 @@ func (in *SolrCloudSpec) DeepCopyInto(out *SolrCloudSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.AvailabilityZoneFromNodeLabel != nil { + in, out := &in.AvailabilityZoneFromNodeLabel, &out.AvailabilityZoneFromNodeLabel + *out = new(bool) + **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 52174c97..5e096937 100644 --- a/config/crd/bases/solr.apache.org_solrclouds.yaml +++ b/config/crd/bases/solr.apache.org_solrclouds.yaml @@ -114,6 +114,15 @@ spec: - enabled type: object type: object + availabilityZoneFromNodeLabel: + description: |- + AvailabilityZoneFromNodeLabel exposes the "topology.kubernetes.io/zone" label of the Kubernetes node + that each Solr pod runs on as the "availability_zone" system property, which Solr's + AffinityPlacementFactory uses to spread replicas across availability zones. + + Requires Kubernetes v1.33+ with the PodTopologyLabels feature enabled, which is the default since v1.35. + On clusters without it the system property is set to an empty value. + type: boolean backupRepositories: description: Allows specification of multiple different "repositories" for Solr to use when backing up data. diff --git a/controllers/solrcloud_controller_test.go b/controllers/solrcloud_controller_test.go index abca59ae..d36be9e8 100644 --- a/controllers/solrcloud_controller_test.go +++ b/controllers/solrcloud_controller_test.go @@ -542,6 +542,37 @@ var _ = FDescribe("SolrCloud controller - General", func() { }) }) + FContext("Solr Cloud with availabilityZoneFromNodeLabel", func() { + BeforeEach(func() { + solrCloud.Spec = solrv1beta1.SolrCloudSpec{ + ZookeeperRef: &solrv1beta1.ZookeeperRef{ + ConnectionInfo: &solrv1beta1.ZookeeperConnectionInfo{ + InternalConnectionString: "host:7271", + }, + }, + AvailabilityZoneFromNodeLabel: newBoolPtr(true), + } + }) + FIt("has the correct resources", func(ctx context.Context) { + By("testing the Solr StatefulSet") + statefulSet := expectStatefulSet(ctx, solrCloud, solrCloud.StatefulSetName()) + + testPodEnvVariables(map[string]string{ + "SOLR_OPTS": "-DhostPort=$(SOLR_NODE_PORT) -Davailability_zone=$(SOLR_AVAILABILITY_ZONE)", + }, statefulSet.Spec.Template.Spec.Containers[0].Env) + + var zoneEnvVar *corev1.EnvVar + for _, envVar := range statefulSet.Spec.Template.Spec.Containers[0].Env { + if envVar.Name == "SOLR_AVAILABILITY_ZONE" { + zoneEnvVar = &envVar + break + } + } + Expect(zoneEnvVar).To(Not(BeNil()), "SOLR_AVAILABILITY_ZONE env var should be set") + Expect(zoneEnvVar.ValueFrom.FieldRef.FieldPath).To(Equal("metadata.labels['topology.kubernetes.io/zone']"), "Wrong fieldRef for the availability zone env var") + }) + }) + FContext("Solr Cloud with changing generated SolrXML", func() { BeforeEach(func() { solrCloud.Spec = solrv1beta1.SolrCloudSpec{ diff --git a/controllers/util/solr_util.go b/controllers/util/solr_util.go index 64c0b92d..ee700213 100644 --- a/controllers/util/solr_util.go +++ b/controllers/util/solr_util.go @@ -453,6 +453,18 @@ func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCl podAnnotations[SolrXmlMd5Annotation] = reconcileConfigInfo[SolrXmlMd5Annotation] } + if solrCloud.Spec.AvailabilityZoneFromNodeLabel != nil && *solrCloud.Spec.AvailabilityZoneFromNodeLabel { + envVars = append(envVars, corev1.EnvVar{ + Name: "SOLR_AVAILABILITY_ZONE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: fmt.Sprintf("metadata.labels['%s']", corev1.LabelTopologyZone), + }, + }, + }) + allSolrOpts = append(allSolrOpts, "-Davailability_zone=$(SOLR_AVAILABILITY_ZONE)") + } + if solrCloud.Spec.SolrOpts != "" { allSolrOpts = append(allSolrOpts, solrCloud.Spec.SolrOpts) } diff --git a/docs/modules/solr-cloud/pages/solr-cloud-crd.adoc b/docs/modules/solr-cloud/pages/solr-cloud-crd.adoc index 07b37b3a..d220c0cd 100644 --- a/docs/modules/solr-cloud/pages/solr-cloud-crd.adoc +++ b/docs/modules/solr-cloud/pages/solr-cloud-crd.adoc @@ -45,6 +45,21 @@ Use the **`SolrCloud.spec.solrModules`** property to add a list of module names, 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. +=== Availability Zone Awareness + +Solr's https://solr.apache.org/guide/solr/latest/configuration-guide/replica-placement-plugins.html#affinityplacementfactory[AffinityPlacementFactory] spreads replicas across availability zones, using the `availability_zone` system property of each Solr node. + +Set **`SolrCloud.spec.availabilityZoneFromNodeLabel`** to `true` to have the operator set that system property from the `topology.kubernetes.io/zone` label of the Kubernetes node each pod runs on. + +[source,yaml] +---- +spec: + availabilityZoneFromNodeLabel: true +---- + +This requires Kubernetes `v1.33`+ with the `PodTopologyLabels` feature enabled, which is the default since `v1.35`. +On clusters without it the system property is set to an empty value, and Solr places replicas as if all nodes were in one zone. + == Data Storage The SolrCloud CRD gives the option for users to use either diff --git a/helm/solr-operator/Chart.yaml b/helm/solr-operator/Chart.yaml index 9b5e6ffc..d806714d 100644 --- a/helm/solr-operator/Chart.yaml +++ b/helm/solr-operator/Chart.yaml @@ -55,6 +55,11 @@ annotations: # Allowed syntax is described at: https://artifacthub.io/docs/topics/annotations/helm/#example # 'kind' accepts values: "added", "changed", "deprecated", "removed", "fixed" and "security" artifacthub.io/changes: | + - kind: added + description: New SolrCloud option 'availabilityZoneFromNodeLabel', which exposes the Kubernetes node's topology.kubernetes.io/zone label to Solr as the availability_zone system property used by AffinityPlacementFactory + links: + - name: Github Issue + url: https://github.com/apache/solr-operator/issues/556 - kind: added description: The operator can now resize (expand) persistent data PVCs, which requires new RBAC permissions for persistentvolumeclaims (update/patch) and storageclasses (get/list/watch) links: diff --git a/helm/solr-operator/crds/crds.yaml b/helm/solr-operator/crds/crds.yaml index 5756983f..1450a1a2 100644 --- a/helm/solr-operator/crds/crds.yaml +++ b/helm/solr-operator/crds/crds.yaml @@ -372,6 +372,15 @@ spec: - enabled type: object type: object + availabilityZoneFromNodeLabel: + description: |- + AvailabilityZoneFromNodeLabel exposes the "topology.kubernetes.io/zone" label of the Kubernetes node + that each Solr pod runs on as the "availability_zone" system property, which Solr's + AffinityPlacementFactory uses to spread replicas across availability zones. + + Requires Kubernetes v1.33+ with the PodTopologyLabels feature enabled, which is the default since v1.35. + On clusters without it the system property is set to an empty value. + type: boolean backupRepositories: description: Allows specification of multiple different "repositories" for Solr to use when backing up data. diff --git a/helm/solr/README.md b/helm/solr/README.md index 02bce33e..021a7610 100644 --- a/helm/solr/README.md +++ b/helm/solr/README.md @@ -96,6 +96,7 @@ Descriptions on how to use these options can be found in the [SolrCloud document | 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. (There is no need to include paths for solrModules here if already listed in `solrModules`, those paths will be added automatically.) Note that this setting has no effect on solrcloud clusters that rely on a user-provided `solr.xml` file. | +| solrOptions.availabilityZoneFromNodeLabel | boolean | `false` | Expose the Kubernetes node's `topology.kubernetes.io/zone` label to Solr as the `availability_zone` system property, used by Solr's AffinityPlacementFactory. Requires Kubernetes v1.33+ with the PodTopologyLabels feature, which is the default since v1.35. | | 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 3f0dcfa1..efe3beab 100644 --- a/helm/solr/templates/solrcloud.yaml +++ b/helm/solr/templates/solrcloud.yaml @@ -177,6 +177,10 @@ spec: {{- toYaml .Values.solrOptions.additionalLibs | nindent 4 }} {{- end }} + {{- if .Values.solrOptions.availabilityZoneFromNodeLabel }} + availabilityZoneFromNodeLabel: true + {{- end }} + {{- if .Values.backupRepositories }} backupRepositories: {{- toYaml .Values.backupRepositories | nindent 4 }} diff --git a/helm/solr/values.yaml b/helm/solr/values.yaml index 07599d99..5305fb94 100644 --- a/helm/solr/values.yaml +++ b/helm/solr/values.yaml @@ -64,6 +64,11 @@ solrOptions: solrModules: [] additionalLibs: [] + # Expose the Kubernetes node's "topology.kubernetes.io/zone" label to Solr as the + # "availability_zone" system property, for use by Solr's AffinityPlacementFactory. + # Requires Kubernetes v1.33+ with the PodTopologyLabels feature, the default since v1.35. + availabilityZoneFromNodeLabel: false + # Enable authentication for the Solr Cloud # More information can be found at: # https://solr.apache.org/guide/operator/latest/solr-cloud/authentication-and-authorization.html From cff61a30cdd244a96e3cc7c13951c66baa155fa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Mon, 9 Nov 2026 15:31:50 +0100 Subject: [PATCH 2/2] Fix formatting of FieldPath in solr_util.go Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- controllers/util/solr_util.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/controllers/util/solr_util.go b/controllers/util/solr_util.go index ee700213..6f0247c1 100644 --- a/controllers/util/solr_util.go +++ b/controllers/util/solr_util.go @@ -458,7 +458,8 @@ func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCl Name: "SOLR_AVAILABILITY_ZONE", ValueFrom: &corev1.EnvVarSource{ FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: fmt.Sprintf("metadata.labels['%s']", corev1.LabelTopologyZone), + FieldPath: fmt.Sprintf("metadata.labels['%s']", corev1.LabelTopologyZone), + APIVersion: "v1", }, }, })