/*
 * 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/md5"
	"fmt"
	"strconv"
	"strings"

	solr "github.com/apache/solr-operator/api/v1beta1"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	SolrTlsCertMd5Annotation       = "solr.apache.org/tlsCertMd5"
	SolrClientTlsCertMd5Annotation = "solr.apache.org/tlsClientCertMd5"
	DefaultKeyStorePath            = "/var/solr/tls"
	DefaultClientKeyStorePath      = "/var/solr/client-tls"
	DefaultWritableKeyStorePath    = "/var/solr/tls/pkcs12"
	TLSCertKey                     = "tls.crt"
	DefaultTrustStorePath          = "/var/solr/tls-truststore"
	DefaultClientTrustStorePath    = "/var/solr/client-tls-truststore"
	InitdbInitContainer            = "generate-init-db"
	InitdbPath                     = "/docker-entrypoint-initdb.d"
	DefaultPkcs12KeystoreFile      = "keystore.p12"
	DefaultPkcs12TruststoreFile    = "truststore.p12"
	DefaultKeystorePasswordFile    = "keystore-password"
)

// Helper struct for holding server and/or client cert config
// This struct is intended for internal use only and is only exposed outside the package so that the controllers can access
type TLSCerts struct {
	// Server cert config
	ServerConfig *TLSConfig
	// Client cert config
	ClientConfig *TLSConfig
	// Image used for initContainers that help configure the TLS settings
	InitContainerImage *solr.ContainerImage
}

// Holds TLS options from the user config as well as other config properties determined during reconciliation
// This struct is intended for internal use only and is only exposed outside the package so that the controllers can access
type TLSConfig struct {
	// TLS options provided by the user in the CRD definition
	Options *solr.SolrTLSOptions
	// Flag to indicate if we need to convert the provided keystore into the p12 format needed by Java
	NeedsPkcs12InitContainer bool
	// The MD5 hash of the cert, used for restarting Solr pods after the cert updates if so desired
	CertMd5 string
	// The annotation varies based on the cert type (client or server)
	CertMd5Annotation string
	// The paths vary based on whether this config is for a client or server cert
	KeystorePath   string
	TruststorePath string
	VolumePrefix   string
	Namespace      string
}

// Get a TLSCerts struct for reconciling TLS on a SolrCloud
func TLSCertsForSolrCloud(instance *solr.SolrCloud) *TLSCerts {
	tls := &TLSCerts{
		ServerConfig: &TLSConfig{
			Options:           instance.Spec.SolrTLS.DeepCopy(),
			KeystorePath:      DefaultKeyStorePath,
			TruststorePath:    DefaultTrustStorePath,
			CertMd5Annotation: SolrTlsCertMd5Annotation,
			Namespace:         instance.Namespace,
		},
		InitContainerImage: instance.Spec.BusyBoxImage,
	}
	if instance.Spec.SolrClientTLS != nil {
		tls.ClientConfig = &TLSConfig{
			Options:           instance.Spec.SolrClientTLS.DeepCopy(),
			KeystorePath:      DefaultClientKeyStorePath,
			TruststorePath:    DefaultClientTrustStorePath,
			VolumePrefix:      "client-",
			CertMd5Annotation: SolrClientTlsCertMd5Annotation,
			Namespace:         instance.Namespace,
		}
	}
	return tls
}

// Get a TLSCerts struct for reconciling TLS on an Exporter
func TLSCertsForExporter(prometheusExporter *solr.SolrPrometheusExporter) *TLSCerts {
	// when using mounted dir option, we need a busy box image for our initContainers
	bbImage := prometheusExporter.Spec.BusyBoxImage
	if bbImage == nil {
		bbImage = &solr.ContainerImage{
			Repository: solr.DefaultBusyBoxImageRepo,
			Tag:        solr.DefaultBusyBoxImageVersion,
			PullPolicy: solr.DefaultPullPolicy,
		}
	}
	return &TLSCerts{
		ClientConfig: &TLSConfig{
			Options:           prometheusExporter.Spec.SolrReference.SolrTLS.DeepCopy(),
			KeystorePath:      DefaultKeyStorePath,
			TruststorePath:    DefaultTrustStorePath,
			CertMd5Annotation: SolrClientTlsCertMd5Annotation,
			Namespace:         prometheusExporter.Namespace,
		},
		InitContainerImage: bbImage,
	}
}

// Enrich the config for a SolrCloud StatefulSet to enable TLS, either loaded from a secret or
// a directory on the main pod containing per-pod specific TLS files. In the latter case, the "mounted dir"
// typically comes from an external agent (such as a cert-manager extension) or CSI driver that injects the
// pod-specific TLS files using mutating web hooks
func (tls *TLSCerts) enableTLSOnSolrCloudStatefulSet(stateful *appsv1.StatefulSet) {
	serverCert := tls.ServerConfig

	// Add the SOLR_SSL_* vars to the main container's environment
	mainContainer := &stateful.Spec.Template.Spec.Containers[0]
	mainContainer.Env = append(mainContainer.Env, serverCert.serverEnvVars()...)
	// Was a client cert mounted too? If so, add the client env vars to the main container as well
	if tls.ClientConfig != nil {
		mainContainer.Env = append(mainContainer.Env, tls.ClientConfig.clientEnvVars()...)
	}

	if serverCert.Options.PKCS12Secret != nil {
		// Cert comes from a secret, so setup the pod template to mount the secret
		serverCert.mountTLSSecretOnPodTemplate(&stateful.Spec.Template)

		// mount the client certificate from a different secret (at different mount points)
		if tls.ClientConfig != nil && tls.ClientConfig.Options.PKCS12Secret != nil {
			tls.ClientConfig.mountTLSSecretOnPodTemplate(&stateful.Spec.Template)
		}
	} else if tls.hasPasswordsInFiles() {
		// the TLS files come from some auto-mounted directory on the main container
		mountInitDbIfNeeded(stateful)
		// use an initContainer to create the wrapper script in the initdb
		stateful.Spec.Template.Spec.InitContainers = append(stateful.Spec.Template.Spec.InitContainers, tls.generateTLSInitdbScriptInitContainer())
	}
}

// Enrich the config for a Prometheus Exporter Deployment to allow the exporter to make requests to TLS enabled Solr pods
func (tls *TLSCerts) enableTLSOnExporterDeployment(deployment *appsv1.Deployment) {
	clientCert := tls.ClientConfig

	// Add the SOLR_SSL_* vars to the main container's environment
	mainContainer := &deployment.Spec.Template.Spec.Containers[0]
	mainContainer.Env = append(mainContainer.Env, clientCert.clientEnvVars()...)
	mainContainer.Env = append(mainContainer.Env, corev1.EnvVar{Name: "SOLR_SSL_CHECK_PEER_NAME", Value: strconv.FormatBool(clientCert.Options.CheckPeerName)})

	// the exporter process doesn't read the SOLR_SSL_* env vars, so we need to pass them via JAVA_OPTS
	appendJavaOptsToEnv(mainContainer, clientCert.clientJavaOpts())

	if clientCert.Options.PKCS12Secret != nil || clientCert.Options.TrustStoreSecret != nil {
		// Cert comes from a secret, so setup the pod template to mount the secret
		clientCert.mountTLSSecretOnPodTemplate(&deployment.Spec.Template)
	} else if clientCert.Options.MountedTLSDir != nil && clientCert.hasPasswordsInFiles() {
		// volumes and mounts for TLS when using the mounted dir option
		clientCert.mountTLSWrapperScriptAndInitContainer(deployment, tls.InitContainerImage)
	}
}

// Configures a pod template (either StatefulSet or Deployment) to mount the TLS files from a secret
func (tls *TLSConfig) mountTLSSecretOnPodTemplate(template *corev1.PodTemplateSpec) *corev1.Container {
	mainContainer := &template.Spec.Containers[0]

	// the TLS files are mounted from a secret, setup the volumes and mounts
	vols, mounts := tls.volumesAndMounts()

	// 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 tls.NeedsPkcs12InitContainer {
		vols = append(vols, corev1.Volume{Name: "pkcs12", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}})
		mounts = append(mounts, corev1.VolumeMount{Name: "pkcs12", ReadOnly: false, MountPath: DefaultWritableKeyStorePath})
		pkcs12InitContainer := tls.generatePkcs12InitContainer(mainContainer.Image, mainContainer.ImagePullPolicy, mounts)
		template.Spec.InitContainers = append(template.Spec.InitContainers, pkcs12InitContainer)
	}
	template.Spec.Volumes = append(template.Spec.Volumes, vols...)
	mainContainer.VolumeMounts = append(mainContainer.VolumeMounts, mounts...)

	// track the MD5 of the TLS cert (from secret) to trigger restarts if the cert changes
	if tls.Options.RestartOnTLSSecretUpdate && tls.CertMd5 != "" {
		if template.Annotations == nil {
			template.Annotations = make(map[string]string, 1)
		}
		template.Annotations[tls.CertMd5Annotation] = tls.CertMd5
	}

	return mainContainer
}

// Make sure the secret containing the keystore and corresponding password secret exist and have the expected keys
// Also, set up to watch for updates if desired
// Also, verifies the configured truststore if provided
func (tls *TLSConfig) VerifyKeystoreAndTruststoreSecretConfig(client *client.Client) (*corev1.Secret, error) {
	opts := tls.Options
	foundTLSSecret, err := verifyTLSSecretConfig(client, opts.PKCS12Secret.Name, tls.Namespace, opts.KeyStorePasswordSecret)
	if err != nil {
		return nil, err
	} else {
		if opts.RestartOnTLSSecretUpdate {
			err = tls.saveCertMd5(foundTLSSecret)
			if err != nil {
				return nil, err
			}
		}

		if _, ok := foundTLSSecret.Data[opts.PKCS12Secret.Key]; !ok {
			// the keystore.p12 key is not in the TLS secret, indicating we need to create it using an initContainer
			tls.NeedsPkcs12InitContainer = true
		}
	}

	// verify the truststore config is valid too
	if opts.TrustStoreSecret != nil {
		// verify the TrustStore secret is configured correctly
		passwordSecret := opts.TrustStorePasswordSecret
		if passwordSecret == nil {
			passwordSecret = opts.KeyStorePasswordSecret
		}
		_, err := verifyTLSSecretConfig(client, opts.TrustStoreSecret.Name, tls.Namespace, passwordSecret)
		if err != nil {
			return nil, err
		}
	} else {
		// does the supplied keystore secret also contain the truststore?
		if _, ok := foundTLSSecret.Data[DefaultPkcs12TruststoreFile]; ok {
			// there's a truststore in the supplied TLS secret
			opts.TrustStoreSecret =
				&corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: foundTLSSecret.Name}, Key: DefaultPkcs12TruststoreFile}
			if opts.TrustStorePasswordSecret == nil {
				opts.TrustStorePasswordSecret = opts.KeyStorePasswordSecret
			}
		}
	}

	return foundTLSSecret, nil
}

// Special case where the user only configured a truststore for the exporter (no keystore)
func (tls *TLSConfig) VerifyTruststoreOnly(client *client.Client) error {
	secret := tls.Options.TrustStoreSecret
	truststoreSecret, err := verifyTLSSecretConfig(client, secret.Name, tls.Namespace, tls.Options.TrustStorePasswordSecret)
	if err != nil {
		return err
	}

	// make sure truststore.p12 is actually in the supplied secret
	if _, ok := truststoreSecret.Data[secret.Key]; !ok {
		return fmt.Errorf("%s key not found in truststore password secret %s", secret.Key, secret.Name)
	}

	// If we have a watch on secrets, then get notified when the secret changes (such as after cert renewal)
	// capture the hash of the truststore and stash in an annotation so that pods get restarted if the cert changes
	// If watch = false, then we may be watching the keystore instead
	if tls.Options.RestartOnTLSSecretUpdate {
		tls.CertMd5 = fmt.Sprintf("%x", md5.Sum(truststoreSecret.Data[secret.Key]))
	}

	return nil
}

func (tls *TLSConfig) saveCertMd5(tlsSecret *corev1.Secret) error {
	// We have a watch on secrets, so will get notified when the secret changes (such as after cert renewal)
	// capture the hash of the secret and stash in an annotation so that pods get restarted if the cert changes
	if tlsCertBytes, ok := tlsSecret.Data[TLSCertKey]; ok {
		tls.CertMd5 = fmt.Sprintf("%x", md5.Sum(tlsCertBytes))
		return nil
	}

	return fmt.Errorf("%s key not found in TLS secret %s, cannot watch for updates to the cert without this data but 'restartOnTLSSecretUpdate' is enabled", TLSCertKey, tlsSecret.Name)
}

func (tls *TLSConfig) volumeName(baseName string) string {
	return tls.VolumePrefix + baseName
}

// Get a list of volumes for the keystore and optionally a truststore loaded from a TLS secret
func (tls *TLSConfig) volumesAndMounts() ([]corev1.Volume, []corev1.VolumeMount) {
	optional := false
	vols := []corev1.Volume{}
	mounts := []corev1.VolumeMount{}
	keystoreSecretName := ""

	opts := tls.Options
	if opts.PKCS12Secret != nil {
		keystoreSecretName = opts.PKCS12Secret.Name
		volName := tls.volumeName("keystore")
		vols = append(vols, corev1.Volume{
			Name: volName,
			VolumeSource: corev1.VolumeSource{
				Secret: &corev1.SecretVolumeSource{
					SecretName:  opts.PKCS12Secret.Name,
					DefaultMode: &SecretReadOnlyPermissions,
					Optional:    &optional,
				},
			},
		})
		mounts = append(mounts, corev1.VolumeMount{Name: volName, ReadOnly: true, MountPath: tls.KeystorePath})
	}

	// 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 != keystoreSecretName {
		volName := tls.volumeName("truststore")
		vols = append(vols, corev1.Volume{
			Name: volName,
			VolumeSource: corev1.VolumeSource{
				Secret: &corev1.SecretVolumeSource{
					SecretName:  opts.TrustStoreSecret.Name,
					DefaultMode: &SecretReadOnlyPermissions,
					Optional:    &optional,
				},
			},
		})
		mounts = append(mounts, corev1.VolumeMount{Name: volName, ReadOnly: true, MountPath: tls.TruststorePath})
	}

	return vols, mounts
}

// Determine whether any passwords for Keystores/Truststores are stored in files
func (tls *TLSCerts) hasPasswordsInFiles() (hasPasswordsInFiles bool) {
	return tls != nil && (tls.ServerConfig.hasPasswordsInFiles() || tls.ClientConfig.hasPasswordsInFiles())
}

// Determine whether any passwords for Keystores/Truststores are stored in files
func (tls *TLSConfig) hasPasswordsInFiles() (hasPasswordsInFiles bool) {
	if tls != nil && tls.Options.MountedTLSDir != nil {
		serverDir := tls.Options.MountedTLSDir
		hasPasswordsInFiles = serverDir.KeystorePasswordFile != "" || serverDir.KeystorePassword == ""

		if serverDir.TruststorePasswordFile != "" {
			hasPasswordsInFiles = true
		}
	}
	return
}

// Get the SOLR_SSL_* env vars for enabling TLS on Solr pods
func (tls *TLSConfig) serverEnvVars() []corev1.EnvVar {
	opts := tls.Options

	// 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"
	}

	envVars := []corev1.EnvVar{
		{
			Name:  "SOLR_SSL_ENABLED",
			Value: "true",
		},
		{
			Name:  "SOLR_SSL_WANT_CLIENT_AUTH",
			Value: wantClientAuth,
		},
		{
			Name:  "SOLR_SSL_NEED_CLIENT_AUTH",
			Value: needClientAuth,
		},
		{
			Name:  "SOLR_SSL_CHECK_PEER_NAME",
			Value: strconv.FormatBool(opts.CheckPeerName),
		},
	}

	// tricky ... bin/solr checks for null SOLR_SSL_CLIENT_HOSTNAME_VERIFICATION via -z to set -Dsolr.jetty.ssl.verifyClientHostName=HTTPS
	// so only add the SOLR_SSL_CLIENT_HOSTNAME_VERIFICATION env var if false
	if !opts.VerifyClientHostname {
		envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_CLIENT_HOSTNAME_VERIFICATION", Value: "false"})
	}

	// keystore / truststore come from either a mountedTLSDir or sourced from a secret mounted on the pod
	if opts.MountedTLSDir != nil {
		// TLS files are mounted by some external agent
		envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_KEY_STORE", Value: mountedTLSKeystorePath(opts.MountedTLSDir)})
		keyStorePassword := ""
		if opts.MountedTLSDir.KeystorePassword != "" && opts.MountedTLSDir.KeystorePasswordFile == "" {
			envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_KEY_STORE_PASSWORD", Value: opts.MountedTLSDir.KeystorePassword})
			keyStorePassword = opts.MountedTLSDir.KeystorePassword
		}
		if opts.MountedTLSDir.TruststoreFile != "" {
			envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_TRUST_STORE", Value: mountedTLSTruststorePath(opts.MountedTLSDir)})
			trustStorePassword := opts.MountedTLSDir.TruststorePassword
			if trustStorePassword == "" && keyStorePassword != "" {
				trustStorePassword = keyStorePassword
			}
			if trustStorePassword != "" && opts.MountedTLSDir.TruststorePasswordFile == "" {
				envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_TRUST_STORE_PASSWORD", Value: trustStorePassword})
			}
		} else {
			envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_TRUST_STORE", Value: mountedTLSKeystorePath(opts.MountedTLSDir)})
			if keyStorePassword != "" {
				envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_TRUST_STORE_PASSWORD", Value: keyStorePassword})
			}
		}
	} else {
		// keystore / truststore + passwords come from a secret
		envVars = append(envVars, tls.keystoreEnvVars("SOLR_SSL_KEY_STORE")...)
		envVars = append(envVars, tls.truststoreEnvVars("SOLR_SSL_TRUST_STORE")...)
	}

	return envVars
}

// Set the SOLR_SSL_* for a Solr client process, e.g. the Exporter, which only needs a subset of SSL vars that a Solr pod would need
func (tls *TLSConfig) clientEnvVars() []corev1.EnvVar {
	opts := tls.Options

	var envVars []corev1.EnvVar
	if opts.MountedTLSDir != nil {
		// passwords get exported from files in the TLS dir using an initdb wrapper script if they come from files
		keyStorePassword := ""
		if opts.MountedTLSDir.KeystoreFile != "" {
			envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_CLIENT_KEY_STORE", Value: mountedTLSKeystorePath(opts.MountedTLSDir)})
			if opts.MountedTLSDir.KeystorePassword != "" && opts.MountedTLSDir.KeystorePasswordFile == "" {
				envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_CLIENT_KEY_STORE_PASSWORD", Value: opts.MountedTLSDir.KeystorePassword})
				keyStorePassword = opts.MountedTLSDir.KeystorePassword
			}
		}
		if opts.MountedTLSDir.TruststoreFile != "" {
			envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_CLIENT_TRUST_STORE", Value: mountedTLSTruststorePath(opts.MountedTLSDir)})
			trustStorePassword := opts.MountedTLSDir.TruststorePassword
			if trustStorePassword == "" && keyStorePassword != "" {
				trustStorePassword = keyStorePassword
			}
			if trustStorePassword != "" && opts.MountedTLSDir.TruststorePasswordFile == "" {
				envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD", Value: trustStorePassword})
			}
		} else if opts.MountedTLSDir.KeystoreFile != "" {
			envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_CLIENT_TRUST_STORE", Value: "$(SOLR_SSL_CLIENT_KEY_STORE)"})
			envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD", Value: keyStorePassword})
		}
	}

	if opts.PKCS12Secret != nil {
		envVars = append(envVars, tls.keystoreEnvVars("SOLR_SSL_CLIENT_KEY_STORE")...)
		// if no additional truststore secret provided, just use the keystore for both
		if opts.TrustStoreSecret == nil {
			envVars = append(envVars, tls.keystoreEnvVars("SOLR_SSL_CLIENT_TRUST_STORE")...)
		}
	}

	if opts.TrustStoreSecret != nil {
		envVars = append(envVars, tls.truststoreEnvVars("SOLR_SSL_CLIENT_TRUST_STORE")...)
	}

	return envVars
}

func (tls *TLSConfig) keystoreEnvVars(varName string) []corev1.EnvVar {
	return []corev1.EnvVar{
		{
			Name:  varName,
			Value: tls.keystoreFile(),
		},
		{
			Name:      varName + "_PASSWORD",
			ValueFrom: &corev1.EnvVarSource{SecretKeyRef: tls.Options.KeyStorePasswordSecret},
		},
	}
}

// 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
func (tls *TLSConfig) keystoreFile() string {
	var keystorePath string
	if tls.NeedsPkcs12InitContainer {
		keystorePath = DefaultWritableKeyStorePath
	} else {
		keystorePath = tls.KeystorePath
	}
	return keystorePath + "/" + DefaultPkcs12KeystoreFile
}

func (tls *TLSConfig) truststoreEnvVars(varName string) []corev1.EnvVar {
	opts := tls.Options
	keystoreSecretName := ""
	if opts.PKCS12Secret != nil {
		keystoreSecretName = opts.PKCS12Secret.Name
	}

	var truststoreFile string
	if opts.TrustStoreSecret != nil {
		if opts.TrustStoreSecret.Name != keystoreSecretName {
			// trust store is in a different secret, so will be mounted in a different dir
			truststoreFile = tls.TruststorePath + "/" + opts.TrustStoreSecret.Key
		} else {
			// trust store is a different key in the same secret as the keystore
			truststoreFile = tls.KeystorePath + "/" + DefaultPkcs12TruststoreFile
		}
	} else {
		// truststore is the same as the keystore
		truststoreFile = tls.keystoreFile()
	}

	var truststorePassFrom *corev1.EnvVarSource
	if opts.TrustStorePasswordSecret != nil {
		truststorePassFrom = &corev1.EnvVarSource{SecretKeyRef: opts.TrustStorePasswordSecret}
	} else {
		truststorePassFrom = &corev1.EnvVarSource{SecretKeyRef: opts.KeyStorePasswordSecret}
	}

	return []corev1.EnvVar{
		{
			Name:  varName,
			Value: truststoreFile,
		},
		{
			Name:      varName + "_PASSWORD",
			ValueFrom: truststorePassFrom,
		},
	}
}

// For the mounted dir approach, we need to customize the Prometheus exporter's entry point with a wrapper script
// that reads the keystore / truststore passwords from a file and passes them via JAVA_OPTS
func (tls *TLSConfig) mountTLSWrapperScriptAndInitContainer(deployment *appsv1.Deployment, initContainerImage *solr.ContainerImage) {
	opts := tls.Options
	mainContainer := &deployment.Spec.Template.Spec.Containers[0]

	volName := "tls-wrapper-script"
	mountPath := "/usr/local/solr-exporter-tls"
	wrapperScript := mountPath + "/launch-exporter-with-tls.sh"

	// the Prom exporter needs the keystore & truststore passwords in a Java system property, but the password
	// is stored in a file when using the mounted TLS dir approach, so we use a wrapper script around the main
	// container entry point to add these properties to JAVA_OPTS at runtime
	vol, mount := createEmptyVolumeAndMount(volName, mountPath)
	deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, *vol)
	mainContainer.VolumeMounts = append(mainContainer.VolumeMounts, *mount)

	entrypoint := mainContainer.Command[0]

	// may not have a keystore for the client cert, but must have have a truststore in that case
	kspJavaSysProp := ""
	catKsp := ""
	if opts.MountedTLSDir.KeystoreFile != "" {
		if opts.MountedTLSDir.KeystorePasswordFile != "" || opts.MountedTLSDir.KeystorePassword == "" {
			catKsp = fmt.Sprintf("SOLR_SSL_CLIENT_KEY_STORE_PASSWORD=\\$(cat %s)", mountedTLSKeystorePasswordPath(opts.MountedTLSDir))
			kspJavaSysProp = " -Djavax.net.ssl.keyStorePassword=\\${SOLR_SSL_CLIENT_KEY_STORE_PASSWORD}"
		}
	}

	tspJavaSysProp := ""
	catTsp := ""
	if opts.MountedTLSDir.TruststorePasswordFile != "" || opts.MountedTLSDir.TruststorePassword == "" {
		catTsp = fmt.Sprintf("SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD=\\$(cat %s)", mountedTLSTruststorePasswordPath(opts.MountedTLSDir))
		tspJavaSysProp = " -Djavax.net.ssl.trustStorePassword=\\${SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD}"
	} else if catKsp != "" {
		tspJavaSysProp = " -Djavax.net.ssl.trustStorePassword=\\${SOLR_SSL_CLIENT_KEY_STORE_PASSWORD}"
	}

	/*
		  Create a wrapper script like:

			#!/bin/bash
			ksp=$(cat $MOUNTED_TLS_DIR/keystore-password)
			tsp=$(cat $MOUNTED_TLS_DIR/truststore-password)
			JAVA_OPTS="${JAVA_OPTS} -Djavax.net.ssl.keyStorePassword=${ksp} -Djavax.net.ssl.trustStorePassword=${tsp}"
			/opt/solr/contrib/prometheus-exporter/bin/solr-exporter $@

	*/
	writeWrapperScript := fmt.Sprintf("cat << EOF > %s\n#!/bin/bash\n%s\n%s\nJAVA_OPTS=\"\\${JAVA_OPTS}%s%s\"\n%s \\$@\nEOF\nchmod +x %s",
		wrapperScript, catKsp, catTsp, kspJavaSysProp, tspJavaSysProp, entrypoint, wrapperScript)

	createTLSWrapperScriptInitContainer := corev1.Container{
		Name:            "create-tls-wrapper-script",
		Image:           initContainerImage.ToImageName(),
		ImagePullPolicy: initContainerImage.PullPolicy,
		Command:         []string{"sh", "-c", writeWrapperScript},
		VolumeMounts:    []corev1.VolumeMount{{Name: volName, MountPath: mountPath}},
	}
	deployment.Spec.Template.Spec.InitContainers = append(deployment.Spec.Template.Spec.InitContainers, createTLSWrapperScriptInitContainer)

	// Call the wrapper script to start the exporter process
	mainContainer.Command = []string{wrapperScript}
}

