in controllers/util/solr_util.go [87:666]
func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCloudStatus, hostNameIPs map[string]string, reconcileConfigInfo map[string]string, tls *TLSCerts, security *SecurityConfig) *appsv1.StatefulSet {
terminationGracePeriod := int64(60)
shareProcessNamespace := false
solrPodPort := solrCloud.Spec.SolrAddressability.PodPort
defaultFSGroup := int64(DefaultSolrGroup)
probeScheme := corev1.URISchemeHTTP
if tls != nil {
probeScheme = corev1.URISchemeHTTPS
}
defaultProbeTimeout := int32(1)
defaultStartupProbe := createDefaultProbeHandlerForPath(probeScheme, solrPodPort, DefaultStartupProbePath)
defaultLivenessProbe := createDefaultProbeHandlerForPath(probeScheme, solrPodPort, DefaultLivenessProbePath)
defaultReadinessProbe := createDefaultProbeHandlerForPath(probeScheme, solrPodPort, DefaultReadinessProbePath)
labels := solrCloud.SharedLabelsWith(solrCloud.GetLabels())
selectorLabels := solrCloud.SharedLabels()
labels["technology"] = solr.SolrTechnologyLabel
selectorLabels["technology"] = solr.SolrTechnologyLabel
annotations := map[string]string{
SolrZKConnectionStringAnnotation: solrCloudStatus.ZkConnectionString(),
}
podLabels := labels
customSSOptions := solrCloud.Spec.CustomSolrKubeOptions.StatefulSetOptions
if nil != customSSOptions {
labels = MergeLabelsOrAnnotations(labels, customSSOptions.Labels)
annotations = MergeLabelsOrAnnotations(annotations, customSSOptions.Annotations)
}
customPodOptions := solrCloud.Spec.CustomSolrKubeOptions.PodOptions.DeepCopy()
var podAnnotations map[string]string
if nil != customPodOptions {
podLabels = MergeLabelsOrAnnotations(podLabels, customPodOptions.Labels)
podAnnotations = customPodOptions.Annotations
if customPodOptions.TerminationGracePeriodSeconds != nil {
terminationGracePeriod = *customPodOptions.TerminationGracePeriodSeconds
}
shareProcessNamespace = customPodOptions.ShareProcessNamespace
}
if podAnnotations == nil {
podAnnotations = make(map[string]string, 1)
}
var serviceType string
if solrCloud.UsesHeadlessService() {
serviceType = HeadlessServiceType
} else if solrCloud.UsesIndividualNodeServices() {
serviceType = PerNodeServiceType
}
podAnnotations[ServiceTypeAnnotation] = serviceType
// The isNotStopped readiness gate will always be used for managedUpdates
podReadinessGates := []corev1.PodReadinessGate{
{
ConditionType: SolrIsNotStoppedReadinessCondition,
},
}
// Keep track of the SolrOpts that the Solr Operator needs to set
// These will be added to the SolrOpts given by the user.
allSolrOpts := []string{"-DhostPort=$(SOLR_NODE_PORT)"}
// Volumes & Mounts
solrVolumes := []corev1.Volume{
{
Name: "solr-xml",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: reconcileConfigInfo[SolrXmlFile],
},
Items: []corev1.KeyToPath{
{
Key: SolrXmlFile,
Path: SolrXmlFile,
},
},
DefaultMode: &PublicReadOnlyPermissions,
},
},
},
{
Name: "tmp",
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
},
}
solrDataVolumeName := solrCloud.DataVolumeName()
var pvcs []corev1.PersistentVolumeClaim
if solrCloud.UsesPersistentStorage() {
pvc := solrCloud.Spec.StorageOptions.PersistentStorage.PersistentVolumeClaimTemplate.DeepCopy()
// Set the default name of the pvc
pvc.ObjectMeta.Name = solrDataVolumeName
// Set some defaults in the PVC Spec
if len(pvc.Spec.AccessModes) == 0 {
pvc.Spec.AccessModes = []corev1.PersistentVolumeAccessMode{
corev1.ReadWriteOnce,
}
}
if pvc.Spec.VolumeMode == nil {
temp := corev1.PersistentVolumeFilesystem
pvc.Spec.VolumeMode = &temp
}
// Add internally-used labels.
internalLabels := map[string]string{
SolrPVCTechnologyLabel: SolrCloudPVCTechnology,
SolrPVCStorageLabel: SolrCloudPVCDataStorage,
SolrPVCInstanceLabel: solrCloud.Name,
}
pvc.ObjectMeta.Labels = MergeLabelsOrAnnotations(internalLabels, pvc.ObjectMeta.Labels)
pvcs = []corev1.PersistentVolumeClaim{
{
ObjectMeta: metav1.ObjectMeta{
Name: pvc.ObjectMeta.Name,
Labels: pvc.ObjectMeta.Labels,
Annotations: pvc.ObjectMeta.Annotations,
},
Spec: pvc.Spec,
},
}
} else {
ephemeralVolume := corev1.Volume{
Name: solrDataVolumeName,
VolumeSource: corev1.VolumeSource{},
}
if solrCloud.Spec.StorageOptions.EphemeralStorage != nil {
if nil != solrCloud.Spec.StorageOptions.EphemeralStorage.HostPath {
ephemeralVolume.VolumeSource.HostPath = solrCloud.Spec.StorageOptions.EphemeralStorage.HostPath
} else if nil != solrCloud.Spec.StorageOptions.EphemeralStorage.EmptyDir {
ephemeralVolume.VolumeSource.EmptyDir = solrCloud.Spec.StorageOptions.EphemeralStorage.EmptyDir
} else {
ephemeralVolume.VolumeSource.EmptyDir = &corev1.EmptyDirVolumeSource{}
}
} else {
ephemeralVolume.VolumeSource.EmptyDir = &corev1.EmptyDirVolumeSource{}
}
solrVolumes = append(solrVolumes, ephemeralVolume)
// Add an evictPodReadinessCondition for when deleting pods with ephemeral storage
podReadinessGates = append(podReadinessGates, corev1.PodReadinessGate{
ConditionType: SolrReplicasNotEvictedReadinessCondition,
})
}
volumeMounts := []corev1.VolumeMount{{Name: solrDataVolumeName, MountPath: "/var/solr/data"}}
// Add necessary specs for backupRepos
backupEnvVars := make([]corev1.EnvVar, 0)
for _, repo := range solrCloud.Spec.BackupRepositories {
volumeSource, mount := RepoVolumeSourceAndMount(&repo, solrCloud.Name)
if volumeSource != nil {
solrVolumes = append(solrVolumes, corev1.Volume{
Name: RepoVolumeName(&repo),
VolumeSource: *volumeSource,
})
volumeMounts = append(volumeMounts, *mount)
}
repoEnvVars := RepoEnvVars(&repo)
if len(repoEnvVars) > 0 {
backupEnvVars = append(backupEnvVars, repoEnvVars...)
}
}
// Add annotation specifying the backupRepositories available with this version of the Pod.
podAnnotations = SetAvailableBackupRepos(solrCloud, podAnnotations)
if nil != customPodOptions {
// Add Custom Volumes to pod
for _, volume := range customPodOptions.Volumes {
// Only add the container mount if one has been provided.
if volume.DefaultContainerMount != nil {
volume.DefaultContainerMount.Name = volume.Name
volumeMounts = append(volumeMounts, *volume.DefaultContainerMount)
}
solrVolumes = append(solrVolumes, corev1.Volume{
Name: volume.Name,
VolumeSource: volume.Source,
})
}
}
// Host Aliases
hostAliases := make([]corev1.HostAlias, len(hostNameIPs))
if len(hostAliases) == 0 {
hostAliases = nil
} else {
hostNames := make([]string, len(hostNameIPs))
index := 0
for hostName := range hostNameIPs {
hostNames[index] = hostName
index += 1
}
sort.Strings(hostNames)
for index, hostName := range hostNames {
hostAliases[index] = corev1.HostAlias{
IP: hostNameIPs[hostName],
Hostnames: []string{hostName},
}
index++
}
}
solrHostName := solrCloud.AdvertisedNodeHost("$(POD_NAME)")
solrAdressingPort := solrCloud.NodePort()
// Solr can take longer than SOLR_STOP_WAIT to run solr stop, give it a few extra seconds before forcefully killing the pod.
solrStopWait := terminationGracePeriod - 5
if solrStopWait < 0 {
solrStopWait = 0
}
// Environment Variables
envVars := []corev1.EnvVar{
{
Name: "SOLR_JAVA_MEM",
Value: solrCloud.Spec.SolrJavaMem,
},
{
Name: "SOLR_HOME",
Value: "/var/solr/data",
},
{
// This is the port that jetty will listen on
Name: "SOLR_PORT",
Value: strconv.Itoa(solrPodPort),
},
{
// This is the port that the Solr Node will advertise itself as listening on in live_nodes
// TODO Remove in 0.9.0 once users have had a chance to switch any custom solr.xml files over to using the `solr.port.advertise` placeholder
Name: "SOLR_NODE_PORT",
Value: strconv.Itoa(solrAdressingPort),
},
{
// Supercedes SOLR_NODE_PORT above. 'bin/solr' converts to 'solr.port.advertise' sysprop automatically.
Name: "SOLR_PORT_ADVERTISE",
Value: strconv.Itoa(solrAdressingPort),
},
// POD_HOSTNAME is deprecated and will be removed in a future version. Use POD_NAME instead
{
Name: "POD_HOSTNAME",
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.name",
APIVersion: "v1",
},
},
},
{
Name: "POD_NAME",
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.name",
APIVersion: "v1",
},
},
},
{
Name: "POD_IP",
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "status.podIP",
APIVersion: "v1",
},
},
},
{
Name: "POD_NAMESPACE",
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.namespace",
APIVersion: "v1",
},
},
},
{
Name: "SOLR_HOST",
Value: solrHostName,
},
{
Name: "SOLR_LOG_LEVEL",
Value: solrCloud.Spec.SolrLogLevel,
},
{
Name: "GC_TUNE",
Value: solrCloud.Spec.SolrGCTune,
},
{
Name: "SOLR_STOP_WAIT",
Value: strconv.FormatInt(solrStopWait, 10),
},
}
// Add all necessary information for connection to Zookeeper
zkEnvVars, zkSolrOpt, _ := createZkConnectionEnvVars(solrCloud, solrCloudStatus)
if zkSolrOpt != "" {
allSolrOpts = append(allSolrOpts, zkSolrOpt)
}
envVars = append(envVars, zkEnvVars...)
// Add envVars for backupRepos if any are needed
if len(backupEnvVars) > 0 {
envVars = append(envVars, backupEnvVars...)
}
// Default preStop hook
preStop := &corev1.LifecycleHandler{
Exec: &corev1.ExecAction{
Command: []string{"solr", "stop", "-p", strconv.Itoa(solrPodPort)},
},
}
// Add Custom EnvironmentVariables to the solr container
if nil != customPodOptions {
envVars = append(envVars, customPodOptions.EnvVariables...)
}
// Did the user provide a custom log config?
if reconcileConfigInfo[LogXmlFile] != "" {
if reconcileConfigInfo[LogXmlMd5Annotation] != "" {
podAnnotations[LogXmlMd5Annotation] = reconcileConfigInfo[LogXmlMd5Annotation]
}
// cannot use /var/solr as a mountPath, so mount the custom log config
// in a sub-dir named after the user-provided ConfigMap
volMount, envVar, newVolume := setupVolumeMountForUserProvidedConfigMapEntry(reconcileConfigInfo, LogXmlFile, solrVolumes, "LOG4J_PROPS")
volumeMounts = append(volumeMounts, *volMount)
envVars = append(envVars, *envVar)
if newVolume != nil {
solrVolumes = append(solrVolumes, *newVolume)
}
}
// track the MD5 of the custom solr.xml in the pod spec annotations,
// so we get a rolling restart when the configMap changes
if reconcileConfigInfo[SolrXmlMd5Annotation] != "" {
podAnnotations[SolrXmlMd5Annotation] = reconcileConfigInfo[SolrXmlMd5Annotation]
}
if solrCloud.Spec.SolrOpts != "" {
allSolrOpts = append(allSolrOpts, solrCloud.Spec.SolrOpts)
}
// Add SOLR_OPTS last, so that it can use values from all of the other ENV_VARS
envVars = append(envVars, corev1.EnvVar{
Name: "SOLR_OPTS",
Value: strings.Join(allSolrOpts, " "),
})
initContainers := generateSolrSetupInitContainers(solrCloud, solrCloudStatus, solrDataVolumeName, security)
// Add user defined additional init containers
if customPodOptions != nil && len(customPodOptions.InitContainers) > 0 {
initContainers = append(initContainers, customPodOptions.InitContainers...)
}
var containerSecurityContext *corev1.SecurityContext
if customPodOptions != nil {
containerSecurityContext = customPodOptions.ContainerSecurityContext
}
containers := []corev1.Container{
{
Name: SolrNodeContainer,
Image: solrCloud.Spec.SolrImage.ToImageName(),
ImagePullPolicy: solrCloud.Spec.SolrImage.PullPolicy,
Ports: []corev1.ContainerPort{
{
ContainerPort: int32(solrPodPort),
Name: SolrClientPortName,
Protocol: "TCP",
},
},
// Wait 60 seconds for Solr to startup
StartupProbe: &corev1.Probe{
InitialDelaySeconds: 10,
TimeoutSeconds: defaultProbeTimeout,
SuccessThreshold: 1,
FailureThreshold: 10,
PeriodSeconds: 5,
ProbeHandler: defaultStartupProbe,
},
// Kill Solr if it is unavailable for any 60-second period
LivenessProbe: &corev1.Probe{
TimeoutSeconds: defaultProbeTimeout,
SuccessThreshold: 1,
FailureThreshold: 3,
PeriodSeconds: 20,
ProbeHandler: defaultLivenessProbe,
},
// Do not route requests to solr if it is not available for any 20-second period
ReadinessProbe: &corev1.Probe{
TimeoutSeconds: defaultProbeTimeout,
SuccessThreshold: 1,
FailureThreshold: 2,
PeriodSeconds: 10,
ProbeHandler: defaultReadinessProbe,
},
VolumeMounts: volumeMounts,
Env: envVars,
Lifecycle: &corev1.Lifecycle{
PreStop: preStop,
},
SecurityContext: containerSecurityContext,
},
}
// Add user defined additional sidecar containers
if customPodOptions != nil && len(customPodOptions.SidecarContainers) > 0 {
containers = append(containers, customPodOptions.SidecarContainers...)
}
// Decide which update strategy to use
updateStrategy := appsv1.OnDeleteStatefulSetStrategyType
if solrCloud.Spec.UpdateStrategy.Method == solr.StatefulSetUpdate {
// Only use the rolling update strategy if the StatefulSetUpdate method is specified.
updateStrategy = appsv1.RollingUpdateStatefulSetStrategyType
}
// Determine which podManagementPolicy to use for the statefulSet
podManagementPolicy := DefaultStatefulSetPodManagementPolicy
if solrCloud.Spec.CustomSolrKubeOptions.StatefulSetOptions != nil && solrCloud.Spec.CustomSolrKubeOptions.StatefulSetOptions.PodManagementPolicy != "" {
podManagementPolicy = solrCloud.Spec.CustomSolrKubeOptions.StatefulSetOptions.PodManagementPolicy
}
// Create the Stateful Set
stateful := &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: solrCloud.StatefulSetName(),
Namespace: solrCloud.GetNamespace(),
Labels: labels,
Annotations: annotations,
},
Spec: appsv1.StatefulSetSpec{
Selector: &metav1.LabelSelector{
MatchLabels: selectorLabels,
},
ServiceName: solrCloud.HeadlessServiceName(),
Replicas: solrCloud.Spec.Replicas,
PodManagementPolicy: podManagementPolicy,
UpdateStrategy: appsv1.StatefulSetUpdateStrategy{
Type: updateStrategy,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: podLabels,
Annotations: podAnnotations,
},
Spec: corev1.PodSpec{
TerminationGracePeriodSeconds: &terminationGracePeriod,
ShareProcessNamespace: &shareProcessNamespace,
SecurityContext: &corev1.PodSecurityContext{
FSGroup: &defaultFSGroup,
},
Volumes: solrVolumes,
InitContainers: initContainers,
HostAliases: hostAliases,
Containers: containers,
ReadinessGates: podReadinessGates,
},
},
VolumeClaimTemplates: pvcs,
},
}
if solrCloud.UsesHeadlessService() {
stateful.Spec.Template.Spec.Subdomain = solrCloud.HeadlessServiceName()
}
var imagePullSecrets []corev1.LocalObjectReference
if customPodOptions != nil {
imagePullSecrets = customPodOptions.ImagePullSecrets
}
if solrCloud.Spec.SolrImage.ImagePullSecret != "" {
imagePullSecrets = append(
imagePullSecrets,
corev1.LocalObjectReference{Name: solrCloud.Spec.SolrImage.ImagePullSecret},
)
}
stateful.Spec.Template.Spec.ImagePullSecrets = imagePullSecrets
if nil != customPodOptions {
solrContainer := &stateful.Spec.Template.Spec.Containers[0]
if customPodOptions.ServiceAccountName != "" {
stateful.Spec.Template.Spec.ServiceAccountName = customPodOptions.ServiceAccountName
}
if customPodOptions.Affinity != nil {
stateful.Spec.Template.Spec.Affinity = customPodOptions.Affinity
}
if customPodOptions.Resources.Limits != nil || customPodOptions.Resources.Requests != nil {
solrContainer.Resources = customPodOptions.Resources
}
if customPodOptions.PodSecurityContext != nil {
stateful.Spec.Template.Spec.SecurityContext = customPodOptions.PodSecurityContext
if stateful.Spec.Template.Spec.SecurityContext.FSGroup == nil {
stateful.Spec.Template.Spec.SecurityContext.FSGroup = &defaultFSGroup
}
}
if customPodOptions.Lifecycle != nil {
solrContainer.Lifecycle = customPodOptions.Lifecycle
}
if customPodOptions.Tolerations != nil {
stateful.Spec.Template.Spec.Tolerations = customPodOptions.Tolerations
}
if customPodOptions.NodeSelector != nil {
stateful.Spec.Template.Spec.NodeSelector = customPodOptions.NodeSelector
}
if customPodOptions.StartupProbe != nil {
// Default Solr container does not contain a startupProbe, so copy the livenessProbe
baseProbe := solrContainer.LivenessProbe.DeepCopy()
// Two options are different by default from the livenessProbe
baseProbe.TimeoutSeconds = 30
baseProbe.FailureThreshold = 15
solrContainer.StartupProbe = customizeProbe(baseProbe, *customPodOptions.StartupProbe)
}
if customPodOptions.LivenessProbe != nil {
solrContainer.LivenessProbe = customizeProbe(solrContainer.LivenessProbe, *customPodOptions.LivenessProbe)
}
if customPodOptions.ReadinessProbe != nil {
solrContainer.ReadinessProbe = customizeProbe(solrContainer.ReadinessProbe, *customPodOptions.ReadinessProbe)
}
if customPodOptions.PriorityClassName != "" {
stateful.Spec.Template.Spec.PriorityClassName = customPodOptions.PriorityClassName
}
if len(customPodOptions.TopologySpreadConstraints) > 0 {
stateful.Spec.Template.Spec.TopologySpreadConstraints = customPodOptions.TopologySpreadConstraints
// Set the label selector for constraints to the statefulSet label selector, if none is provided
for i := range stateful.Spec.Template.Spec.TopologySpreadConstraints {
if stateful.Spec.Template.Spec.TopologySpreadConstraints[i].LabelSelector == nil {
stateful.Spec.Template.Spec.TopologySpreadConstraints[i].LabelSelector = stateful.Spec.Selector.DeepCopy()
}
}
}
}
// Enrich the StatefulSet config to enable TLS on Solr pods if needed
if tls != nil {
tls.enableTLSOnSolrCloudStatefulSet(stateful)
}
// If probes require auth is set OR tls is configured to want / need client auth, then reconfigure the probes to use an exec
if (solrCloud.Spec.SolrSecurity != nil && solrCloud.Spec.SolrSecurity.ProbesRequireAuth) || (tls != nil && tls.ServerConfig != nil && tls.ServerConfig.Options.ClientAuth != solr.None) {
enableSecureProbesOnSolrCloudStatefulSet(solrCloud, stateful)
} else {
// If we are not using secure probes, but still using TLS, then make sure that the HOST header is correct when sending liveness and readiness checks.
// Otherwise it is likely that the SNI checks will fail for newer versions of Solr (9.2+)
setHostHeaderForProbesOnSolrCloudStatefulSet(solrCloud, stateful)
}
return stateful
}