Add integration tests for TLS Solr (#611)

- Fixed small bug.
- Add tests for Secret TLS & CSI Driver TLS.
- Multiple configurations tested, including verifyPeerName, wantAuth, needAuth, etc.

For now, tests will only work with 8.11
diff --git a/controllers/solrcloud_controller_tls_test.go b/controllers/solrcloud_controller_tls_test.go
index 34cbd5c..fe55021 100644
--- a/controllers/solrcloud_controller_tls_test.go
+++ b/controllers/solrcloud_controller_tls_test.go
@@ -827,7 +827,7 @@
 			"-Djavax.net.ssl.trustStorePassword=$(cat " + expectedTruststorePasswordFile + ")"
 		tlsJavaSysProps = "-Djavax.net.ssl.trustStore=$SOLR_SSL_CLIENT_TRUST_STORE -Djavax.net.ssl.keyStore=$SOLR_SSL_CLIENT_KEY_STORE"
 	} else {
-		expectedKeystorePassword := solrCloud.Spec.SolrTLS.MountedTLSDir.KeystorePassword
+		expectedKeystorePassword := "${SOLR_SSL_KEY_STORE_PASSWORD}"
 		if solrCloud.Spec.SolrTLS.MountedTLSDir.KeystorePasswordFile != "" {
 			expectedKeystorePassword = "$(cat " + solrCloud.Spec.SolrTLS.MountedTLSDir.Path + "/" + solrCloud.Spec.SolrTLS.MountedTLSDir.KeystorePasswordFile + ")"
 		}
@@ -835,7 +835,7 @@
 		if solrCloud.Spec.SolrTLS.MountedTLSDir.TruststorePasswordFile != "" {
 			expectedTruststorePassword = "$(cat " + solrCloud.Spec.SolrTLS.MountedTLSDir.Path + "/" + solrCloud.Spec.SolrTLS.MountedTLSDir.TruststorePasswordFile + ")"
 		} else if solrCloud.Spec.SolrTLS.MountedTLSDir.TruststorePassword != "" {
-			expectedTruststorePassword = solrCloud.Spec.SolrTLS.MountedTLSDir.TruststorePassword
+			expectedTruststorePassword = "${SOLR_SSL_TRUST_STORE_PASSWORD}"
 		}
 
 		tlsJavaToolOpts = "-Djavax.net.ssl.keyStorePassword=" + expectedKeystorePassword + " " +
diff --git a/controllers/util/solr_tls_util.go b/controllers/util/solr_tls_util.go
index 1866581..7316cb5 100644
--- a/controllers/util/solr_tls_util.go
+++ b/controllers/util/solr_tls_util.go
@@ -717,21 +717,23 @@
 	if solrCloud.Spec.SolrTLS != nil {
 		// prefer the mounted client cert for probes if provided
 		tlsDir := solrCloud.Spec.SolrTLS.MountedTLSDir
+		clientPrefix := ""
 		if solrCloud.Spec.SolrClientTLS != nil && solrCloud.Spec.SolrClientTLS.MountedTLSDir != nil {
 			tlsDir = solrCloud.Spec.SolrClientTLS.MountedTLSDir
+			clientPrefix = "CLIENT_"
 		}
 		if tlsDir != nil {
 			// The keystore passwords are in a file, then we need to cat the file(s) into JAVA_TOOL_OPTIONS
 			keyStorePassword := "$(cat " + mountedTLSKeystorePasswordPath(tlsDir) + ")"
 			if tlsDir.KeystorePasswordFile == "" && tlsDir.KeystorePassword != "" {
-				keyStorePassword = "${SOLR_SSL_CLIENT_KEY_STORE_PASSWORD}"
+				keyStorePassword = "${SOLR_SSL_" + clientPrefix + "KEY_STORE_PASSWORD}"
 			}
 			tlsJavaToolOpts += " -Djavax.net.ssl.keyStorePassword=" + keyStorePassword
 			trustStorePassword := keyStorePassword
 			if tlsDir.TruststorePasswordFile != "" {
 				trustStorePassword = "$(cat " + mountedTLSTruststorePasswordPath(tlsDir) + ")"
 			} else if tlsDir.TruststorePassword != "" {
-				trustStorePassword = tlsDir.TruststorePassword
+				trustStorePassword = "${SOLR_SSL_" + clientPrefix + "TRUST_STORE_PASSWORD}"
 			}
 			tlsJavaToolOpts += " -Djavax.net.ssl.trustStorePassword=" + trustStorePassword
 		}
diff --git a/go.mod b/go.mod
index 6193a4c..b7d8c67 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@
 go 1.20
 
 require (
+	github.com/cert-manager/cert-manager v1.12.4
 	github.com/fsnotify/fsnotify v1.6.0
 	github.com/go-logr/logr v1.2.4
 	github.com/onsi/ginkgo/v2 v2.12.0
@@ -115,7 +116,6 @@
 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 	github.com/xeipuuv/gojsonschema v1.2.0 // indirect
 	github.com/xlab/treeprint v1.1.0 // indirect
-	go.opencensus.io v0.24.0 // indirect
 	go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
 	go.uber.org/multierr v1.8.0 // indirect
@@ -140,9 +140,11 @@
 	k8s.io/cli-runtime v0.26.0 // indirect
 	k8s.io/component-base v0.27.2 // indirect
 	k8s.io/klog/v2 v2.100.1 // indirect
+	k8s.io/kube-aggregator v0.27.2 // indirect
 	k8s.io/kube-openapi v0.0.0-20230515203736-54b630e78af5 // indirect
 	k8s.io/kubectl v0.26.0 // indirect
 	oras.land/oras-go v1.2.2 // indirect
+	sigs.k8s.io/gateway-api v0.7.0 // indirect
 	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
 	sigs.k8s.io/kustomize/api v0.12.1 // indirect
 	sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect
diff --git a/go.sum b/go.sum
index adc3c52..2de97cc 100644
--- a/go.sum
+++ b/go.sum
@@ -84,6 +84,8 @@
 github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ=
 github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cert-manager/cert-manager v1.12.4 h1:HI38vtBYTG8b2JHDF65+Dbbd09kZps6bglIAlijoj1g=
+github.com/cert-manager/cert-manager v1.12.4/go.mod h1:/RYHUvK9cxuU5dbRyhb7g6am9jCcZc8huF3AnADE+nA=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -561,7 +563,6 @@
 go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
-go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc=
 go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o=
@@ -1004,6 +1005,8 @@
 k8s.io/component-base v0.27.2/go.mod h1:5UPk7EjfgrfgRIuDBFtsEFAe4DAvP3U+M8RTzoSJkpo=
 k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
 k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
+k8s.io/kube-aggregator v0.27.2 h1:jfHoPip+qN/fn3OcrYs8/xMuVYvkJHKo0H0DYciqdns=
+k8s.io/kube-aggregator v0.27.2/go.mod h1:mwrTt4ESjQ7A6847biwohgZWn8P/KzSFHegEScbSGY4=
 k8s.io/kube-openapi v0.0.0-20230515203736-54b630e78af5 h1:azYPdzztXxPSa8wb+hksEKayiz0o+PPisO/d+QhWnoo=
 k8s.io/kube-openapi v0.0.0-20230515203736-54b630e78af5/go.mod h1:kzo02I3kQ4BTtEfVLaPbjvCkX97YqGve33wzlb3fofQ=
 k8s.io/kubectl v0.26.0 h1:xmrzoKR9CyNdzxBmXV7jW9Ln8WMrwRK6hGbbf69o4T0=
@@ -1017,6 +1020,8 @@
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/controller-runtime v0.15.0 h1:ML+5Adt3qZnMSYxZ7gAverBLNPSMQEibtzAgp0UPojU=
 sigs.k8s.io/controller-runtime v0.15.0/go.mod h1:7ngYvp1MLT+9GeZ+6lH3LOlcHkp/+tzA/fmHa4iq9kk=
+sigs.k8s.io/gateway-api v0.7.0 h1:/mG8yyJNBifqvuVLW5gwlI4CQs0NR/5q4BKUlf1bVdY=
+sigs.k8s.io/gateway-api v0.7.0/go.mod h1:Xv0+ZMxX0lu1nSSDIIPEfbVztgNZ+3cfiYrJsa2Ooso=
 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
 sigs.k8s.io/kustomize/api v0.12.1 h1:7YM7gW3kYBwtKvoY216ZzY+8hM+lV53LUayghNRJ0vM=
diff --git a/tests/e2e/resource_utils_test.go b/tests/e2e/resource_utils_test.go
index 75bdb83..a41770b 100644
--- a/tests/e2e/resource_utils_test.go
+++ b/tests/e2e/resource_utils_test.go
@@ -18,6 +18,7 @@
 package e2e
 
 import (
+	certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	zkApi "github.com/pravega/zookeeper-operator/api/v1beta1"
@@ -89,7 +90,7 @@
 		if additionalChecks != nil {
 			additionalChecks(g, foundSolrCloud)
 		}
-	}).WithContext(ctx).Should(Succeed())
+	}).WithTimeout(time.Minute * 4).WithContext(ctx).Should(Succeed())
 
 	return foundSolrCloud
 }