// Create an initContainer that generates the initdb script that exports the keystore / truststore passwords stored in
// a directory to the environment; this is only needed when using the mountedTLSDir approach
func (tls *TLSCerts) generateTLSInitdbScriptInitContainer() corev1.Container {

	exportServerKeystorePassword, exportServerTruststorePassword := "", ""
	if tls.ServerConfig.Options.MountedTLSDir != nil {
		mountedDir := tls.ServerConfig.Options.MountedTLSDir
		if mountedDir.KeystorePasswordFile != "" || mountedDir.KeystorePassword == "" {
			exportServerKeystorePassword = exportVarFromFileInInitdbWrapperScript("SOLR_SSL_KEY_STORE_PASSWORD", mountedTLSKeystorePasswordPath(tls.ServerConfig.Options.MountedTLSDir))
			exportServerTruststorePassword = exportVarFromFileInInitdbWrapperScript("SOLR_SSL_TRUST_STORE_PASSWORD", "${SOLR_SSL_KEY_STORE_PASSWORD}")
		}
		if mountedDir.TruststorePasswordFile != "" {
			exportServerTruststorePassword = exportVarFromFileInInitdbWrapperScript("SOLR_SSL_TRUST_STORE_PASSWORD", mountedTLSTruststorePasswordPath(tls.ServerConfig.Options.MountedTLSDir))
		} else if mountedDir.TruststorePassword != "" {
			exportServerTruststorePassword = ""
		}
	}

	// Might have a client cert too ...
	exportClientKeystorePassword, exportClientTruststorePassword := "", ""
	if tls.ClientConfig != nil && tls.ClientConfig.Options.MountedTLSDir != nil {
		mountedDir := tls.ClientConfig.Options.MountedTLSDir
		if mountedDir.KeystorePasswordFile != "" || mountedDir.KeystorePassword == "" {
			exportClientKeystorePassword = exportVarFromFileInInitdbWrapperScript("SOLR_SSL_CLIENT_KEY_STORE_PASSWORD", mountedTLSKeystorePasswordPath(mountedDir))
			exportClientTruststorePassword = exportVarFromFileInInitdbWrapperScript("SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD", "${SOLR_SSL_CLIENT_KEY_STORE_PASSWORD}")
		}
		if mountedDir.TruststorePasswordFile != "" {
			exportClientTruststorePassword = exportVarFromFileInInitdbWrapperScript("SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD", mountedTLSTruststorePasswordPath(mountedDir))
		} else if mountedDir.TruststorePassword != "" {
			exportClientTruststorePassword = ""
		}
	} else {
		exportClientKeystorePassword = exportServerKeystorePassword
		exportClientKeystorePassword = exportServerTruststorePassword
	}

	shCmd := fmt.Sprintf("echo -e \"#!/bin/bash\\n%s%s%s%s\"",
		exportServerKeystorePassword, exportServerTruststorePassword, exportClientKeystorePassword, exportClientTruststorePassword)
	shCmd += " > /docker-entrypoint-initdb.d/export-tls-vars.sh"
	/*
	   Init container creates a script like:

	      #!/bin/bash

	      export SOLR_SSL_KEY_STORE_PASSWORD=`cat $MOUNTED_SERVER_TLS_DIR/keystore-password`
	      export SOLR_SSL_TRUST_STORE_PASSWORD=`cat $MOUNTED_SERVER_TLS_DIR/truststore-password`
	      export SOLR_SSL_CLIENT_KEY_STORE_PASSWORD=`cat $MOUNTED_CLIENT_TLS_DIR/keystore-password`
	      export SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD=`cat $MOUNTED_CLIENT_TLS_DIR/truststore-password`

	*/

	return corev1.Container{
		Name:            InitdbInitContainer,
		Image:           tls.InitContainerImage.ToImageName(),
		ImagePullPolicy: tls.InitContainerImage.PullPolicy,
		Command:         []string{"sh", "-c", shCmd},
		VolumeMounts:    []corev1.VolumeMount{{Name: "initdb", MountPath: InitdbPath}},
	}
}

