You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by th...@apache.org on 2021/10/06 16:25:34 UTC
[solr-operator] branch main updated: Refactor security related code
into a separate solr_security_util.go vs. sprinkled throughout the
codebase. (#334)
This is an automated email from the ASF dual-hosted git repository.
thelabdude pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-operator.git
The following commit(s) were added to refs/heads/main by this push:
new 75bdb0d Refactor security related code into a separate solr_security_util.go vs. sprinkled throughout the codebase. (#334)
75bdb0d is described below
commit 75bdb0dc05e80fd362e7c2e04fb18faf30f395f6
Author: Timothy Potter <th...@gmail.com>
AuthorDate: Wed Oct 6 10:25:28 2021 -0600
Refactor security related code into a separate solr_security_util.go vs. sprinkled throughout the codebase. (#334)
---
controllers/solrcloud_controller.go | 91 +-----
controllers/solrcloud_controller_tls_test.go | 3 +
controllers/util/prometheus_exporter_util.go | 6 +-
controllers/util/solr_security_util.go | 433 +++++++++++++++++++++++++++
controllers/util/solr_tls_util.go | 3 +-
controllers/util/solr_util.go | 293 ++----------------
6 files changed, 468 insertions(+), 361 deletions(-)
diff --git a/controllers/solrcloud_controller.go b/controllers/solrcloud_controller.go
index cc27f62..19df029 100644
--- a/controllers/solrcloud_controller.go
+++ b/controllers/solrcloud_controller.go
@@ -279,86 +279,13 @@ func (r *SolrCloudReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
}
}
- basicAuthHeader := ""
+ // Holds security config info needed during construction of the StatefulSet
+ var security *util.SecurityConfig = nil
if instance.Spec.SolrSecurity != nil {
- sec := instance.Spec.SolrSecurity
-
- if sec.AuthenticationType != solrv1beta1.Basic {
- return requeueOrNot, fmt.Errorf("%s not supported! Only 'Basic' authentication is supported by the Solr operator",
- instance.Spec.SolrSecurity.AuthenticationType)
- }
-
- // for now, we don't support 'solrSecurity.probesRequireAuth=true' and custom probe paths,
- // so make the user fix that so there are no surprises later
- if sec.ProbesRequireAuth && instance.Spec.CustomSolrKubeOptions.PodOptions != nil {
- for _, path := range util.GetCustomProbePaths(instance) {
- if path != util.DefaultProbePath {
- return requeueOrNot, fmt.Errorf(
- "custom probe path %s not supported when 'solrSecurity.probesRequireAuth=true'; must use 'solrSecurity.probesRequireAuth=false' when using custom probe endpoints", path)
- }
- }
- }
-
- basicAuthSecret := &corev1.Secret{}
-
- // user has the option of providing a secret with credentials the operator should use to make requests to Solr
- if sec.BasicAuthSecret != "" {
- if err := r.Get(ctx, types.NamespacedName{Name: sec.BasicAuthSecret, Namespace: instance.Namespace}, basicAuthSecret); err != nil {
- return requeueOrNot, err
- }
-
- err = util.ValidateBasicAuthSecret(basicAuthSecret)
- if err != nil {
- return requeueOrNot, err
- }
-
- } else {
- // We're supplying a secret with random passwords and a default security.json
- // since we randomly generate the passwords, we need to lookup the secret first and only create if not exist
- err = r.Get(ctx, types.NamespacedName{Name: instance.BasicAuthSecretName(), Namespace: instance.Namespace}, basicAuthSecret)
- if err != nil && errors.IsNotFound(err) {
- authSecret, bootstrapSecret := util.GenerateBasicAuthSecretWithBootstrap(instance)
- if err := controllerutil.SetControllerReference(instance, authSecret, r.Scheme); err != nil {
- return requeueOrNot, err
- }
- if err := controllerutil.SetControllerReference(instance, bootstrapSecret, r.Scheme); err != nil {
- return requeueOrNot, err
- }
- err = r.Create(ctx, authSecret)
- if err != nil {
- return requeueOrNot, err
- }
- err = r.Create(ctx, bootstrapSecret)
- if err == nil {
- // supply the bootstrap security.json to the initContainer via a simple BASE64 encoding env var
- reconcileConfigInfo[util.SecurityJsonFile] = string(bootstrapSecret.Data[util.SecurityJsonFile])
- }
-
- basicAuthSecret = authSecret
- }
- if err != nil {
- return requeueOrNot, err
- }
-
- if reconcileConfigInfo[util.SecurityJsonFile] == "" {
- // the bootstrap secret already exists, so just stash the security.json needed for constructing initContainers
- bootstrapSecret := &corev1.Secret{}
- err = r.Get(ctx, types.NamespacedName{Name: instance.SecurityBootstrapSecretName(), Namespace: instance.Namespace}, bootstrapSecret)
- if err != nil {
- if !errors.IsNotFound(err) {
- return requeueOrNot, err
- } // else perhaps the user deleted it after security was bootstrapped ... this is ok but may trigger a restart on the STS
- } else {
- // stash this so we can configure the setup-zk initContainer to bootstrap the security.json in ZK
- reconcileConfigInfo[util.SecurityJsonFile] = string(bootstrapSecret.Data[util.SecurityJsonFile])
- }
- }
+ security, err = util.ReconcileSecurityConfig(ctx, &r.Client, instance)
+ if err != nil {
+ return requeueOrNot, err
}
-
- reconcileConfigInfo[corev1.BasicAuthUsernameKey] = string(basicAuthSecret.Data[corev1.BasicAuthUsernameKey])
-
- // need the creds below for getting CLUSTERSTATUS
- basicAuthHeader = util.BasicAuthHeader(basicAuthSecret)
}
// Only create stateful set if zkConnectionString can be found (must contain host and port)
@@ -387,7 +314,7 @@ func (r *SolrCloudReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
if !blockReconciliationOfStatefulSet {
// Generate StatefulSet
- statefulSet := util.GenerateStatefulSet(instance, &newStatus, hostNameIpMap, reconcileConfigInfo, tls)
+ statefulSet := util.GenerateStatefulSet(instance, &newStatus, hostNameIpMap, reconcileConfigInfo, tls, security)
// Check if the StatefulSet already exists
statefulSetLogger := logger.WithValues("statefulSet", statefulSet.Name)
@@ -481,10 +408,10 @@ func (r *SolrCloudReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
logger.Info("Pod killed for update.", "pod", pod.Name, "reason", "The solr container in the pod has not yet started, thus it is safe to update.")
}
- // If authn enabled on Solr, we need to pass the basic auth header
+ // If authn enabled on Solr, we need to pass the auth header
var authHeader map[string]string
- if basicAuthHeader != "" {
- authHeader = map[string]string{"Authorization": basicAuthHeader}
+ if security != nil {
+ authHeader = security.AuthHeader()
}
// Pick which pods should be deleted for an update.
diff --git a/controllers/solrcloud_controller_tls_test.go b/controllers/solrcloud_controller_tls_test.go
index 5c2b2bd..b5d1b2e 100644
--- a/controllers/solrcloud_controller_tls_test.go
+++ b/controllers/solrcloud_controller_tls_test.go
@@ -54,6 +54,7 @@ var _ = FDescribe("SolrCloud controller - TLS", func() {
Replicas: &replicas,
ZookeeperRef: &solrv1beta1.ZookeeperRef{
ConnectionInfo: &solrv1beta1.ZookeeperConnectionInfo{
+ ChRoot: "tls-test",
InternalConnectionString: "host:7271",
},
},
@@ -795,6 +796,7 @@ func expectZkSetupInitContainerForTLSWithGomega(g Gomega, solrCloud *solrv1beta1
break
}
}
+ expChrootCmd := "solr zk ls ${ZK_CHROOT} -z ${ZK_SERVER} || solr zk mkroot ${ZK_CHROOT} -z ${ZK_SERVER};"
expCmd := "/opt/solr/server/scripts/cloud-scripts/zkcli.sh -zkhost ${ZK_HOST} -cmd clusterprop -name urlScheme -val https"
if solrCloud.Spec.SolrTLS != nil {
g.Expect(zkSetupInitContainer).To(Not(BeNil()), "Didn't find the zk-setup InitContainer in the sts!")
@@ -802,6 +804,7 @@ func expectZkSetupInitContainerForTLSWithGomega(g Gomega, solrCloud *solrv1beta1
g.Expect(zkSetupInitContainer.Image).To(Equal(statefulSet.Spec.Template.Spec.Containers[0].Image), "The zk-setup init container should use the same image as the Solr container")
g.Expect(zkSetupInitContainer.Command).To(HaveLen(3), "Wrong command length for zk-setup init container")
g.Expect(zkSetupInitContainer.Command[2]).To(ContainSubstring(expCmd), "ZK Setup command does not set urlScheme")
+ g.Expect(zkSetupInitContainer.Command[2]).To(ContainSubstring(expChrootCmd), "ZK Setup command does init the chroot")
expNumVars := 3
if solrCloud.Spec.SolrSecurity != nil && solrCloud.Spec.SolrSecurity.BasicAuthSecret == "" {
expNumVars = 4 // one more for SECURITY_JSON
diff --git a/controllers/util/prometheus_exporter_util.go b/controllers/util/prometheus_exporter_util.go
index f5a122a..fb92a68 100644
--- a/controllers/util/prometheus_exporter_util.go
+++ b/controllers/util/prometheus_exporter_util.go
@@ -164,11 +164,7 @@ func GenerateSolrPrometheusExporterDeployment(solrPrometheusExporter *solr.SolrP
// basic auth enabled?
if solrPrometheusExporter.Spec.SolrReference.BasicAuthSecret != "" {
- lor := corev1.LocalObjectReference{Name: solrPrometheusExporter.Spec.SolrReference.BasicAuthSecret}
- usernameRef := &corev1.SecretKeySelector{LocalObjectReference: lor, Key: corev1.BasicAuthUsernameKey}
- passwordRef := &corev1.SecretKeySelector{LocalObjectReference: lor, Key: corev1.BasicAuthPasswordKey}
- envVars = append(envVars, corev1.EnvVar{Name: "BASIC_AUTH_USER", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: usernameRef}})
- envVars = append(envVars, corev1.EnvVar{Name: "BASIC_AUTH_PASS", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: passwordRef}})
+ envVars = append(envVars, BasicAuthEnvVars(solrPrometheusExporter.Spec.SolrReference.BasicAuthSecret)...)
allJavaOpts = append(allJavaOpts, "-Dbasicauth=$(BASIC_AUTH_USER):$(BASIC_AUTH_PASS)")
allJavaOpts = append(allJavaOpts, "-Dsolr.httpclient.builder.factory=org.apache.solr.client.solrj.impl.PreemptiveBasicAuthClientBuilderFactory")
}
diff --git a/controllers/util/solr_security_util.go b/controllers/util/solr_security_util.go
new file mode 100644
index 0000000..9701ab2
--- /dev/null
+++ b/controllers/util/solr_security_util.go
@@ -0,0 +1,433 @@
+/*
+ * 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 (
+ "context"
+ "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"
+ "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+ "math/rand"
+ "regexp"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+ "strings"
+ "time"
+)
+
+const (
+ SecurityJsonFile = "security.json"
+ BasicAuthMd5Annotation = "solr.apache.org/basicAuthMd5"
+ DefaultProbePath = "/admin/info/system"
+)
+
+type SecurityConfig struct {
+ BasicAuthSecret *corev1.Secret
+ SecurityJson string
+}
+
+// Given a SolrCloud instance and an API service client, produce a SecurityConfig needed to enable Solr security
+func ReconcileSecurityConfig(ctx context.Context, client *client.Client, instance *solr.SolrCloud) (*SecurityConfig, error) {
+ reader := *client
+
+ security := &SecurityConfig{}
+ basicAuthSecret := &corev1.Secret{}
+
+ // user has the option of providing a secret with credentials the operator should use to make requests to Solr
+ sec := instance.Spec.SolrSecurity
+
+ if sec.AuthenticationType != solr.Basic {
+ return nil, fmt.Errorf("%s not supported! Only 'Basic' authentication is supported by the Solr operator",
+ instance.Spec.SolrSecurity.AuthenticationType)
+ }
+
+ // TODO: we shouldn't need to enforce this restriction?!?
+ //
+ // for now, we don't support 'solrSecurity.probesRequireAuth=true' and custom probe paths,
+ // so make the user fix that so there are no surprises later
+ if sec.ProbesRequireAuth && instance.Spec.CustomSolrKubeOptions.PodOptions != nil {
+ for _, path := range GetCustomProbePaths(instance) {
+ if path != DefaultProbePath {
+ return nil, fmt.Errorf(
+ "custom probe path %s not supported when 'solrSecurity.probesRequireAuth=true'; must use 'solrSecurity.probesRequireAuth=false' when using custom probe endpoints", path)
+ }
+ }
+ }
+
+ if sec.BasicAuthSecret != "" {
+ // the user supplied their own basic auth secret, make sure it exists and has the expected keys
+ if err := reader.Get(ctx, types.NamespacedName{Name: sec.BasicAuthSecret, Namespace: instance.Namespace}, basicAuthSecret); err != nil {
+ return nil, err
+ }
+
+ err := ValidateBasicAuthSecret(basicAuthSecret)
+ if err != nil {
+ return nil, err
+ }
+
+ // since the user supplied us with a basic auth secret, we're assuming they're also bootstrapping the security.json,
+ // so there is no bootstrap secret in this case
+
+ } else {
+ // We're supplying a secret with random passwords and a default security.json
+ // since we randomly generate the passwords, we need to lookup the secret first and only create if not exist
+ err := reader.Get(ctx, types.NamespacedName{Name: instance.BasicAuthSecretName(), Namespace: instance.Namespace}, basicAuthSecret)
+ if err != nil && errors.IsNotFound(err) {
+ authSecret, bootstrapSecret := generateBasicAuthSecretWithBootstrap(instance)
+
+ // take ownership of these secrets since we created them
+ if err := controllerutil.SetControllerReference(instance, authSecret, reader.Scheme()); err != nil {
+ return nil, err
+ }
+ if err := controllerutil.SetControllerReference(instance, bootstrapSecret, reader.Scheme()); err != nil {
+ return nil, err
+ }
+ err = reader.Create(ctx, authSecret)
+ if err != nil {
+ return nil, err
+ }
+ err = reader.Create(ctx, bootstrapSecret)
+ if err != nil {
+ return nil, err
+ }
+
+ // supply the bootstrap security.json to the initContainer via a simple BASE64 encoding env var
+ security.SecurityJson = string(bootstrapSecret.Data[SecurityJsonFile])
+ basicAuthSecret = authSecret
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ security.BasicAuthSecret = basicAuthSecret
+
+ if security.SecurityJson == "" {
+ // the bootstrap secret already exists, so just stash the security.json needed for constructing initContainers
+ bootstrapSecret := &corev1.Secret{}
+ err = reader.Get(ctx, types.NamespacedName{Name: instance.SecurityBootstrapSecretName(), Namespace: instance.Namespace}, bootstrapSecret)
+ if err != nil {
+ if !errors.IsNotFound(err) {
+ return nil, err
+ } // else perhaps the user deleted it after security was bootstrapped ... this is ok but may trigger a restart on the STS
+ } else {
+ // stash this so we can configure the setup-zk initContainer to bootstrap the security.json in ZK
+ security.SecurityJson = string(bootstrapSecret.Data[SecurityJsonFile])
+ }
+ }
+ }
+
+ return security, nil
+}
+
+func enableSecureProbesOnSolrCloudStatefulSet(solrCloud *solr.SolrCloud, stateful *appsv1.StatefulSet) {
+ mainContainer := &stateful.Spec.Template.Spec.Containers[0]
+
+ // if probes require auth or Solr wants client auth (mTLS), need to invoke a command on the Solr pod for the probes
+ mountPath := ""
+ if solrCloud.Spec.SolrSecurity != nil && solrCloud.Spec.SolrSecurity.ProbesRequireAuth {
+ vol, volMount := secureProbeVolumeAndMount(solrCloud.BasicAuthSecretName())
+ if vol != nil {
+ stateful.Spec.Template.Spec.Volumes = append(stateful.Spec.Template.Spec.Volumes, *vol)
+ }
+ if volMount != nil {
+ mainContainer.VolumeMounts = append(mainContainer.VolumeMounts, *volMount)
+ mountPath = volMount.MountPath
+ }
+ }
+
+ // update the probes if they are using HTTPGet to use an Exec to call Solr with TLS and/or Basic Auth creds
+ if mainContainer.LivenessProbe.HTTPGet != nil {
+ useSecureProbe(solrCloud, mainContainer.LivenessProbe, mountPath)
+ }
+ if mainContainer.ReadinessProbe.HTTPGet != nil {
+ useSecureProbe(solrCloud, mainContainer.ReadinessProbe, mountPath)
+ }
+ if mainContainer.StartupProbe != nil && mainContainer.StartupProbe.HTTPGet != nil {
+ useSecureProbe(solrCloud, mainContainer.StartupProbe, mountPath)
+ }
+}
+
+func cmdToPutSecurityJsonInZk() string {
+ scriptsDir := "/opt/solr/server/scripts/cloud-scripts"
+ cmd := " ZK_SECURITY_JSON=$(%s/zkcli.sh -zkhost ${ZK_HOST} -cmd get /security.json); "
+ cmd += "if [ ${#ZK_SECURITY_JSON} -lt 3 ]; then echo $SECURITY_JSON > /tmp/security.json; %s/zkcli.sh -zkhost ${ZK_HOST} -cmd putfile /security.json /tmp/security.json; echo \"put security.json in ZK\"; fi"
+ return fmt.Sprintf(cmd, scriptsDir, scriptsDir)
+}
+
+func (security *SecurityConfig) AuthHeader() map[string]string {
+ if security.BasicAuthSecret != nil {
+ return map[string]string{"Authorization": BasicAuthHeader(security.BasicAuthSecret)}
+ }
+ return nil
+}
+
+func BasicAuthEnvVars(secretName string) []corev1.EnvVar {
+ lor := corev1.LocalObjectReference{Name: secretName}
+ usernameRef := &corev1.SecretKeySelector{LocalObjectReference: lor, Key: corev1.BasicAuthUsernameKey}
+ passwordRef := &corev1.SecretKeySelector{LocalObjectReference: lor, Key: corev1.BasicAuthPasswordKey}
+ return []corev1.EnvVar{
+ {Name: "BASIC_AUTH_USER", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: usernameRef}},
+ {Name: "BASIC_AUTH_PASS", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: passwordRef}},
+ }
+}
+
+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
+ }
+
+ probeAuthz := ""
+ for i, p := range getProbePaths(solrCloud) {
+ 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-zk", "role":"k8s", "collection": null, "path":"/admin/zookeeper/status" },
+ { "name": "k8s-ping", "role":"k8s", "collection": "*", "path":"/admin/ping" },
+ { "name": "read", "role":["admin","users"] },
+ { "name": "update", "role":["admin"] },
+ { "name": "security-read", "role": ["admin"] },
+ { "name": "security-edit", "role": ["admin"] },
+ { "name": "all", "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 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))
+}
+
+// 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 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
+}
+
+func secureProbeVolumeAndMount(secretName string) (*corev1.Volume, *corev1.VolumeMount) {
+ vol := &corev1.Volume{
+ Name: strings.ReplaceAll(secretName, ".", "-"),
+ VolumeSource: corev1.VolumeSource{
+ Secret: &corev1.SecretVolumeSource{
+ SecretName: secretName,
+ DefaultMode: &SecretReadOnlyPermissions,
+ },
+ },
+ }
+ volMount := &corev1.VolumeMount{Name: vol.Name, MountPath: fmt.Sprintf("/etc/secrets/%s", vol.Name)}
+ return vol, volMount
+}
+
+// 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 useSecureProbe(solrCloud *solr.SolrCloud, probe *corev1.Probe, mountPath string) {
+ // 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 := ""
+ if solrCloud.Spec.SolrSecurity != nil && solrCloud.Spec.SolrSecurity.ProbesRequireAuth {
+ 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
+ tlsJavaToolOpts, tlsJavaSysProps := secureProbeTLSJavaToolOpts(solrCloud)
+ javaToolOptions := strings.TrimSpace(basicAuthOption + " " + tlsJavaToolOpts)
+
+ // 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 %s %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, tlsJavaSysProps, enableBasicAuth, solrCloud.UrlScheme(false), probe.HTTPGet.Port.IntVal, probe.HTTPGet.Path)
+ probeCommand = regexp.MustCompile(`\s+`).ReplaceAllString(strings.TrimSpace(probeCommand), " ")
+
+ // use an Exec instead of an HTTP GET
+ probe.HTTPGet = nil
+ probe.Exec = &corev1.ExecAction{Command: []string{"sh", "-c", probeCommand}}
+
+ // minimum of 5 seconds for exec probes as they are slow to initialize
+ if probe.TimeoutSeconds < 5 {
+ probe.TimeoutSeconds = 5
+ }
+}
diff --git a/controllers/util/solr_tls_util.go b/controllers/util/solr_tls_util.go
index a87993c..14d9251 100644
--- a/controllers/util/solr_tls_util.go
+++ b/controllers/util/solr_tls_util.go
@@ -722,8 +722,7 @@ func mountedTLSPath(dir *solr.MountedTLSDirectory, fileName string, defaultName
// Command to set the urlScheme cluster prop to "https"
func setUrlSchemeClusterPropCmd() string {
- return "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" +
+ return "/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;"
}
diff --git a/controllers/util/solr_util.go b/controllers/util/solr_util.go
index b73670c..e2d9c8a 100644
--- a/controllers/util/solr_util.go
+++ b/controllers/util/solr_util.go
@@ -18,23 +18,16 @@
package util
import (
- "crypto/sha256"
- b64 "encoding/base64"
- "encoding/json"
"fmt"
- "math/rand"
- "regexp"
- "sort"
- "strconv"
- "strings"
- "time"
-
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/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
+ "sort"
+ "strconv"
+ "strings"
)
const (
@@ -56,9 +49,6 @@ const (
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
)
@@ -68,7 +58,7 @@ const (
// 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, tls *TLSCerts) *appsv1.StatefulSet {
+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)
solrPodPort := solrCloud.Spec.SolrAddressability.PodPort
fsGroup := int64(DefaultSolrGroup)
@@ -357,19 +347,6 @@ func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCl
}
}
- if (tls != nil && tls.ServerConfig != nil && tls.ServerConfig.Options.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}}}
- defaultProbeTimeout = 5
- }
-
// 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] != "" {
@@ -389,7 +366,7 @@ func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCl
Value: strings.Join(allSolrOpts, " "),
})
- initContainers := generateSolrSetupInitContainers(solrCloud, solrCloudStatus, solrDataVolumeName, reconcileConfigInfo)
+ initContainers := generateSolrSetupInitContainers(solrCloud, solrCloudStatus, solrDataVolumeName, security)
// Add user defined additional init containers
if customPodOptions != nil && len(customPodOptions.InitContainers) > 0 {
@@ -563,10 +540,15 @@ func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCl
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)
+ }
+
return stateful
}
-func generateSolrSetupInitContainers(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCloudStatus, solrDataVolumeName string, reconcileConfigInfo map[string]string) (containers []corev1.Container) {
+func generateSolrSetupInitContainers(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCloudStatus, solrDataVolumeName string, security *SecurityConfig) (containers []corev1.Container) {
// The setup of the solr.xml will always be necessary
volumeMounts := []corev1.VolumeMount{
{
@@ -605,7 +587,7 @@ func generateSolrSetupInitContainers(solrCloud *solr.SolrCloud, solrCloudStatus
containers = append(containers, volumePrepInitContainer)
- if hasZKSetupContainer, zkSetupContainer := generateZKInteractionInitContainer(solrCloud, solrCloudStatus, reconcileConfigInfo); hasZKSetupContainer {
+ if hasZKSetupContainer, zkSetupContainer := generateZKInteractionInitContainer(solrCloud, solrCloudStatus, security); hasZKSetupContainer {
containers = append(containers, zkSetupContainer)
}
@@ -987,11 +969,11 @@ func CreateNodeIngressRule(solrCloud *solr.SolrCloud, nodeName string, domainNam
}
// 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) {
+func generateZKInteractionInitContainer(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCloudStatus, security *SecurityConfig) (bool, corev1.Container) {
allSolrOpts := make([]string, 0)
// Add all necessary ZK Info
- envVars, zkSolrOpt, _ := createZkConnectionEnvVars(solrCloud, solrCloudStatus)
+ envVars, zkSolrOpt, hasChroot := createZkConnectionEnvVars(solrCloud, solrCloudStatus)
if zkSolrOpt != "" {
allSolrOpts = append(allSolrOpts, zkSolrOpt)
}
@@ -1010,21 +992,21 @@ func generateZKInteractionInitContainer(solrCloud *solr.SolrCloud, solrCloudStat
cmd := ""
+ if hasChroot {
+ cmd += "solr zk ls ${ZK_CHROOT} -z ${ZK_SERVER} || solr zk mkroot ${ZK_CHROOT} -z ${ZK_SERVER}; "
+ }
+
if solrCloud.Spec.SolrTLS != nil {
- cmd = setUrlSchemeClusterPropCmd()
+ cmd += setUrlSchemeClusterPropCmd()
}
- if reconcileConfigInfo[SecurityJsonFile] != "" {
+ if security != nil && security.SecurityJson != "" {
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"
+ cmd += cmdToPutSecurityJsonInZk()
}
if cmd != "" {
@@ -1105,140 +1087,6 @@ func setupVolumeMountForUserProvidedConfigMapEntry(reconcileConfigInfo map[strin
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-zk", "role":"k8s", "collection": null, "path":"/admin/zookeeper/status" },
- { "name": "k8s-ping", "role":"k8s", "collection": "*", "path":"/admin/ping" },
- { "name": "read", "role":["admin","users"] },
- { "name": "update", "role":["admin"] },
- { "name": "security-read", "role": ["admin"] },
- { "name": "security-edit", "role": ["admin"] },
- { "name": "all", "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{}
@@ -1262,102 +1110,3 @@ func GetCustomProbePaths(solrCloud *solr.SolrCloud) []string {
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 && solrCloud.Spec.SolrSecurity.ProbesRequireAuth {
- secretName := solrCloud.BasicAuthSecretName()
- vol = &corev1.Volume{
- Name: strings.ReplaceAll(secretName, ".", "-"),
- VolumeSource: corev1.VolumeSource{
- Secret: &corev1.SecretVolumeSource{
- SecretName: secretName,
- DefaultMode: &SecretReadOnlyPermissions,
- },
- },
- }
- 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
- tlsJavaToolOpts, tlsJavaSysProps := secureProbeTLSJavaToolOpts(solrCloud)
- javaToolOptions := strings.TrimSpace(basicAuthOption + " " + tlsJavaToolOpts)
-
- // 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 %s %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, tlsJavaSysProps, enableBasicAuth, solrCloud.UrlScheme(false), defaultProbeGetAction.Port.IntVal, defaultProbeGetAction.Path)
- probeCommand = regexp.MustCompile(`\s+`).ReplaceAllString(strings.TrimSpace(probeCommand), " ")
-
- return probeCommand, vol, volMount
-}