@@ -765,6 +766,8 @@
 		&solrv1beta1.SolrCloud{}, &solrv1beta1.SolrBackup{}, &solrv1beta1.SolrPrometheusExporter{},
 		&zkApi.ZookeeperCluster{},
 
+		&certmanagerv1.Certificate{}, &certmanagerv1.Issuer{},
+
 		// All dependent Kubernetes types, in order of dependence (deployment then replicaSet then pod)
 		&corev1.ConfigMap{}, &netv1.Ingress{},
 		&corev1.PersistentVolumeClaim{}, &corev1.PersistentVolume{},
diff --git a/tests/e2e/solrcloud_tls_test.go b/tests/e2e/solrcloud_tls_test.go
new file mode 100644
index 0000000..c814c25
--- /dev/null
+++ b/tests/e2e/solrcloud_tls_test.go
@@ -0,0 +1,579 @@
+/*
+ * 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 e2e
+
+import (
+	"context"
+	solrv1beta1 "github.com/apache/solr-operator/api/v1beta1"
+	certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
+	certmanagermetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/rand"
+	"k8s.io/utils/pointer"
+)
+
+const (
+	solrIssuerName = "solr-issuer"
+
+	secretTlsPasswordKey = "password"
+
+	clientAuthPasswordSecret = "client-auth-password"
+	clientAuthSecret         = "client-auth"
+)
+
+var _ = FDescribe("E2E - SolrCloud - TLS - Secrets", func() {
+	var (
+		solrCloud *solrv1beta1.SolrCloud
+
+		solrCollection = "e2e"
+	)
+
+	/*
+		Create a single SolrCloud that has TLS Enabled
+	*/
+	BeforeEach(func(ctx context.Context) {
+		installSolrIssuer(ctx, testNamespace())
+	})
+
+	/*
+		Start the SolrCloud and ensure that it is running
+	*/
+	JustBeforeEach(func(ctx context.Context) {
+		By("creating the SolrCloud")
+		Expect(k8sClient.Create(ctx, solrCloud)).To(Succeed())
+
+		DeferCleanup(func(ctx context.Context) {
+			cleanupTest(ctx, solrCloud)
+		})
+
+		By("waiting for the SolrCloud to come up healthy")
+		solrCloud = expectSolrCloudToBeReady(ctx, solrCloud)
+
+		By("creating a Solr Collection to query metrics for")
+		createAndQueryCollection(ctx, solrCloud, solrCollection, 1, 2)
+	})
+
+	FContext("No Client TLS", func() {
+
+		BeforeEach(func(ctx context.Context) {
+			solrCloud = generateBaseSolrCloudWithSecretTLS(ctx, 2, false)
+
+			//solrCloud.Spec.SolrOpts = "-Djavax.net.debug=SSL,keymanager,trustmanager,ssl:handshake"
+		})
+
+		FIt("Can run", func() {})
+	})
+
+	FContext("No Client TLS - Just a Keystore", func() {
+
+		BeforeEach(func(ctx context.Context) {
+			solrCloud = generateBaseSolrCloudWithSecretTLS(ctx, 2, false)
+
+			solrCloud.Spec.SolrTLS.TrustStoreSecret = nil
+			solrCloud.Spec.SolrTLS.TrustStorePasswordSecret = nil
+
+			//solrCloud.Spec.SolrOpts = "-Djavax.net.debug=SSL,keymanager,trustmanager,ssl:handshake"
+		})
+
+		FIt("Can run", func() {})
+	})
+
+	FContext("No Client TLS - VerifyClientHostname", func() {
+
+		BeforeEach(func(ctx context.Context) {
+			solrCloud = generateBaseSolrCloudWithSecretTLS(ctx, 2, false)
+
+			solrCloud.Spec.SolrTLS.VerifyClientHostname = true
+
+			solrCloud.Spec.SolrOpts = "-Djavax.net.debug=SSL,keymanager,trustmanager,ssl:handshake"
+		})
+
+		FIt("Can run", func() {})
+	})
+
+	FContext("With Client TLS - VerifyClientHostname", func() {
+
+		BeforeEach(func(ctx context.Context) {
+			solrCloud = generateBaseSolrCloudWithSecretTLS(ctx, 2, true)
+
+			solrCloud.Spec.SolrTLS.VerifyClientHostname = true
+
+			solrCloud.Spec.SolrOpts = "-Djavax.net.debug=SSL,keymanager,trustmanager,ssl:handshake"
+		})
+
+		FIt("Can run", func() {})
+	})
+
+	FContext("With Client TLS - CheckPeerName", func() {
+
+		BeforeEach(func(ctx context.Context) {
+			solrCloud = generateBaseSolrCloudWithSecretTLS(ctx, 2, true)
+
+			solrCloud.Spec.SolrTLS.CheckPeerName = true
+
+			//solrCloud.Spec.SolrOpts = "-Djavax.net.debug=SSL,keymanager,trustmanager,ssl:handshake"
+		})
+
+		FIt("Can run", func(ctx context.Context) {
+			By("Checking that using the wrong peer name fails")
+			response, err := callSolrApiInPod(
+				ctx,
+				solrCloud,
+				"get",
+				"/solr/admin/info/system",
+				nil,
+				"localhost",
+			)
+			Expect(err).To(HaveOccurred(), "Error should have occurred while calling Solr API - Bad hostname for TLS")
+			Expect(response).To(ContainSubstring("doesn't match any of the subject alternative names"), "Wrong error when calling Solr - Bad hostname for TLS expected")
+		})
+	})
+
+	FContext("With Client TLS - Client Auth Need", func() {
+
+		BeforeEach(func(ctx context.Context) {
+			solrCloud = generateBaseSolrCloudWithSecretTLS(ctx, 2, true)
+
+			solrCloud.Spec.SolrTLS.ClientAuth = solrv1beta1.Need
+
+			//solrCloud.Spec.SolrOpts = "-Djavax.net.debug=SSL,keymanager,trustmanager,ssl:handshake"
+		})
+
+		FIt("Can run", func() {})
+	})
+
+	FContext("With Client TLS - Client Auth Want", func() {
+
+		BeforeEach(func(ctx context.Context) {
+			solrCloud = generateBaseSolrCloudWithSecretTLS(ctx, 2, true)
+
+			solrCloud.Spec.SolrTLS.ClientAuth = solrv1beta1.Want
+
+			//solrCloud.Spec.SolrOpts = "-Djavax.net.debug=SSL,keymanager,trustmanager,ssl:handshake"
+		})
+
+		FIt("Can run", func() {})
+	})
+})
+
+var _ = FDescribe("E2E - SolrCloud - TLS - Mounted Dir", func() {
+	var (
+		solrCloud *solrv1beta1.SolrCloud
+
+		solrCollection = "e2e"
+	)
+
+	/*
+		Create a single SolrCloud that has TLS Enabled
+	*/
+	BeforeEach(func(ctx context.Context) {
+		installSolrIssuer(ctx, testNamespace())
+	})
+
+	/*
+		Start the SolrCloud and ensure that it is running
+	*/
+	JustBeforeEach(func(ctx context.Context) {
+		By("creating the SolrCloud")
+		Expect(k8sClient.Create(ctx, solrCloud)).To(Succeed())
+
+		DeferCleanup(func(ctx context.Context) {
+			cleanupTest(ctx, solrCloud)
+		})
+
+		By("waiting for the SolrCloud to come up healthy")
+		solrCloud = expectSolrCloudToBeReady(ctx, solrCloud)
+
+		By("creating a Solr Collection to query metrics for")
+		createAndQueryCollection(ctx, solrCloud, solrCollection, 1, 2)
+	})
+
+	FContext("ClientAuth - Want", func() {
+
+		BeforeEach(func(ctx context.Context) {
+			solrCloud = generateBaseSolrCloudWithCSITLS(1, false, false)
+
+			solrCloud.Spec.SolrTLS.ClientAuth = solrv1beta1.Want
+
+			//solrCloud.Spec.SolrOpts = "-Djavax.net.debug=SSL,keymanager,trustmanager,ssl:handshake"
+		})
+
+		FIt("Can run", func() {})
+	})
+
+	//FContext("ClientAuth - Need", func() {
+	//
+	//	BeforeEach(func(ctx context.Context) {
+	//		solrCloud = generateBaseSolrCloudWithCSITLS(1, false, true)
+	//
+	//		solrCloud.Spec.SolrTLS.ClientAuth = solrv1beta1.Need
+	//
+	//		//solrCloud.Spec.SolrOpts = "-Djavax.net.debug=SSL,keymanager,trustmanager,ssl:handshake"
+	//	})
+	//
+	//	FIt("Can run", func() {})
+	//})
+})
+
+func generateBaseSolrCloudWithSecretTLS(ctx context.Context, replicas int, includeClientTLS bool) (solrCloud *solrv1beta1.SolrCloud) {
+	solrCloud = generateBaseSolrCloud(replicas)
+
+	solrCertSecret, tlsPasswordSecret, clientCertSecret, clientTlsPasswordSecret := generateSolrCert(ctx, solrCloud, includeClientTLS)
+
+	solrCloud.Spec.SolrTLS = &solrv1beta1.SolrTLSOptions{
+		PKCS12Secret: &corev1.SecretKeySelector{
+			LocalObjectReference: corev1.LocalObjectReference{
+				Name: solrCertSecret,
+			},
+			Key: "keystore.p12",
+		},
+		KeyStorePasswordSecret: &corev1.SecretKeySelector{
+			LocalObjectReference: corev1.LocalObjectReference{
+				Name: tlsPasswordSecret,
+			},
+			Key: secretTlsPasswordKey,
+		},
+		TrustStoreSecret: &corev1.SecretKeySelector{
+			LocalObjectReference: corev1.LocalObjectReference{
+				Name: solrCertSecret,
+			},
+			Key: "truststore.p12",
+		},
+		TrustStorePasswordSecret: &corev1.SecretKeySelector{
+			LocalObjectReference: corev1.LocalObjectReference{
+				Name: tlsPasswordSecret,
+			},
+			Key: secretTlsPasswordKey,
+		},
+	}
+
+	if includeClientTLS {
+		solrCloud.Spec.SolrClientTLS = &solrv1beta1.SolrTLSOptions{
+			PKCS12Secret: &corev1.SecretKeySelector{
+				LocalObjectReference: corev1.LocalObjectReference{
+					Name: clientCertSecret,
+				},
+				Key: "keystore.p12",
+			},
+			KeyStorePasswordSecret: &corev1.SecretKeySelector{
+				LocalObjectReference: corev1.LocalObjectReference{
+					Name: clientTlsPasswordSecret,
+				},
+				Key: secretTlsPasswordKey,
+			},
+			TrustStoreSecret: &corev1.SecretKeySelector{
+				LocalObjectReference: corev1.LocalObjectReference{
+					Name: clientCertSecret,
+				},
+				Key: "truststore.p12",
+			},
+			TrustStorePasswordSecret: &corev1.SecretKeySelector{
+				LocalObjectReference: corev1.LocalObjectReference{
+					Name: clientTlsPasswordSecret,
+				},
+				Key: secretTlsPasswordKey,
+			},
+		}
+	}
+	return
+}
+
+func generateBaseSolrCloudWithCSITLS(replicas int, csiClientTLS bool, secretClientTLS bool) (solrCloud *solrv1beta1.SolrCloud) {
+	solrCloud = generateBaseSolrCloud(replicas)
+	solrCloud.Spec.CustomSolrKubeOptions.PodOptions.Volumes = []solrv1beta1.AdditionalVolume{
+		{
+			Name: "server-tls",
+			Source: corev1.VolumeSource{
+				CSI: &corev1.CSIVolumeSource{
+					Driver:   "csi.cert-manager.io",
+					ReadOnly: pointer.Bool(true),
+					VolumeAttributes: map[string]string{
+						"csi.cert-manager.io/issuer-name": solrIssuerName,
+						"csi.cert-manager.io/common-name": "${POD_NAME}." + solrCloud.Name + "-solrcloud-headless.${POD_NAMESPACE}",
+						"csi.cert-manager.io/dns-names": "${POD_NAME}." + solrCloud.Name + "-solrcloud-headless.${POD_NAMESPACE}.svc.cluster.local," +
+							solrCloud.Name + "-solrcloud-common.${POD_NAMESPACE}," +
+							solrCloud.Name + "-solrcloud-common.${POD_NAMESPACE}.svc.cluster.local," +
+							"${POD_NAME}," +
+							"${POD_NAME}.${POD_NAMESPACE}," +
+							"${POD_NAME}.${POD_NAMESPACE}.svc.cluster.local",
+						"csi.cert-manager.io/key-usages":      "server auth,digital signature",
+						"csi.cert-manager.io/pkcs12-enable":   "true",
+						"csi.cert-manager.io/pkcs12-password": "pass",
+						"csi.cert-manager.io/fs-group":        "8983",
+					},
+				},
+			},
+			DefaultContainerMount: &corev1.VolumeMount{
+				ReadOnly:  true,
+				MountPath: "/opt/server-tls",
+			},
+		},
+	}
+
+	solrCloud.Spec.SolrTLS = &solrv1beta1.SolrTLSOptions{
+		MountedTLSDir: &solrv1beta1.MountedTLSDirectory{
+			Path:             "/opt/server-tls",
+			KeystoreFile:     "keystore.p12",
+			KeystorePassword: "pass",
+		},
+	}
+
+	if csiClientTLS {
+		solrCloud.Spec.CustomSolrKubeOptions.PodOptions.Volumes = append(
+			solrCloud.Spec.CustomSolrKubeOptions.PodOptions.Volumes,
+			solrv1beta1.AdditionalVolume{
+				Name: "client-tls",
+				Source: corev1.VolumeSource{
+					CSI: &corev1.CSIVolumeSource{
+						Driver:   "csi.cert-manager.io",
+						ReadOnly: pointer.Bool(true),
+						VolumeAttributes: map[string]string{
+							"csi.cert-manager.io/issuer-name": solrIssuerName,
+							"csi.cert-manager.io/common-name": "${POD_NAME}." + solrCloud.Name + "-solrcloud-headless.${POD_NAMESPACE}",
+							"csi.cert-manager.io/dns-names": "${POD_NAME}." + solrCloud.Name + "-solrcloud-headless.${POD_NAMESPACE}.svc.cluster.local," +
+								"${POD_NAME}," +
+								"${POD_NAME}.${POD_NAMESPACE}.svc.cluster.local",
+							"csi.cert-manager.io/key-usages":      "client auth,digital signature",
+							"csi.cert-manager.io/pkcs12-enable":   "true",
+							"csi.cert-manager.io/pkcs12-password": "pass",
+							"csi.cert-manager.io/fs-group":        "8983",
+						},
+					},
+				},
+				DefaultContainerMount: &corev1.VolumeMount{
+					ReadOnly:  true,
+					MountPath: "/opt/client-tls",
+				},
+			})
+
+		solrCloud.Spec.SolrClientTLS = &solrv1beta1.SolrTLSOptions{
+			MountedTLSDir: &solrv1beta1.MountedTLSDirectory{
+				Path:             "/opt/client-tls",
+				KeystoreFile:     "keystore.p12",
+				KeystorePassword: "pass",
+			},
+		}
+	} else if secretClientTLS {
+		// TODO: It is not currently supported to mix secret and mountedDir TLS.
+		// This will not work until that support is added.
+		solrCloud.Spec.SolrClientTLS = &solrv1beta1.SolrTLSOptions{
+			PKCS12Secret: &corev1.SecretKeySelector{
+				LocalObjectReference: corev1.LocalObjectReference{
+					Name: clientAuthSecret,
+				},
+				Key: "keystore.p12",
+			},
+			TrustStoreSecret: &corev1.SecretKeySelector{
+				LocalObjectReference: corev1.LocalObjectReference{
+					Name: clientAuthSecret,
+				},
+				Key: "truststore.p12",
+			},
+			TrustStorePasswordSecret: &corev1.SecretKeySelector{
+				LocalObjectReference: corev1.LocalObjectReference{
+					Name: clientAuthPasswordSecret,
+				},
+				Key: "password",
+			},
+		}
+	}
+	return
+}
+
+func installBootstrapIssuer(ctx context.Context) {
+	bootstrapIssuer := &certmanagerv1.ClusterIssuer{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "bootstrap-issuer",
+		},
+		Spec: certmanagerv1.IssuerSpec{
+			IssuerConfig: certmanagerv1.IssuerConfig{
+				SelfSigned: &certmanagerv1.SelfSignedIssuer{},
+			},
+		},
+	}
+	Expect(k8sClient.Create(ctx, bootstrapIssuer)).To(Succeed(), "Failed to install SelfSigned ClusterIssuer for bootstrapping CA")
+	DeferCleanup(func(ctx context.Context) {
+		Expect(k8sClient.Delete(ctx, bootstrapIssuer)).To(Succeed(), "Failed to delete SelfSigned bootstrapping ClusterIssuer")
+	})
+}
+
+func installSolrIssuer(ctx context.Context, namespace string) {
+	secretName := "solr-ca-key-pair"
+	clusterCA := &certmanagerv1.Certificate{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "solr-ca",
+			Namespace: namespace,
+		},
+		Spec: certmanagerv1.CertificateSpec{
+			IsCA:       true,
+			CommonName: "solr-ca",
+			SecretName: secretName,
+			PrivateKey: &certmanagerv1.CertificatePrivateKey{
+				RotationPolicy: certmanagerv1.RotationPolicyNever,
+				Algorithm:      "RSA",
+			},
+			IssuerRef: certmanagermetav1.ObjectReference{
+				Name:  "bootstrap-issuer",
+				Kind:  "ClusterIssuer",
+				Group: "cert-manager.io",
+			},
+		},
+	}
+	Expect(k8sClient.Create(ctx, clusterCA)).To(Succeed(), "Failed to install Solr CA for tests")
+
+	namespaceIssuer := &certmanagerv1.Issuer{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      solrIssuerName,
+			Namespace: namespace,
+		},
+		Spec: certmanagerv1.IssuerSpec{
+			IssuerConfig: certmanagerv1.IssuerConfig{
+				CA: &certmanagerv1.CAIssuer{
+					SecretName: secretName,
+				},
+			},
+		},
+	}
+	Expect(k8sClient.Create(ctx, namespaceIssuer)).To(Succeed(), "Failed to install CA Issuer for issuing test certs in namespace "+namespace)
+
+	expectSecret(ctx, clusterCA, secretName)
+}
+
+func generateSolrCert(ctx context.Context, solrCloud *solrv1beta1.SolrCloud, includeClientTLS bool) (certSecretName string, tlsPasswordSecretName string, clientTLSCertSecretName string, clientTLSPasswordSecretName string) {
+	// First create a secret to use as a password for the keystore/truststore
+	tlsPasswordSecret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      solrCloud.Name + "-keystore-password",
+			Namespace: solrCloud.Namespace,
+		},
+		StringData: map[string]string{
+			secretTlsPasswordKey: rand.String(10),
+		},
+		Type: corev1.SecretTypeOpaque,
+	}
+	Expect(k8sClient.Create(ctx, tlsPasswordSecret)).To(Succeed(), "Failed to create secret for tls password in namespace "+solrCloud.Namespace)
+
+	expectSecret(ctx, solrCloud, tlsPasswordSecret.Name)
+	tlsPasswordSecretName = tlsPasswordSecret.Name
+
+	allDNSNames := make([]string, *solrCloud.Spec.Replicas*2+1)
+	for _, pod := range solrCloud.GetAllSolrPodNames() {
+		allDNSNames = append(allDNSNames, pod, solrCloud.InternalNodeUrl(pod, false))
+	}
+
+	certSecretName = solrCloud.Name + "-secret-auth"
+
+	solrCert := &certmanagerv1.Certificate{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      solrCloud.Name + "-secret-auth",
+			Namespace: solrCloud.Namespace,
+		},
+		Spec: certmanagerv1.CertificateSpec{
+			CommonName: solrCloud.InternalCommonUrl(false),
+			DNSNames:   allDNSNames,
+			SecretName: certSecretName,
+			Keystores: &certmanagerv1.CertificateKeystores{
+				PKCS12: &certmanagerv1.PKCS12Keystore{
+					Create: true,
+					PasswordSecretRef: certmanagermetav1.SecretKeySelector{
+						LocalObjectReference: certmanagermetav1.LocalObjectReference{
+							Name: tlsPasswordSecret.Name,
+						},
+						Key: secretTlsPasswordKey,
+					},
+				},
+			},
+			IssuerRef: certmanagermetav1.ObjectReference{
+				Name:  solrIssuerName,
+				Kind:  "Issuer",
+				Group: "cert-manager.io",
+			},
+			IsCA:   false,
+			Usages: []certmanagerv1.KeyUsage{certmanagerv1.UsageServerAuth, certmanagerv1.UsageDigitalSignature},
+			PrivateKey: &certmanagerv1.CertificatePrivateKey{
+				RotationPolicy: certmanagerv1.RotationPolicyNever,
+				Algorithm:      "RSA",
+			},
+		},
+	}
+	Expect(k8sClient.Create(ctx, solrCert)).To(Succeed(), "Failed to install Solr secret cert for tests")
+
+	expectSecret(ctx, solrCert, certSecretName)
+
+	if includeClientTLS {
+		// First create a secret to use as a password for the keystore/truststore
+		clientTlsPasswordSecret := &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      solrCloud.Name + "-client-tls-password",
+				Namespace: solrCloud.Namespace,
+			},
+			StringData: map[string]string{
+				secretTlsPasswordKey: rand.String(10),
+			},
+			Type: corev1.SecretTypeOpaque,
+		}
+		Expect(k8sClient.Create(ctx, clientTlsPasswordSecret)).To(Succeed(), "Failed to create secret for client tls password in namespace "+solrCloud.Namespace)
+
+		expectSecret(ctx, solrCloud, clientTlsPasswordSecret.Name)
+		clientTLSPasswordSecretName = clientTlsPasswordSecret.Name
+
+		clientTLSCertSecretName = solrCloud.Name + "-client-tls-secret-auth"
+
+		solrClientCert := &certmanagerv1.Certificate{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      solrCloud.Name + "-client-secret-auth",
+				Namespace: solrCloud.Namespace,
+			},
+			Spec: certmanagerv1.CertificateSpec{
+				CommonName: solrCloud.InternalCommonUrl(false),
+				DNSNames:   allDNSNames,
+				SecretName: clientTLSCertSecretName,
+				Keystores: &certmanagerv1.CertificateKeystores{
+					PKCS12: &certmanagerv1.PKCS12Keystore{
+						Create: true,
+						PasswordSecretRef: certmanagermetav1.SecretKeySelector{
+							LocalObjectReference: certmanagermetav1.LocalObjectReference{
+								Name: clientTlsPasswordSecret.Name,
+							},
+							Key: secretTlsPasswordKey,
+						},
+					},
+				},
+				IssuerRef: certmanagermetav1.ObjectReference{
+					Name:  solrIssuerName,
+					Kind:  "Issuer",
+					Group: "cert-manager.io",
+				},
+				IsCA:   false,
+				Usages: []certmanagerv1.KeyUsage{certmanagerv1.UsageClientAuth, certmanagerv1.UsageDigitalSignature},
+				PrivateKey: &certmanagerv1.CertificatePrivateKey{
+					RotationPolicy: certmanagerv1.RotationPolicyNever,
+					Algorithm:      "RSA",
+				},
+			},
+		}
+		Expect(k8sClient.Create(ctx, solrClientCert)).To(Succeed(), "Failed to install Solr clientTLS secret cert for tests")
+
+		expectSecret(ctx, solrClientCert, clientTLSCertSecretName)
+	}
+
+	return
+}
diff --git a/tests/e2e/suite_test.go b/tests/e2e/suite_test.go
index 90bbf94..d6060a3 100644
--- a/tests/e2e/suite_test.go
+++ b/tests/e2e/suite_test.go
@@ -21,9 +21,11 @@
 	"bufio"
 	"bytes"
 	"context"