// Helper function for writing a line to the initdb wrapper script that exports an env var sourced from a file
func exportVarFromFileInInitdbWrapperScript(varName string, varValue string) string {
	if strings.HasPrefix(varValue, "${") {
		// This is a pass-through variable
		return fmt.Sprintf("\\nexport %s=%q\\n", varName, varValue)
	} else {
		// This is a file to "cat"
		return fmt.Sprintf("\\nexport %s=\"$(cat %q)\"\\n", varName, varValue)
	}
}

// Returns an array of Java system properties to configure the TLS certificate used by client applications to call mTLS enabled Solr pods
func (tls *TLSConfig) clientJavaOpts() []string {

	// for clients, we should always have a truststore but the keystore is optional
	javaOpts := []string{
		"-Dsolr.ssl.checkPeerName=$(SOLR_SSL_CHECK_PEER_NAME)",
		"-Djavax.net.ssl.trustStore=$(SOLR_SSL_CLIENT_TRUST_STORE)",
		"-Djavax.net.ssl.trustStoreType=PKCS12",
	}

	if tls.Options.PKCS12Secret != nil || (tls.Options.MountedTLSDir != nil && tls.Options.MountedTLSDir.KeystoreFile != "") {
		javaOpts = append(javaOpts, "-Djavax.net.ssl.keyStore=$(SOLR_SSL_CLIENT_KEY_STORE)")
		javaOpts = append(javaOpts, "-Djavax.net.ssl.keyStoreType=PKCS12")
	}

	hasKeyStorePassword := false
	if tls.Options.PKCS12Secret != nil || (tls.Options.MountedTLSDir != nil && tls.Options.MountedTLSDir.KeystorePasswordFile == "" && tls.Options.MountedTLSDir.KeystorePassword != "") {
		hasKeyStorePassword = true
		javaOpts = append(javaOpts, "-Djavax.net.ssl.keyStorePassword=$(SOLR_SSL_CLIENT_KEY_STORE_PASSWORD)")
	} // else for mounted dir option, the password comes from the wrapper script

	if tls.Options.PKCS12Secret != nil || tls.Options.TrustStoreSecret != nil || (tls.Options.MountedTLSDir != nil && tls.Options.MountedTLSDir.TruststorePasswordFile == "" && (tls.Options.MountedTLSDir.TruststorePassword != "" || hasKeyStorePassword)) {
		javaOpts = append(javaOpts, "-Djavax.net.ssl.trustStorePassword=$(SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD)")
	} // else for mounted dir option, the password comes from the wrapper script

	return javaOpts
}

