| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package util |
| |
| import ( |
| "crypto/sha256" |
| b64 "encoding/base64" |
| "encoding/json" |
| "fmt" |
| solr "github.com/apache/solr-operator/api/v1beta1" |
| appsv1 "k8s.io/api/apps/v1" |
| corev1 "k8s.io/api/core/v1" |
| netv1 "k8s.io/api/networking/v1beta1" |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| "k8s.io/apimachinery/pkg/util/intstr" |
| "math/rand" |
| "regexp" |
| "sort" |
| "strconv" |
| "strings" |
| "time" |
| ) |
| |
| const ( |
| SolrClientPortName = "solr-client" |
| BackupRestoreVolume = "backup-restore" |
| |
| SolrNodeContainer = "solrcloud-node" |
| |
| DefaultSolrUser = 8983 |
| DefaultSolrGroup = 8983 |
| |
| SolrStorageFinalizer = "storage.finalizers.solr.apache.org" |
| SolrZKConnectionStringAnnotation = "solr.apache.org/zkConnectionString" |
| SolrPVCTechnologyLabel = "solr.apache.org/technology" |
| SolrCloudPVCTechnology = "solr-cloud" |
| SolrPVCStorageLabel = "solr.apache.org/storage" |
| SolrCloudPVCDataStorage = "data" |
| SolrPVCInstanceLabel = "solr.apache.org/instance" |
| SolrXmlMd5Annotation = "solr.apache.org/solrXmlMd5" |
| SolrTlsCertMd5Annotation = "solr.apache.org/tlsCertMd5" |
| SolrXmlFile = "solr.xml" |
| LogXmlMd5Annotation = "solr.apache.org/logXmlMd5" |
| LogXmlFile = "log4j2.xml" |
| SecurityJsonFile = "security.json" |
| BasicAuthMd5Annotation = "solr.apache.org/basicAuthMd5" |
| DefaultProbePath = "/admin/info/system" |
| |
| DefaultStatefulSetPodManagementPolicy = appsv1.ParallelPodManagement |
| |
| DefaultKeyStorePath = "/var/solr/tls" |
| Pkcs12KeystoreFile = "keystore.p12" |
| DefaultWritableKeyStorePath = "/var/solr/tls/pkcs12" |
| TLSCertKey = "tls.crt" |
| TLSKeyKey = "tls.key" |
| DefaultTrustStorePath = "/var/solr/tls-truststore" |
| ) |
| |
| // GenerateStatefulSet returns a new appsv1.StatefulSet pointer generated for the SolrCloud instance |
| // object: SolrCloud instance |
| // replicas: the number of replicas for the SolrCloud instance |
| // storage: the size of the storage for the SolrCloud instance (e.g. 100Gi) |
| // zkConnectionString: the connectionString of the ZK instance to connect to |
| func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCloudStatus, hostNameIPs map[string]string, reconcileConfigInfo map[string]string, createPkcs12InitContainer bool, tlsCertMd5 string) *appsv1.StatefulSet { |
| terminationGracePeriod := int64(60) |
| solrPodPort := solrCloud.Spec.SolrAddressability.PodPort |
| fsGroup := int64(DefaultSolrGroup) |
| defaultMode := int32(420) |
| |
| probeScheme := corev1.URISchemeHTTP |
| if solrCloud.Spec.SolrTLS != nil { |
| probeScheme = corev1.URISchemeHTTPS |
| } |
| |
| defaultHandler := corev1.Handler{ |
| HTTPGet: &corev1.HTTPGetAction{ |
| Scheme: probeScheme, |
| Path: "/solr" + DefaultProbePath, |
| Port: intstr.FromInt(solrPodPort), |
| }, |
| } |
| |
| 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 |
| var podAnnotations map[string]string |
| if nil != customPodOptions { |
| podLabels = MergeLabelsOrAnnotations(podLabels, customPodOptions.Labels) |
| podAnnotations = customPodOptions.Annotations |
| |
| if customPodOptions.TerminationGracePeriodSeconds != nil { |
| terminationGracePeriod = *customPodOptions.TerminationGracePeriodSeconds |
| } |
| } |
| |
| // 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: &defaultMode, |
| }, |
| }, |
| }, |
| } |
| |
| solrDataVolumeName := "data" |
| volumeMounts := []corev1.VolumeMount{{Name: solrDataVolumeName, MountPath: "/var/solr/data"}} |
| |
| if solrCloud.Spec.SolrTLS != nil { |
| solrVolumes = append(solrVolumes, tlsVolumes(solrCloud.Spec.SolrTLS, createPkcs12InitContainer)...) |
| volumeMounts = append(volumeMounts, tlsVolumeMounts(solrCloud.Spec.SolrTLS, createPkcs12InitContainer)...) |
| } |
| |
| var pvcs []corev1.PersistentVolumeClaim |
| if solrCloud.UsesPersistentStorage() { |
| pvc := solrCloud.Spec.StorageOptions.PersistentStorage.PersistentVolumeClaimTemplate.DeepCopy() |
| |
| // Set the default name of the pvc |
| if pvc.ObjectMeta.Name == "" { |
| 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 backup volumes |
| if solrCloud.Spec.StorageOptions.BackupRestoreOptions != nil { |
| solrVolumes = append(solrVolumes, corev1.Volume{ |
| Name: BackupRestoreVolume, |
| VolumeSource: solrCloud.Spec.StorageOptions.BackupRestoreOptions.Volume, |
| }) |
| volumeMounts = append(volumeMounts, corev1.VolumeMount{ |
| Name: BackupRestoreVolume, |
| MountPath: BaseBackupRestorePath, |
| SubPath: BackupRestoreSubPathForCloud(solrCloud.Spec.StorageOptions.BackupRestoreOptions.Directory, solrCloud.Name), |
| ReadOnly: false, |
| }) |
| } |
| |
| 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_HOSTNAME)") |
| 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 |
| Name: "SOLR_NODE_PORT", |
| Value: strconv.Itoa(solrAdressingPort), |
| }, |
| { |
| Name: "POD_HOSTNAME", |
| ValueFrom: &corev1.EnvVarSource{ |
| FieldRef: &corev1.ObjectFieldSelector{ |
| FieldPath: "metadata.name", |
| 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, hasChroot := createZkConnectionEnvVars(solrCloud, solrCloudStatus) |
| if zkSolrOpt != "" { |
| allSolrOpts = append(allSolrOpts, zkSolrOpt) |
| } |
| envVars = append(envVars, zkEnvVars...) |
| |
| // Only have a postStart command to create the chRoot, if it is not '/' (which does not need to be created) |
| var postStart *corev1.Handler |
| if hasChroot { |
| postStart = &corev1.Handler{ |
| Exec: &corev1.ExecAction{ |
| Command: []string{"sh", "-c", "solr zk ls ${ZK_CHROOT} -z ${ZK_SERVER} || solr zk mkroot ${ZK_CHROOT} -z ${ZK_SERVER}"}, |
| }, |
| } |
| } |
| |
| // Append TLS related env vars if enabled |
| if solrCloud.Spec.SolrTLS != nil { |
| envVars = append(envVars, TLSEnvVars(solrCloud.Spec.SolrTLS, createPkcs12InitContainer)...) |
| } |
| |
| // 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] != "" { |
| if podAnnotations == nil { |
| podAnnotations = make(map[string]string, 1) |
| } |
| 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) |
| } |
| } |
| |
| if (solrCloud.Spec.SolrTLS != nil && solrCloud.Spec.SolrTLS.ClientAuth != solr.None) || (solrCloud.Spec.SolrSecurity != nil && solrCloud.Spec.SolrSecurity.ProbesRequireAuth) { |
| probeCommand, vol, volMount := configureSecureProbeCommand(solrCloud, defaultHandler.HTTPGet) |
| if vol != nil { |
| solrVolumes = append(solrVolumes, *vol) |
| } |
| if volMount != nil { |
| volumeMounts = append(volumeMounts, *volMount) |
| } |
| // reset the defaultHandler for the probes to invoke the SolrCLI api action instead of HTTP |
| defaultHandler = corev1.Handler{Exec: &corev1.ExecAction{Command: []string{"sh", "-c", probeCommand}}} |
| } |
| |
| // 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] != "" { |
| if podAnnotations == nil { |
| podAnnotations = make(map[string]string, 1) |
| } |
| podAnnotations[SolrXmlMd5Annotation] = reconcileConfigInfo[SolrXmlMd5Annotation] |
| } |
| |
| // track the MD5 of the TLS cert (from secret) to trigger restarts if the cert changes |
| if solrCloud.Spec.SolrTLS != nil && solrCloud.Spec.SolrTLS.RestartOnTLSSecretUpdate && tlsCertMd5 != "" { |
| if podAnnotations == nil { |
| podAnnotations = make(map[string]string, 1) |
| } |
| podAnnotations[SolrTlsCertMd5Annotation] = tlsCertMd5 |
| } |
| |
| 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, reconcileConfigInfo) |
| |
| // Add user defined additional init containers |
| if customPodOptions != nil && len(customPodOptions.InitContainers) > 0 { |
| initContainers = append(initContainers, customPodOptions.InitContainers...) |
| } |
| |
| 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", |
| }, |
| }, |
| LivenessProbe: &corev1.Probe{ |
| InitialDelaySeconds: 20, |
| TimeoutSeconds: 1, |
| SuccessThreshold: 1, |
| FailureThreshold: 3, |
| PeriodSeconds: 10, |
| Handler: defaultHandler, |
| }, |
| ReadinessProbe: &corev1.Probe{ |
| InitialDelaySeconds: 15, |
| TimeoutSeconds: 1, |
| SuccessThreshold: 1, |
| FailureThreshold: 3, |
| PeriodSeconds: 5, |
| Handler: defaultHandler, |
| }, |
| VolumeMounts: volumeMounts, |
| Env: envVars, |
| Lifecycle: &corev1.Lifecycle{ |
| PostStart: postStart, |
| PreStop: &corev1.Handler{ |
| Exec: &corev1.ExecAction{ |
| Command: []string{"solr", "stop", "-p", strconv.Itoa(solrPodPort)}, |
| }, |
| }, |
| }, |
| }, |
| } |
| |
| // 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 |
| } |
| |
| if createPkcs12InitContainer { |
| pkcs12InitContainer := generatePkcs12InitContainer(solrCloud.Spec.SolrTLS, |
| solrCloud.Spec.SolrImage.ToImageName(), solrCloud.Spec.SolrImage.PullPolicy) |
| initContainers = append(initContainers, pkcs12InitContainer) |
| } |
| |
| // 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, |
| SecurityContext: &corev1.PodSecurityContext{ |
| FSGroup: &fsGroup, |
| }, |
| Volumes: solrVolumes, |
| InitContainers: initContainers, |
| HostAliases: hostAliases, |
| Containers: containers, |
| }, |
| }, |
| VolumeClaimTemplates: pvcs, |
| }, |
| } |
| |
| 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 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 |
| } |
| } |
| |
| return stateful |
| } |
| |
| func generateSolrSetupInitContainers(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCloudStatus, solrDataVolumeName string, reconcileConfigInfo map[string]string) (containers []corev1.Container) { |
| // The setup of the solr.xml will always be necessary |
| volumeMounts := []corev1.VolumeMount{ |
| { |
| Name: "solr-xml", |
| MountPath: "/tmp", |
| }, |
| { |
| Name: solrDataVolumeName, |
| MountPath: "/tmp-config", |
| }, |
| } |
| setupCommands := []string{"cp /tmp/solr.xml /tmp-config/solr.xml"} |
| |
| // Add prep for backup-restore volume |
| // This entails setting the correct permissions for the directory |
| if solrCloud.Spec.StorageOptions.BackupRestoreOptions != nil { |
| volumeMounts = append(volumeMounts, corev1.VolumeMount{ |
| Name: BackupRestoreVolume, |
| MountPath: "/backup-restore", |
| SubPath: BackupRestoreSubPathForCloud(solrCloud.Spec.StorageOptions.BackupRestoreOptions.Directory, solrCloud.Name), |
| ReadOnly: false, |
| }) |
| |
| setupCommands = append(setupCommands, fmt.Sprintf("chown -R %d:%d /backup-restore", DefaultSolrUser, DefaultSolrGroup)) |
| } |
| |
| volumePrepInitContainer := corev1.Container{ |
| Name: "cp-solr-xml", |
| Image: solrCloud.Spec.BusyBoxImage.ToImageName(), |
| ImagePullPolicy: solrCloud.Spec.BusyBoxImage.PullPolicy, |
| Command: []string{"sh", "-c", strings.Join(setupCommands, " && ")}, |
| VolumeMounts: volumeMounts, |
| } |
| |
| containers = append(containers, volumePrepInitContainer) |
| |
| if hasZKSetupContainer, zkSetupContainer := generateZKInteractionInitContainer(solrCloud, solrCloudStatus, reconcileConfigInfo); hasZKSetupContainer { |
| containers = append(containers, zkSetupContainer) |
| } |
| |
| return containers |
| } |
| |
| // GenerateConfigMap returns a new corev1.ConfigMap pointer generated for the SolrCloud instance solr.xml |
| // solrCloud: SolrCloud instance |
| func GenerateConfigMap(solrCloud *solr.SolrCloud) *corev1.ConfigMap { |
| labels := solrCloud.SharedLabelsWith(solrCloud.GetLabels()) |
| var annotations map[string]string |
| |
| customOptions := solrCloud.Spec.CustomSolrKubeOptions.ConfigMapOptions |
| if nil != customOptions { |
| labels = MergeLabelsOrAnnotations(labels, customOptions.Labels) |
| annotations = MergeLabelsOrAnnotations(annotations, customOptions.Annotations) |
| } |
| |
| configMap := &corev1.ConfigMap{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: solrCloud.ConfigMapName(), |
| Namespace: solrCloud.GetNamespace(), |
| Labels: labels, |
| Annotations: annotations, |
| }, |
| Data: map[string]string{ |
| "solr.xml": `<?xml version="1.0" encoding="UTF-8" ?> |
| <solr> |
| <solrcloud> |
| <str name="host">${host:}</str> |
| <int name="hostPort">${hostPort:80}</int> |
| <str name="hostContext">${hostContext:solr}</str> |
| <bool name="genericCoreNodeNames">${genericCoreNodeNames:true}</bool> |
| <int name="zkClientTimeout">${zkClientTimeout:30000}</int> |
| <int name="distribUpdateSoTimeout">${distribUpdateSoTimeout:600000}</int> |
| <int name="distribUpdateConnTimeout">${distribUpdateConnTimeout:60000}</int> |
| <str name="zkCredentialsProvider">${zkCredentialsProvider:org.apache.solr.common.cloud.DefaultZkCredentialsProvider}</str> |
| <str name="zkACLProvider">${zkACLProvider:org.apache.solr.common.cloud.DefaultZkACLProvider}</str> |
| </solrcloud> |
| <shardHandlerFactory name="shardHandlerFactory" |
| class="HttpShardHandlerFactory"> |
| <int name="socketTimeout">${socketTimeout:600000}</int> |
| <int name="connTimeout">${connTimeout:60000}</int> |
| </shardHandlerFactory> |
| </solr> |
| `, |
| }, |
| } |
| |
| return configMap |
| } |
| |
| // GenerateCommonService returns a new corev1.Service pointer generated for the entire SolrCloud instance |
| // solrCloud: SolrCloud instance |
| func GenerateCommonService(solrCloud *solr.SolrCloud) *corev1.Service { |
| labels := solrCloud.SharedLabelsWith(solrCloud.GetLabels()) |
| labels["service-type"] = "common" |
| |
| selectorLabels := solrCloud.SharedLabels() |
| selectorLabels["technology"] = solr.SolrTechnologyLabel |
| |
| var annotations map[string]string |
| |
| // Add externalDNS annotation if necessary |
| extOpts := solrCloud.Spec.SolrAddressability.External |
| if extOpts != nil && extOpts.Method == solr.ExternalDNS && !extOpts.HideCommon { |
| annotations = make(map[string]string, 1) |
| urls := []string{solrCloud.ExternalDnsDomain(extOpts.DomainName)} |
| for _, domain := range extOpts.AdditionalDomainNames { |
| urls = append(urls, solrCloud.ExternalDnsDomain(domain)) |
| } |
| annotations["external-dns.alpha.kubernetes.io/hostname"] = strings.Join(urls, ",") |
| } |
| |
| customOptions := solrCloud.Spec.CustomSolrKubeOptions.CommonServiceOptions |
| if nil != customOptions { |
| labels = MergeLabelsOrAnnotations(labels, customOptions.Labels) |
| annotations = MergeLabelsOrAnnotations(annotations, customOptions.Annotations) |
| } |
| |
| service := &corev1.Service{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: solrCloud.CommonServiceName(), |
| Namespace: solrCloud.GetNamespace(), |
| Labels: labels, |
| Annotations: annotations, |
| }, |
| Spec: corev1.ServiceSpec{ |
| Ports: []corev1.ServicePort{ |
| {Name: SolrClientPortName, Port: int32(solrCloud.Spec.SolrAddressability.CommonServicePort), Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromString(SolrClientPortName)}, |
| }, |
| Selector: selectorLabels, |
| }, |
| } |
| return service |
| } |
| |
| // GenerateHeadlessService returns a new Headless corev1.Service pointer generated for the SolrCloud instance |
| // The PublishNotReadyAddresses option is set as true, because we want each pod to be reachable no matter the readiness of the pod. |
| // solrCloud: SolrCloud instance |
| func GenerateHeadlessService(solrCloud *solr.SolrCloud) *corev1.Service { |
| labels := solrCloud.SharedLabelsWith(solrCloud.GetLabels()) |
| labels["service-type"] = "headless" |
| |
| selectorLabels := solrCloud.SharedLabels() |
| selectorLabels["technology"] = solr.SolrTechnologyLabel |
| |
| var annotations map[string]string |
| |
| // Add externalDNS annotation if necessary |
| extOpts := solrCloud.Spec.SolrAddressability.External |
| if extOpts != nil && extOpts.Method == solr.ExternalDNS && !extOpts.HideNodes { |
| annotations = make(map[string]string, 1) |
| urls := []string{solrCloud.ExternalDnsDomain(extOpts.DomainName)} |
| for _, domain := range extOpts.AdditionalDomainNames { |
| urls = append(urls, solrCloud.ExternalDnsDomain(domain)) |
| } |
| annotations["external-dns.alpha.kubernetes.io/hostname"] = strings.Join(urls, ",") |
| } |
| |
| customOptions := solrCloud.Spec.CustomSolrKubeOptions.HeadlessServiceOptions |
| if nil != customOptions { |
| labels = MergeLabelsOrAnnotations(labels, customOptions.Labels) |
| annotations = MergeLabelsOrAnnotations(annotations, customOptions.Annotations) |
| } |
| |
| service := &corev1.Service{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: solrCloud.HeadlessServiceName(), |
| Namespace: solrCloud.GetNamespace(), |
| Labels: labels, |
| Annotations: annotations, |
| }, |
| Spec: corev1.ServiceSpec{ |
| Ports: []corev1.ServicePort{ |
| {Name: SolrClientPortName, Port: int32(solrCloud.NodePort()), Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromString(SolrClientPortName)}, |
| }, |
| Selector: selectorLabels, |
| ClusterIP: corev1.ClusterIPNone, |
| PublishNotReadyAddresses: true, |
| }, |
| } |
| return service |
| } |
| |
| // GenerateNodeService returns a new External corev1.Service pointer generated for the given Solr Node. |
| // The PublishNotReadyAddresses option is set as true, because we want each pod to be reachable no matter the readiness of the pod. |
| // solrCloud: SolrCloud instance |
| // nodeName: string node |
| func GenerateNodeService(solrCloud *solr.SolrCloud, nodeName string) *corev1.Service { |
| labels := solrCloud.SharedLabelsWith(solrCloud.GetLabels()) |
| labels["service-type"] = "external" |
| |
| selectorLabels := solrCloud.SharedLabels() |
| selectorLabels["technology"] = solr.SolrTechnologyLabel |
| selectorLabels["statefulset.kubernetes.io/pod-name"] = nodeName |
| |
| var annotations map[string]string |
| |
| customOptions := solrCloud.Spec.CustomSolrKubeOptions.NodeServiceOptions |
| if nil != customOptions { |
| labels = MergeLabelsOrAnnotations(labels, customOptions.Labels) |
| annotations = MergeLabelsOrAnnotations(annotations, customOptions.Annotations) |
| } |
| |
| service := &corev1.Service{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: nodeName, |
| Namespace: solrCloud.GetNamespace(), |
| Labels: labels, |
| Annotations: annotations, |
| }, |
| Spec: corev1.ServiceSpec{ |
| Selector: selectorLabels, |
| Ports: []corev1.ServicePort{ |
| {Name: SolrClientPortName, Port: int32(solrCloud.NodePort()), Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromString(SolrClientPortName)}, |
| }, |
| PublishNotReadyAddresses: true, |
| }, |
| } |
| return service |
| } |
| |
| // GenerateIngress returns a new Ingress pointer generated for the entire SolrCloud, pointing to all instances |
| // solrCloud: SolrCloud instance |
| // nodeStatuses: []SolrNodeStatus the nodeStatuses |
| func GenerateIngress(solrCloud *solr.SolrCloud, nodeNames []string) (ingress *netv1.Ingress) { |
| labels := solrCloud.SharedLabelsWith(solrCloud.GetLabels()) |
| var annotations map[string]string |
| |
| customOptions := solrCloud.Spec.CustomSolrKubeOptions.IngressOptions |
| if nil != customOptions { |
| labels = MergeLabelsOrAnnotations(labels, customOptions.Labels) |
| annotations = MergeLabelsOrAnnotations(annotations, customOptions.Annotations) |
| } |
| |
| extOpts := solrCloud.Spec.SolrAddressability.External |
| |
| // Create advertised domain name and possible additional domain names |
| rules := CreateSolrIngressRules(solrCloud, nodeNames, append([]string{extOpts.DomainName}, extOpts.AdditionalDomainNames...)) |
| |
| var ingressTLS []netv1.IngressTLS |
| if solrCloud.Spec.SolrTLS != nil { |
| if annotations == nil { |
| annotations = make(map[string]string, 1) |
| } |
| _, ok := annotations["nginx.ingress.kubernetes.io/backend-protocol"] |
| if !ok { |
| annotations["nginx.ingress.kubernetes.io/backend-protocol"] = "HTTPS" |
| } |
| ingressTLS = append(ingressTLS, netv1.IngressTLS{SecretName: solrCloud.Spec.SolrTLS.PKCS12Secret.Name}) |
| } |
| |
| ingress = &netv1.Ingress{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: solrCloud.CommonIngressName(), |
| Namespace: solrCloud.GetNamespace(), |
| Labels: labels, |
| Annotations: annotations, |
| }, |
| Spec: netv1.IngressSpec{ |
| Rules: rules, |
| TLS: ingressTLS, |
| }, |
| } |
| return ingress |
| } |
| |
| // CreateSolrIngressRules returns all applicable ingress rules for a cloud. |
| // solrCloud: SolrCloud instance |
| // nodeNames: the names for each of the solr pods |
| // domainName: string Domain for the ingress rule to use |
| func CreateSolrIngressRules(solrCloud *solr.SolrCloud, nodeNames []string, domainNames []string) []netv1.IngressRule { |
| var ingressRules []netv1.IngressRule |
| if !solrCloud.Spec.SolrAddressability.External.HideCommon { |
| for _, domainName := range domainNames { |
| ingressRules = append(ingressRules, CreateCommonIngressRule(solrCloud, domainName)) |
| } |
| } |
| if !solrCloud.Spec.SolrAddressability.External.HideNodes { |
| for _, nodeName := range nodeNames { |
| for _, domainName := range domainNames { |
| ingressRules = append(ingressRules, CreateNodeIngressRule(solrCloud, nodeName, domainName)) |
| } |
| } |
| } |
| return ingressRules |
| } |
| |
| // CreateCommonIngressRule returns a new Ingress Rule generated for a SolrCloud under the given domainName |
| // solrCloud: SolrCloud instance |
| // domainName: string Domain for the ingress rule to use |
| func CreateCommonIngressRule(solrCloud *solr.SolrCloud, domainName string) (ingressRule netv1.IngressRule) { |
| pathType := netv1.PathTypeImplementationSpecific |
| ingressRule = netv1.IngressRule{ |
| Host: solrCloud.ExternalCommonUrl(domainName, false), |
| IngressRuleValue: netv1.IngressRuleValue{ |
| HTTP: &netv1.HTTPIngressRuleValue{ |
| Paths: []netv1.HTTPIngressPath{ |
| { |
| Backend: netv1.IngressBackend{ |
| ServiceName: solrCloud.CommonServiceName(), |
| ServicePort: intstr.FromInt(solrCloud.Spec.SolrAddressability.CommonServicePort), |
| }, |
| PathType: &pathType, |
| }, |
| }, |
| }, |
| }, |
| } |
| return ingressRule |
| } |
| |
| // CreateNodeIngressRule returns a new Ingress Rule generated for a specific Solr Node under the given domainName |
| // solrCloud: SolrCloud instance |
| // nodeName: string Name of the node |
| // domainName: string Domain for the ingress rule to use |
| func CreateNodeIngressRule(solrCloud *solr.SolrCloud, nodeName string, domainName string) (ingressRule netv1.IngressRule) { |
| pathType := netv1.PathTypeImplementationSpecific |
| ingressRule = netv1.IngressRule{ |
| Host: solrCloud.ExternalNodeUrl(nodeName, domainName, false), |
| IngressRuleValue: netv1.IngressRuleValue{ |
| HTTP: &netv1.HTTPIngressRuleValue{ |
| Paths: []netv1.HTTPIngressPath{ |
| { |
| Backend: netv1.IngressBackend{ |
| ServiceName: nodeName, |
| ServicePort: intstr.FromInt(solrCloud.NodePort()), |
| }, |
| PathType: &pathType, |
| }, |
| }, |
| }, |
| }, |
| } |
| return ingressRule |
| } |
| |
| // TODO: Have this replace the postStart hook for creating the chroot |
| func generateZKInteractionInitContainer(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCloudStatus, reconcileConfigInfo map[string]string) (bool, corev1.Container) { |
| allSolrOpts := make([]string, 0) |
| |
| // Add all necessary ZK Info |
| envVars, zkSolrOpt, _ := createZkConnectionEnvVars(solrCloud, solrCloudStatus) |
| if zkSolrOpt != "" { |
| allSolrOpts = append(allSolrOpts, zkSolrOpt) |
| } |
| |
| 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 |
| if len(allSolrOpts) > 0 { |
| envVars = append(envVars, corev1.EnvVar{ |
| Name: "SOLR_OPTS", |
| Value: strings.Join(allSolrOpts, " "), |
| }) |
| } |
| |
| cmd := "" |
| |
| if solrCloud.Spec.SolrTLS != nil { |
| cmd = "solr zk ls ${ZK_CHROOT} -z ${ZK_SERVER} || solr zk mkroot ${ZK_CHROOT} -z ${ZK_SERVER}" + |
| "; /opt/solr/server/scripts/cloud-scripts/zkcli.sh -zkhost ${ZK_HOST} -cmd clusterprop -name urlScheme -val https" + |
| "; /opt/solr/server/scripts/cloud-scripts/zkcli.sh -zkhost ${ZK_HOST} -cmd get /clusterprops.json;" |
| } |
| |
| if reconcileConfigInfo[SecurityJsonFile] != "" { |
| envVars = append(envVars, corev1.EnvVar{Name: "SECURITY_JSON", ValueFrom: &corev1.EnvVarSource{ |
| SecretKeyRef: &corev1.SecretKeySelector{ |
| LocalObjectReference: corev1.LocalObjectReference{Name: solrCloud.SecurityBootstrapSecretName()}, |
| Key: SecurityJsonFile}}}) |
| |
| if cmd == "" { |
| cmd += "solr zk ls ${ZK_CHROOT} -z ${ZK_SERVER} || solr zk mkroot ${ZK_CHROOT} -z ${ZK_SERVER}; " |
| } |
| cmd += "ZK_SECURITY_JSON=$(/opt/solr/server/scripts/cloud-scripts/zkcli.sh -zkhost ${ZK_HOST} -cmd get /security.json); " |
| cmd += "if [ ${#ZK_SECURITY_JSON} -lt 3 ]; then echo $SECURITY_JSON > /tmp/security.json; /opt/solr/server/scripts/cloud-scripts/zkcli.sh -zkhost ${ZK_HOST} -cmd putfile /security.json /tmp/security.json; echo \"put security.json in ZK\"; fi" |
| } |
| |
| if cmd != "" { |
| return true, corev1.Container{ |
| Name: "setup-zk", |
| Image: solrCloud.Spec.SolrImage.ToImageName(), |
| ImagePullPolicy: solrCloud.Spec.SolrImage.PullPolicy, |
| TerminationMessagePath: "/dev/termination-log", |
| TerminationMessagePolicy: "File", |
| Command: []string{"sh", "-c", cmd}, |
| Env: envVars, |
| } |
| } |
| |
| return false, corev1.Container{} |
| } |
| |
| func TLSEnvVars(opts *solr.SolrTLSOptions, createPkcs12InitContainer bool) []corev1.EnvVar { |
| |
| // Determine the correct values for the SOLR_SSL_WANT_CLIENT_AUTH and SOLR_SSL_NEED_CLIENT_AUTH vars |
| wantClientAuth := "false" |
| needClientAuth := "false" |
| if opts.ClientAuth == solr.Need { |
| needClientAuth = "true" |
| } else if opts.ClientAuth == solr.Want { |
| wantClientAuth = "true" |
| } |
| |
| // the keystore path depends on whether we're just loading it from the secret or whether |
| // our initContainer has to generate it from the TLS secret using openssl |
| // this complexity is due to the secret mount directory not being writable |
| var keystorePath string |
| if createPkcs12InitContainer { |
| keystorePath = DefaultWritableKeyStorePath |
| } else { |
| keystorePath = DefaultKeyStorePath |
| } |
| |
| keystoreFile := keystorePath + "/" + Pkcs12KeystoreFile |
| passwordValueFrom := &corev1.EnvVarSource{SecretKeyRef: opts.KeyStorePasswordSecret} |
| |
| // If using a truststore that is different from the keystore |
| truststoreFile := keystoreFile |
| truststorePassFrom := passwordValueFrom |
| if opts.TrustStoreSecret != nil { |
| if opts.TrustStoreSecret.Name != opts.PKCS12Secret.Name { |
| // trust store is in a different secret, so will be mounted in a different dir |
| truststoreFile = DefaultTrustStorePath |
| } else { |
| // trust store is a different key in the same secret as the keystore |
| truststoreFile = DefaultKeyStorePath |
| } |
| truststoreFile += "/" + opts.TrustStoreSecret.Key |
| if opts.TrustStorePasswordSecret != nil { |
| truststorePassFrom = &corev1.EnvVarSource{SecretKeyRef: opts.TrustStorePasswordSecret} |
| } |
| } |
| |
| envVars := []corev1.EnvVar{ |
| { |
| Name: "SOLR_SSL_ENABLED", |
| Value: "true", |
| }, |
| { |
| Name: "SOLR_SSL_KEY_STORE", |
| Value: keystoreFile, |
| }, |
| { |
| Name: "SOLR_SSL_KEY_STORE_PASSWORD", |
| ValueFrom: passwordValueFrom, |
| }, |
| { |
| Name: "SOLR_SSL_TRUST_STORE", |
| Value: truststoreFile, |
| }, |
| { |
| Name: "SOLR_SSL_TRUST_STORE_PASSWORD", |
| ValueFrom: truststorePassFrom, |
| }, |
| { |
| Name: "SOLR_SSL_WANT_CLIENT_AUTH", |
| Value: wantClientAuth, |
| }, |
| { |
| Name: "SOLR_SSL_NEED_CLIENT_AUTH", |
| Value: needClientAuth, |
| }, |
| { |
| Name: "SOLR_SSL_CLIENT_HOSTNAME_VERIFICATION", |
| Value: strconv.FormatBool(opts.VerifyClientHostname), |
| }, |
| { |
| Name: "SOLR_SSL_CHECK_PEER_NAME", |
| Value: strconv.FormatBool(opts.CheckPeerName), |
| }, |
| } |
| |
| return envVars |
| } |
| |
| func tlsVolumeMounts(opts *solr.SolrTLSOptions, createPkcs12InitContainer bool) []corev1.VolumeMount { |
| mounts := []corev1.VolumeMount{ |
| { |
| Name: "keystore", |
| ReadOnly: true, |
| MountPath: DefaultKeyStorePath, |
| }, |
| } |
| |
| if opts.TrustStoreSecret != nil && opts.TrustStoreSecret.Name != opts.PKCS12Secret.Name { |
| mounts = append(mounts, corev1.VolumeMount{ |
| Name: "truststore", |
| ReadOnly: true, |
| MountPath: DefaultTrustStorePath, |
| }) |
| } |
| |
| // We need an initContainer to convert a TLS cert into the pkcs12 format Java wants (using openssl) |
| // but openssl cannot write to the /var/solr/tls directory because of the way secret mounts work |
| // so we need to mount an empty directory to write pkcs12 keystore into |
| if createPkcs12InitContainer { |
| mounts = append(mounts, corev1.VolumeMount{ |
| Name: "pkcs12", |
| ReadOnly: false, |
| MountPath: DefaultWritableKeyStorePath, |
| }) |
| } |
| |
| return mounts |
| } |
| |
| func tlsVolumes(opts *solr.SolrTLSOptions, createPkcs12InitContainer bool) []corev1.Volume { |
| optional := false |
| defaultMode := int32(0664) |
| vols := []corev1.Volume{ |
| { |
| Name: "keystore", |
| VolumeSource: corev1.VolumeSource{ |
| Secret: &corev1.SecretVolumeSource{ |
| SecretName: opts.PKCS12Secret.Name, |
| DefaultMode: &defaultMode, |
| Optional: &optional, |
| }, |
| }, |
| }, |
| } |
| |
| // if they're using a different truststore other than the keystore, but don't mount an additional volume |
| // if it's just pointing at the same secret |
| if opts.TrustStoreSecret != nil && opts.TrustStoreSecret.Name != opts.PKCS12Secret.Name { |
| vols = append(vols, corev1.Volume{ |
| Name: "truststore", |
| VolumeSource: corev1.VolumeSource{ |
| Secret: &corev1.SecretVolumeSource{ |
| SecretName: opts.TrustStoreSecret.Name, |
| DefaultMode: &defaultMode, |
| Optional: &optional, |
| }, |
| }, |
| }) |
| } |
| |
| if createPkcs12InitContainer { |
| vols = append(vols, corev1.Volume{ |
| Name: "pkcs12", |
| VolumeSource: corev1.VolumeSource{ |
| EmptyDir: &corev1.EmptyDirVolumeSource{}, |
| }, |
| }) |
| } |
| |
| return vols |
| } |
| |
| func generatePkcs12InitContainer(opts *solr.SolrTLSOptions, imageName string, imagePullPolicy corev1.PullPolicy) corev1.Container { |
| // get the keystore password from the env for generating the keystore using openssl |
| passwordValueFrom := &corev1.EnvVarSource{SecretKeyRef: opts.KeyStorePasswordSecret} |
| envVars := []corev1.EnvVar{ |
| { |
| Name: "SOLR_SSL_KEY_STORE_PASSWORD", |
| ValueFrom: passwordValueFrom, |
| }, |
| } |
| |
| cmd := "openssl pkcs12 -export -in " + DefaultKeyStorePath + "/" + TLSCertKey + " -in " + DefaultKeyStorePath + |
| "/ca.crt -inkey " + DefaultKeyStorePath + "/tls.key -out " + DefaultKeyStorePath + |
| "/pkcs12/" + Pkcs12KeystoreFile + " -passout pass:${SOLR_SSL_KEY_STORE_PASSWORD}" |
| return corev1.Container{ |
| Name: "gen-pkcs12-keystore", |
| Image: imageName, |
| ImagePullPolicy: imagePullPolicy, |
| TerminationMessagePath: "/dev/termination-log", |
| TerminationMessagePolicy: "File", |
| Command: []string{"sh", "-c", cmd}, |
| VolumeMounts: tlsVolumeMounts(opts, true), |
| Env: envVars, |
| } |
| } |
| |
| func createZkConnectionEnvVars(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCloudStatus) (envVars []corev1.EnvVar, solrOpt string, hasChroot bool) { |
| zkConnectionStr, zkServer, zkChroot := solrCloudStatus.DissectZkInfo() |
| envVars = []corev1.EnvVar{ |
| { |
| Name: "ZK_HOST", |
| Value: zkConnectionStr, |
| }, |
| { |
| Name: "ZK_CHROOT", |
| Value: zkChroot, |
| }, |
| { |
| Name: "ZK_SERVER", |
| Value: zkServer, |
| }, |
| } |
| |
| // Add ACL information, if given, through Env Vars |
| allACL, readOnlyACL := solrCloud.Spec.ZookeeperRef.GetACLs() |
| if hasACLs, aclEnvs := AddACLsToEnv(allACL, readOnlyACL); hasACLs { |
| envVars = append(envVars, aclEnvs...) |
| |
| // The $SOLR_ZK_CREDS_AND_ACLS parameter does not get picked up when running solr, it must be added to the SOLR_OPTS. |
| solrOpt = "$(SOLR_ZK_CREDS_AND_ACLS)" |
| } |
| |
| return envVars, solrOpt, len(zkChroot) > 1 |
| } |
| |
| func setupVolumeMountForUserProvidedConfigMapEntry(reconcileConfigInfo map[string]string, fileKey string, solrVolumes []corev1.Volume, envVar string) (*corev1.VolumeMount, *corev1.EnvVar, *corev1.Volume) { |
| volName := strings.ReplaceAll(fileKey, ".", "-") |
| mountPath := fmt.Sprintf("/var/solr/%s", reconcileConfigInfo[fileKey]) |
| appendedToExisting := false |
| if reconcileConfigInfo[fileKey] == reconcileConfigInfo[SolrXmlFile] { |
| // the user provided a custom log4j2.xml and solr.xml, append to the volume for solr.xml created above |
| for _, vol := range solrVolumes { |
| if vol.Name == "solr-xml" { |
| vol.ConfigMap.Items = append(vol.ConfigMap.Items, corev1.KeyToPath{Key: fileKey, Path: fileKey}) |
| appendedToExisting = true |
| volName = vol.Name |
| break |
| } |
| } |
| } |
| |
| var vol *corev1.Volume = nil |
| if !appendedToExisting { |
| defaultMode := int32(420) |
| vol = &corev1.Volume{ |
| Name: volName, |
| VolumeSource: corev1.VolumeSource{ |
| ConfigMap: &corev1.ConfigMapVolumeSource{ |
| LocalObjectReference: corev1.LocalObjectReference{Name: reconcileConfigInfo[fileKey]}, |
| Items: []corev1.KeyToPath{{Key: fileKey, Path: fileKey}}, |
| DefaultMode: &defaultMode, |
| }, |
| }, |
| } |
| } |
| pathToFile := fmt.Sprintf("%s/%s", mountPath, fileKey) |
| |
| return &corev1.VolumeMount{Name: volName, MountPath: mountPath}, &corev1.EnvVar{Name: envVar, Value: pathToFile}, vol |
| } |
| |
| func BasicAuthHeader(basicAuthSecret *corev1.Secret) string { |
| creds := fmt.Sprintf("%s:%s", basicAuthSecret.Data[corev1.BasicAuthUsernameKey], basicAuthSecret.Data[corev1.BasicAuthPasswordKey]) |
| return "Basic " + b64.StdEncoding.EncodeToString([]byte(creds)) |
| } |
| |
| func ValidateBasicAuthSecret(basicAuthSecret *corev1.Secret) error { |
| if basicAuthSecret.Type != corev1.SecretTypeBasicAuth { |
| return fmt.Errorf("invalid secret type %v; user-provided secret %s must be of type: %v", |
| basicAuthSecret.Type, basicAuthSecret.Name, corev1.SecretTypeBasicAuth) |
| } |
| |
| if _, ok := basicAuthSecret.Data[corev1.BasicAuthUsernameKey]; !ok { |
| return fmt.Errorf("%s key not found in user-provided basic-auth secret %s", |
| corev1.BasicAuthUsernameKey, basicAuthSecret.Name) |
| } |
| |
| if _, ok := basicAuthSecret.Data[corev1.BasicAuthPasswordKey]; !ok { |
| return fmt.Errorf("%s key not found in user-provided basic-auth secret %s", |
| corev1.BasicAuthPasswordKey, basicAuthSecret.Name) |
| } |
| |
| return nil |
| } |
| |
| func GenerateBasicAuthSecretWithBootstrap(solrCloud *solr.SolrCloud) (*corev1.Secret, *corev1.Secret) { |
| |
| securityBootstrapInfo := generateSecurityJson(solrCloud) |
| |
| labels := solrCloud.SharedLabelsWith(solrCloud.GetLabels()) |
| var annotations map[string]string |
| basicAuthSecret := &corev1.Secret{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: solrCloud.BasicAuthSecretName(), |
| Namespace: solrCloud.GetNamespace(), |
| Labels: labels, |
| Annotations: annotations, |
| }, |
| Data: map[string][]byte{ |
| corev1.BasicAuthUsernameKey: []byte(solr.DefaultBasicAuthUsername), |
| corev1.BasicAuthPasswordKey: securityBootstrapInfo[solr.DefaultBasicAuthUsername], |
| }, |
| Type: corev1.SecretTypeBasicAuth, |
| } |
| |
| // this secret holds the admin and solr user credentials and the security.json needed to bootstrap Solr security |
| // once the security.json is created using the setup-zk initContainer, it is not updated by the operator |
| boostrapSecuritySecret := &corev1.Secret{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: solrCloud.SecurityBootstrapSecretName(), |
| Namespace: solrCloud.GetNamespace(), |
| Labels: labels, |
| Annotations: annotations, |
| }, |
| Data: map[string][]byte{ |
| "admin": securityBootstrapInfo["admin"], |
| "solr": securityBootstrapInfo["solr"], |
| SecurityJsonFile: securityBootstrapInfo[SecurityJsonFile], |
| }, |
| Type: corev1.SecretTypeOpaque, |
| } |
| |
| return basicAuthSecret, boostrapSecuritySecret |
| } |
| |
| func generateSecurityJson(solrCloud *solr.SolrCloud) map[string][]byte { |
| blockUnknown := true |
| |
| probeRole := "\"k8s\"" // probe endpoints are secures |
| if !solrCloud.Spec.SolrSecurity.ProbesRequireAuth { |
| blockUnknown = false |
| probeRole = "null" // a JSON null value here to allow open access |
| } |
| |
| probePaths := getProbePaths(solrCloud) |
| probeAuthz := "" |
| for i, p := range probePaths { |
| if i > 0 { |
| probeAuthz += ", " |
| } |
| if strings.HasPrefix(p, "/solr") { |
| p = p[len("/solr"):] |
| } |
| probeAuthz += fmt.Sprintf("{ \"name\": \"k8s-probe-%d\", \"role\":%s, \"collection\": null, \"path\":\"%s\" }", i, probeRole, p) |
| } |
| |
| // Create the user accounts for security.json with random passwords |
| // hashed with random salt, just as Solr's hashing works |
| username := solr.DefaultBasicAuthUsername |
| users := []string{"admin", username, "solr"} |
| secretData := make(map[string][]byte, len(users)) |
| credentials := make(map[string]string, len(users)) |
| for _, u := range users { |
| secretData[u] = randomPassword() |
| credentials[u] = solrPasswordHash(secretData[u]) |
| } |
| credentialsJson, _ := json.Marshal(credentials) |
| |
| securityJson := fmt.Sprintf(`{ |
| "authentication":{ |
| "blockUnknown": %t, |
| "class":"solr.BasicAuthPlugin", |
| "credentials": %s, |
| "realm":"Solr Basic Auth", |
| "forwardCredentials": false |
| }, |
| "authorization": { |
| "class": "solr.RuleBasedAuthorizationPlugin", |
| "user-role": { |
| "admin": ["admin", "k8s"], |
| "%s": ["k8s"], |
| "solr": ["users", "k8s"] |
| }, |
| "permissions": [ |
| %s, |
| { "name": "k8s-status", "role":"k8s", "collection": null, "path":"/admin/collections" }, |
| { "name": "k8s-metrics", "role":"k8s", "collection": null, "path":"/admin/metrics" }, |
| { "name": "k8s-ping", "role":"k8s", "collection": "*", "path":"/admin/ping" }, |
| { "name": "all", "role":["admin","users"] }, |
| { "name": "read", "role":["admin","users"] }, |
| { "name": "update", "role":["admin"] }, |
| { "name": "security-read", "role": "admin"}, |
| { "name": "security-edit", "role": "admin"} |
| ] |
| } |
| }`, blockUnknown, credentialsJson, username, probeAuthz) |
| |
| // we need to store the security.json in the secret, otherwise we'd recompute it for every reconcile loop |
| // but that doesn't work for randomized passwords ... |
| secretData[SecurityJsonFile] = []byte(securityJson) |
| |
| return secretData |
| } |
| |
| func GetCustomProbePaths(solrCloud *solr.SolrCloud) []string { |
| probePaths := []string{} |
| |
| podOptions := solrCloud.Spec.CustomSolrKubeOptions.PodOptions |
| if podOptions == nil { |
| return probePaths |
| } |
| |
| // include any custom paths |
| if podOptions.ReadinessProbe != nil && podOptions.ReadinessProbe.HTTPGet != nil { |
| probePaths = append(probePaths, podOptions.ReadinessProbe.HTTPGet.Path) |
| } |
| |
| if podOptions.LivenessProbe != nil && podOptions.LivenessProbe.HTTPGet != nil { |
| probePaths = append(probePaths, podOptions.LivenessProbe.HTTPGet.Path) |
| } |
| |
| if podOptions.StartupProbe != nil && podOptions.StartupProbe.HTTPGet != nil { |
| probePaths = append(probePaths, podOptions.StartupProbe.HTTPGet.Path) |
| } |
| |
| return probePaths |
| } |
| |
| // Gets a list of probe paths we need to setup authz for |
| func getProbePaths(solrCloud *solr.SolrCloud) []string { |
| probePaths := []string{DefaultProbePath} |
| probePaths = append(probePaths, GetCustomProbePaths(solrCloud)...) |
| return uniqueProbePaths(probePaths) |
| } |
| |
| func randomPassword() []byte { |
| rand.Seed(time.Now().UnixNano()) |
| lower := "abcdefghijklmnpqrstuvwxyz" // no 'o' |
| upper := strings.ToUpper(lower) |
| digits := "0123456789" |
| chars := lower + upper + digits + "()[]%#@-()[]%#@-" |
| pass := make([]byte, 16) |
| // start with a lower char and end with an upper |
| pass[0] = lower[rand.Intn(len(lower))] |
| pass[len(pass)-1] = upper[rand.Intn(len(upper))] |
| perm := rand.Perm(len(chars)) |
| for i := 1; i < len(pass)-1; i++ { |
| pass[i] = chars[perm[i]] |
| } |
| return pass |
| } |
| |
| func randomSaltHash() []byte { |
| b := make([]byte, 32) |
| rand.Read(b) |
| salt := sha256.Sum256(b) |
| return salt[:] |
| } |
| |
| // this mimics the password hash generation approach used by Solr |
| func solrPasswordHash(passBytes []byte) string { |
| // combine password with salt to create the hash |
| salt := randomSaltHash() |
| passHashBytes := sha256.Sum256(append(salt[:], passBytes...)) |
| passHashBytes = sha256.Sum256(passHashBytes[:]) |
| passHash := b64.StdEncoding.EncodeToString(passHashBytes[:]) |
| return fmt.Sprintf("%s %s", passHash, b64.StdEncoding.EncodeToString(salt)) |
| } |
| |
| func uniqueProbePaths(paths []string) []string { |
| keys := make(map[string]bool) |
| var set []string |
| for _, name := range paths { |
| if _, exists := keys[name]; !exists { |
| keys[name] = true |
| set = append(set, name) |
| } |
| } |
| return set |
| } |
| |
| // When running with TLS and clientAuth=Need or if the probe endpoints require auth, we need to use a command instead of HTTP Get |
| // This function builds the custom probe command and returns any associated volume / mounts needed for the auth secrets |
| func configureSecureProbeCommand(solrCloud *solr.SolrCloud, defaultProbeGetAction *corev1.HTTPGetAction) (string, *corev1.Volume, *corev1.VolumeMount) { |
| // mount the secret in a file so it gets updated; env vars do not see: |
| // https://kubernetes.io/docs/concepts/configuration/secret/#environment-variables-are-not-updated-after-a-secret-update |
| basicAuthOption := "" |
| enableBasicAuth := "" |
| var volMount *corev1.VolumeMount |
| var vol *corev1.Volume |
| if solrCloud.Spec.SolrSecurity != nil { |
| secretName := solrCloud.BasicAuthSecretName() |
| defaultMode := int32(420) |
| vol = &corev1.Volume{ |
| Name: strings.ReplaceAll(secretName, ".", "-"), |
| VolumeSource: corev1.VolumeSource{ |
| Secret: &corev1.SecretVolumeSource{ |
| SecretName: secretName, |
| DefaultMode: &defaultMode, |
| }, |
| }, |
| } |
| mountPath := fmt.Sprintf("/etc/secrets/%s", vol.Name) |
| volMount = &corev1.VolumeMount{Name: vol.Name, MountPath: mountPath} |
| usernameFile := fmt.Sprintf("%s/%s", mountPath, corev1.BasicAuthUsernameKey) |
| passwordFile := fmt.Sprintf("%s/%s", mountPath, corev1.BasicAuthPasswordKey) |
| basicAuthOption = fmt.Sprintf("-Dbasicauth=$(cat %s):$(cat %s)", usernameFile, passwordFile) |
| enableBasicAuth = " -Dsolr.httpclient.builder.factory=org.apache.solr.client.solrj.impl.PreemptiveBasicAuthClientBuilderFactory " |
| } |
| |
| // Is TLS enabled? If so we need some additional SSL related props |
| tlsProps := "" |
| if solrCloud.Spec.SolrTLS != nil { |
| tlsProps = "-Djavax.net.ssl.keyStore=$SOLR_SSL_KEY_STORE -Djavax.net.ssl.keyStorePassword=$SOLR_SSL_KEY_STORE_PASSWORD " + |
| "-Djavax.net.ssl.trustStore=$SOLR_SSL_TRUST_STORE -Djavax.net.ssl.trustStorePassword=$SOLR_SSL_TRUST_STORE_PASSWORD" |
| } |
| |
| javaToolOptions := strings.TrimSpace(basicAuthOption + " " + tlsProps) |
| |
| // construct the probe command to invoke the SolrCLI "api" action |
| // |
| // and yes, this is ugly, but bin/solr doesn't expose the "api" action (as of 8.8.0) so we have to invoke java directly |
| // taking some liberties on the /opt/solr path based on the official Docker image as there is no ENV var set for that path |
| probeCommand := fmt.Sprintf("JAVA_TOOL_OPTIONS=\"%s\" java -Dsolr.ssl.checkPeerName=false %s "+ |
| "-Dsolr.install.dir=\"/opt/solr\" -Dlog4j.configurationFile=\"/opt/solr/server/resources/log4j2-console.xml\" "+ |
| "-classpath \"/opt/solr/server/solr-webapp/webapp/WEB-INF/lib/*:/opt/solr/server/lib/ext/*:/opt/solr/server/lib/*\" "+ |
| "org.apache.solr.util.SolrCLI api -get %s://localhost:%d%s", |
| javaToolOptions, enableBasicAuth, solrCloud.UrlScheme(), defaultProbeGetAction.Port.IntVal, defaultProbeGetAction.Path) |
| probeCommand = regexp.MustCompile(`\s+`).ReplaceAllString(strings.TrimSpace(probeCommand), " ") |
| |
| return probeCommand, vol, volMount |
| } |