Skip to content
Open
12 changes: 12 additions & 0 deletions api/v1beta1/common_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,18 @@ type ServiceOptions struct {
// Labels to be added for the Service.
// +optional
Labels map[string]string `json:"labels,omitempty"`

// SessionAffinity is used to maintain session affinity for the Service.
// Enables client IP based session affinity when set to "ClientIP". Must be "ClientIP" or "None". Defaults to "None".
// +optional
Comment thread
HoustonPutman marked this conversation as resolved.
// +kubebuilder:validation:Enum=None;ClientIP
// +kubebuilder:default=None
SessionAffinity corev1.ServiceAffinity `json:"sessionAffinity,omitempty"`

// SessionAffinityConfig contains the configuration of the Service's session affinity.
// Only used when SessionAffinity is set to "ClientIP".
// +optional
SessionAffinityConfig *corev1.SessionAffinityConfig `json:"sessionAffinityConfig,omitempty"`
}

// IngressOptions defines custom options for ingresses
Expand Down
5 changes: 5 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 81 additions & 0 deletions config/crd/bases/solr.apache.org_solrclouds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2273,6 +2273,33 @@ spec:
type: string
description: Labels to be added for the Service.
type: object
sessionAffinity:
default: None
description: |-
SessionAffinity is used to maintain session affinity for the Service.
Enables client IP based session affinity when set to "ClientIP". Must be "ClientIP" or "None". Defaults to "None".
enum:
- None
- ClientIP
type: string
sessionAffinityConfig:
description: |-
SessionAffinityConfig contains the configuration of the Service's session affinity.
Only used when SessionAffinity is set to "ClientIP".
properties:
clientIP:
description: clientIP contains the configurations of Client
IP based session affinity.
properties:
timeoutSeconds:
description: |-
timeoutSeconds specifies the seconds of ClientIP type session sticky time.
The value must be >0 && <=86400(for 1 day) if ServiceAffinity == "ClientIP".
Default value is 10800(for 3 hours).
format: int32
type: integer
type: object
type: object
type: object
configMapOptions:
description: ServiceOptions defines the custom options for the
Expand Down Expand Up @@ -2307,6 +2334,33 @@ spec:
type: string
description: Labels to be added for the Service.
type: object
sessionAffinity:
default: None
description: |-
SessionAffinity is used to maintain session affinity for the Service.
Enables client IP based session affinity when set to "ClientIP". Must be "ClientIP" or "None". Defaults to "None".
enum:
- None
- ClientIP
type: string
sessionAffinityConfig:
description: |-
SessionAffinityConfig contains the configuration of the Service's session affinity.
Only used when SessionAffinity is set to "ClientIP".
properties:
clientIP:
description: clientIP contains the configurations of Client
IP based session affinity.
properties:
timeoutSeconds:
description: |-
timeoutSeconds specifies the seconds of ClientIP type session sticky time.
The value must be >0 && <=86400(for 1 day) if ServiceAffinity == "ClientIP".
Default value is 10800(for 3 hours).
format: int32
type: integer
type: object
type: object
type: object
ingressOptions:
description: IngressOptions defines the custom options for the
Expand Down Expand Up @@ -2346,6 +2400,33 @@ spec:
type: string
description: Labels to be added for the Service.
type: object
sessionAffinity:
default: None
description: |-
SessionAffinity is used to maintain session affinity for the Service.
Enables client IP based session affinity when set to "ClientIP". Must be "ClientIP" or "None". Defaults to "None".
enum:
- None
- ClientIP
type: string
sessionAffinityConfig:
description: |-
SessionAffinityConfig contains the configuration of the Service's session affinity.
Only used when SessionAffinity is set to "ClientIP".
properties:
clientIP:
description: clientIP contains the configurations of Client
IP based session affinity.
properties:
timeoutSeconds:
description: |-
timeoutSeconds specifies the seconds of ClientIP type session sticky time.
The value must be >0 && <=86400(for 1 day) if ServiceAffinity == "ClientIP".
Default value is 10800(for 3 hours).
format: int32
type: integer
type: object
type: object
type: object
podOptions:
description: SolrPodOptions defines the custom options for solrCloud
Expand Down
27 changes: 27 additions & 0 deletions config/crd/bases/solr.apache.org_solrprometheusexporters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8018,6 +8018,33 @@ spec:
type: string
description: Labels to be added for the Service.
type: object
sessionAffinity:
default: None
description: |-
SessionAffinity is used to maintain session affinity for the Service.
Enables client IP based session affinity when set to "ClientIP". Must be "ClientIP" or "None". Defaults to "None".
enum:
- None
- ClientIP
type: string
sessionAffinityConfig:
description: |-
SessionAffinityConfig contains the configuration of the Service's session affinity.
Only used when SessionAffinity is set to "ClientIP".
properties:
clientIP:
description: clientIP contains the configurations of Client
IP based session affinity.
properties:
timeoutSeconds:
description: |-
timeoutSeconds specifies the seconds of ClientIP type session sticky time.
The value must be >0 && <=86400(for 1 day) if ServiceAffinity == "ClientIP".
Default value is 10800(for 3 hours).
format: int32
type: integer
type: object
type: object
type: object
type: object
exporterEntrypoint:
Expand Down
19 changes: 15 additions & 4 deletions controllers/solrcloud_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,12 +244,19 @@ var _ = FDescribe("SolrCloud controller - General", func() {
PodManagementPolicy: appsv1.OrderedReadyPodManagement,
},
CommonServiceOptions: &solrv1beta1.ServiceOptions{
Annotations: testCommonServiceAnnotations,
Labels: testCommonServiceLabels,
Annotations: testCommonServiceAnnotations,
Labels: testCommonServiceLabels,
SessionAffinity: corev1.ServiceAffinityClientIP,
SessionAffinityConfig: &corev1.SessionAffinityConfig{
ClientIP: &corev1.ClientIPConfig{
TimeoutSeconds: pointer.Int32(3600),
},
},
},
HeadlessServiceOptions: &solrv1beta1.ServiceOptions{
Annotations: testHeadlessServiceAnnotations,
Labels: testHeadlessServiceLabels,
Annotations: testHeadlessServiceAnnotations,
Labels: testHeadlessServiceLabels,
SessionAffinity: corev1.ServiceAffinityClientIP,
},
ConfigMapOptions: &solrv1beta1.ConfigMapOptions{
Annotations: testConfigMapAnnotations,
Expand Down Expand Up @@ -340,6 +347,9 @@ var _ = FDescribe("SolrCloud controller - General", func() {
Expect(commonService.Annotations).To(Equal(testCommonServiceAnnotations), "Incorrect common service annotations")
Expect(commonService.Spec.Ports[0].Protocol).To(Equal(corev1.ProtocolTCP), "Wrong protocol on common Service")
Expect(commonService.Spec.Ports[0].AppProtocol).To(BeNil(), "AppProtocol on common Service should be nil when not running with TLS")
Expect(commonService.Spec.SessionAffinity).To(Equal(corev1.ServiceAffinityClientIP), "Incorrect sessionAffinity on common Service")
Expect(commonService.Spec.SessionAffinityConfig).To(Not(BeNil()), "Common Service should have a sessionAffinityConfig")
Expect(commonService.Spec.SessionAffinityConfig.ClientIP.TimeoutSeconds).To(PointTo(Equal(int32(3600))), "Incorrect sessionAffinityConfig timeout on common Service")

By("testing the Solr Headless Service")
headlessService := expectService(ctx, solrCloud, solrCloud.HeadlessServiceName(), statefulSet.Spec.Selector.MatchLabels, true)
Expand All @@ -348,6 +358,7 @@ var _ = FDescribe("SolrCloud controller - General", func() {
Expect(headlessService.Annotations).To(Equal(testHeadlessServiceAnnotations), "Incorrect headless service annotations")
Expect(headlessService.Spec.Ports[0].Protocol).To(Equal(corev1.ProtocolTCP), "Wrong protocol on headless Service")
Expect(headlessService.Spec.Ports[0].AppProtocol).To(BeNil(), "AppProtocol on headless Service should be nil when not running with TLS")
Expect(headlessService.Spec.SessionAffinity).To(Equal(corev1.ServiceAffinityClientIP), "Incorrect sessionAffinity on headless Service")

By("testing the PodDisruptionBudget")
expectNoPodDisruptionBudget(ctx, solrCloud, solrCloud.StatefulSetName())
Expand Down
15 changes: 13 additions & 2 deletions controllers/solrprometheusexporter_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ import (
solrv1beta1 "github.com/apache/solr-operator/api/v1beta1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gstruct"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
)

var _ = FDescribe("SolrPrometheusExporter controller - General", func() {
Expand Down Expand Up @@ -180,8 +182,14 @@ var _ = FDescribe("SolrPrometheusExporter controller - General", func() {
Labels: testDeploymentLabels,
},
ServiceOptions: &solrv1beta1.ServiceOptions{
Annotations: testMetricsServiceAnnotations,
Labels: testMetricsServiceLabels,
Annotations: testMetricsServiceAnnotations,
Labels: testMetricsServiceLabels,
SessionAffinity: corev1.ServiceAffinityClientIP,
SessionAffinityConfig: &corev1.SessionAffinityConfig{
ClientIP: &corev1.ClientIPConfig{
TimeoutSeconds: pointer.Int32(3600),
},
},
},
ConfigMapOptions: &solrv1beta1.ConfigMapOptions{
Annotations: testConfigMapAnnotations,
Expand Down Expand Up @@ -303,6 +311,9 @@ var _ = FDescribe("SolrPrometheusExporter controller - General", func() {
Expect(service.Spec.Ports[0].Protocol).To(Equal(corev1.ProtocolTCP), "Wrong protocol on metrics Service")
Expect(service.Spec.Ports[0].AppProtocol).ToNot(BeNil(), "AppProtocol on metrics Service should not be nil")
Expect(*service.Spec.Ports[0].AppProtocol).To(Equal("http"), "Wrong appProtocol on metrics Service")
Expect(service.Spec.SessionAffinity).To(Equal(corev1.ServiceAffinityClientIP), "Incorrect sessionAffinity on metrics Service")
Expect(service.Spec.SessionAffinityConfig).To(Not(BeNil()), "Metrics Service should have a sessionAffinityConfig")
Expect(service.Spec.SessionAffinityConfig.ClientIP.TimeoutSeconds).To(PointTo(Equal(int32(3600))), "Incorrect sessionAffinityConfig timeout on metrics Service")
})
})

Expand Down
15 changes: 15 additions & 0 deletions controllers/util/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,21 @@ func CopyServiceFields(from, to *corev1.Service, logger logr.Logger) bool {
}
to.Spec.PublishNotReadyAddresses = from.Spec.PublishNotReadyAddresses

// Only reconcile SessionAffinity when a value is explicitly desired. Kubernetes defaults the live
// Service's SessionAffinity to "None" (and populates SessionAffinityConfig when "ClientIP" is used),
// so copying an unset value would fight the API server's defaulting and cause perpetual updates.
if from.Spec.SessionAffinity != "" && !DeepEqualWithNils(to.Spec.SessionAffinity, from.Spec.SessionAffinity) {
requireUpdate = true
logger.Info("Update required because field changed", "field", "Spec.SessionAffinity", "from", to.Spec.SessionAffinity, "to", from.Spec.SessionAffinity)
to.Spec.SessionAffinity = from.Spec.SessionAffinity
}

if !DeepEqualWithNils(to.Spec.SessionAffinityConfig, from.Spec.SessionAffinityConfig) {
requireUpdate = true
logger.Info("Update required because field changed", "field", "Spec.SessionAffinityConfig", "from", to.Spec.SessionAffinityConfig, "to", from.Spec.SessionAffinityConfig)
to.Spec.SessionAffinityConfig = from.Spec.SessionAffinityConfig
}

return requireUpdate
}

Expand Down
1 change: 1 addition & 0 deletions controllers/util/prometheus_exporter_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ func GenerateSolrMetricsService(solrPrometheusExporter *solr.SolrPrometheusExpor
Selector: selectorLabels,
},
}
applyCustomServiceOptions(&service.Spec, customOptions)
return service
}

Expand Down
15 changes: 15 additions & 0 deletions controllers/util/solr_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,18 @@ func getAppProtocol(solrCloud *solr.SolrCloud) *string {
}
}

// applyCustomServiceOptions applies the ServiceOptions fields that map directly onto the ServiceSpec.
// Labels and annotations are handled separately on the ObjectMeta.
func applyCustomServiceOptions(spec *corev1.ServiceSpec, customOptions *solr.ServiceOptions) {
if customOptions == nil {
return
}
if customOptions.SessionAffinity != "" {
spec.SessionAffinity = customOptions.SessionAffinity
}
spec.SessionAffinityConfig = customOptions.SessionAffinityConfig
}

// GenerateCommonService returns a new corev1.Service pointer generated for the entire SolrCloud instance
// solrCloud: SolrCloud instance
func GenerateCommonService(solrCloud *solr.SolrCloud) *corev1.Service {
Expand Down Expand Up @@ -987,6 +999,7 @@ func GenerateCommonService(solrCloud *solr.SolrCloud) *corev1.Service {
Selector: selectorLabels,
},
}
applyCustomServiceOptions(&service.Spec, customOptions)
return service
}

Expand Down Expand Up @@ -1041,6 +1054,7 @@ func GenerateHeadlessService(solrCloud *solr.SolrCloud) *corev1.Service {
PublishNotReadyAddresses: true,
},
}
applyCustomServiceOptions(&service.Spec, customOptions)
return service
}

Expand Down Expand Up @@ -1085,6 +1099,7 @@ func GenerateNodeService(solrCloud *solr.SolrCloud, nodeName string) *corev1.Ser
PublishNotReadyAddresses: true,
},
}
applyCustomServiceOptions(&service.Spec, customOptions)
return service
}