func (tls *TLSConfig) generatePkcs12InitContainer(imageName string, imagePullPolicy corev1.PullPolicy, mounts []corev1.VolumeMount) corev1.Container {
	// get the keystore password from the env for generating the keystore using openssl
	envVars := []corev1.EnvVar{
		{
			Name:      "SOLR_SSL_KEY_STORE_PASSWORD",
			ValueFrom: &corev1.EnvVarSource{SecretKeyRef: tls.Options.KeyStorePasswordSecret},
		},
	}

	caCrtFileName := DefaultKeyStorePath + "/ca.crt"

	cmd := "OPTIONAL_CACRT=\"$(test -e " + caCrtFileName + " && echo ' -certfile " + caCrtFileName + "')\"; " +
		"openssl pkcs12 -export -in " + DefaultKeyStorePath + "/" + TLSCertKey + " $OPTIONAL_CACRT " +
		"-inkey " + DefaultKeyStorePath + "/tls.key -out " + DefaultKeyStorePath +
		"/pkcs12/" + DefaultPkcs12KeystoreFile + " -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:             mounts,
		Env:                      envVars,
	}
}

func mountedTLSKeystorePath(tlsDir *solr.MountedTLSDirectory) string {
	return mountedTLSPath(tlsDir, tlsDir.KeystoreFile, DefaultPkcs12KeystoreFile)
}