+	"encoding/json"
 	"fmt"
 	solrv1beta1 "github.com/apache/solr-operator/api/v1beta1"
 	"github.com/apache/solr-operator/version"
+	certManagerApi "github.com/cert-manager/cert-manager/pkg/api"
 	"github.com/go-logr/logr"
 	"github.com/onsi/ginkgo/v2/types"
 	zkApi "github.com/pravega/zookeeper-operator/api/v1beta1"
@@ -104,6 +106,7 @@
 	k8sConfig, err = config.GetConfig()
 	Expect(err).NotTo(HaveOccurred(), "Could not load in default kubernetes config")
 	Expect(zkApi.AddToScheme(scheme.Scheme)).To(Succeed())
+	Expect(certManagerApi.AddToScheme(scheme.Scheme)).To(Succeed())
 	k8sClient, err = client.New(k8sConfig, client.Options{Scheme: scheme.Scheme})
 	Expect(err).NotTo(HaveOccurred(), "Could not create controllerRuntime Kubernetes client")
 
@@ -113,6 +116,10 @@
 	By("creating a shared zookeeper cluster")
 	zookeeper := runSharedZookeeperCluster(ctx)
 
+	// Set up a shared Bootstrap issuer for creating CAs for Solr
+	By("creating a boostrap cert issuer")
+	installBootstrapIssuer(ctx)
+
 	// Run this once before all tests, not per-test-process
 	By("starting the test solr operator")
 	solrOperatorRelease := runSolrOperator(ctx)