Expand Down
26 changes: 26 additions & 0 deletions docs/modules/solr-cloud/pages/custom-kube-options.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,29 @@ spec:
----

A complete, runnable example (including the backing `ConfigMap`) is available in https://github.com/apache/solr-operator/blob/main/example/test_solrcloud_additional_volume.yaml[`example/test_solrcloud_additional_volume.yaml`].

== Service Session Affinity

The Services that the operator creates for a SolrCloud (`commonServiceOptions`, `headlessServiceOptions`, and `nodeServiceOptions`) can be configured with https://kubernetes.io/docs/reference/networking/virtual-ips/#session-affinity[session affinity].
This is useful when clients connect to Solr through the common Service and you want requests from a given client IP to be routed to the same Solr pod.
For `headlessServiceOptions` and `nodeServiceOptions`, this option provides no value because each service endpoint maps to a single pod.

Each service option object exposes two fields:

* **`sessionAffinity`** - Either `None` (the default) or `ClientIP`.
When set to `ClientIP`, connections from the same client IP are directed to the same backing pod.
* **`sessionAffinityConfig`** - Optional https://kubernetes.io/docs/reference/kubernetes-api/service-resources/service-v1/#ServiceSpec[session affinity configuration], only used when `sessionAffinity` is `ClientIP`.
Most commonly this sets the `clientIP.timeoutSeconds` sticky-session timeout (defaults to `10800`, i.e. 3 hours).

The following example enables client-IP session affinity on the common Service with a one hour timeout:

[source,yaml]
----
spec:
customSolrKubeOptions:
commonServiceOptions:
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 3600
----
7 changes: 7 additions & 0 deletions helm/solr-operator/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ annotations:
url: https://github.com/apache/solr-operator/issues/338
- name: Github PR
url: https://github.com/apache/solr-operator/pull/832
- kind: added
description: Ability to set SessionAffinity and SessionAffinityConfig for Services
links:
- name: GitHub Issue
url: https://github.com/apache/solr-operator/issues/535
- name: GitHub PR
url: https://github.com/apache/solr-operator/pull/571
artifacthub.io/images: |
- name: solr-operator
image: apache/solr-operator:v0.10.0-prerelease
Expand Down
Loading
Loading