func mountedTLSKeystorePasswordPath(tlsDir *solr.MountedTLSDirectory) string {
	return mountedTLSPath(tlsDir, tlsDir.KeystorePasswordFile, DefaultKeystorePasswordFile)
}

func mountedTLSTruststorePath(tlsDir *solr.MountedTLSDirectory) string {
	return mountedTLSPath(tlsDir, tlsDir.TruststoreFile, DefaultPkcs12TruststoreFile)
}

func mountedTLSTruststorePasswordPath(tlsDir *solr.MountedTLSDirectory) string {
	path := ""
	if tlsDir.TruststorePasswordFile != "" {
		path = mountedTLSPath(tlsDir, tlsDir.TruststorePasswordFile, "")
	} else if tlsDir.TruststorePassword == "" {
		path = mountedTLSKeystorePasswordPath(tlsDir)
	}
	return path
}

func mountedTLSPath(dir *solr.MountedTLSDirectory, fileName string, defaultName string) string {
	if fileName == "" {
		fileName = defaultName
	}
	return fmt.Sprintf("%s/%s", dir.Path, fileName)
}

// Command to set the urlScheme cluster prop to "https"
func setUrlSchemeClusterPropCmd() string {
	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;"
}

// Appends additional Java system properties to the JAVA_OPTS environment variable of the main container and ensures JAVA_OPTS is the last env var
func appendJavaOptsToEnv(mainContainer *corev1.Container, additionalJavaOpts []string) {
	javaOptsValue := ""
	javaOptsAt := -1
	for i, v := range mainContainer.Env {
		if v.Name == "JAVA_OPTS" {
			javaOptsValue = v.Value
			javaOptsAt = i
			break
		}
	}
	javaOptsVar := corev1.EnvVar{Name: "JAVA_OPTS", Value: strings.TrimSpace(javaOptsValue + " " + strings.Join(additionalJavaOpts, " "))}
	if javaOptsAt == -1 {
		// no JAVA_OPTS, add it on the end
		mainContainer.Env = append(mainContainer.Env, javaOptsVar)
	} else {
		// need to move the JAVA_OPTS var to end of array, slice it out ...
		envVars := mainContainer.Env[0:javaOptsAt]
		if javaOptsAt < len(mainContainer.Env)-1 {
			envVars = append(envVars, mainContainer.Env[javaOptsAt+1:]...)
		}
		mainContainer.Env = append(envVars, javaOptsVar)
	}
}