@@ -144,6 +151,7 @@
 	By("setting up the k8s clients")
 	Expect(solrv1beta1.AddToScheme(scheme.Scheme)).To(Succeed())
 	Expect(zkApi.AddToScheme(scheme.Scheme)).To(Succeed())
+	Expect(certManagerApi.AddToScheme(scheme.Scheme)).To(Succeed())
 
 	k8sClient, err = client.New(k8sConfig, client.Options{Scheme: scheme.Scheme})
 	Expect(err).NotTo(HaveOccurred(), "Could not create controllerRuntime Kubernetes client")
@@ -204,7 +212,7 @@
 	return outputDir + "/" + strings.ReplaceAll(strings.ReplaceAll(testName, "  ", "-"), " ", "")
 }
 
-var _ = JustAfterEach(func() {
+var _ = JustAfterEach(func(ctx context.Context) {
 	testOutputDir := outputDirForTest(CurrentSpecReport().FullText())
 
 	// We count "ran" as "passed" or "failed"
@@ -214,14 +222,16 @@
 		// Always save the logs of the Solr Operator for the test
 		startTime := CurrentSpecReport().StartTime
 		writePodLogsToFile(
+			ctx,
 			testOutputDir+"solr-operator.log",
-			getSolrOperatorPodName(solrOperatorReleaseNamespace),
+			getSolrOperatorPodName(ctx, solrOperatorReleaseNamespace),
 			solrOperatorReleaseNamespace,
 			&startTime,
 			fmt.Sprintf("%q: %q", "namespace", testNamespace()),
 		)
 		// Always save the logs of the Solr Operator for the test
-		writeAllSolrLogsToFiles(
+		writeAllSolrInfoToFiles(
+			ctx,
 			testOutputDir,
 			testNamespace(),
 		)
@@ -255,7 +265,7 @@
 	}
 })
 
-func getSolrOperatorPodName(namespace string) string {
+func getSolrOperatorPodName(ctx context.Context, namespace string) string {
 	labelSelector := labels.SelectorFromSet(map[string]string{"control-plane": "solr-operator"})
 	listOps := &client.ListOptions{
 		Namespace:     namespace,
@@ -264,13 +274,13 @@
 	}
 
 	foundPods := &corev1.PodList{}
-	Expect(k8sClient.List(context.TODO(), foundPods, listOps)).To(Succeed(), "Could not fetch Solr Operator pod")
+	Expect(k8sClient.List(ctx, foundPods, listOps)).To(Succeed(), "Could not fetch Solr Operator pod")
 	Expect(foundPods).ToNot(BeNil(), "No Solr Operator pods could be found")
 	Expect(foundPods.Items).ToNot(BeEmpty(), "No Solr Operator pods could be found")
 	return foundPods.Items[0].Name
 }
 
-func writeAllSolrLogsToFiles(directory string, namespace string) {
+func writeAllSolrInfoToFiles(ctx context.Context, directory string, namespace string) {
 	req, err := labels.NewRequirement("technology", selection.In, []string{solrv1beta1.SolrTechnologyLabel, solrv1beta1.SolrPrometheusExporterTechnologyLabel})
 	Expect(err).ToNot(HaveOccurred())
 
@@ -281,20 +291,55 @@
 	}
 
 	foundPods := &corev1.PodList{}
-	Expect(k8sClient.List(context.TODO(), foundPods, listOps)).To(Succeed(), "Could not fetch Solr Operator pod")
+	Expect(k8sClient.List(ctx, foundPods, listOps)).To(Succeed(), "Could not fetch Solr Operator pod")
 	Expect(foundPods).ToNot(BeNil(), "No Solr pods could be found")
 	for _, pod := range foundPods.Items {
-		writePodLogsToFile(
-			directory+pod.Name+".log",
-			pod.Name,
-			namespace,
-			nil,
-			"",
+		writeAllPodInfoToFiles(
+			ctx,
+			directory+pod.Name,
+			&pod,
 		)
 	}
 }
 
-func writePodLogsToFile(filename string, podName string, podNamespace string, startTimeRaw *time.Time, filterLinesWithString string) {
+// writeAllPodInfoToFile writes the following each to a separate file with the given base name & directory.
+//   - Pod Spec/Status
+//   - Pod Events
+//   - Pod logs
+func writeAllPodInfoToFiles(ctx context.Context, baseFilename string, pod *corev1.Pod) {
+	// Write pod to a file
+	statusFile, err := os.Create(baseFilename + ".status.json")
+	defer statusFile.Close()
+	Expect(err).ToNot(HaveOccurred(), "Could not open file to save pod status: %s", baseFilename+".status.json")
+	jsonBytes, marshErr := json.MarshalIndent(pod, "", "\t")
+	Expect(marshErr).ToNot(HaveOccurred(), "Could not serialize pod json")
+	_, writeErr := statusFile.Write(jsonBytes)
+	Expect(writeErr).ToNot(HaveOccurred(), "Could not write pod json to file")
+
+	// Write events for pod to a file
+	eventsFile, err := os.Create(baseFilename + ".events.json")
+	defer eventsFile.Close()
+	Expect(err).ToNot(HaveOccurred(), "Could not open file to save status: %s", baseFilename+".events.yaml")
+
+	eventList, err := rawK8sClient.CoreV1().Events(pod.Namespace).Search(scheme.Scheme, pod)
+	Expect(err).ToNot(HaveOccurred(), "Could not find events for pod: %s", pod.Name)
+	jsonBytes, marshErr = json.MarshalIndent(eventList, "", "\t")
+	Expect(marshErr).ToNot(HaveOccurred(), "Could not serialize events json")
+	_, writeErr = eventsFile.Write(jsonBytes)
+	Expect(writeErr).ToNot(HaveOccurred(), "Could not write events json to file")
+
+	// Write pod logs to a file
+	writePodLogsToFile(
+		ctx,
+		baseFilename+".log",
+		pod.Name,
+		pod.Namespace,
+		nil,
+		"",
+	)
+}
+
+func writePodLogsToFile(ctx context.Context, filename string, podName string, podNamespace string, startTimeRaw *time.Time, filterLinesWithString string) {
 	logFile, err := os.Create(filename)
 	defer logFile.Close()
 	Expect(err).ToNot(HaveOccurred(), "Could not open file to save logs: %s", filename)
@@ -306,7 +351,7 @@
 	}
 
 	req := rawK8sClient.CoreV1().Pods(podNamespace).GetLogs(podName, &podLogOpts)
-	podLogs, logsErr := req.Stream(context.Background())
+	podLogs, logsErr := req.Stream(ctx)
 	defer podLogs.Close()
 	Expect(logsErr).ToNot(HaveOccurred(), "Could not open stream to fetch pod logs. namespace: %s, pod: %s", podNamespace, podName)
 
diff --git a/tests/e2e/test_utils_test.go b/tests/e2e/test_utils_test.go
index 85e08a0..ca5b01c 100644
--- a/tests/e2e/test_utils_test.go
+++ b/tests/e2e/test_utils_test.go
@@ -44,6 +44,7 @@
 	"k8s.io/utils/pointer"
 	"os"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"strconv"
 	"strings"
 	"time"
 )
@@ -211,55 +212,52 @@
 func createAndQueryCollectionWithGomega(ctx context.Context, solrCloud *solrv1beta1.SolrCloud, collection string, shards int, replicasPerShard int, g Gomega, additionalOffset int, nodes ...int) {
 	asyncId := fmt.Sprintf("create-collection-%s-%d-%d", collection, shards, replicasPerShard)
 
-	var nodeSet []string
-	for _, node := range nodes {
-		nodeSet = append(nodeSet, util.SolrNodeName(solrCloud, solrCloud.GetSolrPodName(node)))
+	createParams := map[string]string{
+		"action":            "CREATE",
+		"name":              collection,
+		"replicationFactor": strconv.Itoa(replicasPerShard),
+		"numShards":         strconv.Itoa(shards),
+		"maxShardsPerNode":  "10",
+		"async":             asyncId,
+		"wt":                "json",
 	}
-	createNodeSet := ""
-	if len(nodeSet) > 0 {
-		createNodeSet = "&createNodeSet=" + strings.Join(nodeSet, ",")
+
+	if len(nodes) > 0 {
+		var nodeSet []string
+		for _, node := range nodes {
+			nodeSet = append(nodeSet, util.SolrNodeName(solrCloud, solrCloud.GetSolrPodName(node)))
+		}
+		createParams["createNodeSet"] = strings.Join(nodeSet, ",")
 	}
 
 	additionalOffset += 1
 	g.EventuallyWithOffset(additionalOffset, func(innerG Gomega) {
-		response, err := runExecForContainer(
+		response, err := callSolrApiInPod(
 			ctx,
-			util.SolrNodeContainer,
-			solrCloud.GetRandomSolrPodName(),
-			solrCloud.Namespace,
-			[]string{
-				"curl",
-				fmt.Sprintf(
-					"http://localhost:%d/solr/admin/collections?action=CREATE&name=%s&replicationFactor=%d&numShards=%d%s&async=%s&maxShardsPerNode=10",
-					solrCloud.Spec.SolrAddressability.PodPort,
-					collection,
-					replicasPerShard,
-					shards,
-					createNodeSet,
-					asyncId),
-			},
+			solrCloud,
+			"get",
+			"/solr/admin/collections",
+			createParams,
 		)
 		innerG.Expect(err).ToNot(HaveOccurred(), "Error occurred while starting async command to create Solr Collection")
 		innerG.Expect(response).To(ContainSubstring("\"status\":0"), "Error occurred while starting async command to create Solr Collection")
-	}, time.Second*5).WithContext(ctx).Should(Succeed(), "Collection creation command start was not successful")
+	}).Within(time.Second*10).WithContext(ctx).Should(Succeed(), "Collection creation command start was not successful")
 	// Only wait 5 seconds when trying to create the asyncCommand
 
 	g.EventuallyWithOffset(additionalOffset, func(innerG Gomega) {
-		response, err := runExecForContainer(
+		response, err := callSolrApiInPod(
 			ctx,
-			util.SolrNodeContainer,
-			solrCloud.GetRandomSolrPodName(),
-			solrCloud.Namespace,
-			[]string{
-				"curl",
-				fmt.Sprintf(
-					"http://localhost:%d/solr/admin/collections?action=REQUESTSTATUS&requestid=%s",
-					solrCloud.Spec.SolrAddressability.PodPort,
-					asyncId),
+			solrCloud,
+			"get",
+			"/solr/admin/collections",
+			map[string]string{
+				"action":    "REQUESTSTATUS",
+				"requestid": asyncId,
+				"wt":        "json",
 			},
 		)
 		innerG.Expect(err).ToNot(HaveOccurred(), "Error occurred while checking if Solr Collection creation command was successful")
-		if strings.Contains(response, "\"state\":\"failed\"") || strings.Contains(response, "\"state\":\"notfound\"") {
+		if strings.Contains(response, "failed") || strings.Contains(response, "notfound") {
 			StopTrying("A failure occurred while creating the Solr Collection").
 				Attach("Collection", collection).
 				Attach("Shards", shards).
@@ -269,25 +267,23 @@
 		}
 		innerG.Expect(response).To(ContainSubstring("\"status\":0"), "A failure occurred while creating the Solr Collection")
 		innerG.Expect(response).To(ContainSubstring("\"state\":\"completed\""), "Did not finish creating Solr Collection in time")
-	}).WithContext(ctx).Should(Succeed(), "Collection creation was not successful")
+	}).Within(time.Second*40).WithContext(ctx).Should(Succeed(), "Collection creation was not successful")
 
 	g.EventuallyWithOffset(additionalOffset, func(innerG Gomega) {
-		response, err := runExecForContainer(
+		response, err := callSolrApiInPod(
 			ctx,
-			util.SolrNodeContainer,
-			solrCloud.GetRandomSolrPodName(),
-			solrCloud.Namespace,
-			[]string{
-				"curl",
-				fmt.Sprintf(
-					"http://localhost:%d/solr/admin/collections?action=DELETESTATUS&requestid=%s",
-					solrCloud.Spec.SolrAddressability.PodPort,
-					asyncId),
+			solrCloud,
+			"get",
+			"/solr/admin/collections",
+			map[string]string{
+				"action":    "DELETESTATUS",
+				"requestid": asyncId,
+				"wt":        "json",
 			},
 		)
 		innerG.Expect(err).ToNot(HaveOccurred(), "Error occurred while deleting Solr CollectionsAPI AsyncID")
 		innerG.Expect(response).To(ContainSubstring("\"status\":0"), "Error occurred while deleting Solr CollectionsAPI AsyncID")
-	}, time.Second*5).WithContext(ctx).Should(Succeed(), "Could not delete aysncId after collection creation")
+	}).Within(time.Second*10).WithContext(ctx).Should(Succeed(), "Could not delete aysncId after collection creation")
 	// Only wait 5 seconds when trying to delete the async requestId
 
 	queryCollectionWithGomega(ctx, solrCloud, collection, 0, g, additionalOffset)
@@ -299,31 +295,31 @@
 
 func queryCollectionWithGomega(ctx context.Context, solrCloud *solrv1beta1.SolrCloud, collection string, docCount int, g Gomega, additionalOffset ...int) {
 	g.EventuallyWithOffset(resolveOffset(additionalOffset), func(innerG Gomega) {
-		response, err := runExecForContainer(
+		response, err := callSolrApiInPod(
 			ctx,
-			util.SolrNodeContainer,
-			solrCloud.GetRandomSolrPodName(),
-			solrCloud.Namespace,
-			[]string{
-				"curl",
-				fmt.Sprintf("http://localhost:%d/solr/%s/select?rows=0", solrCloud.Spec.SolrAddressability.PodPort, collection),
+			solrCloud,
+			"get",
+			fmt.Sprintf("/solr/%s/select", collection),
+			map[string]string{
+				"rows": "0",
+				"wt":   "json",
 			},
 		)
 		innerG.Expect(err).ToNot(HaveOccurred(), "Error occurred while querying empty Solr Collection")
 		innerG.Expect(response).To(ContainSubstring("\"numFound\":%d", docCount), "Error occurred while querying Solr Collection '%s'", collection)
-	}, time.Second*5).WithContext(ctx).Should(Succeed(), "Could not successfully query collection: %v", fetchClusterStatus(ctx, solrCloud))
+	}).Within(time.Second*5).WithContext(ctx).Should(Succeed(), "Could not successfully query collection: %v", fetchClusterStatus(ctx, solrCloud))
 	// Only wait 5 seconds for the collection to be query-able
 }
 
 func fetchClusterStatus(ctx context.Context, solrCloud *solrv1beta1.SolrCloud) string {
-	response, err := runExecForContainer(
+	response, err := callSolrApiInPod(
 		ctx,
-		util.SolrNodeContainer,
-		solrCloud.GetRandomSolrPodName(),
-		solrCloud.Namespace,
-		[]string{
-			"curl",
-			fmt.Sprintf("http://localhost:%d/solr/admin/collections?action=CLUSTERSTATUS", solrCloud.Spec.SolrAddressability.PodPort),
+		solrCloud,
+		"get",
+		"/solr/admin/collections",
+		map[string]string{
+			"action": "CLUSTERSTATUS",
+			"wt":     "json",
 		},
 	)
 	Expect(err).ToNot(HaveOccurred(), "Could not fetch clusterStatus for cloud")
@@ -333,19 +329,21 @@
 
 func queryCollectionWithNoReplicaAvailable(ctx context.Context, solrCloud *solrv1beta1.SolrCloud, collection string, additionalOffset ...int) {
 	EventuallyWithOffset(resolveOffset(additionalOffset), func(innerG Gomega) {
-		response, err := runExecForContainer(
+		response, _ := callSolrApiInPod(
 			ctx,
-			util.SolrNodeContainer,
-			solrCloud.GetRandomSolrPodName(),
-			solrCloud.Namespace,
-			[]string{
-				"curl",
-				fmt.Sprintf("http://localhost:%d/solr/%s/select", solrCloud.Spec.SolrAddressability.PodPort, collection),
+			solrCloud,
+			"get",
+			fmt.Sprintf("/solr/%s/select", collection),
+			map[string]string{
+				"rows": "0",
+				"wt":   "json",
 			},
 		)
-		innerG.Expect(err).ToNot(HaveOccurred(), "Error occurred while querying empty Solr Collection")
-		innerG.Expect(response).To(ContainSubstring("Error trying to proxy request for url"), "Wrong occurred while querying Solr Collection '%s', expected a proxy forwarding error", collection)
-	}, time.Second*5).WithContext(ctx).Should(Succeed(), "Collection query did not fail in the correct way")
+		innerG.Expect(response).To(
+			// "Exception in thread "main" is for 8.11, which does not handle the exception correctly
+			Or(ContainSubstring("Error trying to proxy request for url"), ContainSubstring("Exception in thread \"main\" java.lang.NullPointerException")),
+			"Wrong occurred while querying Solr Collection '%s', expected a proxy forwarding error", collection)
+	}).Within(time.Second*5).WithContext(ctx).Should(Succeed(), "Collection query did not fail in the correct way")
 }
 
 func getPrometheusExporterPod(ctx context.Context, solrPrometheusExporter *solrv1beta1.SolrPrometheusExporter) (podName string) {
@@ -414,25 +412,24 @@
 		g.Expect(solrCloud.Spec.BackupRepositories).To(Not(BeEmpty()), "Solr BackupRepository list cannot be empty in backup test")
 	}
 	for _, collection := range solrBackup.Spec.Collections {
-		curlCommand := fmt.Sprintf(
-			"http://localhost:%d/solr/admin/collections?action=LISTBACKUP&name=%s&repository=%s&collection=%s&location=%s",
-			solrCloud.Spec.SolrAddressability.PodPort,
-			util.FullCollectionBackupName(collection, solrBackup.Name),
-			repositoryName,
-			collection,
-			util.BackupLocationPath(repository, solrBackup.Spec.Location))
+		backupParams := map[string]string{
+			"action":     "LISTBACKUP",
+			"name":       util.FullCollectionBackupName(collection, solrBackup.Name),
+			"repository": repositoryName,
+			"collection": collection,
+			"location":   util.BackupLocationPath(repository, solrBackup.Spec.Location),
+			"wt":         "json",
+		}
+
 		g.Eventually(func(innerG Gomega) {
-			response, err := runExecForContainer(
+			response, err := callSolrApiInPod(
 				ctx,
-				util.SolrNodeContainer,
-				solrCloud.GetRandomSolrPodName(),
-				solrCloud.Namespace,
-				[]string{
-					"curl",
-					curlCommand,
-				},
+				solrCloud,
+				"get",
+				"/solr/admin/collections",
+				backupParams,
 			)
-			innerG.Expect(err).ToNot(HaveOccurred(), "Error occurred while fetching backup '%s' for collection '%s': %s", solrBackup.Name, collection, curlCommand)
+			innerG.Expect(err).ToNot(HaveOccurred(), "Error occurred while fetching backup '%s' for collection '%s': %s", solrBackup.Name, collection, backupParams)
 			backupListResponse := &solr_api.SolrBackupListResponse{}
 
 			innerG.Expect(json.Unmarshal([]byte(response), &backupListResponse)).To(Succeed(), "Could not parse json from Solr BackupList API")
@@ -443,6 +440,49 @@
 	}
 }
 
+type ExecError struct {
+	Command string
+
+	Err error
+
+	ErrorOutput string
+
+	ResponseOutput string
+}
+
+func (r *ExecError) Error() string {
+	return fmt.Sprintf("Error from Pod Exec: %v\n\nError output from Pod Exec: %sResponse output from Pod Exec: %s", r.Err, r.ErrorOutput, r.ResponseOutput)
+}
+
+func callSolrApiInPod(ctx context.Context, solrCloud *solrv1beta1.SolrCloud, httpMethod string, apiPath string, queryParams map[string]string, hostnameOptional ...string) (response string, err error) {
+	hostname := "${POD_HOSTNAME}"
+	if len(hostnameOptional) > 0 {
+		hostname = hostnameOptional[0]
+	}
+	var queryParamsSlice []string
+	for param, val := range queryParams {
+		queryParamsSlice = append(queryParamsSlice, param+"="+val)
+	}
+	queryParamsString := strings.Join(queryParamsSlice, "&")
+	if len(queryParamsString) > 0 {
+		queryParamsString = "?" + queryParamsString
+	}
+
+	command := []string{
+		"solr",
+		"api",
+		"-" + strings.ToLower(httpMethod),
+		fmt.Sprintf(
+			"\"%s://%s:%d%s%s\"",
+			solrCloud.UrlScheme(false),
+			hostname,
+			solrCloud.Spec.SolrAddressability.PodPort,
+			apiPath,
+			queryParamsString),
+	}
+	return runExecForContainer(ctx, util.SolrNodeContainer, solrCloud.GetRandomSolrPodName(), solrCloud.Namespace, command)
+}
+
 func runExecForContainer(ctx context.Context, container string, podName string, namespace string, command []string) (response string, err error) {
 	req := rawK8sClient.CoreV1().RESTClient().Post().
 		Resource("pods").
@@ -456,7 +496,7 @@
 
 	parameterCodec := runtime.NewParameterCodec(scheme)
 	req.VersionedParams(&corev1.PodExecOptions{
-		Command:   command,
+		Command:   []string{"sh", "-c", strings.Join(command, " ")},
 		Container: container,
 		Stdin:     false,
 		Stdout:    true,
@@ -477,11 +517,24 @@
 		Tty:    false,
 	})
 
+	responseOutput := stdout.String()
+	errOutput := stderr.String()
+
 	if err != nil {
-		return "", fmt.Errorf("error in Stream: %v", err)
+		err = &ExecError{
+			Command:        strings.Join(command, " "),
+			Err:            err,
+			ResponseOutput: responseOutput,
+			ErrorOutput:    errOutput,
+		}
+	}
+	if len(responseOutput) == 0 {
+		response = errOutput
+	} else {
+		response = responseOutput
 	}
 
-	return stdout.String(), err
+	return response, err
 }
 
 func generateBaseSolrCloud(replicas int) *solrv1beta1.SolrCloud {
diff --git a/tests/scripts/manage_e2e_tests.sh b/tests/scripts/manage_e2e_tests.sh
index 27ca4f2..d6a3b5e 100755
--- a/tests/scripts/manage_e2e_tests.sh
+++ b/tests/scripts/manage_e2e_tests.sh
@@ -35,7 +35,7 @@
     -h  Display this help and exit
     -i  Solr Operator docker image to use (Optional, defaults to apache/solr-operator:<version>)
     -k  Kubernetes Version to test with (full tag, e.g. v1.24.16) (Optional, defaults to a compatible version)
-    -s  Full solr image, or image tag (for the official Solr image), to test with (e.g. apache/solr-nightly:9.0.0, 8.11). (Optional, defaults to a compatible version)
+    -s  Full solr image, or image tag (for the official Solr image), to test with (e.g. apache/solr-nightly:9.4.0, 8.11). (Optional, defaults to a compatible version)
     -a  Load additional local images into the test Kubernetes cluster. Provide option multiple times for multiple images. (Optional)
 EOF
 }
@@ -96,6 +96,9 @@
 export REUSE_KIND_CLUSTER_IF_EXISTS="${REUSE_KIND_CLUSTER_IF_EXISTS:-true}" # This is used for all start_cluster calls
 export LEAVE_KIND_CLUSTER_ON_SUCCESS="${LEAVE_KIND_CLUSTER_ON_SUCCESS:-false}" # This is only used when using run_tests or run_with_cluster
 
+export CERT_MANAGER_VERSION=1.12.3
+export CERT_MANAGER_CSI_DRIVER_VERSION=0.5.0
+
 function add_image_to_kind_repo_if_local() {
   IMAGE="$1"
   PULL_IF_NOT_LOCAL="$2"
@@ -179,6 +182,12 @@
   kubectl create -f "${REPO_DIR}/config/crd/bases/" 2>/dev/null || kubectl replace -f "${REPO_DIR}/config/crd/bases/"
   kubectl create -f "${REPO_DIR}/config/dependencies/" 2>/dev/null || kubectl replace -f "${REPO_DIR}/config/dependencies/"
   echo ""
+
+  printf "Installing Cert Manager\n"
+  helm repo add cert-manager https://charts.jetstack.io --force-update
+  helm upgrade -i -n cert-manager --create-namespace  cert-manager cert-manager/cert-manager --version "${CERT_MANAGER_VERSION}" --set installCRDs=true
+  helm upgrade -i -n cert-manager cert-manager-csi-driver cert-manager/cert-manager-csi-driver --version "${CERT_MANAGER_CSI_DRIVER_VERSION}"
+  echo ""
 }
 
 case "$ACTION" in