// Utility func to create an empty dir and corresponding mount
func createEmptyVolumeAndMount(name string, mountPath string) (*corev1.Volume, *corev1.VolumeMount) {
	return &corev1.Volume{Name: name, VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}},
		&corev1.VolumeMount{Name: name, MountPath: mountPath}
}

// The Docker Solr framework allows us to run scripts from an initdb directory before the main Solr process is started
// Mount the initdb directory if it has not already been mounted by the user via custom pod options
func mountInitDbIfNeeded(stateful *appsv1.StatefulSet) {
	// Auto-TLS uses an initContainer to create a script in the initdb, so mount that if it has not already been mounted
	mainContainer := &stateful.Spec.Template.Spec.Containers[0]
	var initdbMount *corev1.VolumeMount
	for _, mount := range mainContainer.VolumeMounts {
		if mount.MountPath == InitdbPath {
			initdbMount = &mount
			break
		}
	}
	if initdbMount == nil {
		vol, mount := createEmptyVolumeAndMount("initdb", InitdbPath)
		stateful.Spec.Template.Spec.Volumes = append(stateful.Spec.Template.Spec.Volumes, *vol)
		mainContainer.VolumeMounts = append(mainContainer.VolumeMounts, *mount)
	}
}

// Utility method used during reconcile to verify a TLS secret exists and has the correct key
// Also verifies the corresponding password secret exists and has the expected key
// Used for verifying keystore and truststore configuration
func verifyTLSSecretConfig(client *client.Client, secretName string, secretNamespace string, passwordSecret *corev1.SecretKeySelector) (*corev1.Secret, error) {
	ctx := context.TODO()
	reader := *client

	foundTLSSecret := &corev1.Secret{}
	lookupErr := reader.Get(ctx, types.NamespacedName{Name: secretName, Namespace: secretNamespace}, foundTLSSecret)
	if lookupErr != nil {
		return nil, lookupErr
	} else {
		if passwordSecret == nil {
			return nil, fmt.Errorf("no password secret configured for %s", secretName)
		}

		// Make sure the secret containing the keystore password exists as well
		keyStorePasswordSecret := &corev1.Secret{}
		err := reader.Get(ctx, types.NamespacedName{Name: passwordSecret.Name, Namespace: foundTLSSecret.Namespace}, keyStorePasswordSecret)
		if err != nil {
			return nil, err
		}

		// we found the keystore secret, but does it have the key we expect?
		if _, ok := keyStorePasswordSecret.Data[passwordSecret.Key]; !ok {
			return nil, fmt.Errorf("%s key not found in password secret %s", passwordSecret.Key, keyStorePasswordSecret.Name)
		}
	}

	return foundTLSSecret, nil